This commit is contained in:
Zyronon
2025-12-24 02:10:24 +08:00
parent 560546c3bc
commit 7a3475cee1
10 changed files with 537 additions and 506 deletions

View File

@@ -19,7 +19,7 @@ export const ENV = Object.assign(map['DEV'], common)
export let AppEnv = {
TOKEN: localStorage.getItem('token') ?? '',
IS_OFFICIAL: true,
IS_OFFICIAL: false,
IS_LOGIN: false,
CAN_REQUEST: false,
}
@@ -92,6 +92,7 @@ export const TourConfig = {
total: 7,
}
export const IS_DEV = import.meta.env.MODE === 'development'
export const LIB_JS_URL = {
SHEPHERD:
import.meta.env.MODE === 'development'

View File

@@ -144,12 +144,12 @@ export function getCurrentStudyWord(): TaskWords {
}
}
//如果是自由模式那么统统设置到new字段里面去
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
data.new = data.new.length ? data.new : data.review
data.review = []
return data
}
// //如果是自由模式那么统统设置到new字段里面去
// if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
// data.new = data.new.length ? data.new : data.review
// data.review = []
// return data
// }
// 上上次更早的单词
//默认只取start之前的单词

View File

@@ -12,6 +12,7 @@ import {
TaskWords,
Word,
WordPracticeMode,
WordPracticeStage,
WordPracticeType,
} from '@/types/types.ts'
import {
@@ -45,7 +46,14 @@ import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import ConflictNotice from '@/components/ConflictNotice.vue'
import PracticeLayout from '@/components/PracticeLayout.vue'
import { AppEnv, DICT_LIST, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from '@/config/env.ts'
import {
AppEnv,
DICT_LIST,
IS_DEV,
LIB_JS_URL,
PracticeSaveWordKey,
TourConfig,
} from '@/config/env.ts'
import { ToastInstance } from '@/components/base/toast/type.ts'
import { watchOnce } from '@vueuse/core'
import { setUserDictProp } from '@/apis'
@@ -115,11 +123,11 @@ async function loadDict() {
}
watch(
() => store.load,
n => {
if (n && loading) loadDict()
},
{ immediate: true }
() => store.load,
n => {
if (n && loading) loadDict()
},
{ immediate: true }
)
onMounted(() => {
@@ -144,50 +152,50 @@ onUnmounted(() => {
})
watchOnce(
() => data.words.length,
(newVal, oldVal) => {
//如果是从无值变有值,代表是开始
if (!oldVal && newVal) {
_nextTick(async () => {
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD)
const tour = new Shepherd.Tour(TourConfig)
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1')
})
tour.addStep({
id: 'step5',
text: '这里可以练习拼写单词,只需要按下键盘上对应的按键即可,没有输入框!',
attachTo: { element: '#word', on: 'bottom' },
buttons: [
{
text: `下一步5/${TourConfig.total}`,
action: tour.next,
},
],
})
() => data.words.length,
(newVal, oldVal) => {
//如果是从无值变有值,代表是开始
if (!oldVal && newVal) {
_nextTick(async () => {
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD)
const tour = new Shepherd.Tour(TourConfig)
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1')
})
tour.addStep({
id: 'step5',
text: '这里可以练习拼写单词,只需要按下键盘上对应的按键即可,没有输入框!',
attachTo: { element: '#word', on: 'bottom' },
buttons: [
{
text: `下一步5/${TourConfig.total}`,
action: tour.next,
},
],
})
tour.addStep({
id: 'step6',
text: '这里是文章练习',
attachTo: { element: '#article', on: 'top' },
buttons: [
{
text: `下一步6/${TourConfig.total}`,
action() {
tour.next()
router.push('/articles')
},
tour.addStep({
id: 'step6',
text: '这里是文章练习',
attachTo: { element: '#article', on: 'top' },
buttons: [
{
text: `下一步6/${TourConfig.total}`,
action() {
tour.next()
router.push('/articles')
},
],
})
},
],
})
const r = localStorage.getItem('tour-guide')
if (settingStore.first && !r && !isMobile()) {
tour.start()
}
}, 500)
}
const r = localStorage.getItem('tour-guide')
if (settingStore.first && !r && !isMobile()) {
tour.start()
}
}, 500)
}
}
)
useStartKeyboardEventListener()
@@ -197,6 +205,10 @@ function initData(initVal: TaskWords, init: boolean = false) {
let d = localStorage.getItem(PracticeSaveWordKey.key)
if (d && init) {
try {
//todo 记得删除
if (IS_DEV) {
throw new Error('开发环境,抛出错误跳过缓存')
}
let obj = JSON.parse(d)
let s = obj.val
taskWords = Object.assign(taskWords, s.taskWords)
@@ -211,71 +223,41 @@ function initData(initVal: TaskWords, init: boolean = false) {
// taskWords = initVal
//不能直接赋值,会导致 inject 的数据为默认值
taskWords = Object.assign(taskWords, initVal)
// 检查是否为独立模式
const isStandaloneMode = settingStore.wordPracticeMode >= WordPracticeMode.DictationOnly
if (isStandaloneMode) {
// 独立模式:根据模式设置对应的练习类型
switch (settingStore.wordPracticeMode) {
case WordPracticeMode.DictationOnly:
settingStore.wordPracticeType = WordPracticeType.Dictation
break
case WordPracticeMode.ListenOnly:
settingStore.wordPracticeType = WordPracticeType.Listen
break
case WordPracticeMode.IdentifyOnly:
settingStore.wordPracticeType = WordPracticeType.Identify
break
case WordPracticeMode.FollowWriteOnly:
settingStore.wordPracticeType = WordPracticeType.FollowWrite
break
}
// 独立模式:按优先级选择起始单词列表(新词 -> 复习上次 -> 复习之前)
let selectedWords: Word[] = []
if (taskWords.new.length > 0) {
currentWordListStage = 'new'
selectedWords = taskWords.new
} else if (taskWords.review.length > 0) {
currentWordListStage = 'review'
selectedWords = taskWords.review
} else if (taskWords.write.length > 0) {
currentWordListStage = 'write'
selectedWords = taskWords.write
} else {
Toast.warning('没有可学习的单词!')
router.push('/word')
return
}
data.words = selectedWords
statStore.step = 0 // 独立模式不使用 step 逻辑
statStore.total = taskWords.review.length + taskWords.new.length + taskWords.write.length
statStore.newWordNumber = taskWords.new.length
statStore.reviewWordNumber = taskWords.review.length
statStore.writeWordNumber = taskWords.write.length
} else if (taskWords.shuffle.length === 0) {
// 原有的智能模式逻辑
if (taskWords.shuffle.length === 0) {
if (taskWords.new.length === 0) {
if (taskWords.review.length) {
settingStore.wordPracticeType = WordPracticeType.Identify
statStore.step = 3
statStore.stage = WordPracticeStage.IdentifyReview
data.words = taskWords.review
} else {
if (taskWords.write.length) {
settingStore.wordPracticeType = WordPracticeType.Identify
data.words = taskWords.write
statStore.step = 6
statStore.stage = WordPracticeStage.IdentifyReviewAll
} else {
Toast.warning('没有可学习的单词!')
router.push('/word')
}
}
} else {
settingStore.wordPracticeType = WordPracticeType.FollowWrite
data.words = taskWords.new
statStore.step = 0
if (settingStore.wordPracticeMode === WordPracticeMode.System) {
statStore.stage = WordPracticeStage.FollowWriteNewWord
} else if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
statStore.stage = WordPracticeStage.FollowWriteNewWord
} else if (settingStore.wordPracticeMode === WordPracticeMode.IdentifyOnly) {
statStore.stage = WordPracticeStage.IdentifyNewWord
} else if (settingStore.wordPracticeMode === WordPracticeMode.DictationOnly) {
statStore.stage = WordPracticeStage.DictationNewWord
} else if (settingStore.wordPracticeMode === WordPracticeMode.ListenOnly) {
statStore.stage = WordPracticeStage.ListenNewWord
} else if (settingStore.wordPracticeMode === WordPracticeMode.FollowWriteOnly) {
statStore.stage = WordPracticeStage.FollowWriteNewWord
}
}
statStore.total = taskWords.review.length + taskWords.new.length + taskWords.write.length
statStore.newWordNumber = taskWords.new.length
@@ -285,6 +267,7 @@ function initData(initVal: TaskWords, init: boolean = false) {
settingStore.wordPracticeType = WordPracticeType.Dictation
data.words = taskWords.shuffle
statStore.step = 10
statStore.stage = WordPracticeStage.Shuffle
statStore.total = taskWords.shuffle.length
statStore.newWordNumber = 0
statStore.reviewWordNumber = 0
@@ -321,31 +304,34 @@ const nextWord: Word = $computed(() => {
})
watch(
() => settingStore.wordPracticeType,
n => {
// Free 模式不自动设置System 模式和独立模式都需要设置
if (settingStore.wordPracticeMode === WordPracticeMode.Free) return
switch (n) {
case WordPracticeType.Spell:
case WordPracticeType.Dictation:
settingStore.dictation = true
settingStore.translate = true
break
case WordPracticeType.Listen:
settingStore.dictation = true
settingStore.translate = false
break
case WordPracticeType.FollowWrite:
settingStore.dictation = false
settingStore.translate = true
break
case WordPracticeType.Identify:
settingStore.dictation = false
settingStore.translate = false
break
}
},
{ immediate: true }
() => settingStore.wordPracticeMode,
n => {
// Free 模式不自动设置System 模式和独立模式都需要设置
if (settingStore.wordPracticeMode === WordPracticeMode.Free) return
switch (n) {
case WordPracticeMode.DictationOnly:
settingStore.dictation = true
settingStore.translate = true
settingStore.wordPracticeType = WordPracticeType.Dictation
break
case WordPracticeMode.ListenOnly:
settingStore.dictation = true
settingStore.translate = false
settingStore.wordPracticeType = WordPracticeType.Listen
break
case WordPracticeMode.FollowWriteOnly:
settingStore.dictation = false
settingStore.translate = true
settingStore.wordPracticeType = WordPracticeType.FollowWrite
break
case WordPracticeMode.IdentifyOnly:
settingStore.dictation = false
settingStore.translate = false
settingStore.wordPracticeType = WordPracticeType.Identify
break
}
},
{ immediate: true }
)
const groupSize = 7
@@ -389,11 +375,29 @@ function goNextStep(originList, mode, msg) {
}
}
function nextStage(originList, log, nextStage) {
//每次都判断,因为每次都可能新增已掌握的单词
let list = originList.filter(v => !data.excludeWords.includes(v.word))
console.log(log)
if (list.length) {
if (toastInstance) toastInstance.close()
toastInstance = Toast.info('输入完成后按空格键切换下一个', { duration: 5000 })
data.words = list
data.index = 0
statStore.stage = nextStage
} else {
console.log(log + ':无单词略过')
statStore.stage = nextStage
next()
}
}
async function next(isTyping: boolean = true) {
debugger
if (isTyping) statStore.inputWordNumber++
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
if (data.index === data.words.length - 1) {
data.wrongWords = data.wrongWords.filter(v => (!data.excludeWords.includes(v.word)))
data.wrongWords = data.wrongWords.filter(v => !data.excludeWords.includes(v.word))
if (data.wrongWords.length) {
isTypingWrongWord.value = true
settingStore.wordPracticeType = WordPracticeType.FollowWrite
@@ -410,62 +414,9 @@ async function next(isTyping: boolean = true) {
} else {
data.index++
}
} else if (settingStore.wordPracticeMode >= WordPracticeMode.DictationOnly) {
// 独立模式
if (data.index === data.words.length - 1) {
// 处理错词
data.wrongWords = data.wrongWords.filter(v => (!data.excludeWords.includes(v.word)))
if (data.wrongWords.length) {
isTypingWrongWord.value = true
console.log('当前学完了,但还有错词')
data.words = shuffle(cloneDeep(data.wrongWords))
data.index = 0
data.wrongWords = []
} else {
isTypingWrongWord.value = false
// 按顺序切换到下一个单词列表:新词 -> 复习上次 -> 复习之前 -> 结束
let nextWords: Word[] = []
let nextStage: 'new' | 'review' | 'write' | 'finished' = 'finished'
if (currentWordListStage === 'new') {
// 新词完成,切换到复习上次
if (taskWords.review.length > 0) {
nextWords = taskWords.review
nextStage = 'review'
} else if (taskWords.write.length > 0) {
// 如果没有复习上次,直接跳到复习之前
nextWords = taskWords.write
nextStage = 'write'
}
} else if (currentWordListStage === 'review') {
// 复习上次完成,切换到复习之前
if (taskWords.write.length > 0) {
nextWords = taskWords.write
nextStage = 'write'
}
}
// currentWordListStage === 'write' 时nextStage 保持为 'finished'
if (nextStage === 'finished') {
// 全部完成
console.log('独立模式,全完学完了')
showStatDialog = true
clearInterval(timer)
setTimeout(() => localStorage.removeItem(PracticeSaveWordKey.key), 300)
} else {
// 切换到下一个阶段
currentWordListStage = nextStage
data.words = nextWords
data.index = 0
// 保持相同的练习类型
}
}
} else {
data.index++
}
} else {
if (data.index === data.words.length - 1) {
if (statStore.step === 0 || isTypingWrongWord.value) {
if (statStore.stage === WordPracticeStage.FollowWriteNewWord || isTypingWrongWord.value) {
if (settingStore.wordPracticeType !== WordPracticeType.Spell) {
//回到最后一组的开始位置
data.index = Math.floor(data.index / groupSize) * groupSize
@@ -474,7 +425,7 @@ async function next(isTyping: boolean = true) {
return
}
}
data.wrongWords = data.wrongWords.filter(v => (!data.excludeWords.includes(v.word)))
data.wrongWords = data.wrongWords.filter(v => !data.excludeWords.includes(v.word))
if (data.wrongWords.length) {
isTypingWrongWord.value = true
settingStore.wordPracticeType = WordPracticeType.FollowWrite
@@ -485,57 +436,87 @@ async function next(isTyping: boolean = true) {
} else {
isTypingWrongWord.value = false
console.log('当前学完了,没错词', statStore.total, statStore.step, data.index)
//学完了,这里第 7 步如果无单词,加 3 就是 9 了
if (statStore.step >= 8) {
const complete = () => {
console.log('全完学完了')
showStatDialog = true
clearInterval(timer)
setTimeout(() => localStorage.removeItem(PracticeSaveWordKey.key), 300)
return;
}
//开始默写之前
if (statStore.step === 7) {
return goNextStep(shuffle(taskWords.write), WordPracticeType.Dictation, '开始默写之前')
}
//开始听写之前
if (statStore.step === 6) {
return goNextStep(shuffle(taskWords.write), WordPracticeType.Listen, '开始听写之前')
}
//开始自测之前
if (statStore.step === 5) {
return goNextStep(taskWords.write, WordPracticeType.Identify, '开始自测之前')
}
//开始写上次
if (statStore.step === 4) {
return goNextStep(shuffle(taskWords.review), WordPracticeType.Dictation, '开始默写上次')
}
//开始听写上次
if (statStore.step === 3) {
return goNextStep(shuffle(taskWords.review), WordPracticeType.Listen, '开始听写上次')
}
//开始自测昨日
if (statStore.step === 2) {
return goNextStep(taskWords.review, WordPracticeType.Identify, '开始自测昨日')
}
//开始默写新词
if (statStore.step === 1) {
return goNextStep(shuffle(taskWords.new), WordPracticeType.Dictation, '开始默写新词')
}
//开始听写新词
if (statStore.step === 0) {
return goNextStep(shuffle(taskWords.new), WordPracticeType.Listen, '开始听写新词')
if (settingStore.wordPracticeMode === WordPracticeMode.System) {
if (statStore.stage === WordPracticeStage.FollowWriteNewWord) {
settingStore.wordPracticeType = WordPracticeType.Listen
return nextStage(shuffle(taskWords.new), '开始听写新词', WordPracticeStage.ListenNewWord)
}
if (statStore.stage === WordPracticeStage.ListenNewWord) {
settingStore.wordPracticeType = WordPracticeType.Dictation
return nextStage(shuffle(taskWords.new), '开始默写新词', WordPracticeStage.DictationNewWord)
}
if (statStore.stage === WordPracticeStage.DictationNewWord) {
settingStore.wordPracticeType = WordPracticeType.Identify
return nextStage(taskWords.review, '开始自测昨日', WordPracticeStage.IdentifyReview)
}
if (statStore.stage === WordPracticeStage.IdentifyReview) {
settingStore.wordPracticeType = WordPracticeType.Listen
return nextStage(shuffle(taskWords.review), '开始写上次', WordPracticeStage.ListenReview)
}
if (statStore.stage === WordPracticeStage.ListenReview) {
settingStore.wordPracticeType = WordPracticeType.Dictation
return nextStage(shuffle(taskWords.review), '开始默写上次', WordPracticeStage.DictationReview)
}
if (statStore.stage === WordPracticeStage.DictationReview) {
settingStore.wordPracticeType = WordPracticeType.Identify
return nextStage(taskWords.write, '开始自测之前', WordPracticeStage.IdentifyReviewAll)
}
if (statStore.stage === WordPracticeStage.IdentifyReviewAll) {
settingStore.wordPracticeType = WordPracticeType.Listen
return nextStage(shuffle(taskWords.review), '开始听写之前', WordPracticeStage.ListenReviewAll)
}
if (statStore.stage === WordPracticeStage.ListenReviewAll) {
settingStore.wordPracticeType = WordPracticeType.Dictation
return nextStage(shuffle(taskWords.review), '开始默写之前', WordPracticeStage.DictationReviewAll)
}
if (statStore.stage === WordPracticeStage.DictationReviewAll) {
complete()
}
} else if (settingStore.wordPracticeMode === WordPracticeMode.ListenOnly) {
settingStore.wordPracticeType = WordPracticeType.Listen
if (statStore.stage === WordPracticeStage.ListenNewWord) {
return nextStage(taskWords.review, '开始听写昨日', WordPracticeStage.ListenReview)
}
if (statStore.stage === WordPracticeStage.ListenReview) {
return nextStage(taskWords.write, '开始听写之前', WordPracticeStage.ListenReviewAll)
}
if (statStore.stage === WordPracticeStage.ListenReviewAll) {
complete()
}
} else if (settingStore.wordPracticeMode === WordPracticeMode.DictationOnly) {
settingStore.wordPracticeType = WordPracticeType.Dictation
if (statStore.stage === WordPracticeStage.DictationNewWord) {
return nextStage(taskWords.review, '开始默写昨日', WordPracticeStage.DictationReview)
}
if (statStore.stage === WordPracticeStage.DictationReview) {
return nextStage(taskWords.write, '开始默写之前', WordPracticeStage.DictationReviewAll)
}
if (statStore.stage === WordPracticeStage.DictationReviewAll) {
complete()
}
} else if (settingStore.wordPracticeMode === WordPracticeMode.IdentifyOnly) {
settingStore.wordPracticeType = WordPracticeType.Identify
if (statStore.stage === WordPracticeStage.IdentifyNewWord) {
return nextStage(taskWords.review, '开始自测昨日', WordPracticeStage.IdentifyReview)
}
if (statStore.stage === WordPracticeStage.IdentifyReview) {
return nextStage(taskWords.write, '开始自测之前', WordPracticeStage.IdentifyReviewAll)
}
if (statStore.stage === WordPracticeStage.IdentifyReviewAll) {
complete()
}
}
}
} else {
if (statStore.step === 0) {
if (statStore.stage === WordPracticeStage.FollowWriteNewWord) {
wordLoop()
} else {
if (isTypingWrongWord.value) wordLoop()
@@ -580,15 +561,15 @@ function onTypeWrong() {
function savePracticeData() {
// console.log('savePracticeData')
localStorage.setItem(
PracticeSaveWordKey.key,
JSON.stringify({
version: PracticeSaveWordKey.version,
val: {
taskWords,
practiceData: data,
statStoreData: statStore.$state,
},
})
PracticeSaveWordKey.key,
JSON.stringify({
version: PracticeSaveWordKey.version,
val: {
taskWords,
practiceData: data,
statStoreData: statStore.$state,
},
})
)
}
@@ -699,8 +680,8 @@ async function continueStudy() {
if (taskWords.shuffle.length) {
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
temp.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(
0,
runtimeStore.routeData.total
0,
runtimeStore.routeData.total
)
if (showStatDialog) showStatDialog = false
} else {
@@ -782,9 +763,9 @@ useEvents([
</Tooltip>
</div>
<div
class="center gap-2 cursor-pointer float-right mr-3"
@click="next(false)"
v-if="nextWord"
class="center gap-2 cursor-pointer float-right mr-3"
@click="next(false)"
v-if="nextWord"
>
<Tooltip :title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`">
<div class="word" :class="settingStore.dictation && 'word-shadow'">
@@ -795,11 +776,11 @@ useEvents([
</div>
</div>
<TypeWord
ref="typingRef"
:word="word"
@wrong="onTypeWrong"
@complete="next"
@know="onWordKnow"
ref="typingRef"
:word="word"
@wrong="onTypeWrong"
@complete="next"
@know="onWordKnow"
/>
</div>
</template>
@@ -809,18 +790,18 @@ useEvents([
<!-- <span>{{ store.sdict.name }} ({{ data.index + 1 }} / {{ data.words.length }})</span>-->
<div class="center gap-space">
<span
>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} /
>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} /
{{ store.sdict.length }})</span
>
<BaseIcon
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
>
<IconFluentArrowRight16Regular class="arrow" width="22" />
</BaseIcon>
<BaseIcon
@click="randomWrite"
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`"
@click="randomWrite"
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`"
>
<IconFluentArrowShuffle16Regular class="arrow" width="22" />
</BaseIcon>
@@ -828,14 +809,14 @@ useEvents([
</template>
<div class="panel-page-item pl-4">
<WordList
v-if="data.words.length"
:is-active="settingStore.showPanel"
:static="false"
:show-word="!settingStore.dictation"
:show-translate="settingStore.translate"
:list="data.words"
:activeIndex="data.index"
@click="(val: any) => (data.index = val.index)"
v-if="data.words.length"
:is-active="settingStore.showPanel"
:static="false"
:show-word="!settingStore.dictation"
:show-translate="settingStore.translate"
:list="data.words"
:activeIndex="data.index"
@click="(val: any) => (data.index = val.index)"
>
</WordList>
<Empty v-else />
@@ -844,12 +825,12 @@ useEvents([
</template>
<template v-slot:footer>
<Footer
:is-simple="isWordSimple(word)"
@toggle-simple="toggleWordSimpleWrapper"
:is-collect="isWordCollect(word)"
@toggle-collect="toggleWordCollect(word)"
@skip="next(false)"
@skipStep="skipStep"
:is-simple="isWordSimple(word)"
@toggle-simple="toggleWordSimpleWrapper"
:is-collect="isWordCollect(word)"
@toggle-collect="toggleWordCollect(word)"
@skip="next(false)"
@skipStep="skipStep"
/>
</template>
</PracticeLayout>

View File

@@ -130,6 +130,7 @@ function startPractice(practiceMode?: WordPracticeMode): void {
}
// 如果传入了独立模式,临时设置 wordPracticeMode
if (practiceMode !== undefined) {
//todo 临时处理
localStorage.removeItem(PracticeSaveWordKey.key)
settingStore.wordPracticeMode = practiceMode
}
@@ -377,16 +378,14 @@ let isNewHost = $ref(window.location.host === Host)
<div class="num">{{ currentStudy.new.length }}</div>
<div class="txt">新词数</div>
</div>
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
<div class="stat">
<div class="num">{{ currentStudy.review.length }}</div>
<div class="txt">复习上次</div>
</div>
<div class="stat">
<div class="num">{{ currentStudy.write.length }}</div>
<div class="txt">复习之前</div>
</div>
</template>
<div class="stat">
<div class="num">{{ currentStudy.review.length }}</div>
<div class="txt">复习上次</div>
</div>
<div class="stat">
<div class="num">{{ currentStudy.write.length }}</div>
<div class="txt">复习之前</div>
</div>
</div>
<div class="flex items-end mt-4 gap-4 btn-no-margin">
<BaseButton
@@ -422,6 +421,16 @@ let isNewHost = $ref(window.location.host === Host)
<div
class="space-y-2 btn-no-margin pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95 group-hover:opacity-100 group-hover:scale-100 transition-all duration-150 pointer-events-none group-hover:pointer-events-auto"
>
<BaseButton
size="large"
class="w-30"
type="primary"
@click="startPractice(WordPracticeMode.System)"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">智能</span>
</div>
</BaseButton>
<BaseButton
size="large"
class="w-30"
@@ -467,7 +476,6 @@ let isNewHost = $ref(window.location.host === Host)
</div>
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large"
:loading="loading"
@click="check(() => (showShufflePracticeSettingDialog = true))"
@@ -479,10 +487,9 @@ let isNewHost = $ref(window.location.host === Host)
</BaseButton>
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large"
:loading="loading"
@click="check(() => (showShufflePracticeSettingDialog = true))"
@click="startPractice(WordPracticeMode.Free)"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">自由练习</span>

View File

@@ -2,7 +2,7 @@
import { inject, Ref } from 'vue'
import { usePracticeStore } from '@/stores/practice.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { PracticeData, ShortcutKey } from '@/types/types.ts'
import { PracticeData, ShortcutKey, WordPracticeStage } from '@/types/types.ts'
import BaseIcon from '@/components/BaseIcon.vue'
import Tooltip from '@/components/base/Tooltip.vue'
import Progress from '@/components/base/Progress.vue'
@@ -35,43 +35,52 @@ function format(val: number, suffix: string = '', check: number = -1) {
const status = $computed(() => {
if (isTypingWrongWord.value) return '复习错词'
return getStepStr(statStore.step)
return getStageStr(statStore.stage)
})
function getStepStr(step: number) {
function getStageStr(stage: WordPracticeStage) {
let str = ''
switch (step) {
case 0:
str += `学习新词`
switch (stage) {
case WordPracticeStage.FollowWriteNewWord:
str += `跟写新词`
break
case 1:
case WordPracticeStage.IdentifyNewWord:
str += `自测新词`
break
case WordPracticeStage.ListenNewWord:
str += `听写新词`
break
case 2:
case WordPracticeStage.DictationNewWord:
str += `默写新词`
break
case 3:
case WordPracticeStage.FollowWriteReview:
str += `跟写上次学习`
break
case WordPracticeStage.IdentifyReview:
str += `自测上次学习`
break
case 4:
str += '听写上次学习'
case WordPracticeStage.ListenReview:
str += `听写上次学习`
break
case 5:
str += '默写上次学习'
case WordPracticeStage.DictationReview:
str += `默写上次学习`
break
case 6:
case WordPracticeStage.FollowWriteReviewAll:
str += '跟写之前学习'
break
case WordPracticeStage.IdentifyReviewAll:
str += '自测之前学习'
break
case 7:
case WordPracticeStage.ListenReviewAll:
str += '听写之前学习'
break
case 8:
case WordPracticeStage.DictationReviewAll:
str += '默写之前学习'
break
case 9:
case WordPracticeStage.Complete:
str += '学习完成'
break
case 10:
case WordPracticeStage.Shuffle:
str += '随机复习'
break
}
@@ -128,31 +137,31 @@ const progress = $computed(() => {
<BaseIcon
v-if="statStore.step < 9"
@click="emit('skipStep')"
:title="`跳到下一阶段:${getStepStr(statStore.step + 1)}`"
:title="`跳到下一阶段:${getStageStr(statStore.stage + 1)}`"
>
<IconFluentArrowRight16Regular />
</BaseIcon>
<div class="relative z-999 group">
<div
class="space-y-2 btn-no-margin pb-2 left-1/2 -transform-translate-x-1/2 absolute z-999 bottom-full border rounded scale-95 opacity-0 group-hover:opacity-100 group-hover:scale-100 transition-all duration-150 pointer-events-none group-hover:pointer-events-auto"
class="space-y-2 btn-no-margin pb-2 left-1/2 -transform-translate-x-1/2 absolute z-999 bottom-full scale-95 opacity-0 group-hover:opacity-100 group-hover:scale-100 transition-all duration-300 pointer-events-none group-hover:pointer-events-auto"
>
<BaseButton size="large" type="info" class="w-full" @click="$emit('toggleSimple')">
<BaseButton size="normal" type="info" class="w-full" @click="$emit('toggleSimple')">
<div class="flex items-center gap-2">
<IconFluentCheckmarkCircle16Regular v-if="!isSimple" class="text-xl" />
<IconFluentCheckmarkCircle16Filled v-else class="text-xl" />
<IconFluentCheckmarkCircle16Regular v-if="!isSimple" />
<IconFluentCheckmarkCircle16Filled v-else />
<span>
{{
(!isSimple ? '标记已掌握' : '取消标记已掌握') +
(!isSimple ? '标记已掌握' : '取消已掌握') +
`(${settingStore.shortcutKeyMap[ShortcutKey.ToggleSimple]})`
}}</span
>
</div>
</BaseButton>
<BaseButton size="large" type="info" class="w-full" @click="$emit('toggleCollect')">
<BaseButton size="normal" type="info" class="w-full" @click="$emit('toggleCollect')">
<div class="flex items-center gap-2">
<IconFluentStarAdd16Regular v-if="!isCollect" class="text-xl" />
<IconFluentStar16Filled v-else class="text-xl" />
<IconFluentStarAdd16Regular v-if="!isCollect" />
<IconFluentStar16Filled v-else />
<span>
{{
(!isCollect ? '收藏' : '取消收藏') +
@@ -161,17 +170,15 @@ const progress = $computed(() => {
>
</div>
</BaseButton>
<BaseButton size="large" type="info" class="w-full" @click="$emit('skip')">
<BaseButton size="normal" type="info" class="w-full" @click="$emit('skip')">
<div class="flex items-center gap-2">
<IconFluentArrowBounce20Regular class="transform-rotate-180 text-xl" />
<span>
跳过当前单词({{ settingStore.shortcutKeyMap[ShortcutKey.Next] }})</span
>
<IconFluentArrowBounce20Regular class="transform-rotate-180" />
<span> 跳过单词({{ settingStore.shortcutKeyMap[ShortcutKey.Next] }})</span>
</div>
</BaseButton>
</div>
<BaseIcon @click="emit('skip')">
<BaseIcon>
<IconPhMicrosoftWordLogoLight />
</BaseIcon>
</div>

View File

@@ -1,21 +1,24 @@
import {defineStore} from "pinia"
import { defineStore } from 'pinia'
import { WordPracticeStage } from '@/types/types.ts'
export interface PracticeState {
step: number,
startDate: number,
spend: number,
total: number,
newWordNumber: number,
reviewWordNumber: number,
writeWordNumber: number,
inputWordNumber: number,//当前总输入了多少个单词(不包含跳过)
wrong: number,
step: number
stage: WordPracticeStage
startDate: number
spend: number
total: number
newWordNumber: number
reviewWordNumber: number
writeWordNumber: number
inputWordNumber: number //当前总输入了多少个单词(不包含跳过)
wrong: number
}
export const usePracticeStore = defineStore('practice', {
state: (): PracticeState => {
return {
step: 0,
stage: WordPracticeStage.FollowWriteNewWord,
spend: 0,
startDate: Date.now(),
total: 0,

View File

@@ -1,40 +1,40 @@
export type Word = {
id?: string,
custom?: boolean,
word: string,
phonetic0: string,
phonetic1: string,
trans: {
pos: string,
cn: string,
}[],
sentences: {
c: string,//content
cn: string,
}[],
phrases: {
c: string,
cn: string,
}[],
synos: {
pos: string,
cn: string,
ws: string[]
}[],
relWords: {
root: string,
rels: {
pos: string,
words: {
c: string,
cn: string,
}[],
}[]
},
etymology: {
t: string,//title
d: string,//desc
}[],
id?: string
custom?: boolean
word: string
phonetic0: string
phonetic1: string
trans: {
pos: string
cn: string
}[]
sentences: {
c: string //content
cn: string
}[]
phrases: {
c: string
cn: string
}[]
synos: {
pos: string
cn: string
ws: string[]
}[]
relWords: {
root: string
rels: {
pos: string
words: {
c: string
cn: string
}[]
}[]
}
etymology: {
t: string //title
d: string //desc
}[]
}
export const PronunciationApi = 'https://dict.youdao.com/dictvoice?audio='
@@ -43,216 +43,237 @@ export type TranslateLanguageType = 'en' | 'zh-CN' | 'ja' | 'de' | 'common' | ''
export type LanguageType = 'en' | 'ja' | 'de' | 'code'
export enum DictType {
collect = 'collect',
simple = 'simple',
wrong = 'wrong',
known = 'known',
word = 'word',
article = 'article',
collect = 'collect',
simple = 'simple',
wrong = 'wrong',
known = 'known',
word = 'word',
article = 'article',
}
export interface ArticleWord extends Word {
nextSpace: boolean,
symbolPosition: 'start' | 'end' | '',
input: string
type: PracticeArticleWordType
nextSpace: boolean
symbolPosition: 'start' | 'end' | ''
input: string
type: PracticeArticleWordType
}
export interface Sentence {
text: string,
translate: string,
words: ArticleWord[],
audioPosition: number[]
text: string
translate: string
words: ArticleWord[]
audioPosition: number[]
}
export interface Article {
id?: number,
title: string,
titleTranslate: string,
text: string,
textTranslate: string,
newWords: Word[],
sections: Sentence[][],
audioSrc: string,
audioFileId: string,
lrcPosition: number[][],
nameList: string[],
questions: {
stem: string,
options: string[],
correctAnswer: string[],
explanation: string
}[]
id?: number
title: string
titleTranslate: string
text: string
textTranslate: string
newWords: Word[]
sections: Sentence[][]
audioSrc: string
audioFileId: string
lrcPosition: number[][]
nameList: string[]
questions: {
stem: string
options: string[]
correctAnswer: string[]
explanation: string
}[]
}
export interface Statistics {
startDate: number,//开始日期
spend: number,//花费时间
total: number//单词数量
new: number//新学单词数量
review: number//复习单词数量
wrong: number//错误数
startDate: number //开始日期
spend: number //花费时间
total: number //单词数量
new: number //新学单词数量
review: number //复习单词数量
wrong: number //错误数
}
export enum Sort {
normal = 0,
random = 1,
reverse = 2,
reverseAll = 3,
randomAll = 4,
normal = 0,
random = 1,
reverse = 2,
reverseAll = 3,
randomAll = 4,
}
export enum ShortcutKey {
ShowWord = 'ShowWord',
EditArticle = 'EditArticle',
Next = 'Next',
Previous = 'Previous',
ToggleSimple = 'ToggleSimple',
ToggleCollect = 'ToggleCollect',
NextChapter = 'NextChapter',
PreviousChapter = 'PreviousChapter',
RepeatChapter = 'RepeatChapter',
DictationChapter = 'DictationChapter',
PlayWordPronunciation = 'PlayWordPronunciation',
ToggleShowTranslate = 'ToggleShowTranslate',
ToggleDictation = 'ToggleDictation',
ToggleTheme = 'ToggleTheme',
ToggleConciseMode = 'ToggleConciseMode',
TogglePanel = 'TogglePanel',
RandomWrite = 'RandomWrite',
NextRandomWrite = 'NextRandomWrite',
KnowWord = 'KnowWord',
UnknownWord = 'UnknownWord',
ShowWord = 'ShowWord',
EditArticle = 'EditArticle',
Next = 'Next',
Previous = 'Previous',
ToggleSimple = 'ToggleSimple',
ToggleCollect = 'ToggleCollect',
NextChapter = 'NextChapter',
PreviousChapter = 'PreviousChapter',
RepeatChapter = 'RepeatChapter',
DictationChapter = 'DictationChapter',
PlayWordPronunciation = 'PlayWordPronunciation',
ToggleShowTranslate = 'ToggleShowTranslate',
ToggleDictation = 'ToggleDictation',
ToggleTheme = 'ToggleTheme',
ToggleConciseMode = 'ToggleConciseMode',
TogglePanel = 'TogglePanel',
RandomWrite = 'RandomWrite',
NextRandomWrite = 'NextRandomWrite',
KnowWord = 'KnowWord',
UnknownWord = 'UnknownWord',
}
export const DefaultShortcutKeyMap = {
[ShortcutKey.EditArticle]: 'Ctrl+E',
[ShortcutKey.ShowWord]: 'Escape',
[ShortcutKey.Previous]: 'Alt+⬅',
[ShortcutKey.Next]: 'Tab',
[ShortcutKey.ToggleSimple]: '`',
[ShortcutKey.ToggleCollect]: 'Enter',
[ShortcutKey.PreviousChapter]: 'Ctrl+⬅',
[ShortcutKey.NextChapter]: 'Ctrl+➡',
[ShortcutKey.RepeatChapter]: 'Ctrl+Enter',
[ShortcutKey.DictationChapter]: 'Alt+Enter',
[ShortcutKey.PlayWordPronunciation]: 'Ctrl+P',
[ShortcutKey.ToggleShowTranslate]: 'Ctrl+Z',
[ShortcutKey.ToggleDictation]: 'Ctrl+I',
[ShortcutKey.ToggleTheme]: 'Ctrl+Q',
[ShortcutKey.ToggleConciseMode]: 'Ctrl+M',
[ShortcutKey.TogglePanel]: 'Ctrl+L',
[ShortcutKey.RandomWrite]: 'Ctrl+R',
[ShortcutKey.NextRandomWrite]: 'Ctrl+Shift+R',
[ShortcutKey.KnowWord]: '1',
[ShortcutKey.UnknownWord]: '2',
[ShortcutKey.EditArticle]: 'Ctrl+E',
[ShortcutKey.ShowWord]: 'Escape',
[ShortcutKey.Previous]: 'Alt+⬅',
[ShortcutKey.Next]: 'Tab',
[ShortcutKey.ToggleSimple]: '`',
[ShortcutKey.ToggleCollect]: 'Enter',
[ShortcutKey.PreviousChapter]: 'Ctrl+⬅',
[ShortcutKey.NextChapter]: 'Ctrl+➡',
[ShortcutKey.RepeatChapter]: 'Ctrl+Enter',
[ShortcutKey.DictationChapter]: 'Alt+Enter',
[ShortcutKey.PlayWordPronunciation]: 'Ctrl+P',
[ShortcutKey.ToggleShowTranslate]: 'Ctrl+Z',
[ShortcutKey.ToggleDictation]: 'Ctrl+I',
[ShortcutKey.ToggleTheme]: 'Ctrl+Q',
[ShortcutKey.ToggleConciseMode]: 'Ctrl+M',
[ShortcutKey.TogglePanel]: 'Ctrl+L',
[ShortcutKey.RandomWrite]: 'Ctrl+R',
[ShortcutKey.NextRandomWrite]: 'Ctrl+Shift+R',
[ShortcutKey.KnowWord]: '1',
[ShortcutKey.UnknownWord]: '2',
}
export enum TranslateEngine {
Baidu = 0,
Baidu = 0,
}
export type DictResource = {
id: string
name: string
description: string
url: string
length: number
category: string
tags: string[]
translateLanguage: TranslateLanguageType
//todo 可以考虑删除了
type?: DictType
version?: number
language: LanguageType
id: string
name: string
description: string
url: string
length: number
category: string
tags: string[]
translateLanguage: TranslateLanguageType
//todo 可以考虑删除了
type?: DictType
version?: number
language: LanguageType
}
export interface Dict extends DictResource {
lastLearnIndex: number,
perDayStudyNumber: number,
words: Word[],
articles: Article[],
statistics: Statistics[],
custom: boolean,//是否是自定义词典
complete: boolean,//是否学习完成学完了设为true然后lastLearnIndex重置
//后端字段
en_name?: string
createdBy?: string
category_id?: number
is_default?: boolean
update?: boolean
cover?: string
sync?: boolean
userDictId?: number
lastLearnIndex: number
perDayStudyNumber: number
words: Word[]
articles: Article[]
statistics: Statistics[]
custom: boolean //是否是自定义词典
complete: boolean //是否学习完成学完了设为true然后lastLearnIndex重置
//后端字段
en_name?: string
createdBy?: string
category_id?: number
is_default?: boolean
update?: boolean
cover?: string
sync?: boolean
userDictId?: number
}
export interface ArticleItem {
item: Article,
index: number
item: Article
index: number
}
export const SlideType = {
HORIZONTAL: 0,
VERTICAL: 1,
HORIZONTAL: 0,
VERTICAL: 1,
}
export interface PracticeData {
index: number,
words: Word[],
wrongWords: Word[],
excludeWords: string[],
index: number
words: Word[]
wrongWords: Word[]
excludeWords: string[]
}
export interface TaskWords {
new: Word[],
review: Word[],
write: Word[],
shuffle: Word[],
new: Word[]
review: Word[]
write: Word[]
shuffle: Word[]
}
export class DictId {
static wordCollect = 'wordCollect'
static wordWrong = 'wordWrong'
static wordKnown = 'wordKnown'
static articleCollect = 'articleCollect'
static wordCollect = 'wordCollect'
static wordWrong = 'wordWrong'
static wordKnown = 'wordKnown'
static articleCollect = 'articleCollect'
}
export enum PracticeArticleWordType {
Symbol,
Number,
Word
Symbol,
Number,
Word,
}
//练习模式
export enum WordPracticeMode {
System = 0,
Free = 1,
DictationOnly = 2, // 独立默写模式
ListenOnly = 3, // 独立写模式
IdentifyOnly = 4, // 独立自测模式
FollowWriteOnly = 5 // 独立跟写模式(内部会自动切换到 Spell
System = 0,
Free = 1,
IdentifyOnly = 2, // 独立自测模式
DictationOnly = 3, // 独立写模式
ListenOnly = 4, // 独立听写模式
FollowWriteOnly = 5, // 独立跟写模式(内部会自动切换到 Spell
}
//练习类型
export enum WordPracticeType {
FollowWrite,//跟写
Spell,
Identify,
Listen,
Dictation
FollowWrite, //跟写
Spell,
Identify,
Listen,
Dictation,
}
export enum CodeType {
Login = 0,
Register = 1,
ResetPwd = 2,
ChangeEmail = 3,
ChangePhoneNew = 4,
ChangePhoneOld = 5
Login = 0,
Register = 1,
ResetPwd = 2,
ChangeEmail = 3,
ChangePhoneNew = 4,
ChangePhoneOld = 5,
}
export enum ImportStatus {
Idle = 0,
Success = 1,
Fail = 2
}
Idle = 0,
Success = 1,
Fail = 2,
}
//练习阶段
export enum WordPracticeStage {
FollowWriteNewWord = 0,
IdentifyNewWord = 1,
ListenNewWord = 2,
DictationNewWord = 3,
FollowWriteReview = 4,
IdentifyReview = 5,
ListenReview = 6,
DictationReview = 7,
FollowWriteReviewAll = 8,
IdentifyReviewAll = 9,
ListenReviewAll = 10,
DictationReviewAll = 11,
Shuffle = 12,
Complete = 13,
}