feat:keep a record of historical practice
This commit is contained in:
@@ -114,6 +114,7 @@ export function genArticleSectionData(article: Article): number {
|
||||
item = item.trim()
|
||||
//如果没有空格,导致修改一行一行的数据时,汇总时全没有空格了,库无法正常断句
|
||||
//所以要保证最后一个是空格,但防止用户打N个空格,就去掉再加上一个空格,只需要一个即可
|
||||
//2025/10/1:最后一句不需要空格
|
||||
if (i < arr.length - 1) item += ' '
|
||||
let sentence: Sentence = cloneDeep({
|
||||
text: item,
|
||||
@@ -127,7 +128,6 @@ export function genArticleSectionData(article: Article): number {
|
||||
|
||||
sections = sections.filter(v => v.length)
|
||||
article.sections = sections
|
||||
console.log(sections)
|
||||
|
||||
let failCount = 0
|
||||
let translateList = article.textTranslate?.split('\n\n') || []
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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('导入失败!')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const SAVE_SETTING_KEY = {
|
||||
}
|
||||
export const EXPORT_DATA_KEY = {
|
||||
key: 'typing-word-export',
|
||||
version: 3
|
||||
version: 4
|
||||
}
|
||||
|
||||
export const LOCAL_FILE_KEY = 'typing-word-files'
|
||||
|
||||
Reference in New Issue
Block a user