Files
Tasks/todo.md
T
2026-08-01 02:11:12 +08:00

192 lines
14 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.
# 任务管理系统重构 TODO
> 分支:`dev-refactor` 目标:在**不改变任何现有功能与数据格式**的前提下,把单文件架构重构为模块化、可维护的结构。
> 原则:小步提交,每个阶段完成后跑一遍「回归测试清单」再进入下一阶段。
---
## 一、现状分析
| 文件 | 行数 | 问题概述 |
|------|------|----------|
| `src/index.html` | 311 | 内联 onclick、3 份重复的排序下拉、favicon 重复 4 次 |
| `src/script.js` | 1788 | 单文件巨石:状态、i18n、渲染、存储、导入导出全部混在一起,约 30 个全局函数 |
| `src/styles.css` | 432 | 颜色硬编码、选择器重复定义、存在疑似死代码 |
### 主要问题清单
1. **全局可变状态散落**`tasks``currentLang``sortOrders``showHiddenCompletedTasks``showHiddenTodoTasks``currentEditingTaskId``pendingImportData``idCounter` 共 8 个全局变量,任何函数都能直接改。
2. **内联事件处理**:HTML 和 JS 模板字符串里大量 `onclick="xxx()"`,迫使所有函数必须挂在全局作用域。
3. **成对重复代码**completed / todo 两套几乎相同的逻辑):
- `toggleShowHiddenCompletedTasks` / `toggleShowHiddenTodoTasks`script.js:1665 / 1728
- `updateShowHiddenCompletedTasksUI` / `updateShowHiddenTodoTasksUI`script.js:1682 / 1745
- `save/loadShowHiddenCompletedTasks` / `save/loadShowHiddenTodoTasks`script.js:1708 / 1771
- `renderTasks` 内部对 completed 和 todo 的隐藏任务处理两段几乎一样(script.js:630-667
4. **HTML 重复**:三列的排序 `<select>` 各 13 个 option 完全相同(index.html:72-86 / 99-113 / 132-146);favicon 的 data-URI 重复 4 次(index.html:9-12)。
5. **i18n 内嵌**:约 240 行翻译对象直接写在 script.js:162-403`{count}` 占位符靠手工 `.replace()`
6. **渲染方式**:整列 `innerHTML` 全量重绘,每次保存/通知都调 `renderAllTasks()`,导致时间轴展开状态丢失。
7. **职责重叠**`renderTasks``updateTaskCounts` 都在写计数 DOMscript.js:640-666 与 831-859),结果一致但逻辑双份。
8. **业务种子数据硬编码**`loadInitialTasks()`script.js:417-513)内嵌 5 条真实业务任务。
9. **排序逻辑重复**`priorityOrder` 映射定义了 3 次(script.js:562 / 605 / 608),number-asc/desc、priority-asc/desc 成对复制。
10. **无工程化设施**:无 ESLint / Prettier / 测试 / 构建脚本。
### 顺带发现的小 bug / 待改进点(重构中一并处理)
- [x] `exportData()` 失败时错用 `importError` 文案(script.js:1491)→ 应新增 `exportError` 翻译键
- [x] `exportData()` 中残留调试 `console.log`script.js:1453-1457)→ 删除
- [x] 时间轴区块只在 `timeline.length > 0` 时渲染(script.js:793),**没有任何时间轴条目的任务无法手动添加第一条**→ 补充入口
- [x] `sortTasks(tasks, ...)` 参数名遮蔽全局 `tasks`script.js:549)→ 重命名参数
- [x] `normalizeTask` 未校验 `language` 字段 → 补充白名单 `['zh','en']`
- [x] 每次重渲染后时间轴折叠状态丢失 → 渲染前记录展开的 taskId,渲染后恢复
- [x] CSS`.timeline-item` 重复定义(styles.css:87 与 276)→ 合并;确认 `.status-title`styles.css:270)、`.tasks-hidden-notice`styles.css:346)是否未使用,未使用则删除
- [x] `saveTask` 附近缩进混乱(script.js:907-1010 有多余空格)→ 统一格式化
---
## 二、目标目录结构
采用**原生 ES Modules,无构建工具**。
⚠️ 注意:ES Modules 无法通过 `file://` 直接打开,开发和使用需本地静态服务器(VSCode Live Server 或 `npx serve src`)。如果必须保留「双击 html 直接用」的能力,改用方案 B:保持多个普通 `<script>` 标签按依赖顺序加载(模块拆分方式不变,只是不用 import/export)。**开工前先确认选哪个方案。**
```
src/
├── index.html
├── css/
│ └── styles.css # 后续可再拆 base / components / responsive
└── js/
├── main.js # 入口:DOMContentLoaded 初始化、顶层事件绑定
├── config.js # 常量:状态列表、排序方式、优先级映射、localStorage 键名、导出版本号
├── utils.js # escapeHtml、formatLocalDate、ID 生成、日期解析
├── i18n/
│ ├── zh.js
│ ├── en.js
│ └── index.js # t(key, params) 翻译函数、applyTranslations()、toggleLanguage()
├── storage.js # localStorage 统一读写 + 容错(唯一接触 localStorage 的模块)
├── store.js # 应用状态(tasks、sortOrders、showHidden…)+ 受控的变更接口
├── task-model.js # normalizeTask / normalizeTasks / isValidId / 校验
├── sort.js # sortTasks 与各比较器
├── render.js # renderAllTasks / renderTasks / createTaskCard / createTimeline / 计数
├── modal.js # 任务编辑模态框:打开、填充、保存
├── timeline.js # 时间轴 CRUD + 系统条目
├── import-export.js # 导出、导入、validateImportData、合并去重
└── notify.js # showNotification / checkDueDates
```
---
## 三、重构步骤
### 阶段 0:准备(半天)
- [x] 确认模块方案:**A. ES Modules + 本地服务器**(推荐)或 B. 普通脚本多文件
- [x] 手工做一次完整功能走查,按「回归测试清单」录一遍基准行为(必要时截图)
- [ ] 导出一份当前 localStorage 数据(用现有导出功能生成 backup-tasks-*.json)作为兼容性测试样本
- [x] 添加 `.editorconfig`、ESLintflat config+ Prettier;先只做检查不大规模改格式
- [ ] git tag 一个重构前基线(如 `pre-refactor`
### 阶段 1:文件拆分(先搬家,不改逻辑)
> 本阶段只移动代码 + 加 import/export,函数体一行不改。每搬完一个模块提交一次。
- [x] 建立 `src/js/``src/css/` 目录,index.html 引用路径同步更新
- [x] `utils.js` ← script.js:8-69generateId、escapeHtml、parseDueDateEnd、getDueDateState、isValidId、generateUniqueId
- [x] `task-model.js` ← script.js:71-152normalizeTask、normalizeTasks、normalizeSortOrders、validStatuses、validSortOrders
- [x] `i18n/zh.js``i18n/en.js` ← script.js:162-403 的翻译对象
- [x] `storage.js` ← saveTasks/loadTasks516-538)、saveSortOrders/loadSortOrders1421-1441)、show-hidden 的 4 个 save/load1708-1725、1771-1788
- [x] `sort.js` ← sortTasks549-622
- [x] `render.js` ← renderAllTasks、renderTasks、applySearchFilter、createTaskCard、createTimeline、updateTaskCounts541-859、1106-1194
- [x] `modal.js` ← openTaskModal、populateForm、updateProgressDisplay、saveTask、editTask、deleteTask862-1027
- [x] `timeline.js` ← toggleTimeline 至 addSystemTimelineEntry1197-1381
- [x] `import-export.js` ← exportData 至 executeImport1444-1662
- [x] `notify.js` ← showNotification1085-1103)、checkDueDates1030-1053
- [x] `main.js` ← DOMContentLoaded 初始化(406-414+ loadInitialTasks417-513,暂时原样保留)
- [x] 过渡措施:内联 onclick 仍需全局函数,在 main.js 里临时 `window.xxx = xxx` 挂出所有被 HTML 引用的函数(阶段 2 已移除,无全局挂载残留)
- [x] 删除旧 `src/script.js`,全功能回归一遍(旧脚本已删除,完整人工回归已通过)
### 阶段 2:去内联事件,改事件委托
- [x] index.html 中静态元素的 `onclick` / `onchange` / `onkeyup` 全部移除,改在 main.js 里 `addEventListener` 绑定(添加任务、导出、导入、语言切换、搜索、排序下拉、眼睛按钮、保存任务、确认导入、进度滑块、文件 input)
- [x] 任务卡片内动态按钮改为 `data-action="pin|hide|edit|delete|toggle-timeline|…"` + `data-task-id` 属性,在三个列容器上做**事件委托**(click 一个监听器搞定)
- [x] 时间轴的添加/编辑/删除/键盘事件同样走委托(`data-action` + `data-timeline-id`
- [x] 移除阶段 1 的所有 `window.xxx` 临时挂载
- [x] 回归:重点测所有按钮、Enter 添加时间轴、Ctrl+Enter 保存、Esc 取消
### 阶段 3:消除重复
- [x] **HTML**:排序 `<select>` 的 13 个 option 改为 JS 根据 `config.js` 中的排序定义生成,三列共用;favicon 只保留一个 `<link>`
- [x] **show-hidden 逻辑合并**completed/todo 两套 toggle/updateUI/save/load 合并为按 status 参数化的一套(状态改为 `showHidden = { completed: false, todo: false }`localStorage 仍写原有两个键保持兼容)
- [x] **renderTasks** 中 completed/todo 的隐藏处理合并为一段参数化逻辑;**计数只由 `updateTaskCounts` 负责**renderTasks 不再写计数 DOM
- [x] **sort.js**`PRIORITY_ORDER` 常量只定义一次;number/priority/dueDate 的 asc/desc 用「比较器 + 方向系数」实现,消除成对复制
- [x] `config.js` 收敛所有魔法值:状态数组、优先级数组、localStorage 键名(`tasks``taskSortOrders``showHiddenCompletedTasks``showHiddenTodoTasks``tasksInitialized``tasksCorruptedBackup`)、导出版本号 `"1.0"`、到期预警天数 3
### 阶段 4:状态与存储收敛
- [x] `store.js`:所有状态私有化,暴露读取接口和语义化变更方法(addTask、updateTask、removeTask、togglePin、toggleHidden、setSortOrder…),变更方法内部统一调用 storage 持久化
- [x] `storage.js`:统一 try/catch 容错模式(现在 loadTasks/loadSortOrders/loadShowHidden* 三种写法各不相同),保留「损坏数据备份到 tasksCorruptedBackup」行为
- [x] 各模块不再直接改 `tasks` 数组,全部经 store 接口
### 阶段 5i18n 完善
- [x] `t(key, params)` 支持 `{count}` 等占位符插值,替换所有手工 `.replace('{count}', …)`
- [x] `applyTranslations()` 抽出(现在的 toggleLanguage 内 data-key 扫描逻辑),初始化时也调用一次(为后续记住语言偏好做准备)
- [x] 新增 `exportError` 键,修复导出失败文案 bug
- [ ] 可选:语言偏好持久化到 localStorage(新键,注意不影响旧数据)
### 阶段 6:渲染层改进
- [x] 渲染前记录已展开时间轴的 taskId 集合,渲染后恢复展开状态(修复现状缺陷)
- [x] 无时间轴条目的卡片也渲染「添加条目」入口(修复现状缺陷)
- [x] 精细化重渲染:单任务变更(置顶/隐藏/时间轴操作)只重渲染受影响列,而非三列全刷
- [x] createTaskCard 保持模板字符串方案即可,但拆出 createPriorityBadge、createDueDateBlock 等小函数,保证每段可读
### 阶段 7:数据清理
- [x] `loadInitialTasks` 的 5 条业务种子任务移到 `js/seed-data.js`(保留首次初始化种子行为)
- [x] 删除 exportData 中的调试 console.log
- [x] `normalizeTask` 补充 language 字段白名单校验
### 阶段 8CSS 整理
- [x] 顶部定义 CSS 自定义属性:主色、三列状态色、优先级色、到期警示色(当前 #007bff / #28a745 / #ffc107 / #dc3545 / #fd7e14 等散落各处)
- [x] 合并重复的 `.timeline-item` 定义;grep 验证并删除未使用的 `.status-title``.tasks-hidden-notice``.show-tasks-btn`
- [x] 按「基础 / 任务卡片 / 时间轴 / 模态框 / 响应式」重排分区注释(是否拆成多文件视方案 A/B 决定)
### 阶段 9:质量保障
- [x] ESLint + Prettier 全量通过,修掉参数遮蔽(sortTasks)等告警
- [x] 纯函数单元测试(Vitest + jsdom,可选但推荐):`normalizeTask``normalizeTasks`ID 去重)、`sortTasks` 全部 13 种排序、`getDueDateState`(过期/临期/边界日)、`validateImportData`、合并导入去重逻辑
- [x] 使用版本 1.0 旧格式 fixture 做**导入兼容性测试**(覆盖模式 + 合并模式)
- [x] 使用重构前格式的完整 localStorage 快照测试 6 个旧键与任务字段无损恢复
### 阶段 10:文档收尾
- [x] 更新 / 新建 README:项目结构说明、本地运行方式(如需服务器要写清楚)、数据存储说明(localStorage 键 + 导出格式)
- [x] CHANGELOG 记录重构版本
- [x] 合并 `dev-refactor``main` 前完整跑一遍下方回归清单
---
## 四、回归测试清单(每阶段结束必跑)
- [x] 添加任务:标题必填校验、进度滑块联动、隐藏勾选生效
- [x] 编辑任务:进展/状态/进度/负责人/到期日/优先级变更均自动生成时间轴系统条目
- [x] 删除任务:confirm 确认后删除
- [x] 三列各 13 种排序均正确,刷新后排序偏好保留;置顶任务在任何排序下都靠前
- [x] 搜索:实时过滤三列卡片
- [x] 隐藏/显示:卡片眼睛按钮、列头眼睛按钮、计数随显示模式变化、「全部已隐藏」提示文案
- [x] 时间轴:展开/收起、添加(按钮 + Enter)、编辑(Ctrl+Enter 保存、Esc 取消)、删除
- [x] 导出:文件名 `backup-tasks-yyyy-mm-dd.json`、内容含 version/exportDate/taskCount/tasks/sortOrders
- [x] 导入:非法文件报错;信息弹窗显示版本/数量/时间;覆盖模式全替换;合并模式按 标题|状态|创建时间 去重并提示跳过数
- [x] 中英切换:所有 data-key 文本、placeholder、页面标题、html lang、卡片内动态文本
- [x] 到期提醒:加载时对过期 / 3 天内到期的未完成任务弹通知
- [x] 容错:手工向 localStorage 写坏数据 → 提示 + 原始数据备份到 `tasksCorruptedBackup`
- [x] 首次访问(清空 localStorage):种子任务加载一次,`tasksInitialized` 置位
- [x] 移动端(<768px / <576px):按钮缩放、文字隐藏、操作按钮可点
## 五、风险与约束
1. **localStorage 兼容是红线**:6 个键名与数据结构不得变更,老用户数据必须无缝迁移。
2. **方案 AES Modules)改变使用方式**`file://` 直接打开会失效,需和使用场景确认后再定。
3. **内联事件改造面广**:HTML 静态元素 + JS 动态模板两处都有,漏改一处即功能失效,必须逐区域回归。
4. **无自动化测试兜底**(阶段 9 之前):靠回归清单人工保障,所以每阶段步子要小、提交要勤。