wip
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
//练习类型
|
||||
|
||||
Reference in New Issue
Block a user