update
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -56,8 +56,10 @@ declare module 'vue' {
|
||||
Toolbar: typeof import('./src/components/Toolbar/Toolbar.vue')['default']
|
||||
Tooltip: typeof import('./src/components/Tooltip.vue')['default']
|
||||
TranslateSetting: typeof import('./src/components/Toolbar/TranslateSetting.vue')['default']
|
||||
Typing: typeof import('./src/components/Practice/PracticeWord/Typing.vue')['default']
|
||||
TypingArticle: typeof import('./src/components/Practice/PracticeArticle/TypingArticle.vue')['default']
|
||||
TypingWord: typeof import('./src/components/Practice/PracticeWord/TypingWord.vue')['default']
|
||||
TypingWord2: typeof import('./src/components/Practice/PracticeWord/TypingWord2.vue')['default']
|
||||
VolumeIcon: typeof import('./src/components/VolumeIcon.vue')['default']
|
||||
VolumeSetting: typeof import('./src/components/Toolbar/VolumeSetting.vue')['default']
|
||||
WordItem: typeof import('./src/components/WordItem.vue')['default']
|
||||
|
||||
198
src/components/Practice/PracticeWord/Typing.vue
Normal file
198
src/components/Practice/PracticeWord/Typing.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import {DefaultWord, Word} from "@/types.ts";
|
||||
import VolumeIcon from "@/components/VolumeIcon.vue";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {onMounted} from "vue/dist/vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {onUnmounted, watch} from "vue";
|
||||
|
||||
interface IProps {
|
||||
word: Word,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
word: () => cloneDeep(DefaultWord),
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
next: [],
|
||||
wrong: []
|
||||
}>()
|
||||
|
||||
let input = $ref('')
|
||||
let wrong = $ref('')
|
||||
let showFullWord = $ref(false)
|
||||
//输入锁定,因为跳转到下一个单词有延时,如果重复在延时期间内重复输入,导致会跳转N次
|
||||
let inputLock = false
|
||||
let wordRepeatCount = 0
|
||||
const store = useBaseStore()
|
||||
const practiceStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const playBeep = usePlayBeep()
|
||||
const playCorrect = usePlayCorrect()
|
||||
const playKeyboardAudio = usePlayKeyboardAudio()
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
const volumeIconRef: any = $ref()
|
||||
|
||||
let displayWord = $computed(() => {
|
||||
return props.word.name.slice(input.length + wrong.length)
|
||||
})
|
||||
|
||||
watch(() => props.word, () => {
|
||||
wrong = input = ''
|
||||
wordRepeatCount = 0
|
||||
inputLock = false
|
||||
if (settingStore.wordSound) {
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.resetWord, () => {
|
||||
wrong = input = ''
|
||||
})
|
||||
|
||||
emitter.on(EventKey.onTyping, onTyping)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.resetWord)
|
||||
emitter.off(EventKey.onTyping, onTyping)
|
||||
})
|
||||
|
||||
|
||||
function repeat() {
|
||||
setTimeout(() => {
|
||||
wrong = input = ''
|
||||
wordRepeatCount++
|
||||
inputLock = false
|
||||
|
||||
if (settingStore.wordSound) {
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
}, settingStore.waitTimeForChangeWord)
|
||||
}
|
||||
|
||||
|
||||
async function onTyping(e: KeyboardEvent) {
|
||||
if (inputLock) return
|
||||
inputLock = true
|
||||
let letter = e.key
|
||||
let isTypingRight = false
|
||||
let isWordRight = false
|
||||
if (settingStore.ignoreCase) {
|
||||
isTypingRight = letter.toLowerCase() === props.word.name[input.length + 1].toLowerCase()
|
||||
isWordRight = letter.toLowerCase() === props.word.name.slice(-1).toLowerCase()
|
||||
} else {
|
||||
isTypingRight = letter === props.word.name[input.length + 1]
|
||||
isWordRight = letter === props.word.name.slice(-1)
|
||||
}
|
||||
if (isTypingRight) {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
} else {
|
||||
emit('wrong')
|
||||
wrong = letter
|
||||
playKeyboardAudio()
|
||||
playBeep()
|
||||
setTimeout(() => {
|
||||
wrong = ''
|
||||
}, 500)
|
||||
}
|
||||
|
||||
if (isWordRight) {
|
||||
playCorrect()
|
||||
if (settingStore.repeatCount == 100) {
|
||||
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('next'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
} else {
|
||||
if (settingStore.repeatCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('next'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputLock = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="typing-word">
|
||||
<div class="translate"
|
||||
:style="{
|
||||
fontSize: settingStore.fontSize.wordTranslateFontSize +'rem',
|
||||
opacity: settingStore.translate ? 1 : 0
|
||||
}"
|
||||
>
|
||||
<div v-for="i in word.trans">{{ i }}</div>
|
||||
</div>
|
||||
<div class="word-wrapper">
|
||||
<div class="word"
|
||||
:class="wrong && 'is-wrong'"
|
||||
:style="{fontSize: settingStore.fontSize.wordForeignFontSize +'rem'}"
|
||||
>
|
||||
<span class="input" v-if="input">{{ input }}</span>
|
||||
<span class="wrong" v-if="wrong">{{ wrong }}</span>
|
||||
<template v-if="settingStore.dictation">
|
||||
<span class="letter" v-if="!showFullWord"
|
||||
@mouseenter="settingStore.allowWordTip && (showFullWord = true)">{{
|
||||
displayWord.split('').map(() => '_').join('')
|
||||
}}</span>
|
||||
<span class="letter" v-else @mouseleave="showFullWord = false">{{ displayWord }}</span>
|
||||
</template>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</div>
|
||||
<VolumeIcon ref="volumeIconRef" :simple="true" :cb="playWordAudio(word.name)"/>
|
||||
</div>
|
||||
<div class="phonetic">{{ settingStore.wordSoundType === 'us' ? word.usphone : word.ukphone }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.typing-word {
|
||||
.phonetic, .translate {
|
||||
font-size: 20rem;
|
||||
margin-left: -30rem;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.word-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
|
||||
.word {
|
||||
font-size: 48rem;
|
||||
line-height: 1;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
||||
letter-spacing: 5rem;
|
||||
|
||||
.input {
|
||||
color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
.wrong {
|
||||
color: rgba(red, 0.6);
|
||||
}
|
||||
|
||||
&.is-wrong {
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,8 +12,8 @@ import {useOnKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import VolumeIcon from "@/components/VolumeIcon.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import WordPanel from "./WordPanel.vue";
|
||||
import Options from "@/components/Practice/Options.vue";
|
||||
import Typing from "@/components/Practice/PracticeWord/Typing.vue";
|
||||
|
||||
interface IProps {
|
||||
words: Word[],
|
||||
@@ -34,20 +34,10 @@ let data = $ref({
|
||||
|
||||
let input = $ref('')
|
||||
let wrong = $ref('')
|
||||
let showFullWord = $ref(false)
|
||||
//输入锁定,因为跳转到下一个单词有延时,如果重复在延时期间内重复输入,导致会跳转N次
|
||||
let inputLock = $ref(false)
|
||||
let wordRepeatCount = $ref(0)
|
||||
const store = useBaseStore()
|
||||
const practiceStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const playBeep = usePlayBeep()
|
||||
const playCorrect = usePlayCorrect()
|
||||
const playKeyboardAudio = usePlayKeyboardAudio()
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
const volumeIconRef: any = $ref()
|
||||
|
||||
watch(() => props.words, () => {
|
||||
data.words = props.words
|
||||
data.index = props.index
|
||||
@@ -63,17 +53,6 @@ watch(() => props.words, () => {
|
||||
practiceStore.wrongWordNumber = 0
|
||||
}, {immediate: true})
|
||||
|
||||
watch(() => data.index, (n) => {
|
||||
wrong = input = ''
|
||||
practiceStore.index = n
|
||||
wordRepeatCount = 0
|
||||
inputLock = false
|
||||
if (settingStore.wordSound) {
|
||||
playWordAudio(word.name)
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
})
|
||||
|
||||
const word = $computed(() => {
|
||||
return data.words[data.index] ?? {
|
||||
trans: [],
|
||||
@@ -91,15 +70,7 @@ const nextWord: Word = $computed(() => {
|
||||
return data.words?.[data.index + 1] ?? undefined
|
||||
})
|
||||
|
||||
let resetWord = $computed(() => {
|
||||
return word.name.slice(input.length + wrong.length)
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.resetWord, () => {
|
||||
wrong = input = ''
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -108,6 +79,7 @@ function next(isTyping: boolean = true) {
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前背完了,但还有错词')
|
||||
data.words = cloneDeep(data.wrongWords)
|
||||
//如果原始错词没值就复制当前错词的,因为第一遍错词是最多的,后续的练习都是从错词中练习
|
||||
if (!data.originWrongWords.length) {
|
||||
data.originWrongWords = cloneDeep(data.wrongWords)
|
||||
}
|
||||
@@ -171,114 +143,46 @@ function remove() {
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
showFullWord = false
|
||||
// showFullWord = false
|
||||
}
|
||||
|
||||
function repeat() {
|
||||
setTimeout(() => {
|
||||
wrong = input = ''
|
||||
wordRepeatCount++
|
||||
inputLock = false
|
||||
|
||||
if (settingStore.wordSound) {
|
||||
playWordAudio(word.name)
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
}, settingStore.waitTimeForChangeWord)
|
||||
function wordWrong() {
|
||||
if (!store.wrong.originWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
store.wrong.originWords.push(word)
|
||||
store.wrong.words.push(word)
|
||||
store.wrong.chapterWords = [store.wrong.words]
|
||||
}
|
||||
if (!data.wrongWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
data.wrongWords.push(word)
|
||||
practiceStore.wrongWordNumber++
|
||||
}
|
||||
}
|
||||
|
||||
async function onKeyDown(e: KeyboardEvent) {
|
||||
//TODO 还有横杠
|
||||
//非英文模式下,输入区域的 keyCode 均为 229时,
|
||||
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'
|
||||
|| e.code === 'BracketLeft'
|
||||
|| e.code === 'BracketRight'
|
||||
|| e.code === 'Period'
|
||||
|| e.code === 'Minus'
|
||||
|| e.code === 'Equal'
|
||||
|| e.code === 'Semicolon'
|
||||
|| e.code === 'Backquote'
|
||||
|| e.keyCode === 229
|
||||
) {
|
||||
if (inputLock) return
|
||||
inputLock = true
|
||||
let letter = e.key
|
||||
let isWrong = false
|
||||
if (settingStore.ignoreCase) {
|
||||
isWrong = (input + letter).toLowerCase() !== word.name.toLowerCase().slice(0, input.length + 1)
|
||||
} else {
|
||||
isWrong = (input + letter) !== word.name.slice(0, input.length + 1)
|
||||
}
|
||||
if (isWrong) {
|
||||
if (!store.wrong.originWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
store.wrong.originWords.push(word)
|
||||
store.wrong.words.push(word)
|
||||
store.wrong.chapterWords = [store.wrong.words]
|
||||
}
|
||||
if (!data.wrongWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
data.wrongWords.push(word)
|
||||
practiceStore.wrongWordNumber++
|
||||
}
|
||||
wrong = letter
|
||||
playKeyboardAudio()
|
||||
playBeep()
|
||||
setTimeout(() => {
|
||||
// console.log('e', e)
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
}, 500)
|
||||
} else {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
}
|
||||
if (input.toLowerCase() === word.name.toLowerCase()) {
|
||||
playCorrect()
|
||||
if (settingStore.repeatCount == 100) {
|
||||
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
|
||||
setTimeout(next, settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
} else {
|
||||
if (settingStore.repeatCount <= wordRepeatCount + 1) {
|
||||
setTimeout(next, settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
input = input.slice(0, -1)
|
||||
}
|
||||
} else {
|
||||
inputLock = false
|
||||
}
|
||||
} else {
|
||||
// console.log('e', e)
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
} else {
|
||||
input = input.slice(0, -1)
|
||||
}
|
||||
break
|
||||
case ShortKeyMap.Collect:
|
||||
collect()
|
||||
break
|
||||
case ShortKeyMap.Remove:
|
||||
remove()
|
||||
break
|
||||
case ShortKeyMap.Ignore:
|
||||
skip()
|
||||
e.preventDefault()
|
||||
break
|
||||
case ShortKeyMap.Show:
|
||||
if (settingStore.allowWordTip) {
|
||||
showFullWord = true
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case ShortKeyMap.Collect:
|
||||
collect()
|
||||
break
|
||||
case ShortKeyMap.Remove:
|
||||
remove()
|
||||
break
|
||||
case ShortKeyMap.Ignore:
|
||||
skip()
|
||||
e.preventDefault()
|
||||
break
|
||||
case ShortKeyMap.Show:
|
||||
if (settingStore.allowWordTip) {
|
||||
// showFullWord = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,33 +208,11 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="translate"
|
||||
:style="{
|
||||
fontSize: settingStore.fontSize.wordTranslateFontSize +'rem',
|
||||
opacity: settingStore.translate ? 1 : 0
|
||||
}"
|
||||
>
|
||||
<div v-for="i in word.trans">{{ i }}</div>
|
||||
</div>
|
||||
<div class="word-wrapper">
|
||||
<div class="word"
|
||||
:class="wrong && 'is-wrong'"
|
||||
:style="{fontSize: settingStore.fontSize.wordForeignFontSize +'rem'}"
|
||||
>
|
||||
<span class="input" v-if="input">{{ input }}</span>
|
||||
<span class="wrong" v-if="wrong">{{ wrong }}</span>
|
||||
<template v-if="settingStore.dictation">
|
||||
<span class="letter" v-if="!showFullWord"
|
||||
@mouseenter="settingStore.allowWordTip && (showFullWord = true)">{{
|
||||
resetWord.split('').map(v => '_').join('')
|
||||
}}</span>
|
||||
<span class="letter" v-else @mouseleave="showFullWord = false">{{ resetWord }}</span>
|
||||
</template>
|
||||
<span class="letter" v-else>{{ resetWord }}</span>
|
||||
</div>
|
||||
<VolumeIcon ref="volumeIconRef" :simple="true" @click="playWordAudio(word.name)"/>
|
||||
</div>
|
||||
<div class="phonetic">{{ settingStore.wordSoundType === 'us' ? word.usphone : word.ukphone }}</div>
|
||||
<Typing
|
||||
:word="word"
|
||||
@wrong="wordWrong"
|
||||
@next="next"
|
||||
/>
|
||||
<Options
|
||||
@remove="remove"
|
||||
@skip="skip"
|
||||
@@ -398,35 +280,5 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
}
|
||||
}
|
||||
|
||||
.phonetic, .translate {
|
||||
font-size: 20rem;
|
||||
margin-left: -30rem;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.word-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
|
||||
.word {
|
||||
font-size: 48rem;
|
||||
line-height: 1;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
||||
letter-spacing: 5rem;
|
||||
|
||||
.input {
|
||||
color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
.wrong {
|
||||
color: rgba(red, 0.6);
|
||||
}
|
||||
|
||||
&.is-wrong {
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
431
src/components/Practice/PracticeWord/TypingWord2.vue
Normal file
431
src/components/Practice/PracticeWord/TypingWord2.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, watch} from "vue"
|
||||
import {$computed, $ref} from "vue/macros"
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import {DictType, DisplayStatistics, ShortKeyMap, Word} from "../../../types";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts"
|
||||
import {cloneDeep} from "lodash-es"
|
||||
import {usePracticeStore} from "@/stores/practice.ts"
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import VolumeIcon from "@/components/VolumeIcon.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import Options from "@/components/Practice/Options.vue";
|
||||
|
||||
interface IProps {
|
||||
words: Word[],
|
||||
index: number,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
words: [],
|
||||
index: -1
|
||||
})
|
||||
|
||||
let data = $ref({
|
||||
index: props.index,
|
||||
words: props.words,
|
||||
wrongWords: [],
|
||||
originWrongWords: [],
|
||||
})
|
||||
|
||||
let input = $ref('')
|
||||
let wrong = $ref('')
|
||||
let showFullWord = $ref(false)
|
||||
//输入锁定,因为跳转到下一个单词有延时,如果重复在延时期间内重复输入,导致会跳转N次
|
||||
let inputLock = $ref(false)
|
||||
let wordRepeatCount = $ref(0)
|
||||
const store = useBaseStore()
|
||||
const practiceStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const playBeep = usePlayBeep()
|
||||
const playCorrect = usePlayCorrect()
|
||||
const playKeyboardAudio = usePlayKeyboardAudio()
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
const volumeIconRef: any = $ref()
|
||||
|
||||
watch(() => props.words, () => {
|
||||
data.words = props.words
|
||||
data.index = props.index
|
||||
data.originWrongWords = []
|
||||
data.wrongWords = []
|
||||
|
||||
practiceStore.wrongWords = []
|
||||
practiceStore.repeatNumber = 0
|
||||
practiceStore.startDate = Date.now()
|
||||
practiceStore.correctRate = -1
|
||||
practiceStore.total = props.words.length
|
||||
practiceStore.inputWordNumber = 0
|
||||
practiceStore.wrongWordNumber = 0
|
||||
}, {immediate: true})
|
||||
|
||||
watch(() => data.index, (n) => {
|
||||
wrong = input = ''
|
||||
practiceStore.index = n
|
||||
wordRepeatCount = 0
|
||||
inputLock = false
|
||||
if (settingStore.wordSound) {
|
||||
playWordAudio(word.name)
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
})
|
||||
|
||||
const word = $computed(() => {
|
||||
return data.words[data.index] ?? {
|
||||
trans: [],
|
||||
name: '',
|
||||
usphone: '',
|
||||
ukphone: '',
|
||||
}
|
||||
})
|
||||
|
||||
const prevWord: Word = $computed(() => {
|
||||
return data.words?.[data.index - 1] ?? undefined
|
||||
})
|
||||
|
||||
const nextWord: Word = $computed(() => {
|
||||
return data.words?.[data.index + 1] ?? undefined
|
||||
})
|
||||
|
||||
let resetWord = $computed(() => {
|
||||
return word.name.slice(input.length + wrong.length)
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.resetWord, () => {
|
||||
wrong = input = ''
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
function next(isTyping: boolean = true) {
|
||||
if (data.index === data.words.length - 1) {
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前背完了,但还有错词')
|
||||
data.words = cloneDeep(data.wrongWords)
|
||||
if (!data.originWrongWords.length) {
|
||||
data.originWrongWords = cloneDeep(data.wrongWords)
|
||||
}
|
||||
data.index = 0
|
||||
practiceStore.total = data.words.length
|
||||
practiceStore.index = 0
|
||||
practiceStore.inputWordNumber = 0
|
||||
practiceStore.wrongWordNumber = 0
|
||||
practiceStore.repeatNumber++
|
||||
data.wrongWords = []
|
||||
} else {
|
||||
console.log('这章节完了')
|
||||
isTyping && practiceStore.inputWordNumber++
|
||||
let now = Date.now()
|
||||
let stat: DisplayStatistics = {
|
||||
startDate: practiceStore.startDate,
|
||||
endDate: now,
|
||||
spend: now - practiceStore.startDate,
|
||||
total: props.words.length,
|
||||
correctRate: -1,
|
||||
wrongWordNumber: data.originWrongWords.length,
|
||||
wrongWords: data.originWrongWords,
|
||||
}
|
||||
stat.correctRate = 100 - Math.trunc(((stat.wrongWordNumber) / (stat.total)) * 100)
|
||||
emitter.emit(EventKey.openStatModal, stat)
|
||||
}
|
||||
} else {
|
||||
data.index++
|
||||
isTyping && practiceStore.inputWordNumber++
|
||||
console.log('这个词完了')
|
||||
if ([DictType.customWord, DictType.word].includes(store.current.dictType)
|
||||
&& store.skipWordNames.includes(word.name.toLowerCase())) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prev() {
|
||||
data.index--
|
||||
}
|
||||
|
||||
function skip() {
|
||||
next(false)
|
||||
}
|
||||
|
||||
function collect() {
|
||||
if (!store.collect.originWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
store.collect.originWords.push(word)
|
||||
store.collect.words.push(word)
|
||||
store.collect.chapterWords = [store.collect.words]
|
||||
}
|
||||
}
|
||||
|
||||
function remove() {
|
||||
if (!store.skipWordNames.includes(word.name.toLowerCase())) {
|
||||
store.skip.originWords.push(word)
|
||||
store.skip.words.push(word)
|
||||
store.skip.chapterWords = [store.skip.words]
|
||||
}
|
||||
next(false)
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
showFullWord = false
|
||||
}
|
||||
|
||||
function repeat() {
|
||||
setTimeout(() => {
|
||||
wrong = input = ''
|
||||
wordRepeatCount++
|
||||
inputLock = false
|
||||
|
||||
if (settingStore.wordSound) {
|
||||
playWordAudio(word.name)
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
}, settingStore.waitTimeForChangeWord)
|
||||
}
|
||||
|
||||
async function onKeyDown(e: KeyboardEvent) {
|
||||
//TODO 还有横杠
|
||||
//非英文模式下,输入区域的 keyCode 均为 229时,
|
||||
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'
|
||||
|| e.code === 'BracketLeft'
|
||||
|| e.code === 'BracketRight'
|
||||
|| e.code === 'Period'
|
||||
|| e.code === 'Minus'
|
||||
|| e.code === 'Equal'
|
||||
|| e.code === 'Semicolon'
|
||||
|| e.code === 'Backquote'
|
||||
|| e.keyCode === 229
|
||||
) {
|
||||
if (inputLock) return
|
||||
inputLock = true
|
||||
let letter = e.key
|
||||
let isWrong = false
|
||||
if (settingStore.ignoreCase) {
|
||||
isWrong = (input + letter).toLowerCase() !== word.name.toLowerCase().slice(0, input.length + 1)
|
||||
} else {
|
||||
isWrong = (input + letter) !== word.name.slice(0, input.length + 1)
|
||||
}
|
||||
if (isWrong) {
|
||||
if (!store.wrong.originWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
store.wrong.originWords.push(word)
|
||||
store.wrong.words.push(word)
|
||||
store.wrong.chapterWords = [store.wrong.words]
|
||||
}
|
||||
if (!data.wrongWords.find((v: Word) => v.name.toLowerCase() === word.name.toLowerCase())) {
|
||||
data.wrongWords.push(word)
|
||||
practiceStore.wrongWordNumber++
|
||||
}
|
||||
wrong = letter
|
||||
playKeyboardAudio()
|
||||
playBeep()
|
||||
setTimeout(() => {
|
||||
wrong = ''
|
||||
}, 500)
|
||||
} else {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
}
|
||||
if (input.toLowerCase() === word.name.toLowerCase()) {
|
||||
playCorrect()
|
||||
if (settingStore.repeatCount == 100) {
|
||||
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
|
||||
setTimeout(next, settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
} else {
|
||||
if (settingStore.repeatCount <= wordRepeatCount + 1) {
|
||||
setTimeout(next, settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputLock = false
|
||||
}
|
||||
} else {
|
||||
// console.log('e', e)
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
} else {
|
||||
input = input.slice(0, -1)
|
||||
}
|
||||
break
|
||||
case ShortKeyMap.Collect:
|
||||
collect()
|
||||
break
|
||||
case ShortKeyMap.Remove:
|
||||
remove()
|
||||
break
|
||||
case ShortKeyMap.Ignore:
|
||||
skip()
|
||||
e.preventDefault()
|
||||
break
|
||||
case ShortKeyMap.Show:
|
||||
if (settingStore.allowWordTip) {
|
||||
showFullWord = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="type-word">
|
||||
<div class="near-word" v-if="settingStore.showNearWord">
|
||||
<div class="prev"
|
||||
@click="prev"
|
||||
v-if="prevWord">
|
||||
<Icon class="arrow" icon="bi:arrow-left" width="22"/>
|
||||
<div class="word">{{ prevWord.name }}</div>
|
||||
</div>
|
||||
<Tooltip title="快捷键:Tab">
|
||||
<div class="next"
|
||||
@click="next(false)"
|
||||
v-if="nextWord">
|
||||
<div class="word" :class="settingStore.dictation && 'shadow'">{{ nextWord.name }}</div>
|
||||
<Icon class="arrow" icon="bi:arrow-right" width="22"/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="translate"
|
||||
:style="{
|
||||
fontSize: settingStore.fontSize.wordTranslateFontSize +'rem',
|
||||
opacity: settingStore.translate ? 1 : 0
|
||||
}"
|
||||
>
|
||||
<div v-for="i in word.trans">{{ i }}</div>
|
||||
</div>
|
||||
<div class="word-wrapper">
|
||||
<div class="word"
|
||||
:class="wrong && 'is-wrong'"
|
||||
:style="{fontSize: settingStore.fontSize.wordForeignFontSize +'rem'}"
|
||||
>
|
||||
<span class="input" v-if="input">{{ input }}</span>
|
||||
<span class="wrong" v-if="wrong">{{ wrong }}</span>
|
||||
<template v-if="settingStore.dictation">
|
||||
<span class="letter" v-if="!showFullWord"
|
||||
@mouseenter="settingStore.allowWordTip && (showFullWord = true)">{{
|
||||
resetWord.split('').map(v => '_').join('')
|
||||
}}</span>
|
||||
<span class="letter" v-else @mouseleave="showFullWord = false">{{ resetWord }}</span>
|
||||
</template>
|
||||
<span class="letter" v-else>{{ resetWord }}</span>
|
||||
</div>
|
||||
<VolumeIcon ref="volumeIconRef" :simple="true" @click="playWordAudio(word.name)"/>
|
||||
</div>
|
||||
<div class="phonetic">{{ settingStore.wordSoundType === 'us' ? word.usphone : word.ukphone }}</div>
|
||||
<Options
|
||||
@remove="remove"
|
||||
@skip="skip"
|
||||
@collect="collect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.type-word {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
//display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
font-size: 14rem;
|
||||
color: gray;
|
||||
gap: 6rem;
|
||||
position: relative;
|
||||
width: var(--toolbar-width);
|
||||
|
||||
.near-word {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
& > div {
|
||||
width: 45%;
|
||||
align-items: center;
|
||||
|
||||
.arrow {
|
||||
min-width: 22rem;
|
||||
min-height: 22rem;
|
||||
}
|
||||
}
|
||||
|
||||
.word {
|
||||
font-size: 24rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
|
||||
.prev {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
float: left;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
.next {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10rem;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
color: transparent !important;
|
||||
text-shadow: #b0b0b0 0 0 6px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.phonetic, .translate {
|
||||
font-size: 20rem;
|
||||
margin-left: -30rem;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.word-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
|
||||
.word {
|
||||
font-size: 48rem;
|
||||
line-height: 1;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
||||
letter-spacing: 5rem;
|
||||
|
||||
.input {
|
||||
color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
.wrong {
|
||||
color: rgba(red, 0.6);
|
||||
}
|
||||
|
||||
&.is-wrong {
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import IconWrapper from "@/components/IconWrapper.vue";
|
||||
const props = withDefaults(defineProps<{
|
||||
time?: number,
|
||||
simple?: boolean
|
||||
cb?: Function
|
||||
}>(), {
|
||||
time: 400,
|
||||
simple: false
|
||||
@@ -15,22 +16,21 @@ let count = $ref(0)
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
function play(time = props.time) {
|
||||
if (count === 0) {
|
||||
props?.cb()
|
||||
}
|
||||
count++
|
||||
setTimeout(() => {
|
||||
if (step === 2) {
|
||||
if (count === 0) {
|
||||
play(time + 100)
|
||||
if (count === 1) {
|
||||
step = 0
|
||||
play(time + 100)
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
} else {
|
||||
step++
|
||||
if (step === 2) {
|
||||
count++
|
||||
play(time + 100)
|
||||
} else {
|
||||
play(time + 100)
|
||||
}
|
||||
play(time + 100)
|
||||
}
|
||||
}, time)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,26 @@ export function useStartKeyboardEventListener() {
|
||||
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (!runtimeStore.disableEventListener) {
|
||||
emitter.emit(EventKey.keydown, e)
|
||||
//非英文模式下,输入区域的 keyCode 均为 229时,
|
||||
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'
|
||||
|| e.code === 'BracketLeft'
|
||||
|| e.code === 'BracketRight'
|
||||
|| e.code === 'Period'
|
||||
|| e.code === 'Minus'
|
||||
|| e.code === 'Equal'
|
||||
|| e.code === 'Semicolon'
|
||||
|| e.code === 'Backquote'
|
||||
|| e.keyCode === 229
|
||||
) {
|
||||
emitter.emit(EventKey.onTyping, e)
|
||||
}else {
|
||||
emitter.emit(EventKey.keydown, e)
|
||||
}
|
||||
}
|
||||
})
|
||||
useEventListener('keyup', (e: KeyboardEvent) => {
|
||||
|
||||
@@ -9,4 +9,5 @@ export const EventKey = {
|
||||
closeOther: 'closeOther',
|
||||
keydown: 'keydown',
|
||||
keyup: 'keyup',
|
||||
onTyping: 'onTyping',
|
||||
}
|
||||
Reference in New Issue
Block a user