Develop dictionary management function
This commit is contained in:
179
src/components/DictListPanel.vue
Normal file
179
src/components/DictListPanel.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {DictResource, languageCategoryOptions} from "@/types.ts";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {dictionaryResources} from "@/assets/dictionary.ts";
|
||||
import {groupBy} from "lodash-es";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import DictList from "@/components/list/DictList.vue";
|
||||
import DictGroup from "@/components/toolbar/DictGroup.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: [],
|
||||
selectDict: [val: { dict: any, index: number }]
|
||||
}>()
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
let currentLanguage = $ref('my')
|
||||
let currentTranslateLanguage = $ref('common')
|
||||
let groupByLanguage = groupBy(dictionaryResources, 'language')
|
||||
let translateLanguageList = $ref([])
|
||||
|
||||
function groupByDictTags(dictList: DictResource[]) {
|
||||
return dictList.reduce<Record<string, DictResource[]>>((result, dict) => {
|
||||
dict.tags.forEach((tag) => {
|
||||
if (Object.prototype.hasOwnProperty.call(result, tag)) {
|
||||
result[tag].push(dict)
|
||||
} else {
|
||||
result[tag] = [dict]
|
||||
}
|
||||
})
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
const groupByTranslateLanguage = $computed(() => {
|
||||
let data: any
|
||||
if (currentLanguage === 'article') {
|
||||
let articleList = dictionaryResources.filter(v => v.type === 'article')
|
||||
data = groupBy(articleList, 'translateLanguage')
|
||||
} else if (currentLanguage === 'my') {
|
||||
data = {
|
||||
common: store.myDictList.concat([{name: '',} as any])
|
||||
}
|
||||
} else {
|
||||
data = groupBy(groupByLanguage[currentLanguage], 'translateLanguage')
|
||||
}
|
||||
// console.log('groupByTranslateLanguage', data)
|
||||
translateLanguageList = Object.keys(data)
|
||||
currentTranslateLanguage = translateLanguageList[0]
|
||||
return data
|
||||
})
|
||||
|
||||
const groupedByCategoryAndTag = $computed(() => {
|
||||
const currentTranslateLanguageDictList = groupByTranslateLanguage[currentTranslateLanguage]
|
||||
const groupByCategory = groupBy(currentTranslateLanguageDictList, 'category')
|
||||
|
||||
let data = []
|
||||
for (const [key, value] of Object.entries(groupByCategory)) {
|
||||
data.push([key, groupByDictTags(value)])
|
||||
}
|
||||
// console.log('groupedByCategoryAndTag', data)
|
||||
return data
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dict-list-panel">
|
||||
<header>
|
||||
<div class="tabs">
|
||||
<div class="tab"
|
||||
:class="currentLanguage === item.id && 'active'"
|
||||
@click="currentLanguage = item.id"
|
||||
v-for="item in languageCategoryOptions">
|
||||
<img :src='item.flag' alt=""/>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="dict-list-wrapper">
|
||||
<template v-if="currentLanguage === 'my'">
|
||||
<DictList
|
||||
@add="emit('add')"
|
||||
@selectDict="e => emit('selectDict',e)"
|
||||
:list="groupByTranslateLanguage['common']"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="translate">
|
||||
<span>翻译:</span>
|
||||
<el-radio-group v-model="currentTranslateLanguage">
|
||||
<el-radio-button border v-for="i in translateLanguageList" :label="i">{{ i }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<DictGroup
|
||||
v-for="item in groupedByCategoryAndTag"
|
||||
:select-dict-name="runtimeStore.editDict.resourceId"
|
||||
@selectDict="e => emit('selectDict',e)"
|
||||
:groupByTag="item[1]"
|
||||
:category="item[0]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.dict-list-panel {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
$header-height: 60rem;
|
||||
padding: var(--space);
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: $header-height;
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 20rem;
|
||||
|
||||
.tab {
|
||||
color: var(--color-font-1);
|
||||
cursor: pointer;
|
||||
padding: 10rem;
|
||||
padding-bottom: 5rem;
|
||||
transition: all .5s;
|
||||
border-bottom: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rem;
|
||||
|
||||
&.active {
|
||||
border-bottom: 2px solid $main;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 30rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
display: flex;
|
||||
height: calc(100% - $header-height);
|
||||
|
||||
.dict-list-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding-right: var(--space);
|
||||
|
||||
.translate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-font-1);
|
||||
margin-bottom: 30rem;
|
||||
|
||||
& > span {
|
||||
font-size: 22rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,110 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Input from "@/components/Input.vue";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {Article, Word} from "@/types.ts";
|
||||
import ListItem from "@/components/list/ListItem.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {watch} from "vue";
|
||||
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list: Article[],
|
||||
activeIndex?: number,
|
||||
isActive?: boolean
|
||||
}>(), {
|
||||
activeIndex: -1,
|
||||
isActive: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectItem: [val: Article],
|
||||
delSelectItem: [],
|
||||
'update:searchKey': [val: string],
|
||||
'update:list': [list: Article[]],
|
||||
}>()
|
||||
|
||||
let searchKey = $ref('')
|
||||
let localList = $computed({
|
||||
get() {
|
||||
if (searchKey) {
|
||||
return props.list.filter((item: Article) => {
|
||||
//把搜索内容,分词之后,判断是否有这个词,比单纯遍历包含体验更好
|
||||
return searchKey.toLowerCase().split(' ').filter(v => v).some(value => {
|
||||
return item.title.toLowerCase().includes(value) || item.titleTranslate.toLowerCase().includes(value)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return props.list
|
||||
}
|
||||
},
|
||||
set(newValue) {
|
||||
emit('update:list', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const listRef: HTMLElement = $ref(null as any)
|
||||
|
||||
function scrollViewToCenter(index: number) {
|
||||
if (index === -1) return
|
||||
listRef.children[index + 1]?.scrollIntoView({block: 'center', behavior: 'smooth'})
|
||||
}
|
||||
|
||||
watch(() => props.activeIndex, (n: any) => {
|
||||
if (settingStore.showPanel) {
|
||||
scrollViewToCenter(n)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.isActive, (n: boolean) => {
|
||||
setTimeout(() => {
|
||||
if (n) scrollViewToCenter(props.activeIndex)
|
||||
}, 300)
|
||||
})
|
||||
|
||||
watch(() => props.list, () => {
|
||||
listRef.scrollTo(0, 0)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list"
|
||||
ref="listRef"
|
||||
>
|
||||
<div class="search">
|
||||
<Input v-model="searchKey"/>
|
||||
</div>
|
||||
<ListItem
|
||||
@click="emit('selectItem',item)"
|
||||
v-for="(item,i) in localList"
|
||||
:active="activeIndex === i"
|
||||
:key="item.id">
|
||||
<div class="name"> {{ `${i + 1}. ${item.title}` }}</div>
|
||||
<div class="item-translate"> {{ ` ${item.titleTranslate}` }}</div>
|
||||
</ListItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable.scss";
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rem;
|
||||
flex: 1;
|
||||
overflow: overlay;
|
||||
padding: 0 var(--space);
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.translate {
|
||||
font-size: 16rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,365 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {saveAs} from "file-saver";
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {Article, DefaultArticle, DictType, Sort} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import List from "@/components/list/List.vue";
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import EditArticle from "@/components/article/EditArticle.vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useDisableEventListener} from "@/hooks/event.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {nanoid} from "nanoid";
|
||||
|
||||
const base = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
let article = $ref<Article>(cloneDeep(DefaultArticle))
|
||||
let show = $ref(false)
|
||||
let showImportBtn = $ref(true)
|
||||
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.textCustomTranslate = editArticle.textCustomTranslate.trim()
|
||||
editArticle.textNetworkTranslate = editArticle.textNetworkTranslate.trim()
|
||||
|
||||
if (
|
||||
editArticle.title !== article.title ||
|
||||
editArticle.titleTranslate !== article.titleTranslate ||
|
||||
editArticle.text !== article.text ||
|
||||
editArticle.textCustomTranslate !== article.textCustomTranslate ||
|
||||
editArticle.textNetworkTranslate !== article.textNetworkTranslate ||
|
||||
editArticle.useTranslateType !== article.useTranslateType
|
||||
) {
|
||||
return MessageBox.confirm(
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => void 0,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (editArticle.title.trim() && editArticle.text.trim()) {
|
||||
return MessageBox.confirm(
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
|
||||
async function add() {
|
||||
let r = await checkDataChange()
|
||||
if (r) {
|
||||
article = cloneDeep(DefaultArticle)
|
||||
}
|
||||
}
|
||||
|
||||
function importData(e: Event) {
|
||||
showImportBtn = false
|
||||
let file = e.target.files[0]
|
||||
let reader = new FileReader();//新建一个FileReader
|
||||
reader.readAsText(file, "UTF-8");//读取文件
|
||||
reader.onload = function (evt) { //读取完文件之后会回来这里
|
||||
let fileString = evt.target.result; // 读取文件内容
|
||||
// console.log('fileString', fileString)
|
||||
try {
|
||||
let obj: any = JSON.parse(fileString)
|
||||
console.log('obj', obj)
|
||||
if (!obj?.name) {
|
||||
showImportBtn = true
|
||||
return ElMessage.error('请填写词典名称!')
|
||||
} else {
|
||||
if (base.myDictList.find(v => v.name === obj.name)) {
|
||||
showImportBtn = true
|
||||
return ElMessage.error('词典名称已存在!')
|
||||
}
|
||||
}
|
||||
if (!obj?.articles) {
|
||||
showImportBtn = true
|
||||
return ElMessage.error('请填写文章!')
|
||||
}
|
||||
if (!obj?.articles instanceof Array) {
|
||||
showImportBtn = true
|
||||
return ElMessage.error('请填写文章!')
|
||||
}
|
||||
for (let i = 0; i < obj.articles.length; i++) {
|
||||
let item = obj.articles[i]
|
||||
if (!item?.title) {
|
||||
showImportBtn = true
|
||||
return ElMessage.error(`请填写第${i + 1}篇文章名称!`)
|
||||
}
|
||||
if (!item?.text) {
|
||||
showImportBtn = true
|
||||
return ElMessage.error(`请填写第${i + 1}篇文章正文!`)
|
||||
}
|
||||
if (item?.useTranslateType === 'custom') {
|
||||
if (!item?.textCustomTranslate) {
|
||||
showImportBtn = true
|
||||
return ElMessage.error(`请填写第${i + 1}篇文章 翻译 正文!`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj.articles[i]?.titleTranslate) obj.articles[i].titleTranslate = ''
|
||||
if (!obj.articles[i]?.textFormat) obj.articles[i].textFormat = ''
|
||||
if (!obj.articles[i]?.textCustomTranslate) obj.articles[i].textCustomTranslate = ''
|
||||
if (!obj.articles[i]?.newWords) obj.articles[i].newWords = []
|
||||
if (!obj.articles[i]?.textCustomTranslateIsFormat) obj.articles[i].textCustomTranslateIsFormat = false
|
||||
if (!obj.articles[i]?.useTranslateType) obj.articles[i].useTranslateType = 'none'
|
||||
if (!obj.articles[i]?.textAllWords) obj.articles[i].textAllWords = []
|
||||
if (!obj.articles[i]?.sections) obj.articles[i].sections = []
|
||||
obj.articles[i].id = nanoid(6)
|
||||
}
|
||||
obj.sort = Sort.normal
|
||||
obj.type = DictType.customArticle
|
||||
obj.originWords = []
|
||||
obj.words = []
|
||||
obj.chapterWords = []
|
||||
obj.chapterWordNumber = 0
|
||||
obj.chapterIndex = 0
|
||||
obj.chapterWordIndex = 0
|
||||
obj.url = ''
|
||||
if (!obj.statistics) obj.statistics = []
|
||||
|
||||
ElMessage.success({
|
||||
message: '导入成功,已切换到',
|
||||
duration: 5000
|
||||
})
|
||||
base.myDictList.push(obj)
|
||||
runtimeStore.editDict = cloneDeep(runtimeStore.editDict)
|
||||
showImportBtn = true
|
||||
} catch (e) {
|
||||
showImportBtn = true
|
||||
ElMessage.error('文件解析失败,报错原因:' + e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
let data = {
|
||||
name: runtimeStore.editDict.name,
|
||||
articles: cloneDeep(runtimeStore.editDict.articles).map(v => {
|
||||
delete v.sections
|
||||
delete v.id
|
||||
return v
|
||||
}),
|
||||
url: location.origin + runtimeStore.editDict.url,
|
||||
statistics: runtimeStore.editDict.statistics,
|
||||
}
|
||||
|
||||
let blob = new Blob([JSON.stringify(data, null, 2)], {type: "text/plain;charset=utf-8"});
|
||||
saveAs(blob, `${data.name}.json`);
|
||||
}
|
||||
|
||||
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('保存成功!')
|
||||
return true
|
||||
}
|
||||
|
||||
function saveAndNext(val: Article) {
|
||||
if (saveArticle(val)) {
|
||||
add()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:full-screen="true"
|
||||
:header="false"
|
||||
>
|
||||
<div class="add-article">
|
||||
<div class="slide">
|
||||
<header>
|
||||
<div class="dict-name">{{ runtimeStore.editDict.name }}</div>
|
||||
<BaseIcon title="选择其他词典/文章" icon="carbon:change-catalog"/>
|
||||
</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" v-if="showImportBtn">
|
||||
<BaseButton>导入</BaseButton>
|
||||
<input type="file" accept="application/json" @change="importData">
|
||||
</div>
|
||||
<BaseButton @click="exportData">导出</BaseButton>
|
||||
<BaseButton @click="add">新增</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<EditArticle
|
||||
ref="editArticleRef"
|
||||
type="batch"
|
||||
@save="saveArticle"
|
||||
@saveAndNext="saveAndNext"
|
||||
:article="article"/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.add-article {
|
||||
//position: fixed;
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-font-1);
|
||||
background: var(--color-second-bg);
|
||||
display: flex;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 20rem;
|
||||
top: 20rem;
|
||||
}
|
||||
|
||||
.slide {
|
||||
height: 100%;
|
||||
background: var(--color-main-bg);
|
||||
padding: 0 10rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
$height: 60rem;
|
||||
|
||||
header {
|
||||
height: $height;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
//opacity: 0;
|
||||
|
||||
.dict-name {
|
||||
font-size: 30rem;
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 18rem;
|
||||
}
|
||||
|
||||
.translate-name {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.add {
|
||||
width: 260rem;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8rem;
|
||||
margin-bottom: 10rem;
|
||||
padding: 10rem;
|
||||
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: 10rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.import {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import {dictionaryResources} from '@/assets/dictionary.ts'
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import {onMounted, reactive, watch} from "vue"
|
||||
import {DefaultDict, Dict, DictResource, DictType, languageCategoryOptions, Sort, Word} from "@/types.ts"
|
||||
import {chunk, cloneDeep, groupBy, reverse, shuffle} from "lodash-es";
|
||||
import {onMounted, watch} from "vue"
|
||||
import {DefaultDict, Dict, DictResource, DictType, Sort} from "@/types.ts"
|
||||
import {chunk, cloneDeep, reverse, shuffle} from "lodash-es";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {Icon} from '@iconify/vue';
|
||||
import DictGroup from "@/components/toolbar/DictGroup.vue";
|
||||
import {ActivityCalendar} from "vue-activity-calendar";
|
||||
import "vue-activity-calendar/style.css";
|
||||
import ChapterList from "@/components/list/ChapterList.vue";
|
||||
@@ -17,22 +15,18 @@ import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import Slide from "@/components/Slide.vue";
|
||||
import DictList from "@/components/list/DictList.vue";
|
||||
import VirtualWordList from "@/components/list/VirtualWordList.vue";
|
||||
import {FormInstance, FormRules} from "element-plus";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import EditBatchArticleModal from "@/components/article/EditBatchArticleModal.vue";
|
||||
import {nanoid} from "nanoid";
|
||||
import DictListPanel from "@/components/DictListPanel.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
let currentLanguage = $ref('en')
|
||||
let currentTranslateLanguage = $ref('common')
|
||||
let groupByLanguage = groupBy(dictionaryResources, 'language')
|
||||
let translateLanguageList = $ref([])
|
||||
let wordList = $ref([])
|
||||
let router = useRouter()
|
||||
|
||||
let step = $ref(1)
|
||||
let loading = $ref(false)
|
||||
@@ -44,13 +38,9 @@ function close() {
|
||||
|
||||
async function selectDict(val: { dict: DictResource | Dict, index: number }) {
|
||||
let item = val.dict
|
||||
console.log('item', item)
|
||||
// console.log('item', item)
|
||||
step = 1
|
||||
isAddDict = false
|
||||
detailListTabIndex = 0
|
||||
wordFormMode = FormMode.None
|
||||
loading = true
|
||||
wordList = []
|
||||
let find: Dict = store.myDictList.find((v: Dict) => v.id === item.id)
|
||||
if (find) {
|
||||
runtimeStore.editDict = cloneDeep(find)
|
||||
@@ -62,7 +52,6 @@ async function selectDict(val: { dict: DictResource | Dict, index: number }) {
|
||||
}
|
||||
|
||||
if ([DictType.collect, DictType.simple, DictType.wrong].includes(runtimeStore.editDict.type)) {
|
||||
wordList = cloneDeep(runtimeStore.editDict.words)
|
||||
} else {
|
||||
let url = `./dicts/${runtimeStore.editDict.language}/${runtimeStore.editDict.type}/${runtimeStore.editDict.translateLanguage}/${runtimeStore.editDict.url}`;
|
||||
if (runtimeStore.editDict.type === DictType.word) {
|
||||
@@ -72,14 +61,12 @@ async function selectDict(val: { dict: DictResource | Dict, index: number }) {
|
||||
runtimeStore.editDict.originWords = cloneDeep(v)
|
||||
changeSort(runtimeStore.editDict.sort)
|
||||
}
|
||||
wordList = cloneDeep(runtimeStore.editDict.words)
|
||||
}
|
||||
|
||||
if (runtimeStore.editDict.type === DictType.customWord) {
|
||||
if (!runtimeStore.editDict.words.length) {
|
||||
changeSort(runtimeStore.editDict.sort)
|
||||
}
|
||||
wordList = cloneDeep(runtimeStore.editDict.words)
|
||||
}
|
||||
|
||||
if (runtimeStore.editDict.type === DictType.article) {
|
||||
@@ -101,49 +88,6 @@ function changeDict() {
|
||||
close()
|
||||
}
|
||||
|
||||
function groupByDictTags(dictList: DictResource[]) {
|
||||
return dictList.reduce<Record<string, DictResource[]>>((result, dict) => {
|
||||
dict.tags.forEach((tag) => {
|
||||
if (Object.prototype.hasOwnProperty.call(result, tag)) {
|
||||
result[tag].push(dict)
|
||||
} else {
|
||||
result[tag] = [dict]
|
||||
}
|
||||
})
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
const groupByTranslateLanguage = $computed(() => {
|
||||
let data: any
|
||||
if (currentLanguage === 'article') {
|
||||
let articleList = dictionaryResources.filter(v => v.type === 'article')
|
||||
data = groupBy(articleList, 'translateLanguage')
|
||||
} else if (currentLanguage === 'my') {
|
||||
data = {
|
||||
common: store.myDictList.concat([{name: '',} as any])
|
||||
}
|
||||
} else {
|
||||
data = groupBy(groupByLanguage[currentLanguage], 'translateLanguage')
|
||||
}
|
||||
// console.log('groupByTranslateLanguage', data)
|
||||
translateLanguageList = Object.keys(data)
|
||||
currentTranslateLanguage = translateLanguageList[0]
|
||||
return data
|
||||
})
|
||||
|
||||
const groupedByCategoryAndTag = $computed(() => {
|
||||
const currentTranslateLanguageDictList = groupByTranslateLanguage[currentTranslateLanguage]
|
||||
const groupByCategory = groupBy(currentTranslateLanguageDictList, 'category')
|
||||
|
||||
let data = []
|
||||
for (const [key, value] of Object.entries(groupByCategory)) {
|
||||
data.push([key, groupByDictTags(value)])
|
||||
}
|
||||
// console.log('groupedByCategoryAndTag', data)
|
||||
return data
|
||||
})
|
||||
|
||||
function clickEvent(e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
@@ -179,256 +123,23 @@ function changeSort(v) {
|
||||
resetChapterList()
|
||||
}
|
||||
|
||||
let detailListTabIndex = $ref(0)
|
||||
|
||||
function changeDetailListTab(val: number) {
|
||||
detailListTabIndex = val
|
||||
}
|
||||
|
||||
/**/
|
||||
/*词典相关*/
|
||||
/**/
|
||||
|
||||
|
||||
let isAddDict = $ref(false)
|
||||
let categoryList = {}
|
||||
let tagList = {}
|
||||
|
||||
const DefaultDictForm = {
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
tags: [],
|
||||
translateLanguage: 'zh-CN',
|
||||
language: 'en',
|
||||
type: DictType.customWord
|
||||
}
|
||||
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'},
|
||||
],
|
||||
category: [{required: true, message: '请选择', trigger: 'change'}],
|
||||
tags: [{required: true, message: '请选择', trigger: 'change'}],
|
||||
})
|
||||
|
||||
watch(() => dictForm.language, () => isAddDict && (dictForm.category = ''))
|
||||
watch(() => dictForm.category, () => isAddDict && (dictForm.tags = []))
|
||||
|
||||
function editDict() {
|
||||
// dictForm.id = store.editDict.id
|
||||
// dictForm.name = store.editDict.name
|
||||
// dictForm.description = store.editDict.description
|
||||
console.log('store.editDict', runtimeStore.editDict)
|
||||
dictForm = cloneDeep(runtimeStore.editDict)
|
||||
//直接复制,上面会watch到category,然后把tags 设置为空
|
||||
setTimeout(() => isAddDict = true, 0)
|
||||
show = false
|
||||
setTimeout(() => {
|
||||
router.push({path: '/dict', query: {type: 'editDict'}})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function closeDictForm() {
|
||||
if (dictForm.id) {
|
||||
isAddDict = false
|
||||
dictForm = cloneDeep(DefaultDictForm)
|
||||
} else {
|
||||
step = 0
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
await dictFormRef.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
let data: Dict = {
|
||||
...DefaultDict,
|
||||
...dictForm,
|
||||
}
|
||||
//任意修改,都将其变为自定义词典
|
||||
if (data.type === DictType.word) data.type = DictType.customWord
|
||||
if (data.type === DictType.article) data.type = DictType.customArticle
|
||||
|
||||
if (data.id) {
|
||||
let rIndex = store.myDictList.findIndex(v => v.id === data.id)
|
||||
store.myDictList[rIndex] = cloneDeep(data)
|
||||
runtimeStore.editDict = cloneDeep(data)
|
||||
isAddDict = false
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
data.id = 'custom-dict-' + Date.now()
|
||||
if (store.myDictList.find(v => v.name === dictForm.name)) {
|
||||
return ElMessage.warning('已有相同名称词典!')
|
||||
} else {
|
||||
store.myDictList.push(cloneDeep(data))
|
||||
runtimeStore.editDict = cloneDeep(data)
|
||||
isAddDict = false
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
}
|
||||
console.log('submit!', data)
|
||||
} else {
|
||||
ElMessage.warning('请填写完整')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**/
|
||||
/*词典相关*/
|
||||
/**/
|
||||
|
||||
|
||||
/**/
|
||||
/* 单词修改相关*/
|
||||
/**/
|
||||
enum FormMode {
|
||||
None = -1,
|
||||
Add = -2,
|
||||
}
|
||||
|
||||
const DefaultFormWord = {
|
||||
name: '',
|
||||
usphone: '',
|
||||
ukphone: '',
|
||||
trans: ''
|
||||
}
|
||||
|
||||
let wordFormMode = $ref(FormMode.None)
|
||||
let wordForm = $ref(cloneDeep(DefaultFormWord))
|
||||
const wordFormRef = $ref<FormInstance>()
|
||||
const wordRules = reactive<FormRules>({
|
||||
name: [
|
||||
{required: true, message: '请输入单词', trigger: 'blur'},
|
||||
{max: 30, message: '名称不能超过30个字符', trigger: 'blur'},
|
||||
],
|
||||
})
|
||||
let wordListRef: any = $ref()
|
||||
|
||||
//同步到我的词典列表
|
||||
function syncMyDictList() {
|
||||
//任意修改,都将其变为自定义词典
|
||||
if (runtimeStore.editDict.type === DictType.word) runtimeStore.editDict.type = DictType.customWord
|
||||
if (runtimeStore.editDict.type === DictType.article) runtimeStore.editDict.type = DictType.customArticle
|
||||
|
||||
let rIndex = store.myDictList.findIndex(v => v.id === runtimeStore.editDict.id)
|
||||
if (rIndex > -1) {
|
||||
store.myDictList[rIndex] = cloneDeep(runtimeStore.editDict)
|
||||
} else {
|
||||
store.myDictList.push(cloneDeep(runtimeStore.editDict))
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmitWord() {
|
||||
await wordFormRef.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
let data: any = cloneDeep(wordForm)
|
||||
if (data.trans) {
|
||||
data.trans = data.trans.split('\n');
|
||||
} else {
|
||||
data.trans = []
|
||||
}
|
||||
if (wordFormMode === FormMode.Add) {
|
||||
if (wordList.find(v => v.name === wordForm.name)) {
|
||||
return ElMessage.warning('已有相同名称单词!')
|
||||
} else {
|
||||
runtimeStore.editDict.originWords.push(data)
|
||||
runtimeStore.editDict.words.push(data)
|
||||
//因为虚拟列表,必须重新赋值才能检测到更新
|
||||
wordList = cloneDeep(runtimeStore.editDict.words)
|
||||
|
||||
if (runtimeStore.editDict.chapterWords.length) {
|
||||
runtimeStore.editDict.chapterWords[runtimeStore.editDict.chapterWords.length - 1].push(data)
|
||||
} else {
|
||||
runtimeStore.editDict.chapterWords.push([data])
|
||||
runtimeStore.editDict.chapterIndex = 0
|
||||
}
|
||||
|
||||
ElMessage.success('添加成功')
|
||||
wordForm = cloneDeep(DefaultFormWord)
|
||||
setTimeout(wordListRef?.scrollToBottom, 100)
|
||||
}
|
||||
console.log('runtimeStore.editDict', runtimeStore.editDict)
|
||||
} else {
|
||||
let oldData = cloneDeep(runtimeStore.editDict.words[wordFormMode])
|
||||
runtimeStore.editDict.words[wordFormMode] = data
|
||||
//因为虚拟列表,必须重新赋值才能检测到更新
|
||||
wordList = cloneDeep(runtimeStore.editDict.words)
|
||||
//同步到原始列表,因为word可能是随机的,所以需要自己寻找index去修改原始列表
|
||||
let rIndex = runtimeStore.editDict.originWords.findIndex(v => v.name === oldData.name)
|
||||
if (rIndex > -1) {
|
||||
runtimeStore.editDict.originWords[rIndex] = data
|
||||
}
|
||||
|
||||
runtimeStore.editDict.chapterWords = runtimeStore.editDict.chapterWords.map(list => {
|
||||
let rIndex2 = list.findIndex(v => v.name === oldData.name)
|
||||
if (rIndex2 > -1) {
|
||||
list[rIndex2] = data
|
||||
}
|
||||
return list
|
||||
})
|
||||
console.log('runtimeStore.editDict.chapterWords', runtimeStore.editDict.chapterWords)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
syncMyDictList()
|
||||
} else {
|
||||
ElMessage.warning('请填写完整')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function delWord(word: Word, index: number) {
|
||||
//同步到原始列表,因为word可能是随机的,所以需要自己寻找index去修改原始列表
|
||||
let rIndex = runtimeStore.editDict.originWords.findIndex(v => v.name === word.name)
|
||||
if (rIndex > -1) {
|
||||
runtimeStore.editDict.originWords.splice(rIndex, 1)
|
||||
}
|
||||
|
||||
runtimeStore.editDict.chapterWords.map(list => {
|
||||
let rIndex2 = list.findIndex(v => v.name === word.name)
|
||||
if (rIndex2 > -1) {
|
||||
list.splice(rIndex2, 1)
|
||||
}
|
||||
})
|
||||
|
||||
runtimeStore.editDict.chapterWords = runtimeStore.editDict.chapterWords.filter(v => v.length)
|
||||
if (runtimeStore.editDict.chapterWords.length === 0) runtimeStore.editDict.chapterIndex = -1
|
||||
else {
|
||||
if (runtimeStore.editDict.chapterIndex >= runtimeStore.editDict.chapterWords.length) {
|
||||
runtimeStore.editDict.chapterIndex = runtimeStore.editDict.chapterWords.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
runtimeStore.editDict.words.splice(index, 1)
|
||||
wordList = cloneDeep(runtimeStore.editDict.words)
|
||||
syncMyDictList()
|
||||
|
||||
closeWordForm()
|
||||
}
|
||||
|
||||
function editWord(val: { word: Word, index: number }) {
|
||||
wordFormMode = val.index
|
||||
wordForm.name = val.word.name
|
||||
wordForm.ukphone = val.word.ukphone
|
||||
wordForm.usphone = val.word.usphone
|
||||
wordForm.trans = val.word.trans.join('\n')
|
||||
}
|
||||
|
||||
function closeWordForm() {
|
||||
wordFormMode = FormMode.None
|
||||
wordForm = cloneDeep(DefaultFormWord)
|
||||
}
|
||||
|
||||
function addWord() {
|
||||
// setTimeout(wordListRef?.scrollToBottom, 100)
|
||||
detailListTabIndex = 1
|
||||
wordFormMode = FormMode.Add
|
||||
wordForm = cloneDeep(DefaultFormWord)
|
||||
}
|
||||
|
||||
function add() {
|
||||
show = false
|
||||
setTimeout(() => {
|
||||
router.push({path: '/dict', query: {type: 'addWordOrArticle'}})
|
||||
}, 500)
|
||||
if (dictIsArticle) {
|
||||
|
||||
} else {
|
||||
addWord()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,75 +148,35 @@ function add() {
|
||||
/**/
|
||||
|
||||
|
||||
/**/
|
||||
/* 文章修改相关*/
|
||||
/**/
|
||||
|
||||
function delChapter(index: number) {
|
||||
runtimeStore.editDict.articles.splice(index, 1)
|
||||
if (runtimeStore.editDict.chapterIndex >= index) runtimeStore.editDict.chapterIndex--
|
||||
if (runtimeStore.editDict.chapterIndex < 0) runtimeStore.editDict.chapterIndex = 0
|
||||
|
||||
syncMyDictList()
|
||||
}
|
||||
|
||||
/**/
|
||||
/* 文章修改相关*/
|
||||
/**/
|
||||
|
||||
watch(() => step, v => {
|
||||
if (v === 0) {
|
||||
closeWordForm()
|
||||
closeDictForm()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
dictionaryResources.map(v => {
|
||||
if (categoryList[v.language]) {
|
||||
if (!categoryList[v.language].find(w => w === v.category)) {
|
||||
categoryList[v.language].push(v.category)
|
||||
}
|
||||
} else {
|
||||
categoryList[v.language] = [v.category]
|
||||
}
|
||||
if (tagList[v.category]) {
|
||||
tagList[v.category] = Array.from(new Set(tagList[v.category].concat(v.tags)))
|
||||
} else {
|
||||
tagList[v.category] = v.tags
|
||||
}
|
||||
})
|
||||
|
||||
emitter.on(EventKey.openDictModal, (type: 'detail' | 'list' | 'my' | 'collect' | 'simple') => {
|
||||
emitter.on(EventKey.openDictModal, (type: 'detail' | 'list' | 'my') => {
|
||||
if (type === "detail") {
|
||||
selectDict({dict: store.currentDict, index: 0})
|
||||
}
|
||||
if (type === "list") {
|
||||
currentLanguage = 'en'
|
||||
// currentLanguage = 'en'
|
||||
step = 0
|
||||
}
|
||||
if (type === "my") {
|
||||
currentLanguage = 'my'
|
||||
// currentLanguage = 'my'
|
||||
step = 0
|
||||
}
|
||||
if (type === "collect") {
|
||||
selectDict({dict: store.collect, index: 0})
|
||||
wordFormMode = FormMode.Add
|
||||
addWord()
|
||||
}
|
||||
if (type === "simple") {
|
||||
selectDict({dict: store.simple, index: 0})
|
||||
addWord()
|
||||
}
|
||||
|
||||
show = true
|
||||
})
|
||||
|
||||
// console.log('categoryList', categoryList)
|
||||
// console.log('tagList', tagList)
|
||||
})
|
||||
|
||||
function addDict() {
|
||||
show = false
|
||||
setTimeout(() => {
|
||||
router.push({path: '/dict', query: {type: 'addDict'}})
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -515,49 +186,10 @@ onMounted(() => {
|
||||
:show-close="false">
|
||||
<div id="DictDialog">
|
||||
<Slide :slide-count="2" :step="step">
|
||||
<div class="dict-page">
|
||||
<header>
|
||||
<div class="tabs">
|
||||
<div class="tab"
|
||||
:class="currentLanguage === item.id && 'active'"
|
||||
@click="currentLanguage = item.id"
|
||||
v-for="item in languageCategoryOptions">
|
||||
<img :src='item.flag'/>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Icon @click="close"
|
||||
class="hvr-grow pointer"
|
||||
width="20" color="#929596"
|
||||
icon="ion:close-outline"/>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="dict-list-wrapper">
|
||||
<template v-if="currentLanguage === 'my'">
|
||||
<DictList
|
||||
@add="step = 1;isAddDict = true"
|
||||
@selectDict="selectDict"
|
||||
:list="groupByTranslateLanguage['common']"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="translate">
|
||||
<span>翻译:</span>
|
||||
<el-radio-group v-model="currentTranslateLanguage">
|
||||
<el-radio-button border v-for="i in translateLanguageList" :label="i">{{ i }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<DictGroup
|
||||
v-for="item in groupedByCategoryAndTag"
|
||||
:select-dict-name="runtimeStore.editDict.resourceId"
|
||||
@selectDict="selectDict"
|
||||
@detail="step = 1"
|
||||
:groupByTag="item[1]"
|
||||
:category="item[0]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DictListPanel
|
||||
@add="addDict"
|
||||
@select-dict="selectDict"
|
||||
/>
|
||||
<div class="dict-detail-page">
|
||||
<header>
|
||||
<div class="left" @click.stop="step = 0">
|
||||
@@ -571,7 +203,7 @@ onMounted(() => {
|
||||
width="20" color="#929596"
|
||||
icon="ion:close-outline"/>
|
||||
</header>
|
||||
<div class="detail" v-if="!isAddDict">
|
||||
<div class="detail">
|
||||
<div class="page-content">
|
||||
<div class="left-column">
|
||||
<BaseIcon
|
||||
@@ -591,7 +223,7 @@ onMounted(() => {
|
||||
}}词</span>
|
||||
</div>
|
||||
<BaseIcon icon="mi:add"
|
||||
@click='addWord'
|
||||
@click='add'
|
||||
/>
|
||||
</div>
|
||||
<div class="text">开始日期:-</div>
|
||||
@@ -604,8 +236,8 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="center-column">
|
||||
<div class="setting" v-if="wordFormMode === FormMode.None">
|
||||
<div class="common-title">学习设置</div>
|
||||
<div class="common-title">学习设置</div>
|
||||
<div class="setting">
|
||||
<template v-if="!dictIsArticle">
|
||||
<div class="row">
|
||||
<div class="label">每章单词数</div>
|
||||
@@ -681,85 +313,16 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add" v-else>
|
||||
<div class="common-title">{{ wordFormMode === FormMode.Add ? '添加' : '修改' }}单词</div>
|
||||
<el-form
|
||||
class="form"
|
||||
ref="wordFormRef"
|
||||
:rules="wordRules"
|
||||
:model="wordForm"
|
||||
label-width="140rem">
|
||||
<el-form-item label="单词" prop="name">
|
||||
<el-input v-model="wordForm.name"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="翻译">
|
||||
<el-input v-model="wordForm.trans"
|
||||
placeholder="多个翻译请换行"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
type="textarea"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="音标/发音/注音①">
|
||||
<el-input v-model="wordForm.usphone"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="音标/发音/注音②">
|
||||
<el-input v-model="wordForm.ukphone"/>
|
||||
</el-form-item>
|
||||
<div class="flex-center">
|
||||
<el-button @click="closeWordForm">关闭</el-button>
|
||||
<el-button type="primary" @click="onSubmitWord">{{
|
||||
wordFormMode === FormMode.Add ? '添加' : '保存'
|
||||
}}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-column">
|
||||
<div class="tabs">
|
||||
<div class="tab"
|
||||
@click="detailListTabIndex = 0"
|
||||
:class="detailListTabIndex === 0 && 'active'">
|
||||
<span>{{ dictIsArticle ? '文章' : '章节' }}列表</span>
|
||||
</div>
|
||||
<div class="tab"
|
||||
v-if="!dictIsArticle"
|
||||
@click="detailListTabIndex = 1"
|
||||
:class="detailListTabIndex === 1 && 'active'">
|
||||
<span>单词列表</span>
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
v-if="detailListTabIndex === 0"
|
||||
>
|
||||
<ChapterList
|
||||
v-if="chapterList"
|
||||
v-loading="loading"
|
||||
:is-article="dictIsArticle"
|
||||
@del="delChapter"
|
||||
v-model:active-index="runtimeStore.editDict.chapterIndex"
|
||||
:dict="runtimeStore.editDict"/>
|
||||
<Empty v-else :show-add="true" @add="add"/>
|
||||
</template>
|
||||
|
||||
<div class="scroll" v-else>
|
||||
<VirtualWordList
|
||||
ref="wordListRef"
|
||||
v-if="wordList.length"
|
||||
class="word-list"
|
||||
:is-active="true"
|
||||
@change="editWord"
|
||||
:list="wordList"
|
||||
:activeIndex="wordFormMode">
|
||||
<template v-slot="{word,index}">
|
||||
<BaseIcon
|
||||
class-name="del"
|
||||
@click="delWord(word,index)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</template>
|
||||
</VirtualWordList>
|
||||
<Empty v-else :show-add="true" @add="addWord"/>
|
||||
</div>
|
||||
<div class="common-title">{{ dictIsArticle ? '文章' : '章节' }}列表</div>
|
||||
<ChapterList
|
||||
v-if="chapterList"
|
||||
v-loading="loading"
|
||||
:is-article="dictIsArticle"
|
||||
v-model:active-index="runtimeStore.editDict.chapterIndex"
|
||||
:dict="runtimeStore.editDict"/>
|
||||
<Empty v-else :show-add="true" @add="add"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="false" class="activity">
|
||||
@@ -781,62 +344,6 @@ onMounted(() => {
|
||||
<BaseButton @click="changeDict">切换</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-dict" v-else>
|
||||
<div class="wrapper">
|
||||
<div class="common-title">{{ dictForm.id ? '修改' : '添加' }}词典</div>
|
||||
<el-form
|
||||
ref="dictFormRef"
|
||||
:rules="dictRules"
|
||||
:model="dictForm"
|
||||
label-width="120rem">
|
||||
<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>
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select v-model="dictForm.category" placeholder="请选择选项">
|
||||
<el-option :label="i" :value="i" v-for="i in categoryList[dictForm.language]"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签" prop="tags">
|
||||
<el-select
|
||||
multiple
|
||||
v-model="dictForm.tags" placeholder="请选择选项">
|
||||
<el-option :label="i" :value="i" v-for="i in tagList[dictForm.category]"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="dictForm.type" placeholder="请选择选项">
|
||||
<el-option label="单词" :value="DictType.customWord"/>
|
||||
<el-option label="文章" :value="DictType.customArticle"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="flex-center">
|
||||
<el-button @click="closeDictForm">关闭</el-button>
|
||||
<el-button type="primary" @click="onSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Slide>
|
||||
</div>
|
||||
@@ -848,9 +355,6 @@ onMounted(() => {
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
$modal-mask-bg: rgba(#000, .15);
|
||||
$radius: 16rem;
|
||||
$time: 0.3s;
|
||||
$header-height: 60rem;
|
||||
|
||||
#DictDialog {
|
||||
@@ -864,74 +368,9 @@ $header-height: 60rem;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
.dict-page {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
$header-height: 60rem;
|
||||
padding: var(--space);
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: $header-height;
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 20rem;
|
||||
|
||||
.tab {
|
||||
color: var(--color-font-1);
|
||||
cursor: pointer;
|
||||
padding: 10rem;
|
||||
padding-bottom: 5rem;
|
||||
transition: all .5s;
|
||||
border-bottom: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rem;
|
||||
|
||||
&.active {
|
||||
border-bottom: 2px solid $main;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 30rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
display: flex;
|
||||
height: calc(100% - $header-height);
|
||||
|
||||
.dict-list-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding-right: var(--space);
|
||||
|
||||
.translate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-font-1);
|
||||
margin-bottom: 30rem;
|
||||
|
||||
& > span {
|
||||
font-size: 22rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dict-detail-page {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
$header-height: 60rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1089,23 +528,6 @@ $header-height: 60rem;
|
||||
padding-right: var(--space);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-dict {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.wrapper {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -82,11 +82,13 @@ watch(() => props.list, () => {
|
||||
@click="emit('selectItem',source)"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="item-title">
|
||||
<div class="name"> {{ `${index + 1}. ${source.title}` }}</div>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="source.titleTranslate && showTranslate">
|
||||
<div class="item-translate"> {{ ` ${source.titleTranslate}` }}</div>
|
||||
<div class="title-wrapper">
|
||||
<div class="item-title">
|
||||
<div class="name"> {{ `${index + 1}. ${source.title}` }}</div>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="source.titleTranslate && showTranslate">
|
||||
<div class="item-translate"> {{ ` ${source.titleTranslate}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
||||
@@ -14,7 +14,6 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:activeIndex': [index: number]
|
||||
del: [index: number]
|
||||
}>()
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
@@ -43,28 +42,23 @@ function showWordListModal(index: number, item: Word[]) {
|
||||
<div class="flex gap10">
|
||||
<input type="radio" :checked="activeIndex === index">
|
||||
<div class="left">
|
||||
<template v-if="isArticle">
|
||||
<div class="item-title"
|
||||
@click.stop="emitter.emit(EventKey.openArticleListModal,item)"
|
||||
>{{ index + 1 }}. {{ item.title }}
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="item.titleTranslate"> {{ item.titleTranslate }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="item-title"
|
||||
@click.stop="showWordListModal(index,item)"
|
||||
>第{{ index + 1 }}章 {{ item.length }}词
|
||||
</div>
|
||||
</template>
|
||||
<div class="title-wrapper">
|
||||
<template v-if="isArticle">
|
||||
<div class="item-title"
|
||||
@click.stop="emitter.emit(EventKey.openArticleListModal,item)"
|
||||
>{{ index + 1 }}. {{ item.title }}
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="item.titleTranslate"> {{ item.titleTranslate }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="item-title"
|
||||
@click.stop="showWordListModal(index,item)"
|
||||
>第{{ index + 1 }}章 {{ item.length }}词
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right" v-if="isArticle">
|
||||
<BaseIcon
|
||||
class-name="del"
|
||||
@click="emit('del',index)"
|
||||
title="移除"
|
||||
icon="solar:trash-bin-minimalistic-linear"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -59,13 +59,15 @@ const playWordAudio = usePlayWordAudio()
|
||||
@click="emit('change',{word:source,index})"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="item-title">
|
||||
<span class="word" :class="!showWord && 'text-shadow'">{{ source.name }}</span>
|
||||
<span class="phonetic">{{ source.usphone }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(source.name)"></VolumeIcon>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="source.trans.length && showTranslate">
|
||||
<div v-for="item in source.trans">{{ item }}</div>
|
||||
<div class="title-wrapper">
|
||||
<div class="item-title">
|
||||
<span class="word" :class="!showWord && 'text-shadow'">{{ source.name }}</span>
|
||||
<span class="phonetic">{{ source.usphone }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(source.name)"></VolumeIcon>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="source.trans.length && showTranslate">
|
||||
<div v-for="item in source.trans">{{ item }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
||||
@@ -76,13 +76,15 @@ defineExpose({scrollToBottom})
|
||||
@click="emit('change',{word:source,index})"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="item-title">
|
||||
<span class="word" :class="!showWord && 'text-shadow'">{{ source.name }}</span>
|
||||
<span class="phonetic">{{ source.usphone }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(source.name)"></VolumeIcon>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="source.trans.length && showTranslate">
|
||||
<div v-for="item in source.trans">{{ item }}</div>
|
||||
<div class="title-wrapper">
|
||||
<div class="item-title">
|
||||
<span class="word" :class="!showWord && 'text-shadow'">{{ source.name }}</span>
|
||||
<span class="phonetic">{{ source.usphone }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(source.name)"></VolumeIcon>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="source.trans.length && showTranslate">
|
||||
<div v-for="item in source.trans">{{ item }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
||||
Reference in New Issue
Block a user