Feature: Added status progress bar

This commit is contained in:
Zyronon
2025-12-29 19:54:34 +08:00
committed by GitHub
parent 0997a4f7f1
commit 8b1d4fe34d
16 changed files with 804 additions and 529 deletions

View File

@@ -1,7 +1,7 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5",
"arrowParens": "avoid",

View File

@@ -59,8 +59,7 @@
--color-main-text: rgb(91, 91, 91);
--color-select-bg: rgb(12, 140, 233);
//修改的进度条底色
--color-progress-bar: #d1d5df !important;
--color-progress-bar: #d1d5df;
--color-label-bg: whitesmoke;
--color-link: #2563eb;

View File

@@ -1,10 +1,9 @@
<script setup lang="ts">
import {Origin} from "@/config/env.ts";
import BaseButton from "@/components/BaseButton.vue";
import {set} from 'idb-keyval'
import {defineAsyncComponent} from "vue";
import Toast from "@/components/base/toast/Toast.ts";
import { Origin } from "@/config/env.ts";
import { set } from 'idb-keyval';
import { defineAsyncComponent } from "vue";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -111,4 +110,4 @@ async function transfer() {
<style scoped lang="scss">
</style>
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="flex gap-5 w-full h-4">
<template v-for="i of props.stages">
<template v-if="i?.children?.length && i.active">
<div class="flex gap-1 h-4" :style="{ width: i.ratio + '%' }">
<template v-for="j of i.children">
<Tooltip :title="j.name">
<Progress
:style="{ width: j.ratio + '%' }"
:percentage="j.percentage"
:stroke-width="8"
:color="j.active ? '#72c240' : '#69b1ff'"
:active="j.active"
:show-text="false"
/>
</Tooltip>
</template>
</div>
</template>
<template v-else>
<Tooltip :title="i.name">
<Progress
:style="{ width: i.ratio + '%' }"
:percentage="i.percentage"
:stroke-width="8"
:color="i.active && props.stages.length > 1 ? '#72c240' : '#69b1ff'"
:active="i.active"
:show-text="false"
/>
</Tooltip>
</template>
</template>
</div>
</template>
<script setup lang="ts">
import Tooltip from '@/components/base/Tooltip.vue'
import Progress from '@/components/base/Progress.vue'
const props = defineProps<{
stages: {
name: string
active?: boolean
percentage: number
ratio: number
children: {
active: boolean
name: string
percentage: number
ratio: number
}[]
}[]
}>()
</script>
<style scoped lang="scss"></style>

View File

@@ -7,6 +7,7 @@ interface IProps {
textInside?: boolean;
strokeWidth?: number;
color?: string;
active?: boolean;
format?: (percentage: number) => string;
size?: 'normal' | 'large';
}
@@ -16,6 +17,7 @@ const props = withDefaults(defineProps<IProps>(), {
textInside: false,
strokeWidth: 6,
color: '#409eff',
active: true,
format: (percentage) => `${percentage}%`,
size: 'normal',
});
@@ -31,6 +33,7 @@ const trackStyle = computed(() => {
const height = props.size === 'large' ? props.strokeWidth * 2.5 : props.strokeWidth;
return {
height: `${height}px`,
opacity: props.active ? 1 : 0.4,
};
});

View File

@@ -29,7 +29,7 @@ const emit = defineEmits<{
}>()
//虚拟列表长度限制
const limit = 101
const limit = 200
const settingStore = useSettingStore()
const listRef: any = $ref()

View File

@@ -1,7 +1,19 @@
<script setup lang="ts">
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import BasePage from "@/components/BasePage.vue";
import { myDictList } from '@/apis'
import Progress from '@/components/base/Progress.vue'
import Toast from '@/components/base/toast/Toast.ts'
import BaseButton from '@/components/BaseButton.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import BasePage from '@/components/BasePage.vue'
import Book from '@/components/Book.vue'
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
import PopConfirm from '@/components/PopConfirm.vue'
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { useBaseStore } from '@/stores/base.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultDict } from '@/types/func.ts'
import { DictResource, DictType } from '@/types/types.ts'
import {
_getDictDataByUrl,
_nextTick,
@@ -10,32 +22,20 @@ import {
msToHourMinute,
resourceWrap,
total,
useNav
} from "@/utils";
import { DictResource, DictType } from "@/types/types.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Book from "@/components/Book.vue";
import Progress from '@/components/base/Progress.vue';
import Toast from '@/components/base/toast/Toast.ts'
import BaseButton from "@/components/BaseButton.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import { watch } from "vue";
import { getDefaultDict } from "@/types/func.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
useNav,
} from '@/utils'
import { getPracticeArticleCache } from '@/utils/cache.ts'
import { useFetch } from '@vueuse/core'
import dayjs from 'dayjs'
import isBetween from 'dayjs/plugin/isBetween'
import isoWeek from 'dayjs/plugin/isoWeek'
import { useFetch } from "@vueuse/core";
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, TourConfig } from "@/config/env.ts";
import { myDictList } from "@/apis";
import { useSettingStore } from "@/stores/setting.ts";
import {PRACTICE_ARTICLE_CACHE} from "@/utils/cache.ts";
import { watch } from 'vue'
import { useRouter } from 'vue-router'
dayjs.extend(isoWeek)
dayjs.extend(isBetween);
dayjs.extend(isBetween)
const {nav} = useNav()
const { nav } = useNav()
const base = useBaseStore()
const store = useBaseStore()
const settingStore = useSettingStore()
@@ -43,76 +43,73 @@ const router = useRouter()
const runtimeStore = useRuntimeStore()
let isSaveData = $ref(false)
watch(() => store.load, n => {
if (n) init()
}, {immediate: true})
watch(
() => store.load,
n => {
if (n) init()
},
{ immediate: true }
)
async function init() {
if (AppEnv.CAN_REQUEST) {
let res = await myDictList({type: "article"})
let res = await myDictList({ type: 'article' })
if (res.success) {
store.setState(Object.assign(store.$state, res.data))
}
}
if (store.article.studyIndex >= 1) {
if (!store.sbook.custom && !store.sbook.articles.length) {
store.article.bookList[store.article.studyIndex] = await _getDictDataByUrl(store.sbook, DictType.article)
store.article.bookList[store.article.studyIndex] = await _getDictDataByUrl(
store.sbook,
DictType.article
)
}
}
let d = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
let d = getPracticeArticleCache()
if (d) {
try {
let obj = JSON.parse(d)
let data = obj.val
//如果全是0说明未进行练习直接重置
if (
data.practiceData.sectionIndex === 0 &&
data.practiceData.sentenceIndex === 0 &&
data.practiceData.wordIndex === 0
) {
throw new Error()
}
isSaveData = true
} catch (e) {
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
}
isSaveData = true
}
}
watch(() => store?.sbook?.id, (n) => {
console.log('n', n)
if (!n) {
_nextTick(async () => {
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD);
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step7',
text: '点击这里选择一本书籍开始学习,步骤前面选词典相同,让我们跳过中间步骤,直接开始练习吧',
attachTo: {
element: '#no-book',
on: 'bottom'
},
buttons: [
{
text: `下一步7/${TourConfig.total}`,
action() {
tour.next()
nav('/practice-articles/article_nce2', {guide: 1})
}
}
]
});
watch(
() => store?.sbook?.id,
n => {
console.log('n', n)
if (!n) {
_nextTick(async () => {
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD)
const tour = new Shepherd.Tour(TourConfig)
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1')
})
tour.addStep({
id: 'step7',
text: '点击这里选择一本书籍开始学习,步骤前面选词典相同,让我们跳过中间步骤,直接开始练习吧',
attachTo: {
element: '#no-book',
on: 'bottom',
},
buttons: [
{
text: `下一步7/${TourConfig.total}`,
action() {
tour.next()
nav('/practice-articles/article_nce2', { guide: 1 })
},
},
],
})
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r && !isMobile()) {
tour.start();
}
}, 500)
}
}, {immediate: true})
const r = localStorage.getItem('tour-guide')
if (settingStore.first && !r && !isMobile()) {
tour.start()
}
}, 500)
}
},
{ immediate: true }
)
function startStudy() {
// console.log(store.sbook.articles[1])
@@ -126,7 +123,7 @@ function startStudy() {
name: base.sbook.name,
custom: base.sbook.custom,
complete: base.sbook.complete,
s:`name:${base.sbook.name},index:${base.sbook.lastLearnIndex},title:${base.sbook.articles[base.sbook.lastLearnIndex].title}`,
s: `name:${base.sbook.name},index:${base.sbook.lastLearnIndex},title:${base.sbook.articles[base.sbook.lastLearnIndex].title}`,
})
nav('/practice-articles/' + store.sbook.id)
} else {
@@ -152,7 +149,7 @@ function handleBatchDel() {
}
})
selectIds = []
Toast.success("删除成功!")
Toast.success('删除成功!')
}
function toggleSelect(item) {
@@ -177,7 +174,12 @@ const totalSpend = $computed(() => {
})
const todayTotalSpend = $computed(() => {
if (base.sbook.statistics?.length) {
return msToHourMinute(total(base.sbook.statistics.filter(v => dayjs(v.startDate).isSame(dayjs(), 'day')), 'spend'))
return msToHourMinute(
total(
base.sbook.statistics.filter(v => dayjs(v.startDate).isSame(dayjs(), 'day')),
'spend'
)
)
}
return 0
})
@@ -190,40 +192,42 @@ const totalDay = $computed(() => {
})
const weekList = $computed(() => {
const list = Array(7).fill(false);
const list = Array(7).fill(false)
// 获取本周的起止时间
const startOfWeek = dayjs().startOf('isoWeek'); // 周一
const endOfWeek = dayjs().endOf('isoWeek'); // 周日
const startOfWeek = dayjs().startOf('isoWeek') // 周一
const endOfWeek = dayjs().endOf('isoWeek') // 周日
store.sbook.statistics?.forEach(item => {
const date = dayjs(item.startDate);
const date = dayjs(item.startDate)
if (date.isBetween(startOfWeek, endOfWeek, null, '[]')) {
let idx = date.day();
let idx = date.day()
// dayjs().day() 0=周日, 1=周一, ..., 6=周六
// 需要转换为 0=周一, ..., 6=周日
if (idx === 0) {
idx = 6; // 周日放到最后
idx = 6 // 周日放到最后
} else {
idx = idx - 1; // 其余前移一位
idx = idx - 1 // 其余前移一位
}
list[idx] = true;
list[idx] = true
}
});
})
return list
})
const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.ARTICLE.RECOMMENDED)).json()
const { data: recommendBookList, isFetching } = useFetch(
resourceWrap(DICT_LIST.ARTICLE.RECOMMENDED)
).json()
let isNewHost = $ref(window.location.host === Host)
</script>
<template>
<BasePage>
<div class="mb-4" v-if="!isNewHost">
新域名已启用后续请访问 <a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前
2study.top 域名将在不久后停止使用
新域名已启用后续请访问
<a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前 2study.top
域名将在不久后停止使用
</div>
<div class="card flex flex-col md:flex-row justify-between gap-space p-4 md:p-6">
@@ -234,10 +238,9 @@ let isNewHost = $ref(window.location.host === Host)
quantifier="篇"
:item="base.sbook"
:show-progress="false"
@click="goBookDetail(base.sbook)"/>
<Book v-else
:is-add="true"
@click="router.push('/book-list')"/>
@click="goBookDetail(base.sbook)"
/>
<Book v-else :is-add="true" @click="router.push('/book-list')" />
</div>
<div class="flex-1">
<div class="flex justify-between items-start">
@@ -249,7 +252,8 @@ let isNewHost = $ref(window.location.host === Host)
:class="item ? 'bg-[#409eff] color-white' : 'bg-gray-200'"
v-for="(item, i) in weekList"
:key="i"
>{{ i + 1 }}
>
{{ i + 1 }}
</div>
</div>
</div>
@@ -259,67 +263,92 @@ let isNewHost = $ref(window.location.host === Host)
</div>
<div class="flex flex-col sm:flex-row gap-3 items-center mt-3 gap-space w-full">
<div
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200"
>
<div class="text-[#409eff] text-xl font-bold">{{ todayTotalSpend }}</div>
<div class="text-gray-500">今日学习时长</div>
</div>
<div
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200"
>
<div class="text-[#409eff] text-xl font-bold">{{ totalDay }}</div>
<div class="text-gray-500">总学习天数</div>
</div>
<div
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200"
>
<div class="text-[#409eff] text-xl font-bold">{{ totalSpend }}</div>
<div class="text-gray-500">总学习时长</div>
</div>
</div>
<div class="flex gap-3 mt-3">
<Progress class="w-full md:w-auto"
size="large"
:percentage="base.currentBookProgress"
:format="()=> `${ base.sbook?.lastLearnIndex || 0 }/${base.sbook?.length || 0}篇`"
:show-text="true"></Progress>
<Progress
class="w-full md:w-auto"
size="large"
:percentage="base.currentBookProgress"
:format="() => `${base.sbook?.lastLearnIndex || 0}/${base.sbook?.length || 0}篇`"
:show-text="true"
></Progress>
<BaseButton size="large" class="w-full md:w-auto"
@click="startStudy"
:disabled="!base.sbook.name">
<BaseButton
size="large"
class="w-full md:w-auto"
@click="startStudy"
:disabled="!base.sbook.name"
>
<div class="flex items-center gap-2 justify-center w-full">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
<IconFluentArrowCircleRight16Regular class="text-xl" />
</div>
</BaseButton>
</div>
</div>
</div>
<div class="card flex flex-col">
<div class="card flex flex-col">
<div class="flex justify-between">
<div class="title">我的书籍</div>
<div class="flex gap-4 items-center">
<PopConfirm title="确认删除所有选中书籍?" @confirm="handleBatchDel" v-if="selectIds.length">
<PopConfirm
title="确认删除所有选中书籍?"
@confirm="handleBatchDel"
v-if="selectIds.length"
>
<BaseIcon class="del" title="删除">
<DeleteIcon/>
<DeleteIcon />
</BaseIcon>
</PopConfirm>
<div class="color-link cursor-pointer" v-if="base.article.bookList.length > 1"
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理书籍' }}
<div
class="color-link cursor-pointer"
v-if="base.article.bookList.length > 1"
@click="
() => {
isMultiple = !isMultiple
selectIds = []
}
"
>
{{ isMultiple ? '取消' : '管理书籍' }}
</div>
<div class="color-link cursor-pointer" @click="nav('book-detail', { isAdd: true })">
创建个人书籍
</div>
<div class="color-link cursor-pointer" @click="nav('book-detail', { isAdd: true })">创建个人书籍</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false"
:is-user="true"
quantifier=""
:item="item"
:checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)"
:show-checkbox="isMultiple && j >= 1"
v-for="(item, j) in base.article.bookList"
@click="goBookDetail(item)"/>
<Book :is-add="true" @click="router.push('/book-list')"/>
<Book
:is-add="false"
:is-user="true"
quantifier=""
:item="item"
:checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)"
:show-checkbox="isMultiple && j >= 1"
v-for="(item, j) in base.article.bookList"
@click="goBookDetail(item)"
/>
<Book :is-add="true" @click="router.push('/book-list')" />
</div>
</div>
@@ -331,11 +360,14 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false"
quantifier=""
:item="item as any"
v-for="(item, j) in recommendBookList" @click="goBookDetail(item as any)"/>
<div class="flex gap-4 flex-wrap mt-4">
<Book
:is-add="false"
quantifier=""
:item="item as any"
v-for="(item, j) in recommendBookList"
@click="goBookDetail(item as any)"
/>
</div>
</div>
</BasePage>

View File

@@ -1,8 +1,30 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, provide, watch } from 'vue'
import { addStat, setUserDictProp } from '@/apis'
import Toast from '@/components/base/toast/Toast.ts'
import Tooltip from '@/components/base/Tooltip.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import ConflictNotice from '@/components/ConflictNotice.vue'
import ArticleList from '@/components/list/ArticleList.vue'
import Panel from '@/components/Panel.vue'
import PracticeLayout from '@/components/PracticeLayout.vue'
import SettingDialog from '@/components/setting/SettingDialog.vue'
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { genArticleSectionData, usePlaySentenceAudio } from '@/hooks/article.ts'
import { useArticleOptions } from '@/hooks/dict.ts'
import {
useDisableEventListener,
useOnKeyboardEventListener,
useStartKeyboardEventListener,
} from '@/hooks/event.ts'
import useTheme from '@/hooks/theme.ts'
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
import EditSingleArticleModal from '@/pages/article/components/EditSingleArticleModal.vue'
import TypingArticle from '@/pages/article/components/TypingArticle.vue'
import { useBaseStore } from '@/stores/base.ts'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { usePracticeStore } from '@/stores/practice.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultArticle, getDefaultDict, getDefaultWord } from '@/types/func.ts'
import {
Article,
ArticleItem,
@@ -14,13 +36,6 @@ import {
Statistics,
Word,
} from '@/types/types.ts'
import {
useDisableEventListener,
useOnKeyboardEventListener,
useStartKeyboardEventListener,
} from '@/hooks/event.ts'
import useTheme from '@/hooks/theme.ts'
import Toast from '@/components/base/toast/Toast.ts'
import {
_getDictDataByUrl,
_nextTick,
@@ -31,25 +46,10 @@ import {
resourceWrap,
total,
} from '@/utils'
import { usePracticeStore } from '@/stores/practice.ts'
import { useArticleOptions } from '@/hooks/dict.ts'
import { genArticleSectionData, usePlaySentenceAudio } from '@/hooks/article.ts'
import { getDefaultArticle, getDefaultDict, getDefaultWord } from '@/types/func.ts'
import TypingArticle from '@/pages/article/components/TypingArticle.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import Panel from '@/components/Panel.vue'
import ArticleList from '@/components/list/ArticleList.vue'
import EditSingleArticleModal from '@/pages/article/components/EditSingleArticleModal.vue'
import Tooltip from '@/components/base/Tooltip.vue'
import ConflictNotice from '@/components/ConflictNotice.vue'
import { getPracticeArticleCache, setPracticeArticleCache } from '@/utils/cache.ts'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { computed, onMounted, onUnmounted, provide, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PracticeLayout from '@/components/PracticeLayout.vue'
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { addStat, setUserDictProp } from '@/apis'
import { useRuntimeStore } from '@/stores/runtime.ts'
import SettingDialog from '@/components/setting/SettingDialog.vue'
import { PRACTICE_ARTICLE_CACHE } from '@/utils/cache.ts'
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
@@ -70,6 +70,7 @@ let editArticle = $ref<Article>(getDefaultArticle())
let audioRef = $ref<HTMLAudioElement>()
let timer = $ref(0)
let isFocus = true
let isTyped = $ref(false)
function write() {
// console.log('write')
@@ -107,6 +108,7 @@ function toggleConciseMode() {
}
function next() {
setPracticeArticleCache(null)
if (store.sbook.lastLearnIndex >= articleData.list.length - 1) {
store.sbook.complete = true
store.sbook.lastLearnIndex = 0
@@ -228,7 +230,6 @@ onMounted(() => {
} else {
loading = true
}
if (route.query.guide) {
showConflictNotice = false
} else {
@@ -238,61 +239,18 @@ onMounted(() => {
onUnmounted(() => {
runtimeStore.disableEventListener = false
let cache = getPracticeArticleCache()
//如果有缓存,则更新花费的时间;因为用户不输入不会保存数据
if (cache) {
cache.statStoreData.spend = statStore.spend
setPracticeArticleCache(cache)
}
clearInterval(timer)
savePracticeData(true, false)
})
useStartKeyboardEventListener()
useDisableEventListener(() => loading)
function savePracticeData(init = true, regenerate = true) {
let d = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
if (d) {
try {
let obj = JSON.parse(d)
if (obj.val.practiceData.id !== articleData.article.id) {
throw new Error()
}
if (init) {
let data = obj.val
//如果全是0说明未进行练习直接重置
if (
data.practiceData.sectionIndex === 0 &&
data.practiceData.sentenceIndex === 0 &&
data.practiceData.wordIndex === 0
) {
throw new Error()
}
//初始化时spend为0把本地保存的值设置给statStore里面再保存保持一致。不然每次进来都是0
statStore.$patch(data.statStoreData)
}
obj.val.statStoreData = statStore.$state
localStorage.setItem(PRACTICE_ARTICLE_CACHE.key, JSON.stringify(obj))
} catch (e) {
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
regenerate && savePracticeData()
}
} else {
localStorage.setItem(
PRACTICE_ARTICLE_CACHE.key,
JSON.stringify({
version: PRACTICE_ARTICLE_CACHE.version,
val: {
practiceData: {
sectionIndex: 0,
sentenceIndex: 0,
wordIndex: 0,
stringIndex: 0,
id: articleData.article.id,
},
statStoreData: statStore.$state,
},
})
)
}
}
function setArticle(val: Article) {
statStore.wrong = 0
statStore.total = 0
@@ -312,12 +270,11 @@ function setArticle(val: Article) {
})
})
savePracticeData()
isTyped = false
clearInterval(timer)
timer = setInterval(() => {
if (isFocus) {
statStore.spend += 1000
savePracticeData(false)
}
}, 1000)
@@ -326,8 +283,9 @@ function setArticle(val: Article) {
async function complete() {
clearInterval(timer)
//延时删除缓存,因为可能还有输入,需要保存
setTimeout(() => {
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
setPracticeArticleCache(null)
}, 1500)
//todo 有空了改成实时保存
@@ -432,6 +390,7 @@ function nextWord(word: ArticleWord) {
}
async function changeArticle(val: ArticleItem) {
setPracticeArticleCache(null)
let rIndex = articleData.list.findIndex(v => v.id === val.item.id)
if (rIndex > -1) {
store.sbook.lastLearnIndex = rIndex
@@ -585,7 +544,8 @@ provide('currentPractice', currentPractice)
<div class="name">记录</div>
</div>
<div class="row">
<div class="num">{{ Math.floor(statStore.spend / 1000 / 60) }}分钟</div>
<div class="num">{{statStore.spend }}分钟</div>
<!-- <div class="num">{{ Math.floor(statStore.spend / 1000 / 60) }}分钟</div> -->
<div class="line"></div>
<div class="name">时间</div>
</div>

View File

@@ -1,32 +1,39 @@
<script setup lang="ts">
import {inject, onMounted, onUnmounted, watch} from "vue"
import {Article, ArticleWord, PracticeArticleWordType, Sentence, ShortcutKey, Word} from "@/types/types.ts";
import {useBaseStore} from "@/stores/base.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {usePlayBeep, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {_dateFormat, _nextTick, isMobile, msToHourMinute, total} from "@/utils";
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
import ContextMenu from '@imengyu/vue3-context-menu'
import BaseButton from "@/components/BaseButton.vue";
import QuestionForm from "@/pages/article/components/QuestionForm.vue";
import {getDefaultArticle, getDefaultWord} from "@/types/func.ts";
import Toast from '@/components/base/toast/Toast.ts'
import TypingWord from "@/pages/article/components/TypingWord.vue";
import Space from "@/pages/article/components/Space.vue";
import {useWordOptions} from "@/hooks/dict.ts";
import nlp from "compromise/three";
import {nanoid} from "nanoid";
import {usePracticeStore} from "@/stores/practice.ts";
import BaseButton from '@/components/BaseButton.vue'
import { useWordOptions } from '@/hooks/dict.ts'
import { usePlayBeep, usePlayKeyboardAudio, usePlayWordAudio } from '@/hooks/sound.ts'
import QuestionForm from '@/pages/article/components/QuestionForm.vue'
import Space from '@/pages/article/components/Space.vue'
import TypingWord from '@/pages/article/components/TypingWord.vue'
import { useBaseStore } from '@/stores/base.ts'
import { usePracticeStore } from '@/stores/practice.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultArticle, getDefaultWord } from '@/types/func.ts'
import {
Article,
ArticleWord,
PracticeArticleWordType,
Sentence,
ShortcutKey,
Word,
} from '@/types/types.ts'
import { _dateFormat, _nextTick, isMobile, msToHourMinute, total } from '@/utils'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import ContextMenu from '@imengyu/vue3-context-menu'
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
import nlp from 'compromise/three'
import { nanoid } from 'nanoid'
import { inject, onMounted, onUnmounted, watch } from 'vue'
import {PRACTICE_ARTICLE_CACHE} from "@/utils/cache.ts";
import { getPracticeArticleCache, setPracticeArticleCache } from '@/utils/cache.ts'
interface IProps {
article: Article,
sectionIndex?: number,
sentenceIndex?: number,
wordIndex?: number,
stringIndex?: number,
article: Article
sectionIndex?: number
sentenceIndex?: number
wordIndex?: number
stringIndex?: number
}
const props = withDefaults(defineProps<IProps>(), {
@@ -38,16 +45,18 @@ const props = withDefaults(defineProps<IProps>(), {
})
const emit = defineEmits<{
ignore: [],
wrong: [val: Word],
play: [val: {
sentence: Sentence,
handle: boolean
}],
nextWord: [val: ArticleWord],
complete: [],
next: [],
replay: [],
ignore: []
wrong: [val: Word]
play: [
val: {
sentence: Sentence
handle: boolean
},
]
nextWord: [val: ArticleWord]
complete: []
next: []
replay: []
}>()
let typeArticleRef = $ref<HTMLInputElement>()
@@ -80,62 +89,63 @@ const playBeep = usePlayBeep()
const playKeyboardAudio = usePlayKeyboardAudio()
const playWordAudio = usePlayWordAudio()
const {
toggleWordCollect,
} = useWordOptions()
const { toggleWordCollect } = useWordOptions()
const store = useBaseStore()
const settingStore = useSettingStore()
const statStore = usePracticeStore()
const isMob = isMobile()
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c,]) => {
localStorage.setItem(PRACTICE_ARTICLE_CACHE.key, JSON.stringify({
version: PRACTICE_ARTICLE_CACHE.version,
val: {
practiceData: {
sectionIndex,
sentenceIndex,
wordIndex,
stringIndex,
id: props.article.id
},
statStoreData: statStore.$state,
watch(
[() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex],
([a, b, c]) => {
if (a !== 0 || b !== 0 || c !== 0) {
setPracticeArticleCache({
practiceData: {
sectionIndex,
sentenceIndex,
wordIndex,
},
statStoreData: statStore.$state,
})
}
}))
checkCursorPosition(a, b, c)
})
checkCursorPosition(a, b, c)
}
)
// watch(() => props.article.id, init, {immediate: true})
watch(() => settingStore.translate, () => {
checkTranslateLocation().then(() => checkCursorPosition())
})
watch(() => isEnd, n => {
if (n) {
_nextTick(() => {
typeArticleRef?.scrollTo({top: typeArticleRef.scrollHeight, behavior: "smooth"})
})
} else {
typeArticleRef?.scrollTo({top: 0, behavior: "smooth"})
watch(
() => settingStore.translate,
() => {
checkTranslateLocation().then(() => checkCursorPosition())
}
})
)
watch(
() => isEnd,
n => {
if (n) {
_nextTick(() => {
typeArticleRef?.scrollTo({ top: typeArticleRef.scrollHeight, behavior: 'smooth' })
})
} else {
typeArticleRef?.scrollTo({ top: 0, behavior: 'smooth' })
}
}
)
function init() {
if (!props.article.id) return
isSpace = isEnd = false
let d = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
let d = getPracticeArticleCache()
if (d) {
try {
let obj = JSON.parse(d)
let data = obj.val
statStore.$patch(data.statStoreData)
jump(data.practiceData.sectionIndex, data.practiceData.sentenceIndex, data.practiceData.wordIndex)
} catch (e) {
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
init()
}
sectionIndex = d.practiceData.sectionIndex
sentenceIndex = d.practiceData.sentenceIndex
wordIndex = d.practiceData.wordIndex
jump(sectionIndex, sentenceIndex, wordIndex)
statStore.$patch(d.statStoreData)
} else {
wrong = input = ''
sectionIndex = 0
@@ -143,17 +153,17 @@ function init() {
wordIndex = 0
stringIndex = 0
//todo 这在直接修改不太合理
props.article.sections.map((v) => {
v.map((w) => {
props.article.sections.map(v => {
v.map(w => {
w.words.map(s => {
s.input = ''
})
})
})
typeArticleRef?.scrollTo({top: 0, behavior: "smooth"})
typeArticleRef?.scrollTo({ top: 0, behavior: 'smooth' })
}
_nextTick(() => {
emit('play', {sentence: props.article.sections[sectionIndex][sentenceIndex], handle: false})
emit('play', { sentence: props.article.sections[sectionIndex][sentenceIndex], handle: false })
if (isNameWord()) next()
})
checkTranslateLocation().then(() => checkCursorPosition())
@@ -164,26 +174,31 @@ function checkCursorPosition(a = sectionIndex, b = sentenceIndex, c = wordIndex)
// console.log('checkCursorPosition')
_nextTick(() => {
// 选中目标元素
const currentWord = document.querySelector(`.section:nth-of-type(${a + 1}) .sentence:nth-of-type(${b + 1}) .word:nth-of-type(${c + 1})`);
const currentWord = document.querySelector(
`.section:nth-of-type(${a + 1}) .sentence:nth-of-type(${b + 1}) .word:nth-of-type(${c + 1})`
)
if (currentWord) {
// 在 currentWord 内找 .word-end
const end = currentWord.querySelector('.word-end');
const end = currentWord.querySelector('.word-end')
if (end) {
// 获取 articleWrapper 的位置
const articleRect = articleWrapperRef.getBoundingClientRect();
const endRect = end.getBoundingClientRect();
const articleRect = articleWrapperRef.getBoundingClientRect()
const endRect = end.getBoundingClientRect()
//如果当前输入位置大于屏幕的0.7高度就滚动屏幕的1/3
if (endRect.y > window.innerHeight * 0.7) {
typeArticleRef?.scrollTo({top: typeArticleRef.scrollTop + window.innerHeight * 0.3, behavior: "smooth"})
typeArticleRef?.scrollTo({
top: typeArticleRef.scrollTop + window.innerHeight * 0.3,
behavior: 'smooth',
})
}
// 计算相对位置
cursor = {
top: endRect.top - articleRect.top,
left: endRect.left - articleRect.left,
};
}
}
}
},)
})
}
function checkTranslateLocation() {
@@ -228,10 +243,8 @@ function processMobileCharacter(char: string) {
const fakeEvent = {
key: char,
code,
preventDefault() {
},
stopPropagation() {
},
preventDefault() {},
stopPropagation() {},
} as unknown as KeyboardEvent
onTyping(fakeEvent)
}
@@ -255,19 +268,29 @@ function handleMobileBeforeInput(event: InputEvent) {
}
}
const normalize = (s: string) => s.toLowerCase().trim()
const namePatterns = $computed(() => {
return Array.from(new Set((props.article?.nameList ?? []).map(normalize).filter(Boolean).map(s => s.split(/\s+/).filter(Boolean)).flat().concat([
'Mr', 'Mrs', 'Ms', 'Dr', 'Miss',
].map(normalize))))
return Array.from(
new Set(
(props.article?.nameList ?? [])
.map(normalize)
.filter(Boolean)
.map(s => s.split(/\s+/).filter(Boolean))
.flat()
.concat(['Mr', 'Mrs', 'Ms', 'Dr', 'Miss'].map(normalize))
)
)
})
const isNameWord = () => {
let currentSection = props.article.sections[sectionIndex]
let currentSentence = currentSection[sentenceIndex]
let w: ArticleWord = currentSentence.words[wordIndex]
return w?.type === PracticeArticleWordType.Word && namePatterns.length > 0 && namePatterns.includes(normalize(w.word))
return (
w?.type === PracticeArticleWordType.Word &&
namePatterns.length > 0 &&
namePatterns.includes(normalize(w.word))
)
}
let isTyping = false
@@ -289,9 +312,9 @@ function nextSentence() {
// if (!store.allIgnoreWords.includes(currentWord.word.toLowerCase()) && currentWord.type === PracticeArticleWordType.Word) {
// statisticsStore.inputNumber++
// }
isSpace = false;
isSpace = false
input = wrong = ''
stringIndex = 0;
stringIndex = 0
wordIndex = 0
sentenceIndex++
if (!currentSection[sentenceIndex]) {
@@ -303,21 +326,20 @@ function nextSentence() {
emit('complete')
} else {
if (isNameWord()) next()
emit('play', {sentence: props.article.sections[sectionIndex][0], handle: false})
emit('play', { sentence: props.article.sections[sectionIndex][0], handle: false })
}
} else {
if (isNameWord()) next()
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
emit('play', { sentence: currentSection[sentenceIndex], handle: false })
}
lock = false
focusMobileInput()
}
const next = () => {
isSpace = false;
isSpace = false
input = wrong = ''
stringIndex = 0;
stringIndex = 0
let currentSection = props.article.sections[sectionIndex]
let currentSentence = currentSection[sentenceIndex]
@@ -325,18 +347,21 @@ const next = () => {
// 检查下一个单词是否存在
if (wordIndex + 1 < currentSentence.words.length) {
wordIndex++;
wordIndex++
currentWord = currentSentence.words[wordIndex]
//这里把未输入的单词补全因为删除时会用到input
currentSentence.words.slice(0, wordIndex).forEach((word, i) => {
word.input = word.input + word.word.slice(word.input?.length ?? 0)
})
if ([PracticeArticleWordType.Symbol, PracticeArticleWordType.Number].includes(currentWord.type) && settingStore.ignoreSymbol) {
if (
[PracticeArticleWordType.Symbol, PracticeArticleWordType.Number].includes(currentWord.type) &&
settingStore.ignoreSymbol
) {
next()
} else if (isNameWord()) {
next()
} else {
emit('nextWord', currentWord);
emit('nextWord', currentWord)
}
} else {
nextSentence()
@@ -346,8 +371,8 @@ const next = () => {
function onTyping(e: KeyboardEvent) {
debugger
if (!props.article.sections.length) return
if (isTyping || isEnd) return;
isTyping = true;
if (isTyping || isEnd) return
isTyping = true
// console.log('keyDown', e.key, e.code, e.keyCode)
try {
let currentSection = props.article.sections[sectionIndex]
@@ -371,7 +396,6 @@ function onTyping(e: KeyboardEvent) {
// }, 500)
}
} else {
// if (isNameWord(currentWord)) {
// isSpace = false
// next()
@@ -412,7 +436,7 @@ function onTyping(e: KeyboardEvent) {
e.preventDefault()
} catch (e) {
//todo 上报
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
setPracticeArticleCache(null)
init()
} finally {
isTyping = false
@@ -421,14 +445,14 @@ function onTyping(e: KeyboardEvent) {
function play() {
let currentSection = props.article.sections[sectionIndex]
emit('play', {sentence: currentSection[sentenceIndex], handle: true})
emit('play', { sentence: currentSection[sentenceIndex], handle: true })
}
function del() {
if (wrong) {
wrong = ''
} else {
if (isEnd) return;
if (isEnd) return
if (isSpace) {
isSpace = false
}
@@ -473,12 +497,16 @@ function del() {
}
}
function showSentence(i1: number = sectionIndex, i2: number = sentenceIndex, i3: number = wordIndex) {
hoverIndex = {sectionIndex: i1, sentenceIndex: i2, wordIndex: i3}
function showSentence(
i1: number = sectionIndex,
i2: number = sentenceIndex,
i3: number = wordIndex
) {
hoverIndex = { sectionIndex: i1, sentenceIndex: i2, wordIndex: i3 }
}
function hideSentence() {
hoverIndex = {sectionIndex: -1, sentenceIndex: -1, wordIndex: -1}
hoverIndex = { sectionIndex: -1, sentenceIndex: -1, wordIndex: -1 }
}
function jump(i, j, w, sentence?) {
@@ -501,22 +529,22 @@ function jump(i, j, w, sentence?) {
})
})
if (sentence) {
emit('play', {sentence: sentence, handle: false})
emit('play', { sentence: sentence, handle: false })
}
}
function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
const selectedText = window.getSelection().toString();
console.log(selectedText);
const selectedText = window.getSelection().toString()
console.log(selectedText)
//prevent the browser's default menu
e.preventDefault();
e.preventDefault()
//show your menu
ContextMenu.showContextMenu({
x: e.x,
y: e.y,
items: [
{
label: "收藏单词",
label: '收藏单词',
onClick: () => {
let word = props.article.sections[i][j].words[w]
let text = word.word
@@ -531,46 +559,46 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
}
if (!text.length) text = word.word
console.log('text', text)
toggleWordCollect(getDefaultWord({word: text, id: nanoid()}))
toggleWordCollect(getDefaultWord({ word: text, id: nanoid() }))
Toast.success(text + ' 添加成功')
}
},
},
{
label: "复制",
label: '复制',
children: [
{
label: "复制句子",
label: '复制句子',
onClick: () => {
navigator.clipboard.writeText(sentence.text).then(r => {
Toast.success('已复制')
})
}
},
},
{
label: "复制单词",
label: '复制单词',
onClick: () => {
let word = props.article.sections[i][j].words[w]
navigator.clipboard.writeText(word.word).then(r => {
Toast.success('已复制')
})
}
}
]
},
},
],
},
{
label: "从这开始",
label: '从这开始',
onClick: () => {
jump(i, j, w + 1, sentence)
}
},
},
{
label: "播放句子",
label: '播放句子',
onClick: () => {
emit('play', {sentence: sentence, handle: true})
}
emit('play', { sentence: sentence, handle: true })
},
},
{
label: "语法分析",
label: '语法分析',
onClick: () => {
navigator.clipboard.writeText(sentence.text).then(r => {
Toast.success('已复制!随后将打开语法分析网站!')
@@ -578,28 +606,28 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
window.open('https://enpuz.com/')
}, 1000)
})
}
},
},
{
label: "有道词典翻译",
label: '有道词典翻译',
children: [
{
label: "翻译单词",
label: '翻译单词',
onClick: () => {
let word = props.article.sections[i][j].words[w]
window.open(`https://www.youdao.com/result?word=${word.word}&lang=en`, '_blank')
}
},
},
{
label: "翻译句子",
label: '翻译句子',
onClick: () => {
window.open(`https://www.youdao.com/result?word=${sentence.text}&lang=en`, '_blank')
}
},
},
]
],
},
]
});
],
})
}
onMounted(() => {
@@ -613,7 +641,7 @@ onMounted(() => {
})
onUnmounted(() => {
emitter.off(EventKey.resetWord,)
emitter.off(EventKey.resetWord)
emitter.off(EventKey.onTyping, onTyping)
})
@@ -622,7 +650,22 @@ useEvents([
[ShortcutKey.UnknownWord, onTyping],
])
defineExpose({showSentence, play, del, hideSentence, nextSentence, init})
defineExpose({
showSentence,
play,
del,
hideSentence,
nextSentence,
init,
getIndex: () => {
return {
sectionIndex,
sentenceIndex,
wordIndex,
stringIndex,
}
},
})
function isCurrent(i: number, j: number, w: number) {
return `${i}${j}${w}` === currentIndex
@@ -631,7 +674,6 @@ function isCurrent(i: number, j: number, w: number) {
let showQuestions = $ref(false)
const currentPractice = inject('currentPractice', [])
</script>
<template>
@@ -649,82 +691,96 @@ const currentPractice = inject('currentPractice', [])
@input="handleMobileInput"
/>
<header class="mb-4">
<div class="title"><span class="font-family text-3xl">{{
store.sbook.lastLearnIndex + 1
}}. </span>{{ props.article?.title ?? '' }}
<div class="title">
<span class="font-family text-3xl">{{ store.sbook.lastLearnIndex + 1 }}. </span
>{{ props.article?.title ?? '' }}
</div>
<div class="titleTranslate" v-if="settingStore.translate">
{{ props.article.titleTranslate }}
</div>
<div class="titleTranslate" v-if="settingStore.translate">{{ props.article.titleTranslate }}</div>
</header>
<div id="article-content" class="article-content"
:class="[
settingStore.translate && 'tall',
settingStore.dictation && 'dictation',
]"
ref="articleWrapperRef">
<div
id="article-content"
class="article-content"
:class="[settingStore.translate && 'tall', settingStore.dictation && 'dictation']"
ref="articleWrapperRef"
>
<article>
<div class="section" v-for="(section,indexI) in props.article.sections">
<span class="sentence"
v-for="(sentence,indexJ) in section">
<span
v-for="(word,indexW) in sentence.words"
@contextmenu="e=>onContextMenu(e,sentence,indexI,indexJ,indexW)"
class="word"
:class="[(sectionIndex>indexI
?'wrote':
(sectionIndex>=indexI &&sentenceIndex>indexJ)
?'wrote' :
(sectionIndex>=indexI &&sentenceIndex>=indexJ && wordIndex>indexW)
?'wrote':
(sectionIndex>=indexI &&sentenceIndex>=indexJ && wordIndex>=indexW && stringIndex>=word.word.length)
?'wrote':
''),
indexW === 0 && `word${indexI}-${indexJ}`,
]">
<span class="word-wrap"
@mouseenter="settingStore.allowWordTip && showSentence(indexI,indexJ,indexW)"
@mouseleave="hideSentence"
:class="[
hoverIndex.sectionIndex === indexI && hoverIndex.sentenceIndex === indexJ && hoverIndex.wordIndex === indexW
&&'hover-show',
word.type === PracticeArticleWordType.Number && 'font-family text-xl'
]"
@click="playWordAudio(word.word)"
>
<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>
<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
class="sentence-translate-mobile"
v-if="isMob && settingStore.translate && sentence.translate">
{{ sentence.translate }}
</span>
</span>
<div class="section" v-for="(section, indexI) in props.article.sections">
<span class="sentence" v-for="(sentence, indexJ) in section">
<span
v-for="(word, indexW) in sentence.words"
@contextmenu="e => onContextMenu(e, sentence, indexI, indexJ, indexW)"
class="word"
:class="[
sectionIndex > indexI
? 'wrote'
: sectionIndex >= indexI && sentenceIndex > indexJ
? 'wrote'
: sectionIndex >= indexI && sentenceIndex >= indexJ && wordIndex > indexW
? 'wrote'
: sectionIndex >= indexI &&
sentenceIndex >= indexJ &&
wordIndex >= indexW &&
stringIndex >= word.word.length
? 'wrote'
: '',
indexW === 0 && `word${indexI}-${indexJ}`,
]"
>
<span
class="word-wrap"
@mouseenter="settingStore.allowWordTip && showSentence(indexI, indexJ, indexW)"
@mouseleave="hideSentence"
:class="[
hoverIndex.sectionIndex === indexI &&
hoverIndex.sentenceIndex === indexJ &&
hoverIndex.wordIndex === indexW &&
'hover-show',
word.type === PracticeArticleWordType.Number && 'font-family text-xl',
]"
@click="playWordAudio(word.word)"
>
<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>
<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
class="sentence-translate-mobile"
v-if="isMob && settingStore.translate && sentence.translate"
>
{{ sentence.translate }}
</span>
</span>
</div>
</article>
<div class="translate" v-show="settingStore.translate">
<template v-for="(v,indexI) in props.article.sections">
<div class="row"
:class="[
`translate${indexI+'-'+indexJ}`,
(sectionIndex>indexI
?'wrote':
(sectionIndex>=indexI &&sentenceIndex>indexJ)
?'wrote' :
''),
]"
v-for="(item,indexJ) in v">
<template v-for="(v, indexI) in props.article.sections">
<div
class="row"
:class="[
`translate${indexI + '-' + indexJ}`,
sectionIndex > indexI
? 'wrote'
: sectionIndex >= indexI && sentenceIndex > indexJ
? 'wrote'
: '',
]"
v-for="(item, indexJ) in v"
>
<span class="space"></span>
<Transition name="fade">
<span class="text" v-if="item.translate">{{ item.translate }}</span>
@@ -732,42 +788,49 @@ const currentPractice = inject('currentPractice', [])
</div>
</template>
</div>
<div class="cursor" v-if="!isEnd" :style="{top:cursor.top+'px',left:cursor.left+'px'}"></div>
<div
class="cursor"
v-if="!isEnd"
:style="{ top: cursor.top + 'px', left: cursor.left + 'px' }"
></div>
</div>
<div class="options flex justify-center" v-if="isEnd">
<BaseButton
@click="emit('replay')">重新练习
</BaseButton>
<BaseButton @click="emit('replay')">重新练习 </BaseButton>
<BaseButton
v-if="store.sbook.lastLearnIndex < store.sbook.articles.length - 1"
@click="emit('next')">下一篇
@click="emit('next')"
>下一篇
</BaseButton>
</div>
<div class="font-family text-base pr-2 mb-50 mt-10" v-if="currentPractice.length && isEnd">
<div class="text-2xl font-bold">学习记录</div>
<div class="mt-1 mb-3">总学习时长{{ msToHourMinute(total(currentPractice, 'spend')) }}</div>
<div class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
:class="i === currentPractice.length-1 && 'color-red!'"
v-for="(item,i) in currentPractice">
<span :class="i === currentPractice.length-1 ? 'color-red':'color-gray'"
>{{
i === currentPractice.length - 1 ? '当前' : i + 1
}}.&nbsp;&nbsp;{{ _dateFormat(item.startDate) }}</span>
<div
class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
:class="i === currentPractice.length - 1 && 'color-red!'"
v-for="(item, i) in currentPractice"
>
<span :class="i === currentPractice.length - 1 ? 'color-red' : 'color-gray'"
>{{ i === currentPractice.length - 1 ? '当前' : i + 1 }}.&nbsp;&nbsp;{{
_dateFormat(item.startDate)
}}</span
>
<span>{{ msToHourMinute(item.spend) }}</span>
</div>
</div>
<template v-if="false">
<div class="center">
<BaseButton @click="showQuestions =! showQuestions">显示题目</BaseButton>
<BaseButton @click="showQuestions = !showQuestions">显示题目</BaseButton>
</div>
<div class="toggle" v-if="showQuestions">
<QuestionForm :questions="article.questions"
:duration="300"
:immediateFeedback="false"
:randomize="true"
<QuestionForm
:questions="article.questions"
:duration="300"
:immediateFeedback="false"
:randomize="true"
/>
</div>
</template>
@@ -775,7 +838,6 @@ const currentPractice = inject('currentPractice', [])
</template>
<style scoped lang="scss">
.wrote {
color: grey;
}
@@ -826,7 +888,7 @@ $article-lh: 2.4;
.border-bottom {
display: inline-block !important;
}
.translate{
.translate {
color: var(--color-reverse-black);
}
}
@@ -843,7 +905,8 @@ $article-lh: 2.4;
white-space: pre-wrap;
font-family: var(--en-article-family);
.wrote, .hover-show {
.wrote,
.hover-show {
:deep(.hide) {
opacity: 1 !important;
}
@@ -867,7 +930,7 @@ $article-lh: 2.4;
margin-bottom: 1.5rem;
.sentence {
transition: all .3s;
transition: all 0.3s;
}
.word {
@@ -875,7 +938,7 @@ $article-lh: 2.4;
.word-wrap {
position: relative;
transition: background-color .3s;
transition: background-color 0.3s;
}
.border-bottom {
@@ -901,7 +964,7 @@ $article-lh: 2.4;
width: 100%;
font-size: 1.2rem;
line-height: $translate-lh;
letter-spacing: .2rem;
letter-spacing: 0.2rem;
font-family: var(--zh-article-family);
font-weight: bold;
color: #818181;
@@ -911,10 +974,10 @@ $article-lh: 2.4;
left: 0;
width: 100%;
opacity: 0;
transition: all .3s;
transition: all 0.3s;
.space {
transition: all .3s;
transition: all 0.3s;
display: inline-block;
}
}

View File

@@ -47,16 +47,12 @@ import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import ConflictNotice from '@/components/ConflictNotice.vue'
import PracticeLayout from '@/components/PracticeLayout.vue'
import { AppEnv, DICT_LIST, IS_DEV, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { ToastInstance } from '@/components/base/toast/type.ts'
import { watchOnce } from '@vueuse/core'
import { setUserDictProp } from '@/apis'
import BaseButton from '@/components/BaseButton.vue'
import OptionButton from '@/components/base/OptionButton.vue'
import Radio from '@/components/base/radio/Radio.vue'
import RadioGroup from '@/components/base/radio/RadioGroup.vue'
import GroupList from '@/pages/word/components/GroupList.vue'
import { getPracticeWordCache, PRACTICE_WORD_CACHE, setPracticeWordCache } from '@/utils/cache.ts'
import { getPracticeWordCache, setPracticeWordCache } from '@/utils/cache.ts'
const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = useWordOptions()
const settingStore = useSettingStore()
@@ -87,8 +83,6 @@ let data = $ref<PracticeData>({
excludeWords: [],
})
let isTypingWrongWord = ref(false)
// 独立模式的当前单词列表阶段:'new' | 'review' | 'write' | 'finished'
let currentWordListStage = $ref<'new' | 'review' | 'write' | 'finished'>('new')
provide('isTypingWrongWord', isTypingWrongWord)
provide('practiceData', data)
@@ -148,6 +142,12 @@ onMounted(() => {
})
onUnmounted(() => {
let cache = getPracticeWordCache()
//如果有缓存,则更新花费的时间;因为用户不输入不会保存数据,但有可能不是初始阶段(比如默写,听写等),所以需要更新花费的时间
if (cache) {
cache.statStoreData.spend = statStore.spend
setPracticeWordCache(cache)
}
timer && clearInterval(timer)
})
@@ -221,6 +221,18 @@ function initData(initVal: TaskWords, init: boolean = false) {
statStore.newWordNumber = 0
statStore.reviewWordNumber = 0
statStore.writeWordNumber = statStore.total
} else if (settingStore.wordPracticeMode === WordPracticeMode.Review) {
if (taskWords.review.length) {
data.words = taskWords.review
statStore.stage = WordPracticeStage.IdentifyReview
} else if (taskWords.write.length) {
data.words = taskWords.write
statStore.stage = WordPracticeStage.IdentifyReviewAll
}
statStore.total = taskWords.review.length + taskWords.write.length
statStore.newWordNumber = 0
statStore.reviewWordNumber = taskWords.review.length
statStore.writeWordNumber = taskWords.write.length
} else {
if (taskWords.new.length === 0) {
if (taskWords.review.length) {
@@ -228,15 +240,13 @@ function initData(initVal: TaskWords, init: boolean = false) {
if (settingStore.wordPracticeMode === WordPracticeMode.System) {
statStore.stage = WordPracticeStage.IdentifyReview
} else if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
statStore.stage = WordPracticeStage.FollowWriteReview
statStore.stage = WordPracticeModeStageMap[settingStore.wordPracticeMode][0]
} else if (settingStore.wordPracticeMode === WordPracticeMode.IdentifyOnly) {
statStore.stage = WordPracticeStage.IdentifyReview
} else if (settingStore.wordPracticeMode === WordPracticeMode.DictationOnly) {
statStore.stage = WordPracticeStage.DictationReview
} else if (settingStore.wordPracticeMode === WordPracticeMode.ListenOnly) {
statStore.stage = WordPracticeStage.ListenReview
} else if (settingStore.wordPracticeMode === WordPracticeMode.FollowWriteOnly) {
statStore.stage = WordPracticeStage.FollowWriteReview
}
} else {
if (taskWords.write.length) {
@@ -244,15 +254,13 @@ function initData(initVal: TaskWords, init: boolean = false) {
if (settingStore.wordPracticeMode === WordPracticeMode.System) {
statStore.stage = WordPracticeStage.IdentifyReviewAll
} else if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
statStore.stage = WordPracticeStage.FollowWriteReviewAll
statStore.stage = WordPracticeModeStageMap[settingStore.wordPracticeMode][0]
} else if (settingStore.wordPracticeMode === WordPracticeMode.IdentifyOnly) {
statStore.stage = WordPracticeStage.IdentifyReviewAll
} else if (settingStore.wordPracticeMode === WordPracticeMode.DictationOnly) {
statStore.stage = WordPracticeStage.DictationReviewAll
} else if (settingStore.wordPracticeMode === WordPracticeMode.ListenOnly) {
statStore.stage = WordPracticeStage.ListenReviewAll
} else if (settingStore.wordPracticeMode === WordPracticeMode.FollowWriteOnly) {
statStore.stage = WordPracticeStage.FollowWriteReviewAll
}
} else {
Toast.warning('没有可学习的单词!')
@@ -283,7 +291,6 @@ function initData(initVal: TaskWords, init: boolean = false) {
timer = setInterval(() => {
if (isFocus) {
statStore.spend += 1000
savePracticeData()
}
}, 1000)
}
@@ -466,9 +473,9 @@ async function next(isTyping: boolean = true) {
} else if (statStore.stage === WordPracticeStage.DictationReview) {
nextStage(taskWords.write, '开始自测之前')
} else if (statStore.stage === WordPracticeStage.IdentifyReviewAll) {
nextStage(shuffle(taskWords.review), '开始听写之前')
nextStage(shuffle(taskWords.write), '开始听写之前')
} else if (statStore.stage === WordPracticeStage.ListenReviewAll) {
nextStage(shuffle(taskWords.review), '开始默写之前')
nextStage(shuffle(taskWords.write), '开始默写之前')
} else if (statStore.stage === WordPracticeStage.DictationReviewAll) {
complete()
}
@@ -492,6 +499,18 @@ async function next(isTyping: boolean = true) {
} else if (statStore.stage === WordPracticeStage.IdentifyReviewAll) complete()
} else if (settingStore.wordPracticeMode === WordPracticeMode.Shuffle) {
if (statStore.stage === WordPracticeStage.Shuffle) complete()
} else if (settingStore.wordPracticeMode === WordPracticeMode.Review) {
if (statStore.stage === WordPracticeStage.IdentifyReview) {
nextStage(shuffle(taskWords.review), '开始听写昨日')
} else if (statStore.stage === WordPracticeStage.ListenReview) {
nextStage(shuffle(taskWords.review), '开始默写昨日')
} else if (statStore.stage === WordPracticeStage.DictationReview) {
nextStage(taskWords.write, '开始自测之前')
} else if (statStore.stage === WordPracticeStage.IdentifyReviewAll) {
nextStage(shuffle(taskWords.write), '开始听写之前')
} else if (statStore.stage === WordPracticeStage.ListenReviewAll) {
nextStage(shuffle(taskWords.write), '开始默写之前')
} else if (statStore.stage === WordPracticeStage.DictationReviewAll) complete()
}
}
} else {
@@ -505,7 +524,6 @@ async function next(isTyping: boolean = true) {
}
//如果单词是已掌握的,则跳过
if (isWordSimple(word)) next(false)
savePracticeData()
}
function skipStep() {
@@ -539,7 +557,6 @@ function onTypeWrong() {
}
function savePracticeData() {
// console.log('savePracticeData')
setPracticeWordCache({
taskWords,
practiceData: data,
@@ -567,20 +584,15 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
function repeat() {
console.log('重学一遍')
setPracticeWordCache(null)
let temp = cloneDeep(taskWords)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
//随机练习单独处理
if (settingStore.wordPracticeMode === WordPracticeMode.Shuffle) {
temp.shuffle = shuffle(temp.shuffle.filter(v => !ignoreList.includes(v.word)))
} else {
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
}
//将学习进度减回去
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
//排除已掌握单词
temp.new = temp.new.filter(v => !ignoreList.includes(v.word))
temp.review = temp.review.filter(v => !ignoreList.includes(v.word))
@@ -648,10 +660,12 @@ function togglePanel() {
}
async function continueStudy() {
setPracticeWordCache(null)
let temp = cloneDeep(taskWords)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
//随机练习单独处理
if (settingStore.wordPracticeMode === WordPracticeMode.Shuffle) {
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
temp.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(
0,
runtimeStore.routeData.total ?? temp.shuffle.length
@@ -662,10 +676,20 @@ async function continueStudy() {
if (!showStatDialog) {
console.log('没学完,强行跳过')
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
// 忽略单词数
const ignoreCount = ignoreList.filter(word =>
store.sdict.words.some(w => w.word.toLowerCase() === word)
).length
// 如果lastLearnIndex已经超过可学单词数则判定完成
if (store.sdict.lastLearnIndex + ignoreCount >= store.sdict.length) {
store.sdict.complete = true
store.sdict.lastLearnIndex = store.sdict.length
}
} else {
console.log('学完了,正常下一组')
showStatDialog = false
}
temp = getCurrentStudyWord()
}
emitter.emit(EventKey.resetWord)
@@ -680,6 +704,7 @@ async function continueStudy() {
}
async function jumpToGroup(group: number) {
setPracticeWordCache(null)
console.log('没学完,强行跳过', group)
store.sdict.lastLearnIndex = (group - 1) * store.sdict.perDayStudyNumber
emitter.emit(EventKey.resetWord)
@@ -701,6 +726,7 @@ function randomWrite() {
}
function nextRandomWrite() {
setPracticeWordCache(null)
console.log('继续随机默写')
initData(getCurrentStudyWord())
randomWrite()
@@ -711,7 +737,6 @@ useEvents([
[EventKey.repeatStudy, repeat],
[EventKey.continueStudy, continueStudy],
[EventKey.randomWrite, nextRandomWrite],
[EventKey.changeDict, () => initData(getCurrentStudyWord())],
[ShortcutKey.ShowWord, show],
[ShortcutKey.Previous, prev],
[ShortcutKey.Next, skip],
@@ -771,18 +796,24 @@ useEvents([
<div class="center gap-1">
<span>{{ store.sdict.name }}</span>
<template v-if="taskWords.new.length">
<GroupList
@click="jumpToGroup"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.Shuffle"
/>
<BaseIcon
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
>
<IconFluentArrowRight16Regular class="arrow" width="22" />
</BaseIcon>
</template>
<GroupList
@click="jumpToGroup"
v-if="
taskWords.new.length && settingStore.wordPracticeMode !== WordPracticeMode.Shuffle
"
/>
<BaseIcon
v-if="
taskWords.new.length &&
![WordPracticeMode.Review, WordPracticeMode.Shuffle].includes(
settingStore.wordPracticeMode
)
"
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
>
<IconFluentArrowRight16Regular class="arrow" width="22" />
</BaseIcon>
<BaseIcon
@click="randomWrite"

View File

@@ -242,7 +242,7 @@ calcWeekList() // 新增:计算本周学习记录
<ChannelIcons />
</div>
<!-- Action Buttons -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="flex min-w-130 justify-center">
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.RepeatChapter]"
@click="options(EventKey.repeatStudy)"
@@ -253,6 +253,7 @@ calcWeekList() // 新增:计算本周学习记录
</div>
</BaseButton>
<BaseButton
v-if="settingStore.wordPracticeMode !== WordPracticeMode.Review"
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
@click="options(EventKey.continueStudy)"
>
@@ -263,6 +264,7 @@ calcWeekList() // 新增:计算本周学习记录
</BaseButton>
<!-- todo 感觉这里的继续默写有问题应该是当前组而不是下一组-->
<BaseButton
v-if="settingStore.wordPracticeMode !== WordPracticeMode.Review"
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextRandomWrite]"
@click="options(EventKey.randomWrite)"
>

View File

@@ -5,8 +5,11 @@ import { useSettingStore } from '@/stores/setting.ts'
import {
PracticeData,
ShortcutKey,
TaskWords,
WordPracticeMode,
WordPracticeModeNameMap,
WordPracticeModeStageMap,
WordPracticeStage,
WordPracticeStageNameMap,
} from '@/types/types.ts'
import BaseIcon from '@/components/BaseIcon.vue'
@@ -36,6 +39,7 @@ const emit = defineEmits<{
let practiceData = inject<PracticeData>('practiceData')
let isTypingWrongWord = inject<Ref<boolean>>('isTypingWrongWord')
let practiceTaskWords = inject<TaskWords>('practiceTaskWords')
function format(val: number, suffix: string = '', check: number = -1) {
return val === check ? '-' : val + suffix
@@ -51,6 +55,147 @@ const progress = $computed(() => {
if (!practiceData.words.length) return 0
return (practiceData.index / practiceData.words.length) * 100
})
const stages = $computed(() => {
let DEFAULT_BAR = {
name: '',
ratio: 100,
percentage: (practiceData.index / practiceData.words.length) * 100,
active: true,
}
if ([WordPracticeMode.Shuffle, WordPracticeMode.Free].includes(settingStore.wordPracticeMode)) {
return [DEFAULT_BAR]
} else {
// 阶段映射:将 WordPracticeStage 映射到 stageIndex 和 childIndex
const stageMap: Partial<Record<WordPracticeStage, { stageIndex: number; childIndex: number }>> = {
[WordPracticeStage.FollowWriteNewWord]: { stageIndex: 0, childIndex: 0 },
[WordPracticeStage.IdentifyNewWord]: { stageIndex: 0, childIndex: 0 },
[WordPracticeStage.ListenNewWord]: { stageIndex: 0, childIndex: 1 },
[WordPracticeStage.DictationNewWord]: { stageIndex: 0, childIndex: 2 },
[WordPracticeStage.IdentifyReview]: { stageIndex: 1, childIndex: 0 },
[WordPracticeStage.ListenReview]: { stageIndex: 1, childIndex: 1 },
[WordPracticeStage.DictationReview]: { stageIndex: 1, childIndex: 2 },
[WordPracticeStage.IdentifyReviewAll]: { stageIndex: 2, childIndex: 0 },
[WordPracticeStage.ListenReviewAll]: { stageIndex: 2, childIndex: 1 },
[WordPracticeStage.DictationReviewAll]: { stageIndex: 2, childIndex: 2 },
}
// 获取当前阶段的配置
const currentStageConfig = stageMap[statStore.stage]
if (!currentStageConfig) {
return stages
}
const { stageIndex, childIndex } = currentStageConfig
const currentProgress = (practiceData.index / practiceData.words.length) * 100
if (
[WordPracticeMode.IdentifyOnly, WordPracticeMode.DictationOnly, WordPracticeMode.ListenOnly].includes(
settingStore.wordPracticeMode
)
) {
const stages = [
{ name: `新词:${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`, ratio: 33, percentage: 0, active: false },
{ name: `上次学习:${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`, ratio: 33, percentage: 0, active: false },
{ name: `之前学习:${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`, ratio: 33, percentage: 0, active: false },
]
// 设置已完成阶段的百分比和比例
for (let i = 0; i < stageIndex; i++) {
stages[i].percentage = 100
stages[i].ratio = 33
}
// 设置当前激活的阶段
stages[stageIndex].active = true
stages[stageIndex].percentage = (practiceData.index / practiceData.words.length) * 100
return stages
} else {
// 阶段配置:定义每个阶段组的基础信息
const stageConfigs = [
{
name: '新词',
ratio: 70,
children: [{ name: '新词:跟写' }, { name: '新词:听写' }, { name: '新词:默写' }],
},
{
name: '上次学习:复习',
ratio: 15,
children: [{ name: '上次学习:自测' }, { name: '上次学习:听写' }, { name: '上次学习:默写' }],
},
{
name: '之前学习:复习',
ratio: 15,
children: [{ name: '之前学习:自测' }, { name: '之前学习:听写' }, { name: '之前学习:默写' }],
},
]
// 初始化 stages
const stages = stageConfigs.map(config => ({
name: config.name,
percentage: 0,
ratio: config.ratio,
active: false,
children: config.children.map(child => ({
name: child.name,
percentage: 0,
ratio: 33,
active: false,
})),
}))
// 设置已完成阶段的百分比和比例
for (let i = 0; i < stageIndex; i++) {
stages[i].percentage = 100
stages[i].ratio = 15
}
// 设置当前激活的阶段
stages[stageIndex].ratio = 70
stages[stageIndex].active = true
// 根据类型设置子阶段的进度
const currentStageChildren = stages[stageIndex].children
if (childIndex === 0) {
// 跟写/自测:只激活第一个子阶段
currentStageChildren[0].active = true
currentStageChildren[0].percentage = currentProgress
} else if (childIndex === 1) {
// 听写:第一个完成,第三个未开始,第二个进行中
currentStageChildren[0].active = false
currentStageChildren[1].active = true
currentStageChildren[2].active = false
currentStageChildren[0].percentage = 100
currentStageChildren[1].percentage = currentProgress
currentStageChildren[2].percentage = 0
} else if (childIndex === 2) {
// 默写:前两个完成,第三个进行中
currentStageChildren[0].active = false
currentStageChildren[1].active = false
currentStageChildren[2].active = true
currentStageChildren[0].percentage = 100
currentStageChildren[1].percentage = 100
currentStageChildren[2].percentage = currentProgress
}
if (settingStore.wordPracticeMode === WordPracticeMode.System) {
return stages
}
if (settingStore.wordPracticeMode === WordPracticeMode.Review) {
stages.shift()
if (stageIndex === 1) stages[1].ratio = 30
if (stageIndex === 2) stages[0].ratio = 30
console.log('stages', stages, childIndex)
return stages
}
}
}
return [DEFAULT_BAR]
})
</script>
<template>
@@ -65,14 +210,7 @@ const progress = $computed(() => {
</Tooltip>
<div class="bottom">
<div class="flex gap-1">
<Tooltip
:title="WordPracticeStageNameMap[i]"
v-for="i of WordPracticeModeStageMap[settingStore.wordPracticeMode]"
>
<Progress :percentage="progress" :stroke-width="8" color="#69b1ff" :show-text="false" />
</Tooltip>
</div>
<StageProgress :stages="stages" />
<div class="flex justify-between items-center">
<div class="stat">
@@ -130,8 +268,7 @@ const progress = $computed(() => {
<IconFluentStar16Filled v-else />
<span>
{{
(!isCollect ? '收藏' : '取消收藏') +
`(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`
(!isCollect ? '收藏' : '取消收藏') + `(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`
}}</span
>
</div>

View File

@@ -285,7 +285,7 @@ async function onTyping(e: KeyboardEvent) {
right = true
letter = word[input.length]
}
console.log('e', e, e.code, e.shiftKey, word[input.length])
// console.log('e', e, e.code, e.shiftKey, word[input.length])
if (right) {
input += letter

View File

@@ -230,9 +230,8 @@ export enum WordPracticeMode {
IdentifyOnly = 2, // 独立自测模式
DictationOnly = 3, // 独立默写模式
ListenOnly = 4, // 独立听写模式
FollowWriteOnly = 5, // 独立跟写模式(内部会自动切换到 Spell
Shuffle = 6, // 随机复习模式
Review = 7, // 复习模式
Shuffle = 5, // 随机复习模式
Review = 6, // 复习模式
}
//练习类型
@@ -282,12 +281,6 @@ export enum WordPracticeStage {
export const WordPracticeModeStageMap: Record<WordPracticeMode, WordPracticeStage[]> = {
[WordPracticeMode.Free]: [WordPracticeStage.FollowWriteNewWord, WordPracticeStage.Complete],
[WordPracticeMode.FollowWriteOnly]: [
WordPracticeStage.FollowWriteNewWord,
WordPracticeStage.FollowWriteReview,
WordPracticeStage.FollowWriteReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.IdentifyOnly]: [
WordPracticeStage.IdentifyNewWord,
WordPracticeStage.IdentifyReview,
@@ -356,7 +349,6 @@ export const WordPracticeModeNameMap: Record<WordPracticeMode, string> = {
[WordPracticeMode.IdentifyOnly]: '自测',
[WordPracticeMode.DictationOnly]: '默写',
[WordPracticeMode.ListenOnly]: '听写',
[WordPracticeMode.FollowWriteOnly]: '跟写',
[WordPracticeMode.Shuffle]: '随机复习',
[WordPracticeMode.Review]: '复习',
}

View File

@@ -1,4 +1,4 @@
import { Article, PracticeData, TaskWords } from '@/types/types.ts'
import { PracticeData, TaskWords } from '@/types/types.ts'
import { PracticeState } from '@/stores/practice.ts'
import { IS_DEV } from '@/config/env'
@@ -18,8 +18,11 @@ export type PracticeWordCache = {
}
export type PracticeArticleCache = {
article: Article
practiceData: PracticeData
practiceData: {
sectionIndex: number
sentenceIndex: number
wordIndex: number
}
statStoreData: PracticeState
}
@@ -74,6 +77,7 @@ export function setPracticeWordCache(cache: PracticeWordCache | null) {
}
export function setPracticeArticleCache(cache: PracticeArticleCache | null) {
debugger
if (cache) {
localStorage.setItem(
PRACTICE_ARTICLE_CACHE.key,

View File

@@ -4,7 +4,6 @@ import {onMounted, onUnmounted} from "vue";
export const emitter = mitt()
export const EventKey = {
resetWord: 'resetWord',
changeDict: 'changeDict',
openStatModal: 'openStatModal',
openWordListModal: 'openWordListModal',
closeOther: 'closeOther',