This commit is contained in:
Zyronon
2025-09-25 01:02:38 +08:00
parent 6ecc9032ab
commit 4b7782a50f
13 changed files with 219 additions and 129 deletions

View File

@@ -16,6 +16,7 @@ import BackIcon from "@/components/BackIcon.vue";
import MiniDialog from "@/components/dialog/MiniDialog.vue";
import {onMounted} from "vue";
import {Origin} from "@/config/ENV.ts";
import {syncBookInMyStudyList} from "@/hooks/article.ts";
const base = useBaseStore()
const runtimeStore = useRuntimeStore()
@@ -106,25 +107,6 @@ function saveArticle(val: Article): boolean {
return true
}
//todo 考虑与syncDictInMyStudyList、changeDict方法合并
function syncBookInMyStudyList(study = false) {
_nextTick(() => {
let rIndex = base.article.bookList.findIndex(v => v.id === runtimeStore.editDict.id)
let temp = cloneDeep(runtimeStore.editDict);
if (!temp.custom && temp.id !== DictId.articleCollect) {
temp.custom = true
}
temp.length = temp.articles.length
if (rIndex > -1) {
base.article.bookList[rIndex] = temp
if (study) base.article.studyIndex = rIndex
} else {
base.article.bookList.push(temp)
if (study) base.article.studyIndex = base.article.bookList.length - 1
}
}, 100)
}
function saveAndNext(val: Article) {
if (saveArticle(val)) {
add()

View File

@@ -11,7 +11,7 @@ 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, usePlaySentenceAudio} from "@/hooks/article.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";
@@ -204,6 +204,7 @@ function saveArticle(val: Article) {
store.sbook.articles[rIndex] = cloneDeep(val)
}
setArticle(val)
store.sbook.custom = true
}
function edit(val: Article = articleData.article) {
@@ -376,7 +377,7 @@ const currentPractice = $computed(() => {
<div class="flex justify-between items-center gap-2">
<div class="stat">
<div class="row">
<div class="num">{{ currentPractice.length }}/{{ msToMinute(total(currentPractice, 'spend'))}}</div>
<div class="num">{{ currentPractice.length }}/{{ msToMinute(total(currentPractice, 'spend')) }}</div>
<div class="line"></div>
<div class="name">记录</div>
</div>

View File

@@ -1,23 +1,23 @@
<script setup lang="ts">
import {onMounted, onUnmounted, watch} from "vue"
import {Article, ArticleWord, Sentence, Word} from "@/types/types.ts";
import {useBaseStore} from "@/stores/base.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {_nextTick} from "@/utils";
import { onMounted, onUnmounted, watch } from "vue"
import { Article, ArticleWord, Sentence, Word } from "@/types/types.ts";
import { useBaseStore } from "@/stores/base.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { usePlayBeep, usePlayCorrect, usePlayKeyboardAudio } from "@/hooks/sound.ts";
import { emitter, EventKey } from "@/utils/eventBus.ts";
import { _nextTick } from "@/utils";
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
import ContextMenu from '@imengyu/vue3-context-menu'
import {getTranslateText} from "@/hooks/article.ts";
import { getTranslateText } from "@/hooks/article.ts";
import BaseButton from "@/components/BaseButton.vue";
import QuestionForm from "@/pages/article/components/QuestionForm.vue";
import {getDefaultArticle, getDefaultWord} from "@/types/func.ts";
import { getDefaultArticle, getDefaultWord } from "@/types/func.ts";
import Toast from '@/components/base/toast/Toast.ts'
import TypingWord from "@/pages/article/components/TypingWord.vue";
import Space from "@/pages/article/components/Space.vue";
import {useWordOptions} from "@/hooks/dict.ts";
import { useWordOptions } from "@/hooks/dict.ts";
import nlp from "compromise/three";
import {nanoid} from "nanoid";
import { nanoid } from "nanoid";
interface IProps {
article: Article,
@@ -76,7 +76,6 @@ const currentIndex = $computed(() => {
const playBeep = usePlayBeep()
const playCorrect = usePlayCorrect()
const playKeyboardAudio = usePlayKeyboardAudio()
const playWordAudio = usePlayWordAudio()
const {
toggleWordCollect,
} = useWordOptions()
@@ -855,22 +854,4 @@ $article-lh: 2.4;
}
}
}
.cursor {
position: absolute;
left: 0;
top: 0;
height: 1.8rem;
animation: underline 1s infinite steps(1, start);
}
@keyframes underline {
0%, 100% {
border-left: .1rem solid var(--color-article);
}
50% {
border-left: .1rem solid transparent;
}
}
</style>

View File

@@ -139,7 +139,6 @@ function getShortcutKeyName(key: string): string {
'ShowWord': '显示单词',
'EditArticle': '编辑文章',
'Next': '下一个',
'Replay': '重播',
'Previous': '上一个',
'ToggleSimple': '切换已掌握状态',
'ToggleCollect': '切换收藏状态',
@@ -350,14 +349,14 @@ function importOldData() {
</SettingItem>
<SettingItem title="允许默写模式下显示提示"
:desc="`开启后,可以通过鼠标 hover 单词或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
:desc="`开启后,可以通过鼠标移动到单词或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
>
<Switch v-model="settingStore.allowWordTip"/>
</SettingItem>
<div class="line"></div>
<SettingItem title="简单词过滤"
desc="开启后,练习单词中不会包含简单词;文章统计的总词数中不会包含简单词"
desc="开启后,练习单词中不会包含简单词;文章统计的总词数中不会包含简单词"
>
<Switch v-model="settingStore.ignoreSimpleWord"/>
</SettingItem>

View File

@@ -1,17 +1,17 @@
<script setup lang="ts">
import {onMounted, provide, watch} from "vue";
import { onMounted, provide, watch } from "vue";
import Statistics from "@/pages/word/Statistics.vue";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {Dict, ShortcutKey, StudyData, Word} from "@/types/types.ts";
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { Dict, ShortcutKey, StudyData, Word } from "@/types/types.ts";
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
import useTheme from "@/hooks/theme.ts";
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
import {_getDictDataByUrl, cloneDeep, shuffle} from "@/utils";
import {useRoute, useRouter} from "vue-router";
import { getCurrentStudyWord, useWordOptions } from "@/hooks/dict.ts";
import { _getDictDataByUrl, cloneDeep, shuffle } from "@/utils";
import { useRoute, useRouter } from "vue-router";
import Footer from "@/pages/word/components/Footer.vue";
import Panel from "@/components/Panel.vue";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -19,13 +19,14 @@ import Tooltip from "@/components/base/Tooltip.vue";
import WordList from "@/components/list/WordList.vue";
import TypeWord from "@/pages/word/components/TypeWord.vue";
import Empty from "@/components/Empty.vue";
import {useBaseStore} from "@/stores/base.ts";
import {usePracticeStore} from "@/stores/practice.ts";
import { useBaseStore } from "@/stores/base.ts";
import { usePracticeStore } from "@/stores/practice.ts";
import Toast from '@/components/base/toast/Toast.ts'
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import ConflictNotice from "@/components/ConflictNotice.vue";
import dict_list from "@/assets/dict-list.json";
import PracticeLayout from "@/components/PracticeLayout.vue";
import { PracticeSaveKey } from "@/utils/const.ts";
interface IProps {
new: Word[],
@@ -92,9 +93,28 @@ watch(() => store.load, (n) => {
if (n && loading) init()
}, {immediate: true})
function checkSaveData() {
let d = localStorage.getItem(PracticeSaveKey.Word)
if (d) {
try {
let obj = JSON.parse(d)
console.log('obj', obj)
studyData = obj.studyData
data = obj.practiceData
return true
} catch (e) {
localStorage.removeItem(PracticeSaveKey.Word)
}
}
return false
}
onMounted(() => {
//如果是从单词学习主页过来的,就直接使用;否则等待加载
if (runtimeStore.routeData) {
studyData = runtimeStore.routeData
if (!checkSaveData()) {
studyData = runtimeStore.routeData
}
} else {
loading = true
}
@@ -168,6 +188,8 @@ function next(isTyping: boolean = true) {
statStore.spend = Date.now() - statStore.startDate
console.log('全完学完了')
showStatDialog = true
localStorage.removeItem(PracticeSaveKey.Word)
return;
// emit('complete', {})
}
@@ -180,8 +202,8 @@ function next(isTyping: boolean = true) {
data.words = shuffle(studyData.write)
data.index = 0
} else {
console.log('开始默认所有单词-无单词过')
next()
console.log('开始默认所有单词-无单词过')
return next()
}
}
@@ -194,8 +216,8 @@ function next(isTyping: boolean = true) {
data.words = shuffle(studyData.review)
data.index = 0
} else {
console.log('开始默写昨日-无单词过')
next()
console.log('开始默写昨日-无单词过')
return next()
}
}
@@ -208,8 +230,8 @@ function next(isTyping: boolean = true) {
data.words = shuffle(studyData.review)
data.index = 0
} else {
console.log('开始复习昨日-无单词过')
next()
console.log('开始复习昨日-无单词过')
return next()
}
}
@@ -230,8 +252,15 @@ function next(isTyping: boolean = true) {
} else {
data.index++
isTyping && statStore.inputWordNumber++
console.log('这个词完了')
// console.log('这个词完了')
}
localStorage.setItem(PracticeSaveKey.Word, JSON.stringify({
studyData,
practiceData: data,
statStoreData: statStore.$state,
}))
console.log('wordPracticeData',)
}
function onTypeWrong() {

View File

@@ -1,25 +1,26 @@
<script setup lang="ts">
import {useBaseStore} from "@/stores/base.ts";
import {useRouter} from "vue-router";
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import {_getAccomplishDate, _getDictDataByUrl, useNav} from "@/utils";
import { _getAccomplishDate, _getDictDataByUrl, useNav } from "@/utils";
import BasePage from "@/components/BasePage.vue";
import {DictResource} from "@/types/types.ts";
import {watch} from "vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import { DictResource } from "@/types/types.ts";
import { watch } from "vue";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import Book from "@/components/Book.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import Progress from '@/components/base/Progress.vue';
import Toast from '@/components/base/toast/Toast.ts';
import BaseButton from "@/components/BaseButton.vue";
import {getDefaultDict} from "@/types/func.ts";
import { getDefaultDict } from "@/types/func.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
import {useSettingStore} from "@/stores/setting.ts";
import { useSettingStore } from "@/stores/setting.ts";
import recommendDictList from "@/assets/recommend-dict-list.json";
import CollectNotice from "@/components/CollectNotice.vue";
import { PracticeSaveKey } from "@/utils/const.ts";
const store = useBaseStore()
@@ -28,6 +29,7 @@ const router = useRouter()
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
let loading = $ref(true)
let isSaveData = $ref(false)
let currentStudy = $ref({
new: [],
review: [],
@@ -46,7 +48,19 @@ async function init() {
}
// console.log(store.sdict)
if (!currentStudy.new.length && store.sdict.words.length) {
currentStudy = getCurrentStudyWord()
let d = localStorage.getItem(PracticeSaveKey.Word)
if (d) {
try {
let data = JSON.parse(d)
currentStudy = data.studyData
isSaveData = true
} catch (e) {
localStorage.removeItem(PracticeSaveKey.Word)
currentStudy = getCurrentStudyWord()
}
} else {
currentStudy = getCurrentStudyWord()
}
}
loading = false
}
@@ -71,7 +85,6 @@ function startPractice() {
}
}
let showPracticeSettingDialog = $ref(false)
let showChangeLastPracticeIndexDialog = $ref(false)
@@ -162,7 +175,7 @@ function check(cb: Function) {
</div>
<div class="w-3/10 flex flex-col justify-evenly">
<div class="center text-xl">今日任务</div>
<div class="center text-xl">{{ isSaveData ? '上次学习任务' : '今日任务' }}</div>
<div class="flex">
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
@@ -196,7 +209,7 @@ function check(cb: Function) {
:loading="loading"
@click="startPractice">
<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>
@@ -246,7 +259,15 @@ function check(cb: Function) {
<PracticeSettingDialog
:show-left-option="false"
v-model="showPracticeSettingDialog" @ok="currentStudy = getCurrentStudyWord()"/>
v-model="showPracticeSettingDialog"
@ok="()=>{
if(isSaveData) {
Toast.success('修改成功,完成当前任务后生效')
}else {
Toast.success('修改成功')
currentStudy = getCurrentStudyWord()
}
}"/>
<ChangeLastPracticeIndexDialog
v-model="showChangeLastPracticeIndexDialog"

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import {ShortcutKey, Word} from "@/types/types.ts";
import { ShortcutKey, Word } from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useSettingStore} from "@/stores/setting.ts";
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio, useTTsPlayAudio} from "@/hooks/sound.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {nextTick, onMounted, onUnmounted, watch} from "vue";
import { useSettingStore } from "@/stores/setting.ts";
import { usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio, useTTsPlayAudio } from "@/hooks/sound.ts";
import { emitter, EventKey } from "@/utils/eventBus.ts";
import { nextTick, onMounted, onUnmounted, watch } from "vue";
import Tooltip from "@/components/base/Tooltip.vue";
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {getDefaultWord} from "@/types/func.ts";
import {sleep} from "@/utils";
import { usePracticeStore } from "@/stores/practice.ts";
import { getDefaultWord } from "@/types/func.ts";
import { _nextTick, sleep } from "@/utils";
interface IProps {
word: Word,
@@ -30,7 +30,12 @@ let showFullWord = $ref(false)
//输入锁定因为跳转到下一个单词有延时如果重复在延时期间内重复输入导致会跳转N次
let inputLock = false
let wordRepeatCount = 0
let cursor = $ref({
top: 0,
left: 0,
})
const settingStore = useSettingStore()
const statStore = usePracticeStore()
const playBeep = usePlayBeep()
const playCorrect = usePlayCorrect()
@@ -38,6 +43,7 @@ const playKeyboardAudio = usePlayKeyboardAudio()
const playWordAudio = usePlayWordAudio()
// const ttsPlayAudio = useTTsPlayAudio()
const volumeIconRef: any = $ref()
const typingWordRef = $ref<HTMLDivElement>()
// const volumeTranslateIconRef: any = $ref()
let displayWord = $computed(() => {
@@ -63,6 +69,7 @@ watch(() => props.word, () => {
}
// 更新当前单词信息
updateCurrentWordInfo();
checkCursorPosition()
}, {deep: true})
// 监听输入变化,更新当前单词信息
@@ -73,7 +80,7 @@ watch(() => input, () => {
onMounted(() => {
// 初始化当前单词信息
updateCurrentWordInfo();
emitter.on(EventKey.resetWord, () => {
wrong = input = ''
updateCurrentWordInfo();
@@ -110,22 +117,22 @@ async function onTyping(e: KeyboardEvent) {
}
let letter = e.key
inputLock = true
// 检查当前单词是否包含空格
const wordContainsSpace = props.word.word.includes(' ')
// 如果是空格键,需要判断是作为输入还是切换单词
if (letter === ' ' || e.code === 'Space') {
// 如果当前单词包含空格
if (wordContainsSpace && props.word.word[input.length] === ' ') {
letter = ' '
}
}
// 如果当前单词不包含空格,且已经输入完成,则视为切换单词的信号
else if (!wordContainsSpace && input.toLowerCase() === props.word.word.toLowerCase()) {
return emit('complete')
}
}
let isTypingRight = false
if (settingStore.ignoreCase) {
isTypingRight = letter.toLowerCase() === props.word.word[input.length].toLowerCase()
@@ -171,6 +178,7 @@ async function onTyping(e: KeyboardEvent) {
} else {
inputLock = false
}
checkCursorPosition()
}
function del() {
@@ -182,12 +190,11 @@ function del() {
} else {
input = input.slice(0, -1)
}
// 更新当前单词信息
updateCurrentWordInfo();
}
const statStore = usePracticeStore()
function showWord() {
if (settingStore.allowWordTip) {
@@ -226,7 +233,7 @@ function hideWordInTranslation(text: string, word: string): string {
if (!text || !word) {
return text
}
// 创建正则表达式,匹配单词本身及其常见变形(如复数、过去式等)
const wordBase = word.toLowerCase()
const patterns = [
@@ -236,13 +243,13 @@ function hideWordInTranslation(text: string, word: string): string {
`\\b${escapeRegExp(wordBase)}ed\\b`, // 过去式
`\\b${escapeRegExp(wordBase)}ing\\b`, // 进行时
]
let result = text
patterns.forEach(pattern => {
const regex = new RegExp(pattern, 'gi')
result = result.replace(regex, match => `<span class="word-shadow">${match}</span>`)
})
return result
}
@@ -251,10 +258,37 @@ function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
watch(() => input, checkCursorPosition)
//检测光标位置
function checkCursorPosition() {
_nextTick(() => {
// 选中目标元素
const cursorEl = document.querySelector(`.cursor`);
const input = document.querySelector(`.input`);
const typingWordRect = typingWordRef.getBoundingClientRect();
if (input) {
let inputRect = input.getBoundingClientRect();
cursor = {
top: inputRect.top + inputRect.height - cursorEl.clientHeight - typingWordRect.top,
left: inputRect.right - typingWordRect.left - 3,
};
} else {
const letter = document.querySelector(`.letter`);
let letterRect = letter.getBoundingClientRect();
cursor = {
top: letterRect.top + letterRect.height - cursorEl.clientHeight - typingWordRect.top,
left: letterRect.left - typingWordRect.left - 3,
};
}
},)
}
</script>
<template>
<div class="typing-word">
<div class="typing-word" ref="typingWordRef">
<div class="flex flex-col items-center">
<div class="flex gap-1 mt-26">
<div class="phonetic" v-if="settingStore.soundType === 'us' && word.phonetic0">[{{
@@ -265,12 +299,9 @@ function escapeRegExp(string: string): string {
(settingStore.dictation && !showFullWord) ? '_'.repeat(word.phonetic1.length) : word.phonetic1
}}]
</div>
<Tooltip
<VolumeIcon
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
>
<VolumeIcon ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
</Tooltip>
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
</div>
<div class="word my-1"
@@ -318,7 +349,7 @@ function escapeRegExp(string: string): string {
<div class="tabs">
<div @click="tab = 0" class="tab" :class="tab === 0 && 'active'">短语</div>
<div @click="tab = 1" class="tab" :class="tab === 1 && 'active'">同近义词</div>
<!-- <div @click="tab = 2" class="tab" :class="tab === 2 && 'active'">同根词</div>-->
<!-- <div @click="tab = 2" class="tab" :class="tab === 2 && 'active'">同根词</div>-->
<div @click="tab = 3" class="tab" :class="tab === 3 && 'active'">词源</div>
</div>
</template>
@@ -361,6 +392,8 @@ function escapeRegExp(string: string): string {
</div>
</template>
</div>
<div class="cursor"
:style="{top:cursor.top+'px',left:cursor.left+'px',height: settingStore.fontSize.wordForeignFontSize +'px'}"></div>
</div>
</template>