This commit is contained in:
zyronon
2023-10-27 01:32:21 +08:00
parent cf3388e115
commit 1cbbd24cda
12 changed files with 329 additions and 718 deletions

View File

@@ -54,3 +54,5 @@ http://enpuz.com/ 语法分析工具
背单词页面div位置应该恒定不应该随翻译内容变动而跳动
点击句子播放的音乐,需要可暂停
footer 的输入数统计有问题,当在列表点一个,然后输入错误之后,不会统计到输入数里面(单词和文章的都有问题)

View File

@@ -103,12 +103,7 @@ function changeIndex(i: number, dict: Dict) {
<div class="dict-name">词数{{ currentData.list.length }}</div>
</header>
<div class="content">
<WordList
class="word-list"
@change="(i:number) => changeIndex(i,currentDict)"
:isActive="settingStore.showPanel && tabIndex === 0"
:list="currentData.list"
:activeIndex="currentData.index"/>
<slot></slot>
</div>
<footer v-if="![DictType.customWord,DictType.word].includes(store.current.dictType)">
<PopConfirm

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import {$ref} from "vue/macros";
import TypingArticle from "./TypingArticle.vue";
import {Article, DefaultArticle, TranslateType} from "@/types.ts";
import {Article, ArticleWord, DefaultArticle, DefaultWord, DisplayStatistics, TranslateType, Word} from "@/types.ts";
import {cloneDeep} from "lodash-es";
import TypingWord from "@/components/Practice/PracticeWord/TypingWord.vue";
import ArticlePanel from "./ArticlePanel.vue";
@@ -10,8 +10,12 @@ import {renewSectionTexts, renewSectionTranslates} from "@/hooks/translate.ts";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {useBaseStore} from "@/stores/base.ts";
import EditSingleArticleModal from "@/components/Article/EditSingleArticleModal.vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
const store = useBaseStore()
const practiceStore = usePracticeStore()
let tabIndex = $ref(0)
let wordData = $ref({
words: [],
@@ -42,26 +46,49 @@ onMounted(() => {
getCurrentPractice()
})
function setArticle(val: Article) {
store.currentDict.articles[store.currentDict.chapterIndex] = cloneDeep(val)
articleData.article = cloneDeep(val)
practiceStore.inputWordNumber = 0
practiceStore.wrongWordNumber = 0
practiceStore.repeatNumber = 0
practiceStore.total = 0
practiceStore.wrongWords = []
practiceStore.startDate = Date.now()
articleData.article.sections.map((v, i) => {
v.map((w, j) => {
w.words.map(s => {
if (!store.skipWordNamesWithSimpleWords.includes(s.name.toLowerCase()) && !s.isSymbol) {
practiceStore.total++
}
})
})
})
}
function getCurrentPractice() {
// console.log('store.currentDict',store.currentDict)
// return
if (!store.currentDict.articles.length) return
tabIndex = 0
articleData.article = cloneDeep(DefaultArticle)
let currentArticle = store.currentDict.articles[store.currentDict.chapterIndex]
let tempArticle = {...DefaultArticle, ...currentArticle}
console.log('article', tempArticle)
if (tempArticle.sections.length) {
articleData.article = tempArticle
setArticle(tempArticle)
} else {
if (tempArticle.useTranslateType === TranslateType.none) {
renewSectionTexts(tempArticle)
articleData.article = tempArticle
setArticle(tempArticle)
} else {
if (tempArticle.useTranslateType === TranslateType.custom) {
if (tempArticle.textCustomTranslate.trim()) {
if (tempArticle.textCustomTranslateIsFormat) {
renewSectionTexts(tempArticle)
renewSectionTranslates(tempArticle, tempArticle.textCustomTranslate)
articleData.article = tempArticle
setArticle(tempArticle)
} else {
//说明有本地翻译,但是没格式化成一行一行的
MessageBox.confirm('检测到存在本地翻译,但未格式化,是否进行编辑?',
@@ -73,7 +100,7 @@ function getCurrentPractice() {
() => {
renewSectionTexts(tempArticle)
tempArticle.useTranslateType = TranslateType.none
store.currentDict.articles[store.currentDict.chapterIndex] = articleData.article = tempArticle
setArticle(tempArticle)
},
{
confirmButtonText: '去编辑',
@@ -92,7 +119,7 @@ function getCurrentPractice() {
() => {
renewSectionTexts(tempArticle)
tempArticle.useTranslateType = TranslateType.none
store.currentDict.articles[store.currentDict.chapterIndex] = articleData.article = tempArticle
setArticle(tempArticle)
},
{
confirmButtonText: '去编辑',
@@ -104,7 +131,7 @@ function getCurrentPractice() {
if (tempArticle.useTranslateType === TranslateType.network) {
renewSectionTexts(tempArticle)
renewSectionTranslates(tempArticle, tempArticle.textNetworkTranslate)
store.currentDict.articles[store.currentDict.chapterIndex] = articleData.article = tempArticle
setArticle(tempArticle)
}
}
}
@@ -114,14 +141,67 @@ function saveArticle(val: Article) {
console.log('saveArticle', val)
showEditArticle = false
// articleData.article = cloneDeep(store.currentDict.articles[store.currentDict.chapterIndex])
store.currentDict.articles[store.currentDict.chapterIndex] = articleData.article = val
setArticle(val)
}
function edit(val: Article) {
tabIndex = 1
wordData.words = [
{
...cloneDeep(DefaultWord),
name: 'test'
}
]
wordData.index = 0
return
editArticle = val
showEditArticle = true
}
function wrong(word: Word) {
let lowerName = word.name.toLowerCase();
if (!store.wrong.originWords.find((v: Word) => v.name.toLowerCase() === lowerName)) {
store.wrong.originWords.push(word)
store.wrong.words.push(word)
store.wrong.chapterWords = [store.wrong.words]
}
if (!store.skipWordNamesWithSimpleWords.includes(lowerName)) {
if (!practiceStore.wrongWords.find((v) => v.name.toLowerCase() === lowerName)) {
practiceStore.wrongWords.push(word)
practiceStore.wrongWordNumber++
}
}
}
function over() {
if (practiceStore.wrongWordNumber === 0) {
// if (false) {
console.log('这章节完了')
let now = Date.now()
let stat: DisplayStatistics = {
startDate: practiceStore.startDate,
endDate: now,
spend: now - practiceStore.startDate,
total: practiceStore.total,
correctRate: -1,
wrongWordNumber: practiceStore.wrongWordNumber,
wrongWords: practiceStore.wrongWords,
}
stat.correctRate = 100 - Math.trunc(((stat.wrongWordNumber) / (stat.total)) * 100)
emitter.emit(EventKey.openStatModal, stat)
} else {
tabIndex = 1
wordData.words = practiceStore.wrongWords
wordData.index = 0
}
}
function nextWord(word: ArticleWord) {
if (!store.skipWordNamesWithSimpleWords.includes(word.name.toLowerCase()) && !word.isSymbol) {
practiceStore.inputWordNumber++
}
}
</script>
<template>
@@ -130,6 +210,11 @@ function edit(val: Article) {
<div class="swiper-list" :class="`step${tabIndex}`">
<div class="swiper-item">
<TypingArticle
:active="tabIndex === 0"
@edit="edit"
@wrong="wrong"
@over="over"
@nextWord="nextWord"
:article="articleData.article"
/>
</div>
@@ -144,7 +229,12 @@ function edit(val: Article) {
</div>
<div class="panel-wrapper">
<ArticlePanel :list="[]" v-model:index="index"/>
<ArticlePanel
v-if="tabIndex === 0"
:list="[]"
v-model:index="index">
1234
</ArticlePanel>
</div>
<EditSingleArticleModal

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import {computed, nextTick, onMounted, watch} from "vue"
import {computed, nextTick, onMounted, onUnmounted, watch} from "vue"
import {$computed, $ref} from "vue/macros";
import {Article, ArticleWord, DefaultArticle, DisplayStatistics, ShortKeyMap, Word} from "@/types";
import {Article, ArticleWord, DefaultArticle, ShortKeyMap, Word} from "@/types";
import {useBaseStore} from "@/stores/base";
import {usePracticeStore} from "@/stores/practice.ts";
import {useSettingStore} from "@/stores/setting.ts";
@@ -9,9 +9,6 @@ import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} fro
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
import {cloneDeep} from "lodash-es";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import Tooltip from "@/components/Tooltip.vue";
import IconWrapper from "@/components/IconWrapper.vue";
import {Icon} from "@iconify/vue";
import Options from "@/components/Practice/Options.vue";
interface IProps {
@@ -20,6 +17,7 @@ interface IProps {
sentenceIndex?: number,
wordIndex?: number,
stringIndex?: number,
active: boolean,
}
const props = withDefaults(defineProps<IProps>(), {
@@ -28,17 +26,19 @@ const props = withDefaults(defineProps<IProps>(), {
sentenceIndex: 0,
wordIndex: 0,
stringIndex: 0,
active: true,
})
const emit = defineEmits<{
ignore: [],
next: [],
wrong: [val: Word],
nextWord: [val: ArticleWord],
over: [],
edit: [val: Article]
}>()
let isPlay = $ref(false)
let articleWrapperRef = $ref<HTMLInputElement>(null)
let tabIndex = $ref(0)
let sectionIndex = $ref(0)
let sentenceIndex = $ref(0)
let wordIndex = $ref(0)
@@ -50,10 +50,6 @@ let hoverIndex = $ref({
sectionIndex: -1,
sentenceIndex: -1,
})
let wordData = $ref({
words: [],
index: -1
})
const currentIndex = computed(() => {
return `${sectionIndex}${sentenceIndex}${wordIndex}`
})
@@ -75,27 +71,9 @@ watch(() => props.article, () => {
sentenceIndex = props.sentenceIndex
wordIndex = props.wordIndex
stringIndex = props.stringIndex
tabIndex = 0
practiceStore.inputWordNumber = 0
practiceStore.wrongWordNumber = 0
practiceStore.repeatNumber = 0
practiceStore.total = 0
props.article.sections.map((v, i) => {
v.map((w, j) => {
w.words.map(s => {
if (!store.skipWordNamesWithSimpleWords.includes(s.name.toLowerCase()) && !s.isSymbol) {
practiceStore.total++
}
})
})
})
practiceStore.wrongWords = []
practiceStore.startDate = Date.now()
calcTranslateLocation()
}, {immediate: true})
watch(() => settingStore.dictation, () => {
calcTranslateLocation()
})
@@ -104,8 +82,143 @@ onMounted(() => {
emitter.on(EventKey.resetWord, () => {
wrong = input = ''
})
emitter.on(EventKey.onTyping, onTyping)
})
onUnmounted(() => {
emitter.off(EventKey.resetWord,)
emitter.off(EventKey.onTyping, onTyping)
})
function nextSentence() {
// wordData.words = [
// {"name": "pharmacy", "trans": ["药房;配药学,药剂学;制药业;一批备用药品"], "usphone": "'fɑrməsi", "ukphone": "'fɑːməsɪ"},
// // {"name": "foregone", "trans": ["过去的;先前的;预知的;预先决定的", "发生在…之前forego的过去分词"], "usphone": "'fɔrɡɔn", "ukphone": "fɔː'gɒn"}, {"name": "president", "trans": ["总统;董事长;校长;主席"], "usphone": "'prɛzɪdənt", "ukphone": "'prezɪd(ə)nt"}, {"name": "plastic", "trans": ["塑料的;(外科)造型的;可塑的", "塑料制品;整形;可塑体"], "usphone": "'plæstɪk", "ukphone": "'plæstɪk"}, {"name": "provisionally", "trans": ["临时地,暂时地"], "usphone": "", "ukphone": ""}, {"name": "incentive", "trans": ["动机;刺激", "激励的;刺激的"], "usphone": "ɪn'sɛntɪv", "ukphone": "ɪn'sentɪv"}, {"name": "calculate", "trans": ["计算;以为;作打算"], "usphone": "'kælkjulet", "ukphone": "'kælkjʊleɪt"}
// ]
// return
let currentSection = props.article.sections[sectionIndex]
isSpace = false
stringIndex = 0
wordIndex = 0
input = wrong = ''
//todo 计得把略过的单词加上统计里面去
// if (!store.skipWordNamesWithSimpleWords.includes(currentWord.name.toLowerCase()) && !currentWord.isSymbol) {
// practiceStore.inputNumber++
// }
sentenceIndex++
if (!currentSection[sentenceIndex]) {
sentenceIndex = 0
sectionIndex++
if (!props.article.sections[sectionIndex]) {
console.log('打完了')
emit('over')
}
} else {
if (settingStore.dictation) {
calcTranslateLocation()
}
playWordAudio(currentSection[sentenceIndex].text)
}
}
function onTyping(e: KeyboardEvent) {
if (!props.active) return
if (!props.article.sections.length) return
// console.log('keyDown', e.key, e.code, e.keyCode)
wrong = ''
let currentSection = props.article.sections[sectionIndex]
let currentSentence = currentSection[sentenceIndex]
let currentWord: ArticleWord = currentSentence.words[wordIndex]
const nextWord = () => {
isSpace = false
stringIndex = 0
wordIndex++
emit('nextWord', currentWord)
if (!currentSentence.words[wordIndex]) {
wordIndex = 0
sentenceIndex++
if (!currentSection[sentenceIndex]) {
sentenceIndex = 0
sectionIndex++
if (!props.article.sections[sectionIndex]) {
console.log('打完了')
}
} else {
if (settingStore.dictation) {
calcTranslateLocation()
}
playWordAudio(currentSection[sentenceIndex].text)
}
}
}
if (isSpace) {
if (e.code === 'Space') {
nextWord()
} else {
wrong = ' '
playBeep()
setTimeout(() => {
wrong = ''
wrong = input = ''
}, 500)
}
playKeyboardAudio()
} else {
let letter = e.key
let key = currentWord.name[stringIndex]
// console.log('key', key,)
let isRight = false
if (settingStore.ignoreCase) {
isRight = key.toLowerCase() === letter.toLowerCase()
} else {
isRight = key === letter
}
if (isRight) {
input += letter
wrong = ''
// console.log('匹配上了')
stringIndex++
//如果当前词没有index说明这个词完了下一个是空格
if (!currentWord.name[stringIndex]) {
input = wrong = ''
if (!currentWord.isSymbol) {
playCorrect()
}
if (currentWord.nextSpace) {
isSpace = true
} else {
nextWord()
}
}
} else {
emit('wrong', currentWord)
wrong = letter
playBeep()
setTimeout(() => {
wrong = ''
}, 500)
// console.log('未匹配')
}
playKeyboardAudio()
}
e.preventDefault()
}
function calcTranslateLocation() {
nextTick(() => {
setTimeout(() => {
@@ -148,201 +261,31 @@ function play() {
}
function onKeyDown(e: KeyboardEvent) {
if (tabIndex !== 0) return
if (!props.article.sections.length) return
// console.log('keyDown', e.key, e.code, e.keyCode)
wrong = ''
let currentSection = props.article.sections[sectionIndex]
let currentSentence = currentSection[sentenceIndex]
let currentWord: ArticleWord = currentSentence.words[wordIndex]
const nextWord = () => {
isSpace = false
stringIndex = 0
wordIndex++
if (!store.skipWordNamesWithSimpleWords.includes(currentWord.name.toLowerCase()) && !currentWord.isSymbol) {
practiceStore.inputWordNumber++
}
if (!currentSentence.words[wordIndex]) {
wordIndex = 0
sentenceIndex++
if (!currentSection[sentenceIndex]) {
sentenceIndex = 0
sectionIndex++
if (!props.article.sections[sectionIndex]) {
console.log('打完了')
}
} else {
if (settingStore.dictation) {
calcTranslateLocation()
}
playWordAudio(currentSection[sentenceIndex].text)
}
}
}
const nextSentence = () => {
// tabIndex = 1
// // wordData.words = practiceStore.wrongWords
// wordData.words = [
// {"name": "pharmacy", "trans": ["药房;配药学,药剂学;制药业;一批备用药品"], "usphone": "'fɑrməsi", "ukphone": "'fɑːməsɪ"},
// // {"name": "foregone", "trans": ["过去的;先前的;预知的;预先决定的", "发生在…之前forego的过去分词"], "usphone": "'fɔrɡɔn", "ukphone": "fɔː'gɒn"}, {"name": "president", "trans": ["总统;董事长;校长;主席"], "usphone": "'prɛzɪdənt", "ukphone": "'prezɪd(ə)nt"}, {"name": "plastic", "trans": ["塑料的;(外科)造型的;可塑的", "塑料制品;整形;可塑体"], "usphone": "'plæstɪk", "ukphone": "'plæstɪk"}, {"name": "provisionally", "trans": ["临时地,暂时地"], "usphone": "", "ukphone": ""}, {"name": "incentive", "trans": ["动机;刺激", "激励的;刺激的"], "usphone": "ɪn'sɛntɪv", "ukphone": "ɪn'sentɪv"}, {"name": "calculate", "trans": ["计算;以为;作打算"], "usphone": "'kælkjulet", "ukphone": "'kælkjʊleɪt"}
// ]
// return
isSpace = false
stringIndex = 0
wordIndex = 0
input = wrong = ''
//todo 计得把略过的单词加上统计里面去
// if (!store.skipWordNamesWithSimpleWords.includes(currentWord.name.toLowerCase()) && !currentWord.isSymbol) {
// practiceStore.inputNumber++
// }
sentenceIndex++
if (!currentSection[sentenceIndex]) {
sentenceIndex = 0
sectionIndex++
if (!props.article.sections[sectionIndex]) {
console.log('打完了')
if (practiceStore.wrongWordNumber === 0) {
// if (false) {
console.log('这章节完了')
let now = Date.now()
let stat: DisplayStatistics = {
startDate: practiceStore.startDate,
endDate: now,
spend: now - practiceStore.startDate,
total: practiceStore.total,
correctRate: -1,
wrongWordNumber: practiceStore.wrongWordNumber,
wrongWords: practiceStore.wrongWords,
}
stat.correctRate = 100 - Math.trunc(((stat.wrongWordNumber) / (stat.total)) * 100)
emitter.emit(EventKey.openStatModal, stat)
} else {
tabIndex = 1
wordData.words = practiceStore.wrongWords
}
}
} else {
if (settingStore.dictation) {
calcTranslateLocation()
}
playWordAudio(currentSection[sentenceIndex].text)
}
}
//非英文模式下,输入区域的 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 (isSpace) {
if (e.code === 'Space') {
nextWord()
} else {
wrong = ' '
playBeep()
setTimeout(() => {
wrong = ''
wrong = input = ''
}, 500)
}
playKeyboardAudio()
} else {
let letter = e.key
let key = currentWord.name[stringIndex]
// console.log('key', key,)
let isWrong = false
if (settingStore.ignoreCase) {
isWrong = key.toLowerCase() !== letter.toLowerCase()
} else {
isWrong = key !== letter
}
if (!isWrong) {
input += letter
if (!props.active) return
switch (e.key) {
case 'Backspace':
if (wrong) {
wrong = ''
// console.log('匹配上了')
stringIndex++
//如果当前词没有index说明这个词完了下一个是空格
if (!currentWord.name[stringIndex]) {
input = wrong = ''
if (!currentWord.isSymbol) {
playCorrect()
}
if (currentWord.nextSpace) {
isSpace = true
} else {
nextWord()
}
}
} else {
if (!store.wrong.originWords.find((v: Word) => v.name.toLowerCase() === currentWord.name.toLowerCase())) {
store.wrong.originWords.push(currentWord)
store.wrong.words.push(currentWord)
store.wrong.chapterWords = [store.wrong.words]
}
if (!store.skipWordNamesWithSimpleWords.includes(currentWord.name.toLowerCase())) {
if (!practiceStore.wrongWords.find((v) => v.name.toLowerCase() === currentWord.name.toLowerCase())) {
practiceStore.wrongWords.push(currentWord)
practiceStore.wrongWordNumber++
}
}
wrong = letter
playBeep()
setTimeout(() => {
wrong = ''
}, 500)
// console.log('未匹配')
input = input.slice(0, -1)
}
playKeyboardAudio()
}
} else {
switch (e.key) {
case 'Backspace':
if (wrong) {
wrong = ''
} else {
input = input.slice(0, -1)
}
break
case ShortKeyMap.Collect:
break
case ShortKeyMap.Collect:
break
case ShortKeyMap.Remove:
break
case ShortKeyMap.Ignore:
nextSentence()
break
case ShortKeyMap.Show:
if (settingStore.allowWordTip) {
hoverIndex = {
sectionIndex: sectionIndex,
sentenceIndex: sentenceIndex,
}
break
case ShortKeyMap.Remove:
break
case ShortKeyMap.Ignore:
nextSentence()
break
case ShortKeyMap.Show:
if (settingStore.allowWordTip) {
hoverIndex = {
sectionIndex: sectionIndex,
sentenceIndex: sentenceIndex,
}
break
}
}
break
}
// console.log(

View File

@@ -13,7 +13,6 @@ let wordData = $ref({
words: [],
index: -1
})
let index = $ref(0)
watch([
() => store.load,
@@ -38,15 +37,14 @@ onMounted(() => {
</script>
<template>
<div class="practice-word">
<TypingWord/>
<div class="panel-wrapper">
<WordPanel :list="[]" v-model:index="index"/>
<!-- <WordPanel :list="data.words" v-model:index="data.index"/>-->
</div>
<div class="practice">
<TypingWord :words="wordData.words" :index="wordData.index"/>
</div>
</template>
<style scoped lang="scss">
.practice {
//height: 100%;
flex: 1;
}
</style>

View File

@@ -6,10 +6,9 @@ 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";
import {onUnmounted, watch, onMounted} from "vue";
interface IProps {
word: Word,
@@ -30,8 +29,6 @@ let showFullWord = $ref(false)
//输入锁定因为跳转到下一个单词有延时如果重复在延时期间内重复输入导致会跳转N次
let inputLock = false
let wordRepeatCount = 0
const store = useBaseStore()
const practiceStore = usePracticeStore()
const settingStore = useSettingStore()
const playBeep = usePlayBeep()
@@ -52,6 +49,7 @@ watch(() => props.word, () => {
volumeIconRef?.play()
}
})
onMounted(() => {
emitter.on(EventKey.resetWord, () => {
wrong = input = ''
@@ -65,7 +63,6 @@ onUnmounted(() => {
emitter.off(EventKey.onTyping, onTyping)
})
function repeat() {
setTimeout(() => {
wrong = input = ''
@@ -78,7 +75,6 @@ function repeat() {
}, settingStore.waitTimeForChangeWord)
}
async function onTyping(e: KeyboardEvent) {
if (inputLock) return
inputLock = true
@@ -86,11 +82,11 @@ async function onTyping(e: KeyboardEvent) {
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()
isTypingRight = letter.toLowerCase() === props.word.name[input.length].toLowerCase()
isWordRight = (input + letter).toLowerCase() === props.word.name.toLowerCase()
} else {
isTypingRight = letter === props.word.name[input.length + 1]
isWordRight = letter === props.word.name.slice(-1)
isTypingRight = letter === props.word.name[input.length]
isWordRight = (input + letter) === props.word.name
}
if (isTypingRight) {
input += letter
@@ -126,6 +122,27 @@ async function onTyping(e: KeyboardEvent) {
}
}
function del() {
playKeyboardAudio()
if (wrong) {
wrong = ''
} else {
input = input.slice(0, -1)
}
}
function showWord() {
if (settingStore.allowWordTip) {
showFullWord = true
}
}
function hideWord() {
showFullWord = false
}
defineExpose({del, showWord, hideWord})
</script>
<template>
@@ -154,7 +171,7 @@ async function onTyping(e: KeyboardEvent) {
</template>
<span class="letter" v-else>{{ displayWord }}</span>
</div>
<VolumeIcon ref="volumeIconRef" :simple="true" :cb="playWordAudio(word.name)"/>
<VolumeIcon ref="volumeIconRef" :simple="true" :cb="()=>playWordAudio(word.name)"/>
</div>
<div class="phonetic">{{ settingStore.wordSoundType === 'us' ? word.usphone : word.ukphone }}</div>
</div>
@@ -164,6 +181,11 @@ async function onTyping(e: KeyboardEvent) {
@import "@/assets/css/variable";
.typing-word {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
.phonetic, .translate {
font-size: 20rem;
margin-left: -30rem;

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import {onMounted, watch} from "vue"
import {watch} from "vue"
import {$computed, $ref} from "vue/macros"
import {useBaseStore} from "@/stores/base.ts"
import {DictType, DisplayStatistics, ShortKeyMap, Word} from "../../../types";
@@ -7,13 +7,12 @@ 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";
import Typing from "@/components/Practice/PracticeWord/Typing.vue";
import WordPanel from "@/components/Practice/PracticeWord/WordPanel.vue";
interface IProps {
words: Word[],
@@ -32,8 +31,7 @@ let data = $ref({
originWrongWords: [],
})
let input = $ref('')
let wrong = $ref('')
let typingRef: any = $ref()
const store = useBaseStore()
const practiceStore = usePracticeStore()
const settingStore = useSettingStore()
@@ -70,10 +68,6 @@ const nextWord: Word = $computed(() => {
return data.words?.[data.index + 1] ?? undefined
})
onMounted(() => {
})
function next(isTyping: boolean = true) {
if (data.index === data.words.length - 1) {
if (data.wrongWords.length) {
@@ -83,9 +77,8 @@ function next(isTyping: boolean = true) {
if (!data.originWrongWords.length) {
data.originWrongWords = cloneDeep(data.wrongWords)
}
data.index = 0
practiceStore.total = data.words.length
practiceStore.index = 0
practiceStore.index = data.index = 0
practiceStore.inputWordNumber = 0
practiceStore.wrongWordNumber = 0
practiceStore.repeatNumber++
@@ -143,7 +136,7 @@ function remove() {
}
function onKeyUp(e: KeyboardEvent) {
// showFullWord = false
typingRef.hideWord()
}
function wordWrong() {
@@ -162,11 +155,7 @@ async function onKeyDown(e: KeyboardEvent) {
// console.log('e', e)
switch (e.key) {
case 'Backspace':
if (wrong) {
wrong = ''
} else {
input = input.slice(0, -1)
}
typingRef.del()
break
case ShortKeyMap.Collect:
collect()
@@ -179,9 +168,7 @@ async function onKeyDown(e: KeyboardEvent) {
e.preventDefault()
break
case ShortKeyMap.Show:
if (settingStore.allowWordTip) {
// showFullWord = true
}
typingRef.showWord()
break
}
}
@@ -191,7 +178,7 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
</script>
<template>
<div class="type-word">
<div class="practice-word">
<div class="near-word" v-if="settingStore.showNearWord">
<div class="prev"
@click="prev"
@@ -209,6 +196,7 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
</Tooltip>
</div>
<Typing
ref="typingRef"
:word="word"
@wrong="wordWrong"
@next="next"
@@ -218,13 +206,15 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
@skip="skip"
@collect="collect"
/>
<WordPanel :list="data.words" v-model:index="data.index"/>
</div>
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.type-word {
.practice-word {
height: 100%;
flex: 1;
display: flex;
//display: none;
@@ -280,5 +270,6 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
}
}
}
</style>

View File

@@ -1,431 +0,0 @@
<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

@@ -65,7 +65,7 @@ async function selectDict(item: DictResource) {
}
} else {
let data: Dict = {
...DefaultDict,
...cloneDeep(DefaultDict),
...item,
}

View File

@@ -17,7 +17,7 @@ const emit = defineEmits(['click'])
function play(time = props.time) {
if (count === 0) {
props?.cb()
props?.cb?.()
}
count++
setTimeout(() => {

View File

@@ -24,26 +24,26 @@ export const useBaseStore = defineStore('base', {
state: (): State => {
return {
collect: {
...DefaultDict,
...cloneDeep(DefaultDict),
id: 'collect',
name: '收藏',
type: DictType.collect,
},
skip: {
...DefaultDict,
...cloneDeep(DefaultDict),
id: 'skip',
name: '简单词',
type: DictType.skip,
},
wrong: {
...DefaultDict,
...cloneDeep(DefaultDict),
id: 'wrong',
name: '错词本',
type: DictType.wrong,
},
myDicts: [
{
...DefaultDict,
...cloneDeep(DefaultDict),
id: '新概念英语2-课文',
name: '新概念英语2-课文',
type: DictType.article,
@@ -52,7 +52,7 @@ export const useBaseStore = defineStore('base', {
language: 'en',
},
{
...DefaultDict,
...cloneDeep(DefaultDict),
id: '新概念英语2',
name: '新概念英语2',
type: DictType.word,
@@ -63,9 +63,10 @@ export const useBaseStore = defineStore('base', {
}
],
current: {
dictType: DictType.word,
// dictType: DictType.article,
index: 1,
// dictType: DictType.word,
// index: 1,
dictType: DictType.article,
index: 0,
editIndex: 0,
repeatNumber: 0,
},

View File

@@ -6,8 +6,8 @@ export interface PracticeState {
repeatNumber: number,
startDate: number,
total: number,
index: number,
inputWordNumber: number,
index: number,//当前输入的第几个用于和total计算进度
inputWordNumber: number,//当前总输入了多少个单词(不包含跳过)
wrongWordNumber: number,
correctRate: number,
}