This commit is contained in:
Zyronon
2025-12-23 19:48:04 +08:00
committed by GitHub
parent 30be8e1cf5
commit 560546c3bc
9 changed files with 392 additions and 224 deletions

View File

@@ -52,6 +52,7 @@
--btn-primary: rgb(75, 85, 99);
--btn-info: white;
--btn-info-hover: #eaeaea;
--color-primary: #E6E8EB;
--color-second: rgb(247, 247, 247);
@@ -119,7 +120,8 @@ html.dark {
--color-sub-gray: #383737;
--color-scrollbar: rgb(92, 93, 94);
--btn-info: transparent;
--btn-info: #1b1b1b;
--btn-info-hover: #3a3a3a;
--color-input-color: white;
--color-input-bg: rgba(14, 18, 23, 1);
@@ -536,3 +538,8 @@ a {
margin-left: 0 !important;
}
}
.target-number {
@apply text-3xl! mx-2;
color: rgb(176, 116, 211)!important;
}

View File

@@ -51,7 +51,7 @@ defineEmits(['click'])
justify-content: center;
outline: none;
text-align: center;
transition: .1s;
transition: all .3s;
user-select: none;
vertical-align: middle;
white-space: nowrap;
@@ -121,7 +121,7 @@ defineEmits(['click'])
color: var(--color-main-text);
&:hover:not(.disabled) {
opacity: 0.6;
background: var(--btn-info-hover);
}
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import {defineComponent, ref, useAttrs, watch, computed} from 'vue';
import {ref, useAttrs, watch, computed} from 'vue';
import Close from "@/components/icon/Close.vue";
import {useDisableEventListener} from "@/hooks/event.ts";

View File

@@ -79,6 +79,8 @@ let data = $ref<PracticeData>({
excludeWords: [],
})
let isTypingWrongWord = ref(false)
// 独立模式的当前单词列表阶段:'new' | 'review' | 'write' | 'finished'
let currentWordListStage = $ref<'new' | 'review' | 'write' | 'finished'>('new')
provide('isTypingWrongWord', isTypingWrongWord)
provide('practiceData', data)
@@ -209,8 +211,52 @@ function initData(initVal: TaskWords, init: boolean = false) {
// taskWords = initVal
//不能直接赋值,会导致 inject 的数据为默认值
taskWords = Object.assign(taskWords, initVal)
//如果 shuffle 数组不为空,就说明是复习
if (taskWords.shuffle.length === 0) {
// 检查是否为独立模式
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.new.length === 0) {
if (taskWords.review.length) {
settingStore.wordPracticeType = WordPracticeType.Identify
@@ -277,6 +323,7 @@ const nextWord: Word = $computed(() => {
watch(
() => settingStore.wordPracticeType,
n => {
// Free 模式不自动设置System 模式和独立模式都需要设置
if (settingStore.wordPracticeMode === WordPracticeMode.Free) return
switch (n) {
case WordPracticeType.Spell:
@@ -363,6 +410,59 @@ 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) {

View File

@@ -122,10 +122,16 @@ async function init() {
loading = false
}
function startPractice() {
function startPractice(practiceMode?: WordPracticeMode): void {
if (store.sdict.id) {
if (!store.sdict.words.length) {
return Toast.warning('没有单词可学习!')
Toast.warning('没有单词可学习!')
return
}
// 如果传入了独立模式,临时设置 wordPracticeMode
if (practiceMode !== undefined) {
localStorage.removeItem(PracticeSaveWordKey.key)
settingStore.wordPracticeMode = practiceMode
}
window.umami?.track('startStudyWord', {
name: store.sdict.name,
@@ -138,6 +144,7 @@ function startPractice() {
//把是否是第一次设置为false
settingStore.first = false
nav('practice-words/' + store.sdict.id, {}, { taskWords: currentStudy })
// 注意:不恢复 originalMode因为练习过程中需要保持独立模式
} else {
window.umami?.track('no-dict')
Toast.warning('请先选择一本词典')
@@ -381,13 +388,14 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</template>
</div>
<div class="flex items-end mt-4">
<div class="flex items-end mt-4 gap-4 btn-no-margin">
<BaseButton
size="large"
class="flex-1"
:disabled="!store.sdict.id"
:loading="loading"
@click="startPractice"
v-if="false"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
@@ -395,10 +403,10 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</BaseButton>
<div v-if="false" class="w-full flex box-border cp color-white">
<div class="w-full flex box-border cp color-white">
<div
@click="startPractice"
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50"
@click="startPractice()"
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] transition-all duration-300 hover:opacity-50"
>
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl" />
@@ -406,40 +414,54 @@ let isNewHost = $ref(window.location.host === Host)
<div class="relative group">
<div
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border"
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border transition-all duration-300"
>
<IconFluentChevronDown20Regular />
</div>
<div
class="space-y-2 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"
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"
>
<div>
<BaseButton
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" />
</div>
</BaseButton>
</div>
<div>
<BaseButton
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" />
</div>
</BaseButton>
</div>
<BaseButton
size="large"
class="w-30"
type="primary"
@click="startPractice(WordPracticeMode.FollowWriteOnly)"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">跟写</span>
</div>
</BaseButton>
<BaseButton
size="large"
class="w-30"
type="primary"
@click="startPractice(WordPracticeMode.IdentifyOnly)"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">自测</span>
</div>
</BaseButton>
<BaseButton
size="large"
class="w-30"
type="primary"
@click="startPractice(WordPracticeMode.ListenOnly)"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">听写</span>
</div>
</BaseButton>
<BaseButton
size="large"
class="w-30"
type="primary"
@click="startPractice(WordPracticeMode.DictationOnly)"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">默写</span>
</div>
</BaseButton>
</div>
</div>
</div>
@@ -447,7 +469,6 @@ let isNewHost = $ref(window.location.host === Host)
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large"
type="orange"
:loading="loading"
@click="check(() => (showShufflePracticeSettingDialog = true))"
>
@@ -456,6 +477,18 @@ let isNewHost = $ref(window.location.host === Host)
<IconFluentArrowShuffle20Filled class="text-xl" />
</div>
</BaseButton>
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large"
:loading="loading"
@click="check(() => (showShufflePracticeSettingDialog = true))"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">自由练习</span>
<IconStreamlineColorPenDrawFlat class="text-xl" />
</div>
</BaseButton>
</div>
</div>
</div>

View File

@@ -1,36 +1,36 @@
<script setup lang="ts">
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 BaseIcon from "@/components/BaseIcon.vue";
import Tooltip from "@/components/base/Tooltip.vue";
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 BaseIcon from '@/components/BaseIcon.vue'
import Tooltip from '@/components/base/Tooltip.vue'
import Progress from '@/components/base/Progress.vue'
import SettingDialog from "@/components/setting/SettingDialog.vue";
import SettingDialog from '@/components/setting/SettingDialog.vue'
import BaseButton from '@/components/BaseButton.vue'
const statStore = usePracticeStore()
const settingStore = useSettingStore()
defineProps<{
showEdit?: boolean,
isCollect: boolean,
showEdit?: boolean
isCollect: boolean
isSimple: boolean
}>()
const emit = defineEmits<{
toggleCollect: [],
toggleSimple: [],
edit: [],
skip: [],
skipStep:[]
toggleCollect: []
toggleSimple: []
edit: []
skip: []
skipStep: []
}>()
let practiceData = inject<PracticeData>('practiceData')
let isTypingWrongWord = inject<Ref<boolean>>('isTypingWrongWord')
function format(val: number, suffix: string = '', check: number = -1) {
return val === check ? '-' : (val + suffix)
return val === check ? '-' : val + suffix
}
const status = $computed(() => {
@@ -80,26 +80,23 @@ function getStepStr(step: number) {
const progress = $computed(() => {
if (!practiceData.words.length) return 0
return ((practiceData.index / practiceData.words.length) * 100)
return (practiceData.index / practiceData.words.length) * 100
})
</script>
<template>
<div class="footer">
<Tooltip :title="settingStore.showToolbar?'收起':'展开'">
<Tooltip :title="settingStore.showToolbar ? '收起' : '展开'">
<IconFluentChevronLeft20Filled
@click="settingStore.showToolbar = !settingStore.showToolbar"
class="arrow"
:class="!settingStore.showToolbar && 'down'"
color="#999"/>
@click="settingStore.showToolbar = !settingStore.showToolbar"
class="arrow"
:class="!settingStore.showToolbar && 'down'"
color="#999"
/>
</Tooltip>
<div class="bottom">
<Progress :percentage="progress"
:stroke-width="8"
color="#69b1ff"
:show-text="false"/>
<Progress :percentage="progress" :stroke-width="8" color="#69b1ff" :show-text="false" />
<div class="flex justify-between items-center">
<div class="stat">
@@ -109,7 +106,7 @@ const progress = $computed(() => {
<div class="name">{{ status }}</div>
</div>
<div class="row">
<!-- <div class="num">{{ statStore.spend }}分钟</div>-->
<!-- <div class="num">{{ statStore.spend }}分钟</div>-->
<div class="num">{{ Math.floor(statStore.spend / 1000 / 60) }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
@@ -126,72 +123,93 @@ const progress = $computed(() => {
</div>
</div>
<div class="flex gap-2 justify-center items-center" id="toolbar-icons">
<SettingDialog type="word"/>
<SettingDialog type="word" />
<BaseIcon
v-if="statStore.step < 9"
@click="emit('skipStep')"
:title="`跳到下一阶段:${getStepStr(statStore.step+1)}`">
<IconFluentArrowRight16Regular/>
</BaseIcon>
<BaseIcon
:class="!isSimple?'collect':'fill'"
@click="$emit('toggleSimple')"
:title="(!isSimple ? '标记为已掌握' : '取消标记已掌握')+`(${settingStore.shortcutKeyMap[ShortcutKey.ToggleSimple]})`">
<IconFluentCheckmarkCircle16Regular v-if="!isSimple"/>
<IconFluentCheckmarkCircle16Filled v-else/>
</BaseIcon>
<BaseIcon
:class="!isCollect?'collect':'fill'"
@click.stop="$emit('toggleCollect')"
:title="(!isCollect ? '收藏' : '取消收藏')+`(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`">
<IconFluentStarAdd16Regular v-if="!isCollect"/>
<IconFluentStar16Filled v-else/>
</BaseIcon>
<BaseIcon
@click="emit('skip')"
:title="`跳过当前单词(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`">
<IconFluentArrowBounce20Regular class="transform-rotate-180"/>
</BaseIcon>
<BaseIcon
@click="settingStore.dictation = !settingStore.dictation"
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
v-if="statStore.step < 9"
@click="emit('skipStep')"
:title="`跳到下一阶段:${getStepStr(statStore.step + 1)}`"
>
<IconFluentEyeOff16Regular v-if="settingStore.dictation"/>
<IconFluentEye16Regular v-else/>
<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"
>
<BaseButton size="large" 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" />
<span>
{{
(!isSimple ? '标记为已掌握' : '取消标记已掌握') +
`(${settingStore.shortcutKeyMap[ShortcutKey.ToggleSimple]})`
}}</span
>
</div>
</BaseButton>
<BaseButton size="large" 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" />
<span>
{{
(!isCollect ? '收藏' : '取消收藏') +
`(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`
}}</span
>
</div>
</BaseButton>
<BaseButton size="large" 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
>
</div>
</BaseButton>
</div>
<BaseIcon @click="emit('skip')">
<IconPhMicrosoftWordLogoLight />
</BaseIcon>
</div>
<BaseIcon
@click="settingStore.dictation = !settingStore.dictation"
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
>
<IconFluentEyeOff16Regular v-if="settingStore.dictation" />
<IconFluentEye16Regular v-else />
</BaseIcon>
<BaseIcon
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
@click="settingStore.translate = !settingStore.translate">
<IconFluentTranslate16Regular v-if="settingStore.translate"/>
<IconFluentTranslateOff16Regular v-else/>
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
@click="settingStore.translate = !settingStore.translate"
>
<IconFluentTranslate16Regular v-if="settingStore.translate" />
<IconFluentTranslateOff16Regular v-else />
</BaseIcon>
<BaseIcon
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`单词本(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`">
<IconFluentTextListAbcUppercaseLtr20Regular/>
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`单词本(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
>
<IconFluentTextListAbcUppercaseLtr20Regular />
</BaseIcon>
</div>
</div>
</div>
<div class="progress-wrap flex gap-3 items-center color-gray">
<span class="shrink-0">{{ status }}</span>
<Progress :percentage="progress"
:stroke-width="8"
color="#69b1ff"
:show-text="false"/>
<Progress :percentage="progress" :stroke-width="8" color="#69b1ff" :show-text="false" />
<div class="num">{{ `${practiceData.index + 1}/${practiceData.words.length}` }}</div>
</div>
</div>
</template>
<style scoped lang="scss">
.footer {
flex-shrink: 0;
width: var(--toolbar-width);
@@ -211,15 +229,15 @@ const progress = $computed(() => {
position: relative;
width: 100%;
box-sizing: border-box;
border-radius: .6rem;
border-radius: 0.6rem;
background: var(--color-second);
padding: .2rem var(--space) calc(.4rem + env(safe-area-inset-bottom, 0px)) var(--space);
padding: 0.2rem var(--space) calc(0.4rem + env(safe-area-inset-bottom, 0px)) var(--space);
border: 1px solid var(--color-item-border);
box-shadow: var(--shadow);
z-index: 10;
.stat {
margin-top: .5rem;
margin-top: 0.5rem;
display: flex;
justify-content: space-around;
gap: var(--stat-gap);
@@ -228,7 +246,7 @@ const progress = $computed(() => {
display: flex;
flex-direction: column;
align-items: center;
gap: .3rem;
gap: 0.3rem;
color: gray;
.line {
@@ -242,8 +260,8 @@ const progress = $computed(() => {
.progress-wrap {
width: var(--toolbar-width);
transition: all .3s;
padding: 0 .6rem;
transition: all 0.3s;
padding: 0 0.6rem;
box-sizing: border-box;
position: fixed;
bottom: 1rem;
@@ -255,9 +273,9 @@ const progress = $computed(() => {
top: -40%;
left: 50%;
cursor: pointer;
transition: all .5s;
transition: all 0.5s;
transform: rotate(-90deg);
padding: .5rem;
padding: 0.5rem;
font-size: 1.2rem;
&.down {
@@ -271,39 +289,39 @@ const progress = $computed(() => {
@media (max-width: 768px) {
.footer {
width: 100%;
.bottom {
padding: 0.3rem 0.5rem 0.5rem 0.5rem;
border-radius: 0.4rem;
.stat {
margin-top: 0.3rem;
gap: 0.2rem;
flex-direction: row;
overflow-x: auto;
.row {
min-width: 3.5rem;
gap: 0.2rem;
.num {
font-size: 0.8rem;
font-weight: bold;
}
.name {
font-size: 0.7rem;
}
}
}
// 移动端按钮组调整 - 改为网格布局
.flex.gap-2 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.4rem;
justify-content: center;
.base-icon {
padding: 0.3rem;
font-size: 1rem;
@@ -315,13 +333,13 @@ const progress = $computed(() => {
}
}
}
.progress-wrap {
width: 100%;
padding: 0 0.5rem;
bottom: 0.5rem;
}
.arrow {
font-size: 1rem;
padding: 0.3rem;
@@ -334,40 +352,40 @@ const progress = $computed(() => {
.footer {
.bottom {
padding: 0.2rem 0.3rem 0.3rem 0.3rem;
.stat {
margin-top: 0.2rem;
gap: 0.1rem;
.row {
min-width: 3rem;
gap: 0.1rem;
.num {
font-size: 0.7rem;
}
.name {
font-size: 0.6rem;
}
// 隐藏部分统计信息,只保留关键数据
&:nth-child(n+3) {
&:nth-child(n + 3) {
display: none;
}
}
}
.flex.gap-2 {
gap: 0.2rem;
.base-icon {
padding: 0.2rem;
font-size: 0.9rem;
}
}
}
.progress-wrap {
padding: 0 0.3rem;
bottom: 0.3rem;

View File

@@ -1,17 +1,16 @@
<script setup lang="ts">
import { _getAccomplishDays } from "@/utils";
import BaseButton from "@/components/BaseButton.vue";
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
import Slider from "@/components/base/Slider.vue";
import { defineAsyncComponent, watch } from "vue";
import { useSettingStore } from "@/stores/setting.ts";
import Toast from "@/components/base/toast/Toast.ts";
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import { useRuntimeStore } from "@/stores/runtime.ts";
import BaseInput from "@/components/base/BaseInput.vue";
import InputNumber from "@/components/base/InputNumber.vue";
import { _getAccomplishDays } from '@/utils'
import BaseButton from '@/components/BaseButton.vue'
import Checkbox from '@/components/base/checkbox/Checkbox.vue'
import Slider from '@/components/base/Slider.vue'
import { defineAsyncComponent, watch } from 'vue'
import { useSettingStore } from '@/stores/setting.ts'
import Toast from '@/components/base/toast/Toast.ts'
import ChangeLastPracticeIndexDialog from '@/pages/word/components/ChangeLastPracticeIndexDialog.vue'
import Tooltip from '@/components/base/Tooltip.vue'
import { useRuntimeStore } from '@/stores/runtime.ts'
import BaseInput from '@/components/base/BaseInput.vue'
import InputNumber from '@/components/base/InputNumber.vue'
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -21,11 +20,11 @@ const runtimeStore = useRuntimeStore()
const model = defineModel()
defineProps<{
showLeftOption: boolean,
showLeftOption: boolean
}>()
const emit = defineEmits<{
ok: [];
ok: []
}>()
let show = $ref(false)
@@ -35,7 +34,6 @@ let tempLastLearnIndex = $ref(0)
let temPracticeMode = $ref(0)
let tempDisableShowPracticeSettingDialog = $ref(false)
function changePerDayStudyNumber() {
runtimeStore.editDict.perDayStudyNumber = tempPerDayStudyNumber
runtimeStore.editDict.lastLearnIndex = tempLastLearnIndex
@@ -45,36 +43,42 @@ function changePerDayStudyNumber() {
emit('ok')
}
watch(() => model.value, (n) => {
if (n) {
if (runtimeStore.editDict.id) {
tempPerDayStudyNumber = runtimeStore.editDict.perDayStudyNumber
tempLastLearnIndex = runtimeStore.editDict.lastLearnIndex
temPracticeMode = settings.wordPracticeMode
tempWordReviewRatio = settings.wordReviewRatio
tempDisableShowPracticeSettingDialog = settings.disableShowPracticeSettingDialog
} else {
Toast.warning('请先选择一本词典')
watch(
() => model.value,
n => {
if (n) {
if (runtimeStore.editDict.id) {
tempPerDayStudyNumber = runtimeStore.editDict.perDayStudyNumber
tempLastLearnIndex = runtimeStore.editDict.lastLearnIndex
temPracticeMode = settings.wordPracticeMode
tempWordReviewRatio = settings.wordReviewRatio
tempDisableShowPracticeSettingDialog = settings.disableShowPracticeSettingDialog
} else {
Toast.warning('请先选择一本词典')
}
}
}
})
)
</script>
<template>
<Dialog
v-model="model"
title="学习设置"
padding
:footer="true"
@ok="changePerDayStudyNumber">
<Dialog v-model="model" title="学习设置" padding :footer="true" @ok="changePerDayStudyNumber">
<div class="target-modal color-main" id="mode">
<div class="center">
<div class="flex gap-4 text-center h-30 w-85">
<div class="mode-item" :class="temPracticeMode == 0 && 'active'" @click=" temPracticeMode = 0">
<div
class="mode-item"
:class="temPracticeMode == 0 && 'active'"
@click="temPracticeMode = 0"
>
<div class="title text-align-center">智能模式</div>
<div class="desc mt-2">自动规划学习复习听写默写</div>
</div>
<div class="mode-item" :class="temPracticeMode == 1 && 'active'" @click=" temPracticeMode = 1">
<div
class="mode-item"
:class="temPracticeMode == 1 && 'active'"
@click="temPracticeMode = 1"
>
<div class="title">自由模式</div>
<div class="desc mt-2">自由练习系统不强制复习与默写</div>
</div>
@@ -82,26 +86,36 @@ watch(() => model.value, (n) => {
</div>
<div class="text-center mt-4">
<span><span class="text-3xl mx-2 inner">{{ runtimeStore.editDict.length }}</span>个单词</span>
<span>预计<span
class="text-3xl mx-2 inner">{{
_getAccomplishDays(runtimeStore.editDict.length - tempLastLearnIndex, tempPerDayStudyNumber)
}}</span>天完成</span>
<span
><span class="target-number">{{ runtimeStore.editDict.length }}</span
>个单词</span
>
<span
>预计<span class="target-number">{{
_getAccomplishDays(
runtimeStore.editDict.length - tempLastLearnIndex,
tempPerDayStudyNumber
)
}}</span
>天完成</span
>
</div>
<div class="text-center mt-4 mb-8 flex gap-1 items-end justify-center">
<span>从第</span>
<div class="w-20">
<BaseInput v-model="tempLastLearnIndex"/>
<BaseInput class="target-number" v-model="tempLastLearnIndex" />
</div>
<span>个开始每日</span>
<div class="w-16">
<BaseInput v-model="tempPerDayStudyNumber"/>
<BaseInput class="target-number" v-model="tempPerDayStudyNumber" />
</div>
<span>个新词</span>
<template v-if="temPracticeMode === 0">
<span>复习</span>
<div class="inner -translate-y-1 mx-1">{{ tempPerDayStudyNumber * tempWordReviewRatio }}</div>
<div class="target-number">
{{ tempPerDayStudyNumber * tempWordReviewRatio }}
</div>
<span></span>
</template>
</div>
@@ -110,35 +124,41 @@ watch(() => model.value, (n) => {
<Tooltip title="复习词与新词的比例">
<div class="flex items-center gap-1 w-20">
<span>复习比</span>
<IconFluentQuestionCircle20Regular/>
<IconFluentQuestionCircle20Regular />
</div>
</Tooltip>
<InputNumber :min="0" :max="10" v-model="tempWordReviewRatio"/>
<InputNumber :min="0" :max="10" v-model="tempWordReviewRatio" />
</div>
<div class="flex mb-4 gap-space">
<span class="shrink-0 w-20">每日学习</span>
<Slider :min="10"
:step="10"
show-text
class="mt-1"
:max="200" v-model="tempPerDayStudyNumber"/>
<Slider
:min="10"
:step="10"
show-text
class="mt-1"
:max="200"
v-model="tempPerDayStudyNumber"
/>
</div>
<div class="flex gap-space">
<span class="shrink-0 w-20">学习进度</span>
<div class="flex-1">
<Slider :min="0"
:step="10"
show-text
class="my-1"
:max="runtimeStore.editDict.words.length" v-model="tempLastLearnIndex"/>
<Slider
:min="0"
:step="10"
show-text
class="my-1"
:max="runtimeStore.editDict.words.length"
v-model="tempLastLearnIndex"
/>
<BaseButton @click="show = true">从词典选起始位置</BaseButton>
</div>
</div>
</div>
<template v-slot:footer-left v-if="showLeftOption">
<div class="flex items-center">
<Checkbox v-model="tempDisableShowPracticeSettingDialog"/>
<Checkbox v-model="tempDisableShowPracticeSettingDialog" />
<Tooltip title="可在设置页面更改">
<span class="text-sm">保持默认不再显示</span>
</Tooltip>
@@ -146,23 +166,20 @@ watch(() => model.value, (n) => {
</template>
</Dialog>
<ChangeLastPracticeIndexDialog
v-model="show"
@ok="e => {
v-model="show"
@ok="
e => {
tempLastLearnIndex = e
show = false
}"
}
"
/>
</template>
<style scoped lang="scss">
.target-modal {
width: 35rem;
:deep(.inner) {
font-size: 1.8rem;
color: rgb(176, 116, 211)
}
.mode-item {
@apply w-50% border border-blue border-solid p-2 rounded-lg cursor-pointer;
@@ -212,7 +229,8 @@ watch(() => model.value, (n) => {
}
// 滑块控件
.flex.mb-4, .flex.mb-6 {
.flex.mb-4,
.flex.mb-6 {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;

View File

@@ -31,10 +31,10 @@ watch(() => model.value, (n) => {
:footer="true"
:padding="true"
@ok="emit('ok',num)">
<div class="target-modal color-main">
<div class="w-120 color-main">
<div class="flex gap-4 items-end mb-2">
<span>随机复习<span class="font-bold">{{ store.sdict.name }}</span></span>
<span class="text-3xl lh">{{ num }}</span>个单词
<span class="target-number">{{ num }}</span>个单词
</div>
<div class="flex gap-space">
<span class="shrink-0">随机数量</span>
@@ -45,25 +45,13 @@ watch(() => model.value, (n) => {
:max="store.sdict.lastLearnIndex"
v-model="num"/>
</div>
<div class="text-right">
<span class="text-sm text-gray-500">只能复习已学习过的单词</span>
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
.target-modal {
width: 30rem;
.lh {
color: rgb(176, 116, 211)
}
.mode-item {
@apply w-50% border border-blue border-solid p-2 rounded-lg cursor-pointer;
}
.active {
@apply bg-blue color-white;
}
}
</style>

View File

@@ -226,7 +226,11 @@ export enum PracticeArticleWordType {
//练习模式
export enum WordPracticeMode {
System = 0,
Free = 1
Free = 1,
DictationOnly = 2, // 独立默写模式
ListenOnly = 3, // 独立听写模式
IdentifyOnly = 4, // 独立自测模式
FollowWriteOnly = 5 // 独立跟写模式(内部会自动切换到 Spell
}
//练习类型