feat(todo): 子任务系统 — 复杂任务支持子任务拆解与依赖追踪

- 新增 `sub add <parent_id>` / `sub status <sid>` CLI 命令
- SID 格式 {parentId}{Letter},支持依赖声明(deps)与校验
- HTML 新增子任务展开块、进度徽章、依赖色标(绿=完成/红=未完成)
- 父任务全部子任务完成时自动转为「待验收」;手动设 done 时检查子任务
- 更新 /todo 命令文档补充 sub 子命令说明
- #21 已录入 8 个实现子任务(21A–21H),含完整依赖链

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-10 00:16:10 +08:00
parent 8925646eef
commit 18cd2497c1
4 changed files with 732 additions and 98 deletions
+61 -5
View File
@@ -1,6 +1,6 @@
---
description: 项目 TODO 管理(list / add / status / done / reject / reopen / rm
argument-hint: "list | add <描述> | status <id> <open|doing|done> | done <id> [version] | reject <id> <原因> | reopen <id> | rm <id>"
description: 项目 TODO 管理(list / add / status / done / reject / reopen / rm / sub
argument-hint: "list | add <描述> | status <id> <open|doing|done> | done <id> [version] | reject <id> <原因> | reopen <id> | rm <id> | sub add <parent_id> ... | sub status <sid> ..."
model: claude-sonnet-4-6
allowed-tools: Bash, Read
---
@@ -13,6 +13,30 @@ allowed-tools: Bash, Read
- `done` 状态可被用户 `reject`(拒绝),退回 `open` 并升为最高优先级
- Claude 可设:`open` / `doing` / `done`;用户专属:`accepted``/todo done`)、`reject`
**子任务(subtask**:复杂任务(tier-1/2)可拆出子任务,SID 格式为 `{parentId}{Letter}`(如 `21A``21B`)。
- 父任务在子任务全部 `done` 前**不得**手动设为 `done`;全部完成时**自动**转为 `done`
- 子任务状态与父任务独立,自身有 `open` / `doing` / `done` 三态
- 子任务可声明依赖(`deps`),HTML 中以绿/红徽章展示依赖完成情况
---
## 改动等级 (tier) 与开发流程
每个 todo 有两个独立维度:**重要度 `level`**(优先级高低)和 **改动等级 `tier`**(复杂度/工作量)。
| tier | 名称 | 判定标准 | 开发流程 |
|------|------|---------|---------|
| 1 | 一级 | 涉及接口设计 / 数据库 schema 变更,**或**改动跨越 **2 个以上模块** | **必须先进 plan 模式**,用户批准方案后才能编码;完成后同步更新 `docs/architecture/``docs/api/` 等设计文档 |
| 2 | 二级 | 模块内较大改动,涉及 **5 个文件以上** | 可直接开发,完成后本地验证(build/test/analyze |
| 3 | 三级 | 小改动、局部优化、bugfix | 可直接开发,完成后本地验证 |
**判定关键词指引:**
- 一级:「新增接口」「改 schema」「新表」「跨模块」「涉及后端+前端」「新增 API」
- 二级:「重构」「整页改版」「涉及多个 screen」「较大」
- 三级:「修复」「bugfix」「优化」「调整文案」「小改」「单文件」
**重要**:开发完成后只做本地验证(build / test / analyze),**不要**自动触发 `/release`。发版由用户决定。
---
根据 `$ARGUMENTS` 分派,缺省等同 `list`
@@ -31,20 +55,21 @@ node todo/todo.mjs list
- `--title`:核心一句话标题(简洁,去掉平台/类别信息)
- `--level``high`(阻断/紧急)/ `mid`(重要,默认)/ `low`(一般/优化)
- 关键词「紧急」「阻断」「必须」「严重」→ high;「重要」「需要」→ mid;其余 → low
- `--tier`:**必须推断**,见上方「改动等级」判定标准,取值 `1`/`2`/`3`
- `--tags`:从描述推断平台/类别,可多个逗号分隔,常用值:`前端,后端,Web,mac,Windows,Android,iOS,数据库,CI/CD,文档`
- `--desc`:补充说明(可选,如有具体文件/路径/上下文则填入)
然后运行:
```bash
node todo/todo.mjs add --title "..." --level mid --tags "前端,Web" --desc "..."
node todo/todo.mjs add --title "..." --level mid --tier 2 --tags "前端,Web" --desc "..."
```
转述:「已添加 #id [级别] 标题 标签」,并显示 summary。
转述:「已添加 #id [级别·等级] 标题 标签」,并显示 summary。
## status <id> <open|doing|done>
**Claude 在开发过程中主动调用**,更新条目的开发状态:
- 开始处理某个 todo → `status <id> doing`
- 开始处理某个 todo → `status <id> doing`(**先确认 tier**:一级须先 plan 模式,二/三级直接开发)
- 开发完成提交验收 → `status <id> done`
- 退回重做 → `status <id> open`
@@ -99,4 +124,35 @@ node todo/todo.mjs rm <id>
---
## sub add <parent_id> --title "..." [--tier N] [--deps "21A,21B"]
**Claude 在开发过程中主动调用**,为复杂任务(tier-1/2)添加子任务:
- `parent_id`:父任务数字 id(如 `21`
- `--title`:子任务标题
- `--tier`:改动等级(`1`/`2`/`3`,默认 `2`
- `--deps`:依赖的其他子任务 SID,逗号分隔(如 `"21A,21B"`);被依赖项必须已存在
SID 自动分配:第一个子任务为 `{parent_id}A`,依此类推(21A, 21B, 21C…)。
父任务若为 `open` 状态,添加第一个子任务时自动升为 `doing`
```bash
node todo/todo.mjs sub add <parent_id> --title "..." [--tier N] [--deps "21A,21B"]
```
转述:「已添加子任务 {SID} [等级]「{title}」→ #parent 依赖: ...」,并显示 summary。
## sub status <sid> <open|doing|done>
**Claude 在开发过程中主动调用**,更新子任务状态:
- SID 格式:`21A``21B`(不区分大小写)
- 当**最后一个子任务**标记为 `done` 时,父任务**自动**转为 `done`
```bash
node todo/todo.mjs sub status <sid> <open|doing|done>
```
转述:「{SID}「{title}」→ {状态}」,若触发父任务自动转换则一并提示。
---
所有命令完成后,在终端显示来自脚本的完整 summary(按 open / doing / done / accepted 分组)。
+280 -63
View File
@@ -71,7 +71,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
padding: 14px 18px; margin-bottom: 10px;
border-left-width: 4px;
}
/* 卡片左边框颜色与状态匹配 */
.todo-card.s-open { border-left-color: #3b82f6; }
.todo-card.s-doing { border-left-color: #f97316; }
.todo-card.s-done { border-left-color: #eab308; }
@@ -99,6 +98,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
.t-tag { background: #efefef; color: #555; cursor: pointer; transition: .12s; }
.t-tag:hover { background: #d9e8fb; color: #1d4ed8; }
.ver-badge { font-weight: 700; color: #1f7a44; }
.sub-progress-badge { background: #f0f4ff; color: #3730a3; }
/* 状态徽章 */
.status-badge { font-size: 11.5px; }
@@ -112,6 +112,11 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
.empty-tip { font-size: 14px; color: #8aa3c4; margin: 8px 0 24px; }
.hidden { display: none !important; }
/* ── 改动等级徽章 ── */
.tier-1 { background: #ede9fe; color: #6d28d9; }
.tier-2 { background: #ccfbf1; color: #0f766e; }
.tier-3 { background: #f1f5f9; color: #475569; }
/* ── 拒绝按钮 & 拒绝原因 ── */
.reject-btn {
padding: 3px 10px; border-radius: 6px; border: 1.5px solid #dc2626;
@@ -126,6 +131,56 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
}
.reject-date { color: #ef9999; font-size: 12px; }
/* ── 子任务区 ── */
.subtask-block {
margin-top: 12px; padding: 10px 14px;
background: #f8fafc; border: 1px solid #e4e9ef; border-radius: 8px;
}
.subtask-header {
display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap;
}
.subtask-label {
font-size: 11px; font-weight: 700; color: #7b8794;
text-transform: uppercase; letter-spacing: .6px;
}
.subtask-progress-text { font-size: 12px; color: #52606d; }
.subtask-progress-bar {
flex: 1; height: 5px; background: #e4e9ef; border-radius: 3px; min-width: 60px;
}
.subtask-progress-fill { height: 100%; background: #22c55e; border-radius: 3px; transition: width .4s; }
.subtask-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.subtask-item {
display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
padding: 6px 10px; border-radius: 6px; font-size: 13px;
background: #fff; border: 1px solid #e8edf2;
}
.subtask-item.s-done { background: #f0fdf4; border-color: #bbf7d0; }
.subtask-item.s-doing { background: #fff7ed; border-color: #fed7aa; }
.subtask-item.s-accepted { background: #f0fdf4; border-color: #bbf7d0; opacity: .75; }
.sub-icon {
width: 16px; text-align: center; font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.sub-icon.s-done { color: #16a34a; }
.sub-icon.s-doing { color: #ea580c; }
.sub-icon.s-open { color: #93aec8; }
.sub-icon.s-accepted { color: #16a34a; }
.sub-sid {
font-family: "JetBrains Mono", monospace; font-size: 11.5px;
color: #52606d; font-weight: 700; min-width: 32px; flex-shrink: 0;
}
.sub-title { flex: 1; font-size: 13px; min-width: 120px; }
.subtask-item.s-done .sub-title,
.subtask-item.s-accepted .sub-title { text-decoration: line-through; color: #52606d; }
.sub-badges { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.dep-section { display: flex; align-items: center; gap: 3px; }
.dep-label { font-size: 11px; color: #8aa3c4; margin-right: 2px; }
.dep-badge {
padding: 1px 6px; border-radius: 8px; font-size: 11px;
font-weight: 700; font-family: monospace; cursor: default;
}
.dep-done { background: #dcfce7; color: #166534; }
.dep-pending { background: #fee2e2; color: #b91c1c; }
/* ── 拒绝 Modal ── */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.45);
@@ -166,12 +221,12 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<header>
<div class="wrap">
<h1>酒库管理系统 — 项目 TODO</h1>
<div class="header-meta">生成于 2026-06-09 · 真相源 todo/todo.json</div>
<div class="header-meta">生成于 2026-06-10 · 真相源 todo/todo.json</div>
<div class="stats">
<div class="stat-pill"><strong>24</strong>全部</div>
<div class="stat-pill"><strong>6</strong>待开始</div>
<div class="stat-pill"><strong>0</strong>开发中</div>
<div class="stat-pill"><strong>4</strong>待验收</div>
<div class="stat-pill"><strong>4</strong>待开始</div>
<div class="stat-pill"><strong>1</strong>开发中</div>
<div class="stat-pill"><strong>5</strong>待验收</div>
<div class="stat-pill"><strong>14</strong>已验收</div>
</div>
</div>
@@ -195,6 +250,14 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<button class="filter-btn" data-filter-status="accepted">已验收</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">改动等级</span>
<button class="filter-btn active" data-filter-tier="all">全部</button>
<button class="filter-btn" data-filter-tier="1">一级</button>
<button class="filter-btn" data-filter-tier="2">二级</button>
<button class="filter-btn" data-filter-tier="3">三级</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">平台 / 标签</span>
<button class="filter-chip" data-filter-tag="Android">Android</button><button class="filter-chip" data-filter-tag="Web">Web</button><button class="filter-chip" data-filter-tag="Windows">Windows</button><button class="filter-chip" data-filter-tag="iOS">iOS</button><button class="filter-chip" data-filter-tag="mac">mac</button><button class="filter-chip" data-filter-tag="前端">前端</button><button class="filter-chip" data-filter-tag="后端">后端</button><button class="filter-chip" data-filter-tag="安全">安全</button><button class="filter-chip" data-filter-tag="数据库">数据库</button><button class="filter-chip" data-filter-tag="文档">文档</button>
@@ -206,7 +269,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="section-block" id="section-open">
<div class="section-title st-open" data-toggle="open">
📋 待开始 <span class="s-count">6</span>
📋 待开始 <span class="s-count">4</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-open">
@@ -216,6 +279,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="2"
data-level="high"
data-status="open"
data-tier=""
data-tags="Web">
<div class="card-header">
<span class="item-title">备案号为占位假值</span>
@@ -223,10 +287,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-block">高优 · 紧急</span>
</div>
</div>
<div class="item-desc">现为沪ICP备2026000000号(全0),上线前必须替换真实备案号。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
@@ -236,58 +303,11 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
</div>
</li>
<li class="todo-card s-open"
data-id="20"
data-level="mid"
data-status="open"
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">商品详情页重设计:展示所有字段,内容为公开页超集</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">当前 product_detail_screen.dart 显示内容过于简单。需先做页面设计(UI 方案),内容要求是公开扫码页的超集:包含商品基本信息、参数(产地/保质期/储存方式)、价格、描述文档、图片、库存批次、入库记录等管理侧专属字段。另:商品公开链接展示为可点击超链接(点击可跳转公开扫码页)。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="21"
data-level="mid"
data-status="open"
data-tags="前端,后端,安全,文档">
<div class="card-header">
<span class="item-title">授权信息功能方案设计</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">完整方案设计,涵盖:界面设计与交互、前端实现方案、后端接口与数据模型、安全策略(防盗用、防滥用、授权校验)等。需产出设计文档后再进入开发阶段。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="安全">安全</span> <span class="tag t-tag" data-tag="文档">文档</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="22"
data-level="low"
data-status="open"
data-tier=""
data-tags="前端,Web,文档">
<div class="card-header">
<span class="item-title">关于我们页文档/更新日志链接更新,Web 新增版本历史页</span>
@@ -295,10 +315,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">1. 客户端「关于我们」页面的「打开文档」「更新日志」链接指向需更新为正确地址。2. Web 营销站新增版本更新页面(/changelog 或 /download/),从 CHANGELOG.md 自动渲染,展示所有历史版本更新记录。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="文档">文档</span></div>
<div class="item-meta">
@@ -312,6 +335,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="23"
data-level="low"
data-status="open"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">注册/联系入口「发邮件」升级为「加客服微信」并优化文案</span>
@@ -319,10 +343,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">当前注册成功页和网站联系支持入口引导用户「发邮件」,体验较弱。升级为扫码加客服微信,同时梳理相关文案措辞,使表达更专业、有亲和力。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
@@ -336,6 +363,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="24"
data-level="low"
data-status="open"
data-tier=""
data-tags="Web,文档">
<div class="card-header">
<span class="item-title">服务条款与隐私政策页面</span>
@@ -343,10 +371,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">在营销网站(jiu.51yanmei.com)补充服务条款(Terms of Service)和隐私政策(Privacy Policy)页面,注册页底部添加链接。最低优先级,待其他功能稳定后处理。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="文档">文档</span></div>
<div class="item-meta">
@@ -360,18 +391,99 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
</div>
<div class="section-block" id="section-doing">
<div class="section-title st-doing" data-toggle="doing">
🔨 开发中 <span class="s-count">0</span>
🔨 开发中 <span class="s-count">1</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-doing">
<ul class="todo-list" id="list-doing">
<p class="empty-tip">暂无条目</p>
<li class="todo-card s-doing"
data-id="21"
data-level="mid"
data-status="doing"
data-tier=""
data-tags="前端,后端,安全,文档">
<div class="card-header">
<span class="item-title">授权信息功能方案设计</span>
<div class="card-badges">
<span class="tag status-badge s-doing">开发中</span>
<span class="tag t-high">重要</span>
<span class="tag sub-progress-badge">0/8 子任务</span>
</div>
</div>
<div class="item-desc">完整方案设计,涵盖:界面设计与交互、前端实现方案、后端接口与数据模型、安全策略(防盗用、防滥用、授权校验)等。需产出设计文档后再进入开发阶段。</div>
<div class="subtask-block">
<div class="subtask-header">
<span class="subtask-label">子任务</span>
<span class="subtask-progress-text">0 / 8 完成</span>
<div class="subtask-progress-bar"><div class="subtask-progress-fill" style="width:0%"></div></div>
</div>
<ul class="subtask-list"><li class="subtask-item s-open" data-sid="21A">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21A</span>
<span class="sub-title">后端:Ed25519 签发验签工具 + GenerateKey 修复</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span></div>
</li>
<li class="subtask-item s-open" data-sid="21B">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21B</span>
<span class="sub-title">数据库:license_devices 表 + licenses 表改造</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-pending" title="21A 未完成">21A</span></span></div>
</li>
<li class="subtask-item s-open" data-sid="21C">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21C</span>
<span class="sub-title">后端:注册自动签发 30 天 trial license</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-pending" title="21A 未完成">21A</span></span></div>
</li>
<li class="subtask-item s-open" data-sid="21D">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21D</span>
<span class="sub-title">后端:LicenseGuard 中间件 + JWT claim 扩展</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-pending" title="21B 未完成">21B</span><span class="dep-badge dep-pending" title="21C 未完成">21C</span></span></div>
</li>
<li class="subtask-item s-open" data-sid="21E">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21E</span>
<span class="sub-title">前端:license model/repository/device_id 采集</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-pending" title="21A 未完成">21A</span></span></div>
</li>
<li class="subtask-item s-open" data-sid="21F">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21F</span>
<span class="sub-title">前端:激活与设备管理 UI(授权 Tab 重构)</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-pending" title="21D 未完成">21D</span><span class="dep-badge dep-pending" title="21E 未完成">21E</span></span></div>
</li>
<li class="subtask-item s-open" data-sid="21G">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21G</span>
<span class="sub-title">前端:gating 横幅 + 到期弹窗(app_shell 注入)</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-pending" title="21D 未完成">21D</span><span class="dep-badge dep-pending" title="21E 未完成">21E</span></span></div>
</li>
<li class="subtask-item s-open" data-sid="21H">
<span class="sub-icon s-open"></span>
<span class="sub-sid">21H</span>
<span class="sub-title">端到端验证:phase 切换 + 签名拒绝 + 多设备上限</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-open">待开始</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-pending" title="21D 未完成">21D</span><span class="dep-badge dep-pending" title="21F 未完成">21F</span><span class="dep-badge dep-pending" title="21G 未完成">21G</span></span></div>
</li></ul>
</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="安全">安全</span> <span class="tag t-tag" data-tag="文档">文档</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
</ul>
</div>
</div>
<div class="section-block" id="section-done">
<div class="section-title st-done" data-toggle="done">
🔍 待验收 <span class="s-count">4</span>
🔍 待验收 <span class="s-count">5</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-done">
@@ -381,17 +493,49 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="19"
data-level="high"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">入库单新建表单增加产地/保质期/储存方式,选填项默认折叠</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-block">高优 · 紧急</span>
<button class="reject-btn" data-id="19" data-title="入库单新建表单增加产地/保质期/储存方式,选填项默认折叠">拒绝验收</button>
</div>
</div>
<div class="item-desc">新建入库单的商品行增加产地、保质期、储存方式三个字典下拉选项(已有字典数据)。必填项(商品名、数量等)正常显示;选填项默认隐藏,点击展开按钮后在下一行展示。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="20"
data-level="mid"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">商品详情页重设计:展示所有字段,内容为公开页超集</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<button class="reject-btn" data-id="20" data-title="商品详情页重设计:展示所有字段,内容为公开页超集">拒绝验收</button>
</div>
</div>
<div class="item-desc">当前 product_detail_screen.dart 显示内容过于简单。需先做页面设计(UI 方案),内容要求是公开扫码页的超集:包含商品基本信息、参数(产地/保质期/储存方式)、价格、描述文档、图片、库存批次、入库记录等管理侧专属字段。另:商品公开链接展示为可点击超链接(点击可跳转公开扫码页)。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -405,17 +549,21 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="27"
data-level="mid"
data-status="done"
data-tier=""
data-tags="Windows,mac">
<div class="card-header">
<span class="item-title">桌面客户端单实例限制,禁止同时运行多个进程</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<button class="reject-btn" data-id="27" data-title="桌面客户端单实例限制,禁止同时运行多个进程">拒绝验收</button>
</div>
</div>
<div class="item-desc">Windows 和 macOS 客户端启动时检测是否已有实例运行;若有,则激活已有窗口并退出新进程。Flutter desktop 可通过 window_single_instance 或 dart:io 锁文件实现。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="mac">mac</span></div>
<div class="item-meta">
@@ -429,17 +577,21 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="25"
data-level="low"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">移动端左侧抽屉菜单支持手势侧滑唤出</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-low">一般 / 优化</span>
<button class="reject-btn" data-id="25" data-title="移动端左侧抽屉菜单支持手势侧滑唤出">拒绝验收</button>
</div>
</div>
<div class="item-desc">窄屏(手机)模式下,导航 Drawer 当前需点击汉堡按钮打开,需支持从屏幕左边缘向右滑动手势唤出菜单,符合移动端操作习惯。涉及 app_shell.dart 的 Drawer 配置。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -453,17 +605,21 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="26"
data-level="low"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">入库单详情商品明细改为卡片式竖排布局</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-low">一般 / 优化</span>
<button class="reject-btn" data-id="26" data-title="入库单详情商品明细改为卡片式竖排布局">拒绝验收</button>
</div>
</div>
<div class="item-desc">当前入库单详情的商品明细以横向表格展示,字段多时在窄屏上挤压严重。改为每件商品独占一个卡片区块,字段竖向排列(商品名、规格、数量、单价、产地、保质期等各占一行),信息层次清晰,移动端阅读体验更好。宽屏可保持表格,窄屏自动切换为卡片流(复用 MobileListCard 模式)。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -487,6 +643,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="13"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="后端,iOS,Android">
<div class="card-header">
<span class="item-title">公开商品 API 补充 batch.quantity 和 product code/barcode</span>
@@ -494,10 +651,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">Flutter _AuthenticityCard 读取 batch['quantity'] 但后端 public.go batchData 未返回 quantity。另 product code/barcode 未暴露,参数卡无法显示商品编码。handler/public.go GetProduct</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -511,6 +671,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="6"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">注册成功提示路径需核对</span>
@@ -518,10 +679,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">提示「忘记门店编码可在「系统设置→关于」查看」,需与实际客户端菜单路径一致。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
@@ -535,6 +699,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="5"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">页脚双份维护</span>
@@ -542,10 +707,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">njk 站点用 footer.njk 读变量,功能页把页脚硬编码各写一份,改一处不同步。根因同「两套模板」。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
@@ -559,6 +727,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="3"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">注册接口同源依赖问题</span>
@@ -566,10 +735,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">用 window.location.origin+'/api/v1/public/register',要求同源部署;分离部署时指错。需确认部署形态。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
@@ -583,6 +755,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="18"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">基础数据管理 UI 增加 4 个字典维护 Tab</span>
@@ -590,10 +763,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">products_screen.dart 基础数据管理页增加产地/保质期/储存方式/描述文档 4 个维护 Tab,复用现有 option 维护组件(增删改查);描述文档 Tab 含 title+content 多行输入</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -607,6 +783,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="14"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">Footer「商品报错」「意见反馈」补充实际功能</span>
@@ -614,10 +791,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">public_product_screen.dart _Footer 底部两个链接无 url/action,点击静默。需设计方案:跳转反馈表单、弹出联系方式、或发邮件。line:1353-1355</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -631,6 +811,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="10"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,后端,iOS,Android">
<div class="card-header">
<span class="item-title">公开商品 API 补充 sale_price 并在页面展示价格</span>
@@ -638,10 +819,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">product model 有 sale_price 字段但 public.go GetProduct 未返回。需:1) 后端 batchData/productData 加 sale_price2) Flutter _TitleBlock 或单独区块展示建议零售价。client: public_product_screen.dart, backend: handler/public.go</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -655,6 +839,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="9"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">设计并实现添加门店微信功能</span>
@@ -662,10 +847,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">PublicProductScreen _ShopCard「添加门店微信」按钮 onPressed: () {} 为空。需设计方案:门店维护微信号字段 or 二维码图片,扫码页展示。涉及后端 shop model 扩展 + 前端展示。client/lib/screens/public/public_product_screen.dart:1255</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -679,6 +867,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="8"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">补全「查看本店其他商品」功能</span>
@@ -686,10 +875,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">PublicProductScreen _ShopCard 底部按钮 onPressed: () {} 为空,需实现跳转至该门店商品列表页。client/lib/screens/public/public_product_screen.dart:1240</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -703,6 +895,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="17"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">入库表单关联 4 个商品属性字典</span>
@@ -710,10 +903,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">stock_in_form_screen.dart 生产日期字段附近增加产地/保质期/储存方式/描述文档 4 个下拉选择(数据来自字典 List 接口);FindOrCreate/Update 接受并写入 4 个 id;前端新增对应 API client 方法</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -727,6 +923,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="16"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="后端,前端,iOS,Android">
<div class="card-header">
<span class="item-title">public.go 返回产地/保质期/储存/介绍,公开页去硬编码</span>
@@ -734,10 +931,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">public.go GetProduct Preload 4 张新表 + 三级回退(描述文档→旧Description→通用中性兜底)+ 默认值常量(保质期/储存方式);Flutter public_product_screen.dart _ParamsCard/_DescriptionSection 改读真实数据,移除茅台 mock</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
@@ -751,6 +951,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="15"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="后端,数据库">
<div class="card-header">
<span class="item-title">4 张商品属性字典表 + 后端 CRUD</span>
@@ -758,10 +959,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">新增 product_origin_options/product_shelf_life_options/product_storage_options/product_description_docs 4 张字典表(TenantBase+shop_id 隔离);Product 增加 4 个可空外键;AutoMigrate 注册;handler/product_attr.go CRUD16 个方法);router.go 追加 4 组路由在 /product-options/ 下</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="数据库">数据库</span></div>
<div class="item-meta">
@@ -775,6 +979,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="4"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">scan.html / features 走 passthrough 不享模板</span>
@@ -782,10 +987,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">features/scan.html 直接复制,不经 njk 模板,页脚/备案/CSS 都得手动同步。建议长期改造为模板化。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
@@ -799,6 +1007,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
data-id="1"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">两套 CSS / 设计令牌并存</span>
@@ -806,10 +1015,13 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">主站 njk 用 color.css+style.css,功能页用 colors_and_type.css,导航/页脚各写一份,维护分裂。建议合并到一套,功能页走 base.njk 模板。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
@@ -848,23 +1060,25 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
(function() {
let curLevel = 'all';
let curStatus = 'all';
let curTier = 'all';
let curTags = new Set();
function applyFilter() {
document.querySelectorAll('.todo-card').forEach(card => {
const lvl = card.dataset.level;
const status = card.dataset.status;
const tier = card.dataset.tier;
const tags = card.dataset.tags ? card.dataset.tags.split(',') : [];
let show = true;
if (curLevel !== 'all' && lvl !== curLevel) show = false;
if (curStatus !== 'all' && status !== curStatus) show = false;
if (curTier !== 'all' && String(tier) !== curTier) show = false;
if (curTags.size > 0 && ![...curTags].some(t => tags.includes(t))) show = false;
card.classList.toggle('hidden', !show);
});
// 筛选后自动展开包含结果的区域
if (curStatus !== 'all') {
const wrap = document.getElementById('list-wrap-' + curStatus);
if (wrap) {
@@ -875,7 +1089,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
}
}
// 重要度筛选
document.querySelectorAll('[data-filter-level]').forEach(btn => {
btn.addEventListener('click', () => {
curLevel = btn.dataset.filterLevel;
@@ -884,7 +1097,14 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 状态筛选
document.querySelectorAll('[data-filter-tier]').forEach(btn => {
btn.addEventListener('click', () => {
curTier = btn.dataset.filterTier;
document.querySelectorAll('[data-filter-tier]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
document.querySelectorAll('[data-filter-status]').forEach(btn => {
btn.addEventListener('click', () => {
curStatus = btn.dataset.filterStatus;
@@ -893,7 +1113,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 标签筛选(多选)
document.querySelectorAll('[data-filter-tag]').forEach(btn => {
btn.addEventListener('click', () => {
const t = btn.dataset.filterTag;
@@ -903,7 +1122,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 卡片标签 → 快速筛选
document.querySelectorAll('.t-tag').forEach(tag => {
tag.addEventListener('click', e => {
e.stopPropagation();
@@ -913,7 +1131,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 区域折叠切换
document.querySelectorAll('.section-title[data-toggle]').forEach(title => {
title.addEventListener('click', () => {
const st = title.dataset.toggle;
+89 -4
View File
@@ -1,9 +1,9 @@
{
"meta": {
"title": "酒库管理系统 — 项目 TODO",
"updated_at": "2026-06-09T04:11:47.912Z"
"updated_at": "2026-06-09T16:15:09.789Z"
},
"seq": 27,
"seq": 28,
"items": [
{
"id": 1,
@@ -271,7 +271,7 @@
"done": false,
"completed_at": null,
"version": null,
"status": "open"
"status": "done"
},
{
"id": 21,
@@ -288,7 +288,92 @@
"done": false,
"completed_at": null,
"version": null,
"status": "open"
"status": "doing",
"subtasks": [
{
"sid": "21A",
"title": "后端:Ed25519 签发验签工具 + GenerateKey 修复",
"tier": 2,
"deps": [],
"status": "open",
"created_at": "2026-06-09T16:14:58.628Z"
},
{
"sid": "21B",
"title": "数据库:license_devices 表 + licenses 表改造",
"tier": 2,
"deps": [
"21A"
],
"status": "open",
"created_at": "2026-06-09T16:15:09.490Z"
},
{
"sid": "21C",
"title": "后端:注册自动签发 30 天 trial license",
"tier": 2,
"deps": [
"21A"
],
"status": "open",
"created_at": "2026-06-09T16:15:09.541Z"
},
{
"sid": "21D",
"title": "后端:LicenseGuard 中间件 + JWT claim 扩展",
"tier": 2,
"deps": [
"21B",
"21C"
],
"status": "open",
"created_at": "2026-06-09T16:15:09.590Z"
},
{
"sid": "21E",
"title": "前端:license model/repository/device_id 采集",
"tier": 2,
"deps": [
"21A"
],
"status": "open",
"created_at": "2026-06-09T16:15:09.639Z"
},
{
"sid": "21F",
"title": "前端:激活与设备管理 UI(授权 Tab 重构)",
"tier": 2,
"deps": [
"21D",
"21E"
],
"status": "open",
"created_at": "2026-06-09T16:15:09.689Z"
},
{
"sid": "21G",
"title": "前端:gating 横幅 + 到期弹窗(app_shell 注入)",
"tier": 2,
"deps": [
"21D",
"21E"
],
"status": "open",
"created_at": "2026-06-09T16:15:09.739Z"
},
{
"sid": "21H",
"title": "端到端验证:phase 切换 + 签名拒绝 + 多设备上限",
"tier": 2,
"deps": [
"21D",
"21F",
"21G"
],
"status": "open",
"created_at": "2026-06-09T16:15:09.789Z"
}
]
},
{
"id": 22,
+302 -26
View File
@@ -10,7 +10,7 @@
* accepted 已验收 仅限用户设置(手动编辑 JSON 或 `done <id>` 命令)
*
* 子命令:
* add --title "..." [--level high|mid|low] [--tags a,b] [--desc "..."]
* add --title "..." [--level high|mid|low] [--tier 1|2|3] [--tags a,b] [--desc "..."]
* status <id> <open|doing|done> — 更新开发状态(Claude 可调用)
* done <id> [--version vX.Y.Z] — 标记已验收交付(仅用户调用)
* reject <id> --reason "原因" — 拒绝验收,退回 open 并升为 high(仅用户调用)
@@ -18,6 +18,8 @@
* rm <id>
* list
* render
* sub add <parent_id> --title "..." [--tier 1|2|3] [--deps "21A,21B"]
* sub status <sid> <open|doing|done> — 更新子任务状态(如 21A)
*/
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
@@ -37,6 +39,11 @@ const LEVEL_ORDER = { high: 0, mid: 1, low: 2 };
const STATUS_LABEL = { open: '待开始', doing: '开发中', done: '待验收', accepted: '已验收' };
const STATUS_ORDER = { open: 0, doing: 1, done: 2, accepted: 3 };
const TIER_LABEL = { 1: '一级', 2: '二级', 3: '三级' };
const SUB_STATUS_ICON = { open: '○', doing: '◐', done: '✓' };
const SUB_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
// ─── 数据读写 ────────────────────────────────────────────────────────────────
function load() {
@@ -44,7 +51,6 @@ function load() {
return { meta: { title: '酒库管理系统 — 项目 TODO', updated_at: now() }, seq: 0, items: [] };
}
const db = JSON.parse(readFileSync(DB, 'utf8'));
// 迁移旧格式:无 status 字段时,依据 done 布尔值补全
for (const item of db.items) {
if (!item.status) {
item.status = item.done ? 'accepted' : 'open';
@@ -76,6 +82,45 @@ function gitTag() {
}
}
// ─── 子任务工具 ──────────────────────────────────────────────────────────────
function nextSubtaskLetter(item) {
const existing = (item.subtasks || []).map(s => s.sid);
for (const l of SUB_LETTERS) {
const sid = `${item.id}${l}`;
if (!existing.includes(sid)) return l;
}
return null;
}
function parseSid(sid) {
const m = String(sid).match(/^(\d+)([A-Za-z])$/);
if (!m) return null;
return { parentId: parseInt(m[1], 10), letter: m[2].toUpperCase() };
}
function findSubtask(db, sid) {
const parsed = parseSid(sid);
if (!parsed) { console.error(`错误:无效子任务 ID "${sid}",格式应为 21A`); process.exit(1); }
const parent = db.items.find(i => i.id === parsed.parentId);
if (!parent) { console.error(`错误:找不到父任务 id=${parsed.parentId}`); process.exit(1); }
const normalSid = sid.toUpperCase();
const sub = (parent.subtasks||[]).find(s => s.sid === normalSid);
if (!sub) { console.error(`错误:找不到子任务 ${normalSid}`); process.exit(1); }
return { parent, sub };
}
function subtasksDoneCount(item) {
if (!item.subtasks || !item.subtasks.length) return { done: 0, total: 0 };
const done = item.subtasks.filter(s => s.status === 'done' || s.status === 'accepted').length;
return { done, total: item.subtasks.length };
}
function allSubtasksDone(item) {
const { done, total } = subtasksDoneCount(item);
return total > 0 && done === total;
}
// ─── 终端 summary ────────────────────────────────────────────────────────────
function levelText(level) {
@@ -101,9 +146,20 @@ function printSummary(db) {
if (byStatus[st].length === 0) continue;
console.log(`${label}${byStatus[st].length}):`);
for (const i of byStatus[st]) {
const tags = i.tags && i.tags.length ? ' ' + i.tags.join('/') : '';
const lvl = LEVEL_LABEL[i.level] || i.level;
console.log(` [${lvl}] #${i.id} ${i.title}${tags}`);
const tags = i.tags && i.tags.length ? ' ' + i.tags.join('/') : '';
const lvl = LEVEL_LABEL[i.level] || i.level;
const tier = i.tier ? '·' + TIER_LABEL[i.tier] : '';
const { done, total: stTotal } = subtasksDoneCount(i);
const subInfo = stTotal > 0 ? ` [${done}/${stTotal}子任务]` : '';
console.log(` [${lvl}${tier}] #${i.id} ${i.title}${tags}${subInfo}`);
if (stTotal > 0) {
for (const s of i.subtasks) {
const icon = SUB_STATUS_ICON[s.status] || '○';
const deps = s.deps && s.deps.length ? ` (需: ${s.deps.join(',')})` : '';
const stier = s.tier ? ` [${TIER_LABEL[s.tier]}]` : '';
console.log(` ${icon} ${s.sid}${stier} ${s.title}${deps}`);
}
}
}
}
@@ -136,6 +192,43 @@ function statusClass(status) {
return { open: 's-open', doing: 's-doing', done: 's-done', accepted: 's-accepted' }[status] || 's-open';
}
function renderSubtasks(item) {
if (!item.subtasks || item.subtasks.length === 0) return '';
const { done, total } = subtasksDoneCount(item);
const pct = total > 0 ? Math.round(done / total * 100) : 0;
const subItems = item.subtasks.map(s => {
const stCls = statusClass(s.status);
const stLbl = STATUS_LABEL[s.status] || s.status;
const icon = SUB_STATUS_ICON[s.status] || '○';
const tierBadge = s.tier ? `<span class="tag tier-${s.tier}">${TIER_LABEL[s.tier]}</span>` : '';
const depBadges = (s.deps||[]).map(dep => {
const depSub = item.subtasks.find(x => x.sid === dep);
const depDone = depSub && (depSub.status === 'done' || depSub.status === 'accepted');
return `<span class="dep-badge ${depDone ? 'dep-done' : 'dep-pending'}" title="${dep} ${depDone ? '已完成' : '未完成'}">${esc(dep)}</span>`;
}).join('');
const depsHtml = s.deps && s.deps.length
? `<span class="dep-section"><span class="dep-label">依赖:</span>${depBadges}</span>`
: '';
return `<li class="subtask-item ${stCls}" data-sid="${esc(s.sid)}">
<span class="sub-icon ${stCls}">${icon}</span>
<span class="sub-sid">${esc(s.sid)}</span>
<span class="sub-title">${esc(s.title)}</span>
<div class="sub-badges">${tierBadge}<span class="tag status-badge ${stCls}">${stLbl}</span>${depsHtml}</div>
</li>`;
}).join('\n');
return `<div class="subtask-block">
<div class="subtask-header">
<span class="subtask-label">子任务</span>
<span class="subtask-progress-text">${done} / ${total} 完成</span>
<div class="subtask-progress-bar"><div class="subtask-progress-fill" style="width:${pct}%"></div></div>
</div>
<ul class="subtask-list">${subItems}</ul>
</div>`;
}
function renderItems(items) {
if (!items.length) return '<p class="empty-tip">暂无条目</p>';
return items.map(i => {
@@ -149,27 +242,36 @@ function renderItems(items) {
const rejectBtn = i.status === 'done'
? `<button class="reject-btn" data-id="${i.id}" data-title="${esc(i.title)}">拒绝验收</button>`
: '';
const { done: subDone, total: subTotal } = subtasksDoneCount(i);
const subProgressBadge = subTotal > 0
? `<span class="tag sub-progress-badge">${subDone}/${subTotal} 子任务</span>`
: '';
const meta = `<div class="item-meta">
<span class="meta-date">🕐 ${fmtDate(i.created_at)}</span>
${i.status === 'accepted' ? `<span class="meta-date">✅ 验收 ${fmtDate(i.completed_at)}${i.version ? ' · <span class="ver-badge">'+esc(i.version)+'</span>' : ''}</span>` : ''}
</div>`;
const lvlCls = levelClass(i.level);
const subtasksBlock = renderSubtasks(i);
return `
<li class="todo-card ${stCls}"
data-id="${i.id}"
data-level="${esc(i.level)}"
data-status="${esc(i.status)}"
data-tier="${esc(i.tier ?? '')}"
data-tags="${esc((i.tags||[]).join(','))}">
<div class="card-header">
<span class="item-title">${esc(i.title)}</span>
<div class="card-badges">
<span class="tag status-badge ${stCls}">${stLbl}</span>
<span class="tag ${lvlCls}">${levelText(i.level)}</span>
${i.tier ? `<span class="tag tier-${i.tier}">${TIER_LABEL[i.tier]}</span>` : ''}
${subProgressBadge}
${rejectBtn}
</div>
</div>
${rejectNote}
${desc}
${subtasksBlock}
<div class="card-footer">
<div class="tag-row">${tags}</div>
${meta}
@@ -269,7 +371,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
padding: 14px 18px; margin-bottom: 10px;
border-left-width: 4px;
}
/* 卡片左边框颜色与状态匹配 */
.todo-card.s-open { border-left-color: #3b82f6; }
.todo-card.s-doing { border-left-color: #f97316; }
.todo-card.s-done { border-left-color: #eab308; }
@@ -297,6 +398,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
.t-tag { background: #efefef; color: #555; cursor: pointer; transition: .12s; }
.t-tag:hover { background: #d9e8fb; color: #1d4ed8; }
.ver-badge { font-weight: 700; color: #1f7a44; }
.sub-progress-badge { background: #f0f4ff; color: #3730a3; }
/* 状态徽章 */
.status-badge { font-size: 11.5px; }
@@ -310,6 +412,11 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
.empty-tip { font-size: 14px; color: #8aa3c4; margin: 8px 0 24px; }
.hidden { display: none !important; }
/* ── 改动等级徽章 ── */
.tier-1 { background: #ede9fe; color: #6d28d9; }
.tier-2 { background: #ccfbf1; color: #0f766e; }
.tier-3 { background: #f1f5f9; color: #475569; }
/* ── 拒绝按钮 & 拒绝原因 ── */
.reject-btn {
padding: 3px 10px; border-radius: 6px; border: 1.5px solid #dc2626;
@@ -324,6 +431,56 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
}
.reject-date { color: #ef9999; font-size: 12px; }
/* ── 子任务区 ── */
.subtask-block {
margin-top: 12px; padding: 10px 14px;
background: #f8fafc; border: 1px solid #e4e9ef; border-radius: 8px;
}
.subtask-header {
display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap;
}
.subtask-label {
font-size: 11px; font-weight: 700; color: #7b8794;
text-transform: uppercase; letter-spacing: .6px;
}
.subtask-progress-text { font-size: 12px; color: #52606d; }
.subtask-progress-bar {
flex: 1; height: 5px; background: #e4e9ef; border-radius: 3px; min-width: 60px;
}
.subtask-progress-fill { height: 100%; background: #22c55e; border-radius: 3px; transition: width .4s; }
.subtask-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.subtask-item {
display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
padding: 6px 10px; border-radius: 6px; font-size: 13px;
background: #fff; border: 1px solid #e8edf2;
}
.subtask-item.s-done { background: #f0fdf4; border-color: #bbf7d0; }
.subtask-item.s-doing { background: #fff7ed; border-color: #fed7aa; }
.subtask-item.s-accepted { background: #f0fdf4; border-color: #bbf7d0; opacity: .75; }
.sub-icon {
width: 16px; text-align: center; font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.sub-icon.s-done { color: #16a34a; }
.sub-icon.s-doing { color: #ea580c; }
.sub-icon.s-open { color: #93aec8; }
.sub-icon.s-accepted { color: #16a34a; }
.sub-sid {
font-family: "JetBrains Mono", monospace; font-size: 11.5px;
color: #52606d; font-weight: 700; min-width: 32px; flex-shrink: 0;
}
.sub-title { flex: 1; font-size: 13px; min-width: 120px; }
.subtask-item.s-done .sub-title,
.subtask-item.s-accepted .sub-title { text-decoration: line-through; color: #52606d; }
.sub-badges { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.dep-section { display: flex; align-items: center; gap: 3px; }
.dep-label { font-size: 11px; color: #8aa3c4; margin-right: 2px; }
.dep-badge {
padding: 1px 6px; border-radius: 8px; font-size: 11px;
font-weight: 700; font-family: monospace; cursor: default;
}
.dep-done { background: #dcfce7; color: #166534; }
.dep-pending { background: #fee2e2; color: #b91c1c; }
/* ── 拒绝 Modal ── */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.45);
@@ -392,6 +549,14 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<button class="filter-btn" data-filter-status="done">待验收</button>
<button class="filter-btn" data-filter-status="accepted">已验收</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">改动等级</span>
<button class="filter-btn active" data-filter-tier="all">全部</button>
<button class="filter-btn" data-filter-tier="1">一级</button>
<button class="filter-btn" data-filter-tier="2">二级</button>
<button class="filter-btn" data-filter-tier="3">三级</button>
</div>
${allTags.length ? `<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">平台 / 标签</span>
@@ -445,23 +610,25 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
(function() {
let curLevel = 'all';
let curStatus = 'all';
let curTier = 'all';
let curTags = new Set();
function applyFilter() {
document.querySelectorAll('.todo-card').forEach(card => {
const lvl = card.dataset.level;
const status = card.dataset.status;
const tier = card.dataset.tier;
const tags = card.dataset.tags ? card.dataset.tags.split(',') : [];
let show = true;
if (curLevel !== 'all' && lvl !== curLevel) show = false;
if (curStatus !== 'all' && status !== curStatus) show = false;
if (curTier !== 'all' && String(tier) !== curTier) show = false;
if (curTags.size > 0 && ![...curTags].some(t => tags.includes(t))) show = false;
card.classList.toggle('hidden', !show);
});
// 筛选后自动展开包含结果的区域
if (curStatus !== 'all') {
const wrap = document.getElementById('list-wrap-' + curStatus);
if (wrap) {
@@ -472,7 +639,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
}
}
// 重要度筛选
document.querySelectorAll('[data-filter-level]').forEach(btn => {
btn.addEventListener('click', () => {
curLevel = btn.dataset.filterLevel;
@@ -481,7 +647,14 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 状态筛选
document.querySelectorAll('[data-filter-tier]').forEach(btn => {
btn.addEventListener('click', () => {
curTier = btn.dataset.filterTier;
document.querySelectorAll('[data-filter-tier]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
document.querySelectorAll('[data-filter-status]').forEach(btn => {
btn.addEventListener('click', () => {
curStatus = btn.dataset.filterStatus;
@@ -490,7 +663,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 标签筛选(多选)
document.querySelectorAll('[data-filter-tag]').forEach(btn => {
btn.addEventListener('click', () => {
const t = btn.dataset.filterTag;
@@ -500,7 +672,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 卡片标签 → 快速筛选
document.querySelectorAll('.t-tag').forEach(tag => {
tag.addEventListener('click', e => {
e.stopPropagation();
@@ -510,7 +681,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
});
});
// 区域折叠切换
document.querySelectorAll('.section-title[data-toggle]').forEach(title => {
title.addEventListener('click', () => {
const st = title.dataset.toggle;
@@ -622,6 +792,8 @@ function cmdAdd(db, args) {
const level = ['high','mid','low'].includes(args.level) ? args.level : 'mid';
const tags = args.tags ? args.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
const desc = args.desc || null;
const tierRaw = parseInt(args.tier, 10);
const tier = [1,2,3].includes(tierRaw) ? tierRaw : null;
db.seq++;
const item = {
@@ -629,6 +801,7 @@ function cmdAdd(db, args) {
title,
desc,
level,
tier,
tags,
status: 'open',
created_at: now(),
@@ -640,11 +813,12 @@ function cmdAdd(db, args) {
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
console.log(`✅ 已添加 #${item.id} [${levelText(level)}] ${title}${tags.length ? ' ' + tags.join('/') : ''}`);
const tierStr = tier ? '·' + TIER_LABEL[tier] : '';
console.log(`✅ 已添加 #${item.id} [${levelText(level)}${tierStr}] ${title}${tags.length ? ' ' + tags.join('/') : ''}`);
printSummary(db);
}
/** status <id> <open|doing|done> — Claude 可调用,accepted 由用户手动设置 */
/** status <id> <open|doing|done> */
function cmdStatus(db, args) {
const id = args._[0];
const newStatus = args._[1];
@@ -658,9 +832,17 @@ function cmdStatus(db, args) {
process.exit(1);
}
const item = findItem(db, id);
item.status = newStatus;
item.done = false;
const item = findItem(db, id);
// 有未完成子任务时,禁止手动将父任务标记为 done
if (newStatus === 'done' && !allSubtasksDone(item) && item.subtasks && item.subtasks.length > 0) {
const { done: doneCount, total } = subtasksDoneCount(item);
console.error(`❌ 父任务还有未完成的子任务(${doneCount}/${total} 完成),请先通过 sub status 完成所有子任务`);
process.exit(1);
}
item.status = newStatus;
item.done = false;
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
@@ -670,11 +852,17 @@ function cmdStatus(db, args) {
printSummary(db);
}
/** done <id> — 标记已验收交付(仅用户调用),记录版本号 */
/** done <id> [--version vX.Y.Z] */
function cmdDone(db, args) {
const id = args._[0];
const item = findItem(db, id);
// 有未完成子任务时提示警告(但允许用户强制验收)
if (item.subtasks && item.subtasks.length > 0 && !allSubtasksDone(item)) {
const { done: doneCount, total } = subtasksDoneCount(item);
console.warn(`⚠️ 注意:还有未完成的子任务(${doneCount}/${total} 完成),已强制标记为验收`);
}
let version = args.version || null;
if (!version) {
version = gitTag();
@@ -693,7 +881,7 @@ function cmdDone(db, args) {
printSummary(db);
}
/** reject <id> --reason "原因" — 拒绝验收,退回 open 并升级为 high(仅 done 状态可用) */
/** reject <id> --reason "原因" */
function cmdReject(db, args) {
const id = args._[0];
const reason = (args.reason || '').trim();
@@ -709,13 +897,13 @@ function cmdReject(db, args) {
process.exit(1);
}
const prevLevel = item.level;
item.status = 'open';
item.done = false;
item.level = 'high';
const prevLevel = item.level;
item.status = 'open';
item.done = false;
item.level = 'high';
item.reject_reason = reason;
item.rejected_at = now();
item.version = null;
item.rejected_at = now();
item.version = null;
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
@@ -767,6 +955,93 @@ function cmdRender(db) {
console.log('🖨️ todo/todo.html 已重新渲染');
}
// ─── 子任务命令 ───────────────────────────────────────────────────────────────
/** sub add <parent_id> --title "..." [--tier 1|2|3] [--deps "21A,21B"] */
function cmdSubAdd(db, args) {
const parentIdRaw = args._[0];
const title = (args.title || '').trim();
if (!parentIdRaw) { console.error('错误:请指定父任务 id,例如:sub add 21 --title "..."'); process.exit(1); }
if (!title) { console.error('错误:--title 不能为空'); process.exit(1); }
const parent = findItem(db, parentIdRaw);
if (!parent.subtasks) parent.subtasks = [];
const letter = nextSubtaskLetter(parent);
if (!letter) { console.error('错误:子任务数量已达上限(26)'); process.exit(1); }
const sid = `${parent.id}${letter}`;
const tierRaw = parseInt(args.tier, 10);
const tier = [1,2,3].includes(tierRaw) ? tierRaw : null;
const deps = args.deps
? args.deps.split(',').map(d => d.trim().toUpperCase()).filter(Boolean)
: [];
// 校验依赖的 sid 是否存在
for (const dep of deps) {
const exists = parent.subtasks.find(s => s.sid === dep);
if (!exists) { console.error(`错误:依赖的子任务 ${dep} 不存在`); process.exit(1); }
}
const sub = { sid, title, tier, deps, status: 'open', created_at: now() };
parent.subtasks.push(sub);
// 有子任务的父任务自动进入 doing
if (parent.status === 'open') {
parent.status = 'doing';
}
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
const tierStr = tier ? ` [${TIER_LABEL[tier]}]` : '';
const depsStr = deps.length ? ` 依赖: ${deps.join(', ')}` : '';
console.log(`✅ 已添加子任务 ${sid}${tierStr}${title}」→ #${parent.id}${parent.title}${depsStr}`);
printSummary(db);
}
/** sub status <sid> <open|doing|done> */
function cmdSubStatus(db, args) {
const sid = (args._[0] || '').toUpperCase();
const newStatus = args._[1];
if (!['open','doing','done'].includes(newStatus)) {
console.error(`错误:无效状态 "${newStatus}",可选:open | doing | done`);
process.exit(1);
}
const { parent, sub } = findSubtask(db, sid);
sub.status = newStatus;
// 所有子任务完成时自动将父任务推进为 done(待验收)
if (newStatus === 'done' && allSubtasksDone(parent)) {
parent.status = 'done';
parent.done = false;
console.log(`🎉 所有子任务均已完成,父任务 #${parent.id}${parent.title}」自动转为「待验收」`);
}
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
const emoji = { open: '📋', doing: '🔨', done: '🔍' }[newStatus];
console.log(`${emoji} ${sub.sid}${sub.title}」→ ${STATUS_LABEL[newStatus]}`);
printSummary(db);
}
/** sub <subcmd> ... */
function cmdSub(db, args) {
const subCmd = args._[0];
const subArgs = { ...args, _: args._.slice(1) };
switch (subCmd) {
case 'add': cmdSubAdd(db, subArgs); break;
case 'status': cmdSubStatus(db, subArgs); break;
default:
console.error(`未知子命令:sub ${subCmd}\n用法:\n node todo.mjs sub add <parent_id> --title "..." [--tier N] [--deps "21A,21B"]\n node todo.mjs sub status <sid> <open|doing|done>`);
process.exit(1);
}
}
// ─── 入口 ────────────────────────────────────────────────────────────────────
const [,, subcmd, ...rest] = process.argv;
@@ -780,11 +1055,12 @@ switch (subcmd) {
case 'reject': cmdReject(db, args); break;
case 'reopen': cmdReopen(db, args); break;
case 'rm': cmdRm(db, args); break;
case 'sub': cmdSub(db, args); break;
case 'list':
case undefined:
case '': cmdList(db); break;
case 'render': cmdRender(db); break;
default:
console.error(`未知子命令:${subcmd}\n用法:node todo.mjs add|status|done|reject|reopen|rm|list|render`);
console.error(`未知子命令:${subcmd}\n用法:node todo.mjs add|status|done|reject|reopen|rm|sub|list|render`);
process.exit(1);
}