fix:修复文章界面toolbar有问题

This commit is contained in:
zyronon
2025-08-11 01:33:16 +08:00
parent 72cec19810
commit 38b59fb1a5
13 changed files with 462 additions and 627 deletions

View File

@@ -1,34 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/logo.jpg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>TypeWords练习英语</title>
<script defer src="./s.js" data-website-id="160308c9-7900-4b1d-a0b1-c3b25a9530f6"></script>
<script>
;(function () {
var src = '//cdn.jsdelivr.net/npm/eruda';
if (!/eruda=true/.test(window.location) && localStorage.getItem('active-eruda') != 'true') return;
document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>');
document.write('<scr' + 'ipt>eruda.init();</scr' + 'ipt>');
})();
</script>
<script>
if (!location.href.includes('localhost')
&& !location.href.includes('192.168')
&& !location.href.includes('172.16')
&& !location.href.includes('10.0')
) {
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?3dae52fcd5375a19905462e4ad3eb54e";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/logo.jpg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>TypeWords练习英语</title>
<script defer src="./s.js" data-website-id="160308c9-7900-4b1d-a0b1-c3b25a9530f6"></script>
<script>
;(function () {
var src = '//cdn.jsdelivr.net/npm/eruda';
if (!/eruda=true/.test(window.location) && localStorage.getItem('active-eruda') != 'true') return;
document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>');
document.write('<scr' + 'ipt>eruda.init();</scr' + 'ipt>');
})();
}
</script>
</script>
<script>
if (!location.href.includes('localhost')
&& !location.href.includes('192.168')
&& !location.href.includes('172.16')
&& !location.href.includes('10.0')
) {
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?3dae52fcd5375a19905462e4ad3eb54e";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
}
</script>
</head>
<body>
<div id="app"></div>

View File

@@ -17,10 +17,11 @@
--practice-wrapper-translateX: 1px;
--article-width: 50vw;
--article-toolbar-width: 50rem;
--toolbar-width: 50rem;
--panel-width: 24rem;
--space: 1rem;
--stat-gap: 2rem;
--stat-gap: 1rem;
--shadow: rgba(0, 0, 0, 0.08) 0px 4px 12px;
--panel-margin-left: calc(50% + var(--toolbar-width) / 2 + 1rem);
--article-panel-margin-left: calc(50% + var(--article-width) / 2 + 1rem);
@@ -416,7 +417,7 @@ a {
.book {
@extend .anim;
@apply p-4 rounded-md relative cursor-pointer bg-third hover:bg-card-active flex flex-col justify-between;
@apply p-4 rounded-md relative cursor-pointer bg-third hover:bg-card-active flex flex-col justify-between shrink-0;
$w: 6rem;
width: $w;
height: calc($w * 1.4);

View File

@@ -126,7 +126,7 @@ async function goBookDetail(val: DictResource) {
<div class="color-blue cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人书籍</div>
</div>
</div>
<div class="grid grid-cols-6 gap-4 mt-4">
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false" quantifier="篇" :item="item" :checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)"
:show-checkbox="isMultiple && j >= 1"

View File

@@ -2,23 +2,45 @@
import {onMounted, onUnmounted} from "vue";
import {useBaseStore} from "@/stores/base.ts";
import Statistics from "@/pages/pc/word/Statistics.vue";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import PracticeArticle from "@/pages/pc/article/practice-article/index.vue";
import {ShortcutKey} from "@/types/types.ts";
import {useStartKeyboardEventListener} from "@/hooks/event.ts";
import {Article, ArticleItem, ArticleWord, ShortcutKey, Word} from "@/types/types.ts";
import {useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
import useTheme from "@/hooks/theme.ts";
import {ElMessage} from "element-plus";
import {cloneDeep} from "@/utils";
import {usePracticeStore} from "@/stores/practice.ts";
import {useArticleOptions} from "@/hooks/dict.ts";
import {genArticleSectionData, usePlaySentenceAudio} from "@/hooks/article.ts";
import router from "@/router.ts";
import {getDefaultArticle} from "@/types/func.ts";
import TypingArticle from "@/pages/pc/article/components/TypingArticle.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import Panel from "@/pages/pc/components/Panel.vue";
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
import EditSingleArticleModal from "@/pages/pc/article/components/EditSingleArticleModal.vue";
import Tooltip from "@/pages/pc/components/Tooltip.vue";
import {Icon} from "@iconify/vue";
const store = useBaseStore()
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const statisticsStore = usePracticeStore()
const {toggleTheme} = useTheme()
const practiceRef: any = $ref()
let articleData = $ref({
list: [],
article: getDefaultArticle(),
sectionIndex: 0,
sentenceIndex: 0,
wordIndex: 0,
stringIndex: 0,
})
let showEditArticle = $ref(false)
let typingArticleRef = $ref<any>()
let editArticle = $ref<Article>(getDefaultArticle())
useStartKeyboardEventListener()
function write() {
// console.log('write')
@@ -27,81 +49,427 @@ function write() {
}
//TODO 需要判断是否已忽略
//todo 使用场景是?
function repeat() {
// console.log('repeat')
emitter.emit(EventKey.resetWord)
practiceRef.getCurrentPractice()
getCurrentPractice()
}
function prev() {
// console.log('next')
if (store.currentBook.chapterIndex === 0) {
if (store.sbook.lastLearnIndex === 0) {
ElMessage.warning('已经在第一章了~')
} else {
store.currentBook.chapterIndex--
repeat()
store.sbook.lastLearnIndex--
getCurrentPractice()
}
}
function toggleShowTranslate() {
settingStore.translate = !settingStore.translate
}
function toggleDictation() {
settingStore.dictation = !settingStore.dictation
}
const toggleShowTranslate = () => settingStore.translate = !settingStore.translate
const toggleDictation = () => settingStore.dictation = !settingStore.dictation
const togglePanel = () => settingStore.showPanel = !settingStore.showPanel
const skip = () => typingArticleRef?.nextSentence()
const collect = () => toggleArticleCollect(articleData.article)
const shortcutKeyEdit = () => edit()
function toggleConciseMode() {
settingStore.showToolbar = !settingStore.showToolbar
settingStore.showPanel = settingStore.showToolbar
}
function togglePanel() {
settingStore.showPanel = !settingStore.showPanel
function next() {
if (store.sbook.lastLearnIndex >= articleData.list.length - 1) {
store.sbook.lastLearnIndex = 0
//todo 这里应该弹窗
} else store.sbook.lastLearnIndex++
getCurrentPractice()
}
function jumpSpecifiedChapter(val: number) {
store.currentBook.chapterIndex = val
repeat()
function init() {
if (!store.sbook?.articles?.length) {
router.push('/article')
return
}
articleData.list = cloneDeep(store.sbook.articles)
getCurrentPractice()
console.log('init', articleData.article)
}
function setArticle(val: Article) {
statisticsStore.inputWordNumber = 0
statisticsStore.wrong = 0
statisticsStore.total = 0
statisticsStore.startDate = Date.now()
articleData.list[store.sbook.lastLearnIndex] = val
articleData.article = val
articleData.sectionIndex = 0
articleData.sentenceIndex = 0
articleData.wordIndex = 0
articleData.stringIndex = 0
articleData.article.sections.map((v, i) => {
v.map((w, j) => {
w.words.map(s => {
if (!store.knownWordsWithSimpleWords.includes(s.word.toLowerCase()) && !s.isSymbol) {
statisticsStore.total++
}
})
})
})
}
function getCurrentPractice() {
emitter.emit(EventKey.resetWord)
let currentArticle = articleData.list[store.sbook.lastLearnIndex]
let article = getDefaultArticle(currentArticle)
// console.log('article', article)
if (article.sections.length) {
setArticle(article)
} else {
genArticleSectionData(article)
setArticle(article)
}
}
function saveArticle(val: Article) {
console.log('saveArticle', val, JSON.stringify(val.lrcPosition))
console.log('saveArticle', val.textTranslate)
showEditArticle = false
let rIndex = store.sbook.articles.findIndex(v => v.id === val.id)
if (rIndex > -1) {
store.sbook.articles[rIndex] = cloneDeep(val)
}
setArticle(val)
}
function edit(val: Article = articleData.article) {
editArticle = val
showEditArticle = true
}
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)
}
if (!store.knownWordsWithSimpleWords.includes(lowerName)) {
//todo
}
}
function nextWord(word: ArticleWord) {
if (!store.knownWordsWithSimpleWords.includes(word.word.toLowerCase()) && !word.isSymbol) {
statisticsStore.inputWordNumber++
}
}
function changeArticle(val: ArticleItem) {
let rIndex = articleData.list.findIndex(v => v.id === val.item.id)
if (rIndex > -1) {
store.sbook.lastLearnIndex = rIndex
getCurrentPractice()
}
}
const {
isArticleCollect,
toggleArticleCollect
} = useArticleOptions()
function play() {
typingArticleRef?.play()
}
function show() {
typingArticleRef?.showSentence()
}
function onKeyUp() {
typingArticleRef.hideSentence()
}
async function onKeyDown(e: KeyboardEvent) {
// console.log('e', e)
switch (e.key) {
case 'Backspace':
typingArticleRef.del()
break
}
}
useOnKeyboardEventListener(onKeyDown, onKeyUp)
onMounted(init)
useEvents([
[EventKey.write, write],
[EventKey.repeatStudy, repeat],
[EventKey.continueStudy, next],
[ShortcutKey.PreviousChapter, prev],
[ShortcutKey.RepeatChapter, repeat],
[ShortcutKey.DictationChapter, write],
[ShortcutKey.ToggleShowTranslate, toggleShowTranslate],
[ShortcutKey.ToggleDictation, toggleDictation],
[ShortcutKey.ToggleTheme, toggleTheme],
[ShortcutKey.ToggleConciseMode, toggleConciseMode],
[ShortcutKey.TogglePanel, togglePanel],
[ShortcutKey.NextChapter, next],
[ShortcutKey.PlayWordPronunciation, play],
[ShortcutKey.ShowWord, show],
[ShortcutKey.Next, skip],
[ShortcutKey.ToggleCollect, collect],
[ShortcutKey.EditArticle, shortcutKeyEdit],
])
let speedMinute = $ref(0)
let timer = $ref(0)
onMounted(() => {
emitter.on(EventKey.write, write)
emitter.on(EventKey.repeatStudy, repeat)
emitter.on(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.on(ShortcutKey.PreviousChapter, prev)
emitter.on(ShortcutKey.RepeatChapter, repeat)
emitter.on(ShortcutKey.DictationChapter, write)
emitter.on(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
emitter.on(ShortcutKey.ToggleDictation, toggleDictation)
emitter.on(ShortcutKey.ToggleTheme, toggleTheme)
emitter.on(ShortcutKey.ToggleConciseMode, toggleConciseMode)
emitter.on(ShortcutKey.TogglePanel, togglePanel)
timer = setInterval(() => {
speedMinute = Math.floor((Date.now() - statisticsStore.startDate) / 1000 / 60)
}, 1000)
})
onUnmounted(() => {
emitter.off(EventKey.write, write)
emitter.off(EventKey.repeatStudy, repeat)
emitter.off(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.off(ShortcutKey.PreviousChapter, prev)
emitter.off(ShortcutKey.RepeatChapter, repeat)
emitter.off(ShortcutKey.DictationChapter, write)
emitter.off(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
emitter.off(ShortcutKey.ToggleDictation, toggleDictation)
emitter.off(ShortcutKey.ToggleTheme, toggleTheme)
emitter.off(ShortcutKey.ToggleConciseMode, toggleConciseMode)
emitter.off(ShortcutKey.TogglePanel, togglePanel)
timer && clearInterval(timer)
})
useStartKeyboardEventListener()
let audioRef = $ref<HTMLAudioElement>()
const {playSentenceAudio} = usePlaySentenceAudio()
</script>
<template>
<PracticeArticle ref="practiceRef"/>
<Statistics/>
<div class="practice-wrapper">
<div class="practice-article">
<TypingArticle
ref="typingArticleRef"
@edit="edit"
@wrong="wrong"
@next="next"
@nextWord="nextWord"
@play="e => playSentenceAudio(e,audioRef,articleData.article)"
:article="articleData.article"
/>
<div class="panel-wrapper">
<Panel>
<template v-slot:title>
<span>{{
store.sbook.name
}} ({{ store.sbook.lastLearnIndex + 1 }} / {{ articleData.list.length }})</span>
</template>
<div class="panel-page-item pl-4">
<ArticleList
:isActive="true"
:static="false"
:show-translate="settingStore.translate"
@click="changeArticle"
:active-id="articleData.article.id"
:list="articleData.list ">
<template v-slot:suffix="{item,index}">
<BaseIcon
v-if="!isArticleCollect(item)"
class="collect"
@click="toggleArticleCollect(item)"
title="收藏" icon="ph:star"/>
<BaseIcon
v-else
class="fill"
@click="toggleArticleCollect(item)"
title="取消收藏" icon="ph:star-fill"/>
</template>
</ArticleList>
</div>
</Panel>
</div>
<EditSingleArticleModal
v-model="showEditArticle"
:article="editArticle"
@save="saveArticle"
/>
</div>
<div class="footer" :class="!settingStore.showToolbar && 'hide'">
<Tooltip :title="settingStore.showToolbar?'收起':'展开'">
<Icon icon="icon-park-outline:down"
@click="settingStore.showToolbar = !settingStore.showToolbar"
class="arrow"
:class="!settingStore.showToolbar && 'down'"
width="24"
color="#999"/>
</Tooltip>
<div class="bottom">
<div class="flex justify-between items-center">
<div class="stat">
<div class="row">
<div class="num">{{ speedMinute }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
</div>
<div class="row">
<div class="num">{{ statisticsStore.total }}</div>
<div class="line"></div>
<div class="name">单词总数</div>
</div>
</div>
<audio ref="audioRef" v-if="articleData.article.audioSrc" :src="articleData.article.audioSrc"
controls></audio>
<div class="flex flex-col items-center justify-center gap-1">
<div class="flex gap-2 center">
<BaseIcon
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
icon="icon-park-outline:go-ahead"
@click="skip"/>
<BaseIcon
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
icon="fluent:replay-16-filled"
@click="play"/>
<BaseIcon
@click="settingStore.dictation = !settingStore.dictation"
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
:icon="['majesticons:eye-off-line','mdi:eye-outline'][settingStore.dictation?0:1]"/>
<BaseIcon :icon="['mdi:translate','mdi:translate-off'][settingStore.translate?0:1]"
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
@click="settingStore.translate = !settingStore.translate"/>
<!-- <BaseIcon-->
<!-- :title="`编辑(${settingStore.shortcutKeyMap[ShortcutKey.EditArticle]})`"-->
<!-- icon="tabler:edit"-->
<!-- @click="emitter.emit(ShortcutKey.EditArticle)"-->
<!-- />-->
<BaseIcon
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
icon="tdesign:menu-unfold"/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.practice-wrapper {
font-size: 0.9rem;
width: 100%;
height: 100vh;
display: flex;
overflow: hidden;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.swiper-wrapper {
height: 100%;
overflow: hidden;
.swiper-list {
transition: transform .3s;
height: 200%;
.swiper-item {
height: 50%;
overflow: auto;
display: flex;
justify-content: center;
}
}
.step1 {
transform: translate3d(0, -50%, 0);
}
}
.practice-article {
flex: 1;
overflow: hidden;
width: var(--article-width);
}
.typing-word-wrapper {
width: var(--toolbar-width);
}
.panel-wrapper {
position: absolute;
left: var(--article-panel-margin-left);
//left: 0;
top: .8rem;
z-index: 1;
height: calc(100% - 1.5rem);
}
.footer {
width: var(--article-toolbar-width);
margin-bottom: .8rem;
transition: all var(--anim-time);
position: relative;
margin-top: 1rem;
&.hide {
margin-bottom: -6rem;
margin-top: 3rem;
}
.bottom {
position: relative;
width: 100%;
box-sizing: border-box;
border-radius: .6rem;
background: var(--color-second);
padding: .5rem var(--space);
z-index: 2;
border: 1px solid var(--color-item-border);
box-shadow: var(--shadow);
.stat {
margin-top: .5rem;
display: flex;
justify-content: space-around;
gap: var(--stat-gap);
.row {
display: flex;
flex-direction: column;
align-items: center;
gap: .3rem;
width: 5rem;
color: gray;
.line {
height: 1px;
width: 100%;
background: var(--color-sub-gray);
}
}
}
}
.arrow {
position: absolute;
top: -50%;
left: 50%;
cursor: pointer;
transition: all .5s;
transform: rotate(0);
padding: .5rem;
&.down {
top: -90%;
transform: rotate(180deg);
}
}
}
</style>

View File

@@ -21,7 +21,6 @@ interface IProps {
sentenceIndex?: number,
wordIndex?: number,
stringIndex?: number,
active: boolean,
}
const props = withDefaults(defineProps<IProps>(), {
@@ -30,7 +29,6 @@ const props = withDefaults(defineProps<IProps>(), {
sentenceIndex: 0,
wordIndex: 0,
stringIndex: 0,
active: true,
})
const emit = defineEmits<{
@@ -38,7 +36,8 @@ const emit = defineEmits<{
wrong: [val: Word],
play: [val: Sentence],
nextWord: [val: ArticleWord],
over: [],
complete: [],
next: [],
edit: [val: Article]
}>()
@@ -175,7 +174,7 @@ function nextSentence() {
if (!props.article.sections[sectionIndex]) {
console.log('打完了')
isEnd = true
emit('over')
emit('complete')
} else {
emit('play', props.article.sections[sectionIndex][0])
}
@@ -186,7 +185,6 @@ function nextSentence() {
}
function onTyping(e: KeyboardEvent) {
if (!props.active) return
if (!props.article.sections.length) return
// console.log('keyDown', e.key, e.code, e.keyCode)
wrong = ''
@@ -458,7 +456,7 @@ let showQuestions = $ref(false)
<div class="options flex justify-center" v-if="isEnd">
<BaseButton
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"
@click="emitter.emit(EventKey.continueStudy)">下一章
@click="emit('next')">下一章
</BaseButton>
</div>

View File

@@ -1,520 +0,0 @@
<script setup lang="ts">
import TypingArticle from "./TypingArticle.vue";
import {Article, ArticleItem, ArticleWord, DisplayStatistics, ShortcutKey, Word} from "@/types/types.ts";
import {cloneDeep} from "@/utils";
import Panel from "../../components/Panel.vue";
import {onMounted, onUnmounted} from "vue";
import {useBaseStore} from "@/stores/base.ts";
import EditSingleArticleModal from "@/pages/pc/article/components/EditSingleArticleModal.vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import {useArticleOptions} from "@/hooks/dict.ts";
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
import {genArticleSectionData, usePlaySentenceAudio} from "@/hooks/article.ts";
import {ElProgress} from 'element-plus';
import router from "@/router.ts";
import {getDefaultArticle} from "@/types/func.ts";
const store = useBaseStore()
const statisticsStore = usePracticeStore()
const runtimeStore = useRuntimeStore()
let tabIndex = $ref(0)
let wordData = $ref({
words: [],
index: -1
})
let articleData = $ref({
articles: [],
article: getDefaultArticle(),
sectionIndex: 0,
sentenceIndex: 0,
wordIndex: 0,
stringIndex: 0,
})
let showEditArticle = $ref(false)
let typingArticleRef = $ref<any>()
let editArticle = $ref<Article>(getDefaultArticle())
let articleIsActive = $computed(() => tabIndex === 0)
function next() {
if (!articleIsActive) return
if (store.currentBook.lastLearnIndex >= articleData.articles.length - 1) {
store.currentBook.lastLearnIndex = 0
} else store.currentBook.lastLearnIndex++
emitter.emit(EventKey.resetWord)
getCurrentPractice()
}
function init() {
if (!store.currentBook?.articles?.length) {
router.push('/article')
return
}
articleData.articles = cloneDeep(store.currentBook.articles)
getCurrentPractice()
console.log('inin', articleData.article)
}
function setArticle(val: Article) {
let tempVal = cloneDeep(val)
articleData.articles[store.currentBook.lastLearnIndex] = tempVal
articleData.article = tempVal
statisticsStore.inputWordNumber = 0
statisticsStore.wrong = 0
statisticsStore.total = 0
statisticsStore.startDate = Date.now()
articleData.article.sections.map((v, i) => {
v.map((w, j) => {
w.words.map(s => {
if (!store.knownWordsWithSimpleWords.includes(s.word.toLowerCase()) && !s.isSymbol) {
statisticsStore.total++
}
})
})
})
}
function getCurrentPractice() {
// console.log('store.currentBook',store.currentBook)
// return
tabIndex = 0
articleData.article = getDefaultArticle()
let currentArticle = articleData.articles[store.currentBook.lastLearnIndex]
let tempArticle = getDefaultArticle(currentArticle)
// console.log('article', tempArticle)
if (tempArticle.sections.length) {
setArticle(tempArticle)
} else {
genArticleSectionData(tempArticle)
setArticle(tempArticle)
}
}
function saveArticle(val: Article) {
console.log('saveArticle', val, JSON.stringify(val.lrcPosition))
console.log('saveArticle', val.textTranslate)
showEditArticle = false
let rIndex = store.currentBook.articles.findIndex(v => v.id === val.id)
if (rIndex > -1) {
store.currentBook.articles[rIndex] = cloneDeep(val)
}
setArticle(val)
}
function edit(val: Article = articleData.article) {
if (!articleIsActive) return
// tabIndex = 1
// wordData.words = [
// {
// ...cloneDeep(DefaultWord),
// word: 'test'
// }
// ]
// wordData.index = 0
// return
editArticle = val
showEditArticle = true
}
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)
}
if (!store.knownWordsWithSimpleWords.includes(lowerName)) {
}
}
function over() {
if (statisticsStore.wrong === 0) {
// if (false) {
console.log('这章节完了')
let now = Date.now()
let stat: DisplayStatistics = {
startDate: statisticsStore.startDate,
endDate: now,
spend: now - statisticsStore.startDate,
total: statisticsStore.total,
correctRate: -1,
wrong: statisticsStore.wrong,
}
stat.correctRate = 100 - Math.trunc(((stat.wrong) / (stat.total)) * 100)
} else {
tabIndex = 1
wordData.index = 0
}
}
function nextWord(word: ArticleWord) {
if (!store.knownWordsWithSimpleWords.includes(word.word.toLowerCase()) && !word.isSymbol) {
statisticsStore.inputWordNumber++
}
}
function handleChangeChapterIndex(val: ArticleItem) {
let rIndex = articleData.articles.findIndex(v => v.id === val.item.id)
if (rIndex > -1) {
store.currentBook.lastLearnIndex = rIndex
getCurrentPractice()
}
}
const settingStore = useSettingStore()
const {
isArticleCollect,
toggleArticleCollect
} = useArticleOptions()
function sort(list: Word[]) {
wordData.words = list
wordData.index = 0
}
function play() {
if (!articleIsActive) return
typingArticleRef?.play()
}
function show() {
if (!articleIsActive) return
typingArticleRef?.showSentence()
}
function onKeyUp(e: KeyboardEvent) {
typingArticleRef.hideSentence()
}
async function onKeyDown(e: KeyboardEvent) {
// console.log('e', e)
switch (e.key) {
case 'Backspace':
typingArticleRef.del()
break
}
}
useOnKeyboardEventListener(onKeyDown, onKeyUp)
function skip() {
if (!articleIsActive) return
typingArticleRef?.nextSentence()
}
function collect(e: KeyboardEvent) {
if (!articleIsActive) return
toggleArticleCollect(articleData.article)
}
//包装一遍因为快捷建的默认参数是Event
function shortcutKeyEdit() {
edit()
}
onMounted(() => {
init()
})
useEvents([
[EventKey.changeDict, init],
[EventKey.continueStudy, next],
[ShortcutKey.NextChapter, next],
[ShortcutKey.PlayWordPronunciation, play],
[ShortcutKey.ShowWord, show],
[ShortcutKey.Next, skip],
[ShortcutKey.ToggleCollect, collect],
[ShortcutKey.EditArticle, shortcutKeyEdit],
])
defineExpose({getCurrentPractice})
const emit = defineEmits<{
ignore: [],
wrong: [val: Word],
nextWord: [val: ArticleWord],
over: [],
edit: [val: Article]
}>()
function format(val: number, suffix: string = '', check: number = -1) {
return val === check ? '-' : (val + suffix)
}
const progress = $computed(() => {
if (!statisticsStore.total) return 0
if (statisticsStore.index > statisticsStore.total) return 100
return ((statisticsStore.index / statisticsStore.total) * 100)
})
let speedMinute = $ref(0)
let timer = $ref(0)
onMounted(() => {
timer = setInterval(() => {
speedMinute = Math.floor((Date.now() - statisticsStore.startDate) / 1000 / 60)
}, 1000)
})
onUnmounted(() => {
timer && clearInterval(timer)
})
let audioRef = $ref<HTMLAudioElement>()
const {playSentenceAudio} = usePlaySentenceAudio()
</script>
<template>
<div class="practice-wrapper">
<div class="practice-article">
<TypingArticle
ref="typingArticleRef"
:active="tabIndex === 0"
@edit="edit"
@wrong="wrong"
@over="skip"
@nextWord="nextWord"
@play="e => playSentenceAudio(e,audioRef,articleData.article)"
:article="articleData.article"
/>
<div class="panel-wrapper">
<Panel>
<template v-slot:title>
<span>{{
store.currentBook.name
}} ({{ store.currentBook.lastLearnIndex + 1 }} / {{ articleData.articles.length }})</span>
</template>
<div class="panel-page-item pl-4">
<ArticleList
:isActive="true"
:static="false"
:show-translate="settingStore.translate"
@click="handleChangeChapterIndex"
:active-id="articleData.article.id"
:list="articleData.articles ">
<template v-slot:suffix="{item,index}">
<BaseIcon
v-if="!isArticleCollect(item)"
class="collect"
@click="toggleArticleCollect(item)"
title="收藏" icon="ph:star"/>
<BaseIcon
v-else
class="fill"
@click="toggleArticleCollect(item)"
title="取消收藏" icon="ph:star-fill"/>
</template>
</ArticleList>
</div>
</Panel>
</div>
<EditSingleArticleModal
v-model="showEditArticle"
:article="editArticle"
@save="saveArticle"
/>
</div>
<div class="footer" :class="!settingStore.showToolbar && 'hide'">
<div class="bottom">
<ElProgress
class="flex-1"
:percentage="progress"
:stroke-width="8"
:show-text="false"/>
<div class="flex justify-between items-center">
<div class="stat">
<div class="row">
<div class="num">{{ speedMinute }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
</div>
<div class="row">
<div class="num">{{ statisticsStore.total }}</div>
<div class="line"></div>
<div class="name">单词总数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
<div class="line"></div>
<div class="name">输入数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.wrong, '', 0) }}</div>
<div class="line"></div>
<div class="name">错误数</div>
</div>
</div>
<div class="flex flex-col items-center justify-center gap-1">
<audio ref="audioRef" v-if="articleData.article.audioSrc" :src="articleData.article.audioSrc"
controls></audio>
<div class="flex gap-2 center">
<BaseIcon
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
icon="icon-park-outline:go-ahead"
@click="emit('over')"/>
<BaseIcon
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
icon="fluent:replay-16-filled"
@click="play"/>
<BaseIcon
@click="settingStore.dictation = !settingStore.dictation"
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
:icon="['majesticons:eye-off-line','mdi:eye-outline'][settingStore.dictation?0:1]"/>
<BaseIcon :icon="['mdi:translate','mdi:translate-off'][settingStore.translate?0:1]"
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
@click="settingStore.translate = !settingStore.translate"/>
<!-- <BaseIcon-->
<!-- :title="`编辑(${settingStore.shortcutKeyMap[ShortcutKey.EditArticle]})`"-->
<!-- icon="tabler:edit"-->
<!-- @click="emitter.emit(ShortcutKey.EditArticle)"-->
<!-- />-->
<BaseIcon
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
icon="tdesign:menu-unfold"/>
</div>
</div>
</div>
</div>
<div class="progress">
<ElProgress :percentage="progress"
:stroke-width="8"
:show-text="false"/>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.practice-wrapper {
font-size: 0.9rem;
width: 100%;
height: 100vh;
display: flex;
overflow: hidden;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.swiper-wrapper {
height: 100%;
overflow: hidden;
.swiper-list {
transition: transform .3s;
height: 200%;
.swiper-item {
height: 50%;
overflow: auto;
display: flex;
justify-content: center;
}
}
.step1 {
transform: translate3d(0, -50%, 0);
}
}
.practice-article {
flex: 1;
overflow: hidden;
width: var(--article-width);
}
.typing-word-wrapper {
width: var(--toolbar-width);
}
.panel-wrapper {
position: absolute;
left: var(--article-panel-margin-left);
//left: 0;
top: .8rem;
z-index: 1;
height: calc(100% - 1.5rem);
}
.footer {
width: var(--article-width);
margin-bottom: .8rem;
transition: all var(--anim-time);
position: relative;
margin-top: 1rem;
&.hide {
margin-bottom: -6rem;
margin-top: 3rem;
.progress {
bottom: calc(100% + 1.8rem);
}
}
.bottom {
position: relative;
width: 100%;
box-sizing: border-box;
border-radius: .6rem;
background: var(--color-second);
padding: .5rem var(--space);
z-index: 2;
border: 1px solid var(--color-item-border);
box-shadow: var(--shadow);
.stat {
margin-top: .5rem;
display: flex;
justify-content: space-around;
gap: var(--stat-gap);
.row {
display: flex;
flex-direction: column;
align-items: center;
gap: .3rem;
width: 5rem;
color: gray;
.line {
height: 1px;
width: 100%;
background: var(--color-sub-gray);
}
}
}
}
.progress {
width: 100%;
transition: all .3s;
padding: 0 .6rem;
box-sizing: border-box;
position: absolute;
bottom: 0;
}
:deep(.ElProgress-bar__inner) {
background: var(--color-scrollbar);
}
}
</style>

View File

@@ -18,7 +18,7 @@ const emit = defineEmits<{
</script>
<template>
<div class="grid grid-cols-6 gap-4 ">
<div class="flex gap-4 flex-wrap">
<Book v-for="(dict,index) in list"
:is-add="false"
@click="emit('selectDict',{dict,index})"

View File

@@ -18,7 +18,7 @@ import Panel from "@/pages/pc/components/Panel.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import Tooltip from "@/pages/pc/components/Tooltip.vue";
import WordList from "@/pages/pc/components/list/WordList.vue";
import Type from "@/pages/pc/word/components/Type.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";
@@ -378,7 +378,7 @@ useEvents([
<Icon class="arrow" icon="bi:arrow-right" width="22"/>
</div>
</div>
<Type
<TypeWord
ref="typingRef"
:word="word"
@wrong="onTypeWrong"

View File

@@ -203,7 +203,7 @@ const progressTextRight = $computed(() => {
<div class="color-blue cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
</div>
</div>
<div class="grid grid-cols-6 gap-4 mt-4">
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)" :show-checkbox="isMultiple && j >= 3"
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)"/>

View File

@@ -29,18 +29,6 @@ function format(val: number, suffix: string = '', check: number = -1) {
return val === check ? '-' : (val + suffix)
}
let speedMinute = $ref(0)
let timer = $ref(0)
onMounted(() => {
timer = setInterval(() => {
speedMinute = Math.floor((Date.now() - statisticsStore.startDate) / 1000 / 60)
}, 1000)
})
onUnmounted(() => {
timer && clearInterval(timer)
})
let studyData = inject<StudyData>('studyData')
const status = $computed(() => {

View File

@@ -1,5 +1,6 @@
import {Article, ArticleWord, Dict, DictType, Word} from "@/types/types.ts";
import {shallowReactive} from "vue";
import {cloneDeep} from "@/utils";
export function getDefaultWord(val: Partial<Word> = {}): Word {
return {
@@ -42,7 +43,7 @@ export function getDefaultArticle(val: Partial<Article> = {}): Article {
audioSrc: '',
lrcPosition: [],
questions: [],
...val
...cloneDeep(val)
}
}
@@ -64,7 +65,7 @@ export function getDefaultDict(val: Partial<Dict> = {}): Dict {
complete: false,
...val,
words: shallowReactive(val.words ?? []),
articles: shallowReactive(val.articles ?? []),
list: shallowReactive(val.articles ?? []),
statistics: shallowReactive(val.statistics ?? [])
}
}

View File

@@ -17,7 +17,6 @@ export const EventKey = {
write: 'write',
editDict: 'editDict',
openMyDictDialog: 'openMyDictDialog',
jumpSpecifiedChapter: 'jumpSpecifiedChapter',
}
export function useEvent(key: string, func: any) {