This commit is contained in:
Zyronon
2025-11-05 12:12:32 +00:00
parent ca71d98dc8
commit 9cbc9df328
12 changed files with 300 additions and 232 deletions

View File

@@ -69,6 +69,8 @@
//修改的进度条底色
--color-progress-bar: #d1d5df !important;
--color-link: rgb(64, 158, 255)
}
.footer {
@@ -209,8 +211,7 @@ html, body {
}
a {
$main: rgb(64, 158, 255);
color: $main;
color: var(--color-link);
text-decoration: none;
}

View File

@@ -98,7 +98,7 @@ defineEmits(['click'])
}
&:hover:not(.disabled) {
opacity: .8;
opacity: .6;
}
&.primary {

View File

@@ -8,14 +8,16 @@ interface IProps {
strokeWidth?: number;
color?: string;
format?: (percentage: number) => string;
size?: 'normal' | 'large';
}
const props = withDefaults(defineProps<IProps>(), {
showText: true,
textInside: false,
strokeWidth: 6,
color: '#93ADE3',
color: '#409eff',
format: (percentage) => `${percentage}%`,
size: 'normal',
});
const barStyle = computed(() => {
@@ -26,13 +28,15 @@ const barStyle = computed(() => {
});
const trackStyle = computed(() => {
const height = props.size === 'large' ? props.strokeWidth * 2.5 : props.strokeWidth;
return {
height: `${props.strokeWidth}px`,
height: `${height}px`,
};
});
const progressTextSize = computed(() => {
return props.strokeWidth * 0.83 + 6;
const baseSize = props.strokeWidth * 0.83 + 6;
return props.size === 'large' ? baseSize * 1.2 : baseSize;
});
const content = computed(() => {

View File

@@ -215,7 +215,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
</div>
<div class="flex flex-col justify-between items-end">
<div class="flex gap-4 items-center" v-opacity="base.sbook.id">
<div class="color-blue cursor-pointer" @click="router.push('/book-list')">更换</div>
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更换</div>
</div>
<BaseButton size="large"
@click="startStudy"
@@ -238,10 +238,10 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
</BaseIcon>
</PopConfirm>
<div class="color-blue cursor-pointer" v-if="base.article.bookList.length > 1"
<div class="color-link cursor-pointer" v-if="base.article.bookList.length > 1"
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理书籍' }}
</div>
<div class="color-blue cursor-pointer" @click="nav('book-detail', { isAdd: true })">创建个人书籍</div>
<div class="color-link cursor-pointer" @click="nav('book-detail', { isAdd: true })">创建个人书籍</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
@@ -262,7 +262,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
<div class="flex justify-between">
<div class="title">推荐</div>
<div class="flex gap-4 items-center">
<div class="color-blue cursor-pointer" @click="router.push('/book-list')">更多</div>
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更多</div>
</div>
</div>
@@ -278,8 +278,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
<style scoped lang="scss">
.stat {
@apply rounded-xl p-4 box-border relative flex-1;
background: white;
@apply rounded-xl p-4 box-border relative flex-1 bg-[var(--bg-history)];
border: 1px solid gainsboro;
.num {

View File

@@ -1,17 +1,16 @@
<script setup lang="ts">
import { onMounted, provide, ref, watch } from "vue";
import {onMounted, provide, ref, toRef, 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, WordPracticeType, ShortcutKey, TaskWords, Word, WordPracticeMode } 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, WordPracticeType, ShortcutKey, TaskWords, Word, WordPracticeMode} 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 } from "@/utils";
import { useRoute, useRouter } from "vue-router";
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
import {_getDictDataByUrl, cloneDeep, 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";
@@ -19,15 +18,15 @@ 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 { ToastInstance } from "@/components/base/toast/type.ts";
import {DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
import {ToastInstance} from "@/components/base/toast/type.ts";
const {
isWordCollect,
@@ -50,6 +49,7 @@ let taskWords = $ref<TaskWords>({
new: [],
review: [],
write: [],
shuffle: [],
})
let data = $ref<PracticeData>({
@@ -60,10 +60,9 @@ let data = $ref<PracticeData>({
})
let isTypingWrongWord = ref(false)
let practiceMode = ref(WordPracticeType.FollowWrite)
provide('isTypingWrongWord', isTypingWrongWord)
provide('practiceData', data)
provide('practiceMode', practiceMode)
provide('practiceTaskWords', taskWords)
async function loadDict() {
// console.log('load好了开始加载')
@@ -100,7 +99,7 @@ watch(() => store.load, (n) => {
onMounted(() => {
//如果是从单词学习主页过来的,就直接使用;否则等待加载
if (runtimeStore.routeData) {
initData(runtimeStore.routeData, true)
initData(runtimeStore.routeData.taskWords, true)
} else {
loading = true
}
@@ -124,7 +123,10 @@ function initData(initVal: TaskWords, init: boolean = false) {
initData(initVal, true)
}
} else {
taskWords = initVal
// taskWords = initVal
//不能直接赋值,会导致 inject 的数据为默认值
taskWords = Object.assign(taskWords, initVal)
//如果 shuffle 数组不为空,就说明是复习
if (taskWords.shuffle.length === 0) {
if (taskWords.new.length === 0) {
if (taskWords.review.length) {
@@ -146,10 +148,18 @@ function initData(initVal: TaskWords, init: boolean = false) {
data.words = taskWords.new
statStore.step = 0
}
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 {
settingStore.wordPracticeType = WordPracticeType.Dictation
data.words = taskWords.shuffle
statStore.step = 10
statStore.total = taskWords.shuffle.length
statStore.newWordNumber = 0
statStore.reviewWordNumber = 0
statStore.writeWordNumber = statStore.total
}
data.index = 0
@@ -159,11 +169,6 @@ function initData(initVal: TaskWords, init: boolean = false) {
statStore.startDate = Date.now()
statStore.inputWordNumber = 0
statStore.wrong = 0
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
statStore.index = 0
isTypingWrongWord.value = false
}
}
@@ -217,6 +222,8 @@ function wordLoop() {
}
}
let toastInstance: ToastInstance = null
function goNextStep(originList, mode, msg) {
//每次都判断,因为每次都可能新增已掌握的单词
let list = originList.filter(v => (!data.excludeWords.includes(v.word)))
@@ -235,8 +242,6 @@ function goNextStep(originList, mode, msg) {
}
}
let toastInstance: ToastInstance = null
async function next(isTyping: boolean = true) {
if (isTyping) {
statStore.inputWordNumber++
@@ -251,7 +256,7 @@ async function next(isTyping: boolean = true) {
data.words = shuffle(cloneDeep(data.wrongWords))
data.index = 0
data.wrongWords = []
}else {
} else {
console.log('自由模式,全完学完了')
showStatDialog = true
localStorage.removeItem(PracticeSaveWordKey.key)
@@ -395,7 +400,7 @@ function onKeyUp(e: KeyboardEvent) {
typingRef.hideWord()
}
async function onKeyDown(e: KeyboardEvent) {
function onKeyDown(e: KeyboardEvent) {
// console.log('onKeyDown', e)
switch (e.key) {
case 'Backspace':
@@ -408,21 +413,27 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
function repeat() {
console.log('重学一遍')
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
//如果是刚刚完成那么学习进度要从length减回去因为lastLearnIndex为0了同时改complete为false
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
store.sdict.complete = false
let temp = cloneDeep(taskWords)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
//随机练习单独处理
if (taskWords.shuffle.length) {
temp.shuffle = shuffle(temp.shuffle.filter(v => !ignoreList.includes(v.word)))
} else {
//将学习进度减回去
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
//如果是刚刚完成那么学习进度要从length减回去因为lastLearnIndex为0了同时改complete为false
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
store.sdict.complete = false
} else {
//将学习进度减回去
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
}
//排除已掌握单词
temp.new = temp.new.filter(v => !ignoreList.includes(v.word))
temp.review = temp.review.filter(v => !ignoreList.includes(v.word))
temp.write = temp.write.filter(v => !ignoreList.includes(v.word))
}
emitter.emit(EventKey.resetWord)
let temp = cloneDeep(taskWords)
//排除已掌握单词
temp.new = temp.new.filter(v => !store.knownWords.includes(v.word))
temp.review = temp.review.filter(v => !store.knownWords.includes(v.word))
temp.write = temp.write.filter(v => !store.knownWords.includes(v.word))
initData(temp)
}
@@ -484,16 +495,26 @@ function togglePanel() {
}
function continueStudy() {
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
if (!showStatDialog) {
console.log('没学完,强行跳过')
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
let temp = cloneDeep(taskWords)
//随机练习单独处理
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)
if (showStatDialog) showStatDialog = false
} else {
console.log('学完了,正常下一组')
showStatDialog = false
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
if (!showStatDialog) {
console.log('没学完,强行跳过')
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
} else {
console.log('学完了,正常下一组')
showStatDialog = false
}
temp = getCurrentStudyWord()
}
initData(getCurrentStudyWord())
emitter.emit(EventKey.resetWord)
initData(temp)
}
function randomWrite() {
@@ -540,8 +561,8 @@ useEvents([
<template>
<PracticeLayout
v-loading="loading"
panelLeft="var(--word-panel-margin-left)">
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">
@@ -550,7 +571,7 @@ useEvents([
v-if="prevWord">
<IconFluentArrowLeft16Regular class="arrow" width="22"/>
<Tooltip
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
>
<div class="word">{{ prevWord.word }}</div>
</Tooltip>
@@ -559,7 +580,7 @@ useEvents([
@click="next(false)"
v-if="nextWord">
<Tooltip
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
>
<div class="word" :class="settingStore.dictation && 'word-shadow'">{{ nextWord.word }}</div>
</Tooltip>
@@ -567,11 +588,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>
@@ -583,41 +604,41 @@ useEvents([
<span>{{ 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>
</div>
</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"
>
<template v-slot:suffix="{item,index}">
<BaseIcon
:class="!isWordCollect(item)?'collect':'fill'"
@click.stop="toggleWordCollect(item)"
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
: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) ? '标记为已掌握' : '取消标记已掌握'">
:class="!isWordSimple(item)?'collect':'fill'"
@click.stop="toggleWordSimple(item)"
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
<IconFluentCheckmarkCircle16Filled v-else/>
</BaseIcon>
@@ -629,11 +650,11 @@ 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)"
:is-simple="isWordSimple(word)"
@toggle-simple="toggleWordSimpleWrapper"
:is-collect="isWordCollect(word)"
@toggle-collect="toggleWordCollect(word)"
@skip="next(false)"
/>
</template>
</PracticeLayout>

View File

@@ -1,26 +1,26 @@
<script setup lang="ts">
import {useBaseStore} from "@/stores/base.ts";
import BaseButton from "@/components/BaseButton.vue";
import {ShortcutKey, Statistics} from "@/types/types.ts";
import {PracticeData, ShortcutKey, Statistics, TaskWords, WordPracticeMode} from "@/types/types.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {usePracticeStore} from "@/stores/practice.ts";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import {defineAsyncComponent, watch} from "vue";
import {defineAsyncComponent, inject, watch} from "vue";
import isoWeek from 'dayjs/plugin/isoWeek'
dayjs.extend(isoWeek)
dayjs.extend(isBetween);
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
const store = useBaseStore()
const settingStore = useSettingStore()
const statStore = usePracticeStore()
const model = defineModel({default: false})
let list = $ref([])
let dictIsEnd = $ref(false)
let practiceTaskWords = inject<TaskWords>('practiceTaskWords')
function calcWeekList() {
// 获取本周的起止时间
@@ -68,12 +68,16 @@ watch(model, (newVal) => {
complete: store.sdict.complete,
str: `name:${store.sdict.name},per:${store.sdict.perDayStudyNumber},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)},index:${store.sdict.lastLearnIndex}`
})
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
if (store.sdict.lastLearnIndex >= store.sdict.length) {
dictIsEnd = true;
store.sdict.complete = true
store.sdict.lastLearnIndex = 0
//如果 shuffle 数组不为空,就说明是复习,不用修改 lastLearnIndex
if (!practiceTaskWords.shuffle.length) {
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
if (store.sdict.lastLearnIndex >= store.sdict.length) {
dictIsEnd = true;
store.sdict.complete = true
store.sdict.lastLearnIndex = 0
}
}
store.sdict.statistics.push(data as any)
calcWeekList(); // 新增:计算本周学习记录
}
@@ -97,27 +101,36 @@ function options(emitType: string) {
<template>
<Dialog
:close-on-click-bg="false"
:header="false"
:keyboard="false"
:show-close="false"
v-model="model">
:close-on-click-bg="false"
:header="false"
:keyboard="false"
:show-close="false"
v-model="model">
<div class="w-140 bg-white color-black p-6 relative flex flex-col gap-6">
<div class="w-full flex flex-col justify-evenly">
<div class="center text-2xl mb-2">已完成今日任务</div>
<div class="center text-2xl mb-2">已完成{{ practiceTaskWords.shuffle.length ? '随机复习' : '今日任务' }}</div>
<div class="flex">
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">新词数</div>
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习上次</div>
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习之前</div>
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
<div v-if="practiceTaskWords.shuffle.length"
class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">随机复习</div>
<div class="text-4xl font-bold">{{ practiceTaskWords.shuffle.length }}</div>
</div>
<template v-else>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">新词数</div>
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
</div>
<template v-if="settingStore.wordPracticeMode !== WordPracticeMode.Free">
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习上次</div>
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习之前</div>
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
</div>
</template>
</template>
</div>
</div>
@@ -149,29 +162,29 @@ function options(emitType: string) {
<div class="title text-align-center mb-2">本周学习记录</div>
<div class="flex gap-4 color-gray">
<div
class="w-8 h-8 rounded-md center"
:class="item ? 'bg-emerald-500 color-white' : 'bg-gray-200'"
v-for="(item, i) in list"
:key="i"
class="w-8 h-8 rounded-md center"
:class="item ? 'bg-emerald-500 color-white' : 'bg-gray-200'"
v-for="(item, i) in list"
:key="i"
>{{ i + 1 }}
</div>
</div>
</div>
<div class="flex justify-center gap-4 ">
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.RepeatChapter]"
@click="options(EventKey.repeatStudy)">
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.RepeatChapter]"
@click="options(EventKey.repeatStudy)">
重学一遍
</BaseButton>
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
@click="options(EventKey.continueStudy)">
{{ dictIsEnd ? '重新练习' : '再来一组' }}
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
@click="options(EventKey.continueStudy)">
{{ dictIsEnd ? '从头开始练习' : '再来一组' }}
</BaseButton>
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextRandomWrite]"
@click="options(EventKey.randomWrite)">
继续默写
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextRandomWrite]"
@click="options(EventKey.randomWrite)">
继续默写
</BaseButton>
<BaseButton @click="$router.back">
返回主页
@@ -182,7 +195,4 @@ function options(emitType: string) {
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
</style>
</template>

View File

@@ -1,27 +1,27 @@
<script setup lang="ts">
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import {useBaseStore} from "@/stores/base.ts";
import {useRouter} from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import { _getAccomplishDate, _getDictDataByUrl, resourceWrap, shuffle, useNav } from "@/utils";
import {_getAccomplishDate, _getDictDataByUrl, resourceWrap, shuffle, useNav} from "@/utils";
import BasePage from "@/components/BasePage.vue";
import { DictResource, WordPracticeMode } from "@/types/types.ts";
import { watch } from "vue";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import {DictResource, WordPracticeMode} from "@/types/types.ts";
import {watch} from "vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import Book from "@/components/Book.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import Progress from '@/components/base/Progress.vue';
import Toast from '@/components/base/toast/Toast.ts';
import BaseButton from "@/components/BaseButton.vue";
import { getDefaultDict } from "@/types/func.ts";
import {getDefaultDict} from "@/types/func.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
import { useSettingStore } from "@/stores/setting.ts";
import {useSettingStore} from "@/stores/setting.ts";
import CollectNotice from "@/components/CollectNotice.vue";
import { useFetch } from "@vueuse/core";
import { CAN_REQUEST, DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
import { myDictList } from "@/apis";
import {useFetch} from "@vueuse/core";
import {CAN_REQUEST, DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
import {myDictList} from "@/apis";
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePracticeSettingDialog.vue";
@@ -87,7 +87,7 @@ function startPractice() {
complete: store.sdict.complete,
wordPracticeMode: settingStore.wordPracticeMode
})
nav('practice-words/' + store.sdict.id, {}, currentStudy)
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
} else {
window.umami?.track('no-dict')
Toast.warning('请先选择一本词典')
@@ -100,11 +100,12 @@ let showChangeLastPracticeIndexDialog = $ref(false)
let showPracticeWordListDialog = $ref(false)
async function goDictDetail(val: DictResource) {
if (!val.id) return nav('dict-list')
runtimeStore.editDict = getDefaultDict(val)
nav('dict-detail', {})
}
let isMultiple = $ref(false)
let isManageDict = $ref(false)
let selectIds = $ref([])
function handleBatchDel() {
@@ -171,8 +172,12 @@ async function onShufflePracticeSettingOk(total) {
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
currentStudy.shuffle = shuffle(store.sdict.words).slice(0, total)
nav('practice-words/' + store.sdict.id, {}, currentStudy)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
currentStudy.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(0, total)
nav('practice-words/' + store.sdict.id, {}, {
taskWords: currentStudy,
total //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
})
}
async function saveLastPracticeIndex(e) {
@@ -199,20 +204,20 @@ const {
<div class="flex-1 flex flex-col justify-between">
<div class="flex gap-3">
<div class="p-1 center rounded-full bg-white">
<IconFluentBookNumber20Filled class="text-xl color-blue"/>
<IconFluentBookNumber20Filled class="text-xl color-link"/>
</div>
<div
@click="goDictDetail(store.sdict)"
class="text-2xl font-bold cursor-pointer">
@click="goDictDetail(store.sdict)"
class="text-2xl font-bold cursor-pointer">
{{ store.sdict.name || '请选择词典开始学习' }}
</div>
</div>
<div class="mt-4 flex flex-col gap-2">
<div class="">当前进度{{ progressTextLeft }}</div>
<Progress :percentage="store.currentStudyProgress" :show-text="false"></Progress>
<Progress size="large" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
<div class="text-sm flex justify-between">
<span>已完成 {{ progressTextRight }} / {{ store.sdict.words.length }} </span>
<span>
<span v-if="store.sdict.id">
预计完成日期{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
</span>
</div>
@@ -225,11 +230,11 @@ const {
</div>
</BaseButton>
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
<BaseButton type="info"
:disabled="!store.sdict.name"
v-if="store.sdict.id"
>
<div class="center gap-1">
<IconFluentSlideTextTitleEdit20Regular/>
@@ -249,10 +254,14 @@ const {
<div class="text-xl font-bold">
{{ isSaveData ? '上次学习任务' : '今日任务' }}
</div>
<span class="color-blue cursor-pointer" @click="showPracticeWordListDialog = true">词表</span>
<span class="color-link cursor-pointer"
v-if="store.sdict.id"
@click="showPracticeWordListDialog = true">词表</span>
</div>
<div class="flex gap-1 items-center">
<div class="flex gap-1 items-center"
v-if="store.sdict.id"
>
每日目标
<div style="color:#ac6ed1;"
class="bg-third px-2 h-10 flex center text-2xl rounded">
@@ -260,36 +269,35 @@ const {
</div>
个单词
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showPracticeSettingDialog = true)">
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showPracticeSettingDialog = true)">
<BaseButton
:disabled="!store.sdict.name"
type="info" size="small">更改
type="info" size="small">更改
</BaseButton>
</PopConfirm>
</div>
</div>
<div class="flex mt-4 justify-between">
<div class="w-31% box-border flex flex-col center rounded-xl p-2 bg-[var(--bg-history)]">
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
<div class="text-sm">新词数</div>
<div class="stat">
<div class="num">{{ currentStudy.new.length }}</div>
<div class="txt">新词数</div>
</div>
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
<div class="w-31% box-border flex flex-col center rounded-xl p-2 bg-[var(--bg-history)]">
<div class="text-4xl font-bold">{{ currentStudy.review.length }}</div>
<div class="text-sm">复习上次</div>
<div class="stat">
<div class="num">{{ currentStudy.review.length }}</div>
<div class="txt">复习上次</div>
</div>
<div class="w-31% box-border flex flex-col center rounded-xl p-2 bg-[var(--bg-history)]">
<div class="text-4xl font-bold">{{ currentStudy.write.length }}</div>
<div class="text-sm">复习之前</div>
<div class="stat">
<div class="num">{{ currentStudy.write.length }}</div>
<div class="txt">复习之前</div>
</div>
</template>
</div>
<div class="flex items-end mt-4">
<BaseButton size="large"
class="flex-1"
:disabled="!store.sdict.name"
:disabled="!store.sdict.id"
:loading="loading"
@click="startPractice">
<div class="flex items-center gap-2">
@@ -297,10 +305,11 @@ const {
<IconFluentArrowCircleRight16Regular class="text-xl"/>
</div>
</BaseButton>
<BaseButton size="large" type="orange"
:disabled="(!store.sdict.name || !store.sdict.lastLearnIndex)"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
<div class="flex items-center gap-2">
<span class="line-height-[2]">随机复习</span>
<IconFluentArrowShuffle20Filled class="text-xl"/>
@@ -320,15 +329,15 @@ const {
</BaseIcon>
</PopConfirm>
<div class="color-blue cursor-pointer" v-if="store.word.bookList.length > 3"
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理词典' }}
<div class="color-link cursor-pointer" v-if="store.word.bookList.length > 3"
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
</div>
<div class="color-blue cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)" :show-checkbox="isMultiple && j >= 3"
@check="() => toggleSelect(item)" :show-checkbox="isManageDict && j >= 3"
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)"/>
<Book :is-add="true" @click="router.push('/dict-list')"/>
</div>
@@ -338,7 +347,7 @@ const {
<div class="flex justify-between">
<div class="title">推荐</div>
<div class="flex gap-4 items-center">
<div class="color-blue cursor-pointer" @click="router.push('/dict-list')">更多</div>
<div class="color-link cursor-pointer" @click="router.push('/dict-list')">更多</div>
</div>
</div>
@@ -352,26 +361,38 @@ const {
</BasePage>
<PracticeSettingDialog
:show-left-option="false"
v-model="showPracticeSettingDialog"
@ok="savePracticeSetting"/>
:show-left-option="false"
v-model="showPracticeSettingDialog"
@ok="savePracticeSetting"/>
<ChangeLastPracticeIndexDialog
v-model="showChangeLastPracticeIndexDialog"
@ok="saveLastPracticeIndex"
v-model="showChangeLastPracticeIndexDialog"
@ok="saveLastPracticeIndex"
/>
<PracticeWordListDialog
:data="currentStudy"
v-model="showPracticeWordListDialog"
:data="currentStudy"
v-model="showPracticeWordListDialog"
/>
<ShufflePracticeSettingDialog
v-model="showShufflePracticeSettingDialog"
@ok="onShufflePracticeSettingOk"/>
v-model="showShufflePracticeSettingDialog"
@ok="onShufflePracticeSettingOk"/>
<CollectNotice/>
</template>
<style scoped lang="scss">
.stat {
@apply w-31% box-border flex flex-col items-center justify-center rounded-xl p-2 bg-[var(--bg-history)];
border: 1px solid gainsboro;
.num {
@apply color-[#409eff] text-4xl font-bold;
}
.txt {
@apply color-gray-500;
}
}
</style>

View File

@@ -3,12 +3,12 @@
import { inject, Ref, watch } from "vue"
import { usePracticeStore } from "@/stores/practice.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { PracticeData, WordPracticeType, ShortcutKey } from "@/types/types.ts";
import {PracticeData, WordPracticeType, ShortcutKey, TaskWords} from "@/types/types.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import Progress from '@/components/base/Progress.vue'
const statisticsStore = usePracticeStore()
const statStore = usePracticeStore()
const settingStore = useSettingStore()
defineProps<{
@@ -34,7 +34,7 @@ function format(val: number, suffix: string = '', check: number = -1) {
const status = $computed(() => {
if (isTypingWrongWord.value) return '复习错词'
let str = ''
switch (statisticsStore.step) {
switch (statStore.step) {
case 0:
str += `学习新词`
break
@@ -99,22 +99,22 @@ const progress = $computed(() => {
<div class="name">{{ status }}</div>
</div>
<div class="row">
<div class="num">{{ statisticsStore.total }}</div>
<div class="num">{{ statStore.total }}</div>
<div class="line"></div>
<div class="name">单词总数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
<div class="num">{{ format(statStore.inputWordNumber, '', 0) }}</div>
<div class="line"></div>
<div class="name">总输入数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.wrong, '', 0) }}</div>
<div class="num">{{ format(statStore.wrong, '', 0) }}</div>
<div class="line"></div>
<div class="name">总错误数</div>
</div>
</div>
<div class="flex gap-2 justify-center items-center">
<div class="flex gap-2 justify-center items-center">
<BaseIcon
:class="!isSimple?'collect':'fill'"
@click="$emit('toggleSimple')"

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import Slider from "@/components/base/Slider.vue";
import { defineAsyncComponent, watch } from "vue";
import { useBaseStore } from "@/stores/base.ts";
import {defineAsyncComponent, watch} from "vue";
import {useBaseStore} from "@/stores/base.ts";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -20,6 +20,7 @@ let min = $ref(0)
watch(() => model.value, (n) => {
if (n) {
num = Math.floor(store.sdict.lastLearnIndex / 3)
num = num > 50 ? 50 : num
min = num < 10 ? num : 10
}
})

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import {WordPracticeType, ShortcutKey, Word, WordPracticeMode} from "@/types/types.ts";
import {ShortcutKey, Word, WordPracticeType} 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, useEvents} from "@/utils/eventBus.ts";
import {inject, onMounted, onUnmounted, Ref, watch} from "vue";
import {onMounted, onUnmounted, watch} from "vue";
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {getDefaultWord} from "@/types/func.ts";
import {_nextTick, last, sleep} from "@/utils";
import {_nextTick, last} 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,
@@ -104,9 +103,7 @@ function repeat() {
wordRepeatCount++
inputLock = false
if (settingStore.wordSound) {
volumeIconRef?.play()
}
if (settingStore.wordSound) volumeIconRef?.play()
}, settingStore.waitTimeForChangeWord)
}
@@ -153,21 +150,40 @@ function unknown(e) {
async function onTyping(e: KeyboardEvent) {
debugger
let word = props.word.word
// 输入完成会锁死不能再输入
if (inputLock) {
// 因为输入完成会锁死不能再输入,所以在这里判断空格键切换到下一个单词
if (e.code === 'Space' && input.toLowerCase() === word.toLowerCase()) {
showWordResult = inputLock = false
emit('complete')
} else {
//当显示单词时,提示用户正确按键
if (showWordResult) {
pressNumber++
if (pressNumber >= 3) {
Toast.info(right ? '请按空格键切换' : '请按删除键重新输入', {duration: 2000})
pressNumber = 0
//判断是否是空格键以便切换到下一个单词
if (e.code === 'Space') {
//正确时就切换到下一个单词
if (right) {
showWordResult = inputLock = false
emit('complete')
} else {
if (showWordResult) {
// 错误时,提示用户按删除键,仅默写需要提示
pressNumber++
if (pressNumber >= 3) {
Toast.info('请按删除键重新输入', {duration: 2000})
pressNumber = 0
}
}
}
} else {
//当正确时,提醒用户按空格键切下一个
if (right) {
pressNumber++
if (pressNumber >= 3) {
Toast.info('请按空格键继续', {duration: 2000})
pressNumber = 0
}
} else {
//当错误时,按任意键重新输入
showWordResult = inputLock = false
input = wrong = ''
onTyping(e)
}
}
return
}
inputLock = true
@@ -186,12 +202,12 @@ async function onTyping(e: KeyboardEvent) {
} else {
//未显示单词,则播放正确音乐,并在后面设置为 showWordResult 为 true 来显示单词
playCorrect()
volumeIconRef?.play()
if (settingStore.wordSound) volumeIconRef?.play()
}
} else {
//错误处理
playBeep()
volumeIconRef?.play()
if (settingStore.wordSound) volumeIconRef?.play()
emit('wrong')
}
showWordResult = true
@@ -209,7 +225,7 @@ async function onTyping(e: KeyboardEvent) {
if (settingStore.ignoreCase) {
right = letter.toLowerCase() === word[input.length].toLowerCase()
} else {
right = letter === props.word.word[input.length]
right = letter === word[input.length]
}
if (right) {
input += letter
@@ -219,11 +235,11 @@ async function onTyping(e: KeyboardEvent) {
emit('wrong')
wrong = letter
playBeep()
volumeIconRef?.play()
setTimeout(()=>{
if (settingStore.wordSound) volumeIconRef?.play()
setTimeout(() => {
if (settingStore.inputWrongClear) input = ''
wrong = ''
},500)
}, 500)
}
// 更新当前单词信息
updateCurrentWordInfo();

View File

@@ -5,14 +5,11 @@ export interface PracticeState {
startDate: number,
spend: number,
total: number,
index: number,//当前输入的第几个用于和total计算进度
newWordNumber: number,
reviewWordNumber: number,
writeWordNumber: number,
inputWordNumber: number,//当前总输入了多少个单词(不包含跳过)
wrong: number,
startIndex: number,
endIndex: number,
}
export const usePracticeStore = defineStore('practice', {
@@ -22,9 +19,6 @@ export const usePracticeStore = defineStore('practice', {
spend: 0,
startDate: Date.now(),
total: 0,
index: 0,
startIndex: 0,
endIndex: 0,
newWordNumber: 0,
reviewWordNumber: 0,
writeWordNumber: 0,

View File

@@ -11,6 +11,7 @@ export default defineConfig({
'bg-reverse-white': 'bg-[var(--color-reverse-white)]',
'bg-reverse-black': 'bg-[var(--color-reverse-black)]',
'color-main': 'color-[var(--color-main-text)]',
'color-link': 'color-[var(--color-link)]',
'gap-space': 'gap-[var(--space)]',
'p-space': 'p-[var(--space)]',
'px-space': 'px-[var(--space)]',