fix:optimize the input method for article practice
This commit is contained in:
8
components.d.ts
vendored
8
components.d.ts
vendored
@@ -13,7 +13,6 @@ declare module 'vue' {
|
||||
Close: typeof import('./src/components/icon/Close.vue')['default']
|
||||
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
|
||||
Empty: typeof import('./src/components/Empty.vue')['default']
|
||||
IconArcticonsXiaohongshuRednote: typeof import('~icons/arcticons/xiaohongshu-rednote')['default']
|
||||
IconBxVolume: typeof import('~icons/bx/volume')['default']
|
||||
IconBxVolumeFull: typeof import('~icons/bx/volume-full')['default']
|
||||
IconBxVolumeLow: typeof import('~icons/bx/volume-low')['default']
|
||||
@@ -30,7 +29,6 @@ declare module 'vue' {
|
||||
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
|
||||
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
|
||||
IconFluentArrowSort20Regular: typeof import('~icons/fluent/arrow-sort20-regular')['default']
|
||||
IconFluentArrowSync16Regular: typeof import('~icons/fluent/arrow-sync16-regular')['default']
|
||||
IconFluentBookLetter20Regular: typeof import('~icons/fluent/book-letter20-regular')['default']
|
||||
IconFluentCheckmark20Regular: typeof import('~icons/fluent/checkmark20-regular')['default']
|
||||
IconFluentCheckmarkCircle16Filled: typeof import('~icons/fluent/checkmark-circle16-filled')['default']
|
||||
@@ -56,7 +54,6 @@ declare module 'vue' {
|
||||
IconFluentPlay20Regular: typeof import('~icons/fluent/play20-regular')['default']
|
||||
IconFluentQuestionCircle20Regular: typeof import('~icons/fluent/question-circle20-regular')['default']
|
||||
IconFluentReplay20Regular: typeof import('~icons/fluent/replay20-regular')['default']
|
||||
IconFluentScanType20Regular: typeof import('~icons/fluent/scan-type20-regular')['default']
|
||||
IconFluentSearch20Regular: typeof import('~icons/fluent/search20-regular')['default']
|
||||
IconFluentSearch24Regular: typeof import('~icons/fluent/search24-regular')['default']
|
||||
IconFluentSettings20Regular: typeof import('~icons/fluent/settings20-regular')['default']
|
||||
@@ -68,22 +65,17 @@ declare module 'vue' {
|
||||
IconFluentStar20Filled: typeof import('~icons/fluent/star20-filled')['default']
|
||||
IconFluentStarAdd16Regular: typeof import('~icons/fluent/star-add16-regular')['default']
|
||||
IconFluentTextEditStyle20Regular: typeof import('~icons/fluent/text-edit-style20-regular')['default']
|
||||
IconFluentTextField20Regular: typeof import('~icons/fluent/text-field20-regular')['default']
|
||||
IconFluentTextListAbcUppercaseLtr20Regular: typeof import('~icons/fluent/text-list-abc-uppercase-ltr20-regular')['default']
|
||||
IconFluentTextUnderlineDouble20Regular: typeof import('~icons/fluent/text-underline-double20-regular')['default']
|
||||
IconFluentTextWholeWord20Regular: typeof import('~icons/fluent/text-whole-word20-regular')['default']
|
||||
IconFluentTranslate16Regular: typeof import('~icons/fluent/translate16-regular')['default']
|
||||
IconFluentTranslateOff16Regular: typeof import('~icons/fluent/translate-off16-regular')['default']
|
||||
IconFluentWeatherMoon16Regular: typeof import('~icons/fluent/weather-moon16-regular')['default']
|
||||
IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default']
|
||||
IconIconXiaoHongShu: typeof import('~icons/ic/on-xiao-hong-shu')['default']
|
||||
IconMaterialSymbolsMail: typeof import('~icons/material-symbols/mail')['default']
|
||||
IconMdiGithub: typeof import('~icons/mdi/github')['default']
|
||||
IconRiTwitterFill: typeof import('~icons/ri/twitter-fill')['default']
|
||||
IconSimpleIconsGithub: typeof import('~icons/simple-icons/github')['default']
|
||||
IconSimpleIconsWechat: typeof import('~icons/simple-icons/wechat')['default']
|
||||
IconSimpleIconsXiaohongshu: typeof import('~icons/simple-icons/xiaohongshu')['default']
|
||||
IconStreamlineUltimateColorWechatLogo: typeof import('~icons/streamline-ultimate-color/wechat-logo')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SlideHorizontal: typeof import('./src/components/slide/SlideHorizontal.vue')['default']
|
||||
|
||||
@@ -49,6 +49,28 @@
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes shakeBottom {
|
||||
10%,
|
||||
90% {
|
||||
transform: translate3d(-1px, 0.3rem, 0);
|
||||
}
|
||||
|
||||
20%,
|
||||
80% {
|
||||
transform: translate3d(2px, 0.3rem, 0);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translate3d(-4px, 0.3rem, 0);
|
||||
}
|
||||
|
||||
40%,
|
||||
60% {
|
||||
transform: translate3d(4px, 0.3rem, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.go-enter-from {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
|
||||
@@ -40,6 +40,7 @@ let articleData = $ref({
|
||||
let showEditArticle = $ref(false)
|
||||
let typingArticleRef = $ref<any>()
|
||||
let loading = $ref<boolean>(false)
|
||||
let allWrongWords = new Set()
|
||||
let editArticle = $ref<Article>(getDefaultArticle())
|
||||
|
||||
function write() {
|
||||
@@ -128,7 +129,6 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
useStartKeyboardEventListener()
|
||||
useDisableEventListener(() => loading)
|
||||
|
||||
@@ -138,6 +138,7 @@ function setArticle(val: Article) {
|
||||
statisticsStore.total = 0
|
||||
statisticsStore.startDate = Date.now()
|
||||
|
||||
allWrongWords = new Set()
|
||||
articleData.list[store.sbook.lastLearnIndex] = val
|
||||
articleData.article = val
|
||||
articleData.sectionIndex = 0
|
||||
@@ -186,12 +187,15 @@ function edit(val: Article = articleData.article) {
|
||||
}
|
||||
|
||||
function wrong(word: Word) {
|
||||
let lowerName = word.word.toLowerCase();
|
||||
if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === lowerName)) {
|
||||
store.wrong.words.push(word)
|
||||
let temp = word.word.toLowerCase();
|
||||
if (!allWrongWords.has(word.word.toLowerCase())) {
|
||||
allWrongWords.add(word.word.toLowerCase())
|
||||
statisticsStore.wrong++
|
||||
}
|
||||
if (!store.allIgnoreWords.includes(lowerName)) {
|
||||
//todo
|
||||
|
||||
if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === temp)) {
|
||||
store.wrong.words.push(word)
|
||||
store.wrong.length = store.wrong.words.length
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
101
src/pages/pc/article/components/Space.vue
Normal file
101
src/pages/pc/article/components/Space.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
isWrong: boolean,
|
||||
isWait?: boolean,
|
||||
isShake?: boolean,
|
||||
}>(), {
|
||||
isWrong: false,
|
||||
isShake: false,
|
||||
})
|
||||
const settingStore = useSettingStore()
|
||||
const isMoveBottom = $computed(() => {
|
||||
return settingStore.dictation && !props.isWrong
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="word-space"
|
||||
v-if="isWrong"
|
||||
:class="[
|
||||
isWrong && 'wrong',
|
||||
isWait && 'wait',
|
||||
isShake ? isMoveBottom ? 'shakeBottom' : 'shake' : '',
|
||||
isMoveBottom && 'to-bottom'
|
||||
]"
|
||||
v-bind="$attrs"
|
||||
></span>
|
||||
<span v-bind="$attrs" v-else>
|
||||
<span class="word-space mx-0.5!"
|
||||
:class="[
|
||||
isWrong && 'wrong',
|
||||
isWait && 'wait',
|
||||
isShake ? isMoveBottom ? 'shakeBottom' : 'shake' : '',
|
||||
isMoveBottom && 'to-bottom'
|
||||
]"
|
||||
></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.word-space {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 0.8rem;
|
||||
height: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
margin: 0 1px;
|
||||
|
||||
&.wrong {
|
||||
border-bottom: 2px solid red;
|
||||
}
|
||||
|
||||
&.to-bottom {
|
||||
transform: translateY(0.3rem);
|
||||
}
|
||||
|
||||
&.wait {
|
||||
border-bottom: 2px solid var(--color-article);
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: .25rem;
|
||||
background: var(--color-article);
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: .26rem;
|
||||
background: var(--color-article);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shake {
|
||||
border-bottom: 2px solid red !important;
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
|
||||
&::after {
|
||||
background: red !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: red !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shakeBottom {
|
||||
@extend .shake;
|
||||
animation: shakeBottom 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, onUnmounted, watch} from "vue"
|
||||
import {onMounted, onUnmounted, watch} from "vue"
|
||||
import {Article, ArticleWord, Sentence, Word} from "@/types/types.ts";
|
||||
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 {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
@@ -14,6 +13,8 @@ import BaseButton from "@/components/BaseButton.vue";
|
||||
import QuestionForm from "@/pages/pc/article/components/QuestionForm.vue";
|
||||
import {getDefaultArticle} from "@/types/func.ts";
|
||||
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
|
||||
import TypingWord from "@/pages/pc/article/components/TypingWord.vue";
|
||||
import Space from "@/pages/pc/article/components/Space.vue";
|
||||
|
||||
interface IProps {
|
||||
article: Article,
|
||||
@@ -61,7 +62,7 @@ let cursor = $ref({
|
||||
})
|
||||
let isEnd = $ref(false)
|
||||
|
||||
const currentIndex = computed(() => {
|
||||
const currentIndex = $computed(() => {
|
||||
return `${sectionIndex}${sentenceIndex}${wordIndex}`
|
||||
})
|
||||
|
||||
@@ -71,7 +72,6 @@ const playKeyboardAudio = usePlayKeyboardAudio()
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
|
||||
const store = useBaseStore()
|
||||
const statisticsStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c,]) => {
|
||||
@@ -145,6 +145,7 @@ let lockNextSentence = false
|
||||
|
||||
function nextSentence() {
|
||||
if (lockNextSentence) return
|
||||
checkTranslateLocation()
|
||||
lockNextSentence = true
|
||||
// wordData.words = [
|
||||
// {"word": "pharmacy", "trans": ["药房;配药学,药剂学;制药业;一批备用药品"], "phonetic0": "'fɑrməsi", "phonetic1": "'fɑːməsɪ"},
|
||||
@@ -153,6 +154,11 @@ function nextSentence() {
|
||||
// return
|
||||
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
let currentSentence = currentSection[sentenceIndex]
|
||||
//这里把未输入的单词补全,因为删除时会用到input
|
||||
currentSentence.words.forEach((word, i) => {
|
||||
word.input = word.input + word.word.slice(word.input?.length ?? 0)
|
||||
})
|
||||
|
||||
isSpace = false
|
||||
stringIndex = 0
|
||||
@@ -164,6 +170,7 @@ function nextSentence() {
|
||||
// statisticsStore.inputNumber++
|
||||
// }
|
||||
|
||||
|
||||
sentenceIndex++
|
||||
if (!currentSection[sentenceIndex]) {
|
||||
sentenceIndex = 0
|
||||
@@ -222,6 +229,7 @@ function onTyping(e: KeyboardEvent) {
|
||||
let letter = e.key
|
||||
|
||||
let key = currentWord.word[stringIndex]
|
||||
|
||||
// console.log('key', key,)
|
||||
|
||||
let isRight = false
|
||||
@@ -230,32 +238,33 @@ function onTyping(e: KeyboardEvent) {
|
||||
} else {
|
||||
isRight = key === letter
|
||||
}
|
||||
if (isRight) {
|
||||
//这里使用原文的字母,不使用用户输入的,因为原文是大写时,用户输入的小写,会导致布局重绘
|
||||
input += key
|
||||
wrong = ''
|
||||
// console.log('匹配上了')
|
||||
stringIndex++
|
||||
//如果当前词没有index,说明这个词完了,下一个是空格
|
||||
if (!currentWord.word[stringIndex]) {
|
||||
input = wrong = ''
|
||||
if (!currentWord.isSymbol) {
|
||||
playCorrect()
|
||||
}
|
||||
if (currentWord.nextSpace) {
|
||||
isSpace = true
|
||||
} else {
|
||||
nextWord()
|
||||
}
|
||||
if (!isRight) {
|
||||
if (!currentWord.isSymbol){
|
||||
emit('wrong', currentWord)
|
||||
}
|
||||
} else {
|
||||
// emit('wrong', currentWord)
|
||||
wrong = letter
|
||||
playBeep()
|
||||
setTimeout(() => {
|
||||
wrong = ''
|
||||
}, 500)
|
||||
// console.log('未匹配')
|
||||
}
|
||||
|
||||
input += letter
|
||||
|
||||
if (!currentWord.input) currentWord.input = ''
|
||||
currentWord.input = input
|
||||
console.log(currentWord.input)
|
||||
|
||||
wrong = ''
|
||||
// console.log('匹配上了')
|
||||
stringIndex++
|
||||
//如果当前词没有index,说明这个词完了,下一个是空格
|
||||
if (!currentWord.word[stringIndex]) {
|
||||
input = ''
|
||||
if (!currentWord.isSymbol) {
|
||||
playCorrect()
|
||||
}
|
||||
if (currentWord.nextSpace) {
|
||||
isSpace = true
|
||||
} else {
|
||||
nextWord()
|
||||
}
|
||||
}
|
||||
playKeyboardAudio()
|
||||
}
|
||||
@@ -302,26 +311,19 @@ function del() {
|
||||
if (endWord) wordIndex = currentSentence.words.length - 1
|
||||
let currentWord: ArticleWord = currentSentence.words[wordIndex]
|
||||
if (endString) {
|
||||
checkTranslateLocation()
|
||||
if (currentWord.nextSpace) {
|
||||
isSpace = true
|
||||
stringIndex = currentWord.word.length
|
||||
}else {
|
||||
} else {
|
||||
stringIndex = currentWord.word.length - 1
|
||||
}
|
||||
}
|
||||
input = currentWord.word.slice(0, stringIndex)
|
||||
input = currentWord.input = currentWord.input.slice(0, stringIndex)
|
||||
}
|
||||
checkCursorPosition()
|
||||
}
|
||||
|
||||
function indexWord(word: ArticleWord) {
|
||||
return word.word.slice(input.length, input.length + 1)
|
||||
}
|
||||
|
||||
function remainderWord(word: ArticleWord,) {
|
||||
return word.word.slice(input.length + 1)
|
||||
}
|
||||
|
||||
function showSentence(i1: number = sectionIndex, i2: number = sentenceIndex) {
|
||||
hoverIndex = {sectionIndex: i1, sentenceIndex: i2}
|
||||
}
|
||||
@@ -343,6 +345,8 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j) {
|
||||
onClick: () => {
|
||||
sectionIndex = i
|
||||
sentenceIndex = j
|
||||
wordIndex = 0
|
||||
stringIndex = 0
|
||||
emit('play', sentence)
|
||||
}
|
||||
},
|
||||
@@ -389,6 +393,10 @@ onUnmounted(() => {
|
||||
|
||||
defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
|
||||
function isCurrent(i, j, w) {
|
||||
return `${i}${j}${w}` === currentIndex
|
||||
}
|
||||
|
||||
let showQuestions = $ref(false)
|
||||
</script>
|
||||
|
||||
@@ -426,36 +434,22 @@ let showQuestions = $ref(false)
|
||||
(sectionIndex>=indexI &&sentenceIndex>=indexJ && wordIndex>=indexW && stringIndex>=word.word.length)
|
||||
?'wrote':
|
||||
''),
|
||||
(`${indexI}${indexJ}${indexW}` === currentIndex && !isSpace && wrong )?'word-wrong':'',
|
||||
indexW === 0 && `word${indexI}-${indexJ}`
|
||||
]">
|
||||
<span class="word-wrap" v-if="`${indexI}${indexJ}${indexW}` === currentIndex && !isSpace">
|
||||
<span class="word-start" v-if="input">{{ input }}</span>
|
||||
<span class="word-end">
|
||||
<span class="wrong" :class="wrong === ' ' && 'bg-wrong'" v-if="wrong">{{ wrong }}</span>
|
||||
<span :class="!word.isSymbol && 'dictation-hide'" v-else>{{ indexWord(word) }}</span>
|
||||
<span class="dictation-hide">{{ remainderWord(word) }}</span>
|
||||
</span>
|
||||
<span class="word-wrap">
|
||||
<TypingWord :word="word"
|
||||
:is-typing="true"
|
||||
v-if="isCurrent(indexI,indexJ,indexW) && !isSpace"/>
|
||||
<TypingWord :word="word" :is-typing="false" v-else/>
|
||||
<span class="border-bottom" v-if="settingStore.dictation"></span>
|
||||
</span>
|
||||
<span v-else class="word-wrap">
|
||||
<span :class="!word.isSymbol && 'dictation-hide'">{{ word.word }}</span>
|
||||
<span class="border-bottom" v-if="settingStore.dictation"></span>
|
||||
</span>
|
||||
<span
|
||||
v-if="word.nextSpace"
|
||||
class="word-end"
|
||||
:class="[
|
||||
(`${indexI}${indexJ}${indexW}` === currentIndex && isSpace && wrong) && 'bg-wrong',
|
||||
]"
|
||||
>
|
||||
<span class="word-space"
|
||||
:class="[
|
||||
settingStore.dictation && 'to-bottom',
|
||||
(`${indexI}${indexJ}${indexW}` === currentIndex && isSpace && !wrong ) && 'wait',
|
||||
]"
|
||||
></span>
|
||||
</span>
|
||||
<Space
|
||||
v-if="word.nextSpace"
|
||||
class="word-end"
|
||||
:is-wrong="false"
|
||||
:is-wait="isCurrent(indexI,indexJ,indexW) && isSpace"
|
||||
:is-shake="isCurrent(indexI,indexJ,indexW) && isSpace && wrong !== ''"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -516,9 +510,11 @@ let showQuestions = $ref(false)
|
||||
|
||||
.wrote {
|
||||
color: grey;
|
||||
//color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
$translate-lh: 3.2;
|
||||
$article-lh: 2.4;
|
||||
|
||||
.typing-article {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
@@ -548,28 +544,22 @@ let showQuestions = $ref(false)
|
||||
|
||||
.article-content {
|
||||
position: relative;
|
||||
//opacity: 0;
|
||||
}
|
||||
|
||||
article {
|
||||
line-height: 1.3;
|
||||
word-break: keep-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--en-article-family);
|
||||
|
||||
&.dictation {
|
||||
.dictation-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.wrote, .hover-show {
|
||||
.dictation-hide {
|
||||
:deep(.hide) {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
@@ -579,16 +569,21 @@ let showQuestions = $ref(false)
|
||||
}
|
||||
|
||||
.hover-show {
|
||||
border-radius: 0.2rem;
|
||||
background: var(--color-select-bg);
|
||||
color: white !important;
|
||||
|
||||
:deep(.hide) {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.wrote {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.tall {
|
||||
line-height: 2.4;
|
||||
line-height: $article-lh;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -596,7 +591,6 @@ let showQuestions = $ref(false)
|
||||
|
||||
.sentence {
|
||||
transition: all .3s;
|
||||
|
||||
}
|
||||
|
||||
.word {
|
||||
@@ -604,17 +598,17 @@ let showQuestions = $ref(false)
|
||||
|
||||
.word-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-bottom: 2px solid var(--color-article);
|
||||
display: none;
|
||||
transform: translateY(-0.2rem);
|
||||
}
|
||||
.border-bottom {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-bottom: 2px solid var(--color-article);
|
||||
display: none;
|
||||
transform: translateY(-0.2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -628,7 +622,7 @@ let showQuestions = $ref(false)
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 1.2rem;
|
||||
line-height: 3.2;
|
||||
line-height: $translate-lh;
|
||||
letter-spacing: .2rem;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
@@ -646,63 +640,6 @@ let showQuestions = $ref(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-space {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 0.8rem;
|
||||
height: 1.5rem;
|
||||
margin: 0 1px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.to-bottom {
|
||||
transform: translateY(0.3rem);
|
||||
}
|
||||
|
||||
&.wait {
|
||||
border-bottom: 2px solid var(--color-article);
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: .25rem;
|
||||
background: var(--color-article);
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: .26rem;
|
||||
background: var(--color-article);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-start {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
.wrong {
|
||||
color: rgba(red, 0.6);
|
||||
}
|
||||
|
||||
.word-wrong {
|
||||
display: inline-block;
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
|
||||
.bg-wrong {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
background: rgba(red, 0.6);
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor {
|
||||
|
||||
109
src/pages/pc/article/components/TypingWord.vue
Normal file
109
src/pages/pc/article/components/TypingWord.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="tsx">
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import Space from "@/pages/pc/article/components/Space.vue";
|
||||
//引入这个编译就报错
|
||||
// import {ArticleWord} from "@/types/types.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
word: any,
|
||||
isTyping: boolean,
|
||||
}>()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
function compare(a: string, b: string) {
|
||||
return settingStore.ignoreCase ? a.toLowerCase() === b.toLowerCase() : a === b
|
||||
}
|
||||
|
||||
const isHide = $computed(() => {
|
||||
if (settingStore.dictation && !props.word.isSymbol) return 'hide'
|
||||
return ''
|
||||
})
|
||||
|
||||
const list = $computed(() => {
|
||||
let t = []
|
||||
let right = ''
|
||||
let wrong = ''
|
||||
if (props.word.input.length) {
|
||||
if (props.word.input.length === props.word.word.length) {
|
||||
if (settingStore.ignoreCase ? props.word.input.toLowerCase() === props.word.word.toLowerCase() : props.word.input === props.word.word) {
|
||||
t.push({type: 'word-complete', val: props.word.input})
|
||||
return t
|
||||
}
|
||||
}
|
||||
props.word.input.split('').forEach((k, i) => {
|
||||
if (k === ' ') t.push({type: 'space'})
|
||||
else {
|
||||
if (compare(k, props.word.word[i])) {
|
||||
right += k
|
||||
wrong = ''
|
||||
if (t.length) {
|
||||
let last = t[t.length - 1]
|
||||
if (last.type === 'input-right') {
|
||||
last.val = right
|
||||
} else {
|
||||
t.push({type: 'input-right', val: right})
|
||||
}
|
||||
} else {
|
||||
t.push({type: 'input-right', val: right})
|
||||
}
|
||||
} else {
|
||||
wrong += k
|
||||
right = ''
|
||||
if (t.length) {
|
||||
let last = t[t.length - 1]
|
||||
if (last.type === 'input-wrong') {
|
||||
last.val = wrong
|
||||
} else {
|
||||
t.push({type: 'input-wrong', val: wrong})
|
||||
}
|
||||
} else {
|
||||
t.push({type: 'input-wrong', val: wrong})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (props.word.input.length < props.word.word.length) {
|
||||
t.push({type: 'word-end', val: props.word.word.slice(props.word.input.length)})
|
||||
}
|
||||
} else {
|
||||
//word-end这个class用于光标定位,光标会定位到第一个word-end的位置
|
||||
t.push({type: 'word-end', val: props.word.word})
|
||||
}
|
||||
return t
|
||||
})
|
||||
|
||||
defineRender(() => {
|
||||
return list.map((item, i) => {
|
||||
if (item.type === 'word-complete') {
|
||||
return <span>{item.val}</span>
|
||||
}
|
||||
if (item.type === 'word-end') {
|
||||
return <span className={'word-end ' + isHide}>{item.val}</span>
|
||||
}
|
||||
if (item.type === 'input-right') {
|
||||
return <span className={props.isTyping ? 'input-right' : ''}>{item.val}</span>
|
||||
}
|
||||
if (item.type === 'input-wrong') {
|
||||
return <span className="input-wrong">{item.val}</span>
|
||||
}
|
||||
if (item.type === 'space') {
|
||||
return <Space isWrong={true}/>
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.input-right {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
.input-wrong {
|
||||
@apply color-red
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,499 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, provide, watch} from "vue";
|
||||
|
||||
import Statistics from "@/pages/pc/word/Statistics.vue";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {Dict, ShortcutKey, StudyData, Word} from "@/types/types.ts";
|
||||
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
|
||||
import {_getDictDataByUrl, cloneDeep, shuffle} from "@/utils";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import Footer from "@/pages/pc/word/components/Footer.vue";
|
||||
import Panel from "@/pages/pc/components/Panel.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Tooltip from "@/pages/pc/components/base/Tooltip.vue";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import TypeWord from "@/pages/pc/word/components/TypeWord.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
|
||||
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
|
||||
import ConflictNotice from "@/pages/pc/components/ConflictNotice.vue";
|
||||
import dict_list from "@/assets/dict-list.json";
|
||||
|
||||
interface IProps {
|
||||
new: Word[],
|
||||
review: Word[],
|
||||
write: Word[],
|
||||
}
|
||||
|
||||
const {
|
||||
isWordCollect,
|
||||
toggleWordCollect,
|
||||
isWordSimple,
|
||||
toggleWordSimple
|
||||
} = useWordOptions()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const {toggleTheme} = useTheme()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useBaseStore()
|
||||
const statStore = usePracticeStore()
|
||||
const typingRef: any = $ref()
|
||||
let allWrongWords = new Set()
|
||||
let showStatDialog = $ref(false)
|
||||
let loading = $ref(false)
|
||||
let studyData = $ref<IProps>({
|
||||
new: [],
|
||||
review: [],
|
||||
write: []
|
||||
})
|
||||
|
||||
let data = $ref<StudyData>({
|
||||
index: 0,
|
||||
words: [],
|
||||
wrongWords: [],
|
||||
})
|
||||
|
||||
async function init() {
|
||||
console.log('load好了开始加载')
|
||||
let dict = getDefaultDict()
|
||||
let dictId = route.params.id
|
||||
if (dictId) {
|
||||
//先在自己的词典列表里面找,如果没有再在资源列表里面找
|
||||
dict = store.word.bookList.find(v => v.id === dictId)
|
||||
if (!dict) dict = dict_list.flat().find(v => v.id === dictId) as Dict
|
||||
if (dict && dict.id) {
|
||||
//如果是不是自定义词典,就请求数据
|
||||
if (!dict.custom) dict = await _getDictDataByUrl(dict)
|
||||
if (!dict.words.length) {
|
||||
router.push('/word')
|
||||
return Toast.warning('没有单词可学习!')
|
||||
}
|
||||
store.changeDict(dict)
|
||||
studyData = getCurrentStudyWord()
|
||||
loading = false
|
||||
} else {
|
||||
router.push('/word')
|
||||
}
|
||||
} else {
|
||||
router.push('/word')
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => store.load, (n) => {
|
||||
if (n && loading) init()
|
||||
}, {immediate: true})
|
||||
|
||||
onMounted(() => {
|
||||
if (runtimeStore.routeData) {
|
||||
studyData = runtimeStore.routeData
|
||||
} else {
|
||||
loading = true
|
||||
}
|
||||
})
|
||||
|
||||
useStartKeyboardEventListener()
|
||||
useDisableEventListener(() => loading)
|
||||
|
||||
watch(() => studyData, () => {
|
||||
if (studyData.new.length === 0) {
|
||||
if (studyData.review.length) {
|
||||
settingStore.dictation = false
|
||||
statStore.step = 2
|
||||
data.words = studyData.review
|
||||
} else {
|
||||
if (studyData.write.length) {
|
||||
settingStore.dictation = true
|
||||
data.words = studyData.write
|
||||
statStore.step = 4
|
||||
} else {
|
||||
Toast.warning('没有可学习的单词!')
|
||||
router.push('/word')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
settingStore.dictation = false
|
||||
data.words = studyData.new
|
||||
statStore.step = 0
|
||||
}
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
allWrongWords = new Set()
|
||||
|
||||
statStore.startDate = Date.now()
|
||||
statStore.inputWordNumber = 0
|
||||
statStore.wrong = 0
|
||||
statStore.total = studyData.review.length + studyData.new.length + studyData.write.length
|
||||
statStore.newWordNumber = studyData.new.length
|
||||
statStore.reviewWordNumber = studyData.review.length
|
||||
statStore.writeWordNumber = studyData.write.length
|
||||
statStore.index = 0
|
||||
})
|
||||
|
||||
provide('studyData', data)
|
||||
|
||||
const word = $computed(() => {
|
||||
return data.words[data.index] ?? getDefaultWord()
|
||||
})
|
||||
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) {
|
||||
// showStatDialog = true
|
||||
// return
|
||||
if (data.index === data.words.length - 1) {
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前学完了,但还有错词')
|
||||
data.words = shuffle(cloneDeep(data.wrongWords))
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
} else {
|
||||
console.log('当前学完了,没错词', statStore.total, statStore.step, data.index)
|
||||
if (isTyping) statStore.inputWordNumber++
|
||||
|
||||
//学完了
|
||||
if (statStore.step === 4) {
|
||||
statStore.spend = Date.now() - statStore.startDate
|
||||
console.log('全完学完了')
|
||||
showStatDialog = true
|
||||
// emit('complete', {})
|
||||
}
|
||||
|
||||
//开始默认所有单词
|
||||
if (statStore.step === 3) {
|
||||
statStore.step++
|
||||
if (studyData.write.length) {
|
||||
console.log('开始默认所有单词')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(studyData.write)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始默认所有单词-无单词路过')
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始默写昨日
|
||||
if (statStore.step === 2) {
|
||||
statStore.step++
|
||||
if (studyData.review.length) {
|
||||
console.log('开始默写昨日')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(studyData.review)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始默写昨日-无单词路过')
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始复习昨日
|
||||
if (statStore.step === 1) {
|
||||
statStore.step++
|
||||
if (studyData.review.length) {
|
||||
console.log('开始复习昨日')
|
||||
settingStore.dictation = false
|
||||
data.words = shuffle(studyData.review)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始复习昨日-无单词路过')
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始默写新词
|
||||
if (statStore.step === 0) {
|
||||
if (settingStore.wordPracticeMode === 1) {
|
||||
console.log('自由模式,全完学完了')
|
||||
showStatDialog = true
|
||||
return
|
||||
}
|
||||
statStore.step++
|
||||
console.log('开始默写新词')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(studyData.new)
|
||||
data.index = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data.index++
|
||||
isTyping && statStore.inputWordNumber++
|
||||
console.log('这个词完了')
|
||||
}
|
||||
}
|
||||
|
||||
function onTypeWrong() {
|
||||
let temp = word.word.toLowerCase()
|
||||
if (!allWrongWords.has(word.word.toLowerCase())) {
|
||||
allWrongWords.add(word.word.toLowerCase())
|
||||
statStore.wrong++
|
||||
}
|
||||
//测试时这里会卡一下,加上requestIdleCallback就好了
|
||||
requestIdleCallback(() => {
|
||||
if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === temp)) {
|
||||
store.wrong.words.push(word)
|
||||
store.wrong.length = store.wrong.words.length
|
||||
}
|
||||
if (!data.wrongWords.find((v: Word) => v.word.toLowerCase() === temp)) {
|
||||
data.wrongWords.push(word)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
// console.log('onKeyUp', e)
|
||||
typingRef.hideWord()
|
||||
}
|
||||
|
||||
async function onKeyDown(e: KeyboardEvent) {
|
||||
// console.log('onKeyDown', e)
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
typingRef.del()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
|
||||
function repeat() {
|
||||
console.log('重学一遍')
|
||||
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
|
||||
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
|
||||
//如果是刚刚完成,那么学习进度要从length减回去,因为lastLearnIndex为0了,同时改complete为false
|
||||
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
|
||||
store.sdict.complete = false
|
||||
} else {
|
||||
//将学习进度减回去
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
|
||||
}
|
||||
emitter.emit(EventKey.resetWord)
|
||||
let temp = cloneDeep(studyData)
|
||||
//排除已掌握单词
|
||||
temp.new = temp.new.filter(v => !store.knownWords.includes(v.word))
|
||||
temp.review = temp.review.filter(v => !store.knownWords.includes(v.word))
|
||||
temp.write = temp.write.filter(v => !store.knownWords.includes(v.word))
|
||||
studyData = temp
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (data.index === 0) {
|
||||
Toast.warning('已经是第一个了~')
|
||||
} else {
|
||||
data.index--
|
||||
}
|
||||
}
|
||||
|
||||
function skip(e: KeyboardEvent) {
|
||||
next(false)
|
||||
// e.preventDefault()
|
||||
}
|
||||
|
||||
function show(e: KeyboardEvent) {
|
||||
typingRef.showWord()
|
||||
}
|
||||
|
||||
function collect(e: KeyboardEvent) {
|
||||
toggleWordCollect(word)
|
||||
}
|
||||
|
||||
function play() {
|
||||
typingRef.play()
|
||||
}
|
||||
|
||||
function toggleWordSimpleWrapper() {
|
||||
if (!isWordSimple(word)) {
|
||||
toggleWordSimple(word)
|
||||
//延迟一下,不知道为什么不延迟会导致当前条目不自动定位到列表中间
|
||||
setTimeout(() => next(false))
|
||||
} else {
|
||||
toggleWordSimple(word)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTranslate() {
|
||||
settingStore.translate = !settingStore.translate
|
||||
}
|
||||
|
||||
function toggleDictation() {
|
||||
settingStore.dictation = !settingStore.dictation
|
||||
}
|
||||
|
||||
function toggleConciseMode() {
|
||||
settingStore.showToolbar = !settingStore.showToolbar
|
||||
settingStore.showPanel = settingStore.showToolbar
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
settingStore.showPanel = !settingStore.showPanel
|
||||
}
|
||||
|
||||
function continueStudy() {
|
||||
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
|
||||
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
|
||||
if (!showStatDialog) {
|
||||
console.log('没学完,强行跳过')
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
|
||||
} else {
|
||||
console.log('学完了,正常下一组')
|
||||
showStatDialog = false
|
||||
}
|
||||
studyData = getCurrentStudyWord()
|
||||
}
|
||||
|
||||
useEvents([
|
||||
[EventKey.repeatStudy, repeat],
|
||||
[EventKey.continueStudy, continueStudy],
|
||||
[EventKey.changeDict, () => {
|
||||
studyData = getCurrentStudyWord()
|
||||
}],
|
||||
|
||||
[ShortcutKey.ShowWord, show],
|
||||
[ShortcutKey.Previous, prev],
|
||||
[ShortcutKey.Next, skip],
|
||||
[ShortcutKey.ToggleCollect, collect],
|
||||
[ShortcutKey.ToggleSimple, toggleWordSimpleWrapper],
|
||||
[ShortcutKey.PlayWordPronunciation, play],
|
||||
|
||||
[ShortcutKey.RepeatChapter, repeat],
|
||||
[ShortcutKey.NextChapter, continueStudy],
|
||||
[ShortcutKey.ToggleShowTranslate, toggleTranslate],
|
||||
[ShortcutKey.ToggleDictation, toggleDictation],
|
||||
[ShortcutKey.ToggleTheme, toggleTheme],
|
||||
[ShortcutKey.ToggleConciseMode, toggleConciseMode],
|
||||
[ShortcutKey.TogglePanel, togglePanel],
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="practice-wrapper" v-loading="loading">
|
||||
<div class="practice-word">
|
||||
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
|
||||
<div class="center gap-2 cursor-pointer float-left"
|
||||
@click="prev"
|
||||
v-if="prevWord">
|
||||
<IconFluentArrowLeft16Regular class="arrow" width="22"/>
|
||||
<Tooltip
|
||||
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
|
||||
>
|
||||
<div class="word">{{ prevWord.word }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="center gap-2 cursor-pointer float-right "
|
||||
@click="next(false)"
|
||||
v-if="nextWord">
|
||||
<Tooltip
|
||||
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
>
|
||||
<div class="word" :class="settingStore.dictation && 'word-shadow'">{{ nextWord.word }}</div>
|
||||
</Tooltip>
|
||||
<IconFluentArrowRight16Regular class="arrow" width="22"/>
|
||||
</div>
|
||||
</div>
|
||||
<TypeWord
|
||||
ref="typingRef"
|
||||
:word="word"
|
||||
@wrong="onTypeWrong"
|
||||
@complete="next"
|
||||
/>
|
||||
<Footer
|
||||
:is-simple="isWordSimple(word)"
|
||||
@toggle-simple="toggleWordSimpleWrapper"
|
||||
:is-collect="isWordCollect(word)"
|
||||
@toggle-collect="toggleWordCollect(word)"
|
||||
@skip="next(false)"
|
||||
/>
|
||||
</div>
|
||||
<div class="word-panel-wrapper">
|
||||
<Panel>
|
||||
<template v-slot:title>
|
||||
<!-- <span>{{ store.sdict.name }} ({{ data.index + 1 }} / {{ data.words.length }})</span>-->
|
||||
<div class="center gap-space">
|
||||
<span>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} / {{ store.sdict.length }})</span>
|
||||
|
||||
<BaseIcon
|
||||
@click="continueStudy"
|
||||
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
|
||||
<IconFluentArrowRight16Regular class="arrow" width="22"/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="panel-page-item pl-4">
|
||||
<WordList
|
||||
v-if="data.words.length"
|
||||
:is-active="true"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
:class="!isWordCollect(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isWordCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
:class="!isWordSimple(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
|
||||
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
|
||||
<IconFluentCheckmarkCircle16Filled v-else/>
|
||||
</BaseIcon>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
<Statistics v-model="showStatDialog"/>
|
||||
<ConflictNotice/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.practice-wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.practice-word {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: var(--toolbar-width);
|
||||
}
|
||||
|
||||
.word-panel-wrapper {
|
||||
position: absolute;
|
||||
left: var(--panel-margin-left);
|
||||
//left: 0;
|
||||
top: .8rem;
|
||||
z-index: 1;
|
||||
height: calc(100% - 1.5rem);
|
||||
}
|
||||
</style>
|
||||
@@ -26,6 +26,7 @@ export function getDefaultArticleWord(val: Partial<ArticleWord> = {}): ArticleWo
|
||||
nextSpace: true,
|
||||
isSymbol: false,
|
||||
symbolPosition: '',
|
||||
input: '',
|
||||
...val
|
||||
}) as ArticleWord
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface ArticleWord extends Word {
|
||||
nextSpace: boolean,
|
||||
isSymbol: boolean,
|
||||
symbolPosition: 'start' | 'end' | '',
|
||||
input:string
|
||||
}
|
||||
|
||||
export interface Sentence {
|
||||
|
||||
Reference in New Issue
Block a user