This commit is contained in:
Zyronon
2025-12-20 01:15:14 +08:00
parent d9ee131e3d
commit fe4f6155fd
6 changed files with 201 additions and 181 deletions

View File

@@ -26,7 +26,6 @@ let showQQDialog = $ref(false)
<template>
<div class="center" :class="type === 'vertical' ? 'flex-col gap-1' : 'gap-4'">
<ShareIcon v-if="share"/>
<Github v-if="github"/>
@@ -50,6 +49,8 @@ let showQQDialog = $ref(false)
<IconMaterialSymbolsMail class="color-blue"/>
</BaseIcon>
</a>
<ShareIcon v-if="share"/>
</div>
<Dialog v-model="showXhsDialog" title="小红书">
@@ -76,7 +77,4 @@ let showQQDialog = $ref(false)
</template>
<style scoped lang="scss">
.stat-card {
@apply text-center bg-gray-900/30 py-4 rounded-2xl;
}
</style>

View File

@@ -250,5 +250,7 @@ const sentence = $computed(() => {
</template>
<style scoped lang="scss">
</style>
.stat-card {
@apply text-center bg-gray-900/30 py-4 rounded-2xl;
}
</style>

View File

@@ -47,7 +47,7 @@ const {
<div class="item-title">
<span class="text-sm translate-y-0.5 text-gray-500" v-if="index != undefined">{{ index }}.</span>
<span class="word" :class="!showWord && 'word-shadow'">{{ item.word }}</span>
<span class="phonetic text-gray">{{ item.phonetic0 }}</span>
<span class="phonetic text-gray" :class="!showWord && 'word-shadow'">{{ item.phonetic0 }}</span>
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
</div>
<div class="item-sub-title flex flex-col gap-2" v-if="item.trans.length && showTranslate">

View File

@@ -37,9 +37,12 @@ defineExpose({scrollToBottom, scrollToItem})
ref="listRef"
@click="(e:any) => emit('click',e)"
:list="list"
v-bind="$attrs">
v-bind="$attrs">
<template v-slot="{ item, index, active }">
<WordItem :item="item" :index="index" :active="active" />
<WordItem
:show-translate="showTranslate"
:show-word="showWord"
:item="item" :index="index" :active="active" />
</template>
</BaseList>
</template>
</template>

View File

@@ -1,44 +1,59 @@
<script setup lang="ts">
import { onMounted, onUnmounted, provide, ref, watch } from "vue";
import { onMounted, onUnmounted, 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, ShortcutKey, TaskWords, Word, WordPracticeMode, WordPracticeType } 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, _nextTick, cloneDeep, isMobile, loadJsLib, resourceWrap, shuffle } 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";
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 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,
ShortcutKey,
TaskWords,
Word,
WordPracticeMode,
WordPracticeType,
} 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,
_nextTick,
cloneDeep,
isMobile,
loadJsLib,
resourceWrap,
shuffle,
} 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'
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 Toast from '@/components/base/toast/Toast.ts'
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import ConflictNotice from "@/components/ConflictNotice.vue";
import PracticeLayout from "@/components/PracticeLayout.vue";
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 { ToastInstance } from "@/components/base/toast/type.ts";
import { watchOnce } from "@vueuse/core";
import { setUserDictProp } from "@/apis";
import { AppEnv, DICT_LIST, 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'
const {
isWordCollect,
toggleWordCollect,
isWordSimple,
toggleWordSimple
} = useWordOptions()
const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = useWordOptions()
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const {toggleTheme} = useTheme()
const { toggleTheme } = useTheme()
const router = useRouter()
const route = useRoute()
const store = useBaseStore()
@@ -97,9 +112,13 @@ async function loadDict() {
}
}
watch(() => store.load, (n) => {
if (n && loading) loadDict()
}, {immediate: true})
watch(
() => store.load,
n => {
if (n && loading) loadDict()
},
{ immediate: true }
)
onMounted(() => {
//如果是从单词学习主页过来的,就直接使用;否则等待加载
@@ -122,49 +141,52 @@ onUnmounted(() => {
timer && clearInterval(timer)
})
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
}
]
});
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,
},
],
})
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()
useDisableEventListener(() => loading)
@@ -240,7 +262,6 @@ function initData(initVal: TaskWords, init: boolean = false) {
savePracticeData()
}
}, 1000)
}
const word = $computed<Word>(() => {
@@ -253,28 +274,32 @@ const nextWord: Word = $computed(() => {
return data.words?.[data.index + 1] ?? undefined
})
watch(() => settingStore.wordPracticeType, (n) => {
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})
watch(
() => settingStore.wordPracticeType,
n => {
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 }
)
const groupSize = 7
@@ -301,11 +326,11 @@ let toastInstance: ToastInstance = null
function goNextStep(originList, mode, msg) {
//每次都判断,因为每次都可能新增已掌握的单词
let list = originList.filter(v => (!data.excludeWords.includes(v.word)))
let list = originList.filter(v => !data.excludeWords.includes(v.word))
console.log(msg)
if (list.length) {
if (toastInstance) toastInstance.close()
toastInstance = Toast.info('输入完成后按空格键切换下一个', {duration: 5000})
toastInstance = Toast.info('输入完成后按空格键切换下一个', { duration: 5000 })
data.words = list
settingStore.wordPracticeType = mode
data.index = 0
@@ -321,7 +346,7 @@ async function next(isTyping: boolean = true) {
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
@@ -349,7 +374,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
@@ -366,7 +391,7 @@ async function next(isTyping: boolean = true) {
showStatDialog = true
clearInterval(timer)
setTimeout(() => localStorage.removeItem(PracticeSaveWordKey.key), 300)
return;
return
}
//开始默写之前
@@ -454,14 +479,17 @@ function onTypeWrong() {
function savePracticeData() {
// console.log('savePracticeData')
localStorage.setItem(PracticeSaveWordKey.key, JSON.stringify({
version: PracticeSaveWordKey.version,
val: {
taskWords,
practiceData: data,
statStoreData: statStore.$state,
}
}))
localStorage.setItem(
PracticeSaveWordKey.key,
JSON.stringify({
version: PracticeSaveWordKey.version,
val: {
taskWords,
practiceData: data,
statStoreData: statStore.$state,
},
})
)
}
watch(() => data.index, savePracticeData)
@@ -570,7 +598,10 @@ 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)
temp.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(
0,
runtimeStore.routeData.total
)
if (showStatDialog) showStatDialog = false
} else {
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
@@ -588,7 +619,7 @@ async function continueStudy() {
initData(temp)
if (AppEnv.CAN_REQUEST) {
let res = await setUserDictProp(null, {...store.sdict, type: 'word'})
let res = await setUserDictProp(null, { ...store.sdict, type: 'word' })
if (!res.success) {
Toast.error(res.msg)
}
@@ -597,7 +628,7 @@ async function continueStudy() {
function randomWrite() {
console.log('随机默写')
data.words = shuffle(data.words);
data.words = shuffle(data.words)
data.index = 0
settingStore.dictation = true
}
@@ -605,7 +636,7 @@ function randomWrite() {
function nextRandomWrite() {
console.log('继续随机默写')
initData(getCurrentStudyWord())
randomWrite();
randomWrite()
showStatDialog = false
}
@@ -613,9 +644,12 @@ useEvents([
[EventKey.repeatStudy, repeat],
[EventKey.continueStudy, continueStudy],
[EventKey.randomWrite, nextRandomWrite],
[EventKey.changeDict, () => {
initData(getCurrentStudyWord())
}],
[
EventKey.changeDict,
() => {
initData(getCurrentStudyWord())
},
],
[ShortcutKey.ShowWord, show],
[ShortcutKey.Previous, prev],
@@ -634,35 +668,30 @@ useEvents([
[ShortcutKey.RandomWrite, randomWrite],
[ShortcutKey.NextRandomWrite, nextRandomWrite],
])
</script>
<template>
<PracticeLayout
v-loading="loading"
panelLeft="var(--word-panel-margin-left)">
<PracticeLayout v-loading="loading" panelLeft="var(--word-panel-margin-left)">
<template v-slot:practice>
<div class="practice-word">
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
<div class="center gap-2 cursor-pointer float-left"
@click="prev"
v-if="prevWord">
<IconFluentArrowLeft16Regular class="arrow" width="22"/>
<Tooltip
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
>
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
<div class="center gap-2 cursor-pointer float-left" @click="prev" v-if="prevWord">
<IconFluentArrowLeft16Regular class="arrow" width="22" />
<Tooltip :title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`">
<div class="word">{{ prevWord.word }}</div>
</Tooltip>
</div>
<div 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'">{{ nextWord.word }}</div>
<div
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'">
{{ nextWord.word }}
</div>
</Tooltip>
<IconFluentArrowRight16Regular class="arrow" width="22"/>
<IconFluentArrowRight16Regular class="arrow" width="22" />
</div>
</div>
<TypeWord
@@ -679,17 +708,21 @@ useEvents([
<template v-slot:title>
<!-- <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.length }})</span>
<span
>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} /
{{ store.sdict.length }})</span
>
<BaseIcon
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
<IconFluentArrowRight16Regular class="arrow" width="22"/>
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
>
<IconFluentArrowRight16Regular class="arrow" width="22" />
</BaseIcon>
<BaseIcon
@click="randomWrite"
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`">
<IconFluentArrowShuffle16Regular class="arrow" width="22"/>
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`"
>
<IconFluentArrowShuffle16Regular class="arrow" width="22" />
</BaseIcon>
</div>
</template>
@@ -702,27 +735,10 @@ useEvents([
:show-translate="settingStore.translate"
:list="data.words"
:activeIndex="data.index"
@click="(val:any) => data.index = val.index"
@click="(val: any) => (data.index = val.index)"
>
<template v-slot:suffix="{item,index}">
<BaseIcon
:class="!isWordCollect(item)?'collect':'fill'"
@click.stop="toggleWordCollect(item)"
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
<IconFluentStar16Regular v-if="!isWordCollect(item)"/>
<IconFluentStar16Filled v-else/>
</BaseIcon>
<BaseIcon
:class="!isWordSimple(item)?'collect':'fill'"
@click.stop="toggleWordSimple(item)"
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
<IconFluentCheckmarkCircle16Filled v-else/>
</BaseIcon>
</template>
</WordList>
<Empty v-else/>
<Empty v-else />
</div>
</Panel>
</template>
@@ -737,12 +753,11 @@ useEvents([
/>
</template>
</PracticeLayout>
<Statistics v-model="showStatDialog"/>
<ConflictNotice v-if="showConflictNotice"/>
<Statistics v-model="showStatDialog" />
<ConflictNotice v-if="showConflictNotice" />
</template>
<style scoped lang="scss">
.practice-wrapper {
width: 100%;
height: 100vh;
@@ -793,7 +808,7 @@ useEvents([
position: absolute;
left: var(--panel-margin-left);
//left: 0;
top: .8rem;
top: 0.8rem;
z-index: 1;
height: calc(100% - 1.5rem);
}

View File

@@ -178,8 +178,10 @@ export function msToHourMinute(ms) {
const d = dayjs.duration(ms);
const hours = d.hours();
const minutes = d.minutes();
const seconds = d.seconds();
if (hours) return `${hours}小时${minutes}分钟`;
return `${minutes}分钟`;
if (minutes) return `${minutes}分钟`;
return `${seconds}`;
}
export function msToMinute(ms) {
@@ -504,4 +506,4 @@ export async function isNewUser() {
export function jump2Feedback() {
window.open('https://v.wjx.cn/vm/ev0W7fv.aspx#', '_blank');
}
}