feat:save
This commit is contained in:
@@ -60,7 +60,7 @@
|
||||
|
||||
|
||||
//修改element-ui的进度条底色
|
||||
--el-border-color-lighter: #e2e5ed !important;
|
||||
--el-border-color-lighter: #d1d5df !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -70,7 +70,6 @@
|
||||
}
|
||||
|
||||
html.dark {
|
||||
|
||||
--color-primary: #0E1217;
|
||||
--color-second: rgb(30, 31, 34);
|
||||
--color-third: rgb(43, 45, 48);
|
||||
@@ -104,7 +103,7 @@ html.dark {
|
||||
--color-textarea-bg: rgb(43, 45, 48);
|
||||
--color-article: white;
|
||||
|
||||
--el-border-color-lighter: var(--color-third) !important;
|
||||
--el-border-color-lighter: rgb(73, 77, 82) !important;
|
||||
|
||||
.footer {
|
||||
&.hide {
|
||||
@@ -251,57 +250,12 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
//@supports (scrollbar-width: thin) {
|
||||
// * {
|
||||
// scrollbar-color: var(--color-scrollbar) #f3f4f9;
|
||||
// scrollbar-width: thin;
|
||||
// }
|
||||
//}
|
||||
|
||||
footer {
|
||||
$footer-height: 60rem;
|
||||
box-sizing: content-box;
|
||||
height: $footer-height;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
}
|
||||
|
||||
|
||||
.panel-page-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding-bottom: var(--space);
|
||||
box-sizing: border-box;
|
||||
|
||||
.list-header {
|
||||
min-height: 3rem;
|
||||
padding: .6rem var(--space);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
color: var(--color-font-3);
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .6rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.right {
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.scroll {
|
||||
@@ -316,17 +270,6 @@ footer {
|
||||
padding: 0 var(--space);
|
||||
}
|
||||
|
||||
.common-list1 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
gap: 10rem;
|
||||
padding: 0 var(--space);
|
||||
}
|
||||
|
||||
.list-item-wrapper {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
@@ -367,7 +310,6 @@ footer {
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
color: var(--color-icon-hightlight);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -376,10 +318,6 @@ footer {
|
||||
.item-sub-title {
|
||||
color: var(--color-sub-text);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-icon-hightlight);
|
||||
}
|
||||
}
|
||||
|
||||
.fill {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Article, Word} from "@/types.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {markRaw} from "vue";
|
||||
|
||||
|
||||
export function useWordOptions() {
|
||||
@@ -16,25 +15,22 @@ export function useWordOptions() {
|
||||
store.collectWord.words.splice(rIndex, 1)
|
||||
} else {
|
||||
store.collectWord.words.push(val)
|
||||
// store.collectWord.words = markRaw(store.collectWord.words.concat([val]))
|
||||
}
|
||||
store.collectWord.length = store.collectWord.words.length
|
||||
}
|
||||
|
||||
function isWordSimple(val: Word) {
|
||||
return !!store.known.words.find(v => v.word.toLowerCase() === val.word.toLowerCase())
|
||||
return !!store.knownWords.includes(val.word.toLowerCase())
|
||||
}
|
||||
|
||||
function toggleWordSimple(val: Word) {
|
||||
let rIndex = store.known.words.findIndex(v => v.word.toLowerCase() === val.word.toLowerCase())
|
||||
let rIndex = store.knownWords.findIndex(v => v === val.word.toLowerCase())
|
||||
if (rIndex > -1) {
|
||||
store.known.words.splice(rIndex, 1)
|
||||
} else {
|
||||
let rIndex = store.collectWord.words.findIndex(v => v.word.toLowerCase() === val.word.toLowerCase())
|
||||
if (rIndex > -1) {
|
||||
store.collectWord.words.splice(rIndex, 1)
|
||||
}
|
||||
store.known.words.push(val)
|
||||
}
|
||||
store.known.length = store.known.words.length
|
||||
}
|
||||
|
||||
function delWrongWord(val: Word) {
|
||||
@@ -42,6 +38,7 @@ export function useWordOptions() {
|
||||
if (rIndex > -1) {
|
||||
store.wrong.words.splice(rIndex, 1)
|
||||
}
|
||||
store.wrong.length = store.wrong.words.length
|
||||
}
|
||||
|
||||
function delSimpleWord(val: Word) {
|
||||
@@ -49,6 +46,7 @@ export function useWordOptions() {
|
||||
if (rIndex > -1) {
|
||||
store.known.words.splice(rIndex, 1)
|
||||
}
|
||||
store.known.length = store.known.words.length
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -75,6 +73,7 @@ export function useArticleOptions() {
|
||||
} else {
|
||||
store.collectArticle.articles.push(val)
|
||||
}
|
||||
store.collectArticle.length = store.collectArticle.articles.length
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -388,7 +388,7 @@ function importData(e) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row footer">
|
||||
<div class="row">
|
||||
<label class="item-title"></label>
|
||||
<div class="wrapper">
|
||||
<BaseButton @click="resetShortcutKeyMap">恢复默认</BaseButton>
|
||||
@@ -584,10 +584,6 @@ function importData(e) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 1.3rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: .6rem;
|
||||
font-size: .8rem;
|
||||
|
||||
@@ -15,7 +15,9 @@ import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
|
||||
import Input from "@/pages/pc/components/Input.vue";
|
||||
import {computed} from "vue";
|
||||
import Book from "@/pages/pc/components/Book.vue";
|
||||
import {ElProgress} from 'element-plus';
|
||||
import {ElMessage, ElProgress} from 'element-plus';
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
|
||||
const {nav} = useNav()
|
||||
const base = useBaseStore()
|
||||
@@ -26,9 +28,6 @@ let showAddChooseDialog = $ref(false)
|
||||
let showSearchDialog = $ref(false)
|
||||
let searchKey = $ref('')
|
||||
|
||||
function clickEvent(e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
|
||||
async function getBookDetail(val: DictResource) {
|
||||
let r = await getArticleBookDataByUrl(val)
|
||||
@@ -66,6 +65,41 @@ function startStudy() {
|
||||
}
|
||||
router.push('/study-article')
|
||||
}
|
||||
|
||||
|
||||
let isMultiple = $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)
|
||||
}
|
||||
})
|
||||
selectIds = []
|
||||
ElMessage.success("删除成功!")
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
let rIndex = selectIds.findIndex(v => v === item.id)
|
||||
if (rIndex > -1) {
|
||||
selectIds.splice(rIndex, 1)
|
||||
} else {
|
||||
selectIds.push(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function goDictDetail(val: DictResource) {
|
||||
runtimeStore.editDict = getDefaultDict(val)
|
||||
nav('book-detail', {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -80,79 +114,44 @@ function startStudy() {
|
||||
<BaseIcon @click="showSearchDialog = true"
|
||||
:icon="base.currentBook.name ? 'gg:arrows-exchange':'fluent:add-20-filled'"/>
|
||||
</div>
|
||||
<div class="rounded-xl bg-slate-800 flex items-center py-3 px-5 text-white cursor-pointer"
|
||||
:class="base.currentBook.name || 'opacity-70 cursor-not-allowed'"
|
||||
@click="startStudy">
|
||||
开始学习
|
||||
</div>
|
||||
<BaseButton
|
||||
size="large"
|
||||
@click="startStudy"
|
||||
:disabled="!base.currentBook.name"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>开始学习</span>
|
||||
<Icon icon="icons8:right-round" class="text-2xl"/>
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="mt-5 text-sm">已学习{{ base.currentBook.lastLearnIndex }}篇文章</div>
|
||||
<ElProgress class="mt-1" :percentage="base.currentBookProgress" :show-text="false"></ElProgress>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col">
|
||||
<div class="title">
|
||||
我的
|
||||
</div>
|
||||
<div class="grid grid-cols-6 gap-4 mt-4">
|
||||
<Book :is-add="false"
|
||||
quantifier="篇"
|
||||
:item="item"
|
||||
v-for="item in store.article.bookList"
|
||||
@click="getBookDetail2(item)"/>
|
||||
<Book :is-add="true"
|
||||
@click="showAddChooseDialog = true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="title">我的书籍</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<PopConfirm title="确认删除所有选中书籍?" @confirm="handleBatchDel" v-if="selectIds.length">
|
||||
<BaseIcon class="del" title="删除" icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</PopConfirm>
|
||||
|
||||
<div class="card">
|
||||
<div class="title flex justify-between">
|
||||
<span>书籍列表</span>
|
||||
<BaseIcon @click="showSearchDialog = true" icon="fluent:search-24-regular"/>
|
||||
</div>
|
||||
<div class="grid grid-cols-6 gap-4 mt-4">
|
||||
<Book :is-add="false"
|
||||
quantifier="篇"
|
||||
:item="item as Dict"
|
||||
v-for="item in enArticle"
|
||||
@click="getBookDetail(item)"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog v-model="showAddChooseDialog" title="选项">
|
||||
<div class="color-main px-6 w-100">
|
||||
<div class="cursor-pointer hover:bg-black/10 p-2 rounded"
|
||||
@click="showAddChooseDialog = false,showSearchDialog = true">选择一本书籍
|
||||
</div>
|
||||
<p class="cursor-pointer hover:bg-black/10 p-2 rounded" @click="addBook">创建自己的书籍</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model="showSearchDialog"
|
||||
:show-close="false"
|
||||
@close="searchKey = ''"
|
||||
:header="false">
|
||||
<div class="color-main w-140">
|
||||
<div class="p-4">
|
||||
<Input v-if="showSearchDialog" :autofocus="true" v-model="searchKey"/>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div v-if="searchList.length">
|
||||
<div class="p-4 min-h-40 max-h-140 overflow-auto">
|
||||
<div class="flex justify-between my-2 hover:bg-black/10 p-2 rounded"
|
||||
v-for="dict in searchList"
|
||||
@click="getBookDetail(dict)">
|
||||
<div class="name">{{ dict.name }}</div>
|
||||
<div class="">{{ dict.length }}篇</div>
|
||||
</div>
|
||||
<div class="color-blue cursor-pointer" v-if="store.article.bookList.length > 1"
|
||||
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理书籍' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="h-40 center flex-col text-xl color-main">
|
||||
<div> 请输入书籍名称搜索</div>
|
||||
<div>或直接在书籍列表选中</div>
|
||||
<div class="color-blue cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人书籍</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<div class="grid grid-cols-6 gap-4 mt-4">
|
||||
<Book :is-add="false" quantifier="篇" :item="item" :checked="selectIds.includes(item.id)"
|
||||
@check="() => toggleSelect(item)"
|
||||
:show-checkbox="isMultiple && j >= 1"
|
||||
v-for="(item, j) in store.article.bookList"
|
||||
@click="goDictDetail(item)"/>
|
||||
<Book :is-add="true" @click="router.push('/dict-list')"/>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ useWindowClick(() => showExport = false)
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="import">
|
||||
<BaseButton size="small">导入</BaseButton>
|
||||
<BaseButton>导入</BaseButton>
|
||||
<input type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
@change="e => emit('importData',e)">
|
||||
@@ -191,7 +191,7 @@ useWindowClick(() => showExport = false)
|
||||
<div class="export"
|
||||
style="position: relative"
|
||||
@click.stop="null">
|
||||
<BaseButton size="small" @click="showExport = true">导出</BaseButton>
|
||||
<BaseButton @click="showExport = true">导出</BaseButton>
|
||||
<MiniDialog
|
||||
v-model="showExport"
|
||||
style="width: 80rem;bottom: calc(100% + 10rem);top:unset;"
|
||||
@@ -200,14 +200,14 @@ useWindowClick(() => showExport = false)
|
||||
导出选项
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" @click="emit('exportData',{type:'all',data:[]})">全部文章</BaseButton>
|
||||
<BaseButton @click="emit('exportData',{type:'all',data:[]})">全部文章</BaseButton>
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" @click="emit('exportData',{type:'chapter',data:article})">当前章节</BaseButton>
|
||||
<BaseButton @click="emit('exportData',{type:'chapter',data:article})">当前章节</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
<BaseButton size="small" @click="add">新增</BaseButton>
|
||||
<BaseButton @click="add">新增</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<EditArticle2
|
||||
|
||||
@@ -459,26 +459,26 @@ let showQuestions = $ref(false)
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="translate-bottom mb-10" v-if="settingStore.translate">
|
||||
<header class="mb-4">
|
||||
<div class="text-2xl center">{{ props.article.titleTranslate }}</div>
|
||||
</header>
|
||||
<template v-if="getTranslateText(article).length">
|
||||
<div class="text-xl mb-4 indent-8" v-for="t in getTranslateText(article)">{{ t }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<BaseButton @click="showQuestions =! showQuestions">显示题目</BaseButton>
|
||||
</div>
|
||||
<div class="toggle" v-if="showQuestions">
|
||||
<QuestionForm :questions="article.questions"
|
||||
:duration="300"
|
||||
:immediateFeedback="false"
|
||||
:randomize="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="false">
|
||||
<div class="translate-bottom mb-10" v-if="settingStore.translate">
|
||||
<header class="mb-4">
|
||||
<div class="text-2xl center">{{ props.article.titleTranslate }}</div>
|
||||
</header>
|
||||
<template v-if="getTranslateText(article).length">
|
||||
<div class="text-xl mb-4 indent-8" v-for="t in getTranslateText(article)">{{ t }}</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="center">
|
||||
<BaseButton @click="showQuestions =! showQuestions">显示题目</BaseButton>
|
||||
</div>
|
||||
<div class="toggle" v-if="showQuestions">
|
||||
<QuestionForm :questions="article.questions"
|
||||
:duration="300"
|
||||
:immediateFeedback="false"
|
||||
:randomize="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -510,6 +510,7 @@ let showQuestions = $ref(false)
|
||||
.titleTranslate {
|
||||
@extend .title;
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0.5rem;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ import {useBaseStore} from "@/stores/base.ts";
|
||||
import EditSingleArticleModal from "@/pages/pc/article/components/EditSingleArticleModal.vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -19,6 +16,7 @@ import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
|
||||
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import {genArticleSectionData, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {ElProgress} from 'element-plus';
|
||||
import router from "@/router.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const statisticsStore = usePracticeStore()
|
||||
@@ -53,12 +51,13 @@ function next() {
|
||||
}
|
||||
|
||||
function init() {
|
||||
//todo 这个页面,直接访问白屏
|
||||
if (!store.currentBook.articles.length) return
|
||||
if (!store.currentBook?.articles?.length) {
|
||||
router.push('/article')
|
||||
return
|
||||
}
|
||||
articleData.articles = cloneDeep(store.currentBook.articles)
|
||||
getCurrentPractice()
|
||||
console.log('inin', articleData.article)
|
||||
|
||||
}
|
||||
|
||||
function setArticle(val: Article) {
|
||||
@@ -125,8 +124,8 @@ function edit(val: Article = articleData.article) {
|
||||
|
||||
function wrong(word: Word) {
|
||||
let lowerName = word.word.toLowerCase();
|
||||
if (!store.wrong.originWords.find((v: Word) => v.word.toLowerCase() === lowerName)) {
|
||||
store.wrong.originWords.push(word)
|
||||
if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === lowerName)) {
|
||||
store.wrong.words.push(word)
|
||||
}
|
||||
if (!store.knownWordsWithSimpleWords.includes(lowerName)) {
|
||||
}
|
||||
@@ -288,46 +287,33 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
|
||||
<div class="panel-wrapper">
|
||||
<Panel>
|
||||
<template v-slot="{active}">
|
||||
<div class="panel-page-item pl-4">
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="title">
|
||||
{{ store.currentBook.name }}
|
||||
</div>
|
||||
<BaseIcon
|
||||
:title="`下一篇(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
|
||||
v-if="store.currentBook.lastLearnIndex < articleData.articles.length - 1"
|
||||
@click="emitter.emit(EventKey.continueStudy)"
|
||||
icon="octicon:arrow-right-24"/>
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ articleData.articles.length }}篇文章
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArticleList
|
||||
:isActive="active"
|
||||
:static="false"
|
||||
:show-translate="settingStore.translate"
|
||||
@click="handleChangeChapterIndex"
|
||||
:active-id="articleData.article.id"
|
||||
:list="articleData.articles ">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isArticleCollect(item)"
|
||||
class="collect"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
</template>
|
||||
</ArticleList>
|
||||
</div>
|
||||
<template v-slot:title>
|
||||
<span>{{
|
||||
store.currentBook.name
|
||||
}} ({{ store.currentBook.lastLearnIndex + 1 }} / {{ articleData.articles.length }})</span>
|
||||
</template>
|
||||
<div class="panel-page-item pl-4">
|
||||
<ArticleList
|
||||
:isActive="true"
|
||||
:static="false"
|
||||
:show-translate="settingStore.translate"
|
||||
@click="handleChangeChapterIndex"
|
||||
:active-id="articleData.article.id"
|
||||
:list="articleData.articles ">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isArticleCollect(item)"
|
||||
class="collect"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click="toggleArticleCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
</template>
|
||||
</ArticleList>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const studyProgress = $computed(() => {
|
||||
<ElCheckbox v-if="showCheckbox"
|
||||
:model-value="checked"
|
||||
@click.stop="$emit('check')"
|
||||
class="absolute left-3 bottom-2"/>
|
||||
class="absolute left-0 bottom-0 h-5!"/>
|
||||
<div class="custom" v-if="item.custom">自定义</div>
|
||||
</template>
|
||||
<div v-else class="center h-full">
|
||||
|
||||
@@ -1,217 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
|
||||
import {computed, provide, watch} from "vue"
|
||||
import {Dict, DictType, ShortcutKey} from "@/types.ts"
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue"
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {computed, provide} from "vue"
|
||||
import {ShortcutKey} from "@/types.ts"
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import Close from "@/components/icon/Close.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {useArticleOptions, useWordOptions} from "@/hooks/dict.ts";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {emitter, EventKey, useEvent} from "@/utils/eventBus.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useNav} from "@/utils";
|
||||
import WordList from "@/pages/pc/components/list/WordList.vue";
|
||||
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
|
||||
import Slide from "@/pages/pc/components/Slide.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
type?: DictType
|
||||
}>(), {
|
||||
type: DictType.word
|
||||
})
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
let tabIndex = $ref(0)
|
||||
provide('tabIndex', computed(() => tabIndex))
|
||||
|
||||
watch(() => settingStore.showPanel, n => {
|
||||
if (n) {
|
||||
tabIndex = 0
|
||||
}
|
||||
})
|
||||
|
||||
function changeIndex(dict: Dict) {
|
||||
store.changeDict(dict)
|
||||
emitter.emit(EventKey.changeDict)
|
||||
}
|
||||
|
||||
useEvent(EventKey.changeDict, () => {
|
||||
tabIndex = 0
|
||||
})
|
||||
|
||||
const {
|
||||
delWrongWord,
|
||||
delSimpleWord,
|
||||
toggleWordCollect,
|
||||
} = useWordOptions()
|
||||
|
||||
const {
|
||||
toggleArticleCollect
|
||||
} = useArticleOptions()
|
||||
|
||||
const {nav} = useNav()
|
||||
|
||||
const showCollectToggleButton = $computed(() => {
|
||||
if (props.type === DictType.word) return store.collectWord.words.length
|
||||
return store.collectArticle.articles.length
|
||||
})
|
||||
|
||||
function changeCollect() {
|
||||
if (props.type === DictType.word) {
|
||||
changeIndex(store.collectWord)
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div class="panel anim" v-show="settingStore.showPanel">
|
||||
<header>
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">当前</div>
|
||||
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">{{ store.collectWord.name }}</div>
|
||||
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">{{ store.known.name }}</div>
|
||||
<div class="tab" :class="tabIndex === 3 && 'active'" @click="tabIndex = 3">{{ store.wrong.name }}</div>
|
||||
</div>
|
||||
<header class="flex justify-between items-center py-3 px-space">
|
||||
<div class="color-main"><slot name="title"></slot></div>
|
||||
<Tooltip
|
||||
:title="`关闭(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
|
||||
>
|
||||
<Close @click="settingStore.showPanel = false"/>
|
||||
</Tooltip>
|
||||
</header>
|
||||
<Slide :slide-count="4" :step="tabIndex">
|
||||
<div class="slide-item">
|
||||
<slot :active="tabIndex === 0 && settingStore.showPanel"></slot>
|
||||
</div>
|
||||
<div class="slide-item">
|
||||
<div class="panel-page-item">
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="dict-name" v-if="props.type === DictType.word && store.collectWord.words.length">
|
||||
{{ store.collectWord.words.length }}个单词
|
||||
</div>
|
||||
<div class="dict-name" v-if="props.type === DictType.article && store.collectArticle.articles.length">
|
||||
{{ store.collectArticle.articles.length }}篇文章
|
||||
</div>
|
||||
<BaseIcon icon="fluent:add-12-regular" title="添加" @click="nav('edit-word-dict',{type:0})"/>
|
||||
</div>
|
||||
<template v-if="showCollectToggleButton">
|
||||
<PopConfirm
|
||||
:title="`确认切换?`"
|
||||
@confirm="changeCollect"
|
||||
>
|
||||
<BaseButton size="small">切换</BaseButton>
|
||||
</PopConfirm>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="props.type === DictType.word">
|
||||
<WordList
|
||||
v-if="store.collectWord.words.length"
|
||||
class="word-list pl-4"
|
||||
:list="store.collectWord.words">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArticleList
|
||||
v-if="store.collectArticle.articles.length"
|
||||
:list="store.collectArticle.articles">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click.stop="toggleArticleCollect(item)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</ArticleList>
|
||||
<Empty v-else/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide-item">
|
||||
<div class="panel-page-item">
|
||||
<div class="list-header">
|
||||
<div class="left">
|
||||
<div class="dict-name">总词数:{{ store.known.words.length }}</div>
|
||||
<BaseIcon icon="fluent:add-12-regular" title="添加" @click="nav('edit-word-dict',{type:2})"/>
|
||||
</div>
|
||||
<template v-if="store.known.words.length">
|
||||
<PopConfirm
|
||||
:title="`确认切换?`"
|
||||
@confirm="changeIndex( store.known)"
|
||||
>
|
||||
<BaseButton size="small">切换</BaseButton>
|
||||
</PopConfirm>
|
||||
</template>
|
||||
</div>
|
||||
<WordList
|
||||
v-if="store.known.words.length"
|
||||
class="word-list pl-4"
|
||||
:list="store.known.words">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click.stop="delSimpleWord(item)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide-item">
|
||||
<div class="panel-page-item" v-if="store.wrong.words.length">
|
||||
<div class="list-header">
|
||||
<div class="dict-name">总词数:{{ store.wrong.words.length }}</div>
|
||||
<PopConfirm
|
||||
:title="`确认切换?`"
|
||||
@confirm="changeIndex( store.wrong)"
|
||||
>
|
||||
<BaseButton size="small">切换</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
<WordList
|
||||
class="word-list pl-4"
|
||||
:list="store.wrong.words">
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
class="del"
|
||||
@click.stop="delWrongWord(item)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</WordList>
|
||||
</div>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</Slide>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
$header-height: 3rem;
|
||||
.slide-item {
|
||||
width: var(--panel-width);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: .5rem;
|
||||
width: var(--panel-width);
|
||||
@@ -223,41 +41,5 @@ $header-height: 3rem;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
& > header {
|
||||
min-height: 3rem;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .6rem .9rem;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
gap: 1rem;
|
||||
|
||||
.close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .9rem;
|
||||
font-size: .8rem;
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
word-break: keep-all;
|
||||
font-size: 1rem;
|
||||
transition: all .3s;
|
||||
color: gray;
|
||||
|
||||
&.active {
|
||||
color: var(--color-select-bg);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -172,10 +172,8 @@ async function cancel() {
|
||||
<div v-if="content" class="content">{{ content }}</div>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="footer">
|
||||
<div class="left">
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseButton type="link" @click="cancel">{{ cancelButtonText }}</BaseButton>
|
||||
<BaseButton type="info" @click="cancel">{{ cancelButtonText }}</BaseButton>
|
||||
<BaseButton
|
||||
:loading="confirmButtonLoading"
|
||||
@click="ok">{{ confirmButtonText }}
|
||||
@@ -331,39 +329,8 @@ $header-height: 4rem;
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .5rem 1.6rem;
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.text {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
gap: var(--space);
|
||||
}
|
||||
justify-content: flex-end;
|
||||
padding: var(--space);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ const {toggleTheme} = useTheme()
|
||||
@click="settingStore.sideExpand = !settingStore.sideExpand"
|
||||
:icon="settingStore.sideExpand?'formkit:left':'formkit:right'"/>
|
||||
<BaseIcon
|
||||
v-if="settingStore.sideExpand"
|
||||
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
@click="toggleTheme"
|
||||
:icon="settingStore.theme === 'light' ? 'ep:moon' : 'tabler:sun'"/>
|
||||
|
||||
@@ -60,7 +60,6 @@ let data = $ref<StudyData>({
|
||||
wrongWords: [],
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
let dictId = route.query.q
|
||||
//如果url里有词典id,那么直接请求词典数据,并加到bookList里面进行学习
|
||||
@@ -394,47 +393,47 @@ useEvents([
|
||||
</div>
|
||||
<div class="word-panel-wrapper">
|
||||
<Panel>
|
||||
<template v-slot="{active}">
|
||||
<div class="panel-page-item pl-4"
|
||||
>
|
||||
<WordList
|
||||
v-if="data.words.length"
|
||||
:is-active="active"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isWordCollect(item)"
|
||||
class="collect"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
<BaseIcon
|
||||
v-if="!isWordSimple(item)"
|
||||
class="easy"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
title="标记为已掌握"
|
||||
icon="material-symbols:check-circle-outline-rounded"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
title="取消标记已掌握"
|
||||
icon="material-symbols:check-circle-rounded"/>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
<template v-slot:title>
|
||||
<span>{{ store.sdict.name }} ({{ data.index + 1 }} / {{ data.words.length }})</span>
|
||||
</template>
|
||||
<div class="panel-page-item pl-4">
|
||||
<WordList
|
||||
v-if="data.words.length"
|
||||
:is-active="true"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
v-if="!isWordCollect(item)"
|
||||
class="collect"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
title="收藏" icon="ph:star"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
title="取消收藏" icon="ph:star-fill"/>
|
||||
<BaseIcon
|
||||
v-if="!isWordSimple(item)"
|
||||
class="easy"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
title="标记为已掌握"
|
||||
icon="material-symbols:check-circle-outline-rounded"/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
class="fill"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
title="取消标记已掌握"
|
||||
icon="material-symbols:check-circle-rounded"/>
|
||||
</template>
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -245,7 +245,6 @@ const progressTextRight = $computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="title">
|
||||
已学习 <span class="text-3xl">{{ allStudyDays.length }}</span> 天
|
||||
@@ -264,7 +263,7 @@ const progressTextRight = $computed(() => {
|
||||
</div>
|
||||
|
||||
<Dialog v-model="show" title="每日目标" :footer="true" @ok="changePerDayStudyNumber">
|
||||
<div class="target-modal">
|
||||
<div class="target-modal color-main">
|
||||
<div class="center text-2xl gap-2">
|
||||
<span class="text-3xl" style="color:rgb(176,116,211)">{{
|
||||
tempPerDayStudyNumber
|
||||
@@ -295,7 +294,5 @@ const progressTextRight = $computed(() => {
|
||||
width: 30rem;
|
||||
padding: var(--space);
|
||||
padding-top: 0;
|
||||
color: var(--color-font-1);
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as localforage from "localforage";
|
||||
import {nanoid} from "nanoid";
|
||||
import {SAVE_DICT_KEY} from "@/utils/const.ts";
|
||||
import {_getStudyProgress, checkAndUpgradeSaveDict, getDictFile} from "@/utils";
|
||||
import {markRaw} from "vue";
|
||||
import {markRaw, shallowReactive} from "vue";
|
||||
|
||||
export interface BaseState {
|
||||
simpleWords: string[],
|
||||
@@ -106,14 +106,14 @@ export const useBaseStore = defineStore('base', {
|
||||
actions: {
|
||||
setState(obj: BaseState) {
|
||||
obj.word.bookList.map(book => {
|
||||
book.words = markRaw(book.words)
|
||||
book.articles = markRaw(book.articles)
|
||||
book.statistics = markRaw(book.statistics)
|
||||
book.words = shallowReactive(book.words)
|
||||
book.articles = shallowReactive(book.articles)
|
||||
book.statistics = shallowReactive(book.statistics)
|
||||
})
|
||||
obj.article.bookList.map(book => {
|
||||
book.words = markRaw(book.words)
|
||||
book.articles = markRaw(book.articles)
|
||||
book.statistics = markRaw(book.statistics)
|
||||
book.words = shallowReactive(book.words)
|
||||
book.articles = shallowReactive(book.articles)
|
||||
book.statistics = shallowReactive(book.statistics)
|
||||
})
|
||||
this.$patch(obj)
|
||||
},
|
||||
@@ -152,7 +152,7 @@ export const useBaseStore = defineStore('base', {
|
||||
//把其他的词典的单词数据都删掉,全保存在内存里太卡了
|
||||
this.word.bookList.slice(3).map(v => {
|
||||
if (!v.custom) {
|
||||
v.words = markRaw([])
|
||||
v.words = shallowReactive([])
|
||||
}
|
||||
})
|
||||
let rIndex = this.word.bookList.findIndex((v: Dict) => v.id === val.id)
|
||||
@@ -161,10 +161,9 @@ export const useBaseStore = defineStore('base', {
|
||||
}
|
||||
if (rIndex > -1) {
|
||||
this.word.studyIndex = rIndex
|
||||
this.word.bookList[this.word.studyIndex].words = markRaw(val.words)
|
||||
this.word.bookList[this.word.studyIndex].words = shallowReactive(val.words)
|
||||
this.word.bookList[this.word.studyIndex].perDayStudyNumber = val.perDayStudyNumber
|
||||
} else {
|
||||
console.log(1)
|
||||
this.word.bookList.push(getDefaultDict(val))
|
||||
this.word.studyIndex = this.word.bookList.length - 1
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import jaFlag from "@/assets/img/flags/ja.png";
|
||||
import deFlag from "@/assets/img/flags/de.png";
|
||||
import codeFlag from "@/assets/img/flags/code.png";
|
||||
import myFlag from "@/assets/img/flags/my.png";
|
||||
import {markRaw} from "vue";
|
||||
import {shallowReactive} from "vue";
|
||||
|
||||
export type Word = {
|
||||
id?: string,
|
||||
@@ -266,9 +266,9 @@ export function getDefaultDict(val: Partial<Dict> = {}): Dict {
|
||||
custom: false,
|
||||
complete: false,
|
||||
...val,
|
||||
words: markRaw(val.words ?? []),
|
||||
articles: markRaw(val.articles ?? []),
|
||||
statistics: markRaw(val.statistics ?? [])
|
||||
words: shallowReactive(val.words ?? []),
|
||||
articles: shallowReactive(val.articles ?? []),
|
||||
statistics: shallowReactive(val.statistics ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ export default defineConfig({
|
||||
'bg-card-active': 'bg-[var(--color-card-active)]',
|
||||
'color-main': 'color-[var(--color-main-text)]',
|
||||
'gap-space': 'gap-[var(--space)]',
|
||||
'p-space': 'p-[var(--space)]',
|
||||
'px-space': 'px-[var(--space)]',
|
||||
'py-space': 'py-[var(--space)]',
|
||||
},
|
||||
presets: [
|
||||
presetWind3(),
|
||||
|
||||
Reference in New Issue
Block a user