fix:fix toolbar hide
This commit is contained in:
@@ -15,16 +15,18 @@
|
||||
--color-scrollbar: rgb(147, 173, 227);
|
||||
--color-sub-gray: #c0bfbf;
|
||||
|
||||
--practice-wrapper-translateX: 1px;
|
||||
--article-width: 50vw;
|
||||
--article-toolbar-width: 50rem;
|
||||
--article-panel-width: 20rem;
|
||||
--article-panel-margin-left: calc(50% + var(--article-width) / 2 + 1rem);
|
||||
|
||||
--toolbar-width: 50rem;
|
||||
--panel-width: 24rem;
|
||||
|
||||
--space: 1rem;
|
||||
--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);
|
||||
--word-panel-margin-left: calc(50% + var(--toolbar-width) / 2 + 1rem);
|
||||
--anim-time: 0.5s;
|
||||
|
||||
--color-input-color: black;
|
||||
@@ -61,7 +63,7 @@
|
||||
--color-notice-bg: rgb(247, 247, 247);
|
||||
|
||||
|
||||
//修改element-ui的进度条底色
|
||||
//修改的进度条底色
|
||||
--color-progress-bar: #d1d5df !important;
|
||||
}
|
||||
|
||||
@@ -118,31 +120,25 @@ html.dark {
|
||||
@media (max-width: 1720px) {
|
||||
:root {
|
||||
--toolbar-width: 50vw;
|
||||
--article-width: 50vw;
|
||||
--panel-width: 20vw;
|
||||
--space: 0.5rem;
|
||||
}
|
||||
.footer {
|
||||
.bottom {
|
||||
padding: .5rem !important;
|
||||
}
|
||||
|
||||
--article-toolbar-width: 45rem;
|
||||
--article-panel-width: 18rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
:root {
|
||||
--panel-width: 20vw;
|
||||
--article-width: 50vw;
|
||||
--toolbar-width: 50vw;
|
||||
--stat-gap: 0.5rem;
|
||||
--space: 0.3rem;
|
||||
|
||||
--article-toolbar-width: 40rem;
|
||||
--article-panel-width: 16rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
.bottom {
|
||||
padding: .5rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.anim {
|
||||
|
||||
@@ -30,6 +30,7 @@ const LoadingComponent = {
|
||||
// 自定义指令
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
console.log('el',)
|
||||
const position = getComputedStyle(el).position
|
||||
if (position === 'static' || !position) {
|
||||
el.style.position = 'relative' // 保证 loading 居中
|
||||
|
||||
@@ -22,6 +22,7 @@ import Tooltip from "@/pages/pc/components/base/Tooltip.vue";
|
||||
import ConflictNotice from "@/pages/pc/components/ConflictNotice.vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import book_list from "@/assets/book-list.json";
|
||||
import PracticeLayout from "@/pages/pc/components/PracticeLayout.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -274,8 +275,10 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="practice-wrapper" v-loading="loading">
|
||||
<div class="practice-article">
|
||||
<PracticeLayout
|
||||
v-loading="loading"
|
||||
panelLeft="var(--article-panel-margin-left)">
|
||||
<template v-slot:practice>
|
||||
<TypingArticle
|
||||
ref="typingArticleRef"
|
||||
@edit="edit"
|
||||
@@ -285,179 +288,119 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
@play="e => playSentenceAudio(e,audioRef,articleData.article)"
|
||||
:article="articleData.article"
|
||||
/>
|
||||
|
||||
<div class="panel-wrapper">
|
||||
<Panel>
|
||||
<template v-slot:title>
|
||||
</template>
|
||||
<template v-slot:panel>
|
||||
<Panel :style="{width:'var(--article-panel-width)'}">
|
||||
<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}">
|
||||
</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
|
||||
:class="!isArticleCollect(item) ? 'collect' : 'fill'"
|
||||
@click.stop="toggleArticleCollect(item)"
|
||||
:title="!isArticleCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isArticleCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
</BaseIcon>
|
||||
</template>
|
||||
</ArticleList>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="footer">
|
||||
<Tooltip :title="settingStore.showToolbar?'收起':'展开'">
|
||||
<IconFluentChevronLeft20Filled
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
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
|
||||
:class="!isArticleCollect(item) ? 'collect' : 'fill'"
|
||||
@click.stop="toggleArticleCollect(item)"
|
||||
:title="!isArticleCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isArticleCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
@click="skip">
|
||||
<IconFluentArrowBounce20Regular class="transform-rotate-180"/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
@click="play">
|
||||
<IconFluentReplay20Regular/>
|
||||
</BaseIcon>
|
||||
</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?'收起':'展开'">
|
||||
<IconFluentChevronLeft20Filled
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
color="#999"/>
|
||||
</Tooltip>
|
||||
<BaseIcon
|
||||
@click="settingStore.dictation = !settingStore.dictation"
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
>
|
||||
<IconFluentEyeOff16Regular v-if="settingStore.dictation"/>
|
||||
<IconFluentEye16Regular v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<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]})`"
|
||||
@click="skip">
|
||||
<IconFluentArrowBounce20Regular class="transform-rotate-180"/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
@click="play">
|
||||
<IconFluentReplay20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
|
||||
@click="settingStore.translate = !settingStore.translate">
|
||||
<IconFluentTranslate16Regular v-if="settingStore.translate"/>
|
||||
<IconFluentTranslateOff16Regular v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
@click="settingStore.dictation = !settingStore.dictation"
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
>
|
||||
<IconFluentEyeOff16Regular v-if="settingStore.dictation"/>
|
||||
<IconFluentEye16Regular v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
|
||||
@click="settingStore.translate = !settingStore.translate">
|
||||
<IconFluentTranslate16Regular v-if="settingStore.translate"/>
|
||||
<IconFluentTranslateOff16Regular v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<!-- <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]})`">
|
||||
<IconFluentTextListAbcUppercaseLtr20Regular/>
|
||||
</BaseIcon>
|
||||
<!-- <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]})`">
|
||||
<IconFluentTextListAbcUppercaseLtr20Regular/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PracticeLayout>
|
||||
|
||||
<EditSingleArticleModal
|
||||
v-model="showEditArticle"
|
||||
:article="editArticle"
|
||||
@save="saveArticle"
|
||||
/>
|
||||
|
||||
<ConflictNotice/>
|
||||
</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: 1.6rem;
|
||||
|
||||
&.hide {
|
||||
margin-bottom: -6rem;
|
||||
margin-top: 3rem;
|
||||
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: relative;
|
||||
@@ -508,8 +451,5 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -482,15 +482,15 @@ let showQuestions = $ref(false)
|
||||
|
||||
.typing-article {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
color: var(--color-article);
|
||||
width: var(--article-width);
|
||||
font-size: 1.6rem;
|
||||
|
||||
header {
|
||||
word-wrap: break-word;
|
||||
position: relative;
|
||||
padding-top: 1rem;
|
||||
padding-top: 3rem;
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
@@ -549,7 +549,7 @@ let showQuestions = $ref(false)
|
||||
}
|
||||
|
||||
&.tall {
|
||||
line-height: 2.6;
|
||||
line-height: 2.4;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -589,7 +589,7 @@ let showQuestions = $ref(false)
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 1.2rem;
|
||||
line-height: 3.5;
|
||||
line-height: 3.2;
|
||||
letter-spacing: .2rem;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
|
||||
@@ -13,9 +13,11 @@ provide('tabIndex', computed(() => tabIndex))
|
||||
</script>
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div class="panel anim" v-show="settingStore.showPanel">
|
||||
<div class="panel anim" v-bind="$attrs" v-show="settingStore.showPanel">
|
||||
<header class="flex justify-between items-center py-3 px-space">
|
||||
<div class="color-main"><slot name="title"></slot></div>
|
||||
<div class="color-main">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<Tooltip
|
||||
:title="`关闭(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
|
||||
>
|
||||
@@ -37,8 +39,6 @@ provide('tabIndex', computed(() => tabIndex))
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all .3s;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
55
src/pages/pc/components/PracticeLayout.vue
Normal file
55
src/pages/pc/components/PracticeLayout.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
defineProps<{
|
||||
panelLeft: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center relative h-screen"
|
||||
:class="!settingStore.showToolbar && 'footer-hide'">
|
||||
<div class="wrap">
|
||||
<slot name="practice"></slot>
|
||||
</div>
|
||||
<div class="panel-wrap" :style="{left:panelLeft}">
|
||||
<slot name="panel"></slot>
|
||||
</div>
|
||||
<div class="footer-wrap">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.wrap {
|
||||
transition: all var(--anim-time);
|
||||
height: calc(100vh - 8rem);
|
||||
}
|
||||
|
||||
.footer-hide {
|
||||
.wrap {
|
||||
height: calc(100vh - 3rem) !important;
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
bottom: -6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
position: fixed;
|
||||
bottom: 0.8rem;
|
||||
transition: all var(--anim-time);
|
||||
}
|
||||
|
||||
.panel-wrap {
|
||||
position: absolute;
|
||||
top: .8rem;
|
||||
z-index: 1;
|
||||
height: calc(100vh - 1.8rem);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -25,6 +25,7 @@ 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";
|
||||
import PracticeLayout from "@/pages/pc/components/PracticeLayout.vue";
|
||||
|
||||
interface IProps {
|
||||
new: Word[],
|
||||
@@ -378,45 +379,42 @@ useEvents([
|
||||
</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"/>
|
||||
<PracticeLayout
|
||||
v-loading="loading"
|
||||
panelLeft="var(--word-panel-margin-left)">
|
||||
<template v-slot:practice>
|
||||
<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"
|
||||
/>
|
||||
</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">
|
||||
</template>
|
||||
<template v-slot:panel>
|
||||
<Panel>
|
||||
<template v-slot:title>
|
||||
<!-- <span>{{ store.sdict.name }} ({{ data.index + 1 }} / {{ data.words.length }})</span>-->
|
||||
@@ -462,8 +460,17 @@ useEvents([
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<Footer
|
||||
:is-simple="isWordSimple(word)"
|
||||
@toggle-simple="toggleWordSimpleWrapper"
|
||||
:is-collect="isWordCollect(word)"
|
||||
@toggle-collect="toggleWordCollect(word)"
|
||||
@skip="next(false)"
|
||||
/>
|
||||
</template>
|
||||
</PracticeLayout>
|
||||
<Statistics v-model="showStatDialog"/>
|
||||
<ConflictNotice/>
|
||||
</template>
|
||||
|
||||
499
src/pages/pc/word/StudyWord2.vue
Normal file
499
src/pages/pc/word/StudyWord2.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<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>
|
||||
@@ -60,13 +60,13 @@ const progress = $computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="footer" :class="!settingStore.showToolbar && 'hide'">
|
||||
<div class="footer">
|
||||
<Tooltip :title="settingStore.showToolbar?'收起':'展开'">
|
||||
<IconFluentChevronLeft20Filled
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
color="#999"/>
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
color="#999"/>
|
||||
</Tooltip>
|
||||
|
||||
<div class="bottom">
|
||||
@@ -75,7 +75,7 @@ const progress = $computed(() => {
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="stat gap-6">
|
||||
<div class="stat">
|
||||
<div class="row">
|
||||
<div class="num">{{ `${studyData.index}/${studyData.words.length}` }}</div>
|
||||
<div class="line"></div>
|
||||
@@ -156,16 +156,14 @@ const progress = $computed(() => {
|
||||
.footer {
|
||||
flex-shrink: 0;
|
||||
width: var(--toolbar-width);
|
||||
margin-bottom: .8rem;
|
||||
transition: all var(--anim-time);
|
||||
position: relative;
|
||||
margin-top: 1.6rem;
|
||||
|
||||
&.hide {
|
||||
margin-bottom: -6rem;
|
||||
margin-top: 3rem;
|
||||
|
||||
.progress-wrap {
|
||||
|
||||
bottom: calc(100% + 1.8rem);
|
||||
}
|
||||
}
|
||||
@@ -185,6 +183,7 @@ const progress = $computed(() => {
|
||||
margin-top: .5rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: var(--stat-gap);
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
@@ -204,12 +203,12 @@ const progress = $computed(() => {
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
width: 100%;
|
||||
width: var(--toolbar-width);
|
||||
transition: all .3s;
|
||||
padding: 0 .6rem;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
||||
Reference in New Issue
Block a user