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