feat:修改数据结构

This commit is contained in:
zyronon
2025-07-21 01:53:10 +08:00
parent 3a913c8520
commit a2f3bd6363
5 changed files with 166 additions and 50 deletions

View File

@@ -72,13 +72,13 @@ function startStudy() {
<div class="bg-slate-200 p-3 gap-4 rounded-md cursor-pointer flex items-center">
<span class="text-lg font-bold"
@click="getBookDetail2(base.currentBook)">{{
base.currentBook.name ?? '请选择书籍开始学习'
base.currentBook.name || '请选择书籍开始学习'
}}</span>
<BaseIcon @click="showSearchDialog = true"
:icon="base.currentBook.name?'gg:arrows-exchange':'fluent:add-20-filled'"/>
:icon="base.currentBook.name ? 'gg:arrows-exchange':'fluent:add-20-filled'"/>
</div>
<div class="rounded-xl bg-slate-800 flex items-center py-3 px-5 text-white cursor-pointer"
:class="base.currentBook.name??'opacity-70 cursor-not-allowed'"
:class="base.currentBook.name || 'opacity-70 cursor-not-allowed'"
@click="startStudy">
开始学习
</div>

View File

@@ -1,5 +1,9 @@
<script setup lang="tsx">
import {getDefaultWord} from "@/types";
import type {Word} from "@/types";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {computed, onMounted, reactive} from "vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
@@ -8,7 +12,6 @@ import {nanoid} from "nanoid";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseTable from "@/pages/pc/components/BaseTable.vue";
import WordItem from "@/pages/pc/components/WordItem.vue";
import type {Word} from "@/types.ts";
import type {FormInstance, FormRules} from "element-plus";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import BackIcon from "@/components/BackIcon.vue";
@@ -64,40 +67,144 @@ const DefaultFormWord = {
etymology: '',
}
let wordForm = $ref(cloneDeep(DefaultFormWord))
const wordFormRef = $ref<FormInstance>()
let wordFormRef = $ref<FormInstance>()
const wordRules = reactive<FormRules>({
name: [
word: [
{required: true, message: '请输入单词', trigger: 'blur'},
{max: 30, message: '名称不能超过30个字符', trigger: 'blur'},
],
})
//从字符串里面转换为Word格式
function convertToWord(raw) {
const safeString = (str) => (typeof str === 'string' ? str.trim() : '');
const safeSplit = (str, sep) =>
safeString(str) ? safeString(str).split(sep).filter(Boolean) : [];
// 1. trans
const trans = safeSplit(raw.trans, '\n').map(line => {
const match = line.match(/^([^\s.]+\.?)\s*(.*)$/);
if (match) {
let pos = safeString(match[1]);
let cn = safeString(match[2]);
// 如果 pos 不是常规词性(不以字母开头),例如 "【名】"
if (!/^[a-zA-Z]+\.?$/.test(pos)) {
cn = safeString(line); // 整行放到 cn
pos = ''; // pos 置空
}
return {pos, cn};
}
return {pos: '', cn: safeString(line)};
});
// 2. sentences
const sentences = safeSplit(raw.sentences, '\n\n').map(block => {
const [c, cn] = block.split('\n');
return {c: safeString(c), cn: safeString(cn)};
});
// 3. phrases
const phrases = safeSplit(raw.phrases, '\n\n').map(block => {
const [c, cn] = block.split('\n');
return {c: safeString(c), cn: safeString(cn)};
});
// 4. synos
const synos = safeSplit(raw.synos, '\n\n').map(block => {
const lines = block.split('\n').map(safeString);
const [posCn, wsStr] = lines;
let pos = '';
let cn = '';
if (posCn) {
const posMatch = posCn.match(/^([a-zA-Z.]+)(.*)$/);
pos = posMatch ? safeString(posMatch[1]) : '';
cn = posMatch ? safeString(posMatch[2]) : safeString(posCn);
}
const ws = wsStr ? wsStr.split('/').map(safeString) : [];
return {pos, cn, ws};
});
// 5. relWords
const relWordsText = safeString(raw.relWords);
let root = '';
const rels = [];
if (relWordsText) {
const relLines = relWordsText.split('\n').filter(Boolean);
if (relLines.length > 0) {
root = safeString(relLines[0].replace(/^词根:/, ''));
let currentPos = '';
let currentWords = [];
for (let i = 1; i < relLines.length; i++) {
const line = relLines[i].trim();
if (!line) continue;
if (/^[a-z]+\./i.test(line)) {
if (currentPos && currentWords.length > 0) {
rels.push({pos: currentPos, words: currentWords});
}
currentPos = safeString(line.replace(':', ''));
currentWords = [];
} else if (line.includes(':')) {
const [c, cn] = line.split(':');
currentWords.push({c: safeString(c), cn: safeString(cn)});
}
}
if (currentPos && currentWords.length > 0) {
rels.push({pos: currentPos, words: currentWords});
}
}
}
// 6. etymology
const etymology = safeSplit(raw.etymology, '\n\n').map(block => {
const lines = block.split('\n').map(safeString);
const t = lines.shift() || '';
const d = lines.join('\n').trim();
return {t, d};
});
return getDefaultWord({
word: safeString(raw.word),
phonetic0: safeString(raw.phonetic0),
phonetic1: safeString(raw.phonetic1),
trans,
sentences,
phrases,
synos,
relWords: {root, rels},
etymology,
custom: true
});
}
//TODO trans结构变了
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 = []
}
let data: any = convertToWord(wordForm)
if (wordFormData.type === FormMode.Add) {
data.id = nanoid(6)
data.checked = false
let r = list.find(v => v.word === wordForm.word)
// if (r) return ElMessage.warning('已有相同名称单词!')
// else list.push(data)
list.push(data)
if (r) return ElMessage.warning('已有相同名称单词!')
else list.push(data)
ElMessage.success('添加成功')
wordForm = cloneDeep(DefaultFormWord)
// setTimeout(wordListRef?.scrollToBottom, 100)
} else {
let r = list.find(v => v.id === wordFormData.id)
if (r) assign(r, data)
r = list.find(v => v.id === wordFormData.id)
if (r) assign(r, data)
ElMessage.success('修改成功')
if (r) {
assign(r, data)
ElMessage.success('修改成功')
}else {
ElMessage.success('修改失败,未找到单词')
}
}
} else {
ElMessage.warning('请填写完整')
@@ -129,7 +236,9 @@ function editWord(word: Word) {
wordForm.sentences = word.sentences.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
wordForm.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
wordForm.synos = word.synos.map(v => (v.pos + v.cn + "\n" + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
wordForm.relWords = word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + "\n" + v.cn))).replaceAll('"', '')).join('\n\n')
wordForm.relWords = '词根:' + word.relWords.root + '\n\n' +
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')
wordForm.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
}
function addWord() {
@@ -231,13 +340,13 @@ defineRender(() => {
</div>
{
wordFormData.type ? (
<div class="flex-1 ml-4 overflow-auto">
<div class="flex-1 flex flex-col ml-4">
<div class="common-title">
{wordFormData.type === FormMode.Add ? '添加' : '修改'}单词
</div>
<el-form
className="form"
ref="wordFormRef"
class="flex-1 overflow-auto pr-2"
ref={e => wordFormRef = e}
rules={wordRules}
model={wordForm}
label-width="7rem">
@@ -247,13 +356,13 @@ defineRender(() => {
onUpdate:modelValue={e => wordForm.word = e}
/>
</el-form-item>
<el-form-item label="音标/发音①">
<el-form-item label="英音音标">
<el-input
modelValue={wordForm.phonetic0}
onUpdate:modelValue={e => wordForm.phonetic0 = e}
/>
</el-form-item>
<el-form-item label="音标/发音②">
<el-form-item label="美音音标">
<el-input
modelValue={wordForm.phonetic1}
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
@@ -262,7 +371,7 @@ defineRender(() => {
<el-input
modelValue={wordForm.trans}
onUpdate:modelValue={e => wordForm.trans = e}
placeholder="一行一个翻译前面词性后面内容n.取消);多个翻译请换行"
placeholder="一行一个翻译,前面词性,后面内容(n.取消);多个翻译请换行"
autosize={{minRows: 6, maxRows: 10}}
type="textarea"/>
</el-form-item>
@@ -286,35 +395,35 @@ defineRender(() => {
<el-input
modelValue={wordForm.synos}
onUpdate:modelValue={e => wordForm.synos = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}
type="textarea"/>
</el-form-item>
<el-form-item label="同根词">
<el-input
modelValue={wordForm.relWords}
onUpdate:modelValue={e => wordForm.relWords = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}
type="textarea"/>
</el-form-item>
<el-form-item label="词源">
<el-input
modelValue={wordForm.etymology}
onUpdate:modelValue={e => wordForm.etymology = e}
placeholder="一行原文,一行译文;多个请换两行"
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 10}}
type="textarea"/>
</el-form-item>
<div class="center">
<el-button
onClick={closeWordForm}>关闭
</el-button>
<el-button type="primary"
onClick={onSubmitWord}>保存
</el-button>
</div>
</el-form>
<div class="center">
<el-button
onClick={closeWordForm}>关闭
</el-button>
<el-button type="primary"
onClick={onSubmitWord}>保存
</el-button>
</div>
</div>
) : null
}

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import {onMounted, watch} from "vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {useBaseStore} from "@/stores/base.ts";
import {onMounted} from "vue";
import Statistics from "@/pages/pc/word/Statistics.vue";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
@@ -14,11 +12,12 @@ import useTheme from "@/hooks/theme.ts";
import TypingWord from "@/pages/pc/word/components/TypingWord.vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import {cloneDeep} from "lodash-es";
import {useRouter} from "vue-router";
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const {toggleTheme} = useTheme()
const router = useRouter()
function next() {
emitter.emit(EventKey.resetWord)
@@ -59,6 +58,8 @@ onMounted(() => {
settingStore.dictation = false
if (runtimeStore.routeData) {
studyData = runtimeStore.routeData
} else {
router.push('/word')
}
})

View File

@@ -18,7 +18,6 @@ import DictGroup from "@/pages/pc/components/list/DictGroup.vue";
import {cloneDeep} from "lodash-es";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {getArticleBookDataByUrl} from "@/utils/article.ts";
import Typing from "@/pages/pc/word/components/Typing.vue";
const store = useBaseStore()
const router = useRouter()
@@ -46,7 +45,9 @@ useEvent(EventKey.changeDict, () => {
})
function study() {
nav('study-word', {}, currentStudy)
if (store.sdict.name) {
nav('study-word', {}, currentStudy)
}
}
let show = $ref(false)
@@ -83,16 +84,16 @@ function addDict() {
<template>
<BasePage>
<div class="card flex gap-10" v-loading="!store.load">
<div class="card flex gap-10">
<div class="flex-1 flex flex-col gap-2">
<div class="flex">
<div class="bg-slate-200 px-3 h-14 rounded-md flex items-center">
<span class="text-xl font-bold">{{ store.sdict.name }}</span>
<span class="text-xl font-bold">{{ store.sdict.name || '请选择书籍开始学习' }}</span>
<BaseIcon
title="切换词典"
icon="gg:arrows-exchange"
:icon="store.sdict.name ? 'gg:arrows-exchange':'fluent:add-20-filled'"
class="ml-4"
@click="router.push('/dict')"/>
@click="dictListRef.startSearch()"/>
</div>
</div>
<div class="">
@@ -142,6 +143,7 @@ function addDict() {
个单词
</div>
<div class="rounded-xl bg-slate-800 flex items-center gap-2 py-3 px-5 text-white cursor-pointer"
:class="store.sdict.name || 'opacity-70 cursor-not-allowed'"
@click="study">
<span>开始学习</span>
<Icon icon="icons8:right-round" class="text-2xl"/>
@@ -156,7 +158,10 @@ function addDict() {
</div>
<div class="grid grid-cols-6 gap-4 mt-4">
<div class="book" v-for="item in store.word.bookList" @click="goDictDetail2(item)">
<span>{{ item.name }}</span>
<div>
<div>{{ item.name }}</div>
<div>{{ item.description }}</div>
</div>
<div class="absolute bottom-4 right-4">{{ item.words.length }}个词</div>
</div>
<div class="book" @click="dictListRef.startSearch()">

View File

@@ -46,6 +46,7 @@ export type Word = {
export function getDefaultWord(val: Partial<Word> = {}): Word {
return {
custom: false,
"word": "",
"phonetic0": "",
"phonetic1": "",