1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -55,6 +55,7 @@ declare module 'vue' {
|
||||
Ring: typeof import('./src/components/Ring.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Setting: typeof import('./src/components/Setting.vue')['default']
|
||||
SettingDialog: typeof import('./src/components/dialog/SettingDialog.vue')['default']
|
||||
Slide: typeof import('./src/components/Slide.vue')['default']
|
||||
Toolbar: typeof import('./src/components/toolbar/index.vue')['default']
|
||||
|
||||
27
src/App.vue
27
src/App.vue
@@ -2,8 +2,7 @@
|
||||
|
||||
import {onMounted, watch} from "vue";
|
||||
import {BaseState, useBaseStore} from "@/stores/base.ts";
|
||||
import {Dict, DictType, SaveConfig, SaveDict} from "@/types.ts"
|
||||
import Practice from "@/pages/practice/index.vue"
|
||||
import {Dict, DictType} from "@/types.ts"
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
@@ -12,8 +11,9 @@ import useTheme from "@/hooks/theme.ts";
|
||||
import * as localforage from "localforage";
|
||||
import SettingDialog from "@/components/dialog/SettingDialog.vue";
|
||||
import ArticleContentDialog from "@/components/dialog/ArticleContentDialog.vue";
|
||||
import {useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import CollectNotice from "@/components/CollectNotice.vue";
|
||||
import {SAVE_SETTING_KEY, SAVE_DICT_KEY} from "@/utils/const.ts";
|
||||
import {shakeCommonDict} from "@/utils";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
@@ -21,26 +21,11 @@ const settingStore = useSettingStore()
|
||||
const {setTheme} = useTheme()
|
||||
|
||||
watch(store.$state, (n: BaseState) => {
|
||||
let data: BaseState = cloneDeep(n)
|
||||
data.myDictList.map((v: Dict) => {
|
||||
if (v.isCustom) {
|
||||
if (v.type === DictType.article) {
|
||||
v.articles.map(s => {
|
||||
delete s.sections
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (v.type === DictType.word) v.originWords = []
|
||||
if (v.type === DictType.article) v.articles = []
|
||||
v.words = []
|
||||
v.chapterWords = []
|
||||
}
|
||||
})
|
||||
localforage.setItem(SaveDict.key, JSON.stringify({val: data, version: SaveDict.version}))
|
||||
localforage.setItem(SAVE_DICT_KEY.key, JSON.stringify({val: shakeCommonDict(n), version: SAVE_DICT_KEY.version}))
|
||||
})
|
||||
|
||||
watch(settingStore.$state, (n) => {
|
||||
localStorage.setItem(SaveConfig.key, JSON.stringify({val: n, version: SaveConfig.version}))
|
||||
localStorage.setItem(SAVE_SETTING_KEY.key, JSON.stringify({val: n, version: SAVE_SETTING_KEY.version}))
|
||||
})
|
||||
|
||||
//检测几个特定词典
|
||||
@@ -94,7 +79,7 @@ onMounted(() => {
|
||||
<router-view/>
|
||||
<CollectNotice/>
|
||||
<ArticleContentDialog/>
|
||||
<SettingDialog v-if="runtimeStore.showSettingModal" @close="runtimeStore.showSettingModal = false"/>
|
||||
<SettingDialog/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -14,12 +14,10 @@ let timer = -1
|
||||
|
||||
function toggleNotice() {
|
||||
showNotice = true
|
||||
settingStore.first = false
|
||||
timer = setInterval(() => {
|
||||
num--
|
||||
if (num <= 0) {
|
||||
show = settingStore.first = false
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (num <= 0) close()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
@@ -65,7 +63,8 @@ const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
|
||||
点亮它!
|
||||
</div>
|
||||
</div>
|
||||
<div class="collect-keyboard">或使用收藏快捷键<span class="active">{{isMac?'Command':'Ctrl'}} + D</span></div>
|
||||
<div class="collect-keyboard">或使用收藏快捷键<span
|
||||
class="active">{{ isMac ? 'Command' : 'Ctrl' }} + D</span></div>
|
||||
</div>
|
||||
<BaseButton v-else size="large" @click="toggleNotice">我想收藏</BaseButton>
|
||||
</transition>
|
||||
|
||||
@@ -18,8 +18,6 @@ const settingStore = useSettingStore()
|
||||
|
||||
<template>
|
||||
<div class="right-bar">
|
||||
|
||||
|
||||
<Tooltip
|
||||
:title="`切换主题(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
>
|
||||
@@ -42,7 +40,7 @@ const settingStore = useSettingStore()
|
||||
|
||||
.right-bar {
|
||||
position: fixed;
|
||||
right: 30rem;
|
||||
right: var(--space);
|
||||
top: var(--space);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
|
||||
577
src/components/Setting.vue
Normal file
577
src/components/Setting.vue
Normal file
@@ -0,0 +1,577 @@
|
||||
<script setup lang="ts">
|
||||
import {Icon} from '@iconify/vue';
|
||||
import {ref, watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
|
||||
import {getShortcutKey, useDisableEventListener, useEventListener} from "@/hooks/event.ts";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {DefaultShortcutKeyMap, Dict, DictType, ShortcutKey} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions} from "@/utils/const.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {BaseState, useBaseStore} from "@/stores/base.ts";
|
||||
import * as copy from "copy-to-clipboard";
|
||||
import {saveAs} from "file-saver";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
|
||||
import {dayjs} from "element-plus";
|
||||
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
}>()
|
||||
|
||||
const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
//@ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
|
||||
useDisableEventListener(() => undefined)
|
||||
useWatchAllSound()
|
||||
|
||||
let editShortcutKey = $ref('')
|
||||
|
||||
const disabledDefaultKeyboardEvent = $computed(() => {
|
||||
return editShortcutKey && tabIndex === 2
|
||||
})
|
||||
|
||||
watch(() => disabledDefaultKeyboardEvent, v => {
|
||||
emit('toggleDisabledDialogEscKey', !!v)
|
||||
})
|
||||
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (!disabledDefaultKeyboardEvent) return
|
||||
e.preventDefault()
|
||||
|
||||
let shortcutKey = getShortcutKey(e)
|
||||
// console.log('e', e, e.keyCode, e.ctrlKey, e.altKey, e.shiftKey)
|
||||
// console.log('key', shortcutKey)
|
||||
|
||||
// if (shortcutKey[shortcutKey.length-1] === '+') {
|
||||
// settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
|
||||
// return ElMessage.warning('设备失败!')
|
||||
// }
|
||||
|
||||
if (editShortcutKey) {
|
||||
if (shortcutKey === 'Delete') {
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = ''
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(settingStore.shortcutKeyMap)) {
|
||||
if (v === shortcutKey && k !== editShortcutKey) {
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
|
||||
return ElMessage.warning('快捷键重复!')
|
||||
}
|
||||
}
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = shortcutKey
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function resetShortcutKeyMap() {
|
||||
editShortcutKey = ''
|
||||
settingStore.shortcutKeyMap = cloneDeep(DefaultShortcutKeyMap)
|
||||
ElMessage.success('恢复成功')
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
let data = {
|
||||
version: EXPORT_DATA_KEY.version,
|
||||
val: {
|
||||
setting: {
|
||||
version: SAVE_SETTING_KEY.version,
|
||||
val: settingStore.$state
|
||||
},
|
||||
dict: {
|
||||
version: SAVE_DICT_KEY.version,
|
||||
val: shakeCommonDict(store.$state)
|
||||
}
|
||||
}
|
||||
}
|
||||
let blob = new Blob([JSON.stringify(data)], {type: "text/plain;charset=utf-8"});
|
||||
let date = new Date()
|
||||
let dateStr = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
|
||||
saveAs(blob, `${APP_NAME}-User-Data-${dateStr}.json`);
|
||||
ElMessage.success('导出成功!')
|
||||
}
|
||||
|
||||
function importData(e) {
|
||||
let file = e.target.files[0]
|
||||
if (!file) return
|
||||
// no()
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (v) {
|
||||
let str = v.target.result;
|
||||
if (str) {
|
||||
let obj = JSON.parse(str)
|
||||
if (obj.version === EXPORT_DATA_KEY.version) {
|
||||
|
||||
} else {
|
||||
//TODO
|
||||
}
|
||||
let data = obj.val
|
||||
let settingState = checkAndUpgradeSaveSetting(data.setting)
|
||||
settingStore.setState(settingState)
|
||||
let dictState = checkAndUpgradeSaveDict(data.dict)
|
||||
store.init(dictState)
|
||||
ElMessage.success('导入成功!')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting">
|
||||
<div class="left">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
<Icon icon="bx:headphone" width="20" color="#0C8CE9"/>
|
||||
<span>音效设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
|
||||
<Icon icon="material-symbols:keyboard-outline" width="20" color="#0C8CE9"/>
|
||||
<span>快捷键设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
|
||||
<Icon icon="icon-park-outline:setting-config" width="20" color="#0C8CE9"/>
|
||||
<span>其他设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 3 && 'active'" @click="tabIndex = 3">
|
||||
<Icon icon="mdi:database-cog-outline" width="20" color="#0C8CE9"/>
|
||||
<span>数据管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="git-log">
|
||||
Build {{ gitLastCommitHash }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div v-if="tabIndex === 0">
|
||||
<div class="row">
|
||||
<label class="main-title">所有音效</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allSound"
|
||||
@change="useChangeAllSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">单词/句子自动发音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.wordSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">单词/句子发音口音</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.wordSoundType"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option label="美音" value="us"/>
|
||||
<el-option label="英音" value="uk"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundVolume"/>
|
||||
<span>{{ settingStore.wordSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">倍速</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<span>{{ settingStore.wordSoundSpeed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.keyboardSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音效</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="el-option-row">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<span>{{ settingStore.keyboardSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<!-- <div class="row">-->
|
||||
<!-- <label class="item-title">释义发音</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-switch v-model="settingStore.translateSound"-->
|
||||
<!-- inline-prompt-->
|
||||
<!-- active-text="开"-->
|
||||
<!-- inactive-text="关"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row">-->
|
||||
<!-- <label class="sub-title">音量</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-slider v-model="settingStore.translateSoundVolume"/>-->
|
||||
<!-- <span>{{ settingStore.translateSoundVolume }}%</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">效果音(章节结算页烟花音效)</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.effectSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.effectSoundVolume"/>
|
||||
<span>{{ settingStore.effectSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 1">
|
||||
<div class="row">
|
||||
<label class="item-title">显示上一个/下一个单词</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.showNearWord"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,练习中会在上方显示上一个/下一个单词
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">忽略大小写</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.ignoreCase"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">允许默写模式下显示提示</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allowWordTip"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,可以通过鼠标 hover 单词或者按 {{ settingStore.shortcutKeyMap[ShortcutKey.ShowWord] }} 显示正确答案
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">字体设置(仅可调整单词练习)</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">外语字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordForeignFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">中文字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordTranslateFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">其他设置</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">切换下一个单词时间</label>
|
||||
<div class="wrapper">
|
||||
<el-input-number v-model="settingStore.waitTimeForChangeWord"
|
||||
:min="6"
|
||||
:max="100"
|
||||
type="number"
|
||||
/>
|
||||
<span>毫秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body" v-if="tabIndex === 2">
|
||||
<div class="row">
|
||||
<label class="main-title">功能</label>
|
||||
<div class="wrapper">快捷键(点击可修改)</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="row" v-for="item of Object.entries(settingStore.shortcutKeyMap)">
|
||||
<label class="item-title">{{ $t(item[0]) }}</label>
|
||||
<div class="wrapper" @click="editShortcutKey = item[0]">
|
||||
<div class="set-key" v-if="editShortcutKey === item[0]">
|
||||
<input :value="item[1]?item[1]:'未设置快捷键'" readonly type="text" @blur="editShortcutKey = ''">
|
||||
<span @click.stop="editShortcutKey = ''">直接按键盘进行设置</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="item[1]">{{ item[1] }}</div>
|
||||
<span v-else>未设置快捷键</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row footer">
|
||||
<label class="item-title"></label>
|
||||
<div class="wrapper">
|
||||
<BaseButton @click="resetShortcutKeyMap">恢复默认</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 3">
|
||||
<div class="row">
|
||||
<div class="main-title">数据导出</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">
|
||||
目前用户的所有数据(自定义设置、自定义词典、练习进度等)
|
||||
<b>仅保存在本地</b>
|
||||
。如果您需要在不同的设备、浏览器或者其他非官方部署上使用 {{ APP_NAME }}, 您需要手动进行数据同步和保存。
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<BaseButton @click="exportData">数据导出</BaseButton>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="main-title">数据导入</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">
|
||||
请注意,导入数据将
|
||||
<b style="color: red"> 完全覆盖 </b>
|
||||
当前数据。请谨慎操作。
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="import hvr-grow">
|
||||
<BaseButton>数据导入</BaseButton>
|
||||
<input type="file"
|
||||
accept="application/json"
|
||||
@change="importData">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.setting {
|
||||
width: 40vw;
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
color: var(--color-font-1);
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.tabs {
|
||||
padding: 10rem 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//align-items: center;
|
||||
//justify-content: center;
|
||||
gap: 10rem;
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
padding: 10rem 15rem;
|
||||
border-radius: 8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.git-log {
|
||||
font-size: 10rem;
|
||||
color: gray;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--color-header-bg);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10rem var(--space);
|
||||
|
||||
.row {
|
||||
min-height: 40rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: calc(var(--space) * 5);
|
||||
|
||||
.wrapper {
|
||||
height: 30rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
|
||||
span {
|
||||
text-align: right;
|
||||
//width: 30rem;
|
||||
font-size: 12rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.set-key {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
width: 150rem;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10rem;
|
||||
height: 28rem;
|
||||
outline: none;
|
||||
font-size: 16rem;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3rem;
|
||||
padding: 0 5rem;
|
||||
background: var(--color-second-bg);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22rem;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
padding-right: 10rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 10rem;
|
||||
font-size: 12rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid #c4c3c3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-option-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon-wrapper {
|
||||
transform: translateX(10rem);
|
||||
}
|
||||
}
|
||||
|
||||
.import {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {saveAs} from "file-saver";
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {Article, DefaultArticle, DictType, Sort} from "@/types.ts";
|
||||
import {Article, DefaultArticle} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
|
||||
@@ -11,324 +11,19 @@ import {DefaultShortcutKeyMap, ShortcutKey} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {SoundFileOptions} from "@/utils/const.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import Setting from "@/components/Setting.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
|
||||
const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
//@ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
])
|
||||
|
||||
useDisableEventListener()
|
||||
useWatchAllSound()
|
||||
|
||||
let editShortcutKey = $ref('')
|
||||
|
||||
const disabledDefaultKeyboardEvent = $computed(() => {
|
||||
return editShortcutKey && tabIndex === 2
|
||||
})
|
||||
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (!disabledDefaultKeyboardEvent) return
|
||||
e.preventDefault()
|
||||
|
||||
let shortcutKey = getShortcutKey(e)
|
||||
// console.log('e', e, e.keyCode, e.ctrlKey, e.altKey, e.shiftKey)
|
||||
// console.log('key', shortcutKey)
|
||||
|
||||
// if (shortcutKey[shortcutKey.length-1] === '+') {
|
||||
// settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
|
||||
// return ElMessage.warning('设备失败!')
|
||||
// }
|
||||
|
||||
if (editShortcutKey) {
|
||||
if (shortcutKey === 'Delete') {
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = ''
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(settingStore.shortcutKeyMap)) {
|
||||
if (v === shortcutKey && k !== editShortcutKey) {
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
|
||||
return ElMessage.warning('快捷键重复!')
|
||||
}
|
||||
}
|
||||
settingStore.shortcutKeyMap[editShortcutKey] = shortcutKey
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function resetShortcutKeyMap() {
|
||||
editShortcutKey = ''
|
||||
settingStore.shortcutKeyMap = cloneDeep(DefaultShortcutKeyMap)
|
||||
ElMessage.success('恢复成功')
|
||||
}
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
let disabledDialogEscKey = $ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
@close="emit('close')"
|
||||
:keyboard="!disabledDefaultKeyboardEvent"
|
||||
v-model="runtimeStore.showSettingModal"
|
||||
:keyboard="disabledDialogEscKey"
|
||||
title="设置">
|
||||
<div class="setting-modal">
|
||||
<div class="left">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
<Icon icon="bx:headphone" width="20" color="#0C8CE9"/>
|
||||
<span>音效设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
|
||||
<Icon icon="icon-park-outline:setting-config" width="20" color="#0C8CE9"/>
|
||||
<span>其他设置</span>
|
||||
</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
|
||||
<Icon icon="material-symbols:keyboard-outline" width="20" color="#0C8CE9"/>
|
||||
<span>快捷键设置</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="git-log">
|
||||
Build {{ gitLastCommitHash }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div v-if="tabIndex === 0">
|
||||
<div class="row">
|
||||
<label class="main-title">所有音效</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allSound"
|
||||
@change="useChangeAllSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">单词/句子自动发音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.wordSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">单词/句子发音口音</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.wordSoundType"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option label="美音" value="us"/>
|
||||
<el-option label="英音" value="uk"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundVolume"/>
|
||||
<span>{{ settingStore.wordSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">倍速</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
|
||||
<span>{{ settingStore.wordSoundSpeed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.keyboardSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="item-title">按键音效</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="el-option-row">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.keyboardSoundVolume"/>
|
||||
<span>{{ settingStore.keyboardSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<!-- <div class="row">-->
|
||||
<!-- <label class="item-title">释义发音</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-switch v-model="settingStore.translateSound"-->
|
||||
<!-- inline-prompt-->
|
||||
<!-- active-text="开"-->
|
||||
<!-- inactive-text="关"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row">-->
|
||||
<!-- <label class="sub-title">音量</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-slider v-model="settingStore.translateSoundVolume"/>-->
|
||||
<!-- <span>{{ settingStore.translateSoundVolume }}%</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">效果音(章节结算页烟花音效)</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.effectSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">音量</label>
|
||||
<div class="wrapper">
|
||||
<el-slider v-model="settingStore.effectSoundVolume"/>
|
||||
<span>{{ settingStore.effectSoundVolume }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tabIndex === 1">
|
||||
<div class="row">
|
||||
<label class="item-title">显示上一个/下一个单词</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.showNearWord"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,练习中会在上方显示上一个/下一个单词
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">忽略大小写</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.ignoreCase"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">允许默写模式下显示提示</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allowWordTip"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc">
|
||||
开启后,可以通过鼠标 hover 单词或者按 {{ settingStore.shortcutKeyMap[ShortcutKey.ShowWord] }} 显示正确答案
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">字体设置(仅可调整单词练习)</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">外语字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordForeignFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">中文字体</label>
|
||||
<div class="wrapper">
|
||||
<el-slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
<span>{{ settingStore.fontSize.wordTranslateFontSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="line"></div>
|
||||
<div class="row">
|
||||
<label class="item-title">其他设置</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="sub-title">切换下一个单词时间</label>
|
||||
<div class="wrapper">
|
||||
<el-input-number v-model="settingStore.waitTimeForChangeWord"
|
||||
:min="6"
|
||||
:max="100"
|
||||
type="number"
|
||||
/>
|
||||
<span>毫秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body" v-if="tabIndex === 2">
|
||||
<div class="row">
|
||||
<label class="main-title">功能</label>
|
||||
<div class="wrapper">快捷键(点击可修改)</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="row" v-for="item of Object.entries(settingStore.shortcutKeyMap)">
|
||||
<label class="item-title">{{ $t(item[0]) }}</label>
|
||||
<div class="wrapper" @click="editShortcutKey = item[0]">
|
||||
<div class="set-key" v-if="editShortcutKey === item[0]">
|
||||
<input :value="item[1]?item[1]:'未设置快捷键'" readonly type="text" @blur="editShortcutKey = ''">
|
||||
<span @click.stop="editShortcutKey = ''">直接按键盘进行设置</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="item[1]">{{ item[1] }}</div>
|
||||
<span v-else>未设置快捷键</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row footer">
|
||||
<label class="item-title"></label>
|
||||
<div class="wrapper">
|
||||
<BaseButton @click="resetShortcutKeyMap">恢复默认</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Setting @toggle-disabled-dialog-esc-key="e => disabledDialogEscKey = !e"/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -467,15 +162,4 @@ function resetShortcutKeyMap() {
|
||||
}
|
||||
}
|
||||
|
||||
.el-option-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon-wrapper {
|
||||
transform: translateX(10rem);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
let length = $computed(() => {
|
||||
let isWord = props.dict.type === DictType.word
|
||||
let isWord = [DictType.word,DictType.collect,DictType.simple,DictType.wrong].includes(props.dict.type)
|
||||
let len: any = ''
|
||||
if (props.dict.length) {
|
||||
len = props.dict.length
|
||||
|
||||
@@ -1,136 +1,131 @@
|
||||
import {onMounted, onUnmounted, toRef, toValue, watch} from "vue";
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import {DefaultShortcutKeyMap} from "@/types.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
export function useWindowClick(cb: (e: PointerEvent) => void) {
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.closeOther, cb)
|
||||
window.addEventListener('click', cb)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', cb)
|
||||
})
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.closeOther, cb)
|
||||
window.addEventListener('click', cb)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', cb)
|
||||
})
|
||||
}
|
||||
|
||||
export function useEventListener(type: string, listener: EventListenerOrEventListenerObject) {
|
||||
onMounted(() => window.addEventListener(type, listener))
|
||||
onUnmounted(() => window.removeEventListener(type, listener))
|
||||
onMounted(() => window.addEventListener(type, listener))
|
||||
onUnmounted(() => window.removeEventListener(type, listener))
|
||||
}
|
||||
|
||||
export function getShortcutKey(e: KeyboardEvent) {
|
||||
let shortcutKey = ''
|
||||
if (e.ctrlKey) shortcutKey += 'Ctrl+'
|
||||
if (e.altKey) shortcutKey += 'Alt+'
|
||||
if (e.shiftKey) shortcutKey += 'Shift+'
|
||||
if (e.key !== 'Control' && e.key !== 'Alt' && e.key !== 'Shift') {
|
||||
if (e.keyCode >= 65 && e.keyCode <= 90) {
|
||||
shortcutKey += e.key.toUpperCase()
|
||||
} else {
|
||||
if (e.key === 'ArrowRight') {
|
||||
shortcutKey += '➡'
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
shortcutKey += '⬅'
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
shortcutKey += '⬆'
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
shortcutKey += '⬇'
|
||||
} else {
|
||||
shortcutKey += e.key
|
||||
}
|
||||
let shortcutKey = ''
|
||||
if (e.ctrlKey) shortcutKey += 'Ctrl+'
|
||||
if (e.altKey) shortcutKey += 'Alt+'
|
||||
if (e.shiftKey) shortcutKey += 'Shift+'
|
||||
if (e.key !== 'Control' && e.key !== 'Alt' && e.key !== 'Shift') {
|
||||
if (e.keyCode >= 65 && e.keyCode <= 90) {
|
||||
shortcutKey += e.key.toUpperCase()
|
||||
} else {
|
||||
if (e.key === 'ArrowRight') {
|
||||
shortcutKey += '➡'
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
shortcutKey += '⬅'
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
shortcutKey += '⬆'
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
shortcutKey += '⬇'
|
||||
} else {
|
||||
shortcutKey += e.key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shortcutKey = shortcutKey.trim()
|
||||
shortcutKey = shortcutKey.trim()
|
||||
|
||||
// console.log('key', shortcutKey)
|
||||
return shortcutKey
|
||||
// console.log('key', shortcutKey)
|
||||
return shortcutKey
|
||||
}
|
||||
|
||||
export function useStartKeyboardEventListener() {
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
// console.log('e',e.keyCode,e.code)
|
||||
if (!runtimeStore.disableEventListener) {
|
||||
e.preventDefault()
|
||||
let shortcutKey = getShortcutKey(e)
|
||||
// console.log('shortcutKey', shortcutKey)
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
// console.log('e',e.keyCode,e.code)
|
||||
if (!runtimeStore.disableEventListener) {
|
||||
e.preventDefault()
|
||||
let shortcutKey = getShortcutKey(e)
|
||||
// console.log('shortcutKey', shortcutKey)
|
||||
|
||||
let list = Object.entries(settingStore.shortcutKeyMap)
|
||||
let shortcutEvent = ''
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let [k, v] = list[i]
|
||||
if (v === shortcutKey) {
|
||||
console.log('快捷键', k)
|
||||
shortcutEvent = k
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shortcutEvent) {
|
||||
emitter.emit(shortcutEvent, e)
|
||||
} else {
|
||||
//非英文模式下,输入区域的 keyCode 均为 229时,
|
||||
if ((e.keyCode >= 65 && e.keyCode <= 90)
|
||||
|| (e.keyCode >= 48 && e.keyCode <= 57)
|
||||
|| e.code === 'Space'
|
||||
|| e.code === 'Slash'
|
||||
|| e.code === 'Quote'
|
||||
|| e.code === 'Comma'
|
||||
|| e.code === 'BracketLeft'
|
||||
|| e.code === 'BracketRight'
|
||||
|| e.code === 'Period'
|
||||
|| e.code === 'Minus'
|
||||
|| e.code === 'Equal'
|
||||
|| e.code === 'Semicolon'
|
||||
// || e.code === 'Backquote'
|
||||
|| e.keyCode === 229
|
||||
) {
|
||||
emitter.emit(EventKey.onTyping, e)
|
||||
} else {
|
||||
emitter.emit(EventKey.keydown, e)
|
||||
}
|
||||
}
|
||||
|
||||
let list = Object.entries(settingStore.shortcutKeyMap)
|
||||
let shortcutEvent = ''
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let [k, v] = list[i]
|
||||
if (v === shortcutKey) {
|
||||
console.log('快捷键', k)
|
||||
shortcutEvent = k
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shortcutEvent) {
|
||||
emitter.emit(shortcutEvent, e)
|
||||
} else {
|
||||
//非英文模式下,输入区域的 keyCode 均为 229时,
|
||||
if ((e.keyCode >= 65 && e.keyCode <= 90)
|
||||
|| (e.keyCode >= 48 && e.keyCode <= 57)
|
||||
|| e.code === 'Space'
|
||||
|| e.code === 'Slash'
|
||||
|| e.code === 'Quote'
|
||||
|| e.code === 'Comma'
|
||||
|| e.code === 'BracketLeft'
|
||||
|| e.code === 'BracketRight'
|
||||
|| e.code === 'Period'
|
||||
|| e.code === 'Minus'
|
||||
|| e.code === 'Equal'
|
||||
|| e.code === 'Semicolon'
|
||||
// || e.code === 'Backquote'
|
||||
|| e.keyCode === 229
|
||||
) {
|
||||
emitter.emit(EventKey.onTyping, e)
|
||||
} else {
|
||||
emitter.emit(EventKey.keydown, e)
|
||||
})
|
||||
useEventListener('keyup', (e: KeyboardEvent) => {
|
||||
if (!runtimeStore.disableEventListener) {
|
||||
emitter.emit(EventKey.keyup, e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
useEventListener('keyup', (e: KeyboardEvent) => {
|
||||
if (!runtimeStore.disableEventListener) {
|
||||
emitter.emit(EventKey.keyup, e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function useOnKeyboardEventListener(onKeyDown: (e: KeyboardEvent) => void, onKeyUp: (e: KeyboardEvent) => void) {
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.keydown, onKeyDown)
|
||||
emitter.on(EventKey.keyup, onKeyUp)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.keydown, onKeyDown)
|
||||
emitter.off(EventKey.keyup, onKeyUp)
|
||||
})
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.keydown, onKeyDown)
|
||||
emitter.on(EventKey.keyup, onKeyUp)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.keydown, onKeyDown)
|
||||
emitter.off(EventKey.keyup, onKeyUp)
|
||||
})
|
||||
}
|
||||
|
||||
export function useDisableEventListener(watchVal?: any) {
|
||||
const runtimeStore = useRuntimeStore()
|
||||
watch(watchVal, n => {
|
||||
if (n) {
|
||||
runtimeStore.disableEventListener = true
|
||||
} else {
|
||||
runtimeStore.disableEventListener = false
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
if (watchVal === undefined) {
|
||||
runtimeStore.disableEventListener = true
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (watchVal === undefined) {
|
||||
runtimeStore.disableEventListener = false
|
||||
}
|
||||
})
|
||||
export function useDisableEventListener(watchVal: any) {
|
||||
const runtimeStore = useRuntimeStore()
|
||||
watch(watchVal, (n: any) => {
|
||||
if (n === true) runtimeStore.disableEventListener = true
|
||||
if (n === false) runtimeStore.disableEventListener = false
|
||||
})
|
||||
onMounted(() => {
|
||||
if (watchVal() === undefined) {
|
||||
runtimeStore.disableEventListener = true
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (watchVal() === undefined) {
|
||||
runtimeStore.disableEventListener = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ const showCollectToggleButton = $computed(() => {
|
||||
<WordList
|
||||
class="word-list"
|
||||
:list="store.wrong.words">
|
||||
<template v-slot="{item,index}">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click="delWrongWord(item)"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import TypingWord from "@/pages/practice/practice-word/TypingWord.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import {chunk, cloneDeep} from "lodash-es";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {ShortcutKey, Word} from "@/types.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
|
||||
@@ -1,23 +1,110 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {DefaultDict, Dict, DictType, DisplayStatistics, SaveDict, Word} from "../types.ts"
|
||||
import {DefaultDict, Dict, DictType, DisplayStatistics, Word} from "../types.ts"
|
||||
import {chunk, cloneDeep, merge} from "lodash-es";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts"
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import * as localforage from "localforage";
|
||||
import {nanoid} from "nanoid";
|
||||
import {state} from "vue-tsc/out/shared";
|
||||
import {SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
|
||||
import {checkAndUpgradeSaveDict} from "@/utils";
|
||||
|
||||
export interface BaseState {
|
||||
myDictList: Dict[],
|
||||
collectDictIds: string[],
|
||||
current: {
|
||||
index: number,
|
||||
practiceType: DictType,//练习类型,目前仅词典为collect时判断是练单词还是文章使用
|
||||
},
|
||||
simpleWords: string[],
|
||||
load: boolean
|
||||
myDictList: Dict[],
|
||||
collectDictIds: string[],
|
||||
current: {
|
||||
index: number,
|
||||
practiceType: DictType,//练习类型,目前仅词典为collect时判断是练单词还是文章使用
|
||||
},
|
||||
simpleWords: string[],
|
||||
load: boolean
|
||||
}
|
||||
|
||||
export const DefaultBaseState = (): BaseState => ({
|
||||
myDictList: [
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'collect',
|
||||
name: '收藏',
|
||||
type: DictType.collect,
|
||||
category: '自带字典',
|
||||
tags: ['自带'],
|
||||
isCustom: true,
|
||||
},
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'skip',
|
||||
name: '简单词',
|
||||
type: DictType.simple,
|
||||
category: '自带字典',
|
||||
isCustom: true,
|
||||
},
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'wrong',
|
||||
name: '错词本',
|
||||
type: DictType.wrong,
|
||||
category: '自带字典',
|
||||
isCustom: true,
|
||||
},
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'cet4',
|
||||
name: 'CET-4',
|
||||
description: '大学英语四级词库',
|
||||
category: '中国考试',
|
||||
tags: ['大学英语'],
|
||||
url: 'CET4_T.json',
|
||||
length: 2607,
|
||||
translateLanguage: 'common',
|
||||
language: 'en',
|
||||
type: DictType.word
|
||||
},
|
||||
// {
|
||||
// ...cloneDeep(DefaultDict),
|
||||
// id: 'article_nce2',
|
||||
// name: "新概念英语2-课文",
|
||||
// description: '新概念英语2-课文',
|
||||
// category: '英语学习',
|
||||
// tags: ['新概念英语'],
|
||||
// url: 'NCE_2.json',
|
||||
// translateLanguage: 'common',
|
||||
// language: 'en',
|
||||
// type: DictType.article,
|
||||
// resourceId: 'article_nce2',
|
||||
// length: 96
|
||||
// },
|
||||
// {
|
||||
// ...cloneDeep(DefaultDict),
|
||||
// id: 'nce-new-2',
|
||||
// name: '新概念英语(新版)-2',
|
||||
// description: '新概念英语新版第二册',
|
||||
// category: '青少年英语',
|
||||
// tags: ['新概念英语'],
|
||||
// url: 'nce-new-2.json',
|
||||
// translateLanguage: 'common',
|
||||
// language: 'en',
|
||||
// type: DictType.word,
|
||||
// resourceId: 'nce-new-2',
|
||||
// length: 862
|
||||
// },
|
||||
],
|
||||
collectDictIds: [],
|
||||
current: {
|
||||
index: 3,
|
||||
// dictType: DictType.article,
|
||||
// index: 0,
|
||||
practiceType: DictType.word,
|
||||
},
|
||||
simpleWords: [
|
||||
'a', 'an',
|
||||
'i', 'my', 'you', 'your', 'me', 'it',
|
||||
'what', 'who', 'where', 'how', 'when', 'which',
|
||||
'be', 'am', 'is', 'do', 'are', 'did', 'were', 'was', 'can', 'could', 'will', 'would',
|
||||
'the', 'that', 'this', 'to', 'of', 'for', 'and', 'at', 'not', 'no', 'yes',
|
||||
],
|
||||
load: false
|
||||
})
|
||||
|
||||
// words: [
|
||||
// {
|
||||
// "name": "cancelcancelcancelcancelcancelcancelcancelcancel",
|
||||
@@ -64,296 +151,206 @@ export interface BaseState {
|
||||
// ],
|
||||
|
||||
export const useBaseStore = defineStore('base', {
|
||||
state: (): BaseState => {
|
||||
return {
|
||||
myDictList: [
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'collect',
|
||||
name: '收藏',
|
||||
type: DictType.collect,
|
||||
category: '自带字典',
|
||||
tags: ['自带'],
|
||||
isCustom: true,
|
||||
state: (): BaseState => {
|
||||
return DefaultBaseState()
|
||||
},
|
||||
getters: {
|
||||
collect(): Dict {
|
||||
return this.myDictList[0]
|
||||
},
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'skip',
|
||||
name: '简单词',
|
||||
type: DictType.simple,
|
||||
category: '自带字典',
|
||||
isCustom: true,
|
||||
simple(): Dict {
|
||||
return this.myDictList[1]
|
||||
},
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'wrong',
|
||||
name: '错词本',
|
||||
type: DictType.wrong,
|
||||
category: '自带字典',
|
||||
isCustom: true,
|
||||
wrong(): Dict {
|
||||
return this.myDictList[2]
|
||||
},
|
||||
{
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'cet4',
|
||||
name: 'CET-4',
|
||||
description: '大学英语四级词库',
|
||||
category: '中国考试',
|
||||
tags: ['大学英语'],
|
||||
url: 'CET4_T.json',
|
||||
length: 2607,
|
||||
translateLanguage: 'common',
|
||||
language: 'en',
|
||||
type: DictType.word
|
||||
skipWordNames() {
|
||||
return this.simple.originWords.map(v => v.name.toLowerCase())
|
||||
},
|
||||
// {
|
||||
// ...cloneDeep(DefaultDict),
|
||||
// id: 'article_nce2',
|
||||
// name: "新概念英语2-课文",
|
||||
// description: '新概念英语2-课文',
|
||||
// category: '英语学习',
|
||||
// tags: ['新概念英语'],
|
||||
// url: 'NCE_2.json',
|
||||
// translateLanguage: 'common',
|
||||
// language: 'en',
|
||||
// type: DictType.article,
|
||||
// resourceId: 'article_nce2',
|
||||
// length: 96
|
||||
// },
|
||||
// {
|
||||
// ...cloneDeep(DefaultDict),
|
||||
// id: 'nce-new-2',
|
||||
// name: '新概念英语(新版)-2',
|
||||
// description: '新概念英语新版第二册',
|
||||
// category: '青少年英语',
|
||||
// tags: ['新概念英语'],
|
||||
// url: 'nce-new-2.json',
|
||||
// translateLanguage: 'common',
|
||||
// language: 'en',
|
||||
// type: DictType.word,
|
||||
// resourceId: 'nce-new-2',
|
||||
// length: 862
|
||||
// },
|
||||
],
|
||||
collectDictIds: [],
|
||||
current: {
|
||||
index: 3,
|
||||
// dictType: DictType.article,
|
||||
// index: 0,
|
||||
practiceType: DictType.word,
|
||||
},
|
||||
simpleWords: [
|
||||
'a', 'an',
|
||||
'i', 'my', 'you', 'your', 'me', 'it',
|
||||
'what', 'who', 'where', 'how', 'when', 'which',
|
||||
'be', 'am', 'is', 'do', 'are', 'did', 'were', 'was', 'can', 'could', 'will', 'would',
|
||||
'the', 'that', 'this', 'to', 'of', 'for', 'and', 'at', 'not', 'no', 'yes',
|
||||
],
|
||||
load: false
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
collect(): Dict {
|
||||
return this.myDictList[0]
|
||||
},
|
||||
simple(): Dict {
|
||||
return this.myDictList[1]
|
||||
},
|
||||
wrong(): Dict {
|
||||
return this.myDictList[2]
|
||||
},
|
||||
skipWordNames() {
|
||||
return this.simple.originWords.map(v => v.name.toLowerCase())
|
||||
},
|
||||
skipWordNamesWithSimpleWords() {
|
||||
return this.simple.originWords.map(v => v.name.toLowerCase()).concat(this.simpleWords)
|
||||
},
|
||||
isArticle(state: BaseState): boolean {
|
||||
//如果是收藏时,特殊判断
|
||||
if (this.currentDict.type === DictType.collect) {
|
||||
return state.current.practiceType === DictType.article
|
||||
}
|
||||
return [
|
||||
DictType.article,
|
||||
].includes(this.currentDict.type)
|
||||
},
|
||||
currentDict(): Dict {
|
||||
return this.myDictList[this.current.index]
|
||||
},
|
||||
chapter(state: BaseState): Word[] {
|
||||
return this.currentDict.chapterWords[this.currentDict.chapterIndex] ?? []
|
||||
},
|
||||
chapterName(state: BaseState) {
|
||||
let title = ''
|
||||
switch (this.currentDict.type) {
|
||||
case DictType.collect:
|
||||
if (state.current.practiceType === DictType.article) {
|
||||
return `第${this.currentDict.chapterIndex + 1}章`
|
||||
}
|
||||
case DictType.wrong:
|
||||
case DictType.simple:
|
||||
return this.currentDict.name
|
||||
case DictType.word:
|
||||
return `第${this.currentDict.chapterIndex + 1}章`
|
||||
}
|
||||
return title
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setState(obj: any) {
|
||||
//这样不会丢失watch的值的引用
|
||||
merge(this, obj)
|
||||
},
|
||||
async init() {
|
||||
return new Promise(async resolve => {
|
||||
try {
|
||||
let configStr: string = await localforage.getItem(SaveDict.key)
|
||||
// console.log(configStr)
|
||||
// console.log('s', new Blob([configStr]).size)
|
||||
// configStr = ''
|
||||
if (configStr) {
|
||||
let data = JSON.parse(configStr)
|
||||
let state: BaseState = data.val
|
||||
let version = Number(data.version)
|
||||
// console.log('state', state)
|
||||
state.load = false
|
||||
|
||||
if (version === SaveDict.version) {
|
||||
this.setState(state)
|
||||
} else {
|
||||
if (version === 2) {
|
||||
|
||||
}
|
||||
// this.setState(state)
|
||||
skipWordNamesWithSimpleWords() {
|
||||
return this.simple.originWords.map(v => v.name.toLowerCase()).concat(this.simpleWords)
|
||||
},
|
||||
isArticle(state: BaseState): boolean {
|
||||
//如果是收藏时,特殊判断
|
||||
if (this.currentDict.type === DictType.collect) {
|
||||
return state.current.practiceType === DictType.article
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('读取本地dict数据失败', e)
|
||||
return [
|
||||
DictType.article,
|
||||
].includes(this.currentDict.type)
|
||||
},
|
||||
currentDict(): Dict {
|
||||
return this.myDictList[this.current.index]
|
||||
},
|
||||
chapter(state: BaseState): Word[] {
|
||||
return this.currentDict.chapterWords[this.currentDict.chapterIndex] ?? []
|
||||
},
|
||||
chapterName(state: BaseState) {
|
||||
let title = ''
|
||||
switch (this.currentDict.type) {
|
||||
case DictType.collect:
|
||||
if (state.current.practiceType === DictType.article) {
|
||||
return `第${this.currentDict.chapterIndex + 1}章`
|
||||
}
|
||||
case DictType.wrong:
|
||||
case DictType.simple:
|
||||
return this.currentDict.name
|
||||
case DictType.word:
|
||||
return `第${this.currentDict.chapterIndex + 1}章`
|
||||
}
|
||||
return title
|
||||
}
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
if (location.href.includes('?mode=article')) {
|
||||
console.log('文章')
|
||||
let dict = {
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'article_nce2',
|
||||
name: "新概念英语2-课文",
|
||||
description: '新概念英语2-课文',
|
||||
category: '英语学习',
|
||||
tags: ['新概念英语'],
|
||||
url: 'NCE_2.json',
|
||||
translateLanguage: 'common',
|
||||
language: 'en',
|
||||
type: DictType.article,
|
||||
resourceId: 'article_nce2',
|
||||
length: 96
|
||||
}
|
||||
let rIndex = this.myDictList.findIndex((v: Dict) => v.id === dict.id)
|
||||
if (rIndex > -1) {
|
||||
this.myDictList[rIndex] = dict
|
||||
this.current.index = rIndex
|
||||
} else {
|
||||
this.myDictList.push(cloneDeep(dict))
|
||||
this.current.index = this.myDictList.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
if (this.current.index < 3) {
|
||||
|
||||
} else {
|
||||
let dictResourceUrl = `./dicts/${this.currentDict.language}/${this.currentDict.type}/${this.currentDict.translateLanguage}/${this.currentDict.url}`;
|
||||
if ([DictType.word].includes(this.currentDict.type)) {
|
||||
if (!this.currentDict.originWords.length) {
|
||||
let r = await fetch(dictResourceUrl)
|
||||
// let r = await fetch(`.${this.currentDict.url}`)
|
||||
let v = await r.json()
|
||||
v.map(s => {
|
||||
s.id = nanoid(6)
|
||||
})
|
||||
if (this.currentDict.translateLanguage === 'common') {
|
||||
let r2 = await fetch('./translate/en2zh_CN-min.json')
|
||||
// fetch('http://sc.ttentau.top/en2zh_CN-min.json').then(r2 => {
|
||||
let list: Word[] = await r2.json()
|
||||
if (list && list.length) {
|
||||
runtimeStore.translateWordList = list
|
||||
},
|
||||
actions: {
|
||||
setState(obj: any) {
|
||||
//这样不会丢失watch的值的引用
|
||||
merge(this, obj)
|
||||
},
|
||||
async init(outData?: any) {
|
||||
return new Promise(async resolve => {
|
||||
try {
|
||||
if (outData) {
|
||||
this.setState(outData)
|
||||
} else {
|
||||
let configStr: string = await localforage.getItem(SAVE_DICT_KEY.key)
|
||||
let data = checkAndUpgradeSaveDict(configStr)
|
||||
this.setState(data)
|
||||
}
|
||||
localforage.setItem(SAVE_DICT_KEY.key, JSON.stringify({val: this.$state, version: SAVE_DICT_KEY.version}))
|
||||
} catch (e) {
|
||||
console.error('读取本地dict数据失败', e)
|
||||
}
|
||||
}
|
||||
this.currentDict.originWords = cloneDeep(v)
|
||||
this.currentDict.words = cloneDeep(v)
|
||||
this.currentDict.chapterWords = chunk(this.currentDict.words, this.currentDict.chapterWordNumber)
|
||||
}
|
||||
}
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
if ([DictType.article].includes(this.currentDict.type)) {
|
||||
if (!this.currentDict.articles.length) {
|
||||
let r = await fetch(dictResourceUrl)
|
||||
let s: any[] = await r.json()
|
||||
this.currentDict.articles = cloneDeep(s.map(v => {
|
||||
v.id = nanoid(6)
|
||||
return v
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (location.href.includes('?mode=article')) {
|
||||
console.log('文章')
|
||||
let dict = {
|
||||
...cloneDeep(DefaultDict),
|
||||
id: 'article_nce2',
|
||||
name: "新概念英语2-课文",
|
||||
description: '新概念英语2-课文',
|
||||
category: '英语学习',
|
||||
tags: ['新概念英语'],
|
||||
url: 'NCE_2.json',
|
||||
translateLanguage: 'common',
|
||||
language: 'en',
|
||||
type: DictType.article,
|
||||
resourceId: 'article_nce2',
|
||||
length: 96
|
||||
}
|
||||
let rIndex = this.myDictList.findIndex((v: Dict) => v.id === dict.id)
|
||||
if (rIndex > -1) {
|
||||
this.myDictList[rIndex] = dict
|
||||
this.current.index = rIndex
|
||||
} else {
|
||||
this.myDictList.push(cloneDeep(dict))
|
||||
this.current.index = this.myDictList.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
//TODO 先这样,默认加载
|
||||
if (!runtimeStore.translateWordList.length) {
|
||||
setTimeout(async () => {
|
||||
let r2 = await fetch('./translate/en2zh_CN-min.json')
|
||||
// fetch('http://sc.ttentau.top/en2zh_CN-min.json').then(r2 => {
|
||||
let list: Word[] = await r2.json()
|
||||
if (list && list.length) {
|
||||
runtimeStore.translateWordList = list
|
||||
if (this.current.index < 3) {
|
||||
store.currentDict.words = cloneDeep(n)
|
||||
store.currentDict.chapterWords = [store.currentDict.words]
|
||||
} else {
|
||||
//自定义的词典,文章只删除了sections,单词并未做删除,所以这里不需要处理
|
||||
if (this.currentDict.isCustom) {
|
||||
|
||||
} else {
|
||||
//处理非自定义的情况。
|
||||
let dictResourceUrl = `./dicts/${this.currentDict.language}/${this.currentDict.type}/${this.currentDict.translateLanguage}/${this.currentDict.url}`;
|
||||
if ([DictType.word].includes(this.currentDict.type)) {
|
||||
if (!this.currentDict.originWords.length) {
|
||||
let r = await fetch(dictResourceUrl)
|
||||
let v = await r.json()
|
||||
v.map(s => {
|
||||
s.id = nanoid(6)
|
||||
})
|
||||
if (this.currentDict.translateLanguage === 'common') {
|
||||
let r2 = await fetch('./translate/en2zh_CN-min.json')
|
||||
// fetch('http://sc.ttentau.top/en2zh_CN-min.json').then(r2 => {
|
||||
let list: Word[] = await r2.json()
|
||||
if (list && list.length) {
|
||||
runtimeStore.translateWordList = list
|
||||
}
|
||||
}
|
||||
this.currentDict.originWords = cloneDeep(v)
|
||||
this.currentDict.words = cloneDeep(v)
|
||||
this.currentDict.chapterWords = chunk(this.currentDict.words, this.currentDict.chapterWordNumber)
|
||||
}
|
||||
}
|
||||
|
||||
if ([DictType.article].includes(this.currentDict.type)) {
|
||||
if (!this.currentDict.articles.length) {
|
||||
let r = await fetch(dictResourceUrl)
|
||||
let s: any[] = await r.json()
|
||||
this.currentDict.articles = cloneDeep(s.map(v => {
|
||||
v.id = nanoid(6)
|
||||
return v
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO 先这样,默认加载
|
||||
if (!runtimeStore.translateWordList.length) {
|
||||
setTimeout(async () => {
|
||||
let r2 = await fetch('./translate/en2zh_CN-min.json')
|
||||
// fetch('http://sc.ttentau.top/en2zh_CN-min.json').then(r2 => {
|
||||
let list: Word[] = await r2.json()
|
||||
if (list && list.length) {
|
||||
runtimeStore.translateWordList = list
|
||||
}
|
||||
})
|
||||
}
|
||||
emitter.emit(EventKey.changeDict)
|
||||
resolve(true)
|
||||
})
|
||||
},
|
||||
saveStatistics(statistics: DisplayStatistics) {
|
||||
if (statistics.spend > 1000 * 10) {
|
||||
delete statistics.wrongWords
|
||||
this.currentDict.statistics.push(statistics)
|
||||
}
|
||||
})
|
||||
},
|
||||
async changeDict(dict: Dict, practiceType?: DictType, chapterIndex?: number, wordIndex?: number) {
|
||||
//TODO 保存统计
|
||||
// this.saveStatistics()
|
||||
console.log('changeDict', cloneDeep(dict), chapterIndex, wordIndex)
|
||||
if (chapterIndex === undefined) chapterIndex = dict.chapterIndex
|
||||
if (wordIndex === undefined) wordIndex = dict.wordIndex
|
||||
if (practiceType === undefined) this.current.practiceType = practiceType
|
||||
if ([DictType.collect,
|
||||
DictType.simple,
|
||||
DictType.wrong].includes(dict.type)) {
|
||||
dict.chapterIndex = 0
|
||||
dict.wordIndex = wordIndex
|
||||
dict.chapterWordNumber = dict.words.length
|
||||
dict.chapterWords = [dict.words]
|
||||
} else {
|
||||
if (dict.type === DictType.article) {
|
||||
if (chapterIndex > dict.articles.length) {
|
||||
dict.chapterIndex = 0
|
||||
dict.wordIndex = 0
|
||||
}
|
||||
} else {
|
||||
if (chapterIndex > dict.chapterWords.length) {
|
||||
dict.chapterIndex = 0
|
||||
dict.wordIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
// await checkDictHasTranslate(dict)
|
||||
let rIndex = this.myDictList.findIndex((v: Dict) => v.id === dict.id)
|
||||
if (rIndex > -1) {
|
||||
this.myDictList[rIndex] = dict
|
||||
this.current.index = rIndex
|
||||
} else {
|
||||
this.myDictList.push(cloneDeep(dict))
|
||||
this.current.index = this.myDictList.length - 1
|
||||
}
|
||||
|
||||
emitter.emit(EventKey.changeDict)
|
||||
}
|
||||
emitter.emit(EventKey.changeDict)
|
||||
resolve(true)
|
||||
})
|
||||
},
|
||||
saveStatistics(statistics: DisplayStatistics) {
|
||||
if (statistics.spend > 1000 * 10) {
|
||||
delete statistics.wrongWords
|
||||
this.currentDict.statistics.push(statistics)
|
||||
}
|
||||
},
|
||||
async changeDict(dict: Dict, practiceType?: DictType, chapterIndex?: number, wordIndex?: number) {
|
||||
//TODO 保存统计
|
||||
// this.saveStatistics()
|
||||
console.log('changeDict', cloneDeep(dict), chapterIndex, wordIndex)
|
||||
if (chapterIndex === undefined) chapterIndex = dict.chapterIndex
|
||||
if (wordIndex === undefined) wordIndex = dict.wordIndex
|
||||
if (practiceType === undefined) this.current.practiceType = practiceType
|
||||
if ([DictType.collect,
|
||||
DictType.simple,
|
||||
DictType.wrong].includes(dict.type)) {
|
||||
dict.chapterIndex = 0
|
||||
dict.wordIndex = wordIndex
|
||||
dict.chapterWordNumber = dict.words.length
|
||||
dict.chapterWords = [dict.words]
|
||||
} else {
|
||||
if (dict.type === DictType.article) {
|
||||
if (chapterIndex > dict.articles.length) {
|
||||
dict.chapterIndex = 0
|
||||
dict.wordIndex = 0
|
||||
}
|
||||
} else {
|
||||
if (chapterIndex > dict.chapterWords.length) {
|
||||
dict.chapterIndex = 0
|
||||
dict.wordIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
// await checkDictHasTranslate(dict)
|
||||
let rIndex = this.myDictList.findIndex((v: Dict) => v.id === dict.id)
|
||||
if (rIndex > -1) {
|
||||
this.myDictList[rIndex] = dict
|
||||
this.current.index = rIndex
|
||||
} else {
|
||||
this.myDictList.push(cloneDeep(dict))
|
||||
this.current.index = this.myDictList.length - 1
|
||||
}
|
||||
|
||||
emitter.emit(EventKey.changeDict)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import {defineStore} from "pinia"
|
||||
import {cloneDeep, merge} from "lodash-es";
|
||||
import {DefaultShortcutKeyMap, SaveConfig} from "@/types.ts";
|
||||
import {DefaultShortcutKeyMap} from "@/types.ts";
|
||||
import {SAVE_SETTING_KEY} from "@/utils/const.ts";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting} from "@/utils";
|
||||
|
||||
export interface SettingState {
|
||||
showToolbar: boolean,
|
||||
@@ -41,92 +43,63 @@ export interface SettingState {
|
||||
load: boolean
|
||||
}
|
||||
|
||||
export const DefaultSettingState = (): SettingState => ({
|
||||
showToolbar: true,
|
||||
show: false,
|
||||
showPanel: true,
|
||||
|
||||
allSound: true,
|
||||
wordSound: true,
|
||||
wordSoundVolume: 100,
|
||||
wordSoundSpeed: 1,
|
||||
wordSoundType: 'us',
|
||||
keyboardSound: true,
|
||||
keyboardSoundVolume: 100,
|
||||
keyboardSoundFile: '机械键盘2',
|
||||
translateSound: true,
|
||||
translateSoundVolume: 100,
|
||||
effectSound: true,
|
||||
effectSoundVolume: 100,
|
||||
repeatCount: 1,
|
||||
repeatCustomCount: null,
|
||||
dictation: false,
|
||||
translate: true,
|
||||
|
||||
showNearWord: true,
|
||||
ignoreCase: true,
|
||||
allowWordTip: true,
|
||||
fontSize: {
|
||||
articleForeignFontSize: 48,
|
||||
articleTranslateFontSize: 20,
|
||||
wordForeignFontSize: 48,
|
||||
wordTranslateFontSize: 20,
|
||||
},
|
||||
waitTimeForChangeWord: 300,
|
||||
|
||||
theme: 'auto',
|
||||
collapse: false,
|
||||
chapterWordNumber: DefaultChapterWordNumber,
|
||||
shortcutKeyMap: cloneDeep(DefaultShortcutKeyMap),
|
||||
first: true,
|
||||
load: false
|
||||
})
|
||||
export const DefaultChapterWordNumber = 30
|
||||
export const useSettingStore = defineStore('setting', {
|
||||
state: (): SettingState => {
|
||||
return {
|
||||
showToolbar: true,
|
||||
show: false,
|
||||
showPanel: true,
|
||||
|
||||
allSound: true,
|
||||
wordSound: true,
|
||||
wordSoundVolume: 100,
|
||||
wordSoundSpeed: 1,
|
||||
wordSoundType: 'us',
|
||||
keyboardSound: true,
|
||||
keyboardSoundVolume: 100,
|
||||
keyboardSoundFile: '机械键盘2',
|
||||
translateSound: true,
|
||||
translateSoundVolume: 100,
|
||||
effectSound: true,
|
||||
effectSoundVolume: 100,
|
||||
repeatCount: 1,
|
||||
repeatCustomCount: null,
|
||||
dictation: false,
|
||||
translate: true,
|
||||
|
||||
showNearWord: true,
|
||||
ignoreCase: true,
|
||||
allowWordTip: true,
|
||||
fontSize: {
|
||||
articleForeignFontSize: 48,
|
||||
articleTranslateFontSize: 20,
|
||||
wordForeignFontSize: 48,
|
||||
wordTranslateFontSize: 20,
|
||||
},
|
||||
waitTimeForChangeWord: 300,
|
||||
|
||||
theme: 'auto',
|
||||
collapse: false,
|
||||
chapterWordNumber: DefaultChapterWordNumber,
|
||||
shortcutKeyMap: cloneDeep(DefaultShortcutKeyMap),
|
||||
first: true,
|
||||
load: false
|
||||
}
|
||||
return DefaultSettingState()
|
||||
},
|
||||
actions: {
|
||||
setState(obj: any) {
|
||||
// for (const [key, value] of Object.entries(obj)) {
|
||||
// this[key] = value
|
||||
// }
|
||||
//这样不会丢失watch的值的引用
|
||||
merge(this, obj)
|
||||
},
|
||||
init() {
|
||||
return new Promise(resolve => {
|
||||
const setDefaultConfig = () => {
|
||||
localStorage.setItem(SaveConfig.key, JSON.stringify({val: this.$state, version: SaveConfig.version}))
|
||||
}
|
||||
let configStr = localStorage.getItem(SaveConfig.key)
|
||||
if (configStr) {
|
||||
try {
|
||||
let obj: any = JSON.parse(configStr)
|
||||
if (!obj.version) {
|
||||
setDefaultConfig()
|
||||
} else {
|
||||
obj.val.load = false
|
||||
if (obj.version !== SaveConfig.version) {
|
||||
for (const [key, value] of Object.entries(this.shortcutKeyMap)) {
|
||||
if (obj.val.shortcutKeyMap[key]) this.shortcutKeyMap[key] = obj.val.shortcutKeyMap[key]
|
||||
}
|
||||
delete obj.val.shortcutKeyMap
|
||||
|
||||
for (const [key, value] of Object.entries(this.fontSize)) {
|
||||
if (obj.val.fontSize[key]) this.fontSize[key] = obj.val.fontSize[key]
|
||||
}
|
||||
delete obj.val.fontSize
|
||||
|
||||
this.setState(obj.val)
|
||||
} else {
|
||||
this.setState(obj.val)
|
||||
}
|
||||
localStorage.setItem(SaveConfig.key, JSON.stringify({val: this.$state, version: SaveConfig.version}))
|
||||
}
|
||||
} catch (e) {
|
||||
setDefaultConfig()
|
||||
}
|
||||
}
|
||||
let configStr = localStorage.getItem(SAVE_SETTING_KEY.key)
|
||||
if (!configStr) configStr = localStorage.getItem(SAVE_SETTING_KEY.oldKey)
|
||||
let data = checkAndUpgradeSaveSetting(configStr)
|
||||
this.setState(data)
|
||||
localStorage.setItem(SAVE_SETTING_KEY.key, JSON.stringify({val: this.$state, version: SAVE_SETTING_KEY.version}))
|
||||
this.load = true
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
11
src/types.ts
11
src/types.ts
@@ -1,7 +1,5 @@
|
||||
import bookFlag from "@/assets/img/flags/book.png";
|
||||
import enFlag from "@/assets/img/flags/en.png";
|
||||
import jpFlag from "@/assets/img/flags/ja.png";
|
||||
import deFlag from "./assets/img/flags/de.png";
|
||||
import codeFlag from "@/assets/img/flags/code.png";
|
||||
import myFlag from "@/assets/img/flags/my.png";
|
||||
import {DefaultChapterWordNumber} from "@/stores/setting.ts";
|
||||
@@ -144,15 +142,6 @@ export const ShortcutKeyMap = {
|
||||
Collect: 'Enter',
|
||||
}
|
||||
|
||||
export const SaveDict = {
|
||||
key: 'typing-word-dict',
|
||||
version: 3
|
||||
}
|
||||
export const SaveConfig = {
|
||||
key: 'typing-word-config',
|
||||
version: 8
|
||||
}
|
||||
|
||||
export enum ShortcutKey {
|
||||
ShowWord = 'ShowWord',
|
||||
EditArticle = 'EditArticle',
|
||||
|
||||
@@ -4,4 +4,20 @@ export const SoundFileOptions = [
|
||||
{value: '机械键盘2', label: '机械键盘2'},
|
||||
{value: '老式机械键盘', label: '老式机械键盘'},
|
||||
{value: '笔记本键盘', label: '笔记本键盘'},
|
||||
]
|
||||
]
|
||||
|
||||
export const APP_NAME = 'Typing Word'
|
||||
|
||||
export const SAVE_DICT_KEY = {
|
||||
key: 'typing-word-dict',
|
||||
version: 3
|
||||
}
|
||||
export const SAVE_SETTING_KEY = {
|
||||
key: 'typing-word-setting',
|
||||
oldKey: 'typing-word-config',
|
||||
version: 8
|
||||
}
|
||||
export const EXPORT_DATA_KEY = {
|
||||
key: 'typing-word-export',
|
||||
version: 1
|
||||
}
|
||||
@@ -1,7 +1,124 @@
|
||||
import localforage from "localforage";
|
||||
import {SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
|
||||
import {BaseState, DefaultBaseState} from "@/stores/base.ts";
|
||||
import {DefaultSettingState, SettingState} from "@/stores/setting.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {Dict, DictType} from "@/types.ts";
|
||||
|
||||
export function getRandom(a: number, b: number): number {
|
||||
return Math.random() * (b - a) + a;
|
||||
return Math.random() * (b - a) + a;
|
||||
}
|
||||
|
||||
export function no(){
|
||||
ElMessage.warning('未现实')
|
||||
export function no() {
|
||||
ElMessage.warning('未现实')
|
||||
}
|
||||
|
||||
export function checkAndUpgradeSaveDict(val: string) {
|
||||
// console.log(configStr)
|
||||
// console.log('s', new Blob([val]).size)
|
||||
// val = ''
|
||||
if (val) {
|
||||
try {
|
||||
let data
|
||||
if (typeof val === 'string') {
|
||||
data = JSON.parse(val)
|
||||
} else {
|
||||
data = val
|
||||
}
|
||||
let state: BaseState = data.val
|
||||
if (typeof state !== 'object') {
|
||||
return {}
|
||||
}
|
||||
if (!data.version) {
|
||||
return {}
|
||||
}
|
||||
let version = Number(data.version)
|
||||
// console.log('state', state)
|
||||
let defaultBaseState = DefaultBaseState()
|
||||
if (version === SAVE_DICT_KEY.version) {
|
||||
//防止人为删除数据,导致数据不完整报错
|
||||
for (const [key, value] of Object.entries(defaultBaseState)) {
|
||||
if (state[key] !== undefined) defaultBaseState[key] = state[key]
|
||||
}
|
||||
return defaultBaseState
|
||||
} else {
|
||||
//防止人为删除数据,导致数据不完整报错
|
||||
for (const [key, value] of Object.entries(defaultBaseState)) {
|
||||
if (state[key] !== undefined) defaultBaseState[key] = state[key]
|
||||
}
|
||||
return defaultBaseState
|
||||
}
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function checkAndUpgradeSaveSetting(val: string) {
|
||||
// console.log(configStr)
|
||||
// console.log('s', new Blob([val]).size)
|
||||
// val = ''
|
||||
if (val) {
|
||||
try {
|
||||
let data
|
||||
if (typeof val === 'string') {
|
||||
data = JSON.parse(val)
|
||||
} else {
|
||||
data = val
|
||||
}
|
||||
let state: SettingState = data.val
|
||||
if (typeof state !== 'object') {
|
||||
return {}
|
||||
}
|
||||
if (!data.version) {
|
||||
return {}
|
||||
}
|
||||
let version = Number(data.version)
|
||||
|
||||
let defaultSettingState = DefaultSettingState()
|
||||
if (version === SAVE_SETTING_KEY.version) {
|
||||
//防止人为删除数据,导致数据不完整报错
|
||||
for (const [key, value] of Object.entries(defaultSettingState)) {
|
||||
if (state[key] !== undefined) defaultSettingState[key] = state[key]
|
||||
}
|
||||
return defaultSettingState
|
||||
} else {
|
||||
//为了保持永远是最新的快捷键选项列表,但保留住用户的自定义设置,去掉无效的快捷键选项
|
||||
//例: 2版本,可能有快捷键A。3版本没有了
|
||||
for (const [key, value] of Object.entries(defaultSettingState.shortcutKeyMap)) {
|
||||
if (state.shortcutKeyMap[key] !== undefined) defaultSettingState.shortcutKeyMap[key] = state.shortcutKeyMap[key]
|
||||
}
|
||||
delete state.shortcutKeyMap
|
||||
|
||||
for (const [key, value] of Object.entries(defaultSettingState)) {
|
||||
if (state[key] !== undefined) defaultSettingState[key] = state[key]
|
||||
}
|
||||
return defaultSettingState
|
||||
}
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
//筛选未自定义的词典,未自定义的词典不需要保存单词,用的时候再下载
|
||||
export function shakeCommonDict(n: BaseState): BaseState {
|
||||
let data: BaseState = cloneDeep(n)
|
||||
data.myDictList.map((v: Dict) => {
|
||||
if (v.isCustom) {
|
||||
if (v.type === DictType.article) {
|
||||
v.articles.map(s => {
|
||||
delete s.sections
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (v.type === DictType.word) v.originWords = []
|
||||
if (v.type === DictType.article) v.articles = []
|
||||
v.words = []
|
||||
v.chapterWords = []
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
Reference in New Issue
Block a user