feat:修改默写单词计算逻辑,修改统计组件

This commit is contained in:
zyronon
2025-07-24 01:58:35 +08:00
parent a14af056f7
commit 699703d5cb
13 changed files with 198 additions and 111 deletions

1
components.d.ts vendored
View File

@@ -14,6 +14,7 @@ declare module 'vue' {
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInput: typeof import('element-plus/es')['ElInput']

View File

@@ -112,51 +112,83 @@ export function getCurrentStudyWord() {
let data = {new: [], review: [], write: []}
let dict = store.sdict;
if (dict.words?.length) {
for (let i = dict.lastLearnIndex; i < dict.words.length; i++) {
if (data.new.length >= dict.perDayStudyNumber) break
let item = dict.words[i]
if (!store.known.words.map(v => v.word.toLowerCase()).includes(item.word.toLowerCase())) {
const perDay = store.sdict.perDayStudyNumber;
let start = dict.lastLearnIndex;
let end = start + dict.perDayStudyNumber
dict.words.slice(start, end).map(item => {
if (!store.knownWords.includes(item.word)) {
data.new.push(item)
}
}
})
const getList = (startIndex, endIndex) => {
if (startIndex < 0) return []
const getList = (startIndex: number, endIndex: number) => {
if (startIndex < 0) startIndex = 0
return dict.words.slice(startIndex, endIndex)
}
let s = dict.lastLearnIndex - dict.perDayStudyNumber
let e = dict.lastLearnIndex
end = start
start = start - dict.perDayStudyNumber
//取上一次学习的单词用于复习
let list = getList(s, e)
let list = getList(start, end)
list.map(item => {
if (!store.known.words.map(v => v.word.toLowerCase()).includes(item.word.toLowerCase())) {
if (!store.knownWords.includes(item.word)) {
data.review.push(item)
}
})
//取前天至再往前数3天的单词用于默写
Array.from({length: 4}).map((_, j) => {
e = s
s -= dict.perDayStudyNumber
list = getList(s, e)
let d = []
for (let i = 0; i < list.length; i++) {
if (j === 3) {
if (d.length >= dict.perDayStudyNumber - data.write.length) break
} else {
if (d.length >= Math.floor(dict.perDayStudyNumber / 4)) break
}
let item = list[i]
if (!store.known.words.map(v => v.word.toLowerCase()).includes(item.word.toLowerCase())) {
d.push(item)
}
}
data.write = data.write.concat(d)
})
// end = start
// start = start - dict.perDayStudyNumber * 3
// //在上次学习再往前取前3次学习的单词用于默写
// list = getList(start, end)
// list.map(item => {
// if (!store.knownWords.includes(item.word)) {
// data.write.push(item)
// }
// })
//write数组放的是上上次之前的单词总的数量为perDayStudyNumber * 3取单词的规则为从后往前取6个perDayStudyNumber的越靠前的取的单词越多。
end = start
const totalNeed = perDay * 3;
const allWords = dict.words;
// 上上次更早的单词
const candidateWords = allWords.slice(0, end).filter(w => !store.knownWords.includes(w.word));
// 分6组每组 perDay 个
const groupCount = 6;
const groupSize = perDay;
const groups: Word[][] = [];
for (let i = 0; i < groupCount; i++) {
const start = candidateWords.length - (i + 1) * groupSize;
const end = candidateWords.length - i * groupSize;
if (start < 0 && end <= 0) break;
groups.unshift(candidateWords.slice(Math.max(0, start), Math.max(0, end)));
}
// 分配数量,靠前组多,靠后组少
// 例如分配比例 [1,2,3,4,5,6]
const ratio = [1, 2, 3, 4, 5, 6];
const ratioSum = ratio.reduce((a, b) => a + b, 0);
const realRatio = ratio.map(r => Math.round(r * totalNeed / ratioSum));
// 按比例从每组取单词
let writeWords: Word[] = [];
for (let i = 0; i < groups.length; i++) {
writeWords = writeWords.concat(groups[i].slice(-realRatio[i]));
}
// 如果数量不够,补足
if (writeWords.length < totalNeed) {
const extra = candidateWords.filter(w => !writeWords.includes(w)).slice(-(totalNeed - writeWords.length));
writeWords = writeWords.concat(extra);
}
// 最终数量截断
writeWords = writeWords.slice(-totalNeed);
//这里需要反转一下,越靠近今天的单词,越先练习
data.write = writeWords.reverse();
}
// console.timeEnd()
// console.log('data', data)
console.log('data', data)
return data
}

View File

@@ -75,7 +75,7 @@ function jumpSpecifiedChapter(val: number) {
onMounted(() => {
emitter.on(EventKey.write, write)
emitter.on(EventKey.repeat, repeat)
emitter.on(EventKey.repeatStudy, repeat)
emitter.on(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.on(ShortcutKey.PreviousChapter, prev)
@@ -91,7 +91,7 @@ onMounted(() => {
onUnmounted(() => {
emitter.off(EventKey.write, write)
emitter.off(EventKey.repeat, repeat)
emitter.off(EventKey.repeatStudy, repeat)
emitter.off(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.off(ShortcutKey.PreviousChapter, prev)

View File

@@ -38,7 +38,7 @@ onMounted(() => {
useEvents([
[EventKey.changeDict, getCurrentPractice],
[EventKey.next, next],
[EventKey.continueStudy, next],
[ShortcutKey.NextChapter, next],
])

View File

@@ -70,7 +70,7 @@ function jumpSpecifiedChapter(val: number) {
onMounted(() => {
emitter.on(EventKey.write, write)
emitter.on(EventKey.repeat, repeat)
emitter.on(EventKey.repeatStudy, repeat)
emitter.on(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.on(ShortcutKey.PreviousChapter, prev)
@@ -86,7 +86,7 @@ onMounted(() => {
onUnmounted(() => {
emitter.off(EventKey.write, write)
emitter.off(EventKey.repeat, repeat)
emitter.off(EventKey.repeatStudy, repeat)
emitter.off(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.off(ShortcutKey.PreviousChapter, prev)

View File

@@ -455,7 +455,7 @@ let showQuestions = $ref(false)
<div class="options flex justify-center" v-if="isEnd">
<BaseButton
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"
@click="emitter.emit(EventKey.next)">下一章
@click="emitter.emit(EventKey.continueStudy)">下一章
</BaseButton>
</div>

View File

@@ -226,7 +226,7 @@ onMounted(() => {
useEvents([
[EventKey.changeDict, init],
[EventKey.next, next],
[EventKey.continueStudy, next],
[ShortcutKey.NextChapter, next],
[ShortcutKey.PlayWordPronunciation, play],
@@ -303,7 +303,7 @@ const {playSentenceAudio} = usePlaySentenceAudio()
:title="`下一章(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
v-if="store.currentBook.lastLearnIndex < articleData.articles.length - 1">
<IconWrapper>
<Icon @click="emitter.emit(EventKey.next)" icon="octicon:arrow-right-24"/>
<Icon @click="emitter.emit(EventKey.continueStudy)" icon="octicon:arrow-right-24"/>
</IconWrapper>
</Tooltip>
</div>

View File

@@ -19,6 +19,7 @@ export interface ModalProps {
confirmButtonText?: string
cancelButtonText?: string,
keyboard?: boolean,
closeOnClickBg?: boolean,
confirm?: any
beforeClose?: any
}
@@ -26,6 +27,7 @@ export interface ModalProps {
const props = withDefaults(defineProps<ModalProps>(), {
modelValue: undefined,
showClose: true,
closeOnClickBg: true,
fullScreen: false,
footer: false,
header: true,
@@ -148,7 +150,7 @@ async function cancel() {
<div class="modal-mask"
ref="maskRef"
v-if="!fullScreen"
@click.stop="close"></div>
@click.stop="closeOnClickBg && close()"></div>
<div class="modal"
ref="modalRef"
:class="[

View File

@@ -3,35 +3,68 @@ import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
import {useBaseStore} from "@/stores/base.ts";
import BaseButton from "@/components/BaseButton.vue";
import {ShortcutKey} from "@/types.ts";
import {emitter, EventKey, useEvent, useEvents} from "@/utils/eventBus.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {Icon} from '@iconify/vue';
import {useSettingStore} from "@/stores/setting.ts";
import {usePracticeStore} from "@/stores/practice.ts";
import {dayjs} from "element-plus";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import {watch} from "vue";
dayjs.extend(isBetween);
const store = useBaseStore()
const settingStore = useSettingStore()
const statStore = usePracticeStore()
let open = $ref(false)
const model = defineModel({default: false})
let list = $ref([])
useEvent(EventKey.openStatModal, () => {
let data = {
speed: statStore.speed,
startDate: statStore.startDate,
total: statStore.total,
wrong: statStore.wrong,
function calcWeekList() {
// 获取本周的起止时间
const startOfWeek = dayjs().startOf('week').add(1, 'day'); // 周一
const endOfWeek = dayjs().endOf('week').add(1, 'day'); // 周日
// 初始化 7 天的数组,默认 false
const weekList = Array(7).fill(false);
store.sdict.statistics.forEach(item => {
const date = dayjs(item.startDate);
if (date.isBetween(startOfWeek, endOfWeek, null, '[]')) {
let idx = date.day();
// dayjs().day() 0=周日, 1=周一, ..., 6=周六
// 需要转换为 0=周一, ..., 6=周日
if (idx === 0) {
idx = 6; // 周日放到最后
} else {
idx = idx - 1; // 其余前移一位
}
weekList[idx] = true;
}
});
list = weekList;
console.log(list)
}
// 监听 model 弹窗打开时重新计算
watch(model, (newVal) => {
if (newVal) {
let data = {
speed: statStore.speed,
startDate: statStore.startDate,
total: statStore.total,
wrong: statStore.wrong,
}
//这里不知为啥会卡,打开有延迟
requestIdleCallback(() => {
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
store.sdict.statistics.push(data as any)
store.sdict.statistics.sort((a, b) => a.startDate - b.startDate)
calcWeekList(); // 新增:计算本周学习记录
})
}
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
store.sdict.statistics.push(data as any)
store.sdict.statistics.sort((a, b) => a.startDate - b.startDate)
console.log('staa', JSON.parse(JSON.stringify(store.sdict.statistics)))
open = true
})
const close = () => {
open = false
}
const close = () => model.value = false
useEvents([
[ShortcutKey.NextChapter, close],
@@ -39,8 +72,8 @@ useEvents([
[ShortcutKey.DictationChapter, close],
])
function options(emitType: 'write' | 'repeat' | 'next') {
open = false
function options(emitType: string) {
model.value = false
emitter.emit(EventKey[emitType])
}
@@ -53,26 +86,29 @@ const isEnd = $computed(() => {
<template>
<Dialog
:close-on-click-bg="false"
:header="false"
v-model="open">
<div class="statistics relative flex flex-col gap-6">
:keyboard="false"
:show-close="false"
v-model="model">
<div class="w-120 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-xl mb-2">已完成今日任务</div>
<div class="center text-2xl mb-2">已完成今日任务</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 class="text">新词数</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.newWordNumber }}</div>
<div class="text">复习数</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.newWordNumber
}}
</div>
<div class="text">默写数</div>
</div>
</div>
</div>
@@ -102,31 +138,38 @@ const isEnd = $computed(() => {
</div>
</div>
</div>
<div class="center flex-col">
<div class="title text-align-center mb-2">本周学习记录</div>
<div class="flex gap-4 color-gray">
<div
class="w-8 h-8 rounded-full center"
:class="item ? 'bg-green 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('repeat')">
@click="options(EventKey.repeatStudy)">
重学
</BaseButton>
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
@click="options('next')">
@click="options(EventKey.continueStudy)">
{{ isEnd ? '重新练习' : '再来一组' }}
</BaseButton>
<BaseButton>
分享
</BaseButton>
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
$card-radius: .5rem;
$dark-second-bg: rgb(60, 63, 65);
$item-hover: rgb(75, 75, 75);
.statistics {
padding: var(--space);
width: 30rem;
background: $dark-second-bg;
border-radius: $card-radius;
}
</style>

View File

@@ -43,7 +43,7 @@ const store = useBaseStore()
const statStore = usePracticeStore()
const typingRef: any = $ref()
let allWrongWords = new Set()
let showStatDialog = $ref(false)
let studyData = $ref<IProps>({
new: [],
review: [],
@@ -94,29 +94,32 @@ const nextWord: Word = $computed(() => {
})
function next(isTyping: boolean = true) {
showStatDialog = true
return
if (data.index === data.words.length - 1) {
if (data.wrongWords.length) {
console.log('学完了,但还有错词')
console.log('当前学完了,但还有错词')
data.words = shuffle(cloneDeep(data.wrongWords))
data.index = 0
data.wrongWords = []
} else {
console.log('学完了,没错词', statStore.total, statStore.step, data.index)
console.log('当前学完了,没错词', statStore.total, statStore.step, data.index)
isTyping && statStore.inputWordNumber++
statStore.speed = Date.now() - statStore.startDate
//学完了
if (statStore.step === 2) {
console.log('emit')
statStore.speed = Date.now() - statStore.startDate
console.log('全完学完了')
emitter.emit(EventKey.openStatModal, {})
// emit('complete', {})
}
//开始默认
//开始默认所有单词
if (statStore.step === 1) {
console.log('开始默认所有单词')
statStore.step++
settingStore.dictation = true
data.words = shuffle(studyData.write.concat(studyData.new).concat(studyData.review))
statStore.step++
data.index = 0
}
@@ -124,6 +127,7 @@ function next(isTyping: boolean = true) {
if (statStore.step === 0) {
statStore.step++
if (studyData.review.length) {
console.log('开始复习')
data.words = shuffle(studyData.review)
settingStore.dictation = false
data.index = 0
@@ -145,18 +149,16 @@ function onTypeWrong() {
allWrongWords.add(word.word.toLowerCase())
statStore.wrong++
}
//todo 后续要测试有非常的多的错词时,这会还卡不卡
setTimeout(() => {
requestAnimationFrame(() => {
if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === temp)) {
store.wrong.words.push(word)
store.wrong.length = store.wrong.words.length
}
if (!data.wrongWords.find((v: Word) => v.word.toLowerCase() === temp)) {
data.wrongWords.push(word)
}
})
}, 500)
//测试时这里会卡一下加上requestIdleCallback就好了
requestIdleCallback(() => {
if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === temp)) {
store.wrong.words.push(word)
store.wrong.length = store.wrong.words.length
}
if (!data.wrongWords.find((v: Word) => v.word.toLowerCase() === temp)) {
data.wrongWords.push(word)
}
})
}
function onKeyUp(e: KeyboardEvent) {
@@ -177,12 +179,16 @@ useStartKeyboardEventListener()
useOnKeyboardEventListener(onKeyDown, onKeyUp)
//TODO 需要判断是否已忽略
function repeat() {
// console.log('repeat')
console.log('repeat')
settingStore.dictation = false
emitter.emit(EventKey.resetWord)
studyData = cloneDeep(studyData)
let temp = cloneDeep(studyData)
//排除已掌握单词
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))
studyData = temp
}
//TODO 略过忽略的单词上
@@ -243,8 +249,8 @@ function togglePanel() {
}
useEvents([
[EventKey.repeat, repeat],
[EventKey.next, next],
[EventKey.repeatStudy, repeat],
[EventKey.continueStudy, next],
[EventKey.changeDict, () => {
studyData = getCurrentStudyWord()
}],
@@ -354,7 +360,9 @@ useEvents([
</Panel>
</div>
</div>
<Statistics/>
<Statistics v-model="showStatDialog"
/>
</template>
<style scoped lang="scss">

View File

@@ -155,18 +155,16 @@ function toggleSelect(item) {
<div class="flex">
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
<div class="text">新词</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 class="text">复习</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{
currentStudy.new.length + currentStudy.review.length + currentStudy.write.length
}}
<div class="text-4xl font-bold">{{ currentStudy.write.length }}
</div>
<div class="text">默写</div>
<div class="text">默写</div>
</div>
</div>
</div>

View File

@@ -105,6 +105,9 @@ export const useBaseStore = defineStore('base', {
known(): Dict {
return this.word.bookList[2]
},
knownWords(): string[] {
return this.known.words.map(v => v.word)
},
skipWordNames() {
return this.simple.words.map(v => v.word.toLowerCase())
},

View File

@@ -14,8 +14,8 @@ export const EventKey = {
keydown: 'keydown',
keyup: 'keyup',
onTyping: 'onTyping',
repeat: 'repeat',
next: 'next',
repeatStudy: 'repeatStudy',
continueStudy: 'continueStudy',
write: 'write',
editDict: 'editDict',
openMyDictDialog: 'openMyDictDialog',