feat:重构代码

This commit is contained in:
zyronon
2025-07-12 02:25:30 +08:00
parent 6bf6d8638e
commit cd4a779e6e
32 changed files with 789 additions and 317 deletions

View File

@@ -54,6 +54,9 @@
--word-font-family: ui-monospace, sans-serif;
--en-article-family: Georgia, sans-serif;
--zh-article-family: "Songti SC", "SimSun", "Noto Serif CJK SC", serif;
--btn-primary: rgb(75, 85, 99);
--btn-info: #909399;
}
html.dark {
@@ -142,9 +145,11 @@ html.dark {
.anim {
transition: background var(--anim-time), color var(--anim-time), border var(--anim-time);
}
.en-article-family {
font-family: var(--en-article-family);
}
.font-family {
font-family: var(--font-family);
}
@@ -262,35 +267,6 @@ footer {
gap: var(--space);
}
.pointer {
cursor: pointer;
}
.flex {
display: flex;
}
.flex1 {
flex: 1;
}
.gap10 {
gap: 10rem;
}
.space-between {
justify-content: space-between;
}
.align-center {
align-items: center;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.panel-page-item {
display: flex;
@@ -339,10 +315,6 @@ footer {
padding: 0 var(--space);
}
.space15 {
margin-bottom: 15rem;
}
.common-list1 {
display: flex;
flex-direction: column;
@@ -485,3 +457,24 @@ footer {
.center {
@apply flex justify-center items-center;
}
.card {
@apply rounded-xl bg-white p-4 mb-5 box-border;
}
.center {
@apply flex justify-center items-center;
}
.title {
@apply text-lg font-medium;
}
.book {
@apply p-4 rounded-md bg-slate-200 relative cursor-pointer h-40 hover:bg-red;
}
.line {
width: 100%;
border-bottom: 1px solid var(--color-item-border);
}

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import BaseIcon from "@/components/BaseIcon.vue";
</script>
<template>
<BaseIcon
title="返回"
icon="formkit:left"/>
</template>
<style scoped lang="scss">
</style>

View File

@@ -8,7 +8,7 @@ interface IProps {
disabled?: boolean
loading?: boolean
size?: 'small' | 'normal' | 'large',
type?: 'primary' | 'link'
type?: 'primary' | 'link' | 'info'
}
withDefaults(defineProps<IProps>(), {
@@ -87,6 +87,7 @@ defineEmits(['click'])
height: 3rem;
font-size: 1.1rem;
padding: 0 1.4rem;
& > span {
font-size: 1.1rem;
}
@@ -108,7 +109,7 @@ defineEmits(['click'])
&.primary {
background: rgb(75, 85, 99);
background: var(--btn-primary);
}
&.link {
@@ -120,6 +121,10 @@ defineEmits(['click'])
}
}
&.info {
background: var(--btn-info);
}
&.active {
opacity: .4;
}

View File

@@ -6,103 +6,145 @@ import {useRouter} from "vue-router";
import {enArticle} from "@/assets/dictionary.ts";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {useNav} from "@/utils";
import {Dict, DictResource, getDefaultDict} from "@/types.ts";
import {cloneDeep} from "lodash-es";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {getArticleBookDataByUrl} from "@/utils/article.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
import Input from "@/pages/pc/components/Input.vue";
import {computed} from "vue";
const {nav} = useNav()
const base = useBaseStore()
const router = useRouter()
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
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)
runtimeStore.editDict = cloneDeep(r)
nav('book-detail')
}
async function getBookDetail2(val: Dict) {
runtimeStore.editDict = cloneDeep(val)
nav('book-detail')
}
const searchList = computed(() => {
if (searchKey) {
return enArticle.filter(v => v.name.toLocaleLowerCase().includes(searchKey.toLocaleLowerCase()))
}
return []
})
function addBook() {
showAddChooseDialog = false
runtimeStore.editDict = getDefaultDict()
nav('book-detail', {isAdd: true})
}
</script>
<template>
<BasePage>
<div class="flex gap-6">
<div class="card w-1/4 flex flex-col">
<div class="title">
我的词典
<div class="card ">
<div class="flex justify-between items-center">
<div class="bg-slate-200 p-3 gap-4 rounded-md cursor-pointer flex items-center">
<span class="text-lg font-bold">{{ base.currentArticleDict.name }}</span>
<BaseIcon @click="showSearchDialog = true" icon="gg:arrows-exchange"/>
</div>
<div class="grid flex-1 flex gap-5 mt-4">
<div class="p-4 flex-1 rounded-md bg-slate-200 relative">
<span>收藏</span>
<div class="absolute bottom-4 right-4">3</div>
</div>
</div>
<div class="grid flex-1 flex gap-5 mt-4" @click="router.push('edit-article')">
<div class="p-4 flex-1 rounded-md bg-slate-200 relative">
<span>添加</span>
<div class="absolute bottom-4 right-4">3</div>
</div>
</div>
</div>
<div class="w-3/4">
<div class="card ">
<div class="flex justify-between items-center">
<div class="bg-slate-200 p-3 rounded-md cursor-pointer flex items-center">
<span class="text-lg font-bold">{{ base.currentArticleDict.name }}</span>
<Icon icon="gg:arrows-exchange" class="text-2xl ml-2"/>
<Icon icon="uil:setting" class="text-2xl ml-2"/>
</div>
<div class="rounded-xl bg-slate-800 flex items-center py-3 px-5 text-white cursor-pointer"
@click="router.push('/learn-article')">
开始学习
</div>
</div>
<div class="mt-5 text-sm">已学习5555个单词的1%</div>
<el-progress class="mt-1" :percentage="80" :show-text="false"></el-progress>
<div class="rounded-xl bg-slate-800 flex items-center py-3 px-5 text-white cursor-pointer"
@click="router.push('/learn-article')">
开始学习
</div>
</div>
<div class="mt-5 text-sm">已学习5555个单词的1%</div>
<el-progress class="mt-1" :percentage="80" :show-text="false"></el-progress>
</div>
<div class="card flex flex-col">
<div class="title">
我的词典
我的
</div>
<div class="grid grid-cols-6 gap-4 mt-4">
<div class="my-dict" @click="nav('edit-word-dict',{type:0})">
<span>收藏</span>
<div class="absolute bottom-4 right-4">{{ store.collectWord.words.length }}个词</div>
<div class="book"
v-for="dict in store.article.bookList"
@click="getBookDetail2(dict)">
<div class="name">{{ dict.name }}</div>
<div class="desc">{{ dict.description }}</div>
<div class="absolute bottom-4 right-4">{{ dict.length }}</div>
</div>
<div class="my-dict" @click="nav('edit-word-dict',{type:1})">
<span>错词本</span>
<div class="absolute bottom-4 right-4">{{ store.wrong.words.length }}个词</div>
<div class="book" @click="showAddChooseDialog = true">
<div class="center h-full">
<Icon
width="40px"
icon="fluent:add-20-filled"/>
</div>
</div>
</div>
</div>
<div class="mt-4">
<div class="title">文章</div>
<div class="mt-4 flex gap-4">
<div
class="bg-white rounded-md p-4 h-40 w-30 relative cursor-pointer"
v-for="dict in enArticle"
>
<div class="top">
<div class="name">{{ dict.name }}</div>
<div class="desc">{{ dict.description }}</div>
</div>
<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">
<div class="book"
v-for="dict in enArticle"
@click="getBookDetail(dict)">
<div class="name">{{ dict.name }}</div>
<div class="desc">{{ dict.description }}</div>
<div class="absolute bottom-4 right-4">{{ dict.length }}</div>
</div>
</div>
</div>
<Dialog v-model="showAddChooseDialog" title="选项">
<div class="color-black 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-black 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>
</div>
<div v-else class="h-40 center flex-col text-xl color-black/60">
<div> 请输入书籍名称搜索</div>
<div>或直接在书籍列表选中</div>
</div>
</div>
</Dialog>
</BasePage>
</template>
<style scoped lang="scss">
.card {
@apply rounded-xl bg-white p-4 mt-5;
}
.center {
@apply flex justify-center items-center;
}
.title {
@apply text-lg font-medium;
}
.my-dict {
@apply p-4 rounded-md bg-slate-200 relative cursor-pointer h-40;
}
</style>

View File

@@ -1,164 +0,0 @@
<script setup lang="ts">
import Toolbar from "@/pages/pc/components/toolbar/index.vue"
import {onMounted, onUnmounted, watch} from "vue";
import {usePracticeStore} from "@/stores/practice.ts";
import Footer from "@/pages/pc/word/Footer.vue";
import {useBaseStore} from "@/stores/base.ts";
import Statistics from "@/pages/pc/word/Statistics.vue";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {MessageBox} from "@/utils/MessageBox.tsx";
import PracticeArticle from "@/pages/pc/practice/practice-article/index.vue";
import {ShortcutKey} from "@/types.ts";
import DictModal from "@/pages/pc/components/dialog/DictDiglog.vue";
import {useStartKeyboardEventListener} from "@/hooks/event.ts";
import useTheme from "@/hooks/theme.ts";
const statisticsStore = usePracticeStore()
const store = useBaseStore()
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const {toggleTheme} = useTheme()
const practiceRef: any = $ref()
watch(statisticsStore, () => {
if (statisticsStore.inputWordNumber < 1) {
return statisticsStore.correctRate = -1
}
if (statisticsStore.wrong > statisticsStore.inputWordNumber) {
return statisticsStore.correctRate = 0
}
statisticsStore.correctRate = 100 - Math.trunc(((statisticsStore.wrong) / (statisticsStore.inputWordNumber)) * 100)
})
function test() {
MessageBox.confirm(
'2您选择了“本地翻译”但译文内容却为空白是否修改为“不需要翻译”并保存?',
'1提示',
() => {
console.log('ok')
},
() => {
console.log('cencal')
})
}
function write() {
// console.log('write')
settingStore.dictation = true
repeat()
}
//TODO 需要判断是否已忽略
function repeat() {
// console.log('repeat')
emitter.emit(EventKey.resetWord)
practiceRef.getCurrentPractice()
}
function prev() {
// console.log('next')
if (store.currentDict.chapterIndex === 0) {
ElMessage.warning('已经在第一章了~')
} else {
store.currentDict.chapterIndex--
repeat()
}
}
function toggleShowTranslate() {
settingStore.translate = !settingStore.translate
}
function toggleDictation() {
settingStore.dictation = !settingStore.dictation
}
function openSetting() {
runtimeStore.showSettingModal = true
}
function openDictDetail() {
emitter.emit(EventKey.openDictModal, 'detail')
}
function toggleConciseMode() {
settingStore.showToolbar = !settingStore.showToolbar
settingStore.showPanel = settingStore.showToolbar
}
function togglePanel() {
settingStore.showPanel = !settingStore.showPanel
}
function jumpSpecifiedChapter(val: number) {
store.currentDict.chapterIndex = val
repeat()
}
onMounted(() => {
emitter.on(EventKey.write, write)
emitter.on(EventKey.repeat, repeat)
emitter.on(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.on(ShortcutKey.PreviousChapter, prev)
emitter.on(ShortcutKey.RepeatChapter, repeat)
emitter.on(ShortcutKey.DictationChapter, write)
emitter.on(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
emitter.on(ShortcutKey.ToggleDictation, toggleDictation)
emitter.on(ShortcutKey.OpenSetting, openSetting)
emitter.on(ShortcutKey.OpenDictDetail, openDictDetail)
emitter.on(ShortcutKey.ToggleTheme, toggleTheme)
emitter.on(ShortcutKey.ToggleConciseMode, toggleConciseMode)
emitter.on(ShortcutKey.TogglePanel, togglePanel)
})
onUnmounted(() => {
emitter.off(EventKey.write, write)
emitter.off(EventKey.repeat, repeat)
emitter.off(EventKey.jumpSpecifiedChapter, jumpSpecifiedChapter)
emitter.off(ShortcutKey.PreviousChapter, prev)
emitter.off(ShortcutKey.RepeatChapter, repeat)
emitter.off(ShortcutKey.DictationChapter, write)
emitter.off(ShortcutKey.ToggleShowTranslate, toggleShowTranslate)
emitter.off(ShortcutKey.ToggleDictation, toggleDictation)
emitter.off(ShortcutKey.OpenSetting, openSetting)
emitter.off(ShortcutKey.OpenDictDetail, openDictDetail)
emitter.off(ShortcutKey.ToggleTheme, toggleTheme)
emitter.off(ShortcutKey.ToggleConciseMode, toggleConciseMode)
emitter.off(ShortcutKey.TogglePanel, togglePanel)
})
useStartKeyboardEventListener()
</script>
<template>
<div class="practice-wrapper">
<Toolbar/>
<PracticeArticle ref="practiceRef"/>
<Footer/>
</div>
<DictModal/>
<Statistics/>
</template>
<style scoped lang="scss">
.practice-wrapper {
font-size: 0.9rem;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
//padding-right: var(--practice-wrapper-padding-right);
transform: translateX(var(--practice-wrapper-translateX));
}
</style>

View File

@@ -0,0 +1,277 @@
<script setup lang="ts">
import {onMounted, onUnmounted} from "vue";
import {Article, DefaultArticle} from "@/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {cloneDeep} from "lodash-es";
import {useBaseStore} from "@/stores/base.ts";
import List from "@/pages/pc/components/list/List.vue";
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {useDisableEventListener, useWindowClick} from "@/hooks/event.ts";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {nanoid} from "nanoid";
import {syncMyDictList} from "@/hooks/dict.ts";
import MiniDialog from "@/pages/pc/components/dialog/MiniDialog.vue";
import EditArticle2 from "@/pages/pc/article/components/EditArticle2.vue";
import BaseIcon from "@/components/BaseIcon.vue";
const emit = defineEmits<{
importData: [val: Event]
exportData: [val: string]
}>()
const base = useBaseStore()
const runtimeStore = useRuntimeStore()
let article = $ref<Article>(cloneDeep(DefaultArticle))
let show = $ref(false)
let editArticleRef: any = $ref()
let listEl: any = $ref()
onMounted(() => {
emitter.on(EventKey.openArticleListModal, (val: Article) => {
console.log('val', val)
show = true
if (val) {
article = cloneDeep(val)
}
})
})
onUnmounted(() => {
emitter.off(EventKey.openArticleListModal)
})
useDisableEventListener(() => show)
async function selectArticle(item: Article) {
let r = await checkDataChange()
if (r) {
article = cloneDeep(item)
}
}
function checkDataChange() {
return new Promise(resolve => {
let editArticle: Article = editArticleRef.getEditArticle()
if (editArticle.id !== '-1') {
editArticle.title = editArticle.title.trim()
editArticle.titleTranslate = editArticle.titleTranslate.trim()
editArticle.text = editArticle.text.trim()
editArticle.textTranslate = editArticle.textTranslate.trim()
if (
editArticle.title !== article.title ||
editArticle.titleTranslate !== article.titleTranslate ||
editArticle.text !== article.text ||
editArticle.textTranslate !== article.textTranslate
) {
return MessageBox.confirm(
'检测到数据有变动,是否保存?',
'提示',
async () => {
let r = await editArticleRef.save('save')
if (r) resolve(true)
},
() => resolve(true),
)
}
} else {
if (editArticle.title.trim() && editArticle.text.trim()) {
return MessageBox.confirm(
'检测到数据有变动,是否保存?',
'提示',
async () => {
let r = await editArticleRef.save('save')
if (r) resolve(true)
},
() => resolve(true),
)
}
}
resolve(true)
})
}
async function add() {
let r = await checkDataChange()
if (r) {
article = cloneDeep(DefaultArticle)
}
}
function saveArticle(val: Article): boolean {
console.log('saveArticle', val)
if (val.id) {
let rIndex = runtimeStore.editDict.articles.findIndex(v => v.id === val.id)
if (rIndex > -1) {
runtimeStore.editDict.articles[rIndex] = cloneDeep(val)
}
} else {
let has = runtimeStore.editDict.articles.find((item: Article) => item.title === val.title)
if (has) {
ElMessage.error('已存在同名文章!')
return false
}
val.id = nanoid(6)
runtimeStore.editDict.articles.push(val)
setTimeout(() => {
listEl.scrollBottom()
})
}
article = cloneDeep(val)
//TODO 保存完成后滚动到对应位置
ElMessage.success('保存成功!')
syncMyDictList(runtimeStore.editDict)
return true
}
function saveAndNext(val: Article) {
if (saveArticle(val)) {
add()
}
}
let showExport = $ref(false)
useWindowClick(() => showExport = false)
</script>
<template>
<div class="add-article">
<div class="aslide">
<header class="flex justify-between items-center">
<BaseIcon
title="返回"
@click="$router.back"
icon="formkit:left"/>
<div class="text-xl">{{ runtimeStore.editDict.name }}</div>
</header>
<List
ref="listEl"
v-model:list="runtimeStore.editDict.articles"
:select-item="article"
@del-select-item="article = cloneDeep(DefaultArticle)"
@select-item="selectArticle"
>
<template v-slot="{item,index}">
<div class="name"> {{ `${index + 1}. ${item.title}` }}</div>
<div class="translate-name"> {{ ` ${item.titleTranslate}` }}</div>
</template>
</List>
<div class="add" v-if="!article.title">
正在添加新文章...
</div>
<div class="footer">
<div class="import">
<BaseButton size="small">导入</BaseButton>
<input type="file"
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
@change="e => emit('importData',e)">
</div>
<div class="export"
style="position: relative"
@click.stop="null">
<BaseButton size="small" @click="showExport = true">导出</BaseButton>
<MiniDialog
v-model="showExport"
style="width: 80rem;bottom: calc(100% + 10rem);top:unset;"
>
<div class="mini-row-title">
导出选项
</div>
<div class="mini-row">
<BaseButton size="small" @click="emit('exportData',{type:'all',data:[]})">全部文章</BaseButton>
</div>
<div class="mini-row">
<BaseButton size="small" @click="emit('exportData',{type:'chapter',data:article})">当前章节</BaseButton>
</div>
</MiniDialog>
</div>
<BaseButton size="small" @click="add">新增</BaseButton>
</div>
</div>
<EditArticle2
ref="editArticleRef"
type="batch"
@save="saveArticle"
@saveAndNext="saveAndNext"
:article="article"/>
</div>
</template>
<style scoped lang="scss">
.add-article {
width: 100%;
height: 100vh;
box-sizing: border-box;
color: var(--color-font-1);
background: var(--color-second-bg);
display: flex;
.close {
position: absolute;
right: 1.2rem;
top: 1.2rem;
}
.aslide {
width: 14vw;
height: 100%;
padding: 0 .6rem;
display: flex;
flex-direction: column;
$height: 4rem;
header {
height: $height;
}
.name {
font-size: 1.1rem;
}
.translate-name {
font-size: 1rem;
}
.add {
width: 16rem;
box-sizing: border-box;
border-radius: .5rem;
margin-bottom: .6rem;
padding: .6rem;
display: flex;
justify-content: space-between;
transition: all .3s;
color: var(--color-font-1);
background: var(--color-item-active);
}
.footer {
height: $height;
display: flex;
gap: .6rem;
align-items: center;
justify-content: flex-end;
.import {
display: inline-flex;
position: relative;
input {
position: absolute;
height: 100%;
width: 100%;
opacity: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,126 @@
<script setup lang="ts">
import BasePage from "@/pages/pc/components/BasePage.vue";
import BackIcon from "@/components/BackIcon.vue";
import Empty from "@/components/Empty.vue";
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
import {useBaseStore} from "@/stores/base.ts";
import {Article, DefaultArticle} from "@/types.ts";
import {cloneDeep} from "lodash-es";
import {useRuntimeStore} from "@/stores/runtime.ts";
import BaseButton from "@/components/BaseButton.vue";
import {useRoute, useRouter} from "vue-router";
import EditBook from "@/pages/pc/article/components/EditBook.vue";
import {computed, onMounted} from "vue";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
const router = useRouter()
const route = useRoute()
let isEdit = $ref(false)
let isAdd = $ref(false)
let article: Article = $ref(cloneDeep(DefaultArticle))
let chapterIndex = $ref(-1)
function handleCheckedChange(val) {
let rIndex = runtimeStore.editDict.articles.findIndex(v => v.id === val.item.id)
if (rIndex > -1) {
chapterIndex = rIndex
article = val.item
}
}
const activeId = $computed(() => {
return runtimeStore.editDict.articles?.[chapterIndex]?.id ?? ''
})
function addMyBookList() {
let rIndex = base.article.bookList.findIndex(v => v.name === runtimeStore.editDict.name)
if (rIndex > -1) {
base.article.studyIndex = rIndex
} else {
base.article.bookList.push(runtimeStore.editDict)
base.article.studyIndex = base.article.bookList.length - 1
}
router.back()
}
const showBookDetail = computed(() => {
return !(isAdd || isEdit);
})
onMounted(() => {
if (route.query?.isAdd) {
isAdd = true
}
})
function formClose() {
if (isEdit) isEdit = false
else router.back()
}
</script>
<template>
<BasePage>
<div class="card mb-0 h-[95vh] flex flex-col" v-if="showBookDetail">
<div class="flex justify-between items-center relative">
<BackIcon class="z-2" @click="$router.back"/>
<div class="absolute text-2xl text-align-center w-full">{{ runtimeStore.editDict.name }}</div>
<div class="flex gap-2">
<BaseButton type="info" @click="isEdit = true">编辑信息</BaseButton>
<BaseButton type="info" @click="router.push('batch-edit-article')">文章管理</BaseButton>
<BaseButton @click="addMyBookList">学习</BaseButton>
</div>
</div>
<div class="text-lg ">介绍{{ runtimeStore.editDict.description }}</div>
<div class="line my-3"></div>
<div class="flex flex-1 overflow-hidden">
<div class="left flex-[2] scroll p-0">
<ArticleList
v-if="runtimeStore.editDict.articles.length"
@title="handleCheckedChange"
@click="handleCheckedChange"
:list="runtimeStore.editDict.articles"
:active-id="activeId">
</ArticleList>
<Empty v-else/>
</div>
<div class="right flex-[3] shrink-0 pl-4 overflow-auto">
<div v-if="chapterIndex>-1">
<div class="en-article-family title text-xl">
<div class="text-center text-2xl">{{ article.title }}</div>
<div class="text-2xl" v-if="article.text">
<div class="my-5" v-for="t in article.text.split('\n\n')">{{ t }}</div>
</div>
</div>
<div class="mt-2">
<div class="text-center text-2xl">{{ article.titleTranslate }}</div>
<div class="text-xl" v-if="article.textTranslate">
<div class="my-5" v-for="t in article.textTranslate.split('\n\n')">{{ t }}</div>
</div>
<Empty v-else/>
</div>
</div>
<Empty v-else/>
</div>
</div>
</div>
<div class="center" v-else>
<div class="w-1/2">
<EditBook :is-add="isAdd"
@close="formClose"
@submit="isEdit = isAdd = false"
/>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import EditArticle2 from "@/pages/pc/components/article/EditArticle2.vue";
import EditArticle2 from "@/pages/pc/article/components/EditArticle2.vue";
import BaseIcon from "@/components/BaseIcon.vue";
</script>

View File

@@ -487,7 +487,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center">
<div>开始时间</div>
<div class="flex space-between flex-1">
<div class="flex justify-between flex-1">
<div class="flex items-center gap-2">
<el-input-number v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
<template #suffix>
@@ -510,7 +510,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
</div>
<div class="flex gap-2 items-center">
<div>结束时间</div>
<div class="flex space-between flex-1">
<div class="flex justify-between flex-1">
<div class="flex items-center gap-2">
<el-input-number v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1">
<template #suffix>

View File

@@ -14,7 +14,7 @@ import {useRuntimeStore} from "@/stores/runtime.ts";
import {nanoid} from "nanoid";
import {syncMyDictList} from "@/hooks/dict.ts";
import MiniDialog from "@/pages/pc/components/dialog/MiniDialog.vue";
import EditArticle2 from "@/pages/pc/components/article/EditArticle2.vue";
import EditArticle2 from "@/pages/pc/article/components/EditArticle2.vue";
const emit = defineEmits<{
importData: [val: Event]

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import {Dict, DictType, getDefaultDict} from "@/types.ts";
import {cloneDeep} from "lodash-es";
import {FormInstance, FormRules} from "element-plus";
import {onMounted, reactive} from "vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useBaseStore} from "@/stores/base.ts";
import {syncMyDictList} from "@/hooks/dict.ts";
const props = defineProps<{
isAdd: boolean
}>()
const emit = defineEmits<{
submit: []
close: []
}>()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
const DefaultDictForm = {
id: '',
name: '',
description: '',
category: '',
tags: [],
translateLanguage: 'zh-CN',
language: 'en',
type: DictType.article
}
let dictForm: any = $ref(cloneDeep(DefaultDictForm))
const dictFormRef = $ref<FormInstance>()
const dictRules = reactive<FormRules>({
name: [
{required: true, message: '请输入名称', trigger: 'blur'},
{max: 20, message: '名称不能超过20个字符', trigger: 'blur'},
],
})
async function onSubmit() {
await dictFormRef.validate((valid) => {
if (valid) {
let data: Dict = cloneDeep({
...getDefaultDict(),
...dictForm,
})
//任意修改,都将其变为自定义词典
data.isCustom = true
if (props.isAdd) {
data.id = 'custom-dict-' + Date.now()
//TODO 允许同名?
if (store.article.bookList.find(v => v.name === data.name)) {
return ElMessage.warning('已有相同名称书籍!')
} else {
store.article.bookList.push(data)
runtimeStore.editDict = data
emit('submit')
ElMessage.success('添加成功')
}
} else {
let rIndex = store.article.bookList.findIndex(v => v.id === data.id)
if (rIndex > -1) {
store.article.bookList[rIndex] = cloneDeep(data)
runtimeStore.editDict = cloneDeep(data)
emit('submit')
ElMessage.success('修改成功')
}else {
ElMessage.warning('修改失败')
}
}
console.log('submit!', data)
} else {
ElMessage.warning('请填写完整')
}
})
}
onMounted(() => {
if (!props.isAdd) {
dictForm = cloneDeep(runtimeStore.editDict)
}
})
</script>
<template>
<div class="edit-dict">
<div class="wrapper">
<div class="common-title">{{ dictForm.id ? '修改' : '添加' }}书籍</div>
<el-form
ref="dictFormRef"
:rules="dictRules"
:model="dictForm"
label-width="8rem">
<el-form-item label="名称" prop="name">
<el-input v-model="dictForm.name"/>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="dictForm.description" type="textarea"/>
</el-form-item>
<el-form-item label="原文语言">
<el-select v-model="dictForm.language" placeholder="请选择选项">
<el-option label="英语" value="en"/>
<el-option label="德语" value="de"/>
<el-option label="日语" value="ja"/>
<el-option label="代码" value="code"/>
</el-select>
</el-form-item>
<el-form-item label="译文语言">
<el-select v-model="dictForm.translateLanguage" placeholder="请选择选项">
<!-- <el-option label="通用" value="common"/>-->
<el-option label="中文" value="zh-CN"/>
<el-option label="英语" value="en"/>
<el-option label="德语" value="de"/>
<el-option label="日语" value="ja"/>
</el-select>
</el-form-item>
<div class="center">
<el-button @click="emit('close')">关闭</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="scss">
.edit-dict {
flex: 1;
width: 100%;
overflow: auto;
display: flex;
justify-content: center;
.wrapper {
width: 80rem;
}
.el-select {
width: 100%;
}
}
</style>

View File

@@ -4,7 +4,7 @@ import {Article, DefaultArticle} from "@/types.ts";
import {cloneDeep} from "lodash-es";
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
import {useDisableEventListener} from "@/hooks/event.ts";
import EditArticle2 from "@/pages/pc/components/article/EditArticle2.vue";
import EditArticle2 from "@/pages/pc/article/components/EditArticle2.vue";
interface IProps {
article?: Article

View File

@@ -3,8 +3,8 @@
</script>
<template>
<div class="flex justify-center h-full">
<div class="w-5/10 py-5">
<div class="flex justify-center">
<div class="w-[70vw] 2xl:w-[50vw] my-5">
<slot></slot>
</div>
</div>
@@ -12,4 +12,4 @@
<style scoped lang="scss">
</style>
</style>

View File

@@ -3,10 +3,10 @@
import {Icon} from "@iconify/vue";
import Close from "@/components/icon/Close.vue";
import {useDisableEventListener, useWindowClick} from "@/hooks/event.ts";
import {watch} from "vue";
defineProps<{
modelValue: string
autofocus?: boolean
}>()
defineEmits(['update:modelValue'])
@@ -20,6 +20,14 @@ useWindowClick((e: PointerEvent) => {
useDisableEventListener(() => focus)
const vFocus = {
mounted: (el, bind) => {
if (bind.value) {
el.focus()
setTimeout(() => focus = true)
}
}
}
</script>
<template>
@@ -31,6 +39,7 @@ useDisableEventListener(() => focus)
width="20"/>
<input type="text"
:value="modelValue"
v-focus="autofocus"
@input="e=>$emit('update:modelValue',e.target.value)"
>
<transition name="fade">

View File

@@ -31,7 +31,7 @@
/>
</form>
<div class="flex-center items-center gap-2 mt-10">
<div class="center items-center gap-2 mt-10">
<button
class="bg-green-600 text-white px-6 py-2 rounded"
@click="submitAll"

View File

@@ -158,7 +158,7 @@ async function cancel() {
<Tooltip title="关闭">
<Icon @click="close"
v-if="showClose"
class="close hvr-grow pointer"
class="close hvr-grow cursor-pointer"
width="24" color="#929596"
icon="ion:close-outline"/>
</Tooltip>

View File

@@ -48,7 +48,7 @@ const speedTime = $computed(() => {
<div id="DictDialog">
<div class="detail">
<div class="desc">{{ store.sdict.description }}</div>
<div class="text flex align-center gap-2">
<div class="text flex items-center gap-2">
<div>总词汇 {{ store.sdict.words.length }}</div>
<BaseIcon icon="circum:view-list"
@click='showAllWordModal'

View File

@@ -173,6 +173,7 @@ defineExpose({scrollToBottom, scrollToItem})
.scroller {
flex: 1;
padding: 0 var(--space);
//padding: 0 var(--space);
padding-right: var(--space);
}
</style>

View File

@@ -7,7 +7,7 @@ import {useRuntimeStore} from "@/stores/runtime.ts";
import {cloneDeep} from "lodash-es";
import {Article, DefaultArticle, Dict, DictResource, DictType, getDefaultDict, Sort, TranslateType} from "@/types.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import EditBatchArticleModal from "@/pages/pc/components/article/EditBatchArticleModal.vue";
import EditBatchArticleModal from "@/pages/pc/article/components/EditBatchArticleModal.vue";
import {Icon} from "@iconify/vue";
import EditDict from "@/pages/pc/dict/components/EditDict.vue";
import {nanoid} from "nanoid";
@@ -530,4 +530,4 @@ defineExpose({getDictDetail, add, editDict})
}
}
}
</style>
</style>

View File

@@ -162,7 +162,7 @@ onMounted(() => {
<el-option label="文章" :value="DictType.article"/>
</el-select>
</el-form-item>
<div class="flex-center">
<div class="center">
<el-button @click="closeDictForm">关闭</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</div>
@@ -188,4 +188,4 @@ onMounted(() => {
}
}
</style>
</style>

View File

@@ -488,7 +488,7 @@ defineExpose({getDictDetail, add: addWord, editDict})
<el-form-item label="音标/发音②">
<el-input v-model="wordForm.phonetic1"/>
</el-form-item>
<div class="flex-center">
<div class="center">
<el-button @click="closeWordForm">关闭</el-button>
<el-button type="primary" @click="onSubmitWord">保存</el-button>
</div>
@@ -619,4 +619,4 @@ defineExpose({getDictDetail, add: addWord, editDict})
}
}
}
</style>
</style>

View File

@@ -22,7 +22,9 @@ const {toggleTheme} = useTheme()
<template>
<div class="layout">
<div class="aside" :class="{'expand':settingStore.sideExpand}">
<!-- 第一个aside 占位用-->
<div class="aside space" :class="{'expand':settingStore.sideExpand}"></div>
<div class="aside fixed" :class="{'expand':settingStore.sideExpand}">
<div class="top">
<Logo v-if="settingStore.sideExpand"/>
<div class="row" @click="router.push('/home')">

View File

@@ -469,7 +469,7 @@ let showQuestions = $ref(false)
</template>
</div>
<div class="flex-center">
<div class="center">
<BaseButton @click="showQuestions =! showQuestions">显示题目</BaseButton>
</div>
<div class="toggle" v-if="showQuestions">

View File

@@ -6,7 +6,7 @@ import TypingWord from "@/pages/pc/components/TypingWord.vue";
import Panel from "../../components/Panel.vue";
import {onMounted, onUnmounted} from "vue";
import {useBaseStore} from "@/stores/base.ts";
import EditSingleArticleModal from "@/pages/pc/components/article/EditSingleArticleModal.vue";
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";

View File

@@ -252,7 +252,7 @@ defineRender(() => {
modelValue={wordForm.phonetic1}
onUpdate:model-value={e => wordForm.phonetic1 = e}/>
</el-form-item>
<div class="flex-center">
<div class="center">
<el-button
onClick={closeWordForm}>关闭
</el-button>
@@ -272,4 +272,4 @@ defineRender(() => {
<style scoped lang="scss">
</style>
</style>

View File

@@ -97,10 +97,10 @@ const isEnd = $computed(() => {
</div>
<div class="absolute right-5 top-20 flex flex-col gap-4">
<Tooltip title="分享给朋友">
<Icon class="hvr-grow pointer" icon="ph:share-light" width="20" color="#929596"/>
<Icon class="hvr-grow cursor-pointer" icon="ph:share-light" width="20" color="#929596"/>
</Tooltip>
<Tooltip title="请我喝杯咖啡">
<Icon class="hvr-grow pointer" icon="twemoji:teacup-without-handle" width="20" color="#929596"/>
<Icon class="hvr-grow cursor-pointer" icon="twemoji:teacup-without-handle" width="20" color="#929596"/>
</Tooltip>
</div>
<div class="footer">

View File

@@ -162,19 +162,19 @@ function changePerDayStudyNumber() {
我的词典
</div>
<div class="grid grid-cols-6 gap-4 mt-4">
<div class="my-dict" @click="nav('edit-word-dict',{type:0})">
<div class="book" @click="nav('edit-word-dict',{type:0})">
<span>收藏</span>
<div class="absolute bottom-4 right-4">{{ store.collectWord.words.length }}个词</div>
</div>
<div class="my-dict" @click="nav('edit-word-dict',{type:1})">
<div class="book" @click="nav('edit-word-dict',{type:1})">
<span>错词本</span>
<div class="absolute bottom-4 right-4">{{ store.wrong.words.length }}个词</div>
</div>
<div class="my-dict" @click="nav('edit-word-dict',{type:2})">
<div class="book" @click="nav('edit-word-dict',{type:2})">
<span>简单词</span>
<div class="absolute bottom-4 right-4">{{ store.simple.words.length }}个词</div>
</div>
<div class="my-dict" @click="nav('edit-word-dict',{type:3})">
<div class="book" @click="nav('edit-word-dict',{type:3})">
<span>已掌握</span>
<div class="absolute bottom-4 right-4">{{ store.master.words.length }}个词</div>
</div>
@@ -238,24 +238,7 @@ function changePerDayStudyNumber() {
</template>
<style scoped lang="scss">
.card {
@apply rounded-xl p-4 mt-5;
background: var(--color-second-bg);
}
.center {
@apply flex justify-center items-center;
}
.title {
@apply text-lg font-medium;
}
.my-dict {
@apply p-4 rounded-md bg-slate-200 relative cursor-pointer h-40;
}
.target-modal {
.target-modal {
width: 30rem;
padding: var(--space);
padding-top: 0;

View File

@@ -25,6 +25,8 @@ import LearnArticle from "@/pages/pc/article/LearnArticle.vue";
import EditWordDict from "@/pages/pc/word/EditWordDict.vue";
import StudyWord from "@/pages/pc/word/StudyWord.vue";
import EditArticlePage from "@/pages/pc/article/EditArticlePage.vue";
import BookDetail from "@/pages/pc/article/BookDetail.vue";
import BatchEditArticlePage from "@/pages/pc/article/BatchEditArticlePage.vue";
export const routes: RouteRecordRaw[] = [
{
@@ -39,7 +41,9 @@ export const routes: RouteRecordRaw[] = [
{path: 'article', component: ArticleIndex},
{path: 'article2', component: Article2Index},
{path: 'edit-article', component: EditArticlePage},
{path: 'batch-edit-article', component: BatchEditArticlePage},
{path: 'learn-article', component: LearnArticle},
{path: 'book-detail', component: BookDetail},
]
},
@@ -61,7 +65,7 @@ export const routes: RouteRecordRaw[] = [
]
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
history: VueRouter.createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// console.log('savedPosition', savedPosition)

View File

@@ -1,5 +1,5 @@
import {defineStore} from 'pinia'
import {Dict, DictType, getDefaultDict, Sort, Word} from "../types.ts"
import {Article, Dict, DictType, getDefaultDict, Sort, Word} from "../types.ts"
import {cloneDeep, merge, reverse, shuffle} from "lodash-es";
import {emitter, EventKey} from "@/utils/eventBus.ts"
import * as localforage from "localforage";
@@ -27,6 +27,14 @@ export interface BaseState {
article: {
dictIndex: number,
}
},
// word: {
// studyIndex: number,
// dictList: [],
// },
article: {
bookList: Dict[],
studyIndex: number,
}
}
@@ -132,7 +140,7 @@ export const DefaultBaseState = (): BaseState => ({
type: DictType.article,
resourceId: 'article_nce2',
length: 96,
lastLearnIndex:1
lastLearnIndex: 1
},
],
wordDictList: [
@@ -230,7 +238,13 @@ export const DefaultBaseState = (): BaseState => ({
'be', 'am', 'is', 'do', 'are', 'did', 'were', 'was', 'can', 'could', 'will', 'would',
'the', 'that', 'this', 'to', 'of', 'for', 'and', 'at', 'not', 'no', 'yes',
],
load: false
load: false,
article: {
bookList: [
getDefaultDict({name: '收藏'})
],
studyIndex: -1,
}
})
export const useBaseStore = defineStore('base', {
@@ -293,8 +307,11 @@ export const useBaseStore = defineStore('base', {
otherWordDictList(): Dict[] {
return this.wordDictList.filter(v => this.sdict.id !== v.id)
},
currentArticleCollectDict(): Dict {
return this.article.bookList[0]
},
currentArticleDict(): Dict {
return this.articleDictList[this.currentStudy.article.dictIndex] ?? {}
return this.article.bookList[this.article.studyIndex] ?? {}
},
chapter(state: BaseState): Word[] {
return this.currentDict.chapterWords[this.currentDict.chapterIndex] ?? []

View File

@@ -233,7 +233,7 @@ export const languageCategoryOptions = [
{id: 'my', name: '我的', flag: myFlag},
]
export function getDefaultDict(val = {}): Dict {
export function getDefaultDict(val: Partial<Dict> = {}): Dict {
return {
id: '',
name: '',
@@ -288,4 +288,4 @@ export interface ArticleItem {
export const SlideType = {
HORIZONTAL: 0,
VERTICAL: 1,
}
}

18
src/utils/article.ts Normal file
View File

@@ -0,0 +1,18 @@
import {Dict, DictResource, getDefaultDict} from "@/types.ts";
import {getDictFile} from "@/utils/index.ts";
import {cloneDeep} from "lodash-es";
import {nanoid} from "nanoid";
export async function getArticleBookDataByUrl(val: DictResource) {
let dictResourceUrl = `./dicts/${val.language}/${val.type}/${val.translateLanguage}/${val.url}`;
let s = await getDictFile(dictResourceUrl)
let articles = cloneDeep(s.map(v => {
v.id = nanoid(6)
return v
}))
return cloneDeep({
...getDefaultDict(),
...val,
articles
})
}