This commit is contained in:
zyronon
2023-12-15 02:32:00 +08:00
parent d8a761205f
commit eac0d60628
12 changed files with 510 additions and 141 deletions

View File

@@ -0,0 +1,357 @@
<script setup lang="ts">
import {DefaultWord, ShortcutKey, Word} from "@/types.ts";
import VolumeIcon from "@/components/icon/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, useTTsPlayAudio} from "@/hooks/sound.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {cloneDeep} from "lodash-es";
import {onUnmounted, watch, onMounted, nextTick} from "vue";
import Tooltip from "@/components/Tooltip.vue";
interface IProps {
word: Word,
}
const props = withDefaults(defineProps<IProps>(), {
word: () => cloneDeep(DefaultWord),
})
const emit = defineEmits<{
complete: [],
wrong: []
}>()
let input = $ref('')
let wrong = $ref('')
let showFullWord = $ref(false)
//输入锁定因为跳转到下一个单词有延时如果重复在延时期间内重复输入导致会跳转N次
let inputLock = false
let wordRepeatCount = 0
const settingStore = useSettingStore()
const playBeep = usePlayBeep()
const playCorrect = usePlayCorrect()
const playKeyboardAudio = usePlayKeyboardAudio()
const playWordAudio = usePlayWordAudio()
const ttsPlayAudio = useTTsPlayAudio()
const volumeIconRef: any = $ref()
const volumeTranslateIconRef: any = $ref()
let displayWord = $computed(() => {
return props.word.word.slice(input.length + wrong.length)
})
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.word[input.length].toLowerCase()
isWordRight = (input + letter).toLowerCase() === props.word.word.toLowerCase()
} else {
isTypingRight = letter === props.word.word[input.length]
isWordRight = (input + letter) === props.word.word
}
if (isTypingRight) {
input += letter
wrong = ''
playKeyboardAudio()
} else {
emit('wrong')
wrong = letter
playKeyboardAudio()
playBeep()
volumeIconRef?.play()
setTimeout(() => {
wrong = ''
}, 500)
}
if (isWordRight) {
playCorrect()
if (settingStore.repeatCount == 100) {
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
} else {
repeat()
}
} else {
if (settingStore.repeatCount <= wordRepeatCount + 1) {
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
} else {
repeat()
}
}
} else {
inputLock = false
}
}
function del() {
playKeyboardAudio()
if (wrong) {
wrong = ''
} else {
input = input.slice(0, -1)
}
}
function showWord() {
if (settingStore.allowWordTip) {
showFullWord = true
}
}
function hideWord() {
showFullWord = false
}
function play() {
volumeIconRef?.play()
}
defineExpose({del, showWord, hideWord, play})
let transHeight = $ref(150)
let transWrapperRef = $ref<HTMLDivElement>()
let showEnd = $ref(true)
const transStyle = $computed(() => {
return {
'justify-content': showEnd ? 'flex-end' : 'unset',
height: transHeight + 'px',
opacity: settingStore.translate ? 1 : 0
}
})
watch(() => props.word, () => {
wrong = input = ''
wordRepeatCount = 0
inputLock = false
if (settingStore.wordSound) {
volumeIconRef?.play(400, true)
}
transHeight = 150
nextTick(() => {
console.log('transWrapperRef.scrollHeight', transWrapperRef.scrollHeight)
let scrollHeight = transWrapperRef.scrollHeight
if (scrollHeight <= 240) {
showEnd = true
if (scrollHeight > transHeight) {
transHeight = scrollHeight
}
} else {
showEnd = scrollHeight <= transHeight
}
})
})
</script>
<template>
<div class="typing-word">
<div class="translate"
:style="transStyle"
>
<div class="wrapper" ref="transWrapperRef">
<div class="translate-item" v-for="(v,i) in word.trans">
<span>{{ (v.pos ? v.pos + '.' : '') + v.cn }}</span>
</div>
</div>
</div>
<div class="word-wrapper"
:style="{marginTop: transHeight + 6 + 'px'}"
>
<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>
<Tooltip
:title="`发音(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
>
<VolumeIcon ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
</Tooltip>
</div>
<div class="phonetic" v-if="settingStore.wordSoundType === 'us' && word.phonetic0">[{{ word.phonetic0 }}]</div>
<div class="phonetic" v-if="settingStore.wordSoundType === 'uk' && word.phonetic1">[{{ word.phonetic1 }}]</div>
<transition name="fade">
<div class="other" v-if="settingStore.detail">
<div class="sentences" v-if="word.sentences.length">
<div class="title">例句</div>
<div class="sentence" v-for="item in word.sentences">
<div class="tran">{{ item.tran }}</div>
<div class="v">{{ item.v }}</div>
</div>
</div>
<div class="sentences" v-if="word.phrases.length">
<div class="title">短语</div>
<div class="sentence" v-for="item in word.phrases">
<div class="tran">{{ item.tran }}</div>
<div class="v">{{ item.v }}</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.typing-word {
width: 95%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
word-break: break-word;
position: relative;
color: var(--color-font-2);
overflow: auto;
padding-bottom: 20rem;
&::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.other {
margin-top: 10rem;
width: 100%;
font-size: 18rem;
.sentences {
margin-bottom: 20rem;
.title {
}
.sentence {
margin-bottom: 8rem;
.tran {
color: white;
font-size: 18rem;
margin-bottom: 2rem;
}
.v {
color: var(--color-font-1);
font-size: 14rem;
}
}
}
}
.phonetic, .translate {
transition: opacity .3s;
}
.phonetic {
font-size: 14rem;
margin-top: 5rem;
font-family: var(--word-font-family);
}
.translate {
font-size: 18rem;
width: 100%;
position: absolute;
height: 150px;
display: flex;
align-items: center;
flex-direction: column;
overflow: auto;
.wrapper {
}
&:hover {
.volumeIcon {
opacity: 1;
}
}
.translate-item {
display: flex;
align-items: center;
gap: 10rem;
}
.volumeIcon {
transition: opacity .3s;
opacity: 0;
}
}
.word-wrapper {
margin-top: 150px;
margin-left: 30rem;
display: flex;
align-items: center;
gap: 10rem;
color: var(--color-font-1);
.word {
font-size: 48rem;
line-height: 1;
font-family: var(--word-font-family);
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

@@ -8,7 +8,7 @@ import {cloneDeep, reverse, shuffle} from "lodash-es"
import {usePracticeStore} from "@/stores/practice.ts"
import {useSettingStore} from "@/stores/setting.ts";
import {useOnKeyboardEventListener, useWindowClick} from "@/hooks/event.ts";
import Typing from "@/pages/pc/practice/practice-word/Typing.vue";
import Typing from "@/pages/mobile/practice/practice-word/Typing.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useWordOptions} from "@/hooks/dict.ts";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -21,6 +21,8 @@ import SlideItem from "@/components/slide/SlideItem.vue";
import MobilePanel from "@/pages/mobile/components/MobilePanel.vue";
import router from "@/router.ts";
import {Icon} from "@iconify/vue";
import IconWrapper from "@/components/IconWrapper.vue";
import useTheme from "@/hooks/theme.ts";
interface IProps {
words: Word[],
@@ -42,6 +44,7 @@ const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const practiceStore = usePracticeStore()
const settingStore = useSettingStore()
const {toggleTheme} = useTheme()
const {
isWordCollect,
@@ -80,23 +83,15 @@ watch(data, () => {
practiceStore.index = data.index
})
const word = $computed(() => {
const word: Word = $computed(() => {
return data.words[data.index] ?? {
trans: [],
name: '',
usphone: '',
ukphone: '',
word: '',
phonetic0: '',
phonetic1: '',
}
})
const prevWord: Word = $computed(() => {
return data.words?.[data.index - 1] ?? undefined
})
const nextWord: Word = $computed(() => {
return data.words?.[data.index + 1] ?? undefined
})
function next(isTyping: boolean = true) {
if (data.index === data.words.length - 1) {
@@ -251,14 +246,19 @@ function change(e) {
inputRef.value = ''
}
function know(isTyping: boolean = false) {
inputRef.blur()
function nextWord() {
settingStore.translate = false
settingStore.detail = false
setTimeout(() => {
next(isTyping)
next(true)
}, 300)
}
function complete() {
inputRef.blur()
settingStore.detail = true
}
function unknow() {
settingStore.translate = true
inputRef.focus()
@@ -283,6 +283,12 @@ onMounted(() => {
/>
</div>
<div class="right">
<IconWrapper>
<Icon icon="ep:moon" v-if="settingStore.theme === 'dark'"
@click="toggleTheme"/>
<Icon icon="tabler:sun" v-else @click="toggleTheme"/>
</IconWrapper>
<BaseIcon
v-if="!isWordCollect(word)"
class="collect"
@@ -303,16 +309,18 @@ onMounted(() => {
@input="change"
type="text">
<Typing
style="width: 90%;"
v-loading="!store.load"
ref="typingRef"
:word="word"
@complete="know(true)"
@complete="complete"
/>
<div class="options">
<div class="wrapper">
<BaseButton @click="unknow">不认识</BaseButton>
<BaseButton @click="know">认识</BaseButton>
<BaseButton size="large" v-if="settingStore.detail" @click="nextWord">下一个</BaseButton>
<template v-else>
<BaseButton size="large" @click="unknow">不认识</BaseButton>
<BaseButton size="large" @click="nextWord">认识</BaseButton>
</template>
</div>
</div>
</div>
@@ -419,10 +427,9 @@ onMounted(() => {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
justify-content: space-between;
align-items: center;
gap: 10rem;
padding: 10rem;
padding: 0 10rem;
box-sizing: border-box;
.tool-bar {
@@ -437,7 +444,7 @@ onMounted(() => {
:deep(.word) {
letter-spacing: 0;
font-size: 40rem !important;
font-size: 36rem !important;
}
.options {
@@ -450,12 +457,11 @@ onMounted(() => {
.wrapper {
width: 80%;
display: flex;
flex-direction: column;
gap: 10rem;
gap: 20rem;
}
.base-button {
width: 100%;
:deep(.base-button) {
flex: 1;
}
}
}

View File

@@ -62,6 +62,7 @@ async function onSubmit() {
if (props.isAdd) {
data.id = 'custom-dict-' + Date.now()
//TODO 允许同名?
if (store.myDictList.find(v => v.name === data.name)) {
return ElMessage.warning('已有相同名称词典!')
} else {

View File

@@ -283,7 +283,6 @@ function syncEditDict2MyDictList() {
let wordFormData = $ref({
where: '',
type: '',
name: '',
id: '',
index: 0
})

View File

@@ -82,7 +82,7 @@ function setArticle(val: Article) {
articleData.article.sections.map((v, i) => {
v.map((w, j) => {
w.words.map(s => {
if (!store.skipWordNamesWithSimpleWords.includes(s.name.toLowerCase()) && !s.isSymbol) {
if (!store.skipWordNamesWithSimpleWords.includes(s.word.toLowerCase()) && !s.isSymbol) {
practiceStore.total++
}
})

View File

@@ -163,17 +163,17 @@ defineExpose({del, showWord, hideWord, play})
>
<div class="translate-item" v-for="(v,i) in word.trans">
<span>{{ (v.pos ? v.pos + '.' : '') + v.cn }}</span>
<!-- <div class="volumeIcon">-->
<!-- <Tooltip-->
<!-- v-if="i === word.trans.length - 1"-->
<!-- :title="`发音(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.PlayTranslatePronunciation]})`"-->
<!-- >-->
<!-- <VolumeIcon-->
<!-- ref="volumeTranslateIconRef"-->
<!-- :simple="true"-->
<!-- :cb="()=>ttsPlayAudio(word.trans.join(';'))"/>-->
<!-- </Tooltip>-->
<!-- </div>-->
<!-- <div class="volumeIcon">-->
<!-- <Tooltip-->
<!-- v-if="i === word.trans.length - 1"-->
<!-- :title="`发音(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.PlayTranslatePronunciation]})`"-->
<!-- >-->
<!-- <VolumeIcon-->
<!-- ref="volumeTranslateIconRef"-->
<!-- :simple="true"-->
<!-- :cb="()=>ttsPlayAudio(word.trans.join(';'))"/>-->
<!-- </Tooltip>-->
<!-- </div>-->
</div>
</div>
<div class="word-wrapper">
@@ -215,6 +215,7 @@ defineExpose({del, showWord, hideWord, play})
justify-content: center;
word-break: break-word;
position: relative;
color: var(--color-font-2);
.phonetic, .translate {
font-size: 20rem;
@@ -230,7 +231,6 @@ defineExpose({del, showWord, hideWord, play})
position: absolute;
transform: translateY(-50%);
margin-bottom: 90rem;
color: var(--color-font-2);
&:hover {
.volumeIcon {

View File

@@ -389,6 +389,7 @@ onUnmounted(() => {
position: absolute;
top: 0;
width: 100%;
z-index: 1;
& > div {
width: 45%;