Merge pull request #11 from zyronon/dev

Dev
This commit is contained in:
Zyronon
2023-12-08 01:29:28 +08:00
committed by GitHub
16 changed files with 1179 additions and 849 deletions

1
components.d.ts vendored
View File

@@ -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']

View File

@@ -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">

View File

@@ -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>

View File

@@ -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
View 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>

View File

@@ -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";

View File

@@ -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>

View File

@@ -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

View File

@@ -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
}
})
}

View File

@@ -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)"

View File

@@ -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";

View File

@@ -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)
}
},
})

View File

@@ -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)
})

View File

@@ -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',

View File

@@ -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
}

View File

@@ -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
}