This commit is contained in:
Zyronon
2025-12-26 18:55:30 +08:00
committed by GitHub
parent 4134b4b30b
commit e7e7b202bc
11 changed files with 321 additions and 261 deletions

View File

@@ -541,6 +541,6 @@ a {
}
.target-number {
@apply text-3xl! mx-2;
@apply text-3xl!;
color: rgb(176, 116, 211)!important;
}

View File

@@ -1,19 +1,24 @@
<script setup lang="ts">
import {useSettingStore} from "@/stores/setting.ts";
import { useSettingStore } from '@/stores/setting.ts'
import { useRouter } from 'vue-router'
import { IS_DEV } from '@/config/env'
const settingStore = useSettingStore()
const router = useRouter()
function goHome() {
router.push('/')
if (IS_DEV) {
router.push('/')
} else {
location.href = window.atob('aHR0cHM6Ly90eXBld29yZHMuY2M=')
}
}
</script>
<template>
<div class="center mb-2" @click="goHome">
<img v-show="settingStore.theme === 'dark'" src="/logo-text-white.png" alt="">
<img v-show="settingStore.theme !== 'dark'" src="/logo-text-black.png" alt="">
<img v-show="settingStore.theme === 'dark'" src="/logo-text-white.png" alt="" />
<img v-show="settingStore.theme !== 'dark'" src="/logo-text-black.png" alt="" />
</div>
</template>

View File

@@ -1,8 +1,8 @@
import { Article, TaskWords, Word, WordPracticeMode } from "@/types/types.ts";
import { useBaseStore } from "@/stores/base.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { getDefaultWord } from "@/types/func.ts";
import { getRandomN, splitIntoN } from "@/utils";
import { Article, TaskWords, Word, WordPracticeMode } from '@/types/types.ts'
import { useBaseStore } from '@/stores/base.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultWord } from '@/types/func.ts'
import { cloneDeep, getRandomN, shuffle, splitIntoN } from '@/utils'
export function useWordOptions() {
const store = useBaseStore()
@@ -12,7 +12,9 @@ export function useWordOptions() {
}
function toggleWordCollect(val: Word) {
let rIndex = store.collectWord.words.findIndex(v => v.word.toLowerCase() === val.word.toLowerCase())
let rIndex = store.collectWord.words.findIndex(
v => v.word.toLowerCase() === val.word.toLowerCase()
)
if (rIndex > -1) {
store.collectWord.words.splice(rIndex, 1)
} else {
@@ -57,7 +59,7 @@ export function useWordOptions() {
isWordSimple,
toggleWordSimple,
delWrongWord,
delSimpleWord
delSimpleWord,
}
}
@@ -88,7 +90,7 @@ export function useArticleOptions() {
export function getCurrentStudyWord(): TaskWords {
const store = useBaseStore()
let data = { new: [], review: [], write: [], shuffle: [] }
let dict = store.sdict;
let dict = store.sdict
let isTest = false
let words = dict.words.slice()
if (isTest) {
@@ -100,47 +102,62 @@ export function getCurrentStudyWord(): TaskWords {
const settingStore = useSettingStore()
//忽略时是否加上自定义的简单词
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
const perDay = dict.perDayStudyNumber;
let start = dict.lastLearnIndex;
let complete = dict.complete;
const perDay = dict.perDayStudyNumber
let start = dict.lastLearnIndex
let complete = dict.complete
let isEnd = start >= dict.length - 1
if (isTest) {
start = 1
complete = true
}
//如果已完成,并且记录在最后,那么直接随机取复习词
if (complete && isEnd) {
//复习比最小是1
let ratio = settingStore.wordReviewRatio || 1
let ignoreList = [store.allIgnoreWords, store.knownWords][
settingStore.ignoreSimpleWord ? 0 : 1
]
// 先将可用词表全部随机,再按需过滤忽略列表,只取到目标数量为止
let shuffled = shuffle(cloneDeep(dict.words))
let count = 0
data.write = []
for (let item of shuffled) {
if (!ignoreList.includes(item.word.toLowerCase())) {
data.write.push(item)
count++
if (count >= perDay * ratio) {
break
}
}
}
return data
}
let end = start
let list = dict.words.slice(start)
if (complete) {
//如果是已完成,那么把应该学的新词放到复习词组里面
//从start往后取perDay个单词作为当前练习单词
for (let item of list) {
if (!ignoreList.includes(item.word.toLowerCase())) {
if (data.new.length < perDay) {
data.new.push(item)
} else break
}
end++
}
//如果复习比大于等于1或者已完成那么就取复习词
if (settingStore.wordReviewRatio >= 1 || complete) {
//从start往前取perDay个单词作为当前复习单词取到0为止
list = dict.words.slice(0, start).reverse()
//但如果已完成,则滚动取值
if (complete) list = list.concat(dict.words.slice(end).reverse())
for (let item of list) {
if (!ignoreList.includes(item.word.toLowerCase())) {
if (data.review.length < perDay) {
data.review.push(item)
} else break
}
end++
}
} else {
//从start往后取perDay个单词作为当前练习单词
for (let item of list) {
if (!ignoreList.includes(item.word.toLowerCase())) {
if (data.new.length < perDay) {
data.new.push(item)
} else break
}
end++
}
if (settingStore.wordReviewRatio >= 1) {
//从start往前取perDay个单词作为当前复习单词取到0为止
list = dict.words.slice(0, start).reverse()
for (let item of list) {
if (!ignoreList.includes(item.word.toLowerCase())) {
if (data.review.length < perDay) {
data.review.push(item)
} else break
}
start--
}
start--
}
}
@@ -157,10 +174,10 @@ export function getCurrentStudyWord(): TaskWords {
let candidateWords = dict.words.slice(0, start).reverse()
//但如果已完成,则滚动取值
if (complete) candidateWords = candidateWords.concat(dict.words.slice(end).reverse())
candidateWords = candidateWords.filter(w => !ignoreList.includes(w.word.toLowerCase()));
candidateWords = candidateWords.filter(w => !ignoreList.includes(w.word.toLowerCase()))
// console.log(candidateWords.map(v => v.word))
//最终要获取的单词数量
const totalNeed = perDay * (settingStore.wordReviewRatio - 1);
const totalNeed = perDay * (settingStore.wordReviewRatio - 1)
if (candidateWords.length <= totalNeed) {
data.write = candidateWords
} else {
@@ -171,20 +188,27 @@ export function getCurrentStudyWord(): TaskWords {
// console.log('groups', groups)
// 分配数量,靠前组多,靠后组少,例如分配比例 [6,5,4,3,2,1]
const ratio = Array.from({ length: days }, (_, i) => i + 1).reverse();
const ratioSum = ratio.reduce((a, b) => a + b, 0);
const realRatio = ratio.map(r => Math.round(r / ratioSum * totalNeed));
const ratio = Array.from({ length: days }, (_, i) => i + 1).reverse()
const ratioSum = ratio.reduce((a, b) => a + b, 0)
const realRatio = ratio.map(r => Math.round((r / ratioSum) * totalNeed))
// console.log(ratio, ratioSum, realRatio, realRatio.reduce((a, b) => a + b, 0))
// 按比例从每组随机取单词
let writeWords: Word[] = [];
let writeWords: Word[] = []
groups.map((v, i) => {
writeWords = writeWords.concat(getRandomN(v, realRatio[i]))
})
// console.log('writeWords', writeWords)
data.write = writeWords;
data.write = writeWords
}
}
//如果已完成,那么合并写词和复习词
if(complete){
// data.new = []
// data.review = data.review.concat(data.write)
// data.write = []
}
}
// console.log('data-new', data.new.map(v => v.word))
// console.log('data-review', data.review.map(v => v.word))

View File

@@ -138,7 +138,7 @@ watch(dict_list, (val) => {
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
quantifier="词"
quantifier="词"
:select-id="'-1'"/>
<Empty v-else text="没有相关词典"/>
</div>
@@ -147,7 +147,7 @@ watch(dict_list, (val) => {
v-for="item in groupedByCategoryAndTag"
:select-id="store.sdict.id"
@selectDict="selectDict"
quantifier="词"
quantifier="词"
:groupByTag="item[1]"
:category="item[0]"
/>

View File

@@ -703,7 +703,7 @@ async function continueStudy() {
}
async function jumpToGroup(group: number) {
console.log('没学完,强行跳过',group)
console.log('没学完,强行跳过', group)
store.sdict.lastLearnIndex = (group - 1) * store.sdict.perDayStudyNumber
emitter.emit(EventKey.resetWord)
initData(getCurrentStudyWord())
@@ -794,14 +794,11 @@ useEvents([
<div class="center gap-1">
<span>{{ store.sdict.name }}</span>
<GroupList
@click="jumpToGroup"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.Shuffle"
/>
<template v-if="taskWords.new.length">
<GroupList
@click="jumpToGroup"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.Shuffle"
/>
<BaseIcon
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"

View File

@@ -73,23 +73,12 @@ watch(model, async newVal => {
complete: store.sdict.complete,
str: `name:${store.sdict.name},per:${store.sdict.perDayStudyNumber},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)},index:${store.sdict.lastLearnIndex}`,
})
debugger
//如果 shuffle 数组不为空,就说明是复习,不用修改 lastLearnIndex
//todo
if (settingStore.wordPracticeMode !== WordPracticeMode.Shuffle) {
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
//todo 这里计算不正确,因为有可能有单词被忽略,所以需要计算忽略的单词数
// 检查已忽略的单词数量,是否全部完成
let ignoreList = [store.allIgnoreWords, store.knownWords][
settingStore.ignoreSimpleWord ? 0 : 1
]
// 忽略单词数
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.lastLearnIndex = store.sdict.lastLearnIndex + store.sdict.perDayStudyNumber
if (store.sdict.lastLearnIndex >= store.sdict.length - 1) {
dictIsEnd = true
store.sdict.complete = true
store.sdict.lastLearnIndex = store.sdict.length
@@ -178,30 +167,25 @@ calcWeekList() // 新增:计算本周学习记录
<p class="font-medium text-lg">{{ encouragementText }}</p>
</div>
<!-- Main Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<!-- Study Time -->
<div class="item">
<IconFluentClock20Regular class="text-purple-500" />
<div class="text-sm mb-1 font-medium">学习时长</div>
<div class="text-xl font-bold">{{ formattedStudyTime }}</div>
</div>
<!-- Accuracy Rate -->
<div class="item">
<IconFluentTarget20Regular class="text-purple-500" />
<div class="text-sm mb-1 font-medium">正确率</div>
<div class="text-xl font-bold">{{ accuracyRate }}%</div>
</div>
<!-- New Words -->
<div class="item">
<IconFluentSparkle20Regular class="text-purple-500" />
<div class="text-sm mb-1 font-medium">新词</div>
<div class="text-xl font-bold">{{ statStore.newWordNumber }}</div>
</div>
<!-- New Words -->
<div class="item">
<IconFluentBook20Regular class="text-purple-500" />
<div class="text-sm mb-1 font-medium">复习</div>

View File

@@ -14,7 +14,7 @@ import {
useNav,
} from '@/utils'
import BasePage from '@/components/BasePage.vue'
import { DictResource, WordPracticeMode } from '@/types/types.ts'
import { DictResource, WordPracticeMode, WordPracticeModeNameMap } from '@/types/types.ts'
import { watch } from 'vue'
import { getCurrentStudyWord } from '@/hooks/dict.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
@@ -204,12 +204,8 @@ function toggleSelect(item) {
}
const progressTextLeft = $computed(() => {
if (store.sdict.complete) return '已学完,进入复习阶段'
return '已学' + store.currentStudyProgress + '%'
})
const progressTextRight = $computed(() => {
// if (store.sdict.complete) return store.sdict?.length
return store.sdict?.lastLearnIndex
if (store.sdict.complete) return '已学完,进入复习阶段'
return '当前进度:已学' + store.currentStudyProgress + '%'
})
function check(cb: Function) {
@@ -258,6 +254,7 @@ async function onShufflePracticeSettingOk(total) {
async function saveLastPracticeIndex(e) {
Toast.success('修改成功')
runtimeStore.editDict.lastLearnIndex = e
// runtimeStore.editDict.complete = e >= runtimeStore.editDict.length - 1
showChangeLastPracticeIndexDialog = false
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
@@ -292,16 +289,9 @@ let isNewHost = $ref(window.location.host === Host)
</div>
<template v-if="store.sdict.id">
<div class="mt-4 flex flex-col gap-2">
<div class="">当前进度{{ progressTextLeft }}</div>
<Progress
size="large"
:percentage="store.currentStudyProgress"
:show-text="false"
></Progress>
<div class="mt-4 space-y-2">
<div class="text-sm flex justify-between">
<span>已完成 {{ progressTextRight }} / {{ store.sdict.words.length }} </span>
<span v-if="store.sdict.id">
<span v-opacity="store.sdict.id && store.sdict.lastLearnIndex < store.sdict.length">
预计完成日期{{
_getAccomplishDate(
store.sdict.words.length - store.sdict.lastLearnIndex,
@@ -310,6 +300,16 @@ let isNewHost = $ref(window.location.host === Host)
}}
</span>
</div>
<Progress
size="large"
:percentage="store.currentStudyProgress"
:show-text="false"
></Progress>
<div class="text-sm flex justify-between">
<span>{{ progressTextLeft }}</span>
<span> {{ store.sdict?.lastLearnIndex }} / {{ store.sdict.words.length }} </span>
</div>
</div>
<div class="flex items-center mt-4 gap-4">
<BaseButton type="info" size="small" @click="router.push('/dict-list')">
@@ -398,37 +398,54 @@ let isNewHost = $ref(window.location.host === Host)
size="large"
:disabled="!store.sdict.id"
:loading="loading"
@click="startPractice(WordPracticeMode.System)"
@click="startPractice(settingStore.wordPracticeMode)"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<span class="line-height-[2]">{{
isSaveData
? `继续${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`
: `开始${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`
}}</span>
<IconFluentArrowCircleRight16Regular class="text-xl" />
</div>
</BaseButton>
<template #options>
<BaseButton class="w-20" @click="startPractice(WordPracticeMode.System)">
智能
<BaseButton
class="w-23"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.System"
@click="startPractice(WordPracticeMode.System)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.System] }}
</BaseButton>
<BaseButton class="w-20" @click="startPractice(WordPracticeMode.FollowWriteOnly)">
跟写
<BaseButton
class="w-23"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.IdentifyOnly"
@click="startPractice(WordPracticeMode.IdentifyOnly)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.IdentifyOnly] }}
</BaseButton>
<BaseButton class="w-20" @click="startPractice(WordPracticeMode.IdentifyOnly)">
自测
<BaseButton
class="w-23"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.ListenOnly"
@click="startPractice(WordPracticeMode.ListenOnly)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.ListenOnly] }}
</BaseButton>
<BaseButton class="w-20" @click="startPractice(WordPracticeMode.ListenOnly)">
听写
</BaseButton>
<BaseButton class="w-20" @click="startPractice(WordPracticeMode.DictationOnly)">
默写
<BaseButton
class="w-23"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.DictationOnly"
@click="startPractice(WordPracticeMode.DictationOnly)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.DictationOnly] }}
</BaseButton>
</template>
</OptionButton>
<OptionButton class="flex-1">
<OptionButton class="flex-1" v-if="currentStudy.new.length">
<BaseButton
size="large"
:loading="loading"
@click="startPractice(WordPracticeMode.Review, true)"
@click="startPractice(WordPracticeMode.Review)"
>
复习
</BaseButton>
@@ -438,6 +455,13 @@ let isNewHost = $ref(window.location.host === Host)
</BaseButton>
</template>
</OptionButton>
<BaseButton
v-else
size="large"
@click="check(() => (showShufflePracticeSettingDialog = true))"
>
随机复习
</BaseButton>
<BaseButton size="large" :loading="loading" @click="startPractice(WordPracticeMode.Free)">
<div class="flex items-center gap-2">
@@ -483,7 +507,7 @@ let isNewHost = $ref(window.location.host === Host)
<div class="flex gap-4 flex-wrap mt-4">
<Book
:is-add="false"
quantifier="词"
quantifier="词"
:item="item"
:checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)"
@@ -506,7 +530,7 @@ let isNewHost = $ref(window.location.host === Host)
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
<Book
:is-add="false"
quantifier=""
quantifier=""
:item="item as any"
v-for="(item, j) in recommendDictList"
@click="goDictDetail(item as any)"

View File

@@ -63,11 +63,11 @@ watch(
<div class="target-modal color-main" id="mode">
<div class="text-center mt-4">
<span
><span class="target-number">{{ runtimeStore.editDict.length }}</span
><span class="target-number mx-2">{{ runtimeStore.editDict.length }}</span
>个单词</span
>
<span
>预计<span class="target-number">{{
>预计<span class="target-number mx-2">{{
_getAccomplishDays(
runtimeStore.editDict.length - tempLastLearnIndex,
tempPerDayStudyNumber
@@ -87,23 +87,30 @@ watch(
<BaseInput class="target-number" v-model="tempPerDayStudyNumber" />
</div>
<span>个新词</span>
<template v-if="temPracticeMode === 0">
<span>复习</span>
<div class="target-number">
{{ tempPerDayStudyNumber * tempWordReviewRatio }}
</div>
<span></span>
</template>
<span>复习</span>
<div class="target-number mx-2">
{{ tempPerDayStudyNumber * tempWordReviewRatio }}
</div>
<span></span>
</div>
<div class="flex mb-4 gap-space" v-if="temPracticeMode === 0">
<Tooltip title="复习词与新词的比例">
<div class="flex items-center gap-1 w-20">
<span>复习比</span>
<IconFluentQuestionCircle20Regular />
<div class="mb-4 space-y-2">
<div class="flex items-center gap-space">
<Tooltip title="复习词与新词的比例">
<div class="flex items-center gap-1 w-20 break-keep">
<span>复习比</span>
<IconFluentQuestionCircle20Regular />
</div>
</Tooltip>
<InputNumber :min="0" :max="10" v-model="tempWordReviewRatio" />
</div>
<div class="flex" v-if="!tempWordReviewRatio">
<div class="w-23 flex-shrink-0"></div>
<div class="text-sm text-gray-500">
<div>未完成学习时复习数量按照设置的复习比生成为0则不复习</div>
<div>完成学习后新词数量固定为0复习数量按照比例生成若复习比小于1 1 计算</div>
</div>
</Tooltip>
<InputNumber :min="0" :max="10" v-model="tempWordReviewRatio" />
</div>
</div>
<div class="flex mb-4 gap-space">
@@ -156,7 +163,6 @@ watch(
.target-modal {
width: 35rem;
.mode-item {
@apply w-50% border border-blue border-solid p-2 rounded-lg cursor-pointer;
}

View File

@@ -221,6 +221,7 @@ export const useBaseStore = defineStore('base', {
this.word.bookList[this.word.studyIndex].perDayStudyNumber = val.perDayStudyNumber
this.word.bookList[this.word.studyIndex].lastLearnIndex = val.lastLearnIndex
this.word.bookList[this.word.studyIndex].userDictId = val.userDictId
this.word.bookList[this.word.studyIndex].complete = val.complete
} else {
this.word.bookList.push(getDefaultDict(val))
this.word.studyIndex = this.word.bookList.length - 1

View File

@@ -349,3 +349,14 @@ export const WordPracticeStageNameMap: Record<WordPracticeStage, string> = {
[WordPracticeStage.Complete]: '完成学习',
[WordPracticeStage.Shuffle]: '随机复习',
}
export const WordPracticeModeNameMap: Record<WordPracticeMode, string> = {
[WordPracticeMode.System]: '智能学习',
[WordPracticeMode.Free]: '自由',
[WordPracticeMode.IdentifyOnly]: '自测',
[WordPracticeMode.DictationOnly]: '默写',
[WordPracticeMode.ListenOnly]: '听写',
[WordPracticeMode.FollowWriteOnly]: '跟写',
[WordPracticeMode.Shuffle]: '随机复习',
[WordPracticeMode.Review]: '复习',
}

View File

@@ -1,16 +1,16 @@
import { BaseState, getDefaultBaseState, useBaseStore } from "@/stores/base.ts";
import { getDefaultSettingState, SettingState } from "@/stores/setting.ts";
import { Dict, DictId, DictResource, DictType } from "@/types/types.ts";
import { useRouter } from "vue-router";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { BaseState, getDefaultBaseState, useBaseStore } from '@/stores/base.ts'
import { getDefaultSettingState, SettingState } from '@/stores/setting.ts'
import { Dict, DictId, DictResource, DictType } from '@/types/types.ts'
import { useRouter } from 'vue-router'
import { useRuntimeStore } from '@/stores/runtime.ts'
import dayjs from 'dayjs'
import { AppEnv, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/config/env.ts";
import { nextTick } from "vue";
import { AppEnv, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env.ts'
import { nextTick } from 'vue'
import Toast from '@/components/base/toast/Toast.ts'
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import duration from "dayjs/plugin/duration";
import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import duration from 'dayjs/plugin/duration'
dayjs.extend(duration);
dayjs.extend(duration)
export function no() {
Toast.warning('未现实')
@@ -60,7 +60,9 @@ export function checkAndUpgradeSaveDict(val: any) {
return defaultState
} else {
// 版本不匹配时,尽量保留数据而不是直接返回默认状态
console.warn(`数据版本不匹配: 当前版本 ${version}, 期望版本 ${SAVE_DICT_KEY.version},尝试保留数据`)
console.warn(
`数据版本不匹配: 当前版本 ${version}, 期望版本 ${SAVE_DICT_KEY.version},尝试保留数据`
)
try {
checkRiskKey(defaultState, state)
// 尝试保留 bookList 数据
@@ -133,7 +135,8 @@ export function checkAndUpgradeSaveSetting(val: any) {
export function shakeCommonDict(n: BaseState): BaseState {
let data: BaseState = cloneDeep(n)
data.word.bookList.map((v: Dict) => {
if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id)) v.words = []
if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id))
v.words = []
})
data.article.bookList.map((v: Dict) => {
if (!v.custom && ![DictId.articleCollect].includes(v.id)) v.articles = []
@@ -159,10 +162,10 @@ export function useNav() {
if (data) {
runtimeStore.routeData = cloneDeep(data)
}
router.push({path, query})
router.push({ path, query })
}
return {nav, push: nav, back: router.back}
return { nav, push: nav, back: router.back }
}
export function _dateFormat(val: any, format: string = 'YYYY/MM/DD HH:mm'): string {
@@ -175,17 +178,17 @@ export function _dateFormat(val: any, format: string = 'YYYY/MM/DD HH:mm'): stri
}
export function msToHourMinute(ms) {
const d = dayjs.duration(ms);
const hours = d.hours();
const minutes = d.minutes();
const seconds = d.seconds();
if (hours) return `${hours}小时${minutes}分钟`;
if (minutes) return `${minutes}分钟`;
return `${seconds}`;
const d = dayjs.duration(ms)
const hours = d.hours()
const minutes = d.minutes()
const seconds = d.seconds()
if (hours) return `${hours}小时${minutes}分钟`
if (minutes) return `${minutes}分钟`
return `${seconds}`
}
export function msToMinute(ms) {
return `${Math.floor(dayjs.duration(ms).asMinutes())}分钟`;
return `${Math.floor(dayjs.duration(ms).asMinutes())}分钟`
}
//获取完成天数
@@ -218,44 +221,47 @@ export function _copy(val: string) {
navigator.clipboard.writeText(val)
}
export function _parseLRC(lrc: string): { start: number, end: number, text: string }[] {
const lines = lrc.split("\n").filter(line => line.trim() !== "");
const regex = /\[(\d{2}):(\d{2}\.\d{2})\](.*)/;
let parsed: any = [];
export function _parseLRC(lrc: string): { start: number; end: number; text: string }[] {
const lines = lrc.split('\n').filter(line => line.trim() !== '')
const regex = /\[(\d{2}):(\d{2}\.\d{2})\](.*)/
let parsed: any = []
for (let i = 0; i < lines.length; i++) {
let match = lines[i].match(regex);
let match = lines[i].match(regex)
if (match) {
let start = parseFloat(match[1]) * 60 + parseFloat(match[2]); // 转换成秒
let text = match[3].trim();
let start = parseFloat(match[1]) * 60 + parseFloat(match[2]) // 转换成秒
let text = match[3].trim()
// 计算结束时间(下一个时间戳)
let nextMatch = lines[i + 1] ? lines[i + 1].match(regex) : null;
let end = nextMatch ? parseFloat(nextMatch[1]) * 60 + parseFloat(nextMatch[2]) : null;
let nextMatch = lines[i + 1] ? lines[i + 1].match(regex) : null
let end = nextMatch ? parseFloat(nextMatch[1]) * 60 + parseFloat(nextMatch[2]) : null
parsed.push({start, end, text});
parsed.push({ start, end, text })
}
}
return parsed;
return parsed
}
export async function sleep(time: number) {
return new Promise(resolve => setTimeout(resolve, time));
return new Promise(resolve => setTimeout(resolve, time))
}
export async function _getDictDataByUrl(val: DictResource, type: DictType = DictType.word): Promise<Dict> {
export async function _getDictDataByUrl(
val: DictResource,
type: DictType = DictType.word
): Promise<Dict> {
// await sleep(2000);
let dictResourceUrl = `/dicts/${val.language}/word/${val.url}`
if (type === DictType.article) {
dictResourceUrl = `/dicts/${val.language}/article/${val.url}`;
dictResourceUrl = `/dicts/${val.language}/article/${val.url}`
}
let s = await fetch(resourceWrap(dictResourceUrl, val.version)).then(r => r.json())
if (s) {
if (type === DictType.word) {
return getDefaultDict({...val, words: s})
return getDefaultDict({ ...val, words: s })
} else {
return getDefaultDict({...val, articles: s})
return getDefaultDict({ ...val, articles: s })
}
}
return getDefaultDict()
@@ -263,97 +269,97 @@ export async function _getDictDataByUrl(val: DictResource, type: DictType = Dict
//从字符串里面转换为Word格式
export function convertToWord(raw: any) {
const safeString = (str) => (typeof str === 'string' ? str.trim() : '');
const safeString = str => (typeof str === 'string' ? str.trim() : '')
const safeSplit = (str, sep) =>
safeString(str) ? safeString(str).split(sep).filter(Boolean) : [];
safeString(str) ? safeString(str).split(sep).filter(Boolean) : []
// 1. trans
const trans = safeSplit(raw.trans, '\n').map(line => {
const match = safeString(line).match(/^([^\s.]+\.?)\s*(.*)$/);
const match = safeString(line).match(/^([^\s.]+\.?)\s*(.*)$/)
if (match) {
let pos = safeString(match[1]);
let cn = safeString(match[2]);
let pos = safeString(match[1])
let cn = safeString(match[2])
// 如果 pos 不是常规词性(不以字母开头),例如 "【名】"
if (!/^[a-zA-Z]+\.?$/.test(pos)) {
cn = safeString(line); // 整行放到 cn
pos = ''; // pos 置空
cn = safeString(line) // 整行放到 cn
pos = '' // pos 置空
}
return {pos, cn};
return { pos, cn }
}
return {pos: '', cn: safeString(line)};
});
return { pos: '', cn: safeString(line) }
})
// 2. sentences
const sentences = safeSplit(raw.sentences, '\n\n').map(block => {
const [c, cn] = block.split('\n');
return {c: safeString(c), cn: safeString(cn)};
});
const [c, cn] = block.split('\n')
return { c: safeString(c), cn: safeString(cn) }
})
// 3. phrases
const phrases = safeSplit(raw.phrases, '\n\n').map(block => {
const [c, cn] = block.split('\n');
return {c: safeString(c), cn: safeString(cn)};
});
const [c, cn] = block.split('\n')
return { c: safeString(c), cn: safeString(cn) }
})
// 4. synos
const synos = safeSplit(raw.synos, '\n\n').map(block => {
const lines = block.split('\n').map(safeString);
const [posCn, wsStr] = lines;
let pos = '';
let cn = '';
const lines = block.split('\n').map(safeString)
const [posCn, wsStr] = lines
let pos = ''
let cn = ''
if (posCn) {
const posMatch = posCn.match(/^([a-zA-Z.]+)(.*)$/);
pos = posMatch ? safeString(posMatch[1]) : '';
cn = posMatch ? safeString(posMatch[2]) : safeString(posCn);
const posMatch = posCn.match(/^([a-zA-Z.]+)(.*)$/)
pos = posMatch ? safeString(posMatch[1]) : ''
cn = posMatch ? safeString(posMatch[2]) : safeString(posCn)
}
const ws = wsStr ? wsStr.split('/').map(safeString) : [];
const ws = wsStr ? wsStr.split('/').map(safeString) : []
return {pos, cn, ws};
});
return { pos, cn, ws }
})
// 5. relWords
const relWordsText = safeString(raw.relWords);
let root = '';
const rels = [];
const relWordsText = safeString(raw.relWords)
let root = ''
const rels = []
if (relWordsText) {
const relLines = relWordsText.split('\n').filter(Boolean);
const relLines = relWordsText.split('\n').filter(Boolean)
if (relLines.length > 0) {
root = safeString(relLines[0].replace(/^词根:/, ''));
let currentPos = '';
let currentWords = [];
root = safeString(relLines[0].replace(/^词根:/, ''))
let currentPos = ''
let currentWords = []
for (let i = 1; i < relLines.length; i++) {
const line = relLines[i].trim();
if (!line) continue;
const line = relLines[i].trim()
if (!line) continue
if (/^[a-z]+\./i.test(line)) {
if (currentPos && currentWords.length > 0) {
rels.push({pos: currentPos, words: currentWords});
rels.push({ pos: currentPos, words: currentWords })
}
currentPos = safeString(line.replace(':', ''));
currentWords = [];
currentPos = safeString(line.replace(':', ''))
currentWords = []
} else if (line.includes(':')) {
const [c, cn] = line.split(':');
currentWords.push({c: safeString(c), cn: safeString(cn)});
const [c, cn] = line.split(':')
currentWords.push({ c: safeString(c), cn: safeString(cn) })
}
}
if (currentPos && currentWords.length > 0) {
rels.push({pos: currentPos, words: currentWords});
rels.push({ pos: currentPos, words: currentWords })
}
}
}
// 6. etymology
const etymology = safeSplit(raw.etymology, '\n\n').map(block => {
const lines = block.split('\n').map(safeString);
const t = lines.shift() || '';
const d = lines.join('\n').trim();
return {t, d};
});
const lines = block.split('\n').map(safeString)
const t = lines.shift() || ''
const d = lines.join('\n').trim()
return { t, d }
})
return getDefaultWord({
id: raw.id,
@@ -364,68 +370,68 @@ export function convertToWord(raw: any) {
sentences,
phrases,
synos,
relWords: {root, rels},
relWords: { root, rels },
etymology,
custom: true
});
custom: true,
})
}
export function cloneDeep<T>(val: T) {
export function cloneDeep<T>(val: T): T {
return JSON.parse(JSON.stringify(val))
}
export function shuffle<T>(array: T[]): T[] {
const result = array.slice(); // 复制数组,避免修改原数组
const result = array.slice() // 复制数组,避免修改原数组
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // 生成 0 ~ i 的随机索引
[result[i], result[j]] = [result[j], result[i]]; // 交换元素
const j = Math.floor(Math.random() * (i + 1)) // 生成 0 ~ i 的随机索引
;[result[i], result[j]] = [result[j], result[i]] // 交换元素
}
return result;
return result
}
export function last<T>(array: T[]): T | undefined {
return array.length > 0 ? array[array.length - 1] : undefined;
return array.length > 0 ? array[array.length - 1] : undefined
}
export function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
let timer: ReturnType<typeof setTimeout> | null = null;
let timer: ReturnType<typeof setTimeout> | null = null
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (timer) clearTimeout(timer);
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args);
}, wait);
};
func.apply(this, args)
}, wait)
}
}
export function throttle<T extends (...args: any[]) => void>(func: T, wait: number) {
let lastTime = 0;
let lastTime = 0
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
const now = Date.now();
const now = Date.now()
if (now - lastTime >= wait) {
func.apply(this, args);
lastTime = now;
func.apply(this, args)
lastTime = now
}
};
}
}
export function reverse<T>(array: T[]): T[] {
return array.slice().reverse();
return array.slice().reverse()
}
export function groupBy<T extends Record<string, any>>(array: T[], key: string) {
return array.reduce<Record<string, T[]>>((result, item) => {
const groupKey = String(item[key]);
(result[groupKey] ||= []).push(item);
return result;
}, {});
const groupKey = String(item[key])
;(result[groupKey] ||= []).push(item)
return result
}, {})
}
//随机取N个
export function getRandomN(arr: any[], n: number) {
const copy = [...arr]
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]] // 交换
const j = Math.floor(Math.random() * (i + 1))
;[copy[i], copy[j]] = [copy[j], copy[i]] // 交换
}
return copy.slice(0, n)
}
@@ -434,8 +440,8 @@ export function getRandomN(arr: any[], n: number) {
export function splitIntoN(arr: any[], n: number) {
const result = []
const len = arr.length
const base = Math.floor(len / n) // 每份至少这么多
let extra = len % n // 前几份多 1 个
const base = Math.floor(len / n) // 每份至少这么多
let extra = len % n // 前几份多 1 个
let index = 0
for (let i = 0; i < n; i++) {
@@ -448,43 +454,43 @@ export function splitIntoN(arr: any[], n: number) {
}
export async function loadJsLib(key: string, url: string) {
if (window[key]) return window[key];
if (window[key]) return window[key]
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const script = document.createElement('script')
// 判断是否是 .mjs 文件,如果是,则使用 type="module"
if (url.endsWith(".mjs")) {
script.type = "module"; // 需要加上 type="module"
script.src = url;
if (url.endsWith('.mjs')) {
script.type = 'module' // 需要加上 type="module"
script.src = url
script.onload = async () => {
try {
// 使用动态 import 加载模块
const module = await import(url); // 动态导入 .mjs 模块
window[key] = module.default || module; // 将模块挂到 window 对象
resolve(window[key]);
const module = await import(url) // 动态导入 .mjs 模块
window[key] = module.default || module // 将模块挂到 window 对象
resolve(window[key])
} catch (err) {
reject(`${key} 加载失败: ${err.message}`);
reject(`${key} 加载失败: ${err.message}`)
}
};
}
} else {
// 如果是非 .mjs 文件,直接按原方式加载
script.src = url;
script.onload = () => resolve(window[key]);
script.src = url
script.onload = () => resolve(window[key])
}
script.onerror = () => reject(key + " 加载失败");
document.head.appendChild(script);
});
script.onerror = () => reject(key + ' 加载失败')
document.head.appendChild(script)
})
}
export function total(arr, key) {
return arr.reduce((a, b) => {
a += b[key];
a += b[key]
return a
}, 0);
}, 0)
}
export function resourceWrap(resource: string, version?: number) {
if (AppEnv.IS_OFFICIAL) {
if (resource.includes('.json')) resource = resource.replace('.json', '');
if (resource.includes('.json')) resource = resource.replace('.json', '')
if (!resource.includes('http')) resource = RESOURCE_PATH + resource
if (version === undefined) {
const store = useBaseStore()
@@ -492,7 +498,7 @@ export function resourceWrap(resource: string, version?: number) {
}
return `${resource}_v${version}.json`
}
return resource;
return resource
}
// check if it is a new user
@@ -501,9 +507,11 @@ export async function isNewUser() {
let base = useBaseStore()
console.log(JSON.stringify(base.$state))
console.log(JSON.stringify(getDefaultBaseState()))
return JSON.stringify(base.$state) === JSON.stringify({...getDefaultBaseState(), ...{load: true}})
return (
JSON.stringify(base.$state) === JSON.stringify({ ...getDefaultBaseState(), ...{ load: true } })
)
}
export function jump2Feedback() {
window.open('https://v.wjx.cn/vm/ev0W7fv.aspx#', '_blank');
window.open('https://v.wjx.cn/vm/ev0W7fv.aspx#', '_blank')
}