From 63ca3ba29f0d412c3e46337334e056411500aff4 Mon Sep 17 00:00:00 2001 From: Zyronon Date: Fri, 5 Dec 2025 01:06:55 +0800 Subject: [PATCH 1/4] wip --- components.d.ts | 1 + docker-compose.yml | 2 +- src/config/env.ts | 4 +++- src/hooks/export.ts | 4 ++-- src/pages/article/BatchEditArticlePage.vue | 6 +++--- src/pages/setting/Setting.vue | 4 ++-- src/pages/word/DictDetail.vue | 4 ++-- 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/components.d.ts b/components.d.ts index 0172f6b1..4e0eeb0c 100644 --- a/components.d.ts +++ b/components.d.ts @@ -99,6 +99,7 @@ declare module 'vue' { IconFluentSlideTextTitleEdit20Regular: typeof import('~icons/fluent/slide-text-title-edit20-regular')['default'] IconFluentSparkle20Regular: typeof import('~icons/fluent/sparkle20-regular')['default'] IconFluentSpeakerEdit20Regular: typeof import('~icons/fluent/speaker-edit20-regular')['default'] + IconFluentSpeakerSettings20Regular: typeof import('~icons/fluent/speaker-settings20-regular')['default'] IconFluentStar16Filled: typeof import('~icons/fluent/star16-filled')['default'] IconFluentStar16Regular: typeof import('~icons/fluent/star16-regular')['default'] IconFluentStar20Filled: typeof import('~icons/fluent/star20-filled')['default'] diff --git a/docker-compose.yml b/docker-compose.yml index adb90cde..20151700 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "2" services: typeword: image: "node:latest" - #environment: #按需配置,主要为了科学上网解决依赖安装网络问题 + #environment: #按需配置, 主要为了科学上网解决依赖安装网络问题 # - HTTP_PROXY=http://127.0.0.1:80 # HTTPS_PROXY=http://127.0.0.1:80 working_dir: /home/node/app diff --git a/src/config/env.ts b/src/config/env.ts index 4c74899a..5ed48642 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -94,5 +94,7 @@ export const LIB_JS_URL = { SHEPHERD: import.meta.env.MODE === 'development' ? 'https://cdn.jsdelivr.net/npm/shepherd.js@14.5.1/dist/esm/shepherd.mjs' : Origin + '/libs/Shepherd.14.5.1.mjs', - SNAPDOM: `${Origin}/libs/snapdom.min.js` + SNAPDOM: `${Origin}/libs/snapdom.min.js`, + JSZIP: `${Origin}/libs/jszip.min.js`, + XLSX: `${Origin}/libs/xlsx.full.min.js`, } \ No newline at end of file diff --git a/src/hooks/export.ts b/src/hooks/export.ts index a681ceef..025aa94a 100644 --- a/src/hooks/export.ts +++ b/src/hooks/export.ts @@ -2,7 +2,7 @@ import {loadJsLib, shakeCommonDict} from "@/utils"; import { APP_NAME, APP_VERSION, - EXPORT_DATA_KEY, + EXPORT_DATA_KEY, LIB_JS_URL, LOCAL_FILE_KEY, Origin, PracticeSaveArticleKey, @@ -28,7 +28,7 @@ export function useExport() { if (loading.value) return loading.value = true try { - const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`); + const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP); let data = { version: EXPORT_DATA_KEY.version, val: { diff --git a/src/pages/article/BatchEditArticlePage.vue b/src/pages/article/BatchEditArticlePage.vue index ae7fef20..d4310a18 100644 --- a/src/pages/article/BatchEditArticlePage.vue +++ b/src/pages/article/BatchEditArticlePage.vue @@ -15,7 +15,7 @@ import {getDefaultArticle} from "@/types/func.ts"; import BackIcon from "@/components/BackIcon.vue"; import MiniDialog from "@/components/dialog/MiniDialog.vue"; import {onMounted} from "vue"; -import {Origin} from "@/config/env.ts"; +import { LIB_JS_URL, Origin } from "@/config/env.ts"; import {syncBookInMyStudyList} from "@/hooks/article.ts"; const base = useBaseStore() @@ -132,7 +132,7 @@ function importData(e: any) { let reader = new FileReader(); reader.onload = async function (s) { importLoading = true - const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`); + const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX); let data = s.target.result; let workbook = XLSX.read(data, {type: 'binary'}); let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']); @@ -198,7 +198,7 @@ function importData(e: any) { async function exportData(val: { type: string, data?: Article }) { exportLoading = true - const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`); + const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX); const {type, data} = val let list = [] let filename = '' diff --git a/src/pages/setting/Setting.vue b/src/pages/setting/Setting.vue index 6fe5a3c7..42246659 100644 --- a/src/pages/setting/Setting.vue +++ b/src/pages/setting/Setting.vue @@ -18,7 +18,7 @@ import {useBaseStore} from "@/stores/base.ts"; import {saveAs} from "file-saver"; import { APP_NAME, APP_VERSION, EMAIL, - EXPORT_DATA_KEY, GITHUB, Host, + EXPORT_DATA_KEY, GITHUB, Host, LIB_JS_URL, LOCAL_FILE_KEY, Origin, PracticeSaveArticleKey, @@ -253,7 +253,7 @@ async function importData(e) { reader.readAsText(file); } else if (file.name.endsWith(".zip")) { try { - const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`); + const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP); const zip = await JSZip.loadAsync(file); const dataFile = zip.file("data.json"); diff --git a/src/pages/word/DictDetail.vue b/src/pages/word/DictDetail.vue index 26253661..5c5aa40c 100644 --- a/src/pages/word/DictDetail.vue +++ b/src/pages/word/DictDetail.vue @@ -278,7 +278,7 @@ function importData(e) { reader.onload = async function (s) { let data = s.target.result; importLoading = true - const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`); + const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX); let workbook = XLSX.read(data, {type: 'binary'}); let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']); if (res.length) { @@ -357,7 +357,7 @@ function importData(e) { async function exportData() { exportLoading = true - const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`); + const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX); let list = runtimeStore.editDict.words let filename = runtimeStore.editDict.name let wb = XLSX.utils.book_new() From 7f05ec95d9c2547ce14d89576ba7044e2a7b0bea Mon Sep 17 00:00:00 2001 From: Zyronon Date: Fri, 5 Dec 2025 18:52:02 +0800 Subject: [PATCH 2/4] bug: Ctrl + C copy function not working --- ... 中忽略人名(基于 editArticle.nameList).md | 71 ------ ...管理弹框(读取与保存 editArticle.nameList).md | 69 ----- .trae/documents/静态首页移动端适配.md | 109 -------- src/hooks/event.ts | 23 +- src/pages/article/ArticlesPage.vue | 2 +- .../article/components/TypingArticle.vue | 2 +- src/pages/setting/Log.vue | 237 ++++++++++++++++++ src/pages/setting/Setting.vue | 223 +--------------- src/pages/word/DictList.vue | 2 +- src/pages/word/PracticeWords.vue | 20 +- src/pages/word/components/Footer.vue | 11 +- src/pages/word/components/TypeWord.vue | 38 +-- src/stores/base.ts | 9 - 13 files changed, 301 insertions(+), 515 deletions(-) delete mode 100644 .trae/documents/在 TypingArticle.vue 中忽略人名(基于 editArticle.nameList).md delete mode 100644 .trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md delete mode 100644 .trae/documents/静态首页移动端适配.md create mode 100644 src/pages/setting/Log.vue diff --git a/.trae/documents/在 TypingArticle.vue 中忽略人名(基于 editArticle.nameList).md b/.trae/documents/在 TypingArticle.vue 中忽略人名(基于 editArticle.nameList).md deleted file mode 100644 index 7454be32..00000000 --- a/.trae/documents/在 TypingArticle.vue 中忽略人名(基于 editArticle.nameList).md +++ /dev/null @@ -1,71 +0,0 @@ -## 目标 -- 在 `onTyping` 方法(291-382行)中判断当前/下一个单词是否为人名,并在练习时自动忽略(不要求输入、不提示错误、不停顿等待空格)。 - -## 数据来源 -- 使用 `props.article.nameList: string[]`(来自编辑页保存),作为要忽略的人名列表。 - -## 匹配策略 -- 构建一个人名集合 `nameSet`: - - `trim()` 后的字符串; - - 若开启 `ignoreCase` 则统一转小写匹配。 -- 判定函数 `isNameWord(word: ArticleWord)`: - - 仅当 `word.type === PracticeArticleWordType.Word` 时参与匹配; - - 对 `word.word` 进行同样的规范化后 `nameSet.has(...)`。 - -## 处理时机与行为 -- 在 `onTyping` 开始处、拿到 `currentWord` 后: - - 若是“人名”,则直接跳过本词;若该词 `nextSpace` 为真,连带空格也跳过(避免进入 `isSpace` 状态)。 - - 跳过后继续处理当前按键:复用已有模式(如 `isSpace` 分支里)调用 `next()` 和递归 `onTyping(e)`。 -- 在 `next()` 内也追加“人名跳过”逻辑(与已有忽略符号/数字类似),保证连续多个需要忽略的词可以被连续跳过: - - 当 `currentWord` 是人名: - - 若 `currentWord.nextSpace` 为真:`isSpace = false`; - - 递归调用 `next()` 继续到下一个词; - - 否则正常 `emit('nextWord', currentWord)`。 - -## 代码改动点 -- 在组件顶部或方法内构建 `nameSet`(建议用 `$computed`): -```ts -const nameSet = $computed(() => { - const list = props.article?.nameList ?? [] - return new Set(list.map(s => (settingStore.ignoreCase ? s.toLowerCase() : s).trim()).filter(Boolean)) -}) -function isNameWord(w: ArticleWord) { - if (w.type !== PracticeArticleWordType.Word) return false - const token = (settingStore.ignoreCase ? w.word.toLowerCase() : w.word).trim() - return nameSet.has(token) -} -``` -- 在 `onTyping` 里,`let currentWord = currentSentence.words[wordIndex]` 之后: -```ts -if (isNameWord(currentWord)) { - // 跳过当前人名,连带空格 - isSpace = false - const savedTypingFlag = isTyping - next() - isTyping = false - return onTyping(e) -} -``` -- 在 `next()` 内,设置 `currentWord` 后、`emit('nextWord', currentWord)` 之前: -```ts -if (isNameWord(currentWord)) { - // 人名与后续空格都跳过 - isSpace = false - return next() -} -``` - -## 注意事项 -- 保持与现有忽略规则一致(符号/数字已通过 `ignoreSymbol` 处理),人名跳过逻辑与其同层级。 -- 忽略人名时不触发 `wrong` 或提示音,不进入 `isSpace` 等待。 -- 若连续出现多个需要忽略的词(如人名+标点+人名),递归 `next()` 将逐个跳过。 - -## 验证 -- 在包含人名的文本中练习: - - 人名处不需要输入,光标自动跳到下一非忽略词; - - 不会卡在空格等待; - - 大小写忽略效果符合 `settingStore.ignoreCase` 设置。 - -## 交付 -- 按上述方案在 `TypingArticle.vue` 中实现辅助函数与两处调用点的改动; -- 仅修改该文件,不影响其他页面。 \ No newline at end of file diff --git a/.trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md b/.trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md deleted file mode 100644 index a0d1de22..00000000 --- a/.trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md +++ /dev/null @@ -1,69 +0,0 @@ -## 目标 -- 在“人物名称管理”弹框中使用临时变量编辑名称列表,只在点击“确定”时写回 `editArticle.nameList: string[]` - -## 数据结构 -- `editArticle.nameList: string[]` -- 临时变量:`let nameListRef = $ref([])` - -## 生命周期 -- 弹框打开时初始化:`nameListRef = cloneDeep(editArticle.nameList || [])` -- 弹框关闭时不写回:丢弃修改 - -## 交互设计 -- 弹框 `v-model="showNameDialog"`、`:footer="true"`、`@close="showNameDialog = false"`、`@ok="saveNameList"` -- 按钮: - - “添加名称” → `nameListRef.push('')` - - 每行名称使用 `BaseInput v-model="nameListRef[i]"` - - “删除”名称 → `nameListRef.splice(i,1)` - -## 保存逻辑 -- `saveNameList()`: - - 清理:`trim()` + `filter(Boolean)` - - 写回:`editArticle.nameList = cleaned` - - 关闭弹框:`showNameDialog = false` - -## 实现细节(EditArticle.vue 增加) -- 脚本: -```ts -let showNameDialog = $ref(false) -let nameListRef = $ref([]) - -watch(() => showNameDialog, (v) => { - if (v) nameListRef = cloneDeep(Array.isArray(editArticle.nameList) ? editArticle.nameList : []) -}) - -function addName() { nameListRef.push('') } -function removeName(i: number) { nameListRef.splice(i,1) } -function saveNameList() { - const cleaned = nameListRef.map(s => (s ?? '').trim()).filter(Boolean) - editArticle.nameList = cleaned -} -``` -- 模板(620-628 区域): -```vue - -
-
-
配置需要忽略的人名,练习时自动忽略这些名称
- 添加名称 -
- -
-
- - 删除 -
-
-
-
-``` - -## 验证 -- 打开弹框 → 编辑临时列表 → 点击“确定”后检查 `editArticle.nameList` 是否更新;点击关闭则不更新 - -## 注意 -- 若类型仍为旧版 `string[][]`,请同步调整为 `string[]` 以与当前实现一致 \ No newline at end of file diff --git a/.trae/documents/静态首页移动端适配.md b/.trae/documents/静态首页移动端适配.md deleted file mode 100644 index 684a8fca..00000000 --- a/.trae/documents/静态首页移动端适配.md +++ /dev/null @@ -1,109 +0,0 @@ -## 目标与范围 - -* 适配 `public/static-home.html` 在 768px 以下与 480px 以下的移动端展示与触控体验 - -* 不改变页面文案与结构,只进行样式与布局响应式改造 - -## 结构与布局 - -* 将固定宽度 `.w { width: 60vw; }` 改为容器 `.container { width: min(1200px, 92%); }` 并在模板中替换为 `.container` - -* `.card-wrap` 改为自适应栅格:`grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))` - -* 移动端(<=768px)卡片单列展示;间距适当增大,避免拥挤 - -## 样式改造 - -* 标题缩放: - - * `@media (max-width: 768px) h1 { font-size: 3rem; }` - - * `@media (max-width: 480px) h1 { font-size: 2.4rem; }` - -* 主按钮组: - - * 移动端按钮全宽:`.base-button { width: 100%; margin: .5rem 0; height: 2.8rem; font-size: 1rem; }` - - * 悬浮降低透明度改为轻微位移:`transform: translateY(-1px);` - -* 内容区与间距: - - * `@media (max-width: 768px) .content { margin-top: 4rem; gap: 1.4rem; }` - - * `@media (max-width: 480px) .content { margin-top: 3.2rem; gap: 1.2rem; }` - -* 赞助区 `.sky`: - - * 图片强制全宽:`.sky a { width: 100% !important; } .sky-img { width: 100%; }` - - * 移动端上下内边距缩小,减少跳动 - -* 卡片 `.card`: - - * 移动端去掉固定 `width: 25%`,改为自适应栅格控制 - - * 增加触控阴影:`box-shadow: 0 6px 20px rgba(0,0,0,.08);` - -* 底部链接 `.bottom`: - - * 在 <=768px 改为纵向堆叠:`flex-direction: column; align-items: flex-start; gap: .6rem;` - -## 示例变更片段(将添加到 \ No newline at end of file diff --git a/src/pages/setting/Setting.vue b/src/pages/setting/Setting.vue index 42246659..60fd8798 100644 --- a/src/pages/setting/Setting.vue +++ b/src/pages/setting/Setting.vue @@ -41,6 +41,7 @@ import {useRuntimeStore} from "@/stores/runtime.ts"; import {useUserStore} from "@/stores/user.ts"; import {useExport} from "@/hooks/export.ts"; import MigrateDialog from "@/components/MigrateDialog.vue"; +import Log from "@/pages/setting/Log.vue"; const emit = defineEmits<{ toggleDisabledDialogEscKey: [val: boolean] @@ -231,7 +232,7 @@ function importJson(str: string, notice: boolean = true) { notice && Toast.success('导入成功!') } catch (err) { return Toast.error('导入失败!') - }finally { + } finally { importLoading = false } } @@ -397,220 +398,7 @@ function transferOk() { -
-
-
-
-
日期:2025/12/3
-
内容:单词、文章设置修改为弹框,更方便
-
-
-
-
-
-
-
日期:2025/12/3
-
内容:录入新概念(三、四)部分音频,优化文章相关功能
-
-
-
-
-
-
-
日期:2025/12/2
-
内容:完成新概念(一)音频,优化文章管理页面
-
-
-
-
-
-
-
日期:2025/11/30
-
内容:文章里的单词可点击播放
-
-
-
-
-
-
-
日期:2025/11/29
-
内容:修改 Slider 组件显示bug,新增 IE 浏览器检测提示
-
-
-
-
-
-
-
日期:2025/11/28
- -
-
-
-
-
-
-
日期:2025/11/25
-
内容:文章练习新增人名忽略功能(新概念一已全部适配),上传了新概念(一)1-18 音频
-
-
-
-
-
-
-
日期:2025/11/23
-
内容:优化练习完成结算界面,新增分享功能
-
-
-
-
-
-
-
日期:2025/11/22
-
内容:适配移动端
-
-
-
-
-
-
-
日期:2025/11/16
-
内容:自测单词时,不认识单词可以直接输入,自动标识为错误单词,无需按2
-
-
-
-
-
-
-
日期:2025/11/15
-
内容:练习单词时,底部工具栏新增“跳到下一阶段”按钮
-
-
-
-
-
-
-
日期:2025/11/14
-
内容:新增文章练习时可跳过空格:如果在单词的最后一位上,不按空格直接输入下一个字母的话,自动跳下一个单词, - 按空格也自动跳下一个单词 -
-
-
-
-
-
-
-
日期:2025/11/13
-
内容:新增文章练习时“输入时忽略符号/数字”选项
-
-
-
-
-
-
-
日期:2025/11/6
-
内容:新增随机复习功能
-
-
-
-
-
-
-
日期:2025/10/30
-
内容:集成PWA基础配置,支持用户以类App形式打开项目
-
-
-
-
-
-
-
日期:2025/10/26
-
内容:进一步完善单词练习,解决复习数量太多的问题
-
-
-
    -
  1. -
    智能模式优化
    -
    练习时新增四种练习模式:学习、自测、听写、默写。
    -
  2. -
  3. -
    学习模式
    -
    -
      -
    • 仅在练习新词时出现。
    • -
    • 采用「跟写 / 拼写」方式进行学习。
    • -
    • 每 7 个单词会 强制进行听写,解决原来“一次练太多,听写时已忘记”的问题。
    • -
    -
    -
  4. -
  5. -
    自测模式(新增)
    -
    -
      -
    • 仅在复习已学单词时出现。
    • -
    • 不再强制拼写,提供「我认识」与「不认识」选项。
    • -
    • 选择「我认识」后,该单词在后续听写或默写中将不再出现,显著减少复习数量
    • -
    -
    -
  6. -
  7. -
    听写模式
    -
    原有逻辑保持不变。
    -
  8. -
  9. -
    默写模式(新增)
    -
    -
      -
    • 仅显示释义,不自动发音,不显示单词长度。
    • -
    • 适合强化拼写记忆的场景。
    • -
    -
    -
  10. -
- 说明: -
本次更新重点解决了“复习单词数量过多、效率偏低”的问题。
-
通过引入「复习」与「默写」两种模式,使复习流程更加灵活、高效。
-
-
-
-
-
-
-
日期:2025/10/8
-
内容:文章支持自动播放下一篇
-
-
-
-
-
-
-
日期:2025/9/14
-
内容:完善文章编辑、导入、导出等功能
-
-
-
1、文章的音频管理功能,目前已可添加音频、设置句子与音频的对应位置
-
2、文章可导入、导出
-
3、单词可导入、导出
-
-
-
-
-
-
-
日期:2025/8/10
-
内容:2.0版本发布,全新UI,全新逻辑,新增短语、例句、近义词等功能
-
-
-
-
-
-
-
日期:2025/7/19
-
内容:1.0版本发布
-
-
-
-
+

Type Words

@@ -640,11 +428,6 @@ function transferOk() {