This commit is contained in:
Zyronon
2025-12-22 00:48:20 +08:00
parent 5d43d27ef5
commit 083d15646f
3 changed files with 265 additions and 155 deletions

View File

@@ -1,6 +1,10 @@
import http from '@/utils/http.ts'
import { Dict } from '@/types/types.ts'
export function addDict(params?, data?) {
return http<Dict>('dict/addDict', data, params, 'post')
export function copyOfficialDict(params?, data?) {
return http<Dict>('dict/copyOfficialDict', data, params, 'post')
}
export function deleteDict(params?, data?) {
return http<Dict>('dict/delete', data, params, 'post')
}

View File

@@ -38,6 +38,7 @@ import { nanoid } from 'nanoid'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { wordDelete } from '@/apis/words.ts'
import { copyOfficialDict } from '@/apis/dict.ts'
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -147,32 +148,33 @@ async function batchDel(ids: string[]) {
syncDictInMyStudyList()
}
let cloudHandle = async dictId => {
let res = await wordDelete(null, {
wordIds: ids,
dictId,
})
if (res.success) {
tableRef.value.getData()
} else {
return Toast.error(res.msg ?? '删除失败')
}
}
if (AppEnv.CAN_REQUEST) {
if (dict.custom) {
if (dict.sync) {
let res = await wordDelete(null, {
wordIds: ids,
userDictId: dict?.userDictId,
dictId: dict.id,
})
if (res.success) {
tableRef.value.getData()
} else {
return Toast.error(res.msg ?? '删除失败')
}
await cloudHandle(dict.id)
} else {
localHandle()
}
} else {
let r = await add2MyDict({
id: dict.id,
perDayStudyNumber: dict.perDayStudyNumber,
lastLearnIndex: dict.lastLearnIndex,
complete: dict.complete,
})
if (!r.success) return Toast.error(r.msg)
else {
dict.userDictId = r.data
let r = await copyOfficialDict(null, { id: dict.id })
if (r.success) {
await cloudHandle(r.data.id)
getDetail(r.data.id)
} else {
//todo 权限判断,能否复制
return Toast.error(r.msg)
}
}
} else {
@@ -255,11 +257,7 @@ onMounted(async () => {
}
if (base.word.bookList.find(book => book.id === runtimeStore.editDict.id)) {
if (AppEnv.CAN_REQUEST) {
//todo 优化:这里只返回详情
let res = await detail({ id: runtimeStore.editDict.id })
if (res.success) {
runtimeStore.editDict.statistics = res.data.statistics
}
getDetail(runtimeStore.editDict.id)
}
}
loading = false
@@ -270,6 +268,14 @@ onMounted(async () => {
tableRef.value.getData()
})
async function getDetail(id) {
//todo 优化:这里只返回详情
let res = await detail({ id })
if (res.success) {
runtimeStore.editDict = res.data
}
}
function formClose() {
if (isEdit) isEdit = false
else router.back()

View File

@@ -1,38 +1,45 @@
<script setup lang="ts">
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import { useBaseStore } from '@/stores/base.ts'
import { useRouter } from 'vue-router'
import BaseIcon from '@/components/BaseIcon.vue'
import {
_getAccomplishDate,
_getDictDataByUrl, _getStudyProgress,
_getDictDataByUrl,
_nextTick,
isMobile,
loadJsLib,
resourceWrap,
shuffle,
useNav
} from "@/utils";
import BasePage from "@/components/BasePage.vue";
import { DictResource, WordPracticeMode } from "@/types/types.ts";
import { watch } from "vue";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import Book from "@/components/Book.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import Progress from '@/components/base/Progress.vue';
import Toast from '@/components/base/toast/Toast.ts';
import BaseButton from "@/components/BaseButton.vue";
import { getDefaultDict } from "@/types/func.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
import { useSettingStore } from "@/stores/setting.ts";
import { useFetch } from "@vueuse/core";
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
import { myDictList } from "@/apis";
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePracticeSettingDialog.vue";
useNav,
} from '@/utils'
import BasePage from '@/components/BasePage.vue'
import { DictResource, WordPracticeMode } from '@/types/types.ts'
import { watch } from 'vue'
import { getCurrentStudyWord } from '@/hooks/dict.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import Book from '@/components/Book.vue'
import PopConfirm from '@/components/PopConfirm.vue'
import Progress from '@/components/base/Progress.vue'
import Toast from '@/components/base/toast/Toast.ts'
import BaseButton from '@/components/BaseButton.vue'
import { getDefaultDict } from '@/types/func.ts'
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
import PracticeSettingDialog from '@/pages/word/components/PracticeSettingDialog.vue'
import ChangeLastPracticeIndexDialog from '@/pages/word/components/ChangeLastPracticeIndexDialog.vue'
import { useSettingStore } from '@/stores/setting.ts'
import { useFetch } from '@vueuse/core'
import {
AppEnv,
DICT_LIST,
Host,
LIB_JS_URL,
PracticeSaveWordKey,
TourConfig,
} from '@/config/env.ts'
import { myDictList } from '@/apis'
import PracticeWordListDialog from '@/pages/word/components/PracticeWordListDialog.vue'
import ShufflePracticeSettingDialog from '@/pages/word/components/ShufflePracticeSettingDialog.vue'
import { deleteDict } from '@/apis/dict.ts'
const store = useBaseStore()
const settingStore = useSettingStore()
@@ -49,41 +56,45 @@ let currentStudy = $ref({
shuffle: [],
})
watch(() => store.load, n => {
if (n) {
init()
_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: 'step1',
text: '点击这里选择一本词典开始学习',
attachTo: {
element: '#step1',
on: 'bottom'
},
buttons: [
{
text: `下一步1/${TourConfig.total}`,
action() {
tour.next()
router.push('/dict-list')
}
}
]
});
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r && !isMobile()) tour.start();
}, 500)
}
}, { immediate: true })
watch(
() => store.load,
n => {
if (n) {
init()
_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: 'step1',
text: '点击这里选择一本词典开始学习',
attachTo: {
element: '#step1',
on: 'bottom',
},
buttons: [
{
text: `下一步1/${TourConfig.total}`,
action() {
tour.next()
router.push('/dict-list')
},
},
],
})
const r = localStorage.getItem('tour-guide')
if (settingStore.first && !r && !isMobile()) tour.start()
}, 500)
}
},
{ immediate: true }
)
async function init() {
if (AppEnv.CAN_REQUEST) {
let res = await myDictList({ type: "word" })
let res = await myDictList({ type: 'word' })
if (res.success) {
store.setState(Object.assign(store.$state, res.data))
}
@@ -122,7 +133,7 @@ function startPractice() {
perDayStudyNumber: store.sdict.perDayStudyNumber,
custom: store.sdict.custom,
complete: store.sdict.complete,
wordPracticeMode: settingStore.wordPracticeMode
wordPracticeMode: settingStore.wordPracticeMode,
})
//把是否是第一次设置为false
settingStore.first = false
@@ -147,21 +158,30 @@ async function goDictDetail(val: DictResource) {
let isManageDict = $ref(false)
let selectIds = $ref([])
function handleBatchDel() {
selectIds.forEach(id => {
let r = store.word.bookList.findIndex(v => v.id === id)
if (r !== -1) {
if (store.word.studyIndex === r) {
store.word.studyIndex = -1
}
if (store.word.studyIndex > r) {
store.word.studyIndex--
}
store.word.bookList.splice(r, 1)
async function handleBatchDel() {
if (AppEnv.CAN_REQUEST) {
let res = await deleteDict(null, selectIds)
if (res.success) {
init()
} else {
Toast.error(res.msg)
}
})
selectIds = []
Toast.success("删除成功!")
} else {
selectIds.forEach(id => {
let r = store.word.bookList.findIndex(v => v.id === id)
if (r !== -1) {
if (store.word.studyIndex === r) {
store.word.studyIndex = -1
}
if (store.word.studyIndex > r) {
store.word.studyIndex--
}
store.word.bookList.splice(r, 1)
}
})
selectIds = []
Toast.success('删除成功!')
}
}
function toggleSelect(item) {
@@ -212,11 +232,17 @@ async function onShufflePracticeSettingOk(total) {
localStorage.removeItem(PracticeSaveWordKey.key)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
currentStudy.shuffle = shuffle(store.sdict.words.slice(0, store.sdict.lastLearnIndex).filter(v => !ignoreList.includes(v.word))).slice(0, total)
nav('practice-words/' + store.sdict.id, {}, {
taskWords: currentStudy,
total //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
})
currentStudy.shuffle = shuffle(
store.sdict.words.slice(0, store.sdict.lastLearnIndex).filter(v => !ignoreList.includes(v.word))
).slice(0, total)
nav(
'practice-words/' + store.sdict.id,
{},
{
taskWords: currentStudy,
total, //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
}
)
}
async function saveLastPracticeIndex(e) {
@@ -229,20 +255,19 @@ async function saveLastPracticeIndex(e) {
currentStudy = getCurrentStudyWord()
}
const {
data: recommendDictList,
isFetching
} = useFetch(resourceWrap(DICT_LIST.WORD.RECOMMENDED)).json()
const { data: recommendDictList, isFetching } = useFetch(
resourceWrap(DICT_LIST.WORD.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 gap-4">
@@ -259,11 +284,17 @@ let isNewHost = $ref(window.location.host === Host)
<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>
<Progress
size="large"
:percentage="store.currentStudyProgress"
:show-text="false"
></Progress>
<div class="text-sm flex justify-between">
<span>已完成 {{ progressTextRight }} / {{ store.sdict.words.length }} </span>
<span v-if="store.sdict.id">
预计完成日期{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
预计完成日期{{
_getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber)
}}
</span>
</div>
</div>
@@ -274,8 +305,11 @@ let isNewHost = $ref(window.location.host === Host)
<span>选择词典</span>
</div>
</BaseButton>
<PopConfirm :disabled="!isSaveData" title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(() => showChangeLastPracticeIndexDialog = true)">
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(() => (showChangeLastPracticeIndexDialog = true))"
>
<BaseButton type="info" size="small" v-if="store.sdict.id">
<div class="center gap-1">
<IconFluentSlideTextTitleEdit20Regular />
@@ -297,29 +331,37 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</div>
<div class="flex-1 w-full mt-4 md:mt-0" :class="!store.sdict.id && 'opacity-30 cursor-not-allowed'">
<div
class="flex-1 w-full mt-4 md:mt-0"
:class="!store.sdict.id && 'opacity-30 cursor-not-allowed'"
>
<div class="flex justify-between">
<div class="flex items-center gap-2">
<div class="p-2 center rounded-full bg-white ">
<div class="p-2 center rounded-full bg-white">
<IconFluentStar20Filled class="text-lg color-amber" />
</div>
<div class="text-xl font-bold">
{{ isSaveData ? '上次任务' : '今日任务' }}
</div>
<span class="color-link cursor-pointer" v-if="store.sdict.id"
@click="showPracticeWordListDialog = true">词表</span>
<span
class="color-link cursor-pointer"
v-if="store.sdict.id"
@click="showPracticeWordListDialog = true"
>词表</span
>
</div>
<div class="flex gap-1 items-center" v-if="store.sdict.id">
每日目标
<div style="color:#ac6ed1;" class="bg-third px-2 h-10 flex center text-2xl rounded">
<div style="color: #ac6ed1" class="bg-third px-2 h-10 flex center text-2xl rounded">
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
</div>
个单词
<PopConfirm :disabled="!isSaveData" title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(() => showPracticeSettingDialog = true)">
<BaseButton type="info" size="small">更改
</BaseButton>
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(() => (showPracticeSettingDialog = true))"
>
<BaseButton type="info" size="small">更改 </BaseButton>
</PopConfirm>
</div>
</div>
@@ -340,32 +382,45 @@ let isNewHost = $ref(window.location.host === Host)
</template>
</div>
<div class="flex items-end mt-4">
<BaseButton size="large" class="flex-1" :disabled="!store.sdict.id" :loading="loading" @click="startPractice">
<BaseButton
size="large"
class="flex-1"
:disabled="!store.sdict.id"
:loading="loading"
@click="startPractice"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl" />
</div>
</BaseButton>
<div v-if="false" class="w-full flex box-border cp color-white">
<div @click="startPractice"
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50">
<div v-if="false" class="w-full flex box-border cp color-white">
<div
@click="startPractice"
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50"
>
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl" />
</div>
<div class="relative group">
<div
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border">
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border"
>
<IconFluentChevronDown20Regular />
</div>
<div class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95
group-hover:opacity-100 group-hover:scale-100
transition-all duration-150 pointer-events-none group-hover:pointer-events-auto">
<div
class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95 group-hover:opacity-100 group-hover:scale-100 transition-all duration-150 pointer-events-none group-hover:pointer-events-auto"
>
<div>
<BaseButton size="large" type="orange" :loading="loading"
@click="check(() => showShufflePracticeSettingDialog = true)">
<BaseButton
size="large"
type="orange"
:loading="loading"
@click="check(() => (showShufflePracticeSettingDialog = true))"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">随机复习</span>
<IconFluentArrowShuffle20Filled class="text-xl" />
@@ -373,8 +428,12 @@ let isNewHost = $ref(window.location.host === Host)
</BaseButton>
</div>
<div>
<BaseButton size="large" type="orange" :loading="loading"
@click="check(() => showShufflePracticeSettingDialog = true)">
<BaseButton
size="large"
type="orange"
:loading="loading"
@click="check(() => (showShufflePracticeSettingDialog = true))"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">重新学习</span>
<IconFluentArrowShuffle20Filled class="text-xl" />
@@ -385,8 +444,13 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</div>
<BaseButton v-if="store.sdict.id && store.sdict.lastLearnIndex" size="large" type="orange" :loading="loading"
@click="check(() => showShufflePracticeSettingDialog = true)">
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large"
type="orange"
:loading="loading"
@click="check(() => (showShufflePracticeSettingDialog = true))"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">随机复习</span>
<IconFluentArrowShuffle20Filled class="text-xl" />
@@ -400,22 +464,44 @@ let isNewHost = $ref(window.location.host === Host)
<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 />
</BaseIcon>
</PopConfirm>
<div class="color-link cursor-pointer" v-if="store.word.bookList.length > 3"
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
<div
class="color-link cursor-pointer"
v-if="store.word.bookList.length > 3"
@click="
() => {
isManageDict = !isManageDict
selectIds = []
}
"
>
{{ isManageDict ? '取消' : '管理词典' }}
</div>
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">
创建个人词典
</div>
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)" :show-checkbox="isManageDict && j >= 3"
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)" />
<div class="flex gap-4 flex-wrap mt-4">
<Book
:is-add="false"
quantifier="个词"
:item="item"
:checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)"
:show-checkbox="isManageDict && j >= 3"
v-for="(item, j) in store.word.bookList"
@click="goDictDetail(item)"
/>
<Book :is-add="true" @click="router.push('/dict-list')" />
</div>
</div>
@@ -428,21 +514,35 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
<Book :is-add="false" quantifier="个词" :item="item as any" v-for="(item, j) in recommendDictList"
@click="goDictDetail(item as any)" />
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
<Book
:is-add="false"
quantifier="个词"
:item="item as any"
v-for="(item, j) in recommendDictList"
@click="goDictDetail(item as any)"
/>
</div>
</div>
</BasePage>
<PracticeSettingDialog :show-left-option="false" v-model="showPracticeSettingDialog" @ok="savePracticeSetting" />
<PracticeSettingDialog
:show-left-option="false"
v-model="showPracticeSettingDialog"
@ok="savePracticeSetting"
/>
<ChangeLastPracticeIndexDialog v-model="showChangeLastPracticeIndexDialog" @ok="saveLastPracticeIndex" />
<ChangeLastPracticeIndexDialog
v-model="showChangeLastPracticeIndexDialog"
@ok="saveLastPracticeIndex"
/>
<PracticeWordListDialog :data="currentStudy" v-model="showPracticeWordListDialog" />
<ShufflePracticeSettingDialog v-model="showShufflePracticeSettingDialog" @ok="onShufflePracticeSettingOk" />
<ShufflePracticeSettingDialog
v-model="showShufflePracticeSettingDialog"
@ok="onShufflePracticeSettingOk"
/>
</template>
<style scoped lang="scss">