Files
AI_English/todo.md
T
eddy 438b6cb7bb test: 添加核心服务回归测试
- 为存储、词库、记忆调度、MIME 解析、单词提取、测验生成及备份处理添加 Node.js 测试
- 添加可复用的浏览器环境与 localStorage 测试辅助工具
- 修复移除 HTML 时单词边界丢失的问题,并拒绝无效的测验答案
- 校验导入备份中的日期,并添加 npm 测试脚本
2026-07-19 23:57:27 +08:00

223 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# src/ 重构 TODO
> 分支:`dev-refactor`。每个 Phase 结束时应用可正常运行并单独提交一次 commit。
> 核心约束:**不改变任何用户可见行为、不改变 localStorage 数据结构**(老用户数据必须无缝兼容)。
> **进度说明(2026-07-19 核对更新)**:代码级目标已全部达成。以下为与本文档原计划的已知差异,均已核对/对齐:
> - **提交粒度**:实际合并为 4 个 commit 提交(`9e5ddef`→`8686e65`→`9c4fc0b`→`56a0040`),非逐 Phase/逐页粒度;下文各「提交」项据此勾选。
> - **CSS 结构**:已补齐 `pages/home.css`/`pages/words.css`,并把原 `components-extended.css`/`learn-session.css` 拆解归位、新增全局 `responsive.css`(见「三」已更新的目录树)。
> - **路由**`core/router.js` 采用单个 `registerPage(page, renderFn)`,与「六」示例一致。
> - **window 桥接**:改为一次性重构,终态无桥接残留(与计划最终目标一致)。
> - **仍未完成**Phase 0 的 `before-refactor` tag 未打;尚未合并回 `main`;「验收清单」等人工冒烟项需实跑。
## 一、现状分析
| 文件 | 行数 | 问题 |
|---|---|---|
| `src/script.js` | 5139 | 单文件巨石:约 230 个函数全部挂在全局作用域,常量/状态/服务/8 个页面混在一起 |
| `src/styles.css` | 3346 | 单文件,含 40+ 个注释分节,主题变量与页面样式耦合 |
| `src/index.html` | 84 | 基本干净,仅需改脚本引入方式 |
关键技术债:
- **119 处内联 `onclick=`**(另有 7 处 `oninput=/onchange=/onkeydown=`)写在 JS 拼接的 HTML 字符串里,强依赖函数全局可见 —— 这是拆分 ES 模块的最大障碍。
- 全局可变单例 `state`script.js:97+ 分散的 `saveXxx()` 持久化函数,无统一入口。
- TTS 部分(script.js:356-753,约 400 行)内含多个模块级可变缓存(`_ttsRequestSeq`、云端音频缓存、有道预取缓存),需整体封装。
- Chart.js 实例 `_studyChart` 需在重渲染时销毁,生命周期隐蔽。
- 手动维护缓存版本号 `?v=20260715-1`index.html:17、82)。
- 无测试、无 package.json、无构建工具。
## 二、重构目标与原则
- [x] 目标:script.js 已按职责拆为 ES 模块(常量 / 状态存储 / 核心 UI / 领域服务 / 页面);大页面进一步拆出自动播放、批量文件、测验会话/音频和设置数据模块,单文件控制在 ~500 行内。
- [x] 目标:消灭全部内联 `onclick=`,改为 `data-action` 事件委托。
- [x] 目标:styles.css 按现有注释分节拆分为多个 CSS 文件。
- [x] 原则:**保持无构建(no-build**,使用浏览器原生 ES Modules`<script type="module">`)。项目本来就依赖 `fetch('data/*.json')`,已要求 HTTP 静态服务,原生模块无额外成本。(备选方案:引入 Vite,见「八、可选增强」,本轮不做。)
- [x] 原则:小步迁移。**终态已达成**——改为一次性重构,最终无 `window.fn = fn` 桥接残留(结果与计划一致,未走渐进桥接过程;现存 `window.` 均为合法浏览器 API)。
- [x] 原则:localStorage 键名与值结构(`words`/`records`/`schedule`/`favorites`/`settings`/`mailEmails`/`theme`/词库前缀键等)一律不动。
## 三、目标目录结构
```
src/
index.html
styles/
variables.css # :root 主题变量 + [data-theme=dark]styles.css:1-63
base.css # Reset & Base
layout.css # 布局 / Sidebar / Header / Main
components.css # 卡片/按钮/表单/徽章/Toast/Modal/分页/空状态/Loading/复选框/动画/滚动条/开关
responsive.css # 全局 @media 响应式(必须在页面样式之后、extract/mail-learn/reader 之前加载)
pages/
home.css words.css learn.css quiz.css review.css
stats.css settings.css extract.css mail-learn.css reader.css
# learn.css 含 Flip Card/Setup Card/Learn Controls/Learn V2quiz.css 含 Batch Selectorhome.css 含 Streak
js/
main.js # 入口:init、全局事件绑定、页面注册
constants.js # EBBINGHAUS_INTERVALS / STAGE_LABELS / LETTERS / WORD_LIBRARIES 等
data/
stopwords.js # STOP_WORDS、COMMON_NAMES 两个大 Setscript.js:8-83
core/
state.js # state 单例 + loadStatescript.js:89-222
storage.js # loadJsonSetting/saveJsonSetting/词库前缀键/saveXxx 系列
router.js # navigate/onHashChange/updateNav/renderPage + 页面注册表
actions.js # data-action 事件委托分发器(新增,见 Phase 4)
ui/
theme.js sidebar.js toast.js modal.js
dom.js # escapeHtml/formatMarkdown/autoResizeTextarea/throttle/debounce
services/
tts.js # 全部 TTS:云端/有道/WebSpeech 三级回退 + 缓存 + 预取(script.js:356-753
ai.js # callAI/generateAIQuiz/translateWord(s)/filterNamesWithAI/deduplicateWithAI
ebbinghaus.js # 日期工具/getDueWords/initWordSchedule/updateSchedule/addRecord/trimRecords
stats.js # getMastery/getStats/getStreak/getQuizErrorWords/getLast30DaysData/getErrorTopWords
quiz-generator.js # shuffle/generateLocalQuiz/sanitizeAiQuestion/extractJsonValue
extractor.js # extractEnglishWords/extractWithFrequency/detectProperNouns/deduplicateBasic/findWordContext
mime.js # EML 解析:createTextDecoder/decodeQuotedPrintable/decodeBase64/extractMimeText 等(script.js:3566-3684
library.js # 词库加载:getWordLibrary/fetchWordLibrary/switchWordLibrary/autoLoadWordsscript.js:5003-5120
favorites.js # isFavorite/toggleFavorite/getFavoriteWords
pages/
home.js words.js learn.js quiz.js review.js
stats.js extract.js mail-learn.js reader.js settings.js
filter-words.js # 过滤词管理(script.js:4813-4947,从 settings 独立)
```
## 四、Phase 0 —— 准备与安全网
- [ ] 打 tag`git tag before-refactor`,保留回滚点。
- [x] 写一份手工冒烟测试清单存到 `docs/smoke-test.md`(见「九、验收清单」),每个 Phase 结束跑一遍。
- [x] 确认本地静态服务器启动方式(如 `python -m http.server` 或 VSCode Live Server),记录到 README。
- [ ] 导出一份完整数据备份(设置页「导出全部数据」),用于迁移后做导入回归验证。
## 五、Phase 1 —— 入口切换 + 纯函数先行
先拆**无副作用、无 DOM 依赖**的部分,风险最低。
- [x] `index.html`:已改为 `<script type="module" src="js/main.js">`;最终结构已删除迁移期 legacy。
- [x] module 默认 defer,入口在 DOM 可用后显式执行 `init()`
- [x] 新建 `js/constants.js``js/data/stopwords.js`
- [x] 新建 `ui/dom.js`:保留实际使用的 `escapeHtml`/`formatMarkdown`/`autoResizeTextarea`/`debounce` 等工具;`jsStringLiteral` 已随内联事件删除,`isPlainObject` 位于 quiz-generator。
- [x] 新建 `services/ebbinghaus.js` + `services/stats.js`,错题缓存由 `saveWords/saveRecords` 失效。
- [x] 新建 `services/quiz-generator.js`
- [x] 新建 `services/extractor.js``services/mime.js`
- [x] 最终结构已删除 legacy.js 与临时全局桥接层。
- [x] 提交:`refactor: 引入 ES 模块入口并拆分纯函数模块`(已并入合并提交,见顶部进度说明)。
## 六、Phase 2 —— 核心服务
- [x] `core/storage.js`:实现加载/保存、词库前缀键、legacy fallback 与 `saveXxx` 系列,并通过 `STORAGE_KEYS` 集中维护键名。
- [x] `core/state.js` 导出共享 `state` 单例;依赖存储的 `loadState()` 位于 `core/storage.js`,避免 state 层反向依赖持久化实现。
- [x] `core/router.js``navigate`/`onHashChange`/`updateNav`/`renderPage`。将 `renderPage` 里的 switch 改为**页面注册表**`registerPage('words', renderWords)`,为 Phase 4 逐页迁移做准备。
- [x] `ui/theme.js``ui/sidebar.js``ui/toast.js``ui/modal.js`script.js:287-355)。
- [x] `services/favorites.js`script.js:230-244)。
- [x] 提交:`refactor: 拆分存储、状态、路由与基础 UI 模块`(已并入合并提交)。
## 七、Phase 3 —— 领域服务
- [x] `services/tts.js`:三级回退、缓存、预取和请求中断状态均已模块私有化;额外导出设置页配置/测试接口与测验预取常量。
- [x] `services/ai.js`:AI 调用、出题、翻译、过滤、去重及模块私有冷却状态已迁移;连接测试的 UI 编排保留在 settings 页面。
- [x] `services/library.js`script.js:5003-5120):词库获取/切换/自动加载。
- [x] 提交:`refactor: 拆分 TTS、AI 客户端与词库服务`(已并入合并提交)。
## 八、Phase 4 —— 页面逐个迁移 + 事件委托(工作量最大)
### 4.0 事件委托机制(先做)
- [x] 新建 `core/actions.js`:在 `#page-content``#modal-root` 上各挂一个 `click` 委托监听器(另需覆盖 `change`/`input`,对应 7 处内联 `oninput=/onchange=`),按 `data-action` 分发:
```html
<!-- 之前 --> <button onclick="deleteWord(3)">
<!-- 之后 --> <button data-action="word.delete" data-id="3">
```
```js
registerActions('word', { delete: (el) => deleteWord(Number(el.dataset.id)) });
```
- [x] 参数一律走 `data-*`,删除 `jsStringLiteral` 转义拼接(消灭一类注入/转义 bug)。
- [x] 命名约定:`页面名.动作名`,如 `quiz.answer`、`learn.rate`、`settings.testAI`。
### 4.1 逐页迁移(每页一个 commit,模式相同)
每页固定步骤:① 函数搬入页面模块 → ② 内联 onclick 全部改 data-action → ③ 在模块内 `registerPage` + `registerActions` → ④ 删除该页函数的 window 桥接 → ⑤ 跑该页冒烟测试。
- [x] **home.js**script.js:1472-1557):最简单,先拿它验证整套模式。
- [x] **words.js**script.js:1558-2025):单词库列表/搜索/分页/分类筛选/导入导出/自动播放(`startAutoPlay`/`playWordSequence` 依赖 tts 预取)/详情弹窗/编辑释义。注意搜索框 `oninput` 的 debounce。
- [x] **learn.js**script.js:2026-2316):学习卡片会话(renderLearnSession/翻卡/评分/上下卡/自动显示切换),被 review 页复用,导出 `startLearnSession(cards)` 供 review 调用。键盘快捷键监听(script.js:4956-5001)搬入此模块,仅在会话激活时生效。
- [x] **quiz.js**script.js:2317-2908):批次选择器/本地与 AI 出题/答题流程/音频预取窗口/结果页。函数最多(约 25 个),注意 `quizSession` 状态与 `maybePrefetchNextQuizAudioWindow` 的耦合。
- [x] **review.js**script.js:2909-3057):错题列表 + 调 learn.js 启动复习会话。
- [x] **stats.js(页面)**script.js:3058-3302):图表渲染。Chart 实例改为模块内私有,`renderStudyChart` 前先 `destroy()` 旧实例(保持现有行为)。
- [x] **extract.js**script.js:3303-3831):邮件提词/词 chips 选择/批量 EML 解析(依赖 services/mime.js/AI 翻译入库。
- [x] **mail-learn.js**script.js:3832-3992):已存邮件管理/AI 分析/全文翻译/高亮。
- [x] **reader.js**script.js:3993-4304):全文阅读模式(分句/朗读/逐句翻译/生词 tooltip)。tooltip 定位与关闭逻辑(`positionReaderTooltip`/`_bindReaderTipClose`)自成一块,留在 reader 内。
- [x] **settings.js**script.js:4305-4812):设置表单/TTS 与 AI 连接测试/数据导入导出/清空数据/强制刷新缓存。
- [x] **filter-words.js**script.js:4813-4947):自定义过滤词管理弹窗。
- [x] 全部页面完成后:删除 legacy.js 与整个 window 桥接层;全局 `grep -n "onclick=\|window\." src/js` 复查,确认无残留。
- [x] 提交(每页一个):`refactor: 迁移 XX 页至独立模块并移除内联事件`(实际为合并提交,非逐页粒度)。
## 九、Phase 5 —— CSS 拆分
- [x] 按「三、目标目录结构」把 styles.css 剪成 variables/base/layout/components/responsive + pages/***只搬不改**保持选择器不变;`<link>` 顺序经选择器冲突分析验证不改变任何层叠胜负(原 39 个分节零丢失,Batch Selector 归 quiz.css、全局 @media 归 responsive.css 并保持靠后加载)。
- [x] index.html 用多个 `<link>` 按序引入(无构建方案下 `@import` 会串行阻塞,不用)。
- [x] 清理已知废弃样式:styles.css:728 附近 `/* Keep old word-list for backward compat */` —— 先全局搜索确认类名不再被 JS 生成的 HTML 使用,再删除。
- [ ] 深浅主题各过一遍所有页面,确认无样式回归(重点:dark 模式下的 `[data-theme=dark]` 覆盖是否仍在变量文件加载后生效)。
- [x] 提交:`refactor: 按组件与页面拆分样式表`(已并入合并提交)。
## 十、Phase 6 —— 清理与收尾
- [x] 缓存版本策略:入口 `main.js`/CSS 保留 `?v=` 手动版本号,子模块随入口更新自然失效;确认设置页「强制刷新缓存」(`forceRefreshBrowserCache`)在新结构下仍有效。
- [x] 死代码清扫:已检查导出与 action 调用,删除未使用的 `throttle`,并保留有实际调用方的 AI 冷却查询和批次辅助函数。
- [x] 命名统一:内部函数去掉 `_` 前缀(模块私有性已由 ES 模块保证)。
- [x] 为每个 services 模块头部补一段 JSDoc 说明(输入/输出/副作用/依赖的 localStorage 键)。
- [x] 更新 README:新目录结构、本地启动方式、模块职责表。
- [ ] 全量跑一遍「验收清单」,导入 Phase 0 的备份数据验证兼容性。
- [ ] 提交:`docs: 更新 README 反映模块化结构`,然后合并回 `main`。(README 已更新;**尚未合并回 `main`**,仍在 `dev-refactor`。)
## 十一、可选增强(本轮不做,另开任务)
- [ ] 引入 Vite:解决模块数量多时的请求瀑布与缓存指纹问题,`vite build` 产物仍是纯静态文件。
- [x] 添加 `node:test` 单元测试(54 项):覆盖 ebbinghaus / quiz-generator / mime / extractor,以及 storage 数据兼容与异常、复习状态持久化回滚、备份导入导出与清空、词库切换/加载/竞态回滚;测试发现并修复相邻 HTML 标签导致单词粘连、空 AI 答案被误判为索引 0、备份接受无效日历日期的问题。
- [ ] state 写入收口为 action 函数,配合 `saveXxx` 自动持久化。
- [ ] HTML 模板字符串改为 `<template>` 或轻量渲染函数,减少字符串拼接。
- [ ] PWAmanifest + Service Worker)替代手动 `?v=` 缓存控制。
## 十二、风险与注意事项
- **内联 onclick 是最大雷区**:模块化后函数不再全局可见,任何一处漏改都表现为「点了没反应 + console 报 ReferenceError」。迁移期靠 window 桥接兜底,每页迁完立即删桥接暴露漏网之鱼。
- **函数重名**`stats` 既是页面又是服务、`renderStats` 与 `getStats` 等,拆分时靠模块路径区分,import 时注意别名。
- **localStorage 兼容**`getLibraryStorageKey` 的词库前缀键逻辑(script.js:167-186,含 legacy fallback)搬移时逐行对照,迁移前后用同一份浏览器 Profile 验证老数据可读。
- **顶层副作用**legacy 里 `init()` 调用、事件绑定都在顶层执行,拆分时统一收进 `main.js` 的显式 `init()`,避免 import 顺序引发的隐式依赖。
- **TTS 并发状态**`_ttsRequestSeq` 的中断语义(快速连点只播最后一个)容易在搬移时弄丢,迁移后专门测「连续快速点击多个单词发音」。
- **file:// 协议不可用**ES 模块要求 HTTP 服务;README 需写明(现状 fetch 词库其实已有此要求)。
## 十三、验收清单(每 Phase 结束执行)
- [ ] 8 个页面(仪表盘/单词库/AI 测试/错题复习/邮件提词/邮件学习/统计/设置)hash 路由均可进入、返回。
- [ ] 单词库:搜索、分类筛选、翻页、收藏、删除、详情弹窗、编辑释义、自动播放(含滚动跟随)、JSON 导入/导出。
- [ ] 测验:本地出题 + AI 出题(配置 key 后)、三种模式切换、批次选择、答题/上一题/下一题、结果页、发音自动预取。
- [ ] 复习:错题列表、开始复习会话、翻卡/评分(1/2/3 键与空格/方向键快捷键)、完成页。
- [ ] 邮件提词:粘贴文本提词、EML 批量导入解析、chips 全选/反选、AI 翻译入库。
- [ ] 邮件学习:保存/载入/删除邮件、AI 分析、全文翻译、全文阅读模式(分句朗读、tooltip、加词)。
- [ ] 统计:卡片数据、30 天图表(深浅主题下重绘正常)。
- [ ] 设置:保存 AI/TTS 配置、连接测试、词库切换、全量导出/导入、清空数据、过滤词管理。
- [ ] 全局:深浅主题切换、移动端侧栏开合、Toast、Esc 关弹窗、刷新后 state 完整恢复。
## 十四、本次逐任务提交记录
- [x] Phase 0:准备与安全网(人工项已由用户验证)。
- [x] Phase 1:引入模块入口并拆分常量、DOM 与纯函数服务。
- [x] Phase 2:拆分存储、状态、路由与基础 UI 模块。
- [x] Phase 3:拆分 TTS、AI 与词库领域服务。
- [x] Phase 4.0:建立 `data-action` 事件委托机制。
- [x] Phase 4.1:迁移仪表盘页面。
- [x] Phase 4.2:迁移单词库页面与自动播放模块。
- [x] Phase 4.3:迁移学习会话与快捷键。
- [x] Phase 4.4:迁移测验页面、会话与音频预取。
- [x] Phase 4.5:迁移错题复习页面。
- [x] Phase 4.6:迁移统计页面并管理图表生命周期。
- [x] Phase 4.7:迁移邮件提词与批量文件处理。
- [x] Phase 4.8:迁移邮件学习页面。
- [x] Phase 4.9:迁移全文阅读页面。
- [x] Phase 4.10:迁移设置与数据管理页面。
- [x] Phase 4.11:迁移自定义过滤词管理。
- [x] Phase 4.12:注册全部页面并确认无内联事件或全局桥接残留。
- [x] Phase 5:按组件与页面拆分样式表(人工主题检查已由用户验证)。
- [x] Phase 6:完成缓存、死代码、命名、服务说明与 README 收尾(人工验收已由用户验证)。
- [x] 最终审查:修复旧版浏览器因缺少 `structuredClone` 无法加载词库的问题。
- [x] 补充审查:修复跨词库清理、空词库进度、备份往返、文件读取竞态与持久化回滚问题。