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

14 KiB
Raw Blame History

任务管理系统重构 TODO

分支:dev-refactor 目标:在不改变任何现有功能与数据格式的前提下,把单文件架构重构为模块化、可维护的结构。 原则:小步提交,每个阶段完成后跑一遍「回归测试清单」再进入下一阶段。


一、现状分析

文件 行数 问题概述
src/index.html 311 内联 onclick、3 份重复的排序下拉、favicon 重复 4 次
src/script.js 1788 单文件巨石:状态、i18n、渲染、存储、导入导出全部混在一起,约 30 个全局函数
src/styles.css 432 颜色硬编码、选择器重复定义、存在疑似死代码

主要问题清单

  1. 全局可变状态散落taskscurrentLangsortOrdersshowHiddenCompletedTasksshowHiddenTodoTaskscurrentEditingTaskIdpendingImportDataidCounter 共 8 个全局变量,任何函数都能直接改。
  2. 内联事件处理:HTML 和 JS 模板字符串里大量 onclick="xxx()",迫使所有函数必须挂在全局作用域。
  3. 成对重复代码completed / todo 两套几乎相同的逻辑):
    • toggleShowHiddenCompletedTasks / toggleShowHiddenTodoTasksscript.js:1665 / 1728
    • updateShowHiddenCompletedTasksUI / updateShowHiddenTodoTasksUIscript.js:1682 / 1745
    • save/loadShowHiddenCompletedTasks / save/loadShowHiddenTodoTasksscript.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. 职责重叠renderTasksupdateTaskCounts 都在写计数 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 / 待改进点(重构中一并处理)

  • exportData() 失败时错用 importError 文案(script.js:1491)→ 应新增 exportError 翻译键
  • exportData() 中残留调试 console.logscript.js:1453-1457)→ 删除
  • 时间轴区块只在 timeline.length > 0 时渲染(script.js:793),没有任何时间轴条目的任务无法手动添加第一条→ 补充入口
  • sortTasks(tasks, ...) 参数名遮蔽全局 tasksscript.js:549)→ 重命名参数
  • normalizeTask 未校验 language 字段 → 补充白名单 ['zh','en']
  • 每次重渲染后时间轴折叠状态丢失 → 渲染前记录展开的 taskId,渲染后恢复
  • CSS.timeline-item 重复定义(styles.css:87 与 276)→ 合并;确认 .status-titlestyles.css:270)、.tasks-hidden-noticestyles.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、ESLintflat config+ Prettier;先只做检查不大规模改格式
  • git tag 一个重构前基线(如 pre-refactor

阶段 1:文件拆分(先搬家,不改逻辑)

本阶段只移动代码 + 加 import/export,函数体一行不改。每搬完一个模块提交一次。

  • 建立 src/js/src/css/ 目录,index.html 引用路径同步更新
  • utils.js ← script.js:8-69generateId、escapeHtml、parseDueDateEnd、getDueDateState、isValidId、generateUniqueId
  • task-model.js ← script.js:71-152normalizeTask、normalizeTasks、normalizeSortOrders、validStatuses、validSortOrders
  • i18n/zh.jsi18n/en.js ← script.js:162-403 的翻译对象
  • storage.js ← saveTasks/loadTasks516-538)、saveSortOrders/loadSortOrders1421-1441)、show-hidden 的 4 个 save/load1708-1725、1771-1788
  • sort.js ← sortTasks549-622
  • render.js ← renderAllTasks、renderTasks、applySearchFilter、createTaskCard、createTimeline、updateTaskCounts541-859、1106-1194
  • modal.js ← openTaskModal、populateForm、updateProgressDisplay、saveTask、editTask、deleteTask862-1027
  • timeline.js ← toggleTimeline 至 addSystemTimelineEntry1197-1381
  • import-export.js ← exportData 至 executeImport1444-1662
  • notify.js ← showNotification1085-1103)、checkDueDates1030-1053
  • main.js ← DOMContentLoaded 初始化(406-414+ loadInitialTasks417-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.jsPRIORITY_ORDER 常量只定义一次;number/priority/dueDate 的 asc/desc 用「比较器 + 方向系数」实现,消除成对复制
  • config.js 收敛所有魔法值:状态数组、优先级数组、localStorage 键名(taskstaskSortOrdersshowHiddenCompletedTasksshowHiddenTodoTaskstasksInitializedtasksCorruptedBackup)、导出版本号 "1.0"、到期预警天数 3

阶段 4:状态与存储收敛

  • store.js:所有状态私有化,暴露读取接口和语义化变更方法(addTask、updateTask、removeTask、togglePin、toggleHidden、setSortOrder…),变更方法内部统一调用 storage 持久化
  • storage.js:统一 try/catch 容错模式(现在 loadTasks/loadSortOrders/loadShowHidden* 三种写法各不相同),保留「损坏数据备份到 tasksCorruptedBackup」行为
  • 各模块不再直接改 tasks 数组,全部经 store 接口

阶段 5i18n 完善

  • 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 字段白名单校验

阶段 8CSS 整理

  • 顶部定义 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,可选但推荐):normalizeTasknormalizeTasksID 去重)、sortTasks 全部 13 种排序、getDueDateState(过期/临期/边界日)、validateImportData、合并导入去重逻辑
  • 使用版本 1.0 旧格式 fixture 做导入兼容性测试(覆盖模式 + 合并模式)
  • 使用重构前格式的完整 localStorage 快照测试 6 个旧键与任务字段无损恢复

阶段 10:文档收尾

  • 更新 / 新建 README:项目结构说明、本地运行方式(如需服务器要写清楚)、数据存储说明(localStorage 键 + 导出格式)
  • CHANGELOG 记录重构版本
  • 合并 dev-refactormain 前完整跑一遍下方回归清单

四、回归测试清单(每阶段结束必跑)

  • 添加任务:标题必填校验、进度滑块联动、隐藏勾选生效
  • 编辑任务:进展/状态/进度/负责人/到期日/优先级变更均自动生成时间轴系统条目
  • 删除任务: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):按钮缩放、文字隐藏、操作按钮可点

五、风险与约束

  1. localStorage 兼容是红线:6 个键名与数据结构不得变更,老用户数据必须无缝迁移。
  2. 方案 A(ES Modules)改变使用方式file:// 直接打开会失效,需和使用场景确认后再定。
  3. 内联事件改造面广:HTML 静态元素 + JS 动态模板两处都有,漏改一处即功能失效,必须逐区域回归。
  4. 无自动化测试兜底(阶段 9 之前):靠回归清单人工保障,所以每阶段步子要小、提交要勤。