feat:重构代码
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
14
src/components/BackIcon.vue
Normal file
14
src/components/BackIcon.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
277
src/pages/pc/article/BatchEditArticlePage.vue
Normal file
277
src/pages/pc/article/BatchEditArticlePage.vue
Normal 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>
|
||||
126
src/pages/pc/article/BookDetail.vue
Normal file
126
src/pages/pc/article/BookDetail.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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]
|
||||
145
src/pages/pc/article/components/EditBook.vue
Normal file
145
src/pages/pc/article/components/EditBook.vue
Normal 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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -173,6 +173,7 @@ defineExpose({scrollToBottom, scrollToItem})
|
||||
|
||||
.scroller {
|
||||
flex: 1;
|
||||
padding: 0 var(--space);
|
||||
//padding: 0 var(--space);
|
||||
padding-right: var(--space);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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] ?? []
|
||||
|
||||
@@ -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
18
src/utils/article.ts
Normal 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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user