This commit is contained in:
zyronon
2023-10-26 02:19:14 +08:00
parent 564f1a717c
commit cf3388e115
7 changed files with 699 additions and 196 deletions

2
components.d.ts vendored
View File

@@ -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']

View 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>

View File

@@ -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>

View 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>

View File

@@ -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)
}

View File

@@ -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) => {

View File

@@ -9,4 +9,5 @@ export const EventKey = {
closeOther: 'closeOther',
keydown: 'keydown',
keyup: 'keyup',
onTyping: 'onTyping',
}