feat(todo): 新增 /todo slash command,支持 add/done/list/reopen/rm

- todo/todo.json:结构化 JSON 真相源(id/标题/重要度3级/平台标签/版本)
- todo/todo.mjs:Node 零依赖单文件 CLI,写操作自动重渲染 HTML
- todo/todo.html:重新设计布局(未完成置顶/已完成置底/sticky 筛选栏)
- .claude/commands/todo.md:/todo slash command
- CLAUDE.md:追加 TODO 管理规范(唯一真相源、完成即标记、禁止使用其它 todo 工具)

旧 todo.html 备份为 todo.legacy.html;存量 6 条待办已导入 todo.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-08 01:31:41 +08:00
parent 46304ab961
commit e124ed7c65
6 changed files with 1305 additions and 285 deletions
+67
View File
@@ -0,0 +1,67 @@
---
description: 项目 TODO 管理(list / add / done / reopen / rm
argument-hint: "list | add <描述> | done <id> [version] | reopen <id> | rm <id>"
model: claude-sonnet-4-6
allowed-tools: Bash, Read
---
管理项目 `todo/todo.json` 待办清单,所有写操作会自动重渲染 `todo/todo.html`
**重要规则**:所有待办项必须通过本命令管理,禁止使用其它 todo 工具(TaskCreate/TodoWrite 等)。
---
根据 `$ARGUMENTS` 分派,缺省等同 `list`
## list(或无参数)
运行以下命令并将终端 summary 转述给用户,同时提示 HTML 路径便于浏览:
```bash
node todo/todo.mjs list
```
## add <描述>
`$ARGUMENTS` 去掉首词 `add` 后的内容解析为待办描述,推断:
- `--title`:核心一句话标题(简洁,去掉平台/类别信息)
- `--level``high`(阻断/紧急)/ `mid`(重要,默认)/ `low`(一般/优化)
- 关键词「紧急」「阻断」「必须」「严重」→ high;「重要」「需要」→ mid;其余 → low
- `--tags`:从描述推断平台/类别,可多个逗号分隔,常用值:`前端,后端,Web,mac,Windows,Android,iOS,数据库,CI/CD,文档`
- `--desc`:补充说明(可选,如有具体文件/路径/上下文则填入)
然后运行:
```bash
node todo/todo.mjs add --title "..." --level mid --tags "前端,Web" --desc "..."
```
转述:「已添加 #id [级别] 标题 标签」,并显示 summary。
## done <id> [version]
`$ARGUMENTS` 提取 id(数字)和可选 version(格式 vX.Y.Z):
- 有 version`node todo/todo.mjs done <id> --version <version>`
- 无 version`node todo/todo.mjs done <id>`(脚本自动用 `git describe --tags --abbrev=0`
转述:「#id 标记完成,记入版本 vX.Y.Z」,并显示 summary。
## reopen <id>
```bash
node todo/todo.mjs reopen <id>
```
转述「#id 已重新开启」。
## rm <id>
```bash
node todo/todo.mjs rm <id>
```
转述「#id 已删除」。
---
所有命令完成后,在终端显示来自脚本的完整 summary(共/未完成/已完成及分级列表)。
+22
View File
@@ -308,6 +308,28 @@ CI/CDForgejo`.gitea/workflows/deploy.yml`)收到 tag 后自动:编译
}
```
### 项目 TODO 管理
**唯一待办系统**:任务执行过程中如发现需要做的事项,**必须**用 `/todo add <描述>` 记入项目 todo 系统,**禁止**使用 TaskCreate/TodoWrite 等其它 todo 工具,保持单一真相源。
- **真相源**`todo/todo.json`(结构化 JSON,含 id/标题/重要度/平台标签/创建时间/完成版本)
- **HTML**`todo/todo.html` 为生成物,每次写操作自动重渲染
- **重要度**`high`(阻断/紧急)/ `mid`(重要)/ `low`(一般/优化)三级
- **完成即标记**:完成对应 todo 后运行 `/todo done <id>`,脚本自动记入当前 git tag 版本
常用命令:
```
/todo — 查看 summary(含未完成分级列表)
/todo add 修复... — 新增待办(Claude 自动推断级别/标签)
/todo done <id> — 标记完成,自动记入版本号
/todo reopen <id> — 撤销完成
/todo rm <id> — 删除
```
底层脚本:`node todo/todo.mjs <子命令> [参数]`
---
### 搜索实现(拼音)
商品名搜索同时支持汉字、全拼、首字母:
+292 -285
View File
@@ -3,326 +3,333 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web 站点问题清单 / TODO · 岩美</title>
<title>酒库管理系统 — 项目 TODO</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0; padding: 0 0 80px;
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #1f2933; background: #f7f9fb; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
header {
background: linear-gradient(135deg, #0A1F3B 0%, #15407D 100%);
color: #fff; padding: 40px 32px 32px;
}
header h1 { margin: 0 0 8px; font-size: 26px; font-weight: 700; }
header p { margin: 0; color: #ADC9EA; font-size: 14px; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 0 24px; }
.meta { margin-top: 16px; font-size: 13px; color: #8aa3c4; }
* { box-sizing: border-box; }
body {
margin: 0; padding: 0 0 80px;
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #1f2933; background: #f7f9fb; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
header {
background: linear-gradient(135deg, #0A1F3B 0%, #15407D 100%);
color: #fff; padding: 36px 32px 28px;
}
header h1 { margin: 0 0 6px; font-size: 24px; font-weight: 700; }
header .header-meta { color: #ADC9EA; font-size: 13px; margin-top: 4px; }
.stats { display: flex; gap: 16px; margin-top: 14px; }
.stat-pill {
background: rgba(255,255,255,0.12); border-radius: 20px;
padding: 4px 14px; font-size: 13px; color: #fff;
}
.stat-pill strong { font-size: 18px; font-weight: 700; margin-right: 2px; }
.legend { display: flex; flex-wrap: wrap; gap: 10px; margin: 24px auto 8px; max-width: 1040px; padding: 0 24px; }
.tag { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.t-block { background: #fde8e8; color: #b91c1c; } /* 阻断/必须改 */
.t-high { background: #fef0d8; color: #9a6700; } /* 重要 */
.t-low { background: #e6f0fb; color: #1d4ed8; } /* 一般/优化 */
.t-known { background: #ececec; color: #555; } /* 已知占位 */
.t-ver { background: #e3f5ea; color: #1f7a44; } /* 已核实 */
.t-done { background: #d1f0dc; color: #1f7a44; } /* 已完成 */
/* ── 筛选栏 ── */
.filter-bar {
position: sticky; top: 0; z-index: 10;
background: #fff; border-bottom: 1px solid #e4e9ef;
padding: 10px 28px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
}
.filter-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.filter-label { font-size: 12px; color: #7b8794; white-space: nowrap; }
.filter-chip, .filter-btn {
padding: 3px 12px; border-radius: 14px; border: 1.5px solid #d1d9e6;
background: #f7f9fb; color: #52606d; font-size: 12.5px; cursor: pointer;
transition: all .15s; white-space: nowrap;
}
.filter-chip:hover, .filter-btn:hover { border-color: #2563AC; color: #2563AC; }
.filter-chip.active, .filter-btn.active {
background: #2563AC; border-color: #2563AC; color: #fff;
}
.filter-sep { width: 1px; height: 20px; background: #e4e9ef; margin: 0 4px; }
section { max-width: 1040px; margin: 28px auto 0; padding: 0 24px; }
h2 {
font-size: 18px; font-weight: 700; color: #0A1F3B;
border-left: 4px solid #2563AC; padding-left: 12px; margin: 0 0 14px;
}
ul.todo { list-style: none; margin: 0; padding: 0; }
ul.todo li {
background: #fff; border: 1px solid #e4e9ef; border-radius: 8px;
padding: 14px 16px; margin-bottom: 10px;
display: grid; grid-template-columns: 22px 1fr; gap: 12px; align-items: start;
}
ul.todo li:has(input:checked) { background: #f3faf5; border-color: #b7e4c7; opacity: 0.7; }
ul.todo li:has(input:checked) .item-title { color: #52606d; text-decoration: line-through; }
ul.todo li input[type=checkbox] { margin-top: 4px; width: 16px; height: 16px; }
.item-title { font-weight: 600; font-size: 15px; }
.item-title .tag { margin-left: 8px; vertical-align: middle; }
.item-desc { font-size: 13.5px; color: #52606d; margin-top: 4px; }
.item-desc code {
background: #f0f2f5; padding: 1px 5px; border-radius: 3px;
font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: #b91c1c;
}
.file { font-family: "JetBrains Mono", monospace; font-size: 12px; color: #7b8794; }
.note { background: #fffbe9; border:1px solid #f0e3b0; border-radius:8px; padding:12px 16px; font-size:13px; color:#6b5b1d; margin-top:14px; }
/* ── 内容区 ── */
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px; }
.section-title {
font-size: 16px; font-weight: 700; color: #0A1F3B;
border-left: 4px solid #2563AC; padding-left: 12px;
margin: 28px 0 12px;
}
.section-title.done-title { border-color: #6cbb8a; }
ul.todo-list { list-style: none; margin: 0; padding: 0; }
.todo-card {
background: #fff; border: 1px solid #e4e9ef; border-radius: 10px;
padding: 14px 18px; margin-bottom: 10px;
}
.todo-card.done { background: #f3faf5; border-color: #b7e4c7; opacity: .75; }
.card-header { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
.item-title { font-weight: 600; font-size: 15px; flex: 1; }
.todo-card.done .item-title { text-decoration: line-through; color: #52606d; }
.item-desc { font-size: 13.5px; color: #52606d; margin-top: 6px; }
.item-desc code {
background: #f0f2f5; padding: 1px 5px; border-radius: 3px;
font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: #b91c1c;
}
.card-footer { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; }
.item-meta { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; color: #8aa3c4; }
.meta-date { white-space: nowrap; }
/* ── tags ── */
.tag { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.t-block { background: #fde8e8; color: #b91c1c; }
.t-high { background: #fef0d8; color: #9a6700; }
.t-low { background: #e6f0fb; color: #1d4ed8; }
.t-tag { background: #efefef; color: #555; cursor: pointer; transition: .12s; }
.t-tag:hover { background: #d9e8fb; color: #1d4ed8; }
.ver-badge { font-weight: 700; color: #1f7a44; }
/* ── 折叠 ── */
.done-section { margin-bottom: 24px; }
.done-toggle {
background: none; border: 1.5px solid #d1d9e6; border-radius: 6px;
padding: 4px 14px; cursor: pointer; font-size: 13px; color: #52606d;
margin-bottom: 10px; transition: .15s;
}
.done-toggle:hover { border-color: #6cbb8a; color: #1f7a44; }
.empty-tip { font-size: 14px; color: #8aa3c4; margin: 8px 0 24px; }
/* 隐藏 */
.hidden { display: none !important; }
</style>
</head>
<body>
<header>
<div class="wrap">
<h1>Web 营销站点 — 问题清单 / TODO</h1>
<p>检查范围:产品介绍(首页)· 功能详情页 · 价格 · 下载 · 文档 · 注册 · 页眉页脚</p>
<div class="meta">生成日期 2026-06-07 · 对照后端实现核对 · 代码版本 v1.0.20</div>
<h1>酒库管理系统 — 项目 TODO</h1>
<div class="header-meta">生成于 2026-06-08 · 真相源 todo/todo.json</div>
<div class="stats">
<div class="stat-pill"><strong>6</strong>全部</div>
<div class="stat-pill"><strong>6</strong>未完成</div>
<div class="stat-pill"><strong>0</strong>已完成</div>
</div>
</div>
</header>
<div class="legend">
<span class="tag t-block">阻断 必须改</span>
<span class="tag t-high">重要</span>
<span class="tag t-low">一般/优化</span>
<span class="tag t-known">已知占位(暂留)</span>
<span class="tag t-ver">已核实代码</span>
<span class="tag t-done">已完成</span>
<div class="filter-bar" id="filterBar">
<div class="filter-group">
<span class="filter-label">重要</span>
<button class="filter-btn active" data-filter-level="all">全部</button>
<button class="filter-btn" data-filter-level="high">高优</button>
<button class="filter-btn" data-filter-level="mid">重要</button>
<button class="filter-btn" data-filter-level="low">一般</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">状态</span>
<button class="filter-btn active" data-filter-done="all">全部</button>
<button class="filter-btn" data-filter-done="0">未完成</button>
<button class="filter-btn" data-filter-done="1">已完成</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">平台 / 标签</span>
<button class="filter-chip" data-filter-tag="Web">Web</button><button class="filter-chip" data-filter-tag="前端">前端</button>
</div>
</div>
<!-- ============ 虚假/夸大宣传(最优先,已核实代码) ============ -->
<section>
<h2>一、功能宣传与实际实现不符(已对照后端代码核实</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">「红冲入库 / 红冲出库」未实现 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端 <code>internal/</code> 全局无任何红冲 / reversal / 冲销逻辑,但首页 FAQ「审批通过后发现错误」与 <span class="file">features/approval.html</span> 第 5 块(红冲 / 净额演示)大篇幅宣传。属虚假宣传,需删除或落地功能。</div>
<div class="wrap">
<div class="section-title">📋 未完成(6</div>
<ul class="todo-list" id="pendingList">
<li class="todo-card"
data-id="2"
data-level="high"
data-tags="Web"
data-done="0">
<div class="card-header">
<span class="item-title">备案号为占位假值</span>
<span class="tag t-block">高优 · 紧急</span>
</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">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「调拨单」未实现 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端无 transfer / 调拨 任何实现。<span class="file">features/approval.html</span> 状态机把「调拨单」与入库/出库/盘点并列为四大单据。需移除调拨相关展示。</div>
<li class="todo-card"
data-id="1"
data-level="mid"
data-tags="前端,Web"
data-done="0">
<div class="card-header">
<span class="item-title">两套 CSS / 设计令牌并存</span>
<span class="tag t-high">重要</span>
</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">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「库存预警 / 阈值推送」严重夸大 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端仅有 <code>min_stock</code> 单一字段用于显示「偏低/告急」标记,<b>没有</b>任何告警、推送、按 SKU 多级阈值配置、按操作员分发的能力。但首页「库存预警实时提醒」与 <span class="file">features/inventory.html</span> 整块「预警阈值配置 / 推送给张磊 / 短信通知李建国」均为虚构界面。需大幅收敛文案。</div>
<li class="todo-card"
data-id="3"
data-level="low"
data-tags="前端,Web"
data-done="0">
<div class="card-header">
<span class="item-title">注册接口同源依赖问题</span>
<span class="tag t-low">一般 / 优化</span>
</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">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「短信通知 / 邮件提醒」未实现 <span class="tag t-high">重要</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端无 SMS / 邮件发送能力。inventory/approval 页多处「短信通知」「邮件提醒」需删除。</div>
<li class="todo-card"
data-id="4"
data-level="low"
data-tags="前端,Web"
data-done="0">
<div class="card-header">
<span class="item-title">scan.html / features 走 passthrough 不享模板</span>
<span class="tag t-low">一般 / 优化</span>
</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">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「过期预警 / 保质期推算」未实现 <span class="tag t-high">重要</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端 expire 相关仅为「license 授权到期」,无商品保质期/过期预警。<span class="file">features/inventory.html</span> 批次块的「过期预警」与阈值卡「临近过期 30 天邮件提醒」为虚构。</div>
<li class="todo-card"
data-id="5"
data-level="low"
data-tags="前端,Web"
data-done="0">
<div class="card-header">
<span class="item-title">页脚双份维护</span>
<span class="tag t-low">一般 / 优化</span>
</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">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「智能补货建议 v1.7 Beta」「SSO 单点登录」未实现 <span class="tag t-low">一般</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">补货建议(inventory 页标 v1.7 Beta)、价格连锁版的「SSO 单点登录」后端均无。若作为路线图保留需明确标注「规划中」,否则移除。</div>
<li class="todo-card"
data-id="6"
data-level="low"
data-tags="前端,Web"
data-done="0">
<div class="card-header">
<span class="item-title">注册成功提示路径需核对</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「允许超量出库」可配置开关 待核实 <span class="tag t-low">一般</span></div>
<div class="item-desc">approval 页声称系统设置可开启「允许超量出库」,后端未检索到 oversell/超量 配置项,需确认是否真有该设置。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">营销数据疑似杜撰 <span class="tag t-high">重要</span></div>
<div class="item-desc">首页 Trust 区「1,280+ 门店 / 8.4% 损耗下降 / 99.9% 可用性」无数据支撑;功能页「14 个审计字段」「审批延迟 ≤1s」等均需核实,否则改为中性表述。</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">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
</ul>
<div class="note">✅ 已核实<b>确实存在</b>的功能(文案无需改):FIFO 批次扣减出库(<code>stock.go ApproveStockOut</code>)、多仓库(<code>warehouse</code>)、库存盘点、入库/出库审批联动库存+应付应收、商品 <code>min_stock</code> 状态标记、拼音搜索、扫码验真(scan.html)。</div>
</section>
<!-- ============ 版本/品牌一致性 ============ -->
<section>
<h2>二、版本号与品牌/域名一致性</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">首页 Hero 版本号造假 <span class="tag t-block">阻断</span></div>
<div class="item-desc">首页徽标硬编码「v1.6 · 新增多仓库联动盘点」,inventory 页又写「v1.6.2」,而真实版本为 <b>v1.0.20</b>(CHANGELOG)。需统一为真实版本,最好像下载页一样从 <code>changelog</code> 数据动态取。<span class="file">index.njk:180-183</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">品牌域名三处不统一 <span class="tag t-high">重要</span></div>
<div class="item-desc">首页预览写 <code>yanmei.app/inventory</code>、客服邮箱 <code>support@yanmei.app</code>,而真实应用域名是 <code>jiu.51yanmei.com</code>。需统一域名口径。<span class="file">index.njk:201 · _data/site.json</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">下载页 API 域名硬编码 <span class="tag t-low">一般</span></div>
<div class="item-desc"><code>https://jiu.51yanmei.com</code> 直接写死在 <span class="file">download.njk</span> 的 JS 里(释放接口、各端下载、Web 入口),应抽到 <code>_data/site.json</code> 统一管理,便于换域名/环境。</div>
</div>
</li>
</ul>
</section>
<div class="done-section">
<div class="section-title done-title">✅ 已完成(0</div>
<button class="done-toggle" id="doneToggle">▾ 展开已完成列表</button>
<ul class="todo-list hidden" id="doneList">
<p class="empty-tip">暂无条目</p>
</ul>
</div>
<!-- ============ 页眉 ============ -->
<section>
<h2>三、页眉(导航)</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">功能详情页死链:<code>/download.html</code> <span class="tag t-block">阻断</span></div>
<div class="item-desc">两个功能页(手写 HTML)导航和页脚都链向 <code>/download.html</code><code>/docs.html</code>,但 Eleventy 实际产出 pretty URL <code>/download/</code><code>/docs/</code>。dist 中无 <code>download.html</code> → 死链。统一改为 <code>/download/</code><code>/docs/</code><span class="file">features/inventory.html:660-661 · approval.html 同</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">功能详情页未进主导航(孤岛页面) <span class="tag t-high">重要</span></div>
<div class="item-desc"><span class="file">features/inventory.html</span><span class="file">approval.html</span> 制作精良,但主站导航「产品」只跳首页锚点 <code>/#modules</code>,这两页仅能从页脚或彼此跳转进入。建议在导航或首页模块卡加「了解更多」入口。</div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">两套 CSS / 设计令牌并存 <span class="tag t-high">重要</span></div>
<div class="item-desc">主站 njk 用 <code>color.css + style.css</code>,功能页用 <code>colors_and_type.css</code>,导航/页脚样式各写一份。维护分裂、易不一致,建议合并到一套并让功能页也走 base.njk 模板。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">注册入口不一致 <span class="tag t-low">一般</span></div>
<div class="item-desc">功能页 JS 未登录态指向 <code>/app/login</code><code>/app/register</code>Flutter 内路由),而 njk 站点有独立 web 注册页 <code>/register/</code>。两套注册入口口径不一,需统一。</div>
</div>
</li>
</ul>
</section>
</div>
<!-- ============ 页脚 ============ -->
<section>
<h2>四、页脚</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">大量占位死链 <code>href="#"</code> <span class="tag t-high">重要</span></div>
<div class="item-desc">页脚以下链接全部为空锚点,需逐一落地或移除:API 文档、视频教程、数据导入模板、提交工单、技术支持、服务等级、关于岩美、客户案例、招贤纳士、联系我们、版本更新。<span class="file">footer.njk · features/*.html</span></div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">备案号为占位假值 <span class="tag t-known">已知占位</span></div>
<div class="item-desc">现为 <code>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</code>(全 0)。按要求暂填,<b>上线前必须替换为真实备案号</b>(且 ICP 主体地需与实际服务器/主体一致,岩美若在沪外需核对)。<span class="file">_data/site.json:5</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">客服邮箱/电话为占位 <span class="tag t-high">重要</span></div>
<div class="item-desc">邮箱 <code>support@yanmei.app</code>(与真实域名不符)、电话 <code>400-880-8888</code>(明显假号)。需换成真实联系方式。<span class="file">_data/site.json:7-8</span></div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">页脚双份维护 <span class="tag t-low">一般</span></div>
<div class="item-desc">njk 站点用 <code>footer.njk</code>(读 site.json 变量),功能页则把页脚、备案、联系方式硬编码各写一份,改一处不同步。根因同「两套模板」,建议功能页接入 base.njk。</div>
</div>
</li>
</ul>
</section>
<script>
(function() {
// 筛选状态
let curLevel = 'all';
let curDone = 'all';
let curTags = new Set();
<!-- ============ 价格 ============ -->
<section>
<h2>五、价格</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">价格数字需确认是否真实对外 <span class="tag t-high">重要</span></div>
<div class="item-desc">单店版 <code>¥299/月</code>、年付 <code>¥2,988/年</code>、连锁版「定制」疑为占位。上线前需确认真实报价。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「购买 / 联系销售 / 预约演示」按钮无对应流程 <span class="tag t-high">重要</span></div>
<div class="item-desc">价格卡「购买」「联系销售」、Hero「观看演示」、CTA「预约 1 对 1 演示」全部指向 <code>/app/</code>,但并无支付、销售联系、演示预约流程。按钮文案与去向不符。<span class="file">index.njk:554,568,188,622</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「试用版」与「单店版」文案口径混乱 <span class="tag t-low">一般</span></div>
<div class="item-desc">价格区把「试用版(¥0/30天)」与「单店版(¥299)」分列两卡,而 FAQ 又称「标准版提供 30 天免费试用」。试用 vs 标准 的口径需统一。</div>
</div>
</li>
</ul>
</section>
function applyFilter() {
document.querySelectorAll('.todo-card').forEach(card => {
const lvl = card.dataset.level;
const dn = card.dataset.done;
const tags = card.dataset.tags ? card.dataset.tags.split(',') : [];
<!-- ============ 下载 ============ -->
<section>
<h2>六、下载页</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">下载按钮失败无降级 <span class="tag t-high">重要</span></div>
<div class="item-desc">Windows/macOS/Android 按钮初始 <code>href="#"</code>,依赖运行时 fetch <code>/api/v1/public/release</code> 回填真实下载地址;接口失败 / 无数据时点击无任何反应、也无提示。需加失败兜底(如提示「暂未就绪」或禁用态)。<span class="file">download.njk:500-543</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">iOS 状态依赖接口字段 <span class="tag t-low">一般</span></div>
<div class="item-desc">iOS 卡片仅当接口返回 <code>download_urls.ios</code> 才从「敬请期待」切换为「TestFlight 安装」。需确认 TestFlight 公开链接是否已配到 release 接口,否则 iOS 永远显示敬请期待(与首页「五端就绪」矛盾)。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">首页「五端就绪」与下载页 iOS 状态矛盾 <span class="tag t-high">重要</span></div>
<div class="item-desc">首页平台区把 Web/Win/macOS/iOS/Android 全列为可用,下载页 iOS 仍是「敬请期待」。需对齐口径。</div>
</div>
</li>
</ul>
</section>
let show = true;
if (curLevel !== 'all' && lvl !== curLevel) show = false;
if (curDone !== 'all' && dn !== curDone) show = false;
if (curTags.size > 0 && ![...curTags].some(t => tags.includes(t))) show = false;
<!-- ============ 文档 ============ -->
<section>
<h2>七、文档页</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title"><code>⌘K</code> 搜索快捷键是装饰 <span class="tag t-low">一般</span></div>
<div class="item-desc">文档顶栏显示 <code>⌘K</code> 徽标但未绑定键盘事件聚焦搜索框;搜索本身仅按整章显隐,无关键词高亮/定位。要么实现快捷键,要么去掉徽标。<span class="file">docs.njk</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">反馈接口路径不一致 / 待确认 <span class="tag t-low">一般</span></div>
<div class="item-desc">文档反馈用相对路径 <code>/api/v1/public/feedback</code>,下载页却用绝对域名。需确认该端点真实存在且同源可达。<span class="file">docs.njk:182</span></div>
</div>
</li>
</ul>
</section>
card.classList.toggle('hidden', !show);
});
}
<!-- ============ 注册 ============ -->
<section>
<h2>八、注册页</h2>
<ul class="todo">
<li><input type="checkbox" />
<div>
<div class="item-title">注册接口同源依赖 <span class="tag t-low">一般</span></div>
<div class="item-desc"><code>window.location.origin + '/api/v1/public/register'</code>,要求静态站与后端同源部署;本地预览或分离部署时会指错。需确认部署形态(与下载页写死域名的做法也不统一)。<span class="file">register.njk:175</span></div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">成功提示路径需核对 <span class="tag t-low">一般</span></div>
<div class="item-desc">提示「忘记门店编码可在『系统设置 → 关于』查看」,需与实际客户端菜单路径一致。</div>
</div>
</li>
</ul>
</section>
// 重要度
document.querySelectorAll('[data-filter-level]').forEach(btn => {
btn.addEventListener('click', () => {
curLevel = btn.dataset.filterLevel;
document.querySelectorAll('[data-filter-level]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
<!-- ============ 构建/工程 ============ -->
<section>
<h2>九、构建 / 工程结构</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">dist 残留旧产物 <code>docs.html</code> <span class="tag t-low">一般</span></div>
<div class="item-desc">dist 同时存在 <code>docs.html</code>(疑似旧构建残留)与 <code>docs/index.html</code>,造成同内容双 URL。建议清理 dist 后全量重建,避免历史残留。</div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">scan.html / features 走 passthrough,不享模板 <span class="tag t-low">一般</span></div>
<div class="item-desc"><code>.eleventy.js</code> 把 features、scan.html 直接复制,不经 njk 模板,导致页脚、备案、域名、CSS 都得手动同步——是上面多个「双份维护」问题的工程根因。建议长期改造为模板化。</div>
</div>
</li>
</ul>
</section>
// 状态
document.querySelectorAll('[data-filter-done]').forEach(btn => {
btn.addEventListener('click', () => {
curDone = btn.dataset.filterDone;
document.querySelectorAll('[data-filter-done]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
// 标签(多选)
document.querySelectorAll('[data-filter-tag]').forEach(btn => {
btn.addEventListener('click', () => {
const t = btn.dataset.filterTag;
if (curTags.has(t)) { curTags.delete(t); btn.classList.remove('active'); }
else { curTags.add(t); btn.classList.add('active'); }
applyFilter();
});
});
// 点击卡片标签 → 快速筛选
document.querySelectorAll('.t-tag').forEach(tag => {
tag.addEventListener('click', e => {
e.stopPropagation();
const t = tag.dataset.tag;
const chip = document.querySelector('[data-filter-tag="'+t+'"]');
if (chip) chip.click();
});
});
// 已完成折叠
const toggle = document.getElementById('doneToggle');
const doneList = document.getElementById('doneList');
if (toggle && doneList) {
toggle.addEventListener('click', () => {
const collapsed = doneList.classList.toggle('hidden');
toggle.textContent = collapsed ? '▾ 展开已完成列表' : '▴ 收起已完成列表';
});
}
})();
</script>
</body>
</html>
</html>
+92
View File
@@ -0,0 +1,92 @@
{
"meta": {
"title": "酒库管理系统 — 项目 TODO",
"updated_at": "2026-06-07T17:31:30.449Z"
},
"seq": 7,
"items": [
{
"id": 1,
"title": "两套 CSS / 设计令牌并存",
"desc": "主站 njk 用 color.css+style.css,功能页用 colors_and_type.css,导航/页脚各写一份,维护分裂。建议合并到一套,功能页走 base.njk 模板。",
"level": "mid",
"tags": [
"前端",
"Web"
],
"created_at": "2026-06-07T17:30:32.138Z",
"done": false,
"completed_at": null,
"version": null
},
{
"id": 2,
"title": "备案号为占位假值",
"desc": "现为沪ICP备2026000000号(全0),上线前必须替换真实备案号。",
"level": "high",
"tags": [
"Web"
],
"created_at": "2026-06-07T17:30:32.188Z",
"done": false,
"completed_at": null,
"version": null
},
{
"id": 3,
"title": "注册接口同源依赖问题",
"desc": "用 window.location.origin+'/api/v1/public/register',要求同源部署;分离部署时指错。需确认部署形态。",
"level": "low",
"tags": [
"前端",
"Web"
],
"created_at": "2026-06-07T17:30:32.237Z",
"done": false,
"completed_at": null,
"version": null
},
{
"id": 4,
"title": "scan.html / features 走 passthrough 不享模板",
"desc": "features/scan.html 直接复制,不经 njk 模板,页脚/备案/CSS 都得手动同步。建议长期改造为模板化。",
"level": "low",
"tags": [
"前端",
"Web"
],
"created_at": "2026-06-07T17:30:32.287Z",
"done": false,
"completed_at": null,
"version": null
},
{
"id": 5,
"title": "页脚双份维护",
"desc": "njk 站点用 footer.njk 读变量,功能页把页脚硬编码各写一份,改一处不同步。根因同「两套模板」。",
"level": "low",
"tags": [
"前端",
"Web"
],
"created_at": "2026-06-07T17:30:32.334Z",
"done": false,
"completed_at": null,
"version": null
},
{
"id": 6,
"title": "注册成功提示路径需核对",
"desc": "提示「忘记门店编码可在「系统设置→关于」查看」,需与实际客户端菜单路径一致。",
"level": "low",
"tags": [
"前端",
"Web"
],
"created_at": "2026-06-07T17:30:32.381Z",
"done": false,
"completed_at": null,
"version": null
}
]
}
+328
View File
@@ -0,0 +1,328 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web 站点问题清单 / TODO · 岩美</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0; padding: 0 0 80px;
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #1f2933; background: #f7f9fb; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
header {
background: linear-gradient(135deg, #0A1F3B 0%, #15407D 100%);
color: #fff; padding: 40px 32px 32px;
}
header h1 { margin: 0 0 8px; font-size: 26px; font-weight: 700; }
header p { margin: 0; color: #ADC9EA; font-size: 14px; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 0 24px; }
.meta { margin-top: 16px; font-size: 13px; color: #8aa3c4; }
.legend { display: flex; flex-wrap: wrap; gap: 10px; margin: 24px auto 8px; max-width: 1040px; padding: 0 24px; }
.tag { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.t-block { background: #fde8e8; color: #b91c1c; } /* 阻断/必须改 */
.t-high { background: #fef0d8; color: #9a6700; } /* 重要 */
.t-low { background: #e6f0fb; color: #1d4ed8; } /* 一般/优化 */
.t-known { background: #ececec; color: #555; } /* 已知占位 */
.t-ver { background: #e3f5ea; color: #1f7a44; } /* 已核实 */
.t-done { background: #d1f0dc; color: #1f7a44; } /* 已完成 */
section { max-width: 1040px; margin: 28px auto 0; padding: 0 24px; }
h2 {
font-size: 18px; font-weight: 700; color: #0A1F3B;
border-left: 4px solid #2563AC; padding-left: 12px; margin: 0 0 14px;
}
ul.todo { list-style: none; margin: 0; padding: 0; }
ul.todo li {
background: #fff; border: 1px solid #e4e9ef; border-radius: 8px;
padding: 14px 16px; margin-bottom: 10px;
display: grid; grid-template-columns: 22px 1fr; gap: 12px; align-items: start;
}
ul.todo li:has(input:checked) { background: #f3faf5; border-color: #b7e4c7; opacity: 0.7; }
ul.todo li:has(input:checked) .item-title { color: #52606d; text-decoration: line-through; }
ul.todo li input[type=checkbox] { margin-top: 4px; width: 16px; height: 16px; }
.item-title { font-weight: 600; font-size: 15px; }
.item-title .tag { margin-left: 8px; vertical-align: middle; }
.item-desc { font-size: 13.5px; color: #52606d; margin-top: 4px; }
.item-desc code {
background: #f0f2f5; padding: 1px 5px; border-radius: 3px;
font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: #b91c1c;
}
.file { font-family: "JetBrains Mono", monospace; font-size: 12px; color: #7b8794; }
.note { background: #fffbe9; border:1px solid #f0e3b0; border-radius:8px; padding:12px 16px; font-size:13px; color:#6b5b1d; margin-top:14px; }
</style>
</head>
<body>
<header>
<div class="wrap">
<h1>Web 营销站点 — 问题清单 / TODO</h1>
<p>检查范围:产品介绍(首页)· 功能详情页 · 价格 · 下载 · 文档 · 注册 · 页眉页脚</p>
<div class="meta">生成日期 2026-06-07 · 对照后端实现核对 · 代码版本 v1.0.20</div>
</div>
</header>
<div class="legend">
<span class="tag t-block">阻断 必须改</span>
<span class="tag t-high">重要</span>
<span class="tag t-low">一般/优化</span>
<span class="tag t-known">已知占位(暂留)</span>
<span class="tag t-ver">已核实代码</span>
<span class="tag t-done">已完成</span>
</div>
<!-- ============ 虚假/夸大宣传(最优先,已核实代码) ============ -->
<section>
<h2>一、功能宣传与实际实现不符(已对照后端代码核实)</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">「红冲入库 / 红冲出库」未实现 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端 <code>internal/</code> 全局无任何红冲 / reversal / 冲销逻辑,但首页 FAQ「审批通过后发现错误」与 <span class="file">features/approval.html</span> 第 5 块(红冲 / 净额演示)大篇幅宣传。属虚假宣传,需删除或落地功能。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「调拨单」未实现 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端无 transfer / 调拨 任何实现。<span class="file">features/approval.html</span> 状态机把「调拨单」与入库/出库/盘点并列为四大单据。需移除调拨相关展示。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「库存预警 / 阈值推送」严重夸大 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端仅有 <code>min_stock</code> 单一字段用于显示「偏低/告急」标记,<b>没有</b>任何告警、推送、按 SKU 多级阈值配置、按操作员分发的能力。但首页「库存预警实时提醒」与 <span class="file">features/inventory.html</span> 整块「预警阈值配置 / 推送给张磊 / 短信通知李建国」均为虚构界面。需大幅收敛文案。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「短信通知 / 邮件提醒」未实现 <span class="tag t-high">重要</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端无 SMS / 邮件发送能力。inventory/approval 页多处「短信通知」「邮件提醒」需删除。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「过期预警 / 保质期推算」未实现 <span class="tag t-high">重要</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">后端 expire 相关仅为「license 授权到期」,无商品保质期/过期预警。<span class="file">features/inventory.html</span> 批次块的「过期预警」与阈值卡「临近过期 30 天邮件提醒」为虚构。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「智能补货建议 v1.7 Beta」「SSO 单点登录」未实现 <span class="tag t-low">一般</span><span class="tag t-ver">已核实</span></div>
<div class="item-desc">补货建议(inventory 页标 v1.7 Beta)、价格连锁版的「SSO 单点登录」后端均无。若作为路线图保留需明确标注「规划中」,否则移除。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「允许超量出库」可配置开关 待核实 <span class="tag t-low">一般</span></div>
<div class="item-desc">approval 页声称系统设置可开启「允许超量出库」,后端未检索到 oversell/超量 配置项,需确认是否真有该设置。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">营销数据疑似杜撰 <span class="tag t-high">重要</span></div>
<div class="item-desc">首页 Trust 区「1,280+ 门店 / 8.4% 损耗下降 / 99.9% 可用性」无数据支撑;功能页「14 个审计字段」「审批延迟 ≤1s」等均需核实,否则改为中性表述。</div>
</div>
</li>
</ul>
<div class="note">✅ 已核实<b>确实存在</b>的功能(文案无需改):FIFO 批次扣减出库(<code>stock.go ApproveStockOut</code>)、多仓库(<code>warehouse</code>)、库存盘点、入库/出库审批联动库存+应付应收、商品 <code>min_stock</code> 状态标记、拼音搜索、扫码验真(scan.html)。</div>
</section>
<!-- ============ 版本/品牌一致性 ============ -->
<section>
<h2>二、版本号与品牌/域名一致性</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">首页 Hero 版本号造假 <span class="tag t-block">阻断</span></div>
<div class="item-desc">首页徽标硬编码「v1.6 · 新增多仓库联动盘点」,inventory 页又写「v1.6.2」,而真实版本为 <b>v1.0.20</b>(CHANGELOG)。需统一为真实版本,最好像下载页一样从 <code>changelog</code> 数据动态取。<span class="file">index.njk:180-183</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">品牌域名三处不统一 <span class="tag t-high">重要</span></div>
<div class="item-desc">首页预览写 <code>yanmei.app/inventory</code>、客服邮箱 <code>support@yanmei.app</code>,而真实应用域名是 <code>jiu.51yanmei.com</code>。需统一域名口径。<span class="file">index.njk:201 · _data/site.json</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">下载页 API 域名硬编码 <span class="tag t-low">一般</span></div>
<div class="item-desc"><code>https://jiu.51yanmei.com</code> 直接写死在 <span class="file">download.njk</span> 的 JS 里(释放接口、各端下载、Web 入口),应抽到 <code>_data/site.json</code> 统一管理,便于换域名/环境。</div>
</div>
</li>
</ul>
</section>
<!-- ============ 页眉 ============ -->
<section>
<h2>三、页眉(导航)</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">功能详情页死链:<code>/download.html</code> <span class="tag t-block">阻断</span></div>
<div class="item-desc">两个功能页(手写 HTML)导航和页脚都链向 <code>/download.html</code><code>/docs.html</code>,但 Eleventy 实际产出 pretty URL <code>/download/</code><code>/docs/</code>。dist 中无 <code>download.html</code> → 死链。统一改为 <code>/download/</code><code>/docs/</code><span class="file">features/inventory.html:660-661 · approval.html 同</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">功能详情页未进主导航(孤岛页面) <span class="tag t-high">重要</span></div>
<div class="item-desc"><span class="file">features/inventory.html</span><span class="file">approval.html</span> 制作精良,但主站导航「产品」只跳首页锚点 <code>/#modules</code>,这两页仅能从页脚或彼此跳转进入。建议在导航或首页模块卡加「了解更多」入口。</div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">两套 CSS / 设计令牌并存 <span class="tag t-high">重要</span></div>
<div class="item-desc">主站 njk 用 <code>color.css + style.css</code>,功能页用 <code>colors_and_type.css</code>,导航/页脚样式各写一份。维护分裂、易不一致,建议合并到一套并让功能页也走 base.njk 模板。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">注册入口不一致 <span class="tag t-low">一般</span></div>
<div class="item-desc">功能页 JS 未登录态指向 <code>/app/login</code><code>/app/register</code>Flutter 内路由),而 njk 站点有独立 web 注册页 <code>/register/</code>。两套注册入口口径不一,需统一。</div>
</div>
</li>
</ul>
</section>
<!-- ============ 页脚 ============ -->
<section>
<h2>四、页脚</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">大量占位死链 <code>href="#"</code> <span class="tag t-high">重要</span></div>
<div class="item-desc">页脚以下链接全部为空锚点,需逐一落地或移除:API 文档、视频教程、数据导入模板、提交工单、技术支持、服务等级、关于岩美、客户案例、招贤纳士、联系我们、版本更新。<span class="file">footer.njk · features/*.html</span></div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">备案号为占位假值 <span class="tag t-known">已知占位</span></div>
<div class="item-desc">现为 <code>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</code>(全 0)。按要求暂填,<b>上线前必须替换为真实备案号</b>(且 ICP 主体地需与实际服务器/主体一致,岩美若在沪外需核对)。<span class="file">_data/site.json:5</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">客服邮箱/电话为占位 <span class="tag t-high">重要</span></div>
<div class="item-desc">邮箱 <code>support@yanmei.app</code>(与真实域名不符)、电话 <code>400-880-8888</code>(明显假号)。需换成真实联系方式。<span class="file">_data/site.json:7-8</span></div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">页脚双份维护 <span class="tag t-low">一般</span></div>
<div class="item-desc">njk 站点用 <code>footer.njk</code>(读 site.json 变量),功能页则把页脚、备案、联系方式硬编码各写一份,改一处不同步。根因同「两套模板」,建议功能页接入 base.njk。</div>
</div>
</li>
</ul>
</section>
<!-- ============ 价格 ============ -->
<section>
<h2>五、价格</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">价格数字需确认是否真实对外 <span class="tag t-high">重要</span></div>
<div class="item-desc">单店版 <code>¥299/月</code>、年付 <code>¥2,988/年</code>、连锁版「定制」疑为占位。上线前需确认真实报价。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「购买 / 联系销售 / 预约演示」按钮无对应流程 <span class="tag t-high">重要</span></div>
<div class="item-desc">价格卡「购买」「联系销售」、Hero「观看演示」、CTA「预约 1 对 1 演示」全部指向 <code>/app/</code>,但并无支付、销售联系、演示预约流程。按钮文案与去向不符。<span class="file">index.njk:554,568,188,622</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">「试用版」与「单店版」文案口径混乱 <span class="tag t-low">一般</span></div>
<div class="item-desc">价格区把「试用版(¥0/30天)」与「单店版(¥299)」分列两卡,而 FAQ 又称「标准版提供 30 天免费试用」。试用 vs 标准 的口径需统一。</div>
</div>
</li>
</ul>
</section>
<!-- ============ 下载 ============ -->
<section>
<h2>六、下载页</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">下载按钮失败无降级 <span class="tag t-high">重要</span></div>
<div class="item-desc">Windows/macOS/Android 按钮初始 <code>href="#"</code>,依赖运行时 fetch <code>/api/v1/public/release</code> 回填真实下载地址;接口失败 / 无数据时点击无任何反应、也无提示。需加失败兜底(如提示「暂未就绪」或禁用态)。<span class="file">download.njk:500-543</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">iOS 状态依赖接口字段 <span class="tag t-low">一般</span></div>
<div class="item-desc">iOS 卡片仅当接口返回 <code>download_urls.ios</code> 才从「敬请期待」切换为「TestFlight 安装」。需确认 TestFlight 公开链接是否已配到 release 接口,否则 iOS 永远显示敬请期待(与首页「五端就绪」矛盾)。</div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">首页「五端就绪」与下载页 iOS 状态矛盾 <span class="tag t-high">重要</span></div>
<div class="item-desc">首页平台区把 Web/Win/macOS/iOS/Android 全列为可用,下载页 iOS 仍是「敬请期待」。需对齐口径。</div>
</div>
</li>
</ul>
</section>
<!-- ============ 文档 ============ -->
<section>
<h2>七、文档页</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title"><code>⌘K</code> 搜索快捷键是装饰 <span class="tag t-low">一般</span></div>
<div class="item-desc">文档顶栏显示 <code>⌘K</code> 徽标但未绑定键盘事件聚焦搜索框;搜索本身仅按整章显隐,无关键词高亮/定位。要么实现快捷键,要么去掉徽标。<span class="file">docs.njk</span></div>
</div>
</li>
<li><input type="checkbox" checked />
<div>
<div class="item-title">反馈接口路径不一致 / 待确认 <span class="tag t-low">一般</span></div>
<div class="item-desc">文档反馈用相对路径 <code>/api/v1/public/feedback</code>,下载页却用绝对域名。需确认该端点真实存在且同源可达。<span class="file">docs.njk:182</span></div>
</div>
</li>
</ul>
</section>
<!-- ============ 注册 ============ -->
<section>
<h2>八、注册页</h2>
<ul class="todo">
<li><input type="checkbox" />
<div>
<div class="item-title">注册接口同源依赖 <span class="tag t-low">一般</span></div>
<div class="item-desc"><code>window.location.origin + '/api/v1/public/register'</code>,要求静态站与后端同源部署;本地预览或分离部署时会指错。需确认部署形态(与下载页写死域名的做法也不统一)。<span class="file">register.njk:175</span></div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">成功提示路径需核对 <span class="tag t-low">一般</span></div>
<div class="item-desc">提示「忘记门店编码可在『系统设置 → 关于』查看」,需与实际客户端菜单路径一致。</div>
</div>
</li>
</ul>
</section>
<!-- ============ 构建/工程 ============ -->
<section>
<h2>九、构建 / 工程结构</h2>
<ul class="todo">
<li><input type="checkbox" checked />
<div>
<div class="item-title">dist 残留旧产物 <code>docs.html</code> <span class="tag t-low">一般</span></div>
<div class="item-desc">dist 同时存在 <code>docs.html</code>(疑似旧构建残留)与 <code>docs/index.html</code>,造成同内容双 URL。建议清理 dist 后全量重建,避免历史残留。</div>
</div>
</li>
<li><input type="checkbox" />
<div>
<div class="item-title">scan.html / features 走 passthrough,不享模板 <span class="tag t-low">一般</span></div>
<div class="item-desc"><code>.eleventy.js</code> 把 features、scan.html 直接复制,不经 njk 模板,导致页脚、备案、域名、CSS 都得手动同步——是上面多个「双份维护」问题的工程根因。建议长期改造为模板化。</div>
</div>
</li>
</ul>
</section>
</body>
</html>
+504
View File
@@ -0,0 +1,504 @@
#!/usr/bin/env node
/**
* todo/todo.mjs — 项目 TODO 管理 CLI(零依赖,Node.js >= 18
*
* 子命令:
* add --title "..." [--level high|mid|low] [--tags a,b] [--desc "..."]
* done <id> [--version vX.Y.Z]
* reopen <id>
* rm <id>
* list
* render
*/
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __dir = dirname(fileURLToPath(import.meta.url));
const DB = resolve(__dir, 'todo.json');
const HTML = resolve(__dir, 'todo.html');
// ─── 数据读写 ────────────────────────────────────────────────────────────────
function load() {
if (!existsSync(DB)) {
return { meta: { title: '酒库管理系统 — 项目 TODO', updated_at: now() }, seq: 0, items: [] };
}
return JSON.parse(readFileSync(DB, 'utf8'));
}
function save(db) {
db.meta.updated_at = now();
writeFileSync(DB, JSON.stringify(db, null, 2) + '\n', 'utf8');
}
function now() {
return new Date().toISOString();
}
function fmtDate(iso) {
if (!iso) return '';
const d = new Date(iso);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
function gitTag() {
try {
return execSync('git describe --tags --abbrev=0', { encoding: 'utf8', stdio: ['ignore','pipe','ignore'] }).trim();
} catch {
return null;
}
}
// ─── 终端 summary ────────────────────────────────────────────────────────────
const LEVEL_LABEL = { high: '高', mid: '中', low: '低' };
const LEVEL_ORDER = { high: 0, mid: 1, low: 2 };
function printSummary(db) {
const pending = db.items.filter(i => !i.done)
.sort((a,b) => (LEVEL_ORDER[a.level]??9) - (LEVEL_ORDER[b.level]??9) || a.id - b.id);
const done = db.items.filter(i => i.done)
.sort((a,b) => (b.completed_at||'') > (a.completed_at||'') ? 1 : -1);
console.log(`\nTODO 共 ${db.items.length} · 未完成 ${pending.length} · 已完成 ${done.length}`);
if (pending.length) {
console.log('未完成(按重要度):');
for (const i of pending) {
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 ? ' ' + tags : ''}`);
}
}
if (done.length) {
console.log('已完成(最近 5 条):');
for (const i of done.slice(0, 5)) {
const ver = i.version ? `${i.version}` : '';
console.log(` ✅ #${i.id} ${i.title}${ver}`);
}
}
console.log('');
}
// ─── HTML 渲染 ───────────────────────────────────────────────────────────────
function esc(s) {
return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
/** 把描述里的 `code` 渲染成 <code> */
function renderDesc(s) {
if (!s) return '';
return esc(s).replace(/`([^`]+)`/g, '<code>$1</code>');
}
function levelClass(level) {
return { high: 't-block', mid: 't-high', low: 't-low' }[level] || 't-low';
}
function levelText(level) {
return { high: '高优 · 紧急', mid: '重要', low: '一般 / 优化' }[level] || level;
}
function renderItems(items) {
if (!items.length) return '<p class="empty-tip">暂无条目</p>';
return items.map(i => {
const tags = (i.tags||[]).map(t => `<span class="tag t-tag" data-tag="${esc(t)}">${esc(t)}</span>`).join(' ');
const desc = i.desc ? `<div class="item-desc">${renderDesc(i.desc)}</div>` : '';
const meta = `<div class="item-meta">
<span class="meta-date">🕐 ${fmtDate(i.created_at)}</span>
${i.done ? `<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);
return `
<li class="todo-card${i.done ? ' done' : ''}"
data-id="${i.id}"
data-level="${esc(i.level)}"
data-tags="${esc((i.tags||[]).join(','))}"
data-done="${i.done ? '1' : '0'}">
<div class="card-header">
<span class="item-title">${esc(i.title)}</span>
<span class="tag ${lvlCls}">${levelText(i.level)}</span>
</div>
${desc}
<div class="card-footer">
<div class="tag-row">${tags}</div>
${meta}
</div>
</li>`;
}).join('\n');
}
function buildHtml(db) {
const allTags = [...new Set(db.items.flatMap(i => i.tags||[]))].sort();
const pending = db.items.filter(i => !i.done)
.sort((a,b) => (LEVEL_ORDER[a.level]??9) - (LEVEL_ORDER[b.level]??9) || a.id - b.id);
const done = db.items.filter(i => i.done)
.sort((a,b) => (b.completed_at||'') > (a.completed_at||'') ? 1 : -1);
const tagChips = allTags.map(t =>
`<button class="filter-chip" data-filter-tag="${esc(t)}">${esc(t)}</button>`
).join('');
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${esc(db.meta.title)}</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0; padding: 0 0 80px;
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #1f2933; background: #f7f9fb; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
header {
background: linear-gradient(135deg, #0A1F3B 0%, #15407D 100%);
color: #fff; padding: 36px 32px 28px;
}
header h1 { margin: 0 0 6px; font-size: 24px; font-weight: 700; }
header .header-meta { color: #ADC9EA; font-size: 13px; margin-top: 4px; }
.stats { display: flex; gap: 16px; margin-top: 14px; }
.stat-pill {
background: rgba(255,255,255,0.12); border-radius: 20px;
padding: 4px 14px; font-size: 13px; color: #fff;
}
.stat-pill strong { font-size: 18px; font-weight: 700; margin-right: 2px; }
/* ── 筛选栏 ── */
.filter-bar {
position: sticky; top: 0; z-index: 10;
background: #fff; border-bottom: 1px solid #e4e9ef;
padding: 10px 28px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
}
.filter-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.filter-label { font-size: 12px; color: #7b8794; white-space: nowrap; }
.filter-chip, .filter-btn {
padding: 3px 12px; border-radius: 14px; border: 1.5px solid #d1d9e6;
background: #f7f9fb; color: #52606d; font-size: 12.5px; cursor: pointer;
transition: all .15s; white-space: nowrap;
}
.filter-chip:hover, .filter-btn:hover { border-color: #2563AC; color: #2563AC; }
.filter-chip.active, .filter-btn.active {
background: #2563AC; border-color: #2563AC; color: #fff;
}
.filter-sep { width: 1px; height: 20px; background: #e4e9ef; margin: 0 4px; }
/* ── 内容区 ── */
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px; }
.section-title {
font-size: 16px; font-weight: 700; color: #0A1F3B;
border-left: 4px solid #2563AC; padding-left: 12px;
margin: 28px 0 12px;
}
.section-title.done-title { border-color: #6cbb8a; }
ul.todo-list { list-style: none; margin: 0; padding: 0; }
.todo-card {
background: #fff; border: 1px solid #e4e9ef; border-radius: 10px;
padding: 14px 18px; margin-bottom: 10px;
}
.todo-card.done { background: #f3faf5; border-color: #b7e4c7; opacity: .75; }
.card-header { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
.item-title { font-weight: 600; font-size: 15px; flex: 1; }
.todo-card.done .item-title { text-decoration: line-through; color: #52606d; }
.item-desc { font-size: 13.5px; color: #52606d; margin-top: 6px; }
.item-desc code {
background: #f0f2f5; padding: 1px 5px; border-radius: 3px;
font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: #b91c1c;
}
.card-footer { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; }
.item-meta { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; color: #8aa3c4; }
.meta-date { white-space: nowrap; }
/* ── tags ── */
.tag { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.t-block { background: #fde8e8; color: #b91c1c; }
.t-high { background: #fef0d8; color: #9a6700; }
.t-low { background: #e6f0fb; color: #1d4ed8; }
.t-tag { background: #efefef; color: #555; cursor: pointer; transition: .12s; }
.t-tag:hover { background: #d9e8fb; color: #1d4ed8; }
.ver-badge { font-weight: 700; color: #1f7a44; }
/* ── 折叠 ── */
.done-section { margin-bottom: 24px; }
.done-toggle {
background: none; border: 1.5px solid #d1d9e6; border-radius: 6px;
padding: 4px 14px; cursor: pointer; font-size: 13px; color: #52606d;
margin-bottom: 10px; transition: .15s;
}
.done-toggle:hover { border-color: #6cbb8a; color: #1f7a44; }
.empty-tip { font-size: 14px; color: #8aa3c4; margin: 8px 0 24px; }
/* 隐藏 */
.hidden { display: none !important; }
</style>
</head>
<body>
<header>
<div class="wrap">
<h1>${esc(db.meta.title)}</h1>
<div class="header-meta">生成于 ${fmtDate(db.meta.updated_at)} · 真相源 todo/todo.json</div>
<div class="stats">
<div class="stat-pill"><strong>${db.items.length}</strong>全部</div>
<div class="stat-pill"><strong>${pending.length}</strong>未完成</div>
<div class="stat-pill"><strong>${done.length}</strong>已完成</div>
</div>
</div>
</header>
<div class="filter-bar" id="filterBar">
<div class="filter-group">
<span class="filter-label">重要度</span>
<button class="filter-btn active" data-filter-level="all">全部</button>
<button class="filter-btn" data-filter-level="high">高优</button>
<button class="filter-btn" data-filter-level="mid">重要</button>
<button class="filter-btn" data-filter-level="low">一般</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">状态</span>
<button class="filter-btn active" data-filter-done="all">全部</button>
<button class="filter-btn" data-filter-done="0">未完成</button>
<button class="filter-btn" data-filter-done="1">已完成</button>
</div>
${allTags.length ? `<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">平台 / 标签</span>
${tagChips}
</div>` : ''}
</div>
<div class="wrap">
<div class="section-title">📋 未完成(${pending.length}</div>
<ul class="todo-list" id="pendingList">
${renderItems(pending)}
</ul>
<div class="done-section">
<div class="section-title done-title">✅ 已完成(${done.length}</div>
<button class="done-toggle" id="doneToggle">▾ 展开已完成列表</button>
<ul class="todo-list hidden" id="doneList">
${renderItems(done)}
</ul>
</div>
</div>
<script>
(function() {
// 筛选状态
let curLevel = 'all';
let curDone = 'all';
let curTags = new Set();
function applyFilter() {
document.querySelectorAll('.todo-card').forEach(card => {
const lvl = card.dataset.level;
const dn = card.dataset.done;
const tags = card.dataset.tags ? card.dataset.tags.split(',') : [];
let show = true;
if (curLevel !== 'all' && lvl !== curLevel) show = false;
if (curDone !== 'all' && dn !== curDone) show = false;
if (curTags.size > 0 && ![...curTags].some(t => tags.includes(t))) show = false;
card.classList.toggle('hidden', !show);
});
}
// 重要度
document.querySelectorAll('[data-filter-level]').forEach(btn => {
btn.addEventListener('click', () => {
curLevel = btn.dataset.filterLevel;
document.querySelectorAll('[data-filter-level]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
// 状态
document.querySelectorAll('[data-filter-done]').forEach(btn => {
btn.addEventListener('click', () => {
curDone = btn.dataset.filterDone;
document.querySelectorAll('[data-filter-done]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
// 标签(多选)
document.querySelectorAll('[data-filter-tag]').forEach(btn => {
btn.addEventListener('click', () => {
const t = btn.dataset.filterTag;
if (curTags.has(t)) { curTags.delete(t); btn.classList.remove('active'); }
else { curTags.add(t); btn.classList.add('active'); }
applyFilter();
});
});
// 点击卡片标签 → 快速筛选
document.querySelectorAll('.t-tag').forEach(tag => {
tag.addEventListener('click', e => {
e.stopPropagation();
const t = tag.dataset.tag;
const chip = document.querySelector('[data-filter-tag="'+t+'"]');
if (chip) chip.click();
});
});
// 已完成折叠
const toggle = document.getElementById('doneToggle');
const doneList = document.getElementById('doneList');
if (toggle && doneList) {
toggle.addEventListener('click', () => {
const collapsed = doneList.classList.toggle('hidden');
toggle.textContent = collapsed ? '▾ 展开已完成列表' : '▴ 收起已完成列表';
});
}
})();
</script>
</body>
</html>`;
}
// ─── CLI 解析 ────────────────────────────────────────────────────────────────
function parseArgs(argv) {
const args = {};
let pos = [];
for (let i = 0; i < argv.length; i++) {
if (argv[i].startsWith('--')) {
const key = argv[i].slice(2);
const val = argv[i+1] && !argv[i+1].startsWith('--') ? argv[++i] : true;
args[key] = val;
} else {
pos.push(argv[i]);
}
}
args._ = pos;
return args;
}
function findItem(db, id) {
const n = parseInt(id, 10);
const item = db.items.find(i => i.id === n);
if (!item) { console.error(`错误:找不到 id=${n} 的条目`); process.exit(1); }
return item;
}
// ─── 子命令 ─────────────────────────────────────────────────────────────────
function cmdAdd(db, args) {
const title = args.title || (args._.join(' ') || '').trim();
if (!title) { console.error('错误:--title 不能为空'); process.exit(1); }
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;
db.seq++;
const item = {
id: db.seq,
title,
desc,
level,
tags,
created_at: now(),
done: false,
completed_at: null,
version: null,
};
db.items.push(item);
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
console.log(`✅ 已添加 #${item.id} [${levelText(level)}] ${title}${tags.length ? ' ' + tags.join('/') : ''}`);
printSummary(db);
}
function cmdDone(db, args) {
const id = args._[0];
const item = findItem(db, id);
let version = args.version || null;
if (!version) {
version = gitTag();
if (!version) console.warn('⚠️ 无法从 git 获取版本号,version 将为空');
}
item.done = true;
item.completed_at = now();
item.version = version;
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
console.log(`✅ #${item.id}${item.title}」已标记完成${version ? ',记入版本 ' + version : ''}`);
printSummary(db);
}
function cmdReopen(db, args) {
const id = args._[0];
const item = findItem(db, id);
item.done = false;
item.completed_at = null;
item.version = null;
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
console.log(`🔄 #${item.id}${item.title}」已重新开启`);
printSummary(db);
}
function cmdRm(db, args) {
const id = parseInt(args._[0], 10);
const idx = db.items.findIndex(i => i.id === id);
if (idx < 0) { console.error(`错误:找不到 id=${id}`); process.exit(1); }
const [removed] = db.items.splice(idx, 1);
save(db);
writeFileSync(HTML, buildHtml(db), 'utf8');
console.log(`🗑️ #${removed.id}${removed.title}」已删除`);
printSummary(db);
}
function cmdList(db) {
// 确保 HTML 最新
writeFileSync(HTML, buildHtml(db), 'utf8');
printSummary(db);
console.log(`📄 HTMLtodo/todo.html`);
}
function cmdRender(db) {
writeFileSync(HTML, buildHtml(db), 'utf8');
console.log('🖨️ todo/todo.html 已重新渲染');
}
// ─── 入口 ────────────────────────────────────────────────────────────────────
const [,, subcmd, ...rest] = process.argv;
const db = load();
const args = parseArgs(rest);
switch (subcmd) {
case 'add': cmdAdd(db, args); break;
case 'done': cmdDone(db, args); break;
case 'reopen': cmdReopen(db, args); break;
case 'rm': cmdRm(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|done|reopen|rm|list|render`);
process.exit(1);
}