Feature: Added status progress bar
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
54
src/components/StageProgress.vue
Normal file
54
src/components/StageProgress.vue
Normal 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>
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
//虚拟列表长度限制
|
||||
const limit = 101
|
||||
const limit = 200
|
||||
const settingStore = useSettingStore()
|
||||
const listRef: any = $ref()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}}. {{ _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 }}. {{
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]: '复习',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user