@@ -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'
|
||||
|
||||
@@ -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<Sentence>({} as any)
|
||||
@@ -385,10 +412,17 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
<div class="title">结果</div>
|
||||
<div class="center">正文、译文与结果均可编辑,编辑后点击应用按钮会自动同步</div>
|
||||
<div class="flex gap-2">
|
||||
<BaseButton>添加音频</BaseButton>
|
||||
<div class="upload relative">
|
||||
<BaseButton>添加音频</BaseButton>
|
||||
<input type="file"
|
||||
accept="audio/*"
|
||||
@change="handleAudioChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"/>
|
||||
</div>
|
||||
<div class="upload relative">
|
||||
<BaseButton>添加音频LRC文件</BaseButton>
|
||||
<input type="file"
|
||||
accept=".lrc"
|
||||
@change="handleChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"/>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
6
src/types/global.d.ts
vendored
6
src/types/global.d.ts
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user