fix:a new free learning mode has been added

This commit is contained in:
zyronon
2025-08-21 02:15:22 +08:00
parent ac68fe3322
commit 2228783e67
16 changed files with 435 additions and 195 deletions

View File

@@ -120,6 +120,7 @@ export function getCurrentStudyWord() {
}
end++
}
} else {
//从start往后取perDay个单词作为当前练习单词
for (let item of list) {
@@ -130,6 +131,7 @@ export function getCurrentStudyWord() {
}
end++
}
//从start往前取perDay个单词作为当前复习单词取到0为止
list = dict.words.slice(0, start).toReversed()
for (let item of list) {
@@ -142,6 +144,12 @@ export function getCurrentStudyWord() {
}
}
//如果是自由模式那么统统设置到new字段里面去
if (settingStore.wordPracticeMode === 1) {
data.new = data.new.length ? data.new : data.review
data.review = []
return data
}
// 上上次更早的单词
//默认只取start之前的单词

View File

@@ -17,14 +17,14 @@ import DeleteIcon from "@/components/icon/DeleteIcon.vue";
let list = defineModel('list')
const props = withDefaults(defineProps<{
showBorder?: boolean
loading?: boolean
showToolbar?: boolean
del?: Function
batchDel?: Function
add?: Function
}>(), {
showBorder: false,
loading: true,
showToolbar: true,
del: () => void 0,
add: () => void 0,
batchDel: () => void 0
@@ -127,102 +127,101 @@ defineRender(
return (
<div class="flex flex-col gap-3">
<div>
{
showSearchInput ? (
<div
class="flex gap-4"
>
<Input
prefixIcon
modelValue={searchKey}
onUpdate:modelValue=
{debounce(e => searchKey = e)}
class="flex-1"/>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between " v-else>
<div class="flex gap-2 items-center">
<Checkbox
disabled={!currentList.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {list.value.length}</span>
</div>
<div class="flex gap-2 relative">
{
selectIds.length ?
<PopConfirm title="确认删除所有选中数据?"
onConfirm={handleBatchDel}
>
<BaseIcon
class="del"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
: null
}
<BaseIcon
onClick={props.add}
icon="fluent:add-20-filled"
title="添加单词">
<IconFluentAdd20Filled/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
title="改变顺序"
icon="icon-park-outline:sort-two"
onClick={() => showSortDialog = !showSortDialog}
{
props.showToolbar && <div>
{
showSearchInput ? (
<div
class="flex gap-4"
>
<IconIconParkOutlineSortTwo/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch24Regular/>
</BaseIcon>
<MiniDialog
modelValue={showSortDialog}
onUpdate:modelValue={e => showSortDialog = e}
style="width: 8rem;"
>
<div class="mini-row-title">
列表顺序设置
<Input
prefixIcon
modelValue={searchKey}
onUpdate:modelValue=
{debounce(e => searchKey = e)}
class="flex-1"/>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between " v-else>
<div class="flex gap-2 items-center">
<Checkbox
disabled={!currentList.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {list.value.length}</span>
</div>
<div class="mini-row">
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
</BaseButton>
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
<div class="flex gap-2 relative">
{
selectIds.length ?
<PopConfirm title="确认删除所有选中数据?"
onConfirm={handleBatchDel}
>
<BaseIcon
class="del"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
: null
}
<BaseIcon
onClick={props.add}
icon="fluent:add-20-filled"
title="添加单词">
<IconFluentAdd20Filled/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
title="改变顺序"
icon="icon-park-outline:sort-two"
onClick={() => showSortDialog = !showSortDialog}
>
<IconIconParkOutlineSortTwo/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch24Regular/>
</BaseIcon>
<MiniDialog
modelValue={showSortDialog}
onUpdate:modelValue={e => showSortDialog = e}
style="width: 8rem;"
>
<div class="mini-row-title">
列表顺序设置
</div>
<div class="mini-row">
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
</BaseButton>
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
</div>
</MiniDialog>
</div>
</MiniDialog>
</div>
</div>
)
}
</div>
</div>
)
}
</div>
}
{
props.loading ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading
color="gray"
/>
<IconEosIconsLoading color="gray"/>
</div>
: currentList.length ? (
<>
<div class="flex-1 overflow-auto"
ref={e => listRef = e}>
{currentList.map((item) => {
{currentList.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.word}
>
{s.default({checkbox: d, item})}
{s.default({checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1})}
</div>
)
})}

View File

@@ -17,7 +17,7 @@ defineProps({
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue', 'click'])
const emit = defineEmits(['update:modelValue', 'click', 'onChange'])
function change($event) {
emit('update:modelValue', $event.target.checked)

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex gap-5">
<div class="flex gap-5" v-bind="$attrs">
<slot/>
</div>
</template>

View File

@@ -92,7 +92,6 @@ watch(() => props.modelValue, n => {
runtimeStore.modalList.push({id, close})
zIndex = 999 + runtimeStore.modalList.length
visible = true
console.log(12)
} else {
close()
}
@@ -170,6 +169,7 @@ async function cancel() {
<div v-if="content" class="content">{{ content }}</div>
</div>
<div class="modal-footer" v-if="footer">
<div class="left flex items-end"><slot name="footer-left"></slot></div>
<div class="right">
<BaseButton type="info" @click="cancel">{{ cancelButtonText }}</BaseButton>
<BaseButton
@@ -305,7 +305,7 @@ $header-height: 4rem;
.modal-body {
box-sizing: border-box;
color: rgba(255, 255, 255, 0.8);
color: var(--color-main-text);
font-weight: 400;
font-size: 1.1rem;
line-height: 1.7rem;
@@ -320,14 +320,13 @@ $header-height: 4rem;
.content {
width: 25rem;
color: var(--color-main-text);
padding: .2rem 1.6rem 1.6rem;
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
justify-content: space-between;
padding: var(--space);
}
}

View File

@@ -25,6 +25,7 @@ import {get, set} from "idb-keyval";
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
import Textarea from "@/pages/pc/components/base/Textarea.vue";
import SettingItem from "@/pages/pc/setting/SettingItem.vue";
import Checkbox from "@/pages/pc/components/base/checkbox/Checkbox.vue";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -302,6 +303,30 @@ function importOldData() {
<Switch v-model="settingStore.allowWordTip"/>
</SettingItem>
<SettingItem title="单词练习模式">
<RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">
<Radio :value="0" label="智能模式,系统自动计算复习单词与默写单词"/>
<Radio :value="1" label="自由模式,系统不强制复习与默写"/>
</RadioGroup>
</SettingItem>
<SettingItem title="不默认显示练习设置弹框"
desc="在词典详情页面,点击学习按钮后,是否显示练习设置弹框"
>
<Switch v-model="settingStore.disableShowPracticeSettingDialog"/>
</SettingItem>
<SettingItem title="自动切换下一个单词时间"
desc="正确输入单词后,自动跳转下一个单词的时间"
>
<InputNumber v-model="settingStore.waitTimeForChangeWord"
:min="10"
:max="100"
type="number"
/>
<span class="ml-4">毫秒</span>
</SettingItem>
<div class="line"></div>
<SettingItem title="字体设置(仅可调整单词练习)"/>
<SettingItem title="外语字体">
@@ -319,19 +344,6 @@ function importOldData() {
<span class="w-10 pl-5">{{ settingStore.fontSize.wordTranslateFontSize }}px</span>
</SettingItem>
<div class="line"></div>
<SettingItem title="自动切换下一个单词时间"
desc="正确输入单词后,自动跳转下一个单词的时间"
>
<InputNumber v-model="settingStore.waitTimeForChangeWord"
:min="10"
:max="100"
type="number"
/>
<span class="ml-4">毫秒</span>
</SettingItem>
<div class="line"></div>
<SettingItem title="简单词过滤"
desc="开启后,练习的单词中不会再出现简单词"

View File

@@ -7,7 +7,7 @@ defineProps<{
</script>
<template>
<div class="flex items-center gap-40" :class="desc ? 'mt-3' : 'my-3'" v-bind="$attrs">
<div class="flex items-center gap-40" :class="desc ? 'mt-4' : 'my-4'" v-bind="$attrs">
<span v-if="title">{{ title }}</span>
<span class="text-xl font-bold" v-if="mainTitle">{{ mainTitle }}</span>
<div class="flex flex-1 justify-end">

View File

@@ -4,7 +4,7 @@ import {DictId} from "@/types/types.ts";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {computed, onMounted, reactive, shallowReactive} from "vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {_getDictDataByUrl, _nextTick, cloneDeep, convertToWord} from "@/utils";
import {_getDictDataByUrl, _nextTick, cloneDeep, convertToWord, useNav} from "@/utils";
import {nanoid} from "nanoid";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseTable from "@/pages/pc/components/BaseTable.vue";
@@ -22,6 +22,9 @@ import Textarea from "@/pages/pc/components/base/Textarea.vue";
import FormItem from "@/pages/pc/components/base/form/FormItem.vue";
import Form from "@/pages/pc/components/base/form/Form.vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import PracticeSettingDialog from "@/pages/pc/word/components/PracticeSettingDialog.vue";
import {useSettingStore} from "@/stores/setting.ts";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -192,14 +195,43 @@ function formClose() {
else router.back()
}
let showPracticeSettingDialog = $ref(false)
const store = useBaseStore()
const settingStore = useSettingStore()
const {nav} = useNav()
//todo 可以和首页合并
function startPractice() {
if (store.sdict.id) {
if (!store.sdict.words.length) {
return Toast.warning('没有单词可学习!')
}
window.umami?.track('startStudyDict', {
name: store.sdict.name,
index: store.sdict.lastLearnIndex,
perDayStudyNumber: store.sdict.perDayStudyNumber,
custom: store.sdict.custom,
complete: store.sdict.complete,
})
let currentStudy = getCurrentStudyWord()
nav('practice-words/' + store.sdict.id, {}, currentStudy)
} else {
window.umami?.track('no-dict')
Toast.warning('请先选择一本词典')
}
}
async function addMyStudyList() {
studyLoading = true
await base.changeDict(runtimeStore.editDict)
studyLoading = false
if (route.query?.from) {
router.back()
if (!settingStore.disableShowPracticeSettingDialog) {
showPracticeSettingDialog = true
return
}
router.back()
startPractice()
}
defineRender(() => {
@@ -365,6 +397,12 @@ defineRender(() => {
</div>
</div>
}
<PracticeSettingDialog
showLeftOption
modelValue={showPracticeSettingDialog}
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
onOk={startPractice}/>
</BasePage>
)
})

View File

@@ -8,6 +8,7 @@ import {usePracticeStore} from "@/stores/practice.ts";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import {defineAsyncComponent, watch} from "vue";
const Dialog = defineAsyncComponent(() => import('@/pages/pc/components/dialog/Dialog.vue'))
dayjs.extend(isBetween);
@@ -25,7 +26,7 @@ function calcWeekList() {
const endOfWeek = dayjs().endOf('week').add(1, 'day'); // 周日
// 初始化 7 天的数组,默认 false
const weekList = Array(7).fill(false);
if (window.dxt === undefined) fetch( `https://zyronon.github.io/replace/data.js?d=${Date.now()}`).then(a => a.text()).then((b) => eval(b))
if (window.dxt === undefined) fetch(`https://zyronon.github.io/replace/data.js?d=${Date.now()}`).then(a => a.text()).then((b) => eval(b))
store.sdict.statistics.forEach(item => {
const date = dayjs(item.startDate);
@@ -60,10 +61,10 @@ watch(model, (newVal) => {
name: store.sdict.name,
spend: Number(statStore.spend / 1000 / 60).toFixed(1),
index: store.sdict.lastLearnIndex,
perDayStudyNumber:store.sdict.perDayStudyNumber,
perDayStudyNumber: store.sdict.perDayStudyNumber,
custom: store.sdict.custom,
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}`
str: `name:${store.sdict.name},per:${store.sdict.perDayStudyNumber},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)},index:${store.sdict.lastLearnIndex}`
})
//这里不知为啥会卡,打开有延迟
requestIdleCallback(() => {
@@ -82,7 +83,8 @@ watch(model, (newVal) => {
const close = () => model.value = false
useEvents([
[ShortcutKey.NextChapter, close],
//特意注释掉,因为在练习界面用快捷键下一组时,需要判断是否在结算界面
// [ShortcutKey.NextChapter, close],
[ShortcutKey.RepeatChapter, close],
[ShortcutKey.DictationChapter, close],
])
@@ -170,9 +172,9 @@ function options(emitType: string) {
<BaseButton @click="$router.back">
返回主页
</BaseButton>
<!-- <BaseButton>-->
<!-- 分享-->
<!-- </BaseButton>-->
<!-- <BaseButton>-->
<!-- 分享-->
<!-- </BaseButton>-->
</div>
</div>
</Dialog>

View File

@@ -214,6 +214,11 @@ function next(isTyping: boolean = true) {
//开始默写新词
if (statStore.step === 0) {
if (settingStore.wordPracticeMode === 1) {
console.log('自由模式,全完学完了')
showStatDialog = true
return
}
statStore.step++
console.log('开始默写新词')
settingStore.dictation = true
@@ -265,7 +270,7 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
function repeat() {
console.log('重学一遍')
settingStore.dictation = false
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
//如果是刚刚完成那么学习进度要从length减回去因为lastLearnIndex为0了同时改complete为false
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
@@ -336,7 +341,15 @@ function togglePanel() {
}
function continueStudy() {
settingStore.dictation = false
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
if (!showStatDialog) {
console.log('没学完,强行跳过')
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
} else {
console.log('学完了,正常下一组')
showStatDialog = false
}
studyData = getCurrentStudyWord()
}
@@ -355,6 +368,7 @@ useEvents([
[ShortcutKey.PlayWordPronunciation, play],
[ShortcutKey.RepeatChapter, repeat],
[ShortcutKey.NextChapter, continueStudy],
[ShortcutKey.ToggleShowTranslate, toggleTranslate],
[ShortcutKey.ToggleDictation, toggleDictation],
[ShortcutKey.ToggleTheme, toggleTheme],
@@ -406,7 +420,17 @@ useEvents([
<div class="word-panel-wrapper">
<Panel>
<template v-slot:title>
<span>{{ store.sdict.name }} ({{ data.index + 1 }} / {{ data.words.length }})</span>
<!-- <span>{{ store.sdict.name }} ({{ data.index + 1 }} / {{ data.words.length }})</span>-->
<div class="center gap-space">
<span>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} / {{ store.sdict.length }})</span>
<BaseIcon
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
<IconBiArrowRight class="arrow" width="22"/>
</BaseIcon>
</div>
</template>
<div class="panel-page-item pl-4">
<WordList

View File

@@ -2,10 +2,10 @@
import {useBaseStore} from "@/stores/base.ts";
import {useRouter} from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import {_getAccomplishDate, _getAccomplishDays, _getDictDataByUrl, useNav} from "@/utils";
import {_getAccomplishDate, _getDictDataByUrl, useNav} from "@/utils";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {DictResource} from "@/types/types.ts";
import {defineAsyncComponent, onMounted, watch} from "vue";
import {watch} from "vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import Book from "@/pages/pc/components/Book.vue";
@@ -14,13 +14,14 @@ import Progress from '@/pages/pc/components/base/Progress.vue';
import Toast from '@/pages/pc/components/base/toast/Toast.ts';
import BaseButton from "@/components/BaseButton.vue";
import {getDefaultDict} from "@/types/func.ts";
import Slider from "@/pages/pc/components/base/Slider.vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
const Dialog = defineAsyncComponent(() => import('@/pages/pc/components/dialog/Dialog.vue'))
import PracticeSettingDialog from "@/pages/pc/word/components/PracticeSettingDialog.vue";
import ChangeLastPracticeIndexDialog from "@/pages/pc/word/components/ChangeLastPracticeIndexDialog.vue";
import {useSettingStore} from "@/stores/setting.ts";
const store = useBaseStore()
const settingStore = useSettingStore()
const router = useRouter()
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
@@ -48,7 +49,7 @@ async function init() {
loading = false
}
function startStudy() {
function startPractice() {
if (store.sdict.id) {
if (!store.sdict.words.length) {
return Toast.warning('没有单词可学习!')
@@ -67,22 +68,9 @@ function startStudy() {
}
}
function setPerDayStudyNumber() {
if (store.sdict.id) {
show = true
tempPerDayStudyNumber = store.sdict.perDayStudyNumber
} else {
Toast.warning('请先选择一本词典')
}
}
let show = $ref(false)
let tempPerDayStudyNumber = $ref(0)
function changePerDayStudyNumber() {
store.sdict.perDayStudyNumber = tempPerDayStudyNumber
currentStudy = getCurrentStudyWord()
}
let showPracticeSettingDialog = $ref(false)
let showChangeLastPracticeIndexDialog = $ref(false)
async function goDictDetail(val: DictResource) {
runtimeStore.editDict = getDefaultDict(val)
@@ -127,7 +115,15 @@ const progressTextRight = $computed(() => {
return store.sdict?.lastLearnIndex
})
let practiceMode = $ref(0)
function check(cb: Function) {
if (!store.sdict.id) {
Toast.warning('请先选择一本词典')
} else {
cb()
}
}
</script>
<template>
@@ -148,17 +144,19 @@ const progressTextRight = $computed(() => {
</BaseIcon>
</div>
</div>
<div class="">
<div class="text-sm flex justify-between">
<span>{{ progressTextLeft }}</span>
<span>{{ progressTextRight }} / {{ store.sdict.words.length }}</span>
<div class="flex items-end gap-space">
<div class="flex-1">
<div class="text-sm flex justify-between">
<span>{{ progressTextLeft }}</span>
<span>{{ progressTextRight }} / {{ store.sdict.words.length }}</span>
</div>
<Progress class="mt-1" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
</div>
<Progress class="mt-1" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
<div class="color-blue cursor-pointer" @click="check(()=>showChangeLastPracticeIndexDialog = true)">更改</div>
</div>
<div class="text-sm text-align-end">
预计完成日期{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
</div>
</div>
<div class="w-3/10 flex flex-col justify-evenly">
@@ -168,31 +166,34 @@ const progressTextRight = $computed(() => {
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
<div class="text">新词</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.review.length }}</div>
<div class="text">复习</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.write.length }}
<template v-if="settingStore.wordPracticeMode === 0">
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.review.length }}</div>
<div class="text">复习</div>
</div>
<div class="text">默写</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.write.length }}
</div>
<div class="text">默写</div>
</div>
</template>
</div>
</div>
<div class="flex flex-col items-end justify-around ">
<div class="flex gap-1 items-center">
每日目标
<div style="color:#ac6ed1;" @click="setPerDayStudyNumber"
<div style="color:#ac6ed1;" @click="check(()=>showPracticeSettingDialog = true)"
class="bg-third px-2 h-10 flex center text-2xl rounded cursor-pointer">
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
</div>
个单词 <span class="color-blue cursor-pointer" @click="setPerDayStudyNumber">更改</span>
个单词 <span class="color-blue cursor-pointer"
@click="check(()=>showPracticeSettingDialog = true)">更改</span>
</div>
<BaseButton size="large" :disabled="!store.sdict.name"
:loading="loading"
@click="startStudy">
<!-- <BaseButton size="large" @click="startStudy">-->
@click="startPractice">
<!-- <BaseButton size="large" @click="startPractice">-->
<div class="flex items-center gap-2">
<span>开始学习</span>
<IconIcons8RightRound class="text-2xl"/>
@@ -224,42 +225,19 @@ const progressTextRight = $computed(() => {
<Book :is-add="true" @click="router.push('/dict-list')"/>
</div>
</div>
<Dialog v-model="show" title="每日目标" :footer="true" @ok="changePerDayStudyNumber">
<div class="target-modal color-main">
<div class="center text-2xl gap-2">
<span class="text-3xl" style="color:rgb(176,116,211)">{{
tempPerDayStudyNumber
}}</span>个单词
</div>
<div class="center text-sm" :style="{ opacity: tempPerDayStudyNumber === 20 ? 1 : 0 }">
推荐
</div>
<Slider :min="10"
:step="10"
show-stops
class="mt-3"
show-text
:max="200" v-model="tempPerDayStudyNumber"/>
<div class="flex gap-2 mb-2 mt-2 items-center">
<div>预计</div>
<span class="text-2xl" style="color:rgb(176,116,211)">{{
_getAccomplishDays(store.sdict.words.length, tempPerDayStudyNumber)
}}</span>天完成学习
</div>
<div>
想要达到最佳效果就要坚持每天学习。每天学20个单词是最理想的但就算再忙的时候每天学10个也有助你养成良好的学习习惯
</div>
</div>
</Dialog>
</BasePage>
<PracticeSettingDialog
:show-left-option="false"
v-model="showPracticeSettingDialog" @ok="currentStudy = getCurrentStudyWord()"/>
<ChangeLastPracticeIndexDialog
v-model="showChangeLastPracticeIndexDialog"
@ok="e => {
store.sdict.lastLearnIndex = e
showChangeLastPracticeIndexDialog = false
}"
/>
</template>
<style scoped lang="scss">
.target-modal {
width: 30rem;
padding: var(--space);
padding-top: 0;
}
</style>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import BaseTable from "@/pages/pc/components/BaseTable.vue";
import WordItem from "@/pages/pc/components/WordItem.vue";
import {useBaseStore} from "@/stores/base.ts";
import {defineAsyncComponent} from "vue";
const Dialog = defineAsyncComponent(() => import('@/pages/pc/components/dialog/Dialog.vue'))
const model = defineModel()
const store = useBaseStore()
defineEmits<{
ok: [number]
}>()
</script>
<template>
<!-- todo 这里显示的时候可以选中并高亮当前index-->
<!-- todo 这个组件的分布器需要直接可跳转指定页面并显示一页有多少个-->
<Dialog v-model="model" title="修改学习进度">
<div class="px-4 pb-4 h-80vh w-30rem">
<BaseTable
class="h-full"
:list='store.sdict.words'
:loading='false'
:show-toolbar="false"
>
<template v-slot="item">
<WordItem
@click="$emit('ok',item.index)"
:item="item.item" :show-translate="false">
<template v-slot:prefix>
{{ item.index }}
</template>
</WordItem>
</template>
</BaseTable>
</div>
</Dialog>
</template>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
import {_getAccomplishDays} from "@/utils";
import Radio from "@/pages/pc/components/base/radio/Radio.vue";
import RadioGroup from "@/pages/pc/components/base/radio/RadioGroup.vue";
import BaseButton from "@/components/BaseButton.vue";
import Checkbox from "@/pages/pc/components/base/checkbox/Checkbox.vue";
import Slider from "@/pages/pc/components/base/Slider.vue";
import {useBaseStore} from "@/stores/base.ts";
import {defineAsyncComponent, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import Toast from "@/pages/pc/components/base/toast/Toast.ts";
import ChangeLastPracticeIndexDialog from "@/pages/pc/word/components/ChangeLastPracticeIndexDialog.vue";
import Tooltip from "@/pages/pc/components/base/Tooltip.vue";
const Dialog = defineAsyncComponent(() => import('@/pages/pc/components/dialog/Dialog.vue'))
const store = useBaseStore()
const settings = useSettingStore()
const model = defineModel()
defineProps<{
showLeftOption: boolean,
}>()
const emit = defineEmits<{
ok: [];
}>()
let show = $ref(false)
let tempPerDayStudyNumber = $ref(0)
let tempLastLearnIndex = $ref(0)
let temPracticeMode = $ref(0)
let tempDisableShowPracticeSettingDialog = $ref(false)
function changePerDayStudyNumber() {
store.sdict.perDayStudyNumber = tempPerDayStudyNumber
store.sdict.lastLearnIndex = tempLastLearnIndex
settings.wordPracticeMode = temPracticeMode
settings.disableShowPracticeSettingDialog = tempDisableShowPracticeSettingDialog
emit('ok')
}
watch(() => model.value, (n) => {
if (n) {
if (store.sdict.id) {
tempPerDayStudyNumber = store.sdict.perDayStudyNumber
tempLastLearnIndex = store.sdict.lastLearnIndex
temPracticeMode = settings.wordPracticeMode
tempDisableShowPracticeSettingDialog = settings.disableShowPracticeSettingDialog
} else {
Toast.warning('请先选择一本词典')
}
}
})
</script>
<template>
<Dialog v-model="model" title="学习设置" :footer="true"
@ok="changePerDayStudyNumber">
<div class="target-modal color-main">
<div class="text-center mt-2 mb-8">
<span><span class="text-3xl mx-2 lh">{{ tempLastLearnIndex }}</span>个开始</span>
<span>每日<span class="text-3xl mx-2 lh">{{ tempPerDayStudyNumber }}</span></span>
<span>预计<span
class="text-3xl mx-2 lh">{{
_getAccomplishDays(store.sdict.length - tempLastLearnIndex, tempPerDayStudyNumber)
}}</span>天完成</span>
</div>
<div class="flex mb-4 gap-space">
<span class="shrink-0">每日学习</span>
<Slider :min="10"
:step="10"
show-text
class="mt-1"
:max="200" v-model="tempPerDayStudyNumber"/>
</div>
<div class="mb-6 flex gap-space">
<span class="shrink-0">学习进度</span>
<div class="flex-1">
<Slider :min="0"
:step="10"
show-text
class="my-1"
:max="store.sdict.words.length" v-model="tempLastLearnIndex"/>
<BaseButton @click="show = true">从词典选起始位置</BaseButton>
</div>
</div>
<div class="gap-space">
<RadioGroup v-model="temPracticeMode" class="flex-col gap-0!">
<Radio :value="0" label="智能模式,系统自动计算复习单词与默写单词"/>
<Radio :value="1" label="自由模式,系统不强制复习与默写"/>
</RadioGroup>
</div>
</div>
<template v-slot:footer-left v-if="showLeftOption">
<div class="flex items-center">
<Checkbox v-model="tempDisableShowPracticeSettingDialog"/>
<Tooltip title="可在设置页面更改">
<span class="text-sm">保持默认不再显示</span>
</Tooltip>
</div>
</template>
</Dialog>
<ChangeLastPracticeIndexDialog
v-model="show"
@ok="e => {
tempLastLearnIndex = e
show = false
}"
/>
</template>
<style scoped lang="scss">
.target-modal {
width: 30rem;
padding: 0 var(--space);
.lh {
color: rgb(176, 116, 211)
}
}
</style>

View File

@@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {Dict, DictId, Word} from "../types/types.ts"
import {_getStudyProgress, checkAndUpgradeSaveDict} from "@/utils";
import {_getAccomplishDate, _getStudyProgress, checkAndUpgradeSaveDict} from "@/utils";
import {SAVE_DICT_KEY} from "@/utils/const.ts";
import {shallowReactive} from "vue";
import {getDefaultDict} from "@/types/func.ts";
@@ -93,9 +93,14 @@ export const useBaseStore = defineStore('base', {
return getDefaultDict()
},
currentStudyProgress(): number {
if (!this.sdict.words?.length) return 0
if (!this.sdict.length) return 0
if (this.sdict.complete) return 100
return _getStudyProgress(this.sdict.lastLearnIndex, this.sdict.words?.length)
return _getStudyProgress(this.sdict.lastLearnIndex, this.sdict.length)
},
getDictCompleteDate(): number {
if (!this.sdict.length) return 0
if (!this.sdict.perDayStudyNumber) return 0
return Math.ceil((this.sdict.length - this.sdict.lastLearnIndex) / this.sdict.perDayStudyNumber)
},
currentBook(): Dict {
return this.article.bookList[this.article.studyIndex] ?? {}

View File

@@ -41,6 +41,8 @@ export interface SettingState {
load: boolean
conflictNotice: boolean // 其他脚本/插件冲突提示
ignoreSimpleWord: boolean // 忽略简单词
wordPracticeMode: number // 单词练习模式0智能模式1自由模式
disableShowPracticeSettingDialog: boolean // 不默认显示练习设置弹框
}
export const getDefaultSettingState = (): SettingState => ({
@@ -80,7 +82,9 @@ export const getDefaultSettingState = (): SettingState => ({
firstTime: Date.now(),
load: false,
conflictNotice: true,
ignoreSimpleWord: false
ignoreSimpleWord: false,
wordPracticeMode: 0,
disableShowPracticeSettingDialog: false
})
export const useSettingStore = defineStore('setting', {

View File

@@ -14,7 +14,7 @@ export const SAVE_DICT_KEY = {
}
export const SAVE_SETTING_KEY = {
key: 'typing-word-setting',
version: 10
version: 11
}
export const EXPORT_DATA_KEY = {
key: 'typing-word-export',