feat:keep a record of historical practice

This commit is contained in:
Zyronon
2025-10-01 23:00:32 +08:00
parent b672115e20
commit a3133a7f15
7 changed files with 248 additions and 228 deletions

View File

@@ -17,6 +17,7 @@ import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import recommendBookList from "@/assets/book-list.json";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import { PracticeSaveArticleKey } from "@/utils/const.ts";
dayjs.extend(isBetween);
@@ -25,9 +26,11 @@ const base = useBaseStore()
const store = useBaseStore()
const router = useRouter()
const runtimeStore = useRuntimeStore()
let isSaveData = $ref(false)
onMounted(init)
watch(() => store.load, init)
watch(() => store.load, n => {
if (n) init()
}, {immediate: true})
async function init() {
if (store.article.studyIndex >= 1) {
@@ -35,6 +38,24 @@ async function init() {
store.article.bookList[store.article.studyIndex] = await _getDictDataByUrl(store.sbook, DictType.article)
}
}
let d = localStorage.getItem(PracticeSaveArticleKey.key)
if (d) {
try {
let obj = JSON.parse(d)
let data = obj.val
//如果全是0说明未进行练习直接重置
if (
data.practiceData.sectionIndex === 0 &&
data.practiceData.sentenceIndex === 0 &&
data.practiceData.wordIndex === 0
) {
throw new Error()
}
isSaveData = true
} catch (e) {
localStorage.removeItem(PracticeSaveArticleKey.key)
}
}
}
function startStudy() {
@@ -187,7 +208,7 @@ const weekList = $computed(() => {
@click="startStudy"
:disabled="!base.currentBook.name">
<div class="flex items-center gap-2">
<span class="line-height-[2]">开始学习</span>
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
</div>
</BaseButton>

View File

@@ -1,18 +1,18 @@
<script setup lang="ts">
import {computed, onMounted, onUnmounted, provide, watch} from "vue";
import {useBaseStore} from "@/stores/base.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {Article, ArticleItem, ArticleWord, Dict, DictType, ShortcutKey, Statistics, Word} from "@/types/types.ts";
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
import { computed, onMounted, onUnmounted, provide, watch } from "vue";
import { useBaseStore } from "@/stores/base.ts";
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { Article, ArticleItem, ArticleWord, Dict, DictType, ShortcutKey, Statistics, Word } from "@/types/types.ts";
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
import useTheme from "@/hooks/theme.ts";
import Toast from '@/components/base/toast/Toast.ts'
import {_getDictDataByUrl, cloneDeep, msToHourMinute, msToMinute, total} from "@/utils";
import {usePracticeStore} from "@/stores/practice.ts";
import {useArticleOptions} from "@/hooks/dict.ts";
import {genArticleSectionData, syncBookInMyStudyList, usePlaySentenceAudio} from "@/hooks/article.ts";
import {getDefaultArticle, getDefaultDict, getDefaultWord} from "@/types/func.ts";
import { _getDictDataByUrl, _nextTick, cloneDeep, msToHourMinute, msToMinute, total } from "@/utils";
import { usePracticeStore } from "@/stores/practice.ts";
import { useArticleOptions } from "@/hooks/dict.ts";
import { genArticleSectionData, syncBookInMyStudyList, usePlaySentenceAudio } from "@/hooks/article.ts";
import { getDefaultArticle, getDefaultDict, getDefaultWord } from "@/types/func.ts";
import TypingArticle from "@/pages/article/components/TypingArticle.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import Panel from "@/components/Panel.vue";
@@ -20,12 +20,14 @@ import ArticleList from "@/components/list/ArticleList.vue";
import EditSingleArticleModal from "@/pages/article/components/EditSingleArticleModal.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import ConflictNotice from "@/components/ConflictNotice.vue";
import {useRoute, useRouter} from "vue-router";
import { useRoute, useRouter } from "vue-router";
import book_list from "@/assets/book-list.json";
import PracticeLayout from "@/components/PracticeLayout.vue";
import Switch from "@/components/base/Switch.vue";
import Audio from "@/components/base/Audio.vue";
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
import dayjs from "dayjs";
import { PracticeSaveArticleKey } from "@/utils/const.ts";
const store = useBaseStore()
const settingStore = useSettingStore()
@@ -41,6 +43,9 @@ let typingArticleRef = $ref<any>()
let loading = $ref<boolean>(false)
let allWrongWords = new Set()
let editArticle = $ref<Article>(getDefaultArticle())
let speedMinute = $ref(0)
let timer = $ref(0)
let isFocus = true
function write() {
// console.log('write')
@@ -129,17 +134,77 @@ onMounted(() => {
}
})
onUnmounted(() => {
clearInterval(timer)
savePracticeData(true, false)
})
useStartKeyboardEventListener()
useDisableEventListener(() => loading)
function savePracticeData(init = true, regenerate = true) {
let d = localStorage.getItem(PracticeSaveArticleKey.key)
if (d) {
try {
let obj = JSON.parse(d)
if (obj.val.practiceData.id !== articleData.article.id) {
throw new Error()
}
if (init) {
let data = obj.val
//如果全是0说明未进行练习直接重置
if (
data.practiceData.sectionIndex === 0 &&
data.practiceData.sentenceIndex === 0 &&
data.practiceData.wordIndex === 0
) {
throw new Error()
}
//初始化时spend为0把本地保存的值设置给statStore里面再保存保持一致。不然每次进来都是0
statStore.$patch(data.statStoreData)
}
obj.val.statStoreData = statStore.$state
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify(obj))
} catch (e) {
localStorage.removeItem(PracticeSaveArticleKey.key)
regenerate && savePracticeData()
}
} else {
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify({
version: PracticeSaveArticleKey.version,
val: {
practiceData: {
sectionIndex: 0,
sentenceIndex: 0,
wordIndex: 0,
stringIndex: 0,
id: articleData.article.id
},
statStoreData: statStore.$state,
}
}))
}
}
function setArticle(val: Article) {
statStore.wrong = 0
statStore.total = 0
statStore.startDate = Date.now()
statStore.spend = 0
allWrongWords = new Set()
articleData.list[store.sbook.lastLearnIndex] = val
articleData.article = val
savePracticeData()
clearInterval(timer)
timer = setInterval(() => {
if (isFocus) {
statStore.spend += 1000
savePracticeData(false)
}
}, 1000)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
articleData.article.sections.map((v, i) => {
v.map((w) => {
@@ -150,11 +215,16 @@ function setArticle(val: Article) {
})
})
})
_nextTick(typingArticleRef?.init)
}
function complete() {
clearInterval(timer)
setTimeout(() => {
localStorage.removeItem(PracticeSaveArticleKey.key)
}, 1500)
//todo 有空了改成实时保存
statStore.spend = Date.now() - statStore.startDate
let data: Partial<Statistics> & { title: string, id: string } = {
id: articleData.article.id,
title: articleData.article.title,
@@ -173,7 +243,7 @@ function complete() {
s: ''
}
reportData.s = `name:${store.sbook.name},title:${store.sbook.lastLearnIndex}.${data.title},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)}`
window.umami?.track('studyWordArticle', reportData)
window.umami?.track('studyArticleEnd', reportData)
store.sbook.statistics.push(data as any)
console.log(data, reportData)
@@ -292,12 +362,11 @@ useEvents([
[ShortcutKey.EditArticle, shortcutKeyEdit],
])
let speedMinute = $ref(0)
let timer = $ref(0)
onMounted(() => {
timer = setInterval(() => {
speedMinute = Math.floor((Date.now() - statStore.startDate) / 1000 / 60)
}, 1000)
document.addEventListener('visibilitychange', () => {
isFocus = !document.hidden
})
})
onUnmounted(() => {
@@ -315,7 +384,7 @@ function play2(e) {
const currentPractice = computed(() => {
if (store.sbook.statistics?.length) {
return store.sbook.statistics.filter(v => v.title === articleData.article.title)
return store.sbook.statistics.filter((v: any) => v.title === articleData.article.title)
}
return []
})
@@ -324,17 +393,18 @@ provide('currentPractice', currentPractice)
</script>
<template>
<PracticeLayout
v-loading="loading"
panelLeft="var(--article-panel-margin-left)">
v-loading="loading"
panelLeft="var(--article-panel-margin-left)">
<template v-slot:practice>
<TypingArticle
ref="typingArticleRef"
@wrong="wrong"
@next="next"
@nextWord="nextWord"
@play="play2"
@complete="complete"
:article="articleData.article"
ref="typingArticleRef"
@wrong="wrong"
@next="next"
@nextWord="nextWord"
@play="play2"
@replay="setArticle(articleData.article)"
@complete="complete"
:article="articleData.article"
/>
</template>
<template v-slot:panel>
@@ -346,17 +416,17 @@ provide('currentPractice', currentPractice)
</template>
<div class="panel-page-item pl-4">
<ArticleList
:isActive="settingStore.showPanel"
:static="false"
:show-translate="settingStore.translate"
@click="changeArticle"
:active-id="articleData.article.id"
:list="articleData.list ">
:isActive="settingStore.showPanel"
:static="false"
:show-translate="settingStore.translate"
@click="changeArticle"
:active-id="articleData.article.id"
:list="articleData.list ">
<template v-slot:suffix="{item,index}">
<BaseIcon
:class="!isArticleCollect(item) ? 'collect' : 'fill'"
@click.stop="toggleArticleCollect(item)"
:title="!isArticleCollect(item) ? '收藏' : '取消收藏'">
:class="!isArticleCollect(item) ? 'collect' : 'fill'"
@click.stop="toggleArticleCollect(item)"
:title="!isArticleCollect(item) ? '收藏' : '取消收藏'">
<IconFluentStar16Regular v-if="!isArticleCollect(item)"/>
<IconFluentStar16Filled v-else/>
</BaseIcon>
@@ -369,10 +439,10 @@ provide('currentPractice', currentPractice)
<div class="footer">
<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">
<div class="flex justify-between items-center gap-2">
@@ -383,7 +453,7 @@ provide('currentPractice', currentPractice)
<div class="name">记录</div>
</div>
<div class="row">
<div class="num">{{ speedMinute }}分钟</div>
<div class="num">{{ Math.floor(statStore.spend / 1000 / 60) }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
</div>
@@ -411,27 +481,27 @@ provide('currentPractice', currentPractice)
<Switch v-model="settingStore.articleSound"/>
</Tooltip>
<BaseIcon
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
@click="skip">
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
@click="skip">
<IconFluentArrowBounce20Regular class="transform-rotate-180"/>
</BaseIcon>
<BaseIcon
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
@click="play">
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
@click="play">
<IconFluentReplay20Regular/>
</BaseIcon>
<BaseIcon
@click="settingStore.dictation = !settingStore.dictation"
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
@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">
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
@click="settingStore.translate = !settingStore.translate">
<IconFluentTranslate16Regular v-if="settingStore.translate"/>
<IconFluentTranslateOff16Regular v-else/>
</BaseIcon>
@@ -442,8 +512,8 @@ provide('currentPractice', currentPractice)
<!-- @click="emitter.emit(ShortcutKey.EditArticle)"-->
<!-- />-->
<BaseIcon
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`">
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`">
<IconFluentTextListAbcUppercaseLtr20Regular/>
</BaseIcon>
</div>
@@ -455,9 +525,9 @@ provide('currentPractice', currentPractice)
</PracticeLayout>
<EditSingleArticleModal
v-model="showEditArticle"
:article="editArticle"
@save="saveArticle"
v-model="showEditArticle"
:article="editArticle"
@save="saveArticle"
/>
<ConflictNotice/>

View File

@@ -18,7 +18,8 @@ import Space from "@/pages/article/components/Space.vue";
import { useWordOptions } from "@/hooks/dict.ts";
import nlp from "compromise/three";
import { nanoid } from "nanoid";
import { PracticeSaveArticleKey } from "@/utils/const.ts";
import { PracticeSaveArticleKey, PracticeSaveWordKey } from "@/utils/const.ts";
import { usePracticeStore } from "@/stores/practice.ts";
interface IProps {
article: Article,
@@ -46,6 +47,7 @@ const emit = defineEmits<{
nextWord: [val: ArticleWord],
complete: [],
next: [],
replay: [],
}>()
let typeArticleRef = $ref<HTMLInputElement>(null)
@@ -82,19 +84,26 @@ const {
const store = useBaseStore()
const settingStore = useSettingStore()
const statStore = usePracticeStore()
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c,]) => {
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify({
sectionIndex,
sentenceIndex,
wordIndex,
stringIndex,
title: props.article.title
version: PracticeSaveArticleKey.version,
val: {
practiceData: {
sectionIndex,
sentenceIndex,
wordIndex,
stringIndex,
id: props.article.id
},
statStoreData: statStore.$state,
}
}))
checkCursorPosition(a, b, c)
})
watch(() => props.article, init, {immediate: true})
// watch(() => props.article.id, init, {immediate: true})
watch(() => settingStore.translate, () => {
checkTranslateLocation().then(() => checkCursorPosition())
@@ -111,11 +120,19 @@ watch(() => isEnd, n => {
})
function init() {
if (!props.article.id) return
isSpace = isEnd = false
let d = localStorage.getItem(PracticeSaveArticleKey.key)
if (d) {
let obj = JSON.parse(d)
jump(obj.sectionIndex, obj.sentenceIndex, obj.wordIndex)
try {
let obj = JSON.parse(d)
let data = obj.val
statStore.$patch(data.statStoreData)
jump(data.practiceData.sectionIndex, data.practiceData.sentenceIndex, data.practiceData.wordIndex)
} catch (e) {
localStorage.removeItem(PracticeSaveArticleKey.key)
init()
}
} else {
wrong = input = ''
sectionIndex = 0
@@ -188,18 +205,14 @@ function checkTranslateLocation() {
})
}
let lockNextSentence = false
let isTyping = false
//专用锁,因为这个方法父级要调用
let lock = false
function nextSentence() {
if (lockNextSentence || isEnd) return
if (lock || isEnd) return
checkTranslateLocation()
lockNextSentence = true
// wordData.words = [
// {"word": "pharmacy", "trans": ["药房;配药学,药剂学;制药业;一批备用药品"], "phonetic0": "'fɑrməsi", "phonetic1": "'fɑːməsɪ"},
// // {"word": "foregone", "trans": ["过去的;先前的;预知的;预先决定的", "发生在…之前forego的过去分词"], "phonetic0": "'fɔrɡɔn", "phonetic1": "fɔː'gɒn"}, {"word": "president", "trans": ["总统;董事长;校长;主席"], "phonetic0": "'prɛzɪdənt", "phonetic1": "'prezɪd(ə)nt"}, {"word": "plastic", "trans": ["塑料的;(外科)造型的;可塑的", "塑料制品;整形;可塑体"], "phonetic0": "'plæstɪk", "phonetic1": "'plæstɪk"}, {"word": "provisionally", "trans": ["临时地,暂时地"], "phonetic0": "", "phonetic1": ""}, {"word": "incentive", "trans": ["动机;刺激", "激励的;刺激的"], "phonetic0": "ɪn'sɛntɪv", "phonetic1": "ɪn'sentɪv"}, {"word": "calculate", "trans": ["计算;以为;作打算"], "phonetic0": "'kælkjulet", "phonetic1": "'kælkjʊleɪt"}
// ]
// return
lock = true
let currentSection = props.article.sections[sectionIndex]
let currentSentence = currentSection[sentenceIndex]
//这里把未输入的单词补全因为删除时会用到input
@@ -207,22 +220,18 @@ function nextSentence() {
word.input = word.input + word.word.slice(word.input?.length ?? 0)
})
isSpace = false
stringIndex = 0
wordIndex = 0
input = wrong = ''
//todo 计得把略过的单词加上统计里面去
// if (!store.allIgnoreWords.includes(currentWord.word.toLowerCase()) && !currentWord.isSymbol) {
// statisticsStore.inputNumber++
// }
isSpace = false;
input = wrong = ''
stringIndex = 0;
wordIndex = 0
sentenceIndex++
if (!currentSection[sentenceIndex]) {
sentenceIndex = 0
sectionIndex++
if (!props.article.sections[sectionIndex]) {
console.log('打完了')
isEnd = true
@@ -233,88 +242,35 @@ function nextSentence() {
} else {
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
}
// 如果有新的单词,更新当前单词信息
if (!isEnd && props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]) {
updateCurrentWordInfo(props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]);
}
lockNextSentence = false
lock = false
}
// 在全局对象中存储当前单词信息,以便其他模块可以访问
function updateCurrentWordInfo(currentWord: ArticleWord) {
window.__CURRENT_WORD_INFO__ = {
word: currentWord.word,
input: currentWord.input || '',
inputLock: isSpace,
containsSpace: currentWord.word.includes(' ')
};
}
let isTyping = false
function onTyping(e: KeyboardEvent) {
if (isTyping) return;
if (!props.article.sections.length) return
if (isTyping) return;
isTyping = true;
// console.log('keyDown', e.key, e.code, e.keyCode)
wrong = ''
let currentSection = props.article.sections[sectionIndex]
let currentSentence = currentSection[sentenceIndex]
let currentWord: ArticleWord = currentSentence.words[wordIndex]
wrong = ''
// 更新当前单词信息
updateCurrentWordInfo(currentWord);
const nextWord = () => {
isSpace = false
stringIndex = 0
wordIndex++
emit('nextWord', currentWord)
// 只在需要时更新当前单词信息,不自动跳转到下一句话
if (wordIndex < currentSentence.words.length) {
// 更新当前单词信息
updateCurrentWordInfo(currentSentence.words[wordIndex]);
const next = () => {
isSpace = false;
input = wrong = ''
stringIndex = 0;
// 检查下一个单词是否存在
if (wordIndex + 1 < currentSentence.words.length) {
wordIndex++;
emit('nextWord', currentWord);
} else {
nextSentence()
}
}
if (isSpace) {
// 在单词之间的空格处理
if (e.code === 'Space') {
// 检查下一个单词是否存在
const hasNextWord = wordIndex + 1 < currentSentence.words.length;
// 当按下空格键时,移动到下一个单词,而不是下跳过句子,末尾跳转到下一个
if (hasNextWord) {
// 重置isSpace状态
isSpace = false;
stringIndex = 0;
wordIndex++;
input = '';
emit('nextWord', currentWord);
// 获取下一个单词
currentWord = currentSentence.words[wordIndex];
if (currentWord && currentWord.word && currentWord.word[0] === ' ') {
input = ' ';
if (!currentWord.input) currentWord.input = '';
currentWord.input = input;
stringIndex = 1;
}
// 更新当前单词信息
updateCurrentWordInfo(currentWord);
} else {
// 句子末尾跳转到下一句话
nextSentence();
}
next()
} else {
wrong = ' '
playBeep()
@@ -323,24 +279,13 @@ function onTyping(e: KeyboardEvent) {
wrong = input = ''
}, 500)
}
playKeyboardAudio()
} else {
//如果是首句首词
if (sectionIndex === 0 && sentenceIndex === 0 && wordIndex === 0 && stringIndex === 0) {
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
}
let letter = e.key
// 如果是空格键,需要判断是作为输入还是切换单词
if (letter === ' ' || e.code === 'Space') {
// 如果当前单词包含空格,且当前输入位置应该是空格,则视为正常输入
if (currentWord.word.includes(' ') && currentWord.word[stringIndex] === ' ') {
letter = ' '
}
}
let key = currentWord.word[stringIndex]
// console.log('key', key,)
let isRight = false
@@ -357,58 +302,23 @@ function onTyping(e: KeyboardEvent) {
}
input += letter
if (!currentWord.input) currentWord.input = ''
currentWord.input = input
// console.log(currentWord.input)
wrong = ''
// console.log('匹配上了')
stringIndex++
//如果当前词没有index说明这个词完了下一个是空格
//单词输入完毕
if (!currentWord.word[stringIndex]) {
input = ''
if (!currentWord.isSymbol) {
playCorrect()
}
// 检查是否是句子的最后一个单词
// const isLastWordInSentence = wordIndex + 1 >= currentSentence.words.length;
// if (isLastWordInSentence) {
// // 如果是句子的最后一个单词,自动跳转到下一句,不用再输入空格
// nextSentence();
// } else if (currentWord.nextSpace) {
// // 如果不是最后一个单词,且需要空格,设置等待空格输入
// isSpace = true;
// } else {
// // 如果不需要空格,直接移动到下一个单词
// nextWord();
// }
//换句不打空格不符合习惯
//如果不是符号,播放完成音效
if (!currentWord.isSymbol) playCorrect()
if (currentWord.nextSpace) {
isSpace = true
} else {
if (wordIndex === currentSentence.words.length - 1) {
if (sectionIndex === props.article.sections.length - 1 && sentenceIndex === currentSection.length - 1) {
console.log('打完了')
isEnd = true
emit('complete')
} else {
nextSentence()
}
} else {
nextWord()
}
next()
}
}
// 更新当前单词信息
updateCurrentWordInfo(currentWord);
playKeyboardAudio()
}
isTyping = false
playKeyboardAudio()
e.preventDefault()
isTyping = false
}
function play() {
@@ -460,8 +370,8 @@ function del() {
}
}
input = currentWord.input = currentWord.input.slice(0, stringIndex)
checkCursorPosition()
}
checkCursorPosition()
}
function showSentence(i1: number = sectionIndex, i2: number = sentenceIndex, i3: number = wordIndex) {
@@ -594,22 +504,8 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
}
onMounted(() => {
// 初始化当前单词信息
if (props.article.sections &&
props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]) {
updateCurrentWordInfo(props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]);
}
emitter.on(EventKey.resetWord, () => {
wrong = input = ''
// 重置时更新当前单词信息
if (props.article.sections &&
props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]) {
updateCurrentWordInfo(props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]);
}
})
emitter.on(EventKey.onTyping, onTyping)
})
@@ -619,7 +515,7 @@ onUnmounted(() => {
emitter.off(EventKey.onTyping, onTyping)
})
defineExpose({showSentence, play, del, hideSentence, nextSentence})
defineExpose({showSentence, play, del, hideSentence, nextSentence, init})
function isCurrent(i: number, j: number, w: number) {
return `${i}${j}${w}` === currentIndex
@@ -710,7 +606,7 @@ const currentPractice = inject('currentPractice', [])
<div class="options flex justify-center" v-if="isEnd">
<BaseButton
@click="init">重新练习
@click="emit('replay')">重新练习
</BaseButton>
<BaseButton
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"

View File

@@ -10,7 +10,7 @@ import {
APP_NAME,
APP_VERSION,
EXPORT_DATA_KEY,
LOCAL_FILE_KEY,
LOCAL_FILE_KEY, PracticeSaveArticleKey,
PracticeSaveWordKey,
SAVE_DICT_KEY,
SAVE_SETTING_KEY,
@@ -184,7 +184,12 @@ async function exportData(notice = '导出成功!') {
[PracticeSaveWordKey.key]: {
version: PracticeSaveWordKey.version,
val: {}
}
},
[PracticeSaveArticleKey.key]: {
version: PracticeSaveArticleKey.version,
val: {}
},
[APP_VERSION.key]: -1
}
}
let d = localStorage.getItem(PracticeSaveWordKey.key)
@@ -194,6 +199,15 @@ async function exportData(notice = '导出成功!') {
} catch (e) {
}
}
let d1 = localStorage.getItem(PracticeSaveArticleKey.key)
if (d1) {
try {
data.val[PracticeSaveArticleKey.key] = JSON.parse(d1)
} catch (e) {
}
}
let r = await get(APP_VERSION.key)
data.val[APP_VERSION.key] = r
const zip = new JSZip();
zip.file("data.json", JSON.stringify(data));
@@ -216,7 +230,9 @@ function importJson(str: string, notice: boolean = true) {
val: {
setting: {},
dict: {},
[PracticeSaveWordKey.key]: {}
[PracticeSaveWordKey.key]: {},
[PracticeSaveArticleKey.key]: {},
[APP_VERSION.key]: {},
}
}
try {
@@ -238,6 +254,23 @@ function importJson(str: string, notice: boolean = true) {
//todo 上报
}
}
if (obj.version >= 4) {
try {
let save: any = obj.val[PracticeSaveArticleKey.key] || {}
if (save.val && Object.keys(save.val).length > 0) {
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify(obj.val[PracticeSaveArticleKey.key]))
}
} catch (e) {
//todo 上报
}
try {
let r: any = obj.val[APP_VERSION.key] || -1
set(APP_VERSION.key, r)
runtimeStore.isNew = r ? (APP_VERSION.version > Number(r)) : true
} catch (e) {
//todo 上报
}
}
notice && Toast.success('导入成功!')
} catch (err) {
return Toast.error('导入失败!')

View File

@@ -235,7 +235,7 @@ function saveLastPracticeIndex(e) {
:loading="loading"
@click="startPractice">
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{ isSaveData ? '继续上次学习' : '开始学习' }}</span>
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
</div>
</BaseButton>