diff --git a/src/hooks/event.ts b/src/hooks/event.ts index 75d27fd3..cdbdb1ea 100644 --- a/src/hooks/event.ts +++ b/src/hooks/event.ts @@ -2,6 +2,7 @@ import {onMounted, onUnmounted, watch, onDeactivated} from "vue"; import {emitter, EventKey} from "@/utils/eventBus.ts"; import {useRuntimeStore} from "@/stores/runtime.ts"; import {useSettingStore} from "@/stores/setting.ts"; +import {ShortcutKey} from "@/types/types.ts"; export function useWindowClick(cb: (e: PointerEvent) => void) { onMounted(() => { @@ -53,6 +54,24 @@ export function useStartKeyboardEventListener() { useEventListener('keydown', (e: KeyboardEvent) => { if (!runtimeStore.disableEventListener) { + + // 检查当前单词是否包含空格,如果包含,则空格键应该被视为输入 + if (e.code === 'Space') { + // 获取当前正在输入的单词信息 + const currentWord = window.__CURRENT_WORD_INFO__; + + // 如果当前单词包含空格,且下一个字符应该是空格,则将空格键视为输入 + // 或者如果当前处于输入锁定状态(等待空格输入),也将空格键视为输入 + if (currentWord && + ((currentWord.word && + currentWord.word.includes(' ') && + currentWord.word[currentWord.input.length] === ' ') || + currentWord.inputLock === true)) { + e.preventDefault(); + return emitter.emit(EventKey.onTyping, e); + } + } + let shortcutKey = getShortcutKey(e) // console.log('shortcutKey', shortcutKey) @@ -71,9 +90,15 @@ export function useStartKeyboardEventListener() { emitter.emit(shortcutEvent, e) } else { //非英文模式下,输入区域的 keyCode 均为 229时, + // 空格键始终应该被转发到onTyping函数,由它来决定是作为输入还是切换单词 + if (e.code === 'Space') { + e.preventDefault(); + return emitter.emit(EventKey.onTyping, e); + } + if (((e.keyCode >= 65 && e.keyCode <= 90) || (e.keyCode >= 48 && e.keyCode <= 57) - || e.code === 'Space' + // 空格键已经在上面处理过了 || e.code === 'Slash' || e.code === 'Quote' || e.code === 'Comma' diff --git a/src/pages/pc/article/components/EditArticle.vue b/src/pages/pc/article/components/EditArticle.vue index 38402c11..265daa0b 100644 --- a/src/pages/pc/article/components/EditArticle.vue +++ b/src/pages/pc/article/components/EditArticle.vue @@ -147,7 +147,7 @@ function save(option: 'save' | 'saveAndNext') { let d = cloneDeep(editArticle) if (!d.id) d.id = nanoid(6) delete d.sections - copy(console.json(d, 2)) + copy(JSON.stringify(d, null, 2)) const saveTemp = () => { emit(option as any, editArticle) return resolve(true) @@ -160,9 +160,31 @@ function save(option: 'save' | 'saveAndNext') { //不知道为什么直接用editArticle,取到是空的默认值 defineExpose({save, getEditArticle: () => cloneDeep(editArticle)}) -function handleChange(e: any) { +// 处理音频文件上传 +function handleAudioChange(e: any) { + // 获取上传的文件 let uploadFile = e.target?.files?.[0] if (!uploadFile) return + + // 创建一个临时的URL以访问文件 + const audioURL = URL.createObjectURL(uploadFile) + + // 设置音频源 + editArticle.audioSrc = audioURL + + // 重置input,确保即使选择同一个文件也能触发change事件 + e.target.value = '' + + Toast.success('音频添加成功') +} + +// 处理LRC文件上传 +function handleChange(e: any) { + // 获取上传的文件 + let uploadFile = e.target?.files?.[0] + if (!uploadFile) return + + // 读取文件内容 let reader = new FileReader(); reader.readAsText(uploadFile, 'UTF-8'); reader.onload = function (e) { @@ -189,9 +211,14 @@ function handleChange(e: any) { return w.audioPosition ?? [] }) }).flat() + + Toast.success('LRC文件解析成功') } } } + + // 重置input,确保即使选择同一个文件也能触发change事件 + e.target.value = '' } let currentSentence = $ref({} as any) @@ -385,10 +412,17 @@ function setStartTime(val: Sentence, i: number, j: number) {
结果
正文、译文与结果均可编辑,编辑后点击应用按钮会自动同步
- 添加音频 +
+ 添加音频 + +
添加音频LRC文件
diff --git a/src/pages/pc/article/components/TypingArticle.vue b/src/pages/pc/article/components/TypingArticle.vue index 9f94fa4d..49da4e70 100644 --- a/src/pages/pc/article/components/TypingArticle.vue +++ b/src/pages/pc/article/components/TypingArticle.vue @@ -196,6 +196,7 @@ function nextSentence() { if (!currentSection[sentenceIndex]) { sentenceIndex = 0 sectionIndex++ + if (!props.article.sections[sectionIndex]) { console.log('打完了') isEnd = true @@ -206,9 +207,27 @@ 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 } +// 在全局对象中存储当前单词信息,以便其他模块可以访问 +function updateCurrentWordInfo(currentWord: ArticleWord) { + window.__CURRENT_WORD_INFO__ = { + word: currentWord.word, + input: currentWord.input || '', + inputLock: isSpace, + containsSpace: currentWord.word.includes(' ') + }; +} + function onTyping(e: KeyboardEvent) { if (!props.article.sections.length) return // console.log('keyDown', e.key, e.code, e.keyCode) @@ -216,6 +235,9 @@ function onTyping(e: KeyboardEvent) { let currentSection = props.article.sections[sectionIndex] let currentSentence = currentSection[sentenceIndex] let currentWord: ArticleWord = currentSentence.words[wordIndex] + + // 更新当前单词信息 + updateCurrentWordInfo(currentWord); const nextWord = () => { isSpace = false @@ -224,15 +246,45 @@ function onTyping(e: KeyboardEvent) { emit('nextWord', currentWord) - if (!currentSentence.words[wordIndex]) { - wordIndex = 0 - nextSentence() + // 只在需要时更新当前单词信息,不自动跳转到下一句话 + if (wordIndex < currentSentence.words.length) { + // 更新当前单词信息 + updateCurrentWordInfo(currentSentence.words[wordIndex]); } } if (isSpace) { + // 在单词之间的空格处理 if (e.code === 'Space') { - nextWord() + // 检查下一个单词是否存在 + 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(); + } } else { wrong = ' ' playBeep() @@ -248,6 +300,14 @@ function onTyping(e: KeyboardEvent) { 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] @@ -281,12 +341,25 @@ function onTyping(e: KeyboardEvent) { if (!currentWord.isSymbol) { playCorrect() } - if (currentWord.nextSpace) { - isSpace = true + + // 检查是否是句子的最后一个单词 + const isLastWordInSentence = wordIndex + 1 >= currentSentence.words.length; + + if (isLastWordInSentence) { + // 如果是句子的最后一个单词,自动跳转到下一句 + nextSentence(); + } else if (currentWord.nextSpace) { + // 如果不是最后一个单词,且需要空格,设置等待空格输入 + isSpace = true; } else { - nextWord() + // 如果不需要空格,直接移动到下一个单词 + nextWord(); } } + + // 更新当前单词信息 + updateCurrentWordInfo(currentWord); + playKeyboardAudio() } e.preventDefault() @@ -454,8 +527,23 @@ 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) }) diff --git a/src/pages/pc/word/components/TypeWord.vue b/src/pages/pc/word/components/TypeWord.vue index 2d1ae530..c801e10c 100644 --- a/src/pages/pc/word/components/TypeWord.vue +++ b/src/pages/pc/word/components/TypeWord.vue @@ -44,6 +44,16 @@ let displayWord = $computed(() => { return props.word.word.slice(input.length + wrong.length) }) +// 在全局对象中存储当前单词信息,以便其他模块可以访问 +function updateCurrentWordInfo() { + window.__CURRENT_WORD_INFO__ = { + word: props.word.word, + input: input, + inputLock: inputLock, + containsSpace: props.word.word.includes(' ') + }; +} + watch(() => props.word, () => { wrong = input = '' wordRepeatCount = 0 @@ -51,11 +61,22 @@ watch(() => props.word, () => { if (settingStore.wordSound) { volumeIconRef?.play(400, true) } + // 更新当前单词信息 + updateCurrentWordInfo(); }, {deep: true}) +// 监听输入变化,更新当前单词信息 +watch(() => input, () => { + updateCurrentWordInfo(); +}) + onMounted(() => { + // 初始化当前单词信息 + updateCurrentWordInfo(); + emitter.on(EventKey.resetWord, () => { wrong = input = '' + updateCurrentWordInfo(); }) emitter.on(EventKey.onTyping, onTyping) @@ -80,7 +101,8 @@ function repeat() { async function onTyping(e: KeyboardEvent) { if (inputLock) { - //如果是锁定状态,说明要么输入太快;要么就是设置了不自动跳转,然后输入完了,当这种情况时,监听空格键,按下切换下一个 + //如果是锁定状态,说明要么输入太快;要么就是设置了不自动跳转,然后输入完了 + //当单词全部输入完成后,空格键用于切换到下一个单词 if (e.code === 'Space' && input.toLowerCase() === props.word.word.toLowerCase()) { return emit('complete') } @@ -88,6 +110,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() @@ -98,6 +136,8 @@ async function onTyping(e: KeyboardEvent) { input += letter wrong = '' playKeyboardAudio() + // 更新当前单词信息 + updateCurrentWordInfo(); } else { emit('wrong') wrong = letter @@ -106,6 +146,8 @@ async function onTyping(e: KeyboardEvent) { await sleep(500) if (settingStore.inputWrongClear) input = '' wrong = '' + // 更新当前单词信息 + updateCurrentWordInfo(); } if (input.toLowerCase() === props.word.word.toLowerCase()) { @@ -140,6 +182,9 @@ function del() { } else { input = input.slice(0, -1) } + + // 更新当前单词信息 + updateCurrentWordInfo(); } const statStore = usePracticeStore() diff --git a/src/types/global.d.ts b/src/types/global.d.ts index ce1821d0..83269e79 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -8,6 +8,12 @@ declare global { interface Window { umami: { track(name: string, data?: any): void + }, + __CURRENT_WORD_INFO__?: { + word: string, + input: string, + inputLock: boolean, + containsSpace: boolean } } }