feat: save

This commit is contained in:
zyronon
2025-10-25 23:47:32 +08:00
parent 9fe42da643
commit f573016cd6
6 changed files with 4474 additions and 5289 deletions

1
components.d.ts vendored
View File

@@ -42,7 +42,6 @@ declare module 'vue' {
IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default']
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
IconFluentArrowSort20Regular: typeof import('~icons/fluent/arrow-sort20-regular')['default']

9502
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -151,7 +151,9 @@ function getShortcutKeyName(key: string): string {
'ToggleConciseMode': '切换简洁模式',
'TogglePanel': '切换面板',
'RandomWrite': '随机默写',
'NextRandomWrite': '继续随机默写'
'NextRandomWrite': '继续随机默写',
'KnowWord': '认识单词',
'UnknownWord': '不认识单词',
}
return shortcutKeyNameMap[key] || key

View File

@@ -1,17 +1,17 @@
<script setup lang="ts">
import { onMounted, provide, ref, watch } from "vue";
import {onMounted, provide, ref, watch} from "vue";
import Statistics from "@/pages/word/Statistics.vue";
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { Dict, PracticeData, PracticeMode, ShortcutKey, TaskWords, Word } from "@/types/types.ts";
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {Dict, PracticeData, PracticeMode, ShortcutKey, TaskWords, Word} from "@/types/types.ts";
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
import useTheme from "@/hooks/theme.ts";
import { getCurrentStudyWord, useWordOptions } from "@/hooks/dict.ts";
import { _getDictDataByUrl, cloneDeep, resourceWrap, shuffle, sleep } from "@/utils";
import { useRoute, useRouter } from "vue-router";
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
import {_getDictDataByUrl, cloneDeep, resourceWrap, shuffle, sleep} from "@/utils";
import {useRoute, useRouter} from "vue-router";
import Footer from "@/pages/word/components/Footer.vue";
import Panel from "@/components/Panel.vue";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -19,16 +19,16 @@ import Tooltip from "@/components/base/Tooltip.vue";
import WordList from "@/components/list/WordList.vue";
import TypeWord from "@/pages/word/components/TypeWord.vue";
import Empty from "@/components/Empty.vue";
import { useBaseStore } from "@/stores/base.ts";
import { usePracticeStore } from "@/stores/practice.ts";
import {useBaseStore} from "@/stores/base.ts";
import {usePracticeStore} from "@/stores/practice.ts";
import Toast from '@/components/base/toast/Toast.ts'
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
import ConflictNotice from "@/components/ConflictNotice.vue";
import PracticeLayout from "@/components/PracticeLayout.vue";
import { DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
import { set } from "idb-keyval";
import { ToastInstance } from "@/components/base/toast/type.ts";
import {DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
import {set} from "idb-keyval";
import {ToastInstance} from "@/components/base/toast/type.ts";
const {
isWordCollect,
@@ -50,13 +50,14 @@ let loading = $ref(false)
let taskWords = $ref<TaskWords>({
new: [],
review: [],
write: []
write: [],
})
let data = $ref<PracticeData>({
index: 0,
words: [],
wrongWords: [],
excludeWords: [],
})
let isTypingWrongWord = ref(false)
@@ -127,12 +128,12 @@ function initData(initVal: TaskWords, init: boolean = false) {
taskWords = initVal
if (taskWords.new.length === 0) {
if (taskWords.review.length) {
readMode()
practiceMode.value = PracticeMode.Identify
statStore.step = 2
data.words = taskWords.review
} else {
if (taskWords.write.length) {
writeMode()
practiceMode.value = PracticeMode.Identify
data.words = taskWords.write
statStore.step = 4
} else {
@@ -141,12 +142,13 @@ function initData(initVal: TaskWords, init: boolean = false) {
}
}
} else {
readMode()
practiceMode.value = PracticeMode.FollowWrite
data.words = taskWords.new
statStore.step = 0
}
data.index = 0
data.wrongWords = []
data.excludeWords = []
allWrongWords.clear()
statStore.startDate = Date.now()
statStore.inputWordNumber = 0
@@ -160,7 +162,7 @@ function initData(initVal: TaskWords, init: boolean = false) {
}
}
const word = $computed(() => {
const word = $computed<Word>(() => {
return data.words[data.index] ?? getDefaultWord()
})
const prevWord: Word = $computed(() => {
@@ -170,19 +172,27 @@ const nextWord: Word = $computed(() => {
return data.words?.[data.index + 1] ?? undefined
})
function readMode() {
practiceMode.value = PracticeMode.FollowWrite
settingStore.dictation = false
settingStore.translate = true
}
function writeMode() {
practiceMode.value = PracticeMode.Dictation
settingStore.dictation = true
settingStore.translate = false
}
watch(practiceMode, (n) => {
switch (n) {
case PracticeMode.Spell:
case PracticeMode.Dictation:
case PracticeMode.Listen:
settingStore.dictation = true;
settingStore.translate = false;
break
case PracticeMode.FollowWrite:
settingStore.dictation = false;
settingStore.translate = true;
break
case PracticeMode.Identify:
settingStore.dictation = false;
settingStore.translate = false;
break
}
})
function wordLoop() {
return data.index++
let d = Math.floor(data.index / 6) - 1
if (data.index > 0 && data.index % 6 === (d < 0 ? 0 : d)) {
if (practiceMode.value === PracticeMode.FollowWrite) {
@@ -200,7 +210,6 @@ function wordLoop() {
let toastInstance: ToastInstance = null
async function next(isTyping: boolean = true) {
debugger
if (isTyping) {
statStore.inputWordNumber++
}
@@ -262,7 +271,7 @@ async function next(isTyping: boolean = true) {
toastInstance?.close()
toastInstance = Toast.info('输入完成后按空格键切换下一个', {duration: 10000})
practiceMode.value = PracticeMode.Dictation
data.words = shuffle(taskWords.new)
data.words = shuffle(taskWords.write)
data.index = 0
}
@@ -273,7 +282,7 @@ async function next(isTyping: boolean = true) {
toastInstance?.close()
toastInstance = Toast.info('输入完成后按空格键切换下一个', {duration: 10000})
practiceMode.value = PracticeMode.Listen
data.words = shuffle(taskWords.review)
data.words = shuffle(taskWords.write)
data.index = 0
}
@@ -300,7 +309,7 @@ async function next(isTyping: boolean = true) {
toastInstance?.close()
toastInstance = Toast.info('输入完成后按空格键切换下一个', {duration: 10000})
practiceMode.value = PracticeMode.Dictation
data.words = shuffle(taskWords.new)
data.words = shuffle(taskWords.review)
data.index = 0
}
@@ -355,43 +364,10 @@ async function next(isTyping: boolean = true) {
} else {
if (statStore.step === 0) {
wordLoop()
} else if (statStore.step === 1) {
if (isTypingWrongWord.value) wordLoop()
else data.index++
} else if (statStore.step === 2) {
if (isTypingWrongWord.value) wordLoop()
else data.index++
} else if (statStore.step === 3) {
if (isTypingWrongWord.value) wordLoop()
else data.index++
} else if (statStore.step === 4) {
if (isTypingWrongWord.value) wordLoop()
else data.index++
} else if (statStore.step === 5) {
if (isTypingWrongWord.value) wordLoop()
else data.index++
} else if (statStore.step === 6) {
if (isTypingWrongWord.value) wordLoop()
else data.index++
} else if (statStore.step === 7) {
if (isTypingWrongWord.value) wordLoop()
else data.index++
} else if (statStore.step === 8) {
} else {
if (isTypingWrongWord.value) wordLoop()
else data.index++
}
// if ([0, 2].includes(statStore.step) || isTypingWrongWord.value) {
// wordLoop()
// } else {
// if (settingStore.dictation && !settingStore.translate) {
// readMode()
// } else {
// writeMode()
// await sleep(100)
// data.index++
// }
// // await sleep(2000)
// }
}
}
savePracticeData()
@@ -489,12 +465,16 @@ function play() {
function toggleWordSimpleWrapper() {
if (!isWordSimple(word)) {
toggleWordSimple(word)
//延迟一下,不知道为什么不延迟会导致当前条目不自动定位到列表中间
setTimeout(() => next(false))
} else {
toggleWordSimple(word)
}
let rIndex = data.excludeWords.findIndex(v => v === word.word)
if (rIndex > -1) {
data.excludeWords.splice(rIndex, 1)
} else {
data.excludeWords.push(word.word)
}
toggleWordSimple(word)
}
function toggleTranslate() {

View File

@@ -3,7 +3,7 @@ import {PracticeMode, ShortcutKey, Word} from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useSettingStore} from "@/stores/setting.ts";
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {inject, onMounted, onUnmounted, Ref, watch} from "vue";
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
import {usePracticeStore} from "@/stores/practice.ts";
@@ -12,6 +12,7 @@ import {_nextTick, last, sleep} from "@/utils";
import BaseButton from "@/components/BaseButton.vue";
import Space from "@/pages/article/components/Space.vue";
import Toast from "@/components/base/toast/Toast.ts";
import Tooltip from "@/components/base/Tooltip.vue";
interface IProps {
word: Word,
@@ -69,7 +70,7 @@ watch(() => props.word, reset, {deep: true})
function reset() {
wrong = input = ''
wordRepeatCount = 0
showWordResult = inputLock = false
showWordResult = inputLock = false
if (settingStore.wordSound) {
if (practiceMode.value !== PracticeMode.Dictation) {
volumeIconRef?.play(400, true)
@@ -121,6 +122,28 @@ const right = $computed(() => {
}
})
function know(e) {
if (practiceMode.value === PracticeMode.Identify) {
if (!showWordResult) {
inputLock = showWordResult = true
input = props.word.word
return
}
}
onTyping(e)
}
function unknown(e) {
if (practiceMode.value === PracticeMode.Identify) {
if (!showWordResult) {
showWordResult = true
emit('wrong')
return
}
}
onTyping(e)
}
async function onTyping(e: KeyboardEvent) {
debugger
let word = props.word.word
@@ -131,10 +154,10 @@ async function onTyping(e: KeyboardEvent) {
emit('complete')
} else {
//当显示单词时,提示用户正确按键
if (showFullWord) {
if (showWordResult) {
pressNumber++
if (pressNumber >= 3) {
Toast.info(right ? '请按空格键切换' : '请按删除键重新输入')
Toast.info(right ? '请按空格键切换' : '请按删除键重新输入', {duration: 2000})
pressNumber = 0
}
}
@@ -177,7 +200,7 @@ async function onTyping(e: KeyboardEvent) {
} else {
let right = false
if (settingStore.ignoreCase) {
right = letter.toLowerCase() === props.word.word[input.length].toLowerCase()
right = letter.toLowerCase() === word[input.length].toLowerCase()
} else {
right = letter === props.word.word[input.length]
}
@@ -196,21 +219,26 @@ async function onTyping(e: KeyboardEvent) {
}
// 更新当前单词信息
updateCurrentWordInfo();
if (input.toLowerCase() === props.word.word.toLowerCase()) {
//不需要把inputLock设为false输入完成不能再输入了只能删除删除会打开锁
if (input.toLowerCase() === word.toLowerCase()) {
playCorrect()
//不需要把inputLock设为false输入完成不能再输入了只能删除删除会打开锁
if (settingStore.autoNextWord) {
if (settingStore.repeatCount == 100) {
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
if ([PracticeMode.Listen, PracticeMode.Identify].includes(practiceMode.value) && !showWordResult) {
showWordResult = true
}
if ([PracticeMode.Free, PracticeMode.FollowWrite, PracticeMode.Spell].includes(practiceMode.value)) {
if (settingStore.autoNextWord) {
if (settingStore.repeatCount == 100) {
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
} else {
repeat()
}
} else {
repeat()
}
} else {
if (settingStore.repeatCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
} else {
repeat()
if (settingStore.repeatCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
} else {
repeat()
}
}
}
}
@@ -224,7 +252,6 @@ function del() {
playKeyboardAudio()
inputLock = false
if (showWordResult) {
practiceMode.value = PracticeMode.Dictation
input = ''
showWordResult = false
} else {
@@ -331,18 +358,18 @@ function checkCursorPosition() {
},)
}
const word = $computed(() => {
return {...props.word, ...{input: 'abc'}}
})
useEvents([
[ShortcutKey.KnowWord, know],
[ShortcutKey.UnknownWord, unknown],
])
</script>
<template>
<div class="typing-word" ref="typingWordRef" v-if="props.word.word.length">
<div class="typing-word" ref="typingWordRef" v-if="word.word.length">
<div class="flex flex-col items-center">
<div class="flex gap-1 mt-26">
<div class="phonetic"
:class="((settingStore.dictation || [PracticeMode.Spell,PracticeMode.Listen,PracticeMode.Dictation].includes(practiceMode)) && !showFullWord && !showWordResult) && 'word-shadow'"
:class="!(!settingStore.dictation || showFullWord || showWordResult) && 'word-shadow'"
v-if="settingStore.soundType === 'us' && word.phonetic0">[{{ word.phonetic0 }}]
</div>
<div class="phonetic"
@@ -350,8 +377,8 @@ const word = $computed(() => {
v-if="settingStore.soundType === 'uk' && word.phonetic1">[{{ word.phonetic1 }}]
</div>
<VolumeIcon
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
</div>
<div class="word my-1"
@@ -363,12 +390,12 @@ const word = $computed(() => {
<div v-if="practiceMode === PracticeMode.Dictation">
<div class="letter text-align-center w-full inline-block"
v-opacity="showWordResult || showFullWord">
{{ props.word.word }}
{{ word.word }}
</div>
<div
class="mt-2 w-120 dictation"
:style="{minHeight: settingStore.fontSize.wordForeignFontSize +'px'}"
:class="showWordResult ? (right ? 'right' : 'wrong') : ''">
class="mt-2 w-120 dictation"
:style="{minHeight: settingStore.fontSize.wordForeignFontSize +'px'}"
:class="showWordResult ? (right ? 'right' : 'wrong') : ''">
<template v-for="i in input">
<span class="l" v-if="i !== ' '">{{ i }}</span>
<Space class="l" v-else :is-wrong="showWordResult ? (!right) : false" :is-wait="!showWordResult"/>
@@ -396,13 +423,19 @@ const word = $computed(() => {
</template>
</div>
<div class="mt-4" v-if="practiceMode === PracticeMode.Identify && !showWordResult">
<BaseButton size="large" @click="showWordResult = true;emit('wrong')">不认识</BaseButton>
<BaseButton size="large" @click="emit('complete')">我认识</BaseButton>
<div class="mt-4 flex gap-4" v-if="practiceMode === PracticeMode.Identify && !showWordResult">
<BaseButton
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.KnowWord]})`"
size="large" @click="know">我认识
</BaseButton>
<BaseButton
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.UnknownWord]})`"
size="large" @click="unknown">不认识
</BaseButton>
</div>
<div class="translate anim flex flex-col gap-2 my-3"
v-opacity="(settingStore.translate && ![PracticeMode.Listen,PracticeMode.Identify].includes(practiceMode)) || showWordResult || showFullWord"
<div class="translate flex flex-col gap-2 my-3"
v-opacity="settingStore.translate || ![PracticeMode.Listen,PracticeMode.Identify].includes(practiceMode) || showWordResult || showFullWord"
:style="{
fontSize: settingStore.fontSize.wordTranslateFontSize +'px',
}"
@@ -410,8 +443,8 @@ const word = $computed(() => {
<div class="flex" v-for="(v,i) in word.trans">
<div class="shrink-0" :class="v.pos ? 'w-12 en-article-family' : '-ml-3'">{{ v.pos }}</div>
<span
v-if="(settingStore.dictation || [PracticeMode.Spell,PracticeMode.Listen].includes(practiceMode)) && !showFullWord"
v-html="hideWordInTranslation(v.cn, word.word)"></span>
v-if="([PracticeMode.Listen,PracticeMode.Identify].includes(practiceMode) || settingStore.dictation) && !(showWordResult || showFullWord)"
v-html="hideWordInTranslation(v.cn, word.word)"></span>
<span v-else>{{ v.cn }}</span>
</div>
</div>
@@ -423,26 +456,27 @@ const word = $computed(() => {
<div class="flex flex-col gap-3">
<div class="sentence" v-for="item in word.sentences">
<SentenceHightLightWord class="text-xl" :text="item.c" :word="word.word"
:dictation="((settingStore.dictation || [PracticeMode.Spell,PracticeMode.Listen].includes(practiceMode)) && !showFullWord)"/>
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"/>
<div class="text-base anim"
v-opacity="(settingStore.translate && ![PracticeMode.Listen].includes(practiceMode)) || showFullWord">
v-opacity="settingStore.translate || showFullWord || showWordResult">
{{ item.cn }}
</div>
</div>
</div>
<div class="line-white my-2 mb-5 anim" v-opacity="settingStore.translate || showFullWord"></div>
<div class="line-white my-2 mb-5 anim"
v-opacity="settingStore.translate || showFullWord || showWordResult"></div>
</template>
<div class="anim"
v-opacity="(settingStore.translate && !(settingStore.dictation || [PracticeMode.Spell].includes(practiceMode))) || showFullWord ">
v-opacity="settingStore.translate || showFullWord || showWordResult">
<template v-if="word?.phrases?.length">
<div class="flex">
<div class="label">短语</div>
<div class="flex flex-col">
<div class="flex items-center gap-4" v-for="item in word.phrases">
<SentenceHightLightWord class="en" :text="item.c" :word="word.word"
:dictation="((settingStore.dictation || [PracticeMode.Spell,PracticeMode.Listen].includes(practiceMode)) && !showFullWord)"/>
<div class="cn anim" v-opacity="settingStore.translate">{{ item.cn }}</div>
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"/>
<div class="cn anim" v-opacity="settingStore.translate || showFullWord || showWordResult">{{ item.cn }}</div>
</div>
</div>
</div>
@@ -510,7 +544,7 @@ const word = $computed(() => {
<style scoped lang="scss">
.dictation {
border-bottom: 1px solid black;
border-bottom: 2px solid black;
}
.typing-word {

View File

@@ -117,7 +117,9 @@ export enum ShortcutKey {
ToggleConciseMode = 'ToggleConciseMode',
TogglePanel = 'TogglePanel',
RandomWrite = 'RandomWrite',
NextRandomWrite = 'NextRandomWrite'
NextRandomWrite = 'NextRandomWrite',
KnowWord = 'KnowWord',
UnknownWord = 'UnknownWord',
}
export const DefaultShortcutKeyMap = {
@@ -139,6 +141,8 @@ export const DefaultShortcutKeyMap = {
[ShortcutKey.TogglePanel]: 'Ctrl+L',
[ShortcutKey.RandomWrite]: 'Ctrl+R',
[ShortcutKey.NextRandomWrite]: 'Ctrl+Shift+R',
[ShortcutKey.KnowWord]: '1',
[ShortcutKey.UnknownWord]: '2',
}
export enum TranslateEngine {
@@ -189,6 +193,7 @@ export interface PracticeData {
index: number,
words: any[],
wrongWords: any[],
excludeWords: any[],
}
export interface TaskWords {
@@ -211,7 +216,6 @@ export enum PracticeArticleWordType {
}
export enum PracticeMode {
Free,
FollowWrite,//跟写
Spell,
Identify,