docs(design): 新增 UI demo 页面,供 UI 精修评审

- docs/design/ui-demo/:控件总览、表单、卡片、列表与表格,以及工作台、读标两个组合页
- 只用设计规范的变量和组件;新增组件样式放在 demo.css,清单见 index
- index 列出 UI 精修待定问题 Q1–Q12
- design/README 与 AGENTS.md 进度同步

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 12:46:27 +08:00
parent 94c3bfd688
commit e1a9017cd1
11 changed files with 2583 additions and 1 deletions

View File

@@ -142,4 +142,5 @@ samples/
- 2026-11-03:应用图标改为印泥铺满、不留白边,圆角按 Apple 连续曲率圆角由系统裁(B6);VI 手册 v1.1 第 22 页改为 iPhone 18 Pro 主屏幕模拟(`docs/design/vi/mockup/`,Apple 素材不进 git,机身需本人同意 Apple Design Resources 许可后放入)。
- 2026-11-03:分析参考项目标擎 BidEngine 的架构、提示词和模型路由,整理出可参考的做法和避坑清单(`docs/architecture/ref-bid-engine-analysis.md`),留给技术架构阶段用。
- 2026-11-03:技术架构设计 v0.2(`docs/architecture/architecture.md`)。T2–T5 已确认:Python 单语言、桌面端做客户端外壳、SaaS 先 Compose 后 K8s、云无关。之后确认了 AR1–AR3、AR5–AR9,前端改为 Vue 3;AR4 复议后改为引入 Valkey(已确认);AR10 定为不引入带样式的 UI 框架,用 Reka UI 打底。选型全部确认。编码前先做技术验证 S1–S4。
- 下一步:用原型找种子用户走查(顺带问 G15 苏采云客户端的分册对应),再进入 UI 设计精修。
- 2026-11-03:种子用户走查结束,没有问题。进入 UI 精修前先做 UI demo(`docs/design/ui-demo/`,6 个页面):控件、表单、卡片、列表,以及工作台、读标两个组合页;新增组件清单和待定问题 Q1–Q12 见其 index。
- 下一步:UI 精修,先定 UI demo 里的 Q1–Q12。

View File

@@ -10,6 +10,7 @@
| [system/tokens.css](system/tokens.css) | 设计变量(原型和前端直接引用) | v0.1 |
| [system/components.css](system/components.css) | 组件样式参考实现(预览页和原型共用) | v0.1 |
| [system/components.html](system/components.html) | 组件预览页(用样本条款演示) | v0.1 |
| [ui-demo/](ui-demo/index.html) | UI demo:控件总览、表单、卡片、列表与表格,以及工作台、读标两个组合页;附新增组件清单和 UI 精修待定问题 Q1–Q12 | 供 UI 精修评审 |
## 速查

View File

@@ -0,0 +1,240 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>卡片 · 标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>③ 卡片</h1>
<p>卡片是白底 + 1px 分隔线 + 2px 圆角,不加阴影(层次靠分隔线和底色)。卡片里的按钮一律用次按钮或文字按钮,把主按钮留给页面级操作。</p>
<nav class="d-toc" aria-label="本页目录"><a href="#s-proj">01 项目卡</a><a href="#s-stat">02 统计格</a><a href="#s-quota">03 额度卡</a><a href="#s-task">04 任务进度卡</a><a href="#s-ai">05 AI 摘要卡</a><a href="#s-date">06 关键日期卡</a></nav>
</div>
<!-- 01 项目卡 -->
<section class="d-sec" id="s-proj">
<div class="d-sec-h"><span class="no">01</span><h2>项目卡</h2><span class="ref">规范 §7.9 · 卡片视图(表格视图见 ④)</span></div>
<div class="d-grid c3 eq">
<article class="card pcard is-fatal">
<div class="pc-top"><div><div class="proj-name">某市政务服务中心智慧大厅建设项目</div><div class="proj-sub">某市政务服务管理办公室 · 政府采购 · 公开招标</div></div><span class="tag status">已检查</span></div>
<div class="facts">
<div><small>投标截止</small><span class="fig m">11月20日 09:30</span><br><span class="cap">剩 <span class="fig m">17</span> 天</span></div>
<div><small>所投包</small>采购包 1</div>
<div><small>就绪</small>未就绪</div>
</div>
<div class="d-row" style="gap:var(--sp-2)"><span class="sev fatal">致命<span class="fig">3</span></span><span class="sev major">重要<span class="fig">3</span></span><span class="sev hint">提示<span class="fig">2</span></span><span class="cap muted" style="margin-left:auto">第 2 次检查 · 11月2日</span></div>
<div class="card-foot"><button class="btn sm">处理检查结果</button><span class="r"><button class="icon-btn sm" aria-label="更多操作" data-tip="更多操作"><i data-ic="more"></i></button></span></div>
</article>
<article class="card pcard">
<div class="pc-top"><div><div class="proj-name">某区智慧社区平台运维服务</div><div class="proj-sub">某区民政局 · 政府采购 · 竞争性磋商</div></div><span class="tag status">已检查</span></div>
<div class="facts">
<div><small>响应截止</small><span class="due-soon">剩 2 天 5 小时</span></div>
<div><small>所投包</small>全部</div>
<div><small>就绪</small><span class="tag status" data-tip="只表示致命问题都已处理,不代表响应文件没有问题。">就绪</span></div>
</div>
<div class="d-row" style="gap:var(--sp-2)"><span class="cap muted">致命 —</span><span class="sev major">重要<span class="fig">1</span></span><span class="cap muted" style="margin-left:auto">第 3 次检查 · 11月3日</span></div>
<div class="card-foot"><button class="btn sm">导出报告</button><span class="r"><button class="icon-btn sm" aria-label="更多操作" data-tip="更多操作"><i data-ic="more"></i></button></span></div>
</article>
<article class="card pcard">
<div class="pc-top"><div><div class="proj-name">某县数据中心机房改造项目</div><div class="proj-sub">某县大数据管理局 · 招标投标</div></div><span class="tag status">编制中</span></div>
<div class="facts">
<div><small>投标截止</small><span class="due-today">今天 17:00 截止</span></div>
<div><small>所投标段</small>标段 1</div>
<div><small>清单</small>草稿 v1</div>
</div>
<div class="flowline sm"><span class="st done"><i></i>跟进中</span><span class="ln"></span><span class="st now"><i></i>编制中</span><span class="ln"></span><span class="st"><i></i>已检查</span><span class="ln"></span><span class="st"><i></i>已递交</span></div>
<div class="card-foot"><button class="btn sm">确认清单</button><span class="cap muted">待确认 2 条</span><span class="r"><button class="icon-btn sm" aria-label="更多操作" data-tip="更多操作"><i data-ic="more"></i></button></span></div>
</article>
</div>
<div class="d-grid c3 eq" style="margin-top:var(--sp-6)">
<article class="card pcard">
<div class="pc-top"><div><div class="proj-name">某区智慧校园一期</div><div class="proj-sub">某区教育局 · 政府采购 · 公开招标</div></div><span class="tag status">已递交</span></div>
<div class="facts">
<div><small>投标截止</small><span class="due-past">已截止</span></div>
<div><small>所投包</small>全部</div>
<div><small>递交</small><span class="fig m">10月28日</span></div>
</div>
<div class="sign sm" style="margin:0"><span>✓</span><b>王敏</b><span class="fig">10月28日 09:12</span>标记已递交</div>
<div class="card-foot"><button class="btn sm text">查看报告</button><span class="r"><button class="btn sm text">归档</button></span></div>
</article>
<article class="card pcard" aria-busy="true" aria-label="正在加载项目">
<div class="pc-top"><div><span class="skel t" style="width:70%"></span><span class="skel w60"></span></div></div>
<div class="facts"><div><span class="skel w60"></span><span class="skel"></span></div><div><span class="skel w60"></span><span class="skel"></span></div><div><span class="skel w60"></span><span class="skel"></span></div></div>
<span class="skel w30"></span>
<p class="d-cap" style="margin:0">↑ 加载中(骨架屏)</p>
</article>
<button class="d-box flat" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--sp-2);min-height:220px;cursor:pointer;font:inherit;color:var(--text-primary)">
<i data-ic="plus" class="lg"></i><b>新建项目</b><span class="cap muted">上传招标文件,标博士逐条提取要求</span>
</button>
</div>
<ul class="d-rules"><li>致命项目卡左侧加 3px 致命红竖线(与问题行一致),方便在卡片墙里扫读;状态用中性描边标签,不用彩色。</li><li>术语跟着体系走:磋商项目写「响应截止」「响应文件」,招标投标体系写「标段」(§9.1)。</li></ul>
</section>
<!-- 02 统计格 -->
<section class="d-sec" id="s-stat">
<div class="d-sec-h"><span class="no">02</span><h2>统计格</h2><span class="ref">规范 §4.2 Figure 30/36</span></div>
<div class="stats">
<div class="stat"><small>进行中的项目</small><span class="big">4</span><p>已递交 1 · 已归档 1</p></div>
<div class="stat"><small>7 天内截止</small><span class="big">2</span><p class="due-today">最近:今天 17:00</p></div>
<div class="stat"><small>待处理的致命问题</small><span class="big">3</span><p>在 1 个项目里</p></div>
<div class="stat"><small>本期解析剩余</small><span class="big">37</span><span class="unit">次</span><p>检查剩 12 次 · <a class="plain" href="#">查看明细</a></p></div>
</div>
<ul class="d-rules"><li>统计数字一律墨色,即使是「致命问题 3」也不染红:红色留给标签,避免一屏红色太多。</li></ul>
</section>
<!-- 03 额度卡 -->
<section class="d-sec" id="s-quota">
<div class="d-sec-h"><span class="no">03</span><h2>额度卡</h2><span class="ref">规范 §7.10 · §8.3 · 红线 9</span></div>
<div class="d-grid c3">
<div class="card quota">
<div class="card-h"><h3>本期额度</h3><span class="more cap muted">专业版 · 至 2027-10-31</span></div>
<div class="q2">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="bar"><i style="width:26%"></i></div><p>已用 13 次</p></div>
<div><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20</span><div class="bar"><i style="width:40%"></i></div><p>已用 8 次</p></div>
</div>
<div class="card-foot"><a class="plain cap" href="#">扣点规则</a><span class="r"><a class="plain cap" href="#">查看流水</a></span></div>
</div>
<div class="card quota">
<div class="card-h"><h3>检查额度</h3><span class="more cap muted">已用 80%</span></div>
<small>检查剩余</small><span class="big">4</span><span class="unit">/ 20 次</span>
<div class="bar warn"><i style="width:80%"></i></div>
<p class="warn">已用 80%。同一项目 24 小时内复查不扣额度。</p>
<div class="card-foot"><span class="cap muted">提醒每期只出现一次</span><span class="r"><a class="plain cap" href="#">查看流水</a></span></div>
</div>
<div class="card quota">
<div class="card-h"><h3>检查额度</h3><span class="more cap muted">已用完</span></div>
<small>检查剩余</small><span class="big">0</span><span class="unit">/ 20 次</span>
<div class="bar warn"><i style="width:100%"></i></div>
<p class="warn">本期检查额度已用完,续费或下期发放后可继续。</p>
<div class="card-foot"><button class="btn sm">联系客服加额度</button><span class="r"><a class="plain cap" href="#">查看流水</a></span></div>
</div>
</div>
<div class="d-row" style="margin-top:var(--sp-4)"><span class="d-cap">顶栏常驻</span><a class="quota-chip" href="#">解析<span class="fig">37</span>检查<span class="fig">12</span></a><a class="quota-chip warn" href="#">解析<span class="fig">37</span>检查<span class="fig">4</span></a><span class="d-cap">检查用到 80% 时,顶栏描边变赭色</span></div>
</section>
<!-- 04 任务进度卡 -->
<section class="d-sec" id="s-task">
<div class="d-sec-h"><span class="no">04</span><h2>任务进度卡</h2><span class="ref">规范 §7.11</span></div>
<div class="d-grid c3">
<div class="card">
<div class="card-h"><h3>正在解析招标文件</h3><span class="more cap muted">已用时 1 分 40 秒</span></div>
<p class="cap muted" style="margin:0 0 var(--sp-2)">ZC-2026-0418 采购文件.pdf · 186 页</p>
<ol class="steps">
<li class="done"><span class="dot">✓</span><span>识别文件格式</span></li>
<li class="done"><span class="dot">✓</span><span>提取文字</span><span class="fig">186 页</span></li>
<li class="done"><span class="dot">✓</span><span>识别分包</span><span class="fig">2 个包</span></li>
<li class="now"><span class="dot"></span><span>提取清单</span><span class="fig">62%</span></li>
<li class="todo"><span class="dot"></span><span>核对出处</span></li>
</ol>
<div class="progress" style="margin-top:var(--sp-2)"><div class="bar"><i style="width:62%"></i></div><span class="fig">62%</span></div>
<p class="note">可以离开本页,完成后会发站内通知。</p>
<div class="card-foot"><button class="btn sm" data-cancel>取消解析</button><span class="cap muted">取消后退回 1 次解析额度</span></div>
</div>
<div class="card">
<div class="card-h"><h3>解析失败</h3><span class="more cap muted">11月2日 14:31</span></div>
<p class="cap muted" style="margin:0 0 var(--sp-2)">关于 ZC-2026-0418 的更正公告.pdf</p>
<ol class="steps">
<li class="done"><span class="dot">✓</span><span>识别文件格式</span></li>
<li class="fail"><span class="dot">!</span><span>提取文字</span></li>
<li class="todo"><span class="dot"></span><span>识别分包</span></li>
<li class="todo"><span class="dot"></span><span>提取清单</span></li>
</ol>
<div class="alert warn" style="margin-top:var(--sp-3)"><span class="ico w">!</span><div><p><b>文件已加密,无法读取文字。</b>已退回 1 次解析额度。</p><span class="sub">请上传未加密的版本,或在 PDF 阅读器里另存一份。</span></div></div>
<div class="card-foot"><button class="btn sm"><i data-ic="upload"></i>重新上传</button><span class="r"><span class="errno">E-20261102-3M8A</span></span></div>
</div>
<div class="d-stack">
<div class="card">
<div class="card-h"><h3>解析完成</h3><span class="more cap muted">用时 3 分 12 秒</span></div>
<dl class="kv">
<dt>提取要求</dt><dd><span class="fig m">26</span> 条 · 其中 ★ <span class="fig m">4</span> 条</dd>
<dt>待确认</dt><dd><span class="src-ai"><span class="fig m">2</span> 条没有可靠出处</span></dd>
<dt>分包</dt><dd>2 个采购包</dd>
</dl>
<div class="card-foot"><button class="btn sm">去读标</button><span class="r cap muted">已扣 1 次解析</span></div>
</div>
<div class="card tight">
<div class="card-h" style="margin:0"><i data-ic="clock" class="muted"></i><h3 style="font-size:var(--fs-body)">检查排队中</h3><span class="more cap muted">前面还有 <span class="fig m">2</span> 个任务 · 约 4 分钟</span></div>
</div>
</div>
</div>
</section>
<!-- 05 AI 摘要卡 -->
<section class="d-sec" id="s-ai">
<div class="d-sec-h"><span class="no">05</span><h2>AI 摘要卡</h2><span class="ref">规范 §7.16 · §8.1 · 红线 1、2</span></div>
<div class="d-grid c2">
<div class="card">
<div class="card-h"><span class="tag ai">AI 摘要</span><h3>项目要点</h3><span class="more cap muted">未确认 · 每一条都可以点出处核对</span></div>
<div class="ai-block">
<dl class="kv">
<dt>法律体系</dt><dd>政府采购 · 公开招标(按「投标无效」口径检查)</dd>
<dt>预算 / 限价</dt><dd>采购包 1:<span class="fig m">286.00</span> 万元(最高限价同预算)<a class="cite-src inline" href="#">第 <span class="fig">2</span> 页</a></dd>
<dt>评标方法</dt><dd>综合评分法:价格 10、技术 40、实施方案 20、售后 10、认证 6、业绩 8<a class="cite-src inline" href="#">第 <span class="fig">31</span> 页</a></dd>
<dt>联合体</dt><dd>不接受<a class="cite-src inline" href="#">第 <span class="fig">2</span> 页</a></dd>
<dt>保证金</dt><dd>不收取<a class="cite-src inline" href="#">第 <span class="fig">21</span> 页</a></dd>
<dt>样品</dt><dd class="muted">未找到要求 · 已查找:第二章、第三章、第六章</dd>
</dl>
</div>
<div class="card-foot"><button class="btn sm" data-ai-ok>确认无误</button><button class="btn sm text">修改</button><span class="r"><a class="plain cap" href="#">判断有误?</a></span></div>
</div>
<div class="card">
<div class="card-h"><h3>项目要点</h3><span class="more"><span class="sign sm" style="margin:0"><span>✓</span><b>王敏</b><span class="fig">11月2日 14:20</span>确认无误</span></span></div>
<div class="ai-block done">
<dl class="kv">
<dt>法律体系</dt><dd>政府采购 · 公开招标(按「投标无效」口径检查)</dd>
<dt>预算 / 限价</dt><dd>采购包 1:<span class="fig m">286.00</span> 万元(最高限价同预算)<a class="cite-src inline" href="#">第 <span class="fig">2</span> 页</a></dd>
<dt>评标方法</dt><dd>综合评分法:价格 10、技术 40、实施方案 20、售后 10、认证 6、业绩 8<a class="cite-src inline" href="#">第 <span class="fig">31</span> 页</a></dd>
<dt>联合体</dt><dd>不接受 <span class="flag">已修改 · <a class="plain" href="#">查看记录</a></span><a class="cite-src inline" href="#">第 <span class="fig">2</span> 页</a></dd>
<dt>保证金</dt><dd>不收取<a class="cite-src inline" href="#">第 <span class="fig">21</span> 页</a></dd>
<dt>样品</dt><dd class="muted">未找到要求 · 已查找:第二章、第三章、第六章</dd>
</dl>
</div>
<div class="card-foot"><button class="btn sm text"><i data-ic="history"></i>处理记录</button><span class="r"><button class="btn sm text">撤销确认</button></span></div>
</div>
</div>
<ul class="d-rules"><li>确认前:「AI 摘要」蓝铅笔标签 + 左侧虚线;确认后:标签换成绿色署名,虚线变墨色实线,正文本来就是墨色。</li><li>找不到的项写「未找到 · 已查找:…」,不猜测、不留空。胜率之类的数据不足时不展示(红线 5)。</li></ul>
</section>
<!-- 06 关键日期卡 -->
<section class="d-sec" id="s-date">
<div class="d-sec-h"><span class="no">06</span><h2>关键日期卡</h2><span class="ref">规范 §7.9 倒计时 · §9.3</span></div>
<div class="d-grid c2">
<div class="card">
<div class="card-h"><h3>关键日期</h3><span class="more cap muted">以补遗 1 更正后的时间为准</span></div>
<ul class="dates">
<li class="past"><span class="fig">10-28</span><span>采购文件公告发布</span><span></span></li>
<li><span class="fig">11-09</span><span>询问、质疑截止<span class="left">公告期满后 7 个工作日 · 剩 6 天</span></span><a class="cite-src" href="#">第 <span class="fig">6</span> 页</a></li>
<li class="key"><span class="fig">11-20 09:30</span><span><b>投标截止 / 开标</b><span class="left">剩 17 天 · <span class="flag">已被补遗 1 更新,原为 <s>11-18 09:30</s></span></span></span><a class="cite-src" href="#">补遗 1 · 第 <span class="fig">1</span> 页</a></li>
<li><span class="fig">2027-02-18</span><span>投标有效期至<span class="left">截止日起 90 天</span></span><a class="cite-src" href="#">第 <span class="fig">21</span> 页</a></li>
</ul>
<p class="note">MVP 只在站内显示倒计时,不发截止提醒。</p>
</div>
<div class="card">
<div class="card-h"><h3>7 天内截止</h3><span class="more"><a class="plain cap" href="#">全部项目</a></span></div>
<ul class="dates">
<li><span class="fig">今天 17:00</span><span><b>某县数据中心机房改造项目</b><span class="left due-today">剩 6 小时 · 编制中</span></span><span class="cap muted">标段 1</span></li>
<li><span class="fig">11-05 14:30</span><span><b>某区智慧社区平台运维服务</b><span class="left due-soon">剩 2 天 5 小时 · 就绪</span></span><span class="cap muted">全部</span></li>
</ul>
<ul class="d-rules"><li>&gt; 72 小时:墨色「11月20日 09:30 · 剩 17 天」;≤ 72 小时:赭色小字;≤ 24 小时:赭色加粗;已过:辅助灰「已截止」。</li><li>截止临近不用红色、不闪烁。</li></ul>
</div>
</div>
</section>
</main>
<script src="demo.js"></script>
<script>
const { $, toast, dialog } = Demo;
Demo.demoTop("cards.html");
$("[data-cancel]").onclick = () => dialog({ title: "取消解析?", ok: "取消解析", cancel: "继续解析", body: "<p>已完成的阶段不会保留。取消后退回 1 次解析额度,可以稍后重新上传。</p>", onOk: () => toast("已取消解析 · 已退回 1 次解析额度") });
$("[data-ai-ok]").onclick = () => toast("已确认项目要点", { undo: () => toast("已撤销", { ok: false }) });
</script>
</body>
</html>

View File

@@ -0,0 +1,492 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>控件总览 · 标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>① 控件总览</h1>
<p>每个控件按「默认 → 悬停 → 按下 → 聚焦 → 禁用 → 加载中 / 错误」陈列。灰色小字是状态名;标「可以点」的是可交互的。示意数据取自原型的「某市政务服务中心智慧大厅建设项目」。</p>
<nav class="d-toc" aria-label="本页目录">
<a href="#s-btn">01 按钮</a><a href="#s-input">02 输入框</a><a href="#s-select">03 下拉选择</a><a href="#s-date">04 日期选择</a><a href="#s-check">05 复选 · 单选 · 开关</a><a href="#s-tag">06 标签与标识</a><a href="#s-cite">07 出处</a><a href="#s-alert">08 提醒</a><a href="#s-toast">09 轻提示</a><a href="#s-dialog">10 对话框</a><a href="#s-tabs">11 标签页 · 分段 · 分页</a><a href="#s-empty">12 空状态 · 骨架屏 · 进度</a><a href="#s-tip">13 提示气泡 · 菜单 · 快捷键</a>
</nav>
</div>
<!-- 01 按钮 -->
<section class="d-sec" id="s-btn">
<div class="d-sec-h"><span class="no">01</span><h2>按钮</h2><span class="ref">规范 §7.1 · §8.3</span></div>
<div class="d-box">
<h3>类型 × 状态</h3>
<table class="d-matrix">
<thead><tr><th></th><th>默认</th><th>悬停</th><th>按下</th><th>键盘聚焦</th><th>禁用</th><th>加载中</th></tr></thead>
<tbody>
<tr><td>主按钮</td><td><button class="btn primary">发起检查</button></td><td><button class="btn primary is-hover">发起检查</button></td><td><button class="btn primary is-active">发起检查</button></td><td><button class="btn primary is-focus">发起检查</button></td><td><button class="btn primary" disabled>发起检查</button></td><td><button class="btn primary swap is-loading" aria-busy="true"><span class="idle">发起检查</span><span class="busy">正在发起…</span></button></td></tr>
<tr><td>次按钮</td><td><button class="btn">导出报告</button></td><td><button class="btn is-hover">导出报告</button></td><td><button class="btn is-active">导出报告</button></td><td><button class="btn is-focus">导出报告</button></td><td><button class="btn" disabled>导出报告</button></td><td><button class="btn swap is-loading" aria-busy="true"><span class="idle">导出报告</span><span class="busy">正在导出…</span></button></td></tr>
<tr><td>文字按钮</td><td><button class="btn text">查看处理记录</button></td><td><button class="btn text is-hover">查看处理记录</button></td><td><button class="btn text is-active">查看处理记录</button></td><td><button class="btn text is-focus">查看处理记录</button></td><td><button class="btn text" disabled>查看处理记录</button></td><td><span class="d-cap">(不用)</span></td></tr>
</tbody>
</table>
<ul class="d-rules">
<li>一屏只有一个主按钮。没有红色的危险按钮:删除用次按钮 + 确认对话框。</li>
<li>加载中把文字换成「正在…」,两段文字叠在同一格里,按较宽的定宽,切换时按钮不跳动;不加转圈图标。</li>
<li>主按钮悬停变浅 8%(用 color-mix 计算,不新增颜色变量);按下回到墨色并加一道内阴影。</li>
</ul>
</div>
<div class="d-grid c3" style="margin-top:var(--sp-6)">
<div class="d-box">
<h3>尺寸</h3>
<div class="d-row"><button class="btn sm">确认无误</button><button class="btn">确认无误</button><button class="btn lg primary">上传招标文件</button></div>
<ul class="d-rules"><li>28:表格行内 · 32:默认 · 40:登录页、空状态</li></ul>
</div>
<div class="d-box">
<h3>带图标 · 只有图标</h3>
<div class="d-row">
<button class="btn"><i data-ic="upload"></i>上传投标文件</button>
<button class="icon-btn bordered" aria-label="下载报告" data-tip="下载报告"><i data-ic="download"></i></button>
<button class="icon-btn" aria-label="处理记录" data-tip="处理记录"><i data-ic="history"></i></button>
<button class="icon-btn" aria-label="删除" data-tip="删除(不可用:项目已归档)" disabled><i data-ic="trash"></i></button>
</div>
<div class="d-row"><span class="btn-group"><button class="btn sm">上一处</button><button class="btn sm">下一处</button></span></div>
<ul class="d-rules"><li>图标必须和文字一起出现;只有图标时要有 aria-label 和悬停提示。</li></ul>
</div>
<div class="d-box">
<h3>会扣点的按钮 <small>可以点</small></h3>
<div class="d-row"><button class="btn primary swap" data-loading="检查已开始,可以离开本页,完成后会发站内通知"><span class="idle">发起检查</span><span class="busy">正在发起…</span></button><span class="cost">扣 1 次检查 · 剩 <span class="fig">12</span> 次</span></div>
<div class="d-row"><button class="btn swap" data-loading="已开始复查"><span class="idle">重新检查</span><span class="busy">正在发起…</span></button><span class="cost">24 小时内复查,不扣额度</span></div>
<div class="d-row"><button class="btn primary" data-nomore>发起检查</button><span class="cost">剩 <span class="fig">0</span> 次 · 按钮仍可点</span></div>
</div>
</div>
</section>
<!-- 02 输入框 -->
<section class="d-sec" id="s-input">
<div class="d-sec-h"><span class="no">02</span><h2>输入框与文本域</h2><span class="ref">规范 §7.13</span></div>
<div class="d-box">
<div class="d-states wide">
<div class="d-state"><small>默认(占位符用辅助灰)</small><div class="field"><label for="i1">项目名称 <small>(必填)</small></label><input class="input" id="i1" placeholder="例如:智慧大厅建设项目"></div></div>
<div class="d-state"><small>悬停</small><div class="field"><label>项目编号</label><input class="input is-hover" placeholder="招标文件封面上的编号"></div></div>
<div class="d-state"><small>聚焦(2px 墨色焦点环 + 2px 间隙)</small><div class="field"><label>项目编号</label><input class="input is-focus" value="ZC-2026-04"></div></div>
<div class="d-state"><small>已填写</small><div class="field"><label>采购人</label><input class="input" value="某市政务服务管理办公室"></div></div>
<div class="d-state"><small>错误(赭色,不用红)</small><div class="field err"><label>联系人手机</label><input class="input" value="1381234567" aria-invalid="true"><span class="msg"><span class="ico w">!</span>手机号应为 11 位数字</span></div></div>
<div class="d-state"><small>禁用</small><div class="field dis"><label>法律体系</label><input class="input" value="政府采购" disabled></div></div>
<div class="d-state"><small>只读(系统识别结果,可复制)</small><div class="field"><label>文件页数</label><input class="input" value="186 页" readonly></div></div>
<div class="d-state"><small>带说明文字</small><div class="field"><label>投标有效期</label><div class="input-unit"><input class="input" value="90"><span class="unit">天</span></div><span class="msg">从投标截止日起算,招标文件第 21 页</span></div></div>
</div>
</div>
<div class="d-grid c2" style="margin-top:var(--sp-6)">
<div class="d-box">
<h3>文本域 · 字数 <small>可以输入</small></h3>
<div class="field fill" data-count-field>
<label for="ta1">忽略原因 <small>(必填,至少 5 个字)</small></label>
<textarea class="input" id="ta1" maxlength="200" placeholder="写清楚为什么不处理,报告里会如实列出"></textarea>
<div class="msg-row"><span class="msg">报告和处理记录里会显示这段原因。</span><span class="count"><span data-n>0</span> / 200</span></div>
</div>
</div>
<div class="d-box">
<h3>搜索框</h3>
<label class="search"><i data-ic="search"></i><input class="input" placeholder="搜索项目名称、采购人、项目编号" aria-label="搜索项目"><span class="kbd">/</span></label>
<div style="height:var(--sp-4)"></div>
<label class="search" style="width:240px"><i data-ic="search"></i><input class="input" value="服务器" aria-label="搜索清单"></label>
<ul class="d-rules"><li>搜索框的放大镜只做辅助,占位符写清楚能搜什么。</li><li>输入框高 32,左右内边距 10;数值输入用 Barlow 等宽数字。</li></ul>
</div>
</div>
</section>
<!-- 03 下拉选择 -->
<section class="d-sec" id="s-select">
<div class="d-sec-h"><span class="no">03</span><h2>下拉选择</h2><span class="ref">AR10 · Reka UI Select 打底</span></div>
<div class="d-box">
<div class="d-states wide">
<div class="d-state"><small>未选择</small><div class="field"><label>采购方式 <small>(必填)</small></label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val"><span class="ph">请选择</span></span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li class="grp">政府采购</li><li role="option">公开招标</li><li role="option">邀请招标</li><li role="option">竞争性磋商</li><li role="option">竞争性谈判</li><li role="option">询价</li><li class="grp">招标投标</li><li role="option">公开招标(招标投标法)</li></ul></div></div></div>
<div class="d-state"><small>已选择 · 可以点</small><div class="field"><label>采购方式</label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val">公开招标</span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li class="grp">政府采购</li><li role="option" aria-selected="true">公开招标</li><li role="option">邀请招标</li><li role="option">竞争性磋商</li><li role="option">竞争性谈判</li><li role="option">询价</li><li class="grp">招标投标</li><li role="option">公开招标(招标投标法)</li></ul></div></div></div>
<div class="d-state d-open"><small>展开(浮层:4px 圆角 + 阴影)</small><div class="field"><label>所投的包</label>
<div class="pop"><button type="button" class="select is-focus" aria-expanded="true"><span class="val">采购包 1 · 智慧大厅软硬件集成</span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option" aria-selected="true">采购包 1 · 智慧大厅软硬件集成</li><li role="option" class="is-active">采购包 2 · 机房网络设备更新</li><li role="option" aria-disabled="true">采购包 3 · 已终止<small>补遗 1</small></li></ul></div></div></div>
<div class="d-state"><small>错误</small><div class="field err"><label>资质类型 <small>(必填)</small></label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-invalid="true"><span class="val"><span class="ph">请选择</span></span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option">资质证书</li><li role="option">体系认证</li><li role="option">软件著作权</li><li role="option">其他</li></ul></div>
<span class="msg"><span class="ico w">!</span>请选择资质类型,查标时按类型核对</span></div></div>
<div class="d-state"><small>禁用</small><div class="field dis"><label>法律体系</label><div class="pop"><button type="button" class="select" disabled><span class="val">政府采购</span><i data-ic="chev-down"></i></button></div><span class="msg">由招标文件识别,在项目设置里修改</span></div></div>
<div class="d-state"><small>表格行内(28 高)</small><div class="pop inline" data-select><button type="button" class="select sm" aria-haspopup="listbox" aria-expanded="false" style="width:132px"><span class="val">技术册</span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option">商务册</li><li role="option" aria-selected="true">技术册</li><li role="option">报价册</li><li role="option">资格证明册</li></ul></div></div>
</div>
<ul class="d-rules"><li>键盘:↑ ↓ 移动、Enter 选中、Esc 收起。选中项加粗并在右侧打 ✓(形状 + 字重,不只靠颜色)。</li><li>不可选的项用禁用灰,并在右侧写原因。</li></ul>
</div>
</section>
<!-- 04 日期选择 -->
<section class="d-sec" id="s-date">
<div class="d-sec-h"><span class="no">04</span><h2>日期选择</h2><span class="ref">规范 §9.3 · AR10</span></div>
<div class="d-grid c3">
<div class="d-box">
<h3>收起 · 可以点</h3>
<div class="field"><label>投标截止 <small>(必填)</small></label>
<div class="dt-pair"><div class="pop" data-date data-value="2026-11-20" data-marks="2026-11-9,2026-11-20"><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val">2026-11-20</span><i data-ic="calendar"></i></button><div class="cal"></div></div><input class="input date-trigger" value="09:30" aria-label="时间"></div>
<span class="msg">以补遗 1 的更正为准 · 剩 <span class="fig m">17</span> 天</span></div>
<div class="field" style="margin-top:var(--sp-4)"><label>证书有效期至</label>
<div class="pop" data-date data-month="2026-11"><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val"><span class="ph">选择日期</span></span><i data-ic="calendar"></i></button><div class="cal"></div></div></div>
<div class="field err" style="margin-top:var(--sp-4)"><label>发证日期</label>
<div class="pop"><button type="button" class="select date-trigger" aria-invalid="true"><span class="val">2027-03-01</span><i data-ic="calendar"></i></button></div>
<span class="msg"><span class="ico w">!</span>发证日期晚于有效期,请核对证书</span></div>
</div>
<div class="d-box">
<h3>展开</h3>
<div class="pop d-open" data-date data-value="2026-11-20" data-marks="2026-11-9,2026-11-20" data-time="09:30"><div class="cal"></div></div>
<ul class="d-rules"><li>一周从周一开始;数字用 Barlow 等宽。</li><li>今天:控件灰描边;选中:墨底白字;关键日期:下方小圆点。</li></ul>
</div>
<div class="d-box">
<h3>有不可选的日期</h3>
<div class="pop d-open" data-date data-month="2026-11" data-min="2026-11-03"><div class="cal"></div></div>
<ul class="d-rules"><li>不可选的日期加删除线(不只靠变灰)。</li><li>界面显示「11月20日 09:30」,输入框里用「2026-11-20」便于核对和粘贴。</li></ul>
</div>
</div>
</section>
<!-- 05 复选 单选 开关 -->
<section class="d-sec" id="s-check">
<div class="d-sec-h"><span class="no">05</span><h2>复选 · 单选 · 开关</h2><span class="ref">规范 §8.2 批量勾选</span></div>
<div class="d-grid c3">
<div class="d-box">
<h3>复选框</h3>
<div class="d-states">
<div class="d-state"><small>未选</small><label class="choice"><input type="checkbox" class="ck">提示</label></div>
<div class="d-state"><small>已选</small><label class="choice"><input type="checkbox" class="ck" checked>提示</label></div>
<div class="d-state"><small>半选(表头全选)</small><label class="choice"><input type="checkbox" class="ck" data-indeterminate>全选</label></div>
<div class="d-state"><small>聚焦</small><label class="choice"><input type="checkbox" class="ck is-focus">重要</label></div>
<div class="d-state"><small>禁用</small><label class="choice dis" data-tip="只有「提示」级可以批量处理"><input type="checkbox" class="ck" disabled>致命</label></div>
<div class="d-state"><small>禁用 · 已选</small><label class="choice dis"><input type="checkbox" class="ck" disabled checked>已归档</label></div>
</div>
<div class="choices" style="margin-top:var(--sp-5)">
<label class="choice"><input type="checkbox" class="ck" checked><span>电子投标文件已使用 CA 数字证书加密<small>需人工核对 · 招标文件第 21 页</small></span></label>
<label class="choice"><input type="checkbox" class="ck"><span>在截止时间前于电子交易平台上传,并取得回执<small>需人工核对 · 招标文件第 6 页</small></span></label>
</div>
</div>
<div class="d-box">
<h3>单选</h3>
<div class="d-states">
<div class="d-state"><small>未选</small><label class="choice"><input type="radio" class="ck" name="r0">招标投标</label></div>
<div class="d-state"><small>已选</small><label class="choice"><input type="radio" class="ck" name="r0b" checked>政府采购</label></div>
<div class="d-state"><small>禁用</small><label class="choice dis"><input type="radio" class="ck" disabled>企业自主采购</label></div>
<div class="d-state"><small>禁用 · 已选</small><label class="choice dis"><input type="radio" class="ck" disabled checked>政府采购</label></div>
</div>
<span class="d-label" style="margin-top:var(--sp-5)">单选卡片(选项需要说明时)</span>
<div class="choices">
<label class="choice-card"><input type="radio" class="ck" name="r1" checked><span><b>政府采购</b><small>按《政府采购法》和 87 号令检查,结论用「投标无效」</small></span></label>
<label class="choice-card"><input type="radio" class="ck" name="r1"><span><b>招标投标</b><small>按《招标投标法》检查,结论用「否决投标」</small></span></label>
</div>
</div>
<div class="d-box">
<h3>开关 <small>可以点</small></h3>
<div class="d-states">
<div class="d-state"><small>关</small><button type="button" class="switch" role="switch" aria-checked="false"><span class="track"></span><span class="st"></span></button></div>
<div class="d-state"><small>开</small><button type="button" class="switch" role="switch" aria-checked="true"><span class="track"></span><span class="st"></span></button></div>
<div class="d-state"><small>禁用 · 关</small><button type="button" class="switch" role="switch" aria-checked="false" disabled><span class="track"></span><span class="st"></span></button></div>
<div class="d-state"><small>禁用 · 开</small><button type="button" class="switch" role="switch" aria-checked="true" disabled><span class="track"></span><span class="st"></span></button></div>
</div>
<div class="choices" style="margin-top:var(--sp-5)">
<label class="choice" style="justify-content:space-between;width:100%"><span>额度用到 80% 时站内提醒<small>每期只提醒一次</small></span><button type="button" class="switch" role="switch" aria-checked="true" aria-label="额度提醒"><span class="track"></span><span class="st"></span></button></label>
<label class="choice" style="justify-content:space-between;width:100%"><span>证书长期有效<small>打开后不检查有效期</small></span><button type="button" class="switch" role="switch" aria-checked="false" aria-label="长期有效"><span class="track"></span><span class="st"></span></button></label>
</div>
<ul class="d-rules"><li>规范没有定义开关。这里沿用 2px 圆角做成方形滑块(不用胶囊形),旁边写「开 / 关」,不只靠颜色。</li></ul>
</div>
</div>
</section>
<!-- 06 标签与标识 -->
<section class="d-sec" id="s-tag">
<div class="d-sec-h"><span class="no">06</span><h2>级别标签 · 状态标签 · AI 标识 · 署名</h2><span class="ref">规范 §3.3 · §7.2 · §7.5 · §7.16</span></div>
<div class="d-grid c2">
<div class="d-box">
<h3>问题级别(实心 / 描边 / 无框)</h3>
<div class="d-row"><span class="sev fatal">致命</span><span class="sev major">重要</span><span class="sev hint">提示</span><span class="d-cap" style="margin-left:var(--sp-4)">计数</span><span class="sev fatal">致命<span class="fig">3</span></span><span class="sev major">重要<span class="fig">3</span></span><span class="sev hint">提示<span class="fig">3</span></span></div>
<div class="d-row"><span class="d-cap">参数判断</span><span class="verdict fatal">疑似不满足</span><span class="verdict major">疑似不满足</span><span>满足</span><span class="verdict manual">需人工判断</span><span class="verdict major">未找到响应</span></div>
<h3 style="margin-top:var(--sp-6)">项目状态 · 就绪 · 复查标记</h3>
<div class="d-row"><span class="tag status">跟进中</span><span class="tag status">编制中</span><span class="tag status">已检查</span><span class="tag status">已递交</span><span class="tag status">已归档</span></div>
<div class="d-row"><span>未就绪</span><span class="tag status" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span><span class="d-row" style="gap:var(--sp-2)"><span class="flag box">新增</span><span class="flag box">沿用</span><span class="flag box">本次已解决</span></span></div>
<div class="d-row"><span class="d-cap">清单状态</span><span class="tag">草稿 v3</span><span class="tag status">已确认 v2</span><span class="sign sm" style="margin:0"><span>✓</span><b>王敏</b><span class="fig">11月2日 15:12</span>确认</span></div>
<div class="d-row"><span class="d-cap">分类</span><span class="tag">资格条件</span><span class="tag">无效条款</span><span class="tag">技术参数</span><span class="tag">采购包 1</span><span class="tag">通用</span></div>
</div>
<div class="d-box">
<h3>AI 产出 → 人工确认(颜色说明谁说的)</h3>
<div class="d-row"><span class="tag ai">AI 摘要</span><span class="tag ai">AI 初稿</span><span class="src-ai">AI 提取</span><span>人工添加</span><span>补遗 1</span><span class="flag">已修改 · <a class="plain" href="#">查看记录</a></span></div>
<div class="d-stack" style="margin-top:var(--sp-5)">
<div><span class="d-label">AI 产出:蓝铅笔标签 + 左侧虚线</span>
<div class="ai-block"><span class="ai-label"><span class="tag ai">AI 摘要</span>未确认</span>预算 418.00 万元,分 2 个采购包;采购包 1 最高限价 286.00 万元。<a class="cite-src inline" href="#">招标文件 · <span class="fig">第 2 页</span></a></div></div>
<div><span class="d-label">人工确认后:墨色正文 + 实线 + 绿色署名</span>
<div class="ai-block done">预算 418.00 万元,分 2 个采购包;采购包 1 最高限价 286.00 万元。<a class="cite-src inline" href="#">招标文件 · <span class="fig">第 2 页</span></a>
<div class="sign sm"><span>✓</span><b>王敏</b><span class="fig">11月2日 14:20</span>确认无误</div></div></div>
<div><span class="d-label">问题处理后的署名行</span>
<div class="sign" style="margin:0"><span>✓</span><b>王敏</b><span class="fig">11月2日 14:20</span><span>已修改</span><a class="plain undo" href="#">撤销</a></div>
<div class="sign ign"><span>–</span><b>王敏</b><span class="fig">11月2日 14:26</span><span>已忽略:招标文件允许</span><a class="plain undo" href="#">撤销</a></div></div>
</div>
</div>
</div>
</section>
<!-- 07 出处 -->
<section class="d-sec" id="s-cite">
<div class="d-sec-h"><span class="no">07</span><h2>出处</h2><span class="ref">规范 §7.3 · 最重要的组件</span></div>
<div class="d-grid c3">
<div class="d-box">
<h3>招标原文 → 投标原文</h3>
<div class="cite"><a class="cite-src" href="#">招标文件 · <span class="fig">第 41 页</span></a><q>★ 2U机架式服务器,CPU:配置≥2颗海光架构32C 2.7GHz,<mark>内存: ≥512G DDR4</mark>,系统盘: 2块≥480G SSD……</q></div>
<div class="cite"><a class="cite-src" href="#">技术册 · <span class="fig">第 12 页</span></a><q>第 12 行 <mark>内存:256G DDR4</mark> 无偏离</q></div>
<ul class="d-rules"><li>页码用 Barlow;原句照录,不加空格、不改标点。</li></ul>
</div>
<div class="d-box">
<h3>「未找到」也是出处</h3>
<div class="cite"><a class="cite-src" href="#">招标文件 · <span class="fig">第 44 页</span></a><q>★ 支持硬件BFD,最小报文发送间隔≤50ms,<mark>提供相关证明材料(包含但不限于产品官网公开参数截图证明等)</mark>。</q></div>
<div class="cite missing"><span class="cite-src">投标文件 · 未找到</span><p>证明材料未找到 · 已查找:技术册(全部章节)、商务册(附件)</p></div>
<ul class="d-rules"><li>竖线改虚线,写明找过哪些分册、哪些章节。</li></ul>
</div>
<div class="d-box">
<h3>OCR · 截断 · 当前选中</h3>
<div class="cite on"><a class="cite-src" href="#">商务册 · <span class="fig">第 3 页</span><span class="ocr">OCR</span></a><q>投标人(电子签章):南京某信息技术有限公司  <mark>日期:2026年11月25日</mark></q></div>
<div class="cite" data-clamp><a class="cite-src" href="#">招标文件 · <span class="fig">第 23 页</span></a><q class="clamp"><mark>2.财务状况报告,至少提供:(1)本项目开标时间前6个月内任一时间截点的资产负债表的扫描件1份;(2)本项目开标时间前6个月内任何1月(不含开标当月)利润表月报表的扫描件1份。</mark>3.本项目开标时间前6个月内任何1月的依法缴纳税收和社会保障资金的相关材料的扫描件。</q><button class="expand" data-expand>展开原文</button></div>
<button class="more-cites">另有 2 处</button>
<ul class="d-rules"><li>选中的出处竖线加粗到 4px。超过 3 行截断,「展开原文」看全句。</li></ul>
</div>
</div>
</section>
<!-- 08 提醒 -->
<section class="d-sec" id="s-alert">
<div class="d-sec-h"><span class="no">08</span><h2>提醒(页内 Alert)</h2><span class="ref">规范 §7.12 · §7.15</span></div>
<div class="d-grid c2">
<div>
<div class="alert"><span class="ico i">i</span><div><p>清单未确认,检查结果以当前清单为准。</p></div><span class="act"><button class="btn sm">去确认清单</button></span></div>
<div class="alert warn"><span class="ico w">!</span><div><p><b>本期检查额度已用 80%。</b>剩 <span class="fig">4</span> 次,额度用完后续费或下期发放后可继续。</p></div><span class="act"><a class="plain cap" href="#">查看额度</a></span></div>
<div class="alert warn"><span class="ico w">!</span><div><p><b>投标截止还剩 2 天 5 小时。</b>还有 <span class="fig">3</span> 项致命问题待处理。</p><span class="sub">截止临近用赭色,不用红色;只提醒,不拦截。</span></div></div>
</div>
<div>
<div class="alert warn"><span class="ico w">!</span><div><p><b>解析失败:文件已加密。</b>已退回 1 次解析额度。请上传未加密的版本。</p><span class="sub">关于 ZC-2026-0418 的更正公告.pdf</span></div><span class="act"><button class="btn sm">重新上传</button></span></div>
<div class="alert"><span class="ico i">i</span><div><p>出了点问题,请稍后重试。本次操作未扣额度。</p><span class="sub">错误编号 <span class="errno">E-20261103-7Q4K</span> · 联系客服时请提供</span></div><span class="act"><button class="btn sm">重试</button></span></div>
<div class="alert done"><span class="ico ok">✓</span><div><p>报告已导出:智慧大厅建设项目-辅助检查报告-第 2 次.pdf</p></div></div>
</div>
</div>
<ul class="d-rules"><li>提醒放在相关内容上方,不自动消失。系统错误不用红色,用中性提醒 + 错误编号。</li></ul>
</section>
<!-- 09 轻提示 -->
<section class="d-sec" id="s-toast">
<div class="d-sec-h"><span class="no">09</span><h2>轻提示(Toast)</h2><span class="ref">规范 §7.12</span></div>
<div class="d-grid c2">
<div class="d-box">
<h3>样式</h3>
<div class="d-stack" style="display:flex;flex-direction:column;align-items:flex-start">
<div class="toast static"><span class="ok">✓</span><span>清单 v3 已确认</span></div>
<div class="toast static"><span class="ok">✓</span><span>已标记「已修改」 · 复查时会再查</span><span class="sep"></span><button class="lnk">撤销</button></div>
<div class="toast static"><span>已复制错误编号</span></div>
</div>
</div>
<div class="d-box">
<h3>触发 <small>可以点</small></h3>
<div class="d-row"><button class="btn" data-t1>保存资质</button><button class="btn" data-t2>批量确认无误</button><button class="btn" data-t3>连续触发三条</button></div>
<ul class="d-rules"><li>只用于操作结果;4 秒后消失,鼠标悬停时暂停计时。</li><li>可撤销的操作在轻提示里给「撤销」;失败不用轻提示,用页内提醒说清原因。</li><li>多条从下往上堆叠,屏幕阅读器会播报。</li></ul>
</div>
</div>
</section>
<!-- 10 对话框 -->
<section class="d-sec" id="s-dialog">
<div class="d-sec-h"><span class="no">10</span><h2>对话框</h2><span class="ref">规范 §7.14 · 只用于不可撤销的操作</span></div>
<div class="d-grid c-side">
<div class="dialog static" role="dialog" aria-labelledby="sd-t">
<h2 id="sd-t">删除项目「某区智慧社区平台运维服务」?</h2>
<div class="body">
<p>项目下的招标文件、清单、检查结果和报告会一起删除。删除后 30 天内可以在回收站恢复,之后彻底删除。</p>
<div class="field fill"><label for="sd-n">输入项目名称以确认</label><input class="input" id="sd-n" placeholder="某区智慧社区平台运维服务"></div>
</div>
<div class="foot"><button class="btn">取消</button><button class="btn primary" disabled>删除项目</button></div>
</div>
<div class="d-box">
<h3>打开真实对话框 <small>可以点</small></h3>
<div class="d-stack" style="display:flex;flex-direction:column;align-items:flex-start">
<button class="btn" data-d1><i data-ic="trash"></i>删除项目</button>
<button class="btn" data-d2><i data-ic="send"></i>标记已递交</button>
<button class="btn" data-d3>忽略并注明原因</button>
</div>
<ul class="d-rules"><li>宽 480(确认)/ 640(表单),圆角 4。</li><li>标题写要做的事,正文写后果;主操作在右,按钮写动作「删除项目」,不写「确定」。</li><li>删除没有红色:靠输入项目名确认。Esc 关闭,焦点回到触发按钮。</li></ul>
</div>
</div>
</section>
<!-- 11 标签页 分段 分页 -->
<section class="d-sec" id="s-tabs">
<div class="d-sec-h"><span class="no">11</span><h2>标签页 · 分段控件 · 分页</h2><span class="ref">规范 §5.3</span></div>
<div class="d-grid c2">
<div class="d-box">
<h3>标签页 <small>可以点,← → 切换</small></h3>
<div class="tabs" role="tablist" aria-label="项目">
<button role="tab" aria-selected="true" aria-controls="tp1">概览</button>
<button role="tab" aria-selected="false" aria-controls="tp2" tabindex="-1">读标<span class="fig muted">26</span></button>
<button role="tab" aria-selected="false" aria-controls="tp3" tabindex="-1">查标<span class="sev fatal">致命<span class="fig">3</span></span></button>
<button role="tab" aria-selected="false" aria-disabled="true" tabindex="-1" data-tip="先完成一次检查">报告</button>
</div>
<div class="tabpanel" id="tp1" role="tabpanel"><p class="muted" style="margin:0">项目要点、关键日期、文件和检查记录。</p></div>
<div class="tabpanel" id="tp2" role="tabpanel" hidden><p class="muted" style="margin:0">项目检查清单:26 条,其中待确认 2 条。</p></div>
<div class="tabpanel" id="tp3" role="tabpanel" hidden><p class="muted" style="margin:0">第 2 次检查:致命 3、重要 3、提示 3。</p></div>
</div>
<div class="d-box">
<h3>分段控件(筛选) <small>可以点</small></h3>
<div class="d-row"><div class="seg" role="group" aria-label="按状态筛选"><button aria-pressed="true">进行中 <span class="fig">4</span></button><button aria-pressed="false">全部 <span class="fig">6</span></button><button aria-pressed="false">已递交</button><button aria-pressed="false">已归档</button></div></div>
<div class="d-row"><div class="seg" role="group" aria-label="按级别筛选"><button aria-pressed="false">全部 <span class="fig">9</span></button><button aria-pressed="true"><span class="sev fatal">致命<span class="fig">3</span></span></button><button aria-pressed="false"><span class="sev major">重要<span class="fig">3</span></span></button><button aria-pressed="false"><span class="sev hint">提示<span class="fig">3</span></span></button></div></div>
</div>
</div>
<div class="d-box" style="margin-top:var(--sp-6)">
<h3>分页 <small>可以点</small></h3>
<div class="pagination" data-pager></div>
<ul class="d-rules"><li>当前页用墨色描边 + 加粗(不填色,避免和主按钮抢)。每页条数固定 20 / 50,不做无限滚动,方便「第几页第几条」沟通。</li></ul>
</div>
</section>
<!-- 12 空状态 骨架屏 进度 -->
<section class="d-sec" id="s-empty">
<div class="d-sec-h"><span class="no">12</span><h2>空状态 · 骨架屏 · 进度</h2><span class="ref">规范 §7.10 · §7.11 · §7.15</span></div>
<div class="d-grid c3">
<div class="d-box">
<h3>空状态</h3>
<div class="empty" style="padding:var(--sp-6) 0"><i data-ic="file" class="xl"></i><b>还没有招标文件</b><p>上传后,标博士会逐条提取要求,每条都附原文出处。</p><button class="btn lg primary"><i data-ic="upload"></i>上传招标文件</button><span class="cost" style="margin-top:var(--sp-2)">扣 1 次解析 · 剩 <span class="fig">37</span> 次</span></div>
</div>
<div class="d-box">
<h3>骨架屏(加载清单)</h3>
<div aria-busy="true" aria-label="正在加载清单">
<div class="skel-row"><span class="skel"></span><div><span class="skel w80"></span><span class="skel w30"></span></div><span class="skel"></span></div>
<div class="skel-row"><span class="skel"></span><div><span class="skel w60"></span><span class="skel w20"></span></div><span class="skel"></span></div>
<div class="skel-row"><span class="skel"></span><div><span class="skel w80"></span><span class="skel w30"></span></div><span class="skel"></span></div>
<div class="skel-row"><span class="skel"></span><div><span class="skel w60"></span><span class="skel w20"></span></div><span class="skel"></span></div>
</div>
<ul class="d-rules"><li>不用转圈占满屏。骨架做缓慢的明暗变化(1.6 秒),系统开了「减少动态效果」时静止。</li></ul>
</div>
<div class="d-box">
<h3>分阶段进度</h3>
<ol class="steps">
<li class="done"><span class="dot">✓</span><span>识别文件格式</span></li>
<li class="done"><span class="dot">✓</span><span>提取文字</span><span class="fig">186 页</span></li>
<li class="done"><span class="dot">✓</span><span>识别分包</span><span class="fig">2 个包</span></li>
<li class="now"><span class="dot"></span><span>提取清单</span><span class="fig">62%</span></li>
<li class="todo"><span class="dot"></span><span>核对出处</span></li>
</ol>
<div class="progress" style="margin-top:var(--sp-2)"><div class="bar"><i style="width:62%"></i></div><span class="fig">62%</span></div>
<p class="note">可以离开本页,完成后会发站内通知。</p>
</div>
</div>
<div class="d-grid c3" style="margin-top:var(--sp-6)">
<div class="d-box quota"><small>检查剩余 · 正常</small><span class="big">12</span><span class="unit">/ 20 次</span><div class="bar"><i style="width:40%"></i></div><p>已用 8 次</p></div>
<div class="d-box quota"><small>检查剩余 · 已用 80%</small><span class="big">4</span><span class="unit">/ 20 次</span><div class="bar warn"><i style="width:80%"></i></div><p class="warn">已用 80%,剩 4 次</p></div>
<div class="d-box quota"><small>检查剩余 · 已用完</small><span class="big">0</span><span class="unit">/ 20 次</span><div class="bar warn"><i style="width:100%"></i></div><p class="warn">本期检查额度已用完,续费或下期发放后可继续</p></div>
</div>
</section>
<!-- 13 提示气泡 菜单 快捷键 -->
<section class="d-sec" id="s-tip">
<div class="d-sec-h"><span class="no">13</span><h2>提示气泡 · 下拉菜单 · 快捷键</h2><span class="ref">规范 §6 · §8.2</span></div>
<div class="d-grid c3">
<div class="d-box">
<h3>提示气泡</h3>
<div class="d-row" style="padding-bottom:var(--sp-12)"><span class="tag status tip-on tip-below tip-start" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span></div>
<div class="d-row"><button class="btn sm" data-tip="我核对过,投标文件这里没问题(例如响应写在别处)">确认无误</button><button class="btn sm" data-tip="我已经在投标文件里改了,复查时会再查">已修改</button><button class="btn sm" data-tip="我知道这个问题,决定不处理(必须填原因)">忽略并注明原因</button></div>
<ul class="d-rules"><li>处理三按钮的悬停提示写清含义(§7.4)。墨底白字,4px 圆角。</li></ul>
</div>
<div class="d-box">
<h3>下拉菜单(行内更多操作) <small>可以点</small></h3>
<div class="d-row">
<div class="pop inline" data-menu><button type="button" class="btn" data-menu-trigger aria-haspopup="menu" aria-expanded="false">更多操作<i data-ic="chev-down"></i></button>
<ul class="menu" role="menu"><li role="menuitem" data-toast="已打开处理记录"><i data-ic="history"></i>处理记录</li><li role="menuitem" data-toast="已复制项目"><i data-ic="layers"></i>复制为新项目</li><li role="menuitem" data-toast="已追加补遗"><i data-ic="upload"></i>追加补遗 / 澄清</li><hr><li role="menuitem" data-toast="已标记已递交"><i data-ic="send"></i>标记已递交</li><li role="menuitem" aria-disabled="true"><i data-ic="archive"></i>归档<small>递交后可用</small></li><li role="menuitem" data-toast="(演示)删除会打开确认对话框"><i data-ic="trash"></i>删除项目</li></ul></div>
</div>
<div class="pop d-open" style="margin-top:var(--sp-3)"><ul class="menu" role="menu" style="min-width:0;width:240px"><li role="menuitem"><i data-ic="history"></i>处理记录</li><li role="menuitem" class="is-active"><i data-ic="layers"></i>复制为新项目</li><hr><li role="menuitem"><i data-ic="send"></i>标记已递交</li><li role="menuitem" aria-disabled="true"><i data-ic="archive"></i>归档<small>递交后可用</small></li><li role="menuitem"><i data-ic="trash"></i>删除项目</li></ul></div>
</div>
<div class="d-box">
<h3>快捷键</h3>
<table class="data">
<tbody>
<tr><td><span class="kbd">J</span> / <span class="kbd">K</span></td><td>下一条 / 上一条</td></tr>
<tr><td><span class="kbd">O</span> · <span class="kbd">Enter</span></td><td>打开当前出处</td></tr>
<tr><td><span class="kbd">1</span> <span class="kbd">2</span> <span class="kbd">3</span></td><td>确认无误 / 已修改 / 忽略</td></tr>
<tr><td><span class="kbd">U</span></td><td>撤销当前条</td></tr>
<tr><td><span class="kbd">?</span></td><td>快捷键说明</td></tr>
</tbody>
</table>
<p class="note">在「④ 列表与表格」和「⑥ 读标」页可以实际用 J / K。</p>
</div>
</div>
</section>
</main>
<script src="demo.js"></script>
<script>
const { $, $$, toast, dialog, icon } = Demo;
Demo.demoTop("controls.html");
/* 文本域字数 */
$$("[data-count-field]").forEach((f) => {
const t = $("textarea", f), n = $("[data-n]", f);
t.addEventListener("input", () => { n.textContent = t.value.length; });
t.addEventListener("blur", () => { const bad = t.value.trim().length > 0 && t.value.trim().length < 5; f.classList.toggle("err", bad); $(".msg", f).innerHTML = bad ? '<span class="ico w">!</span>至少写 5 个字,说明为什么不处理' : "报告和处理记录里会显示这段原因。"; });
});
/* 展开原文 */
$$("[data-expand]").forEach((b) => b.onclick = () => { const q = b.previousElementSibling; const on = q.classList.toggle("clamp"); q.style.webkitLineClamp = on ? "" : "unset"; b.textContent = on ? "展开原文" : "收起"; });
/* 额度不足:按钮仍可点,点了说明原因 */
$("[data-nomore]").onclick = () => dialog({ title: "本期检查额度已用完", ok: "联系客服加额度", cancel: "关闭",
body: "<p>这次检查需要 1 次检查额度,本期已用完 20 次。续费或下期(2027-11-01)发放后可以继续。</p><p class='muted cap'>24 小时内对同一项目的复查不扣额度,不受影响。</p>" });
/* 轻提示 */
$("[data-t1]").onclick = () => toast("已保存「ISO/IEC 27001 信息安全管理体系」");
$("[data-t2]").onclick = () => toast("已将 3 项「提示」标记为确认无误", { undo: () => toast("已撤销", { ok: false }) });
$("[data-t3]").onclick = () => { toast("清单 v3 已确认"); setTimeout(() => toast("已核对补遗更新"), 300); setTimeout(() => toast("报告已导出"), 600); };
/* 对话框 */
$("[data-d1]").onclick = () => dialog({
title: "删除项目「某区智慧社区平台运维服务」?", ok: "删除项目",
body: `<p>项目下的招标文件、清单、检查结果和报告会一起删除。删除后 30 天内可以在回收站恢复,之后彻底删除。</p>
<div class="field fill"><label for="dn">输入项目名称以确认</label><input class="input" id="dn" placeholder="某区智慧社区平台运维服务" autocomplete="off"></div>`,
onOpen: (s) => { const ok = $("[data-ok]", s), i = $("#dn", s); ok.disabled = true; i.addEventListener("input", () => { ok.disabled = i.value.trim() !== "某区智慧社区平台运维服务"; }); },
onOk: () => toast("已删除,可在回收站恢复(30 天内)")
});
$("[data-d2]").onclick = () => dialog({
title: "标记「某市政务服务中心智慧大厅建设项目」为已递交?", ok: "标记已递交",
body: `<div class="alert warn"><span class="ico w">!</span><div><p><b>还有 3 项致命问题待处理。</b>标记已递交不受影响,处理记录会如实保留。</p></div></div>
<p style="margin-top:var(--sp-3)">标记后项目进入「已递交」,不能再发起检查;清单和检查结果保留,可以导出报告。</p>`,
onOk: () => toast("已标记为已递交")
});
$("[data-d3]").onclick = () => dialog({
title: "忽略并注明原因", wide: true, ok: "忽略这一项",
body: `<p><span class="sev major">重要</span> <b>开标一览表与分项价格表合计不一致</b></p>
<div class="field fill"><label for="ir">原因 <small>(必填,至少 5 个字)</small></label>
<div class="chips"><button type="button" class="btn" data-chip>招标文件允许</button><button type="button" class="btn" data-chip>已在其他分册响应</button><button type="button" class="btn" data-chip>与本项目无关</button></div>
<textarea class="input" id="ir" placeholder="写清楚为什么不处理,报告里会如实列出"></textarea><span class="msg">「已忽略」不等于确认无误,报告里单独列出。</span></div>`,
onOpen: (s) => { $$("[data-chip]", s).forEach((c) => c.onclick = () => { $("#ir", s).value = c.textContent; $("#ir", s).focus(); }); },
onOk: (s) => { const v = $("#ir", s).value.trim(); const f = $("#ir", s).closest(".field"); if (v.length < 5) { f.classList.add("err"); $(".msg", f).innerHTML = '<span class="ico w">!</span>至少写 5 个字,说明为什么不处理'; return false; } toast("已忽略 · 原因已记录", { undo: () => toast("已撤销", { ok: false }) }); }
});
/* 分页 */
(function pager() {
const el = $("[data-pager]"); let cur = 1; const total = 86, per = 20, pages = Math.ceil(total / per);
const render = () => {
const nums = []; for (let i = 1; i <= pages; i++) nums.push(i);
el.innerHTML = `<span class="total">共 <span class="fig">${total}</span> 条 · 每页 <span class="fig">${per}</span> 条</span>
<button class="pg" data-p="${cur - 1}" ${cur === 1 ? "disabled" : ""} aria-label="上一页">${icon("chev-left")}</button>
${nums.map((n) => `<button class="pg" data-p="${n}" ${n === cur ? 'aria-current="page"' : ""}>${n}</button>`).join("")}
<button class="pg" data-p="${cur + 1}" ${cur === pages ? "disabled" : ""} aria-label="下一页">${icon("chev-right")}</button>
<span class="jump">跳到 <input class="input" value="${cur}" aria-label="页码"> 页</span>`;
};
el.addEventListener("click", (e) => { const b = e.target.closest("[data-p]"); if (b && !b.disabled) { cur = +b.dataset.p; render(); } });
el.addEventListener("change", (e) => { const v = +e.target.value; if (v >= 1 && v <= pages) cur = v; render(); });
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,524 @@
/* 标博士 · UI demo 样式(2026-11-03)
* 依赖 ../system/tokens.css 和 ../system/components.css,只用 tokens.css 的变量,不写裸色值。
* 分两部分:
* A. 演示页排版:只属于 demo 页面(章节标题、状态陈列格),不进组件库;
* B. 新增组件:design-system.md 有描述、但 components.css 里还没有的组件。UI 精修定稿后再并入 components.css。
* 新增组件清单见 index.html。 */
/* =====================================================================
* A. 演示页排版
* ===================================================================== */
body.demo{min-width:1280px}
.d-top{position:sticky;top:0;z-index:30;display:flex;align-items:center;gap:var(--sp-8);height:52px;padding:0 var(--sp-8);background:var(--bg-page);border-bottom:1px solid var(--border-subtle)}
.d-top .brand img{height:26px;display:block}
.d-top .tag{margin-left:calc(-1 * var(--sp-5))}
.d-nav{display:flex;gap:var(--sp-1);margin-left:auto}
.d-nav a{display:flex;align-items:center;height:52px;padding:0 var(--sp-3);color:var(--text-secondary);text-decoration:none;white-space:nowrap}
.d-nav a:hover{color:var(--text-primary)}
.d-nav a.on{color:var(--text-primary);font-weight:var(--fw-bold);box-shadow:inset 0 -2px 0 var(--c-ink)}
.d-page{max-width:calc(var(--content-max) + 2 * var(--sp-8));margin:0 auto;padding:var(--sp-10) var(--sp-8) var(--sp-16)}
.d-head h1{font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-serif);margin:0}
.d-head p{color:var(--text-secondary);margin:var(--sp-2) 0 0;max-width:60em}
.d-toc{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4);margin-top:var(--sp-4);font-size:var(--fs-caption)}
.d-toc a{color:var(--text-secondary);text-underline-offset:3px}
.d-toc a:hover{color:var(--text-primary)}
.d-sec{margin-top:var(--sp-12);scroll-margin-top:72px}
.d-sec-h{display:flex;align-items:baseline;gap:var(--sp-3);border-bottom:2px solid var(--border-strong);padding-bottom:var(--sp-2);margin-bottom:var(--sp-5)}
.d-sec-h h2{font:var(--fw-bold) var(--fs-h2)/var(--lh-h2) var(--font-serif);margin:0}
.d-sec-h .no{font:var(--fw-bold) var(--fs-title)/1 var(--font-figures);color:var(--text-secondary)}
.d-sec-h .ref{margin-left:auto;font-size:var(--fs-caption);color:var(--text-secondary)}
.d-grid{display:grid;gap:var(--sp-6);align-items:start}
.d-grid.c2{grid-template-columns:repeat(2,minmax(0,1fr))}
.d-grid.c3{grid-template-columns:repeat(3,minmax(0,1fr))}
.d-grid.eq{align-items:stretch}
.d-grid.c-side{grid-template-columns:minmax(0,1fr) 360px}
.d-box{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:var(--sp-5) var(--sp-6)}
.d-box > h3{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0 0 var(--sp-4);display:flex;gap:var(--sp-2);align-items:baseline}
.d-box > h3 small{font-weight:var(--fw-regular);font-size:var(--fs-caption);color:var(--text-secondary)}
.d-box.flat{background:none;border-style:dashed;border-color:var(--border-control)}
.d-states{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:var(--sp-5) var(--sp-5)}
.d-states.wide{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
.d-state{display:flex;flex-direction:column;align-items:flex-start;gap:var(--sp-2);min-width:0}
.d-state > small,.d-cap{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.d-state .field{width:100%}
.d-rules{margin:var(--sp-4) 0 0;padding:var(--sp-3) 0 0 1.2em;border-top:1px solid var(--border-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.d-rules li + li{margin-top:2px}
.d-label{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);margin-bottom:var(--sp-2)}
.d-stack > * + *{margin-top:var(--sp-4)}
.d-row{display:flex;flex-wrap:wrap;gap:var(--sp-3);align-items:center}
.d-row + .d-row{margin-top:var(--sp-3)}
.d-matrix{border-collapse:collapse;width:100%}
.d-matrix th{font-weight:var(--fw-regular);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);text-align:left;padding:0 var(--sp-3) var(--sp-2) 0;white-space:nowrap}
.d-matrix td{padding:var(--sp-2) var(--sp-3) var(--sp-2) 0;vertical-align:middle}
.d-matrix td:first-child{font-size:var(--fs-caption);color:var(--text-secondary);white-space:nowrap;width:72px}
.d-open{position:relative;padding-bottom:var(--sp-2)} /* 静态展开的浮层,占位用 */
.d-open .menu,.d-open .cal{position:static;margin-top:var(--sp-1)}
/* 目录页 */
.d-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-4)}
.d-link{display:block;background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:var(--sp-4) var(--sp-5);color:var(--text-primary);text-decoration:none}
.d-link:hover{border-color:var(--border-control)}
.d-link b{display:flex;gap:var(--sp-2);align-items:baseline;font-size:var(--fs-title);line-height:var(--lh-title)}
.d-link b .fig{color:var(--text-secondary);font-size:var(--fs-title)}
.d-link p{margin:var(--sp-2) 0 0;color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption)}
/* =====================================================================
* B. 新增组件
* ===================================================================== */
/* ---- B1 图标(Lucide 线性,1.5px 描边,16 / 20px,§6) ---- */
.ic{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;vertical-align:-3px}
.ic.lg{width:20px;height:20px}
.ic.xl{width:32px;height:32px;stroke-width:1.25}
/* ---- B2 页面框架:侧边栏 + 顶栏(§5.3) ---- */
body.app{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr);min-height:100vh;min-width:1280px}
.sidebar{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;border-right:1px solid var(--border-subtle);background:var(--bg-page);padding:var(--sp-5) var(--sp-3)}
.sidebar .brand{display:block;padding:0 var(--sp-2) var(--sp-6)}
.sidebar .brand img{height:30px;display:block}
.nav{display:flex;flex-direction:column;gap:2px}
.nav a{display:flex;align-items:center;gap:var(--sp-3);height:36px;padding:0 var(--sp-3);border-radius:var(--radius-sm);color:var(--text-primary);text-decoration:none}
.nav a:hover{background:var(--bg-hover)}
.nav a.on{background:var(--bg-surface);box-shadow:inset 2px 0 0 var(--c-ink);font-weight:var(--fw-bold)}
.nav a .fig{margin-left:auto;font-weight:var(--fw-medium);color:var(--text-secondary)}
.nav .sub{margin:var(--sp-5) var(--sp-3) var(--sp-2);font-size:var(--fs-caption);color:var(--text-secondary)}
.sidebar-foot{margin-top:auto;padding:var(--sp-3);border-top:1px solid var(--border-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.sidebar-foot b{display:block;color:var(--text-primary);font-size:var(--fs-body)}
.main{min-width:0;display:flex;flex-direction:column}
.top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:var(--sp-4);height:52px;padding:0 var(--sp-8);background:var(--bg-page);border-bottom:1px solid var(--border-subtle)}
.crumbs{display:flex;gap:var(--sp-2);color:var(--text-secondary);min-width:0;white-space:nowrap;overflow:hidden}
.crumbs a{color:var(--text-secondary);text-decoration:none}
.crumbs a:hover{color:var(--text-primary);text-decoration:underline;text-underline-offset:3px}
.crumbs b{color:var(--text-primary);font-weight:var(--fw-regular);overflow:hidden;text-overflow:ellipsis}
.top-right{margin-left:auto;display:flex;align-items:center;gap:var(--sp-3)}
.quota-chip{display:flex;gap:var(--sp-3);align-items:center;height:28px;padding:0 var(--sp-3);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);color:var(--text-secondary);text-decoration:none;font-size:var(--fs-caption);background:var(--bg-surface)}
.quota-chip.warn{border-color:var(--warn);color:var(--warn-text)}
.quota-chip .fig{color:var(--text-primary);font-size:var(--fs-body);margin-left:3px}
.content{padding:var(--sp-6) var(--sp-8) var(--sp-16);max-width:calc(var(--content-max) + 2 * var(--sp-8));width:100%}
.page-title{display:flex;align-items:flex-end;gap:var(--sp-4);margin-bottom:var(--sp-5)}
.page-title h1{font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-serif);margin:0}
.page-title p{margin:var(--sp-1) 0 0;color:var(--text-secondary)}
.page-title .actions{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
.section-h{display:flex;align-items:center;gap:var(--sp-3);margin:var(--sp-8) 0 var(--sp-3)}
.section-h h2{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0}
.section-h .more{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
/* 项目页头 */
.proj-head{padding:var(--sp-5) var(--sp-8) 0;border-bottom:1px solid var(--border-subtle);background:var(--bg-page)}
.proj-head .line1{display:flex;align-items:flex-start;gap:var(--sp-4)}
.proj-head h1{font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-serif);margin:0}
.proj-head .meta{margin-top:var(--sp-1);color:var(--text-secondary);display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4)}
.proj-head .facts{margin-left:auto;display:flex;gap:var(--sp-6);text-align:right;white-space:nowrap}
.proj-head .facts small{display:block;font-size:var(--fs-caption);color:var(--text-secondary)}
/* ---- B3 按钮补充状态(§7.1) ---- */
.btn:active,.btn.is-active{background:var(--bg-sunken);border-color:var(--c-ink)}
.btn.is-hover{background:var(--bg-hover)}
.btn.primary.is-hover{background:color-mix(in srgb,var(--c-ink) 92%,var(--c-white))}
.btn.primary:active,.btn.primary.is-active{background:var(--c-ink);box-shadow:inset 0 2px 0 color-mix(in srgb,var(--c-white) 18%,transparent)}
.btn.text:active,.btn.text.is-active{background:var(--bg-sunken);border-color:transparent}
.btn.is-focus{outline:2px solid var(--focus-ring);outline-offset:2px}
.btn .ic{margin:0 -2px}
/* 加载中:两个文字叠在同一格,按宽的那个定宽,切换时宽度不变 */
.btn.swap{display:inline-grid;place-items:center}
.btn.swap > span{grid-area:1/1;white-space:nowrap}
.btn.swap > .busy,.btn.swap.is-loading > .idle{visibility:hidden}
.btn.swap.is-loading > .busy{visibility:visible}
.btn.is-loading{cursor:progress}
.btn.primary.is-loading{background:color-mix(in srgb,var(--c-ink) 80%,var(--c-white));border-color:transparent}
.btn.is-loading:not(.primary){color:var(--text-secondary)}
.icon-btn{display:inline-grid;place-items:center;width:32px;height:32px;border-radius:var(--radius-sm);color:var(--text-primary);background:none;border:1px solid transparent;cursor:pointer;position:relative;flex:none}
.icon-btn:hover{background:var(--bg-hover)}
.icon-btn.bordered{border-color:var(--border-control);background:var(--bg-surface)}
.icon-btn.sm{width:28px;height:28px}
.icon-btn .dot{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%;background:var(--c-ink)}
.icon-btn[disabled]{color:var(--text-disabled);cursor:not-allowed;background:none}
.btn-group{display:inline-flex}
.btn-group .btn + .btn{margin-left:-1px}
.btn-group .btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}
.btn-group .btn:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}
/* ---- B4 输入框、文本域、带单位、搜索(§7.13) ---- */
.input{width:100%;transition:border-color var(--dur-fast) var(--ease)}
.input:hover{border-color:var(--c-ink)}
.input:focus,.input.is-focus{outline:2px solid var(--focus-ring);outline-offset:2px;border-color:var(--c-ink)}
.input.is-hover{border-color:var(--c-ink)}
.input[disabled],.input.is-disabled{background:var(--bg-sunken);border-color:var(--bg-sunken);color:var(--text-disabled);cursor:not-allowed}
.input[disabled]::placeholder{color:var(--text-disabled)}
.input[readonly]{background:var(--bg-sunken);border-color:var(--border-subtle)}
.input[readonly]:focus{outline-color:var(--border-control)}
textarea.input{height:auto;min-height:80px;padding:var(--sp-2) 10px;resize:vertical;line-height:var(--lh-body)}
select.input{appearance:none;padding-right:28px;cursor:pointer}
.sel-wrap{position:relative;display:block}
.sel-wrap > .ic{position:absolute;right:8px;top:8px;pointer-events:none;color:var(--text-secondary)}
.field label{display:flex;gap:var(--sp-1);align-items:baseline}
.field label small{margin-left:2px}
.field .msg-row{display:flex;gap:var(--sp-3);align-items:baseline}
.field .msg-row .msg{flex:1}
.field .count{font:var(--fw-medium) var(--fs-caption)/var(--lh-caption) var(--font-figures);font-variant-numeric:tabular-nums;color:var(--text-secondary);white-space:nowrap}
.field.err .count{color:var(--warn-text)}
.field.err textarea.input,.field.err .select{border-color:var(--warn);box-shadow:inset 0 0 0 1px var(--warn)}
.field.dis label{color:var(--text-disabled)}
.field.fill{max-width:none}
.field + .field{margin-top:0}
.input-unit{display:flex}
.input-unit .input{border-top-right-radius:0;border-bottom-right-radius:0;font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-size:15px}
.input-unit .unit{display:flex;align-items:center;padding:0 10px;border:1px solid var(--border-control);border-left:0;border-radius:0 var(--radius-sm) var(--radius-sm) 0;background:var(--bg-sunken);color:var(--text-secondary);white-space:nowrap}
.search{position:relative;display:block}
.search > .ic{position:absolute;left:10px;top:8px;color:var(--text-secondary);pointer-events:none}
.search .input{padding-left:32px}
.search .kbd{position:absolute;right:8px;top:6px}
/* 表单布局 */
.form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--sp-5) var(--sp-6)}
.form .span2{grid-column:1 / -1}
.form .field{max-width:none}
.form-sec{margin:0;padding:0;border:0;min-width:0}
.form-sec + .form-sec{margin-top:var(--sp-8);padding-top:var(--sp-6);border-top:1px solid var(--border-subtle)}
.form-sec > legend{float:left;width:100%;padding:0;margin:0 0 var(--sp-4);font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans)}
.form-sec > legend small{font-weight:var(--fw-regular);font-size:var(--fs-caption);color:var(--text-secondary);margin-left:var(--sp-2)}
.form-sec > legend + *{clear:both}
.form-foot{display:flex;gap:var(--sp-3);align-items:center;margin-top:var(--sp-8);padding-top:var(--sp-5);border-top:1px solid var(--border-subtle)}
.form-foot .cost{margin-right:auto}
.err-sum{margin-bottom:var(--sp-5)}
.err-sum ul{margin:var(--sp-1) 0 0;padding-left:1.2em}
.err-sum a{color:var(--warn-text);text-underline-offset:3px}
/* ---- B5 下拉选择 / 下拉菜单(浮层,§5.1 只有浮层有阴影) ---- */
.pop{position:relative;display:block}
.pop.inline{display:inline-block}
.select{display:flex;align-items:center;gap:var(--sp-2);width:100%;height:var(--control-h);padding:0 8px 0 10px;border:1px solid var(--border-control);border-radius:var(--radius-sm);background:var(--bg-surface);font:var(--fs-body) var(--font-sans);color:var(--text-primary);text-align:left;cursor:pointer}
.select:hover{border-color:var(--c-ink)}
.select[aria-expanded="true"],.select.is-focus{outline:2px solid var(--focus-ring);outline-offset:2px;border-color:var(--c-ink)}
.select .val{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.select .ph{color:var(--text-secondary)}
.select > .ic{color:var(--text-secondary);transition:transform var(--dur-fast) var(--ease)}
.select[aria-expanded="true"] > .ic{transform:rotate(180deg)}
.select[disabled]{background:var(--bg-sunken);border-color:var(--bg-sunken);color:var(--text-disabled);cursor:not-allowed}
.select.sm{height:var(--control-h-sm)}
.menu{position:absolute;z-index:45;top:calc(100% + 4px);left:0;min-width:100%;max-height:280px;overflow:auto;background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-md);box-shadow:var(--shadow-overlay);padding:var(--sp-1) 0;margin:0;list-style:none}
.menu.right{left:auto;right:0}
.menu [role=option],.menu [role=menuitem]{display:flex;align-items:center;gap:var(--sp-2);min-height:32px;padding:5px var(--sp-3);cursor:pointer;white-space:nowrap}
.menu [role=option]:hover,.menu [role=menuitem]:hover,.menu .is-active{background:var(--bg-hover)}
.menu [aria-selected="true"]{font-weight:var(--fw-bold)}
.menu [aria-selected="true"]::after{content:"✓";margin-left:auto;padding-left:var(--sp-4)}
.menu [aria-disabled="true"]{color:var(--text-disabled);cursor:not-allowed;background:none}
.menu .grp{padding:var(--sp-2) var(--sp-3) 2px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.menu hr{border:0;border-top:1px solid var(--border-subtle);margin:var(--sp-1) 0}
.menu small{color:var(--text-secondary);font-size:var(--fs-caption);margin-left:auto;padding-left:var(--sp-4)}
.menu .ic{color:var(--text-secondary)}
/* ---- B6 日期选择(§9.3 日期格式) ---- */
.date-trigger{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-size:15px}
.date-trigger .ph{font-family:var(--font-sans);font-size:var(--fs-body)}
.cal{position:absolute;z-index:45;top:calc(100% + 4px);left:0;width:280px;background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-md);box-shadow:var(--shadow-overlay);padding:var(--sp-3)}
.cal-h{display:flex;align-items:center;gap:var(--sp-1);margin-bottom:var(--sp-2)}
.cal-h b{flex:1;text-align:center;font-weight:var(--fw-bold)}
.cal-h b .fig{font-size:15px}
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;text-align:center}
.cal-grid .wd{font-size:var(--fs-caption);line-height:28px;color:var(--text-secondary)}
.cal-grid button{position:relative;height:32px;border:1px solid transparent;border-radius:var(--radius-sm);background:none;font:var(--fw-medium) 15px/1 var(--font-figures);font-variant-numeric:tabular-nums;color:var(--text-primary);cursor:pointer}
.cal-grid button:hover{background:var(--bg-hover)}
.cal-grid button.out{color:var(--text-disabled)}
.cal-grid button.today{border-color:var(--border-control)}
.cal-grid button.mark::after{content:"";position:absolute;left:50%;bottom:3px;width:4px;height:4px;margin-left:-2px;border-radius:50%;background:var(--c-ink)}
.cal-grid button[aria-selected="true"]{background:var(--c-ink);color:var(--text-on-dark);border-color:var(--c-ink)}
.cal-grid button[aria-selected="true"].mark::after{background:var(--c-white)}
.cal-grid button[disabled]{color:var(--text-disabled);cursor:not-allowed;background:none;text-decoration:line-through}
.cal-f{display:flex;align-items:center;gap:var(--sp-2);margin-top:var(--sp-2);padding-top:var(--sp-2);border-top:1px solid var(--border-subtle);font-size:var(--fs-caption);color:var(--text-secondary)}
.cal-f .btn{margin-left:auto}
.cal-f .time{width:72px;height:var(--control-h-sm);font-family:var(--font-figures);font-size:15px;text-align:center}
.dt-pair{display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:var(--sp-2)}
.field.err .dt-pair > .input:not([aria-invalid]){border-color:var(--border-control);box-shadow:none}
/* ---- B7 复选框、单选、开关(§8.2 批量勾选) ---- */
.ck{appearance:none;flex:none;display:inline-grid;place-content:center;width:16px;height:16px;margin:0;border:1px solid var(--border-control);border-radius:var(--radius-sm);background:var(--bg-surface);cursor:pointer;vertical-align:-3px}
.ck:hover{border-color:var(--c-ink)}
.ck::after{content:"";display:none}
.ck:checked,.ck[type=checkbox]:indeterminate{background:var(--c-ink);border-color:var(--c-ink)}
.ck:checked::after{display:block;width:8px;height:4px;border:solid var(--c-white);border-width:0 0 1.75px 1.75px;transform:translateY(-1px) rotate(-45deg)}
.ck[type=checkbox]:indeterminate::after{display:block;width:8px;height:0;border-top:1.75px solid var(--c-white);transform:none}
.ck[type=radio]{border-radius:50%}
.ck[type=radio]:checked{background:var(--bg-surface);border-color:var(--c-ink)}
.ck[type=radio]:checked::after{width:8px;height:8px;border:0;border-radius:50%;background:var(--c-ink);transform:none}
.ck:disabled{background:var(--bg-sunken);border-color:var(--c-rule);cursor:not-allowed}
.ck:disabled:checked{background:var(--c-muted);border-color:var(--c-muted)}
.ck[type=radio]:disabled:checked{background:var(--bg-sunken)}
.ck[type=radio]:disabled:checked::after{background:var(--c-muted)}
.ck.is-focus{outline:2px solid var(--focus-ring);outline-offset:2px}
.choice{display:inline-flex;gap:var(--sp-2);align-items:flex-start;cursor:pointer;line-height:var(--lh-body)}
.choice .ck{margin-top:3px}
.choice small{display:block;color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.choice.dis{color:var(--text-disabled);cursor:not-allowed}
.choice.dis small{color:var(--text-disabled)}
.choices{display:flex;flex-direction:column;gap:var(--sp-2)}
.choices.h{flex-direction:row;flex-wrap:wrap;gap:var(--sp-2) var(--sp-6)}
.choice-card{display:flex;gap:var(--sp-3);align-items:flex-start;padding:var(--sp-3) var(--sp-4);border:1px solid var(--border-control);border-radius:var(--radius-sm);background:var(--bg-surface);cursor:pointer}
.choice-card:has(:checked){border-color:var(--c-ink);box-shadow:inset 0 0 0 1px var(--c-ink)}
.choice-card .ck{margin-top:3px}
.choice-card b{display:block}
.choice-card small{display:block;color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption)}
/* 开关:沿用 2px 圆角,不做胶囊形(§5.1) */
.switch{display:inline-flex;align-items:center;gap:var(--sp-2);padding:0;border:0;background:none;font:var(--fs-body) var(--font-sans);color:var(--text-primary);cursor:pointer}
.switch .track{position:relative;width:32px;height:18px;border:1px solid var(--border-control);border-radius:var(--radius-sm);background:var(--bg-surface);transition:background var(--dur-fast) var(--ease)}
.switch .track::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:1px;background:var(--c-muted);transition:transform var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}
.switch[aria-checked="true"] .track{background:var(--c-ink);border-color:var(--c-ink)}
.switch[aria-checked="true"] .track::after{transform:translateX(14px);background:var(--c-white)}
.switch .st{font-size:var(--fs-caption);color:var(--text-secondary);min-width:2em}
.switch[disabled]{color:var(--text-disabled);cursor:not-allowed}
.switch[disabled] .track{background:var(--bg-sunken);border-color:var(--c-rule)}
.switch[disabled] .track::after{background:var(--c-rule)}
.switch[disabled][aria-checked="true"] .track{background:var(--c-muted);border-color:var(--c-muted)}
/* ---- B8 分段控件、标签页(§5.3 页签、§7.5 分类) ---- */
.seg{display:inline-flex;border:1px solid var(--border-control);border-radius:var(--radius-sm);overflow:hidden;background:var(--bg-surface)}
.seg button{height:28px;padding:0 10px;border:0;background:none;font:var(--fs-body) var(--font-sans);color:var(--text-primary);cursor:pointer;display:flex;gap:6px;align-items:center;white-space:nowrap}
.seg button:hover{background:var(--bg-hover)}
.seg button + button{border-left:1px solid var(--border-subtle)}
.seg button[aria-pressed="true"]{background:var(--c-ink);color:var(--text-on-dark)}
.seg button[aria-pressed="true"] .fig{color:var(--text-on-dark)}
.seg button[aria-pressed="true"] .sev.hint,.seg button[aria-pressed="true"] .sev.major{background:var(--c-white)}
.seg .fig{font-weight:var(--fw-medium);color:var(--text-secondary)}
.seg .sev .fig{color:inherit}
.tabs{display:flex;gap:var(--sp-6);border-bottom:1px solid var(--border-subtle)}
.tabs.flush{border-bottom:0}
.tabs [role=tab]{display:flex;gap:6px;align-items:center;margin-bottom:-1px;padding:var(--sp-2) 0 10px;border:0;border-bottom:2px solid transparent;background:none;font:var(--fs-body) var(--font-sans);color:var(--text-secondary);cursor:pointer;text-decoration:none;white-space:nowrap}
.tabs [role=tab]:hover{color:var(--text-primary)}
.tabs [role=tab][aria-selected="true"]{color:var(--text-primary);font-weight:var(--fw-bold);border-bottom-color:var(--c-ink)}
.tabs [role=tab][aria-disabled="true"]{color:var(--text-disabled);cursor:not-allowed}
.tabs [role=tab] > .fig{font-weight:var(--fw-medium)}
.tabpanel{padding-top:var(--sp-4)}
/* ---- B9 分页 ---- */
.pagination{display:flex;align-items:center;gap:var(--sp-1);margin-top:var(--sp-3);font-size:var(--fs-caption);color:var(--text-secondary)}
.pagination .total{margin-right:auto}
.pagination .fig{font-weight:var(--fw-medium)}
.pagination button.pg{min-width:28px;height:28px;padding:0 6px;border:1px solid transparent;border-radius:var(--radius-sm);background:none;font:var(--fw-medium) 15px/1 var(--font-figures);font-variant-numeric:tabular-nums;color:var(--text-primary);cursor:pointer}
.pagination button.pg:hover{background:var(--bg-hover)}
.pagination button.pg[aria-current="page"]{border-color:var(--c-ink);font-weight:var(--fw-bold);background:var(--bg-surface)}
.pagination button.pg[disabled]{color:var(--text-disabled);cursor:not-allowed;background:none}
.pagination .gap{width:20px;text-align:center}
.pagination .jump{display:flex;align-items:center;gap:6px;margin-left:var(--sp-4)}
.pagination .jump .input{width:52px;height:28px;text-align:center;font-family:var(--font-figures);font-size:15px}
/* ---- B10 轻提示 Toast(§7.12:只用于操作结果,4 秒消失,悬停暂停) ---- */
.toasts{position:fixed;left:50%;bottom:var(--sp-8);transform:translateX(-50%);z-index:60;display:flex;flex-direction:column;gap:var(--sp-2);align-items:center;pointer-events:none}
.toast{display:flex;gap:var(--sp-3);align-items:center;min-height:40px;padding:var(--sp-2) var(--sp-4);background:var(--c-ink);color:var(--text-on-dark);border-radius:var(--radius-md);box-shadow:var(--shadow-overlay);pointer-events:auto;white-space:nowrap}
.toast .ok{display:inline-grid;place-items:center;width:16px;height:16px;border-radius:50%;background:var(--c-green);color:var(--c-white);font-size:11px;line-height:1}
.toast a,.toast button.lnk{color:var(--text-on-dark);text-decoration:underline;text-underline-offset:3px;background:none;border:0;font:inherit;cursor:pointer;padding:0}
.toast .sep{width:1px;align-self:stretch;margin:2px 0;background:color-mix(in srgb,var(--c-white) 24%,transparent)}
.toast.static{position:static}
/* ---- B11 对话框(§7.14) ---- */
.scrim{position:fixed;inset:0;z-index:50;display:grid;place-items:center;background:color-mix(in srgb,var(--c-ink) 32%,transparent)}
.dialog{position:relative;width:480px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px);overflow:auto;background:var(--bg-surface);border-radius:var(--radius-md);box-shadow:var(--shadow-overlay);padding:var(--sp-6)}
.dialog.wide{width:640px}
.dialog.static{position:static;max-width:none;box-shadow:var(--shadow-overlay)}
.dialog h2{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0 var(--sp-8) var(--sp-3) 0}
.dialog .x{position:absolute;top:var(--sp-4);right:var(--sp-4)}
.dialog .body > p{margin:0 0 var(--sp-3)}
.dialog .body > .field{margin-top:var(--sp-4)}
.dialog .foot{display:flex;justify-content:flex-end;align-items:center;gap:var(--sp-2);margin-top:var(--sp-6)}
.dialog .foot .left{margin-right:auto}
/* ---- B12 提醒补充(§7.12、§7.15) ---- */
.alert{align-items:flex-start}
.alert > div{flex:1;min-width:0}
.alert .act{margin-left:auto;flex:none;display:flex;gap:var(--sp-2);align-items:center}
.alert .sub{display:block;margin-top:2px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.alert .fig{font-weight:var(--fw-medium)}
.errno{font:var(--fw-medium) var(--fs-caption)/1 var(--font-figures);color:var(--text-secondary);letter-spacing:.02em}
/* ---- B13 提示气泡(悬停 / 聚焦显示,§6 只有图标的按钮要有悬停提示) ---- */
[data-tip]{position:relative}
[data-tip]:hover::after,[data-tip]:focus-visible::after,[data-tip].tip-on::after{content:attr(data-tip);position:absolute;z-index:55;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);max-width:280px;width:max-content;padding:5px var(--sp-2);border-radius:var(--radius-md);background:var(--c-ink);color:var(--text-on-dark);font:var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-sans);white-space:normal;text-align:left;box-shadow:var(--shadow-overlay);pointer-events:none}
[data-tip].tip-below:hover::after,[data-tip].tip-below.tip-on::after{bottom:auto;top:calc(100% + 6px)}
[data-tip].tip-start:hover::after,[data-tip].tip-start.tip-on::after{left:0;transform:none}
[data-tip].tip-left:hover::after,[data-tip].tip-left.tip-on::after{left:auto;right:0;transform:none}
/* ---- B14 空状态、骨架屏、进度(§7.11、§7.15) ---- */
.empty{display:flex;flex-direction:column;align-items:center;text-align:center;padding:var(--sp-12) var(--sp-6)}
.empty > .ic{color:var(--text-secondary);margin-bottom:var(--sp-3)}
.empty b{font-size:var(--fs-title);line-height:var(--lh-title)}
.empty p{margin:var(--sp-1) 0 var(--sp-5);color:var(--text-secondary);max-width:30em}
.empty.sm{padding:var(--sp-8) var(--sp-4)}
.empty.sm p{margin-bottom:var(--sp-3)}
.skel{display:block;height:12px;border-radius:var(--radius-sm);background:var(--bg-sunken);animation:skel 1.6s var(--ease) infinite alternate}
.skel.t{height:16px;width:40%}
.skel.w60{width:60%} .skel.w80{width:80%} .skel.w30{width:30%} .skel.w20{width:20%}
.skel + .skel{margin-top:var(--sp-2)}
.skel-row{display:grid;grid-template-columns:24px minmax(0,1fr) 120px;gap:var(--sp-3);padding:var(--sp-3) 0;border-top:1px solid var(--border-subtle)}
.skel-row:first-child{border-top:0}
@keyframes skel{from{opacity:1}to{opacity:.45}}
@media (prefers-reduced-motion:reduce){.skel{animation:none}}
.progress{display:flex;align-items:center;gap:var(--sp-3)}
.progress .bar{flex:1;margin:0}
.progress .fig{font-weight:var(--fw-medium);min-width:3em;text-align:right}
.steps .fig{font-weight:var(--fw-medium);color:var(--text-secondary);margin-left:auto}
.steps li > span:nth-child(2){flex:1}
.steps .fail .dot{border-color:var(--warn);color:var(--warn-text);font-weight:var(--fw-bold)}
.steps .fail{color:var(--warn-text)}
/* ---- B15 杂项小件 ---- */
.kbd{display:inline-block;min-width:20px;padding:0 5px;border:1px solid var(--border-control);border-bottom-width:2px;border-radius:3px;font:var(--fw-medium) var(--fs-caption)/18px var(--font-figures);text-align:center;background:var(--bg-surface);color:var(--text-primary)}
.muted{color:var(--text-secondary)}
.nw{white-space:nowrap}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.fig.m{font-weight:var(--fw-medium)}
.kv{display:grid;grid-template-columns:96px minmax(0,1fr);gap:var(--sp-2) var(--sp-4);margin:0}
.kv dt{color:var(--text-secondary)}
.kv dd{margin:0}
.chip{display:inline-flex;align-items:center;gap:var(--sp-1);height:24px;padding:0 4px 0 8px;border:1px solid var(--border-control);border-radius:var(--radius-sm);background:var(--bg-surface);font-size:var(--fs-caption);white-space:nowrap}
.chip button{display:inline-grid;place-items:center;width:18px;height:18px;padding:0;border:0;border-radius:var(--radius-sm);background:none;color:var(--text-secondary);cursor:pointer}
.chip button:hover{background:var(--bg-hover);color:var(--text-primary)}
.drop{display:flex;flex-direction:column;align-items:center;gap:var(--sp-1);border:1.5px dashed var(--border-control);border-radius:var(--radius-sm);background:var(--bg-surface);padding:var(--sp-8) var(--sp-6);text-align:center;cursor:pointer}
.drop:hover,.drop.is-over{border-color:var(--c-ink);background:var(--bg-hover)}
.drop > .ic{color:var(--text-secondary)}
.drop b{font-size:var(--fs-title);line-height:var(--lh-title)}
.drop span{color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.file-row{display:grid;grid-template-columns:40px minmax(0,1fr) auto;gap:var(--sp-3);align-items:center;padding:var(--sp-3) var(--sp-4);background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm)}
.file-row + .file-row{margin-top:var(--sp-2)}
.file-row .ext{font:var(--fw-bold) 11px/1 var(--font-figures);border:1px solid var(--border-control);border-radius:var(--radius-sm);padding:4px 0;text-align:center}
.file-row b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.file-row .acts{display:flex;gap:var(--sp-1);align-items:center}
.flowline{display:flex;align-items:center}
.flowline .st{display:flex;align-items:center;gap:var(--sp-2);color:var(--text-secondary);white-space:nowrap}
.flowline .st i{width:10px;height:10px;border-radius:50%;border:1.5px solid var(--border-control);flex:none}
.flowline .st.done{color:var(--text-primary)}
.flowline .st.done i{background:var(--c-ink);border-color:var(--c-ink)}
.flowline .st.now{color:var(--text-primary);font-weight:var(--fw-bold)}
.flowline .st.now i{border:3px solid var(--c-ink)}
.flowline .ln{flex:1;min-width:16px;height:1px;background:var(--border-control);margin:0 var(--sp-2)}
.flowline.sm{font-size:var(--fs-caption)}
.flowline.sm .st i{width:8px;height:8px}
.flowline.sm .st.now i{border-width:2.5px}
/* ---- B16 AI 产出块(§3.3 左侧虚线竖线、§7.16) ---- */
.ai-block{border-left:1.5px dashed var(--ai);padding:var(--sp-1) 0 var(--sp-1) var(--sp-3)}
.ai-block.done{border-left:2px solid var(--c-ink)}
.ai-label{display:flex;gap:var(--sp-2);align-items:center;margin-bottom:var(--sp-2);font-size:var(--fs-caption);color:var(--text-secondary)}
.flag.box{border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:0 5px;line-height:18px}
.cite-src.inline{display:inline;margin-left:6px;white-space:nowrap}
.sign.ign{color:var(--text-secondary)}
.sign.sm{font-size:var(--fs-caption);margin-top:var(--sp-2)}
.more-cites{display:inline-block;margin-top:var(--sp-2);font-size:var(--fs-caption);color:var(--ai);background:none;border:0;padding:0;cursor:pointer;font-family:var(--font-sans)}
.more-cites:hover{text-decoration:underline;text-underline-offset:3px}
.cite q.clamp{-webkit-line-clamp:3}
.cite .expand{font-size:var(--fs-caption);color:var(--ai);background:none;border:0;padding:0;cursor:pointer;font-family:var(--font-sans)}
/* ---- B17 卡片 ---- */
.card-h{display:flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-3)}
.card-h h3{margin:0;font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans)}
.card-h .more{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
.card.tight{padding:var(--sp-4) var(--sp-5)}
.card-foot{display:flex;gap:var(--sp-2);align-items:center;margin:var(--sp-4) calc(-1 * var(--sp-6)) calc(-1 * var(--sp-5));padding:var(--sp-3) var(--sp-6);border-top:1px solid var(--border-subtle)}
.card.tight .card-foot{margin:var(--sp-4) calc(-1 * var(--sp-5)) calc(-1 * var(--sp-4));padding:var(--sp-3) var(--sp-5)}
.card-foot .r{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
/* 项目卡 */
.pcard{display:flex;flex-direction:column;gap:var(--sp-3);position:relative}
.pcard.is-fatal::before{content:"";position:absolute;left:-1px;top:var(--sp-5);bottom:var(--sp-5);width:3px;background:var(--sev-fatal)}
.pcard .pc-top{display:flex;gap:var(--sp-3);align-items:flex-start}
.pcard .pc-top > div{flex:1;min-width:0}
.pcard .card-foot{margin-top:auto}
.pcard .facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3);padding:var(--sp-3) 0;border-top:1px solid var(--border-subtle);border-bottom:1px solid var(--border-subtle)}
.pcard .facts small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.pcard .facts > div{min-width:0}
/* 统计格 */
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--sp-4)}
.stat{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:var(--sp-4) var(--sp-5)}
.stat small{display:block;color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.stat .big{font:var(--fw-bold) var(--fs-figure)/var(--lh-figure) var(--font-figures);font-variant-numeric:tabular-nums}
.stat .unit{color:var(--text-secondary);margin-left:4px}
.stat p{margin:var(--sp-1) 0 0;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.stat p.due-soon,.stat p.due-today{color:var(--warn-text)}
/* 额度卡 */
.quota small{display:block;color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.quota .q2{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-5)}
.quota p{margin:0;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.quota p.warn{color:var(--warn-text)}
/* 关键日期 */
.dates{list-style:none;margin:0;padding:0}
.dates li{display:grid;grid-template-columns:88px minmax(0,1fr) auto;gap:var(--sp-3);align-items:baseline;padding:var(--sp-2) 0;border-top:1px solid var(--border-subtle)}
.dates li:first-child{border-top:0}
.dates li > .fig{font-weight:var(--fw-medium);font-size:15px}
.dates li.past{color:var(--text-secondary)}
.dates li.key > span:nth-child(2) > b{font-weight:var(--fw-bold)}
.dates .left{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.dates .left.due-soon,.dates .left.due-today{color:var(--warn-text)}
.dates .cite-src{white-space:nowrap}
/* ---- B18 表格补充:排序、勾选、选中行、批量栏、空结果(§7.7、§8.2) ---- */
.toolbar{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-3);flex-wrap:wrap}
.toolbar .right{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
.toolbar .search{width:260px}
.toolbar .pop .select{width:auto;min-width:132px}
.filters-on{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;margin:calc(-1 * var(--sp-1)) 0 var(--sp-3);font-size:var(--fs-caption);color:var(--text-secondary)}
table.data th.sortable{padding:0}
.th-sort{display:flex;align-items:center;gap:4px;width:100%;padding:var(--sp-2) var(--sp-3);border:0;background:none;font:inherit;color:inherit;cursor:pointer;text-align:left;white-space:nowrap}
.th-sort:hover{color:var(--text-primary)}
.th-sort .ic{width:14px;height:14px;color:var(--text-disabled)}
th[aria-sort] .th-sort{color:var(--text-primary)}
th[aria-sort] .th-sort .ic{color:var(--text-primary)}
table.data th.r .th-sort{justify-content:flex-end}
table.data .ckc{width:40px;padding-right:0}
table.data.hover tbody tr{cursor:pointer}
table.data.hover tbody tr:hover td{background:var(--bg-hover)}
table.data tbody tr.sel td{background:var(--bg-sunken)}
table.data tbody tr.sel td:first-child{box-shadow:inset 2px 0 0 var(--c-ink)}
table.data td.empty-cell{height:auto;padding:0}
table.data td .cap{color:var(--text-secondary)}
table.data .acts{white-space:nowrap;text-align:right}
table.data td.mid{vertical-align:middle}
table.data tr.is-fatal td:first-child{box-shadow:inset 3px 0 0 var(--sev-fatal)}
.batchbar{position:sticky;top:calc(52px + var(--sp-2));z-index:10;display:flex;align-items:center;gap:var(--sp-3);height:44px;padding:0 var(--sp-4);margin-bottom:var(--sp-2);background:var(--bg-surface);border:1px solid var(--c-ink);border-radius:var(--radius-sm)}
.batchbar b .fig{font-weight:var(--fw-bold)}
.batchbar .r{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
.list-box{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm)}
.list-box > .lb-h{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-1) var(--sp-4);border-bottom:1px solid var(--border-subtle);background:var(--bg-sunken);font-size:var(--fs-caption);color:var(--text-secondary)}
.list-box > .lb-h .r{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
.list-box .item{padding:var(--sp-3) var(--sp-4);border-top:1px solid var(--border-subtle)}
.list-box .item:first-of-type{border-top:0}
.list-box .item.has-ck{grid-template-columns:20px 24px minmax(0,1fr) auto}
.list-box .item .ck{margin-top:3px}
.item.sel{background:var(--ai-soft);box-shadow:inset 3px 0 0 var(--ai)}
.item.focus{box-shadow:inset 0 0 0 2px var(--c-ink)}
.item[tabindex]{cursor:pointer;outline:none}
.item[tabindex]:hover{background:var(--bg-hover)}
.item.sel:hover{background:var(--ai-soft)}
.item .upd{margin-top:6px;display:flex;gap:var(--sp-2);align-items:center;font-size:var(--fs-caption)}
.item .upd .btn{height:24px;font-size:var(--fs-caption)}
/* 问题行的紧凑版本(列表页) */
.finding .ckw{position:absolute;left:var(--sp-2);top:calc(var(--sp-5) + 2px)}
.finding.has-ck{padding-left:var(--sp-8)}
.finding.focus{box-shadow:inset 0 0 0 2px var(--c-ink)}
.u4{margin-top:var(--sp-3);padding:var(--sp-3) var(--sp-4);background:var(--bg-sunken);border-radius:var(--radius-sm)}
.u4 .field{max-width:none}
.u4 .row{margin-top:var(--sp-2)}
.group-h{display:flex;align-items:center;gap:var(--sp-2);width:100%;padding:var(--sp-3) var(--sp-6);border:0;border-top:1px solid var(--border-subtle);background:var(--bg-sunken);color:var(--text-secondary);font:var(--fs-body) var(--font-sans);cursor:pointer;text-align:left}
.group-h:hover{color:var(--text-primary)}
.verdict.hint{color:var(--sev-hint)}
/* ---- B19 对照布局:分类导航 + 列表 + 原文查看器(§5.3、§7.8) ---- */
.split{display:grid;grid-template-columns:200px minmax(0,1fr) minmax(420px,45%);flex:1;min-height:0}
.cats{border-right:1px solid var(--border-subtle);padding:var(--sp-4) var(--sp-3);overflow:auto}
.cats button{display:flex;width:100%;align-items:center;justify-content:space-between;height:34px;padding:0 var(--sp-3);border:0;background:none;border-radius:var(--radius-sm);font:var(--fs-body) var(--font-sans);color:var(--text-primary);cursor:pointer;text-align:left}
.cats button:hover{background:var(--bg-hover)}
.cats button[aria-current="true"]{background:var(--bg-surface);box-shadow:inset 2px 0 0 var(--c-ink);font-weight:var(--fw-bold)}
.cats button .fig{font-weight:var(--fw-medium);color:var(--text-secondary)}
.cats button.ai,.cats button.ai .fig{color:var(--ai)}
.cats hr{border:0;border-top:1px solid var(--border-subtle);margin:var(--sp-3) 0}
.cats .hint{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);padding:0 var(--sp-3);margin:0}
.viewer{min-height:0}
.viewer-bar{flex-wrap:wrap;min-height:44px}
.viewer-bar .doc-name{display:flex;gap:var(--sp-2);align-items:baseline;min-width:0}
.viewer-bar .doc-file{color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
.viewer-body{flex:1;overflow:auto;padding:var(--sp-4) 0}
.viewer .sheet{min-height:560px;margin:0 auto;width:calc(100% - 48px);max-width:640px;position:relative;background:var(--c-white);color:var(--text-primary)}
.sheet .pno{position:absolute;bottom:14px;left:0;right:0;text-align:center;font:var(--fs-caption) var(--font-figures);color:var(--text-secondary)}
.sheet .h{font-weight:var(--fw-bold);margin:4px 0 10px}
.sheet .faint{color:var(--text-secondary)}
.sheet .stub{height:9px;background:var(--bg-sunken);margin:12px 0;border-radius:1px}
.sheet .stub.s{width:60%}
.viewer-note{font-size:var(--fs-caption);color:var(--text-secondary);padding:var(--sp-2) var(--sp-6) 0;margin:0;text-align:center}

275
docs/design/ui-demo/demo.js Normal file
View File

@@ -0,0 +1,275 @@
/* 标博士 · UI demo 公共脚本:图标、页面框架、轻提示、对话框、下拉、日期、开关、标签页、排序。
* 只演示交互,不连后端。正式实现里这些交互件由 Reka UI 打底(AR10)。 */
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '<rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/>',
folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
help: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8M16 13H8M16 17H8"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
"chev-down": '<path d="m6 9 6 6 6-6"/>',
"chev-left": '<path d="m15 18-6-6 6-6"/>',
"chev-right": '<path d="m9 18 6-6-6-6"/>',
sort: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
"arrow-up": '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>',
"arrow-down": '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>',
calendar: '<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',
check: '<path d="M20 6 9 17l-5-5"/>',
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
inbox: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
trash: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
pencil: '<path d="M21.17 6.81a1 1 0 0 0-3.98-3.98L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5z"/>',
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
keyboard: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/>',
refresh: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
send: '<path d="M14.54 21.69a.5.5 0 0 0 .94-.03l6.5-19a.5.5 0 0 0-.64-.64l-19 6.5a.5.5 0 0 0-.03.94l7.93 3.18a2 2 0 0 1 1.11 1.11z"/><path d="m21.85 2.15-10.94 10.94"/>',
list: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/>',
paperclip: '<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/>',
book: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
archive: '<rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>',
layers: '<path d="m12.83 2.18 8.58 3.91a1 1 0 0 1 0 1.83l-8.58 3.9a2 2 0 0 1-1.66 0L2.6 7.92a1 1 0 0 1 0-1.83l8.58-3.9a2 2 0 0 1 1.66 0Z"/><path d="m22 12-9.17 4.17a2 2 0 0 1-1.66 0L2 12"/><path d="m22 17-9.17 4.17a2 2 0 0 1-1.66 0L2 17"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
function hydrate(root = document) {
$$("i[data-ic]", root).forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
}
/* ---------- demo 顶部导航(控件页) ---------- */
const PAGES = [
["index.html", "目录"], ["controls.html", "① 控件"], ["forms.html", "② 表单"], ["cards.html", "③ 卡片"],
["lists.html", "④ 列表与表格"], ["page-workbench.html", "⑤ 工作台"], ["page-reading.html", "⑥ 读标"]
];
function demoTop(cur) {
const h = $("[data-demo-top]");
if (!h) return;
h.className = "d-top";
h.innerHTML = `<a class="brand" href="index.html"><img src="../logo/svg/logo-horizontal.svg" alt="标博士"></a><span class="tag">UI demo</span>
<nav class="d-nav" aria-label="demo 页面">${PAGES.map(([u, t]) => `<a href="${u}" class="${u === cur ? "on" : ""}">${t}</a>`).join("")}</nav>`;
}
/* ---------- 产品页面框架(组合页) ---------- */
function shell(opt) {
const nav = [["page-workbench.html", "grid", "工作台", "workbench"], ["#", "cert", "资质库", "quals"], ["#", "gauge", "额度", "quota"]]
.map(([h, i, t, k]) => `<a href="${h}" class="${opt.nav === k ? "on" : ""}" ${opt.nav === k ? 'aria-current="page"' : ""}>${icon(i, "lg")}${t}</a>`).join("");
const crumbs = (opt.crumbs || []).map((c, i, a) => i < a.length - 1 ? `<a href="${c[1]}">${c[0]}</a><span>/</span>` : `<b>${c[0]}</b>`).join("");
document.body.classList.add("app");
document.body.insertAdjacentHTML("afterbegin", `<aside class="sidebar">
<a class="brand" href="page-workbench.html"><img src="../logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">${nav}<div class="sub">UI demo</div><a href="index.html">${icon("book", "lg")}demo 目录</a></nav>
<div class="sidebar-foot"><b>王敏</b>南京某信息技术有限公司<br>专业版 · 至 2027-10-31</div></aside>`);
const main = $("main");
main.classList.add("main");
main.insertAdjacentHTML("afterbegin", `<header class="top"><div class="crumbs">${crumbs}</div>
<div class="top-right">
<a class="quota-chip tip-below" href="#" data-tip="本期剩余额度">解析<span class="fig">37</span>检查<span class="fig">12</span></a>
<button class="icon-btn tip-below" aria-label="通知" data-tip="通知" data-notice>${icon("bell", "lg")}<span class="dot"></span></button>
<button class="icon-btn tip-below tip-left" aria-label="快捷键说明" data-tip="快捷键说明(?)" data-keys>${icon("keyboard", "lg")}</button>
</div></header>`);
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
$("[data-keys]").onclick = keysDialog;
}
function keysDialog() {
dialog({ title: "快捷键", cancel: null, ok: "知道了",
body: `<table class="data"><tbody>
<tr><td><span class="kbd">J</span> / <span class="kbd">K</span></td><td>下一条 / 上一条</td></tr>
<tr><td><span class="kbd">O</span> 或 <span class="kbd">Enter</span></td><td>在原文查看器里打开当前出处</td></tr>
<tr><td><span class="kbd">1</span> <span class="kbd">2</span> <span class="kbd">3</span></td><td>确认无误 / 已修改 / 忽略(3 会打开原因输入框)</td></tr>
<tr><td><span class="kbd">U</span></td><td>撤销当前条的处理</td></tr>
<tr><td><span class="kbd">?</span></td><td>显示本说明</td></tr></tbody></table>` });
}
/* ---------- 轻提示:4 秒消失,悬停暂停 ---------- */
function toast(msg, opt = {}) {
let box = $(".toasts");
if (!box) { box = document.createElement("div"); box.className = "toasts"; box.setAttribute("role", "status"); box.setAttribute("aria-live", "polite"); document.body.appendChild(box); }
const t = document.createElement("div");
t.className = "toast";
t.innerHTML = `${opt.ok === false ? "" : '<span class="ok" aria-hidden="true">✓</span>'}<span>${msg}</span>${opt.undo ? '<span class="sep"></span><button class="lnk" data-undo>撤销</button>' : ""}`;
box.appendChild(t);
let left = 4000, start = Date.now(), timer = setTimeout(() => t.remove(), left);
t.addEventListener("mouseenter", () => { clearTimeout(timer); left -= Date.now() - start; });
t.addEventListener("mouseleave", () => { start = Date.now(); timer = setTimeout(() => t.remove(), Math.max(left, 1200)); });
if (opt.undo) $("[data-undo]", t).onclick = () => { t.remove(); opt.undo(); };
}
/* ---------- 对话框:Esc 关闭、焦点回到触发处 ---------- */
function dialog({ title, body, ok = "确定", cancel = "取消", wide = false, onOk, onOpen, footLeft = "" }) {
const back = document.activeElement;
const s = document.createElement("div");
s.className = "scrim";
s.innerHTML = `<div class="dialog ${wide ? "wide" : ""}" role="dialog" aria-modal="true" aria-labelledby="dlg-t">
<h2 id="dlg-t">${title}</h2><button class="icon-btn sm x" aria-label="关闭" data-x>${icon("x")}</button>
<div class="body">${body}</div>
<div class="foot">${footLeft ? `<span class="left">${footLeft}</span>` : ""}${cancel ? `<button class="btn" data-cancel>${cancel}</button>` : ""}<button class="btn primary" data-ok>${ok}</button></div></div>`;
document.body.appendChild(s);
document.body.style.overflow = "hidden";
const close = () => { s.remove(); document.body.style.overflow = ""; document.removeEventListener("keydown", onKey, true); if (back && back.focus) back.focus(); };
const onKey = (e) => {
if (e.key === "Escape") { e.stopPropagation(); close(); }
if (e.key === "Tab") { // 焦点锁定在对话框内
const f = $$("button:not([disabled]), input:not([disabled]), textarea, select, a[href]", s);
if (!f.length) return;
if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
}
};
document.addEventListener("keydown", onKey, true);
s.addEventListener("mousedown", (e) => { if (e.target === s) close(); });
$("[data-x]", s).onclick = close;
if (cancel) $("[data-cancel]", s).onclick = close;
$("[data-ok]", s).onclick = () => { if (!onOk || onOk(s) !== false) close(); };
hydrate(s); init(s);
if (onOpen) onOpen(s);
const f = $("textarea, input:not([type=checkbox]):not([type=radio]), [data-ok]", s);
if (f) f.focus();
return s;
}
/* ---------- 下拉选择(listbox)与下拉菜单 ---------- */
function closePops(except) {
$$(".pop.open").forEach((p) => { if (p !== except) { p.classList.remove("open"); const t = $("[aria-expanded]", p); if (t) t.setAttribute("aria-expanded", "false"); const m = $(".menu,.cal", p); if (m) m.hidden = true; } });
}
function initSelect(p) {
const trg = $(".select, [data-menu-trigger]", p), menu = $(".menu", p);
if (!trg || !menu || p.dataset.ready) return;
p.dataset.ready = 1;
menu.hidden = true;
const opts = () => $$('[role=option]:not([aria-disabled="true"]), [role=menuitem]:not([aria-disabled="true"])', menu);
let ai = -1;
const mark = (i) => { opts().forEach((o, k) => o.classList.toggle("is-active", k === i)); ai = i; const o = opts()[i]; if (o) o.scrollIntoView({ block: "nearest" }); };
const open = () => { closePops(p); p.classList.add("open"); menu.hidden = false; trg.setAttribute("aria-expanded", "true"); const sel = opts().findIndex((o) => o.getAttribute("aria-selected") === "true"); mark(sel < 0 ? 0 : sel); };
const close = () => { p.classList.remove("open"); menu.hidden = true; trg.setAttribute("aria-expanded", "false"); };
const pick = (o) => {
if (o.getAttribute("role") === "option") {
$$("[role=option]", menu).forEach((x) => x.setAttribute("aria-selected", x === o ? "true" : "false"));
const v = $(".val", trg); if (v) v.innerHTML = o.dataset.label || o.firstChild.textContent;
p.closest(".field")?.classList.remove("err");
p.dispatchEvent(new CustomEvent("change", { detail: o.dataset.value || o.textContent }));
} else if (o.dataset.toast) toast(o.dataset.toast);
close(); trg.focus();
};
trg.addEventListener("click", (e) => { e.stopPropagation(); p.classList.contains("open") ? close() : open(); });
trg.addEventListener("keydown", (e) => {
const n = opts().length;
if (["ArrowDown", "ArrowUp"].includes(e.key)) { e.preventDefault(); if (!p.classList.contains("open")) return open(); mark((ai + (e.key === "ArrowDown" ? 1 : n - 1)) % n); }
else if (e.key === "Enter" || e.key === " ") { if (p.classList.contains("open") && ai >= 0) { e.preventDefault(); pick(opts()[ai]); } }
else if (e.key === "Escape" && p.classList.contains("open")) { e.stopPropagation(); close(); }
});
menu.addEventListener("click", (e) => { const o = e.target.closest('[role=option], [role=menuitem]'); if (o && o.getAttribute("aria-disabled") !== "true") { e.stopPropagation(); pick(o); } });
}
/* ---------- 日期选择 ---------- */
const WD = ["一", "二", "三", "四", "五", "六", "日"];
function calHTML(y, m, sel, opt = {}) {
const first = new Date(y, m, 1), start = (first.getDay() + 6) % 7, days = new Date(y, m + 1, 0).getDate();
const prevDays = new Date(y, m, 0).getDate();
let cells = "";
for (let i = 0; i < 42; i++) {
const d = i - start + 1;
let dd, out = false, mm = m;
if (d < 1) { dd = prevDays + d; out = true; mm = m - 1; } else if (d > days) { dd = d - days; out = true; mm = m + 1; } else dd = d;
const date = new Date(y, mm, dd), key = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
const cls = [out ? "out" : "", key === opt.today ? "today" : "", (opt.marks || []).includes(key) ? "mark" : ""].join(" ");
cells += `<button type="button" class="${cls}" data-d="${key}" aria-selected="${key === sel}" ${opt.min && date < opt.min ? "disabled" : ""} tabindex="-1">${dd}</button>`;
if (i === 34 && d >= days) break;
}
return `<div class="cal-h"><button type="button" class="icon-btn sm" data-cm="-1" aria-label="上个月">${icon("chev-left")}</button><b><span class="fig">${y}</span> 年 <span class="fig">${m + 1}</span> 月</b><button type="button" class="icon-btn sm" data-cm="1" aria-label="下个月">${icon("chev-right")}</button></div>
<div class="cal-grid" role="grid">${WD.map((w) => `<span class="wd">${w}</span>`).join("")}${cells}</div>
<div class="cal-f">${opt.time ? `时间 <input class="input time" value="${opt.time}" aria-label="时间">` : '<span>点圆点的日期是本项目的关键日期</span>'}<button type="button" class="btn sm text" data-today>今天</button></div>`;
}
function initDate(p) {
if (p.dataset.ready) return;
p.dataset.ready = 1;
const trg = $(".select", p), cal = $(".cal", p);
let [y, m] = (p.dataset.month || "2026-11").split("-").map(Number); m -= 1;
let sel = p.dataset.value || "";
const opt = { today: "2026-11-3", marks: (p.dataset.marks || "").split(",").filter(Boolean), time: p.dataset.time, min: p.dataset.min ? new Date(...p.dataset.min.split("-").map((v, i) => i === 1 ? v - 1 : +v)) : null };
const render = () => { cal.innerHTML = calHTML(y, m, sel, opt); };
const fmt = (k) => { const [a, b, c] = k.split("-"); return `${a}-${String(b).padStart(2, "0")}-${String(c).padStart(2, "0")}`; };
render();
if (!p.classList.contains("d-open")) cal.hidden = true;
trg?.addEventListener("click", (e) => { e.stopPropagation(); const o = !p.classList.contains("open"); closePops(p); p.classList.toggle("open", o); cal.hidden = !o; trg.setAttribute("aria-expanded", o); });
cal.addEventListener("click", (e) => {
e.stopPropagation();
const b = e.target.closest("button"); if (!b) return;
if (b.dataset.cm) { m += +b.dataset.cm; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } render(); return; }
if (b.dataset.today) { y = 2026; m = 10; sel = opt.today; }
else if (b.dataset.d) sel = b.dataset.d;
else return;
render();
if (trg) { $(".val", trg).innerHTML = fmt(sel); p.closest(".field")?.classList.remove("err"); if (!p.classList.contains("d-open")) { p.classList.remove("open"); cal.hidden = true; trg.setAttribute("aria-expanded", "false"); trg.focus(); } }
p.dispatchEvent(new CustomEvent("change", { detail: fmt(sel) }));
});
}
/* ---------- 开关、分段、标签页、半选、排序、加载按钮 ---------- */
function init(root = document) {
$$(".pop[data-select], .pop[data-menu]", root).forEach(initSelect);
$$(".pop[data-date]", root).forEach(initDate);
$$(".switch:not([data-ready])", root).forEach((s) => {
s.dataset.ready = 1;
const upd = () => { const st = $(".st", s); if (st) st.textContent = s.getAttribute("aria-checked") === "true" ? "开" : "关"; };
upd();
s.addEventListener("click", () => { if (s.disabled) return; s.setAttribute("aria-checked", s.getAttribute("aria-checked") === "true" ? "false" : "true"); upd(); });
});
$$(".seg:not([data-ready])", root).forEach((g) => {
g.dataset.ready = 1;
g.addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; $$("button", g).forEach((x) => x.setAttribute("aria-pressed", x === b)); g.dispatchEvent(new CustomEvent("change", { detail: b.dataset.v })); });
});
$$("[role=tablist]:not([data-ready])", root).forEach((tl) => {
tl.dataset.ready = 1;
const tabs = $$("[role=tab]", tl);
const sel = (t) => { tabs.forEach((x) => { const on = x === t; x.setAttribute("aria-selected", on); x.tabIndex = on ? 0 : -1; const pn = x.getAttribute("aria-controls"); if (pn) document.getElementById(pn).hidden = !on; }); };
tabs.forEach((t) => t.addEventListener("click", () => { if (t.getAttribute("aria-disabled") !== "true") sel(t); }));
tl.addEventListener("keydown", (e) => {
const i = tabs.indexOf(document.activeElement); if (i < 0) return;
const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0; if (!d) return;
e.preventDefault(); let j = i; do { j = (j + d + tabs.length) % tabs.length; } while (tabs[j].getAttribute("aria-disabled") === "true");
tabs[j].focus(); sel(tabs[j]);
});
});
$$("input[data-indeterminate]", root).forEach((c) => { c.indeterminate = true; });
$$("[data-loading]:not([data-ready])", root).forEach((b) => {
b.dataset.ready = 1;
b.addEventListener("click", () => {
if (b.classList.contains("is-loading")) return;
b.classList.add("is-loading"); b.setAttribute("aria-busy", "true");
setTimeout(() => { b.classList.remove("is-loading"); b.removeAttribute("aria-busy"); if (b.dataset.loading) toast(b.dataset.loading); }, 2200);
});
});
}
document.addEventListener("click", () => closePops());
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePops(); });
/* 可排序表格:th[data-sort] 里放 button.th-sort;按 data-sort 指定的列取 td 的 data-v 或文字 */
function sortable(table, onSort) {
$$("th[data-sort]", table).forEach((th) => {
const b = $(".th-sort", th);
b.insertAdjacentHTML("beforeend", icon("sort"));
b.addEventListener("click", () => {
const dir = th.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
$$("th[aria-sort]", table).forEach((x) => { x.removeAttribute("aria-sort"); $("svg", x).outerHTML = icon("sort"); });
th.setAttribute("aria-sort", dir);
$("svg", th).outerHTML = icon(dir === "ascending" ? "arrow-up" : "arrow-down");
onSort(th.dataset.sort, dir);
});
});
}
window.Demo = { $, $$, esc, icon, hydrate, demoTop, shell, toast, dialog, init, sortable, keysDialog, calHTML };
document.addEventListener("DOMContentLoaded", () => { hydrate(); init(); });
})();

View File

@@ -0,0 +1,300 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>表单 · 标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 只属于本页的排版 */
.lot-radios{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3)}
.f-card{max-width:none}
.hint-warn{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--warn-text);display:flex;gap:6px;align-items:flex-start}
.hint-warn .ico{width:14px;height:14px;font-size:10px;margin:3px 0 0}
</style>
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>② 表单</h1>
<p>三张真实业务表单:新建项目、资质录入、处理问题时的「忽略并注明原因」。标签在上,必填写「(必填)」,错误用赭色边框 + 下方赭色小字 + 「!」图标,离开输入框时校验、提交时汇总。</p>
<nav class="d-toc" aria-label="本页目录"><a href="#s-new">01 新建项目</a><a href="#s-states">02 状态对照</a><a href="#s-qual">03 资质录入</a><a href="#s-ignore">04 忽略并注明原因 · 致命确认说明</a></nav>
</div>
<!-- 01 新建项目 -->
<section class="d-sec" id="s-new">
<div class="d-sec-h"><span class="no">01</span><h2>新建项目</h2><span class="ref">规范 §7.13 · §8.3 · FR-M2-01</span></div>
<div class="d-grid c-side">
<form class="card f-card" novalidate data-new>
<div class="alert warn err-sum" data-errsum hidden><span class="ico w">!</span><div><p><b>有 <span data-errn>2</span> 处需要修改</b></p><ul data-errlist></ul></div></div>
<fieldset class="form-sec">
<legend>项目信息<small>招标文件解析后会自动补全,可以先只填名称</small></legend>
<div class="form">
<div class="field span2" id="f-name"><label for="n-name">项目名称 <small>(必填)</small></label><input class="input" id="n-name" placeholder="例如:某市政务服务中心智慧大厅建设项目"><span class="msg">只在你的工作台里显示,可以用简称</span></div>
<div class="field"><label for="n-no">项目编号</label><input class="input" id="n-no" placeholder="招标文件封面上的编号"></div>
<div class="field"><label for="n-buyer">采购人</label><input class="input" id="n-buyer" placeholder="例如:某市政务服务管理办公室"></div>
<div class="field span2"><label id="l-regime">法律体系</label>
<div class="lot-radios" role="radiogroup" aria-labelledby="l-regime">
<label class="choice-card"><input type="radio" class="ck" name="regime" checked><span><b>让标博士识别</b><small>按招标文件的依据和格式判断</small></span></label>
<label class="choice-card"><input type="radio" class="ck" name="regime"><span><b>政府采购</b><small>结论用「投标无效」</small></span></label>
<label class="choice-card"><input type="radio" class="ck" name="regime"><span><b>招标投标</b><small>结论用「否决投标」</small></span></label>
</div></div>
<div class="field"><label>采购方式</label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val"><span class="ph">解析后识别</span></span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option" aria-selected="true" data-label='<span class="ph">解析后识别</span>'>解析后识别</li><li class="grp">政府采购</li><li role="option">公开招标</li><li role="option">邀请招标</li><li role="option">竞争性磋商</li><li role="option">竞争性谈判</li><li role="option">询价</li></ul></div></div>
<div class="field" id="f-due"><label>投标截止 <small>(必填)</small></label>
<div class="dt-pair"><div class="pop" data-date data-marks="2026-11-20" data-new-date><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val"><span class="ph">选择日期</span></span><i data-ic="calendar"></i></button><div class="cal"></div></div><input class="input date-trigger" value="09:30" aria-label="截止时间"></div>
<span class="msg">用于工作台的倒计时;补遗更正后会提示你核对</span></div>
</div>
</fieldset>
<fieldset class="form-sec">
<legend>招标文件<small>PDF、Word 或扫描件;补遗可以以后追加</small></legend>
<div class="field fill" id="f-file">
<div class="drop" tabindex="0" role="button" aria-label="上传招标文件" data-drop><i data-ic="upload" class="lg"></i><b>拖入或点击上传招标文件</b><span>单个文件不超过 200 MB · 扩展名不可信时以文件内容为准</span></div>
<div data-files style="margin-top:var(--sp-3)"></div>
</div>
<div class="field fill" style="margin-top:var(--sp-5)"><label for="n-note">备注 <small>(选填)</small></label><textarea class="input" id="n-note" placeholder="例如:只投采购包 1,联合体伙伴待定"></textarea></div>
</fieldset>
<div class="form-foot">
<span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次 · 解析失败、超时自动退回</span>
<button type="button" class="btn">取消</button>
<button type="submit" class="btn primary swap" data-submit><span class="idle">创建并开始解析</span><span class="busy">正在创建…</span></button>
</div>
</form>
<div class="d-stack">
<div class="d-box">
<h3>试一试</h3>
<ol class="d-rules" style="border:0;padding-top:0;margin-top:0">
<li>什么都不填,直接点「创建并开始解析」:顶部汇总 + 每个字段下方说明,点汇总里的链接跳到字段。</li>
<li>在项目名称里输入再清空,离开输入框时就会校验。</li>
<li>点上传区,会加入两个示意文件,其中一个扩展名和内容不符。</li>
<li>全部填好后提交:按钮变成「正在创建…」,宽度不变,表单整体禁用。</li>
</ol>
</div>
<div class="d-box">
<h3>规则</h3>
<ul class="d-rules" style="border:0;padding-top:0;margin-top:0">
<li>必填写「(必填)」,不用红色星号;选填项多的表单反过来写「(选填)」。</li>
<li>错误说明写清楚怎么改,不写「格式错误」。</li>
<li>会扣点的提交按钮,左侧直接写扣多少、还剩多少。</li>
<li>文件扩展名和内容不符只提醒,不拦截(样本分析 07 的结论)。</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 02 状态对照 -->
<section class="d-sec" id="s-states">
<div class="d-sec-h"><span class="no">02</span><h2>同一字段的状态对照</h2><span class="ref">规范 §7.13</span></div>
<div class="d-box">
<div class="d-states wide">
<div class="d-state"><small>正常</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226IS0458"></div></div>
<div class="d-state"><small>聚焦</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input is-focus" value="01226IS04"></div></div>
<div class="d-state"><small>错误</small><div class="field err"><label>证书编号 <small>(必填)</small></label><input class="input" value="" aria-invalid="true"><span class="msg"><span class="ico w">!</span>请填写证书编号,查标时用它核对附件</span></div></div>
<div class="d-state"><small>禁用</small><div class="field dis"><label>证书编号</label><input class="input" value="01226IS0458" disabled><span class="msg">已被 2 个已递交项目引用,不能修改</span></div></div>
</div>
</div>
<div class="d-grid c2" style="margin-top:var(--sp-6)">
<div class="card">
<div class="card-h"><h3>填写中</h3><span class="more cap muted">正常状态</span></div>
<div class="form">
<div class="field"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系"></div>
<div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877"></div>
</div>
<div class="form-foot"><span class="cost">不扣额度</span><button class="btn">取消</button><button class="btn primary swap"><span class="idle">保存资质</span><span class="busy">正在保存…</span></button></div>
</div>
<div class="card">
<div class="card-h"><h3>提交中</h3><span class="more cap muted">整表禁用,按钮换成「正在…」,宽度不变</span></div>
<fieldset disabled style="border:0;margin:0;padding:0;min-width:0">
<div class="form">
<div class="field dis"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系" disabled></div>
<div class="field dis"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877" disabled></div>
</div>
</fieldset>
<div class="form-foot"><span class="cost">不扣额度</span><button class="btn" disabled>取消</button><button class="btn primary swap is-loading" aria-busy="true"><span class="idle">保存资质</span><span class="busy">正在保存…</span></button></div>
</div>
</div>
</section>
<!-- 03 资质录入 -->
<section class="d-sec" id="s-qual">
<div class="d-sec-h"><span class="no">03</span><h2>资质录入</h2><span class="ref">FR-M6 最小资质库 · 红线 5 不编造</span></div>
<div class="d-grid c-side">
<div class="dialog wide static" role="dialog" aria-labelledby="q-t" style="width:auto">
<h2 id="q-t">添加资质</h2>
<div class="body">
<p class="muted">查标时用来核对证书有没有过期、等级够不够。数据只来自你录入的内容,系统不从外部补数据。</p>
<div class="form">
<div class="field span2"><label for="q-name">名称 <small>(必填)</small></label><input class="input" id="q-name" value="ISO/IEC 27001 信息安全管理体系认证"></div>
<div class="field"><label>类型 <small>(必填)</small></label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val">体系认证</span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option">资质证书</li><li role="option" aria-selected="true">体系认证</li><li role="option">软件著作权</li><li role="option">其他</li></ul></div></div>
<div class="field"><label for="q-lv">等级 <small>(选填)</small></label><input class="input" id="q-lv" placeholder="例如:CS3、二级"></div>
<div class="field"><label for="q-no">证书编号 <small>(必填)</small></label><input class="input" id="q-no" value="01226IS0458"></div>
<div class="field"><label for="q-org">发证机构</label><input class="input" id="q-org" value="某认证中心"></div>
<div class="field"><label>发证日期</label><div class="pop" data-date data-value="2023-11-16" data-month="2023-11"><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val">2023-11-16</span><i data-ic="calendar"></i></button><div class="cal"></div></div></div>
<div class="field"><label>有效期至 <small>(必填)</small></label><div class="pop" data-date data-value="2026-11-15" data-marks="2026-11-20"><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val">2026-11-15</span><i data-ic="calendar"></i></button><div class="cal"></div></div>
<span class="hint-warn"><span class="ico w">!</span><span>早于进行中项目「智慧大厅建设项目」的投标截止日 11月20日</span></span></div>
<div class="field span2"><div class="d-row" style="gap:var(--sp-3)"><span id="q-long">长期有效</span><button type="button" class="switch" role="switch" aria-checked="false" aria-labelledby="q-long"><span class="track"></span><span class="st"></span></button><span class="cap muted">打开后不检查有效期</span></div></div>
<div class="field span2"><label>证书扫描件</label>
<div class="file-row"><span class="ext">PDF</span><div><b>ISO27001 证书 2023.pdf</b><span class="cap muted">2 页 · 1.2 MB</span></div><span class="acts"><button type="button" class="btn sm text">替换</button><button type="button" class="icon-btn sm" aria-label="移除附件" data-tip="移除附件"><i data-ic="x"></i></button></span></div>
</div>
</div>
</div>
<div class="foot"><span class="left cap muted">保存后,进行中项目的查标会用新信息复查</span><button class="btn">取消</button><button class="btn primary">保存资质</button></div>
</div>
<div class="d-stack">
<div class="d-box">
<h3>打开真实对话框 <small>可以点</small></h3>
<button class="btn primary" data-qadd><i data-ic="plus"></i>添加资质</button>
<p class="note">空表单直接保存会看到错误状态;「附件」可以稍后补录,列表里显示「待补录」占位,不生成内容。</p>
</div>
<div class="d-box">
<h3>提醒和错误的区别</h3>
<ul class="d-rules" style="border:0;padding-top:0;margin-top:0">
<li><b>错误</b>(赭色边框):必须改了才能保存,例如漏填证书编号。</li>
<li><b>提醒</b>(只有赭色小字,边框不变):可以保存,但查标会提示,例如证书在投标截止日前到期。</li>
<li>到期判断以每个项目的投标截止日为准,不以今天为准。</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 04 忽略并注明原因 -->
<section class="d-sec" id="s-ignore">
<div class="d-sec-h"><span class="no">04</span><h2>忽略并注明原因 · 致命问题的确认说明</h2><span class="ref">规范 §7.4 · §7.13 · U4</span></div>
<div class="results">
<article class="finding">
<div>
<div class="f-head"><span class="sev major">重要</span><span class="f-title">开标一览表与分项价格表合计不一致</span></div>
<p class="f-text">开标一览表 287.60 万元,分项价格表合计 286.80 万元,相差 0.80 万元。按招标文件规定,两者不一致时以开标一览表为准,不作无效处理;但会影响价格评审。</p>
<div class="u4" data-ign>
<div class="field err">
<label for="ig1">忽略原因 <small>(必填,至少 5 个字)</small></label>
<div class="chips"><button type="button" class="btn" data-chip>招标文件允许</button><button type="button" class="btn" data-chip>已在其他分册响应</button><button type="button" class="btn" data-chip>与本项目无关</button></div>
<textarea class="input" id="ig1" maxlength="200" aria-invalid="true">已核对</textarea>
<div class="msg-row"><span class="msg"><span class="ico w">!</span>至少写 5 个字,说明为什么不处理</span><span class="count"><span data-n>3</span> / 200</span></div>
</div>
<div class="row"><button class="btn primary" data-ign-ok>忽略这一项</button><button class="btn text" data-ign-cancel>取消</button><span class="cap muted">「已忽略」不等于确认无误,报告里单独列出</span></div>
</div>
</div>
<aside>
<div class="cite"><a class="cite-src" href="#">报价册 · <span class="fig">第 1 页</span></a><q>投标总价(小写):<mark>2876000.00 元</mark></q></div>
<div class="cite"><a class="cite-src" href="#">报价册 · <span class="fig">第 3 页</span></a><q><mark>合计:2868000.00 元</mark></q></div>
</aside>
</article>
<article class="finding is-fatal">
<div>
<div class="f-head"><span class="sev fatal">致命</span><span class="f-title">授权委托书的单位签章位置未发现盖章</span><span class="flag box">新增</span></div>
<p class="f-text">商务册第 5 页《法定代表人授权委托书》的「投标人(电子签章)」位置是空的。本检查只看有无,不鉴定真伪。</p>
<div class="u4">
<div class="field">
<label for="ig2">确认无误的说明 <small>(必填)</small></label>
<textarea class="input" id="ig2">电子签章在递交时由交易平台统一加盖,Word 稿里本来就是空的。</textarea>
<div class="msg-row"><span class="msg">对致命问题说「没问题」,需要写一句说明;说明会同时作为误报反馈提交。</span><span class="count"><span>30</span> / 200</span></div>
</div>
<div class="row"><button class="btn primary swap" data-loading="已确认无误 · 说明已作为反馈提交"><span class="idle">确认无误</span><span class="busy">正在保存…</span></button><button class="btn text">取消</button></div>
</div>
</div>
<aside>
<div class="cite"><a class="cite-src" href="#">招标文件 · <span class="fig">第 24 页</span></a><q>4.《法定代表人授权委托书》(法定代表人签字或盖章并<mark>加盖单位电子签章</mark>)。</q></div>
<div class="cite"><a class="cite-src" href="#">商务册 · <span class="fig">第 5 页</span></a><q>法定代表人(签字或盖章):李某某  <mark>投标人(电子签章):(签章位置为空)</mark></q></div>
</aside>
</article>
</div>
</section>
</main>
<script src="demo.js"></script>
<script>
const { $, $$, toast, dialog, icon, esc } = Demo;
Demo.demoTop("forms.html");
/* ---------- 新建项目 ---------- */
const F = $("[data-new]");
let files = [], dateVal = "";
$("[data-new-date]").addEventListener("change", (e) => { dateVal = e.detail; setErr("f-due", ""); });
function setErr(id, msg) {
const f = document.getElementById(id);
f.classList.toggle("err", !!msg);
let m = $(":scope > .msg.e", f);
if (msg) {
if (!m) { m = document.createElement("span"); m.className = "msg e"; f.appendChild(m); }
m.innerHTML = `<span class="ico w">!</span>${msg}`;
$$(":scope > .msg:not(.e)", f).forEach((x) => x.hidden = true);
} else { if (m) m.remove(); $$(":scope > .msg", f).forEach((x) => x.hidden = false); }
}
function checkName() { setErr("f-name", $("#n-name").value.trim() ? "" : "请填写项目名称,可以先写简称,之后再改"); }
$("#n-name").addEventListener("blur", checkName);
function renderFiles() {
$("[data-files]").innerHTML = files.map((f, i) => `<div class="file-row"><span class="ext">${f.ext}</span><div><b>${esc(f.name)}</b>${f.warn ? `<span class="hint-warn"><span class="ico w">!</span><span>${f.warn}</span></span>` : `<span class="cap muted">${f.meta}</span>`}</div><span class="acts"><button type="button" class="icon-btn sm" aria-label="移除" data-tip="移除" data-rm="${i}">${icon("x")}</button></span></div>`).join("");
}
$("[data-drop]").addEventListener("click", () => {
files = [{ ext: "PDF", name: "ZC-2026-0418 采购文件.pdf", meta: "186 页 · 8.4 MB" }, { ext: "DOC", name: "关于 ZC-2026-0418 的更正公告.pdf", warn: "扩展名是 .pdf,内容是 Word 文档,会按 Word 解析" }];
renderFiles(); setErr("f-file", "");
});
$("[data-files]").addEventListener("click", (e) => { const b = e.target.closest("[data-rm]"); if (b) { files.splice(+b.dataset.rm, 1); renderFiles(); } });
F.addEventListener("submit", (e) => {
e.preventDefault();
checkName();
setErr("f-due", dateVal ? "" : "请选择投标截止日期,用于倒计时");
setErr("f-file", files.length ? "" : "请上传招标文件,至少一份");
const errs = $$(".field.err", F);
const sum = $("[data-errsum]");
if (errs.length) {
$("[data-errn]").textContent = errs.length;
$("[data-errlist]").innerHTML = errs.map((f) => `<li><a href="#${f.id}">${$("label", f)?.firstChild.textContent.trim() || "招标文件"}</a>:${$(".msg.e", f).textContent.replace(/^!/, "")}</li>`).join("");
sum.hidden = false; sum.scrollIntoView({ block: "center", behavior: "smooth" });
return;
}
sum.hidden = true;
const b = $("[data-submit]");
b.classList.add("is-loading"); b.setAttribute("aria-busy", "true");
$$("input, textarea, button:not([data-submit]), .drop", F).forEach((x) => { x.disabled = true; x.closest(".field")?.classList.add("dis"); });
setTimeout(() => { b.classList.remove("is-loading"); b.removeAttribute("aria-busy"); $$("input, textarea, button", F).forEach((x) => x.disabled = false); $$(".field.dis", F).forEach((x) => x.classList.remove("dis")); toast("项目已创建,正在解析招标文件 · 可以离开本页"); }, 2400);
});
/* ---------- 资质对话框 ---------- */
$("[data-qadd]").onclick = () => dialog({
title: "添加资质", wide: true, ok: "保存资质",
footLeft: '<span class="cap muted">不扣额度</span>',
body: `<p class="muted">数据只来自你录入的内容,系统不从外部补数据。</p><div class="form">
<div class="field span2" id="qd-name"><label for="qdn">名称 <small>(必填)</small></label><input class="input" id="qdn" placeholder="例如:信息系统建设和服务能力等级证书"></div>
<div class="field"><label>类型 <small>(必填)</small></label><div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val">资质证书</span>${icon("chev-down")}</button><ul class="menu" role="listbox"><li role="option" aria-selected="true">资质证书</li><li role="option">体系认证</li><li role="option">软件著作权</li><li role="option">其他</li></ul></div></div>
<div class="field"><label for="qdl">等级 <small>(选填)</small></label><input class="input" id="qdl"></div>
<div class="field" id="qd-no"><label for="qdo">证书编号 <small>(必填)</small></label><input class="input" id="qdo"></div>
<div class="field"><label>有效期至</label><div class="pop" data-date><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val"><span class="ph">选择日期</span></span>${icon("calendar")}</button><div class="cal"></div></div></div>
<div class="field span2"><label>证书扫描件 <small>(可以稍后补录)</small></label><div class="drop" style="padding:var(--sp-4)">${icon("upload")}<span>拖入或点击上传 PDF / 图片</span></div></div></div>`,
onOk: (s) => {
let bad = 0;
[["qd-name", "#qdn", "请填写资质名称,和证书上的写法一致"], ["qd-no", "#qdo", "请填写证书编号,查标时用它核对附件"]].forEach(([id, sel, msg]) => {
const f = $("#" + id, s), v = $(sel, s).value.trim();
f.classList.toggle("err", !v); $(".msg", f)?.remove();
if (!v) { bad++; f.insertAdjacentHTML("beforeend", `<span class="msg"><span class="ico w">!</span>${msg}</span>`); }
});
if (bad) return false;
toast("已保存资质");
}
});
/* ---------- 忽略原因 ---------- */
const ig = $("[data-ign]"), ta = $("#ig1", ig), fld = $(".field", ig);
const validate = (show) => { const n = ta.value.trim().length; $("[data-n]", ig).textContent = ta.value.length; const bad = n < 5; if (show || !bad) { fld.classList.toggle("err", bad); $(".msg", fld).innerHTML = bad ? '<span class="ico w">!</span>至少写 5 个字,说明为什么不处理' : "报告和处理记录里会显示这段原因"; } return !bad; };
ta.addEventListener("input", () => validate(false));
ta.addEventListener("blur", () => validate(true));
$$("[data-chip]", ig).forEach((c) => c.onclick = () => { ta.value = c.textContent; validate(true); ta.focus(); });
$("[data-ign-ok]").onclick = () => { if (validate(true)) toast("已忽略 · 原因已记录", { undo: () => toast("已撤销", { ok: false }) }); };
</script>
</body>
</html>

View File

@@ -0,0 +1,95 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
.qs td:first-child{white-space:nowrap;font-family:var(--font-figures);font-weight:var(--fw-bold)}
.qs td:nth-child(2){font-weight:var(--fw-bold)}
.comp td:first-child{white-space:nowrap;font-family:var(--font-figures);font-weight:var(--fw-medium);color:var(--text-secondary)}
.comp code,.qs code{font-family:var(--font-figures);font-size:13px;color:var(--text-secondary)}
pre.run{margin:var(--sp-3) 0 0;padding:var(--sp-3) var(--sp-4);background:var(--bg-sunken);border-radius:var(--radius-sm);font:13px/20px ui-monospace,Menlo,Consolas,monospace;color:var(--text-primary);overflow:auto}
</style>
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>标博士 · UI demo(2026-11-03)</h1>
<p>用已确定的界面风格(方向 A「校样」、设计规范 v0.1)把常见控件和它们组合成的页面摆出来,用来决定 UI 精修的方向。颜色、字号、间距只用 <code>system/tokens.css</code> 的变量;已有组件直接用 <code>system/components.css</code>,规范里有描述但还没实现的组件放在本目录的 <code>demo.css</code>。不用任何 UI 框架和 CDN(AR10),图标是 Lucide 风格的内联 SVG(U3)。</p>
<pre class="run">cd docs &amp;&amp; python3 -m http.server 8767
# 打开 http://127.0.0.1:8767/design/ui-demo/ (设计基准 1440 宽,最小 1280)</pre>
</div>
<section class="d-sec">
<div class="d-sec-h"><span class="no">01</span><h2>页面</h2><span class="ref">示意数据:政府采购 · IT 集成,复用原型 prototype/assets/data.js</span></div>
<div class="d-cards">
<a class="d-link" href="controls.html"><b><span class="fig">①</span>控件总览</b><p>按钮(类型 × 6 种状态、尺寸、扣点说明)、输入框 / 文本域 / 下拉选择 / 日期选择 / 复选 / 单选 / 开关、级别标签、AI 标识与署名、出处 5 种形态、页内提醒、轻提示、对话框、标签页、分段、分页、空状态、骨架屏、进度、提示气泡、下拉菜单、快捷键。</p></a>
<a class="d-link" href="forms.html"><b><span class="fig">②</span>表单</b><p>新建项目(离开时校验 + 提交汇总 + 提交中整表禁用)、同一字段的正常 / 聚焦 / 错误 / 禁用 / 提交中对照、资质录入(错误与「到期提醒」的区别)、忽略并注明原因、致命问题的「确认无误」说明(U4)。</p></a>
<a class="d-link" href="cards.html"><b><span class="fig">③</span>卡片</b><p>项目卡(致命 / 临近截止 / 今天截止 / 已递交 / 加载中 / 新建)、统计格、额度卡(正常 / 80% / 用完)、任务进度卡(进行中 / 失败退回 / 完成 / 排队)、AI 摘要卡(确认前后)、关键日期卡。</p></a>
<a class="d-link" href="lists.html"><b><span class="fig">④</span>列表与表格</b><p>项目列表(筛选、搜索、表头排序、空结果)、清单列表(分类、★ 筛选、筛选标签、J / K)、问题列表(级别筛选、只对「提示」批量、J / K + 1 / 2 / 3、已处理折叠)、额度流水、参数对照表、两种空结果。</p></a>
<a class="d-link" href="page-workbench.html"><b><span class="fig">⑤</span>组合页 · 工作台</b><p>侧边栏 + 顶栏框架;截止提醒、统计格、项目表格、待处理的致命问题,右侧任务进度、额度、最近处理。看「列表」布局的密度。</p></a>
<a class="d-link" href="page-reading.html"><b><span class="fig">⑥</span>组合页 · 读标</b><p>项目页头 + 页签、清单状态与确认、分类导航、清单(待确认区、补遗更新、搜索)、原文查看器并排高亮。可以用 J / K 逐条看原文。看「对照」布局的密度。</p></a>
</div>
<p class="note">资质库、额度页属于和工作台相同的「列表」布局,控件已在 ②③④ 覆盖,没有单独做组合页。</p>
</section>
<section class="d-sec">
<div class="d-sec-h"><span class="no">02</span><h2>新增组件清单</h2><span class="ref">都在 demo.css 的 B 部分;精修定稿后并入 system/components.css</span></div>
<table class="data comp">
<thead><tr><th>编号</th><th>组件</th><th>类名</th><th>依据</th><th>说明</th></tr></thead>
<tbody>
<tr><td>B1</td><td>图标</td><td><code>.ic</code></td><td>§6 · U3</td><td>Lucide 路径内联,1.5px 描边,16 / 20 / 32</td></tr>
<tr><td>B2</td><td>页面框架</td><td><code>.sidebar .top .crumbs .quota-chip .content .page-title .proj-head</code></td><td>§5.3</td><td>从原型 app.css 搬过来,去掉裸色值</td></tr>
<tr><td>B3</td><td>按钮补充状态</td><td><code>.btn.swap .is-loading .icon-btn .btn-group</code></td><td>§7.1</td><td>按下态、加载中定宽换字、只有图标的按钮、按钮组</td></tr>
<tr><td>B4</td><td>输入框补充</td><td><code>.input:focus/[disabled]/[readonly] .input-unit .search .form .form-sec .err-sum</code></td><td>§7.13</td><td>聚焦 / 禁用 / 只读、带单位、搜索、两列表单、错误汇总、字数</td></tr>
<tr><td>B5</td><td>下拉选择 · 下拉菜单</td><td><code>.select .menu .pop</code></td><td>AR10</td><td>选中项加粗 + ✓,不可选项写原因;正式实现用 Reka UI Select / DropdownMenu</td></tr>
<tr><td>B6</td><td>日期选择</td><td><code>.cal .dt-pair .date-trigger</code></td><td>§9.3 · AR10</td><td>周一开始、Barlow 数字、今天描边、选中墨底、关键日期圆点、不可选删除线</td></tr>
<tr><td>B7</td><td>复选 · 单选 · 单选卡片 · 开关</td><td><code>.ck .choice .choice-card .switch</code></td><td>§8.2</td><td>墨色勾选、半选;开关是方形滑块 + 开 / 关文字</td></tr>
<tr><td>B8</td><td>分段控件 · 标签页</td><td><code>.seg .tabs [role=tab]</code></td><td>§5.3 · §7.5</td><td>← → 切换、不可用页签</td></tr>
<tr><td>B9</td><td>分页</td><td><code>.pagination</code></td><td>—</td><td>规范未定义;当前页墨色描边</td></tr>
<tr><td>B10</td><td>轻提示</td><td><code>.toasts .toast</code></td><td>§7.12</td><td>4 秒消失、悬停暂停、可撤销、堆叠</td></tr>
<tr><td>B11</td><td>对话框</td><td><code>.scrim .dialog .wide</code></td><td>§7.14</td><td>480 / 640,焦点锁定,Esc 关闭,焦点回到触发处</td></tr>
<tr><td>B12</td><td>提醒补充</td><td><code>.alert .act .sub .errno</code></td><td>§7.12 · §7.15</td><td>右侧操作、副说明、错误编号</td></tr>
<tr><td>B13</td><td>提示气泡</td><td><code>[data-tip]</code></td><td>§6</td><td>墨底白字;正式实现用 Reka UI Tooltip</td></tr>
<tr><td>B14</td><td>空状态 · 骨架屏 · 进度</td><td><code>.empty .skel .skel-row .progress .steps .fail</code></td><td>§7.11 · §7.15</td><td>骨架缓慢明暗变化,减少动态效果时静止</td></tr>
<tr><td>B15</td><td>杂项</td><td><code>.kbd .kv .chip .drop .file-row .flowline</code></td><td>§8.2</td><td>快捷键、键值表、可移除的筛选标签、上传区、文件行、状态流</td></tr>
<tr><td>B16</td><td>AI 产出块</td><td><code>.ai-block .ai-label .sign.ign .more-cites</code></td><td>§3.3 · §7.16</td><td>左侧虚线 → 确认后墨色实线;「已忽略」灰色署名</td></tr>
<tr><td>B17</td><td>卡片</td><td><code>.card-h .card-foot .pcard .stats .stat .quota .dates</code></td><td>§7.9 · §7.10</td><td>项目卡、统计格、额度卡、关键日期</td></tr>
<tr><td>B18</td><td>表格补充</td><td><code>.toolbar .th-sort .batchbar .list-box .filters-on</code></td><td>§7.7 · §8.2</td><td>排序、勾选、选中行、批量栏、空结果行、问题行勾选</td></tr>
<tr><td>B19</td><td>对照布局</td><td><code>.split .cats .viewer-body</code></td><td>§5.3 · §7.8</td><td>分类导航 + 列表 + 原文查看器</td></tr>
</tbody>
</table>
</section>
<section class="d-sec">
<div class="d-sec-h"><span class="no">03</span><h2>做的过程中发现的规范缺口 · 待拍板</h2><span class="ref">UI 精修阶段逐条确认</span></div>
<table class="data qs">
<thead><tr><th>#</th><th>问题</th><th>demo 现在的做法</th><th>需要决定</th></tr></thead>
<tbody>
<tr><td>Q1</td><td>额度流水的「+」用了定稿绿</td><td>沿用 components.css 的 <code>.flow td.plus</code>(绿)</td><td>§3.4 说绿色只表示「有人确认过」。建议改成墨色「+」,靠符号区分</td></tr>
<tr><td>Q2</td><td>AI 产出的正文是什么颜色</td><td>正文始终墨色,只用「AI」标签 + 左侧虚线区分</td><td>§7.16 写「确认后文字变为墨色」,暗示确认前不是墨色;大段蓝字阅读负担重,建议明确「正文始终墨色」</td></tr>
<tr><td>Q3</td><td>「选中」用什么颜色</td><td>读标清单选中 = 蓝铅笔浅底(和原文高亮对应);表格勾选 = 凹陷灰 + 墨色竖线</td><td>蓝色只属于系统和出处(§3.4)。读标选中是否保留蓝色,需统一</td></tr>
<tr><td>Q4</td><td>开关、分页、下拉、日期选择的视觉</td><td>开关做成方形滑块 + 开 / 关文字;分页当前页描边;下拉选中加粗 + ✓;不可选日期加删除线</td><td>规范第 7 节没有这几个组件,确认后补进规范</td></tr>
<tr><td>Q5</td><td>参数对照表加「级别」列</td><td>多了一列级别标签,判断词颜色有形状兜底</td><td>§7.7 的表没有这一列;只靠判断词颜色对色弱不够</td></tr>
<tr><td>Q6</td><td>悬停、按下、遮罩缺颜色变量</td><td>用 <code>color-mix()</code> 从墨色算;components.css 的 <code>#34404D</code>、原文页的 <code>#222</code> <code>#888</code> 是裸色值</td><td>建议 tokens.css 增加 <code>--c-ink-hover</code>、<code>--scrim</code> 等语义变量</td></tr>
<tr><td>Q7</td><td>字阶外的字号</td><td>Barlow 数字用了 13 / 15px(components.css 已有)</td><td>§4.2 字阶里没有,建议加 <code>--fs-figure-sm</code> 等变量</td></tr>
<tr><td>Q8</td><td>骨架屏动不动</td><td>1.6 秒缓慢明暗变化;减少动态效果时静止</td><td>和「不闪烁」原则是否冲突</td></tr>
<tr><td>Q9</td><td>顶栏额度 80% 后是否变色</td><td>顶栏额度描边变赭色</td><td>§7.10 只写了额度条,没写顶栏</td></tr>
<tr><td>Q10</td><td>日期的输入格式</td><td>界面显示「11月20日 09:30」,输入框用「2026-11-20」</td><td>§9.3 只规定了显示格式</td></tr>
<tr><td>Q11</td><td>项目卡片视图</td><td>做了项目卡,工作台仍用表格</td><td>§7.9 只定义了项目行;要不要提供卡片视图</td></tr>
<tr><td>Q12</td><td>卡片 / 页面里同时出现多个主按钮</td><td>组合页每屏一个主按钮;demo 陈列页为了对比会出现多个</td><td>表单页、对话框里的「保存」是否也算「一屏一个」</td></tr>
</tbody>
</table>
</section>
</main>
<script src="demo.js"></script>
<script>Demo.demoTop("index.html");</script>
</body>
</html>

View File

@@ -0,0 +1,295 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>列表与表格 · 标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
.flow-sub td{border-top:0}
.flow-sub td:first-child{color:var(--text-secondary)}
.fl-link{display:inline-block;width:14px;color:var(--text-secondary)}
</style>
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>④ 列表与表格</h1>
<p>筛选栏 + 表格 / 列表。表头凹陷灰底 + 2px 墨色底线,行高 40;数字用 Barlow 等宽。演示筛选、排序、批量勾选、键盘逐条处理和空结果。</p>
<nav class="d-toc" aria-label="本页目录"><a href="#s-proj">01 项目列表</a><a href="#s-list">02 清单列表</a><a href="#s-find">03 问题列表</a><a href="#s-flow">04 额度流水</a><a href="#s-param">05 参数对照表</a><a href="#s-empty">06 空结果</a></nav>
</div>
<!-- 01 项目列表 -->
<section class="d-sec" id="s-proj">
<div class="d-sec-h"><span class="no">01</span><h2>项目列表</h2><span class="ref">规范 §7.9 · 可以筛选、搜索、点表头排序</span></div>
<div class="toolbar">
<div class="seg" role="group" aria-label="按状态筛选" data-pseg><button aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button><button aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button><button aria-pressed="false" data-v="已递交">已递交</button><button aria-pressed="false" data-v="已归档">已归档</button></div>
<div class="pop inline" data-select data-plot><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val">所有体系</span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option" aria-selected="true" data-value="">所有体系</li><li role="option" data-value="政府采购">政府采购</li><li role="option" data-value="招标投标">招标投标</li></ul></div>
<div class="right"><label class="search"><i data-ic="search"></i><input class="input" placeholder="搜索项目、采购人、编号" aria-label="搜索项目" data-psearch></label></div>
</div>
<table class="data hover" data-ptable>
<thead><tr><th class="sortable" data-sort="name"><button class="th-sort">项目</button></th><th>所投包</th><th class="sortable" data-sort="hours" aria-sort="ascending"><button class="th-sort">投标截止</button></th><th>状态</th><th>就绪</th><th class="sortable r" data-sort="fatal"><button class="th-sort">致命</button></th><th class="r"></th></tr></thead>
<tbody data-prows></tbody>
</table>
<p class="note">默认按截止时间升序,截止临近的排在前面。「就绪」只在「已检查」显示,只表示致命问题都已处理,不代表投标文件没有问题。</p>
</section>
<!-- 02 清单列表 -->
<section class="d-sec" id="s-list">
<div class="d-sec-h"><span class="no">02</span><h2>清单列表</h2><span class="ref">规范 §7.5 · 点这块后按 J / K 移动、Enter 打开出处</span></div>
<div class="toolbar">
<div class="seg" role="group" aria-label="按分类筛选" data-cseg><button aria-pressed="true" data-v="">全部 <span class="fig">12</span></button><button aria-pressed="false" data-v="qual">资格条件</button><button aria-pressed="false" data-v="invalid">无效条款</button><button aria-pressed="false" data-v="param">技术参数</button><button aria-pressed="false" data-v="date">日期与递交</button></div>
<label class="choice"><input type="checkbox" class="ck" data-star>只看 ★ 实质性条款</label>
<div class="right"><span class="cap muted"><span class="kbd">J</span> <span class="kbd">K</span> 移动 · <span class="kbd">Enter</span> 打开出处</span></div>
</div>
<div class="filters-on" data-chips></div>
<div class="list-box" data-clist tabindex="0" aria-label="清单,按 J K 移动"></div>
</section>
<!-- 03 问题列表 -->
<section class="d-sec" id="s-find">
<div class="d-sec-h"><span class="no">03</span><h2>问题列表</h2><span class="ref">规范 §7.4 · §8.2 · 只有「提示」级可以批量</span></div>
<div class="toolbar">
<div class="seg" role="group" aria-label="按级别筛选" data-fseg>
<button aria-pressed="true" data-v="">全部 <span class="fig" data-c=""></span></button>
<button aria-pressed="false" data-v="fatal"><span class="sev fatal">致命<span class="fig" data-c="fatal"></span></span></button>
<button aria-pressed="false" data-v="major"><span class="sev major">重要<span class="fig" data-c="major"></span></span></button>
<button aria-pressed="false" data-v="hint"><span class="sev hint">提示<span class="fig" data-c="hint"></span></span></button>
</div>
<span class="cap muted">只显示待处理的问题;已处理的在列表底部。</span>
<div class="right"><button class="btn sm text" onclick="Demo.keysDialog()">快捷键 <span class="kbd">?</span></button></div>
</div>
<div class="batchbar" data-batch hidden><b>已选 <span class="fig" data-bn>0</span> 项「提示」</b><span class="cap muted">批量处理只开放给「提示」级</span><span class="r"><button class="btn sm" data-bok>批量确认无误</button><button class="btn sm text" data-bclear>取消选择</button></span></div>
<div class="results" data-flist tabindex="0" aria-label="问题列表,按 J K 移动,1 2 3 处理"></div>
</section>
<!-- 04 额度流水 -->
<section class="d-sec" id="s-flow">
<div class="d-sec-h"><span class="no">04</span><h2>额度流水</h2><span class="ref">规范 §7.10 · 红线 9 额度即货币</span></div>
<div class="toolbar">
<div class="seg" role="group" aria-label="按额度类型筛选"><button aria-pressed="true">全部</button><button aria-pressed="false">解析</button><button aria-pressed="false">检查</button></div>
<div class="pop inline" data-date data-month="2026-11"><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false" style="width:160px"><span class="val">2026-11</span><i data-ic="calendar"></i></button><div class="cal"></div></div>
<div class="right"><button class="btn sm"><i data-ic="download"></i>导出 CSV</button></div>
</div>
<table class="data flow">
<thead><tr><th>时间</th><th>类型</th><th>额度</th><th class="r">数量</th><th class="r">余额</th><th>关联项目 / 任务</th><th>说明</th></tr></thead>
<tbody>
<tr><td class="num">11-02 14:31</td><td>退回</td><td>解析</td><td class="num r plus">+1</td><td class="num r">37</td><td>智慧大厅建设项目 · 解析任务 PJ-0417</td><td class="muted">解析失败:文件已加密</td></tr>
<tr class="flow-sub"><td class="num"><span class="fl-link">└</span>11-02 14:28</td><td>预扣</td><td>解析</td><td class="num r">−1</td><td class="num r">36</td><td>智慧大厅建设项目 · 解析任务 PJ-0417</td><td class="muted">和上一行的退回对应</td></tr>
<tr><td class="num">11-02 14:20</td><td>—</td><td>检查</td><td class="num r">0</td><td class="num r">12</td><td>智慧大厅建设项目 · 第 2 次检查</td><td class="muted">24 小时内复查,不扣</td></tr>
<tr><td class="num">11-02 10:05</td><td>结算</td><td>解析</td><td class="num r">−1</td><td class="num r">37</td><td>智慧大厅建设项目 · 补遗 1</td><td></td></tr>
<tr><td class="num">11-01 16:00</td><td>结算</td><td>检查</td><td class="num r">−1</td><td class="num r">12</td><td>智慧大厅建设项目 · 第 1 次检查</td><td></td></tr>
<tr><td class="num">11-01 10:40</td><td>结算</td><td>解析</td><td class="num r">−1</td><td class="num r">38</td><td>智慧大厅建设项目 · 招标文件</td><td></td></tr>
<tr><td class="num">11-01 09:30</td><td>人工调整</td><td>检查</td><td class="num r plus">+2</td><td class="num r">13</td><td>—</td><td class="muted">运营:系统异常补偿,工单 GD-1101</td></tr>
<tr><td class="num">11-01 09:00</td><td>发放</td><td>解析 / 检查</td><td class="num r plus">+50 / +20</td><td class="num r">50 / 20</td><td>专业版开通(对公转账)</td><td class="muted">开通单 KT-1101</td></tr>
</tbody>
</table>
<div class="pagination" data-fpager></div>
<ul class="d-rules"><li>增加用定稿绿「+」(这里绿色表示「入账」,是否允许请在精修时拍板,见目录页的待定问题),扣减用墨色「−」;退回记录和对应的预扣记录相邻显示。</li></ul>
</section>
<!-- 05 参数对照表 -->
<section class="d-sec" id="s-param">
<div class="d-sec-h"><span class="no">05</span><h2>参数对照表</h2><span class="ref">规范 §7.7</span></div>
<table class="data">
<thead><tr><th>参数项</th><th>要求</th><th>投标响应</th><th>判断</th><th>级别</th><th>出处</th></tr></thead>
<tbody>
<tr class="is-fatal"><td>★ 应用服务器 · 内存</td><td class="num">≥ 512G DDR4</td><td class="num">256G DDR4</td><td><span class="verdict fatal">疑似不满足</span></td><td><span class="sev fatal">致命</span></td><td><a href="#">第 <span class="fig">41</span> 页</a> ↔ <a href="#">技术册 第 <span class="fig">12</span> 页</a></td></tr>
<tr><td>★ 应用服务器 · 系统盘</td><td class="num">2 块 ≥ 960G SSD<div class="cap">补遗 1 更新,原为 <s>≥ 480G</s></div></td><td class="num">2 块 960G SSD</td><td>满足</td><td></td><td><a href="#">补遗 1 第 <span class="fig">1</span> 页</a> ↔ <a href="#">技术册 第 <span class="fig">12</span> 页</a></td></tr>
<tr><td>★ 应用服务器 · CPU</td><td class="num">≥ 2 颗 32C 2.7GHz</td><td class="num">2 颗 7380 32C 2.7GHz</td><td>满足</td><td></td><td><a href="#">第 <span class="fig">41</span> 页</a> ↔ <a href="#">技术册 第 <span class="fig">12</span> 页</a></td></tr>
<tr><td>★ 核心交换机 · BFD 间隔</td><td class="num">≤ 50ms</td><td class="num">3.3ms</td><td>满足</td><td></td><td><a href="#">第 <span class="fig">44</span> 页</a> ↔ <a href="#">技术册 第 <span class="fig">58</span> 页</a></td></tr>
<tr><td>★ 核心交换机 · 证明材料</td><td>官网参数截图等</td><td class="muted">—</td><td><span class="verdict major">未找到响应</span></td><td><span class="sev major">重要</span></td><td><a href="#">第 <span class="fig">44</span> 页</a> ↔ <span class="cap muted">已查找:技术册、商务册附件</span></td></tr>
<tr><td>▲ 核心交换机 · 交换容量</td><td class="num">≥ 19.2Tbps</td><td class="num">19.2Tbps / 48Tbps</td><td><span class="verdict manual">需人工判断</span></td><td><span class="sev hint">提示</span></td><td><a href="#">第 <span class="fig">44</span> 页</a> ↔ <a href="#">技术册 第 <span class="fig">58</span> 页</a></td></tr>
<tr><td>排队叫号一体机 · 显示屏</td><td class="num">≥ 21.5 英寸</td><td class="num">21 英寸</td><td><span class="verdict hint">疑似不满足</span></td><td><span class="sev hint">提示</span></td><td><a href="#">第 <span class="fig">47</span> 页</a> ↔ <a href="#">技术册 第 <span class="fig">77</span> 页</a></td></tr>
</tbody>
</table>
<ul class="d-rules"><li>判断只用四个词:满足 / 疑似不满足 / 未找到响应 / 需人工判断。「满足」用墨色普通字,不用绿色。</li><li>要求和响应列用 Barlow,数值左对齐。规范的表没有「级别」列,这里加了一列标签,让判断词的颜色有形状兜底(色弱)——是否保留请拍板。</li></ul>
</section>
<!-- 06 空结果 -->
<section class="d-sec" id="s-empty">
<div class="d-sec-h"><span class="no">06</span><h2>空结果</h2><span class="ref">规范 §7.15</span></div>
<div class="d-grid c2">
<div>
<span class="d-label">筛选后没有结果:写清楚条件,给「清除筛选」</span>
<table class="data"><thead><tr><th>项目</th><th>所投包</th><th>投标截止</th><th>状态</th></tr></thead>
<tbody><tr><td colspan="4" class="empty-cell"><div class="empty sm"><b>没有符合条件的项目</b><p>「已归档」里没有名称含「医保」的项目。</p><button class="btn sm">清除筛选</button></div></td></tr></tbody></table>
</div>
<div>
<span class="d-label">问题全部处理完:说清楚意味着什么,不庆祝</span>
<div class="results"><div class="empty sm"><i data-ic="list" class="xl"></i><b>待处理的问题都已处理</b><p>致命问题已全部处理,项目为「就绪」。这只表示每一项都有人处理过,不代表投标文件没有问题。</p><button class="btn sm">导出报告</button></div>
<button class="group-h"><i data-ic="chev-right"></i>已处理(<span class="fig">9</span>)</button></div>
</div>
</div>
</section>
</main>
<script src="demo.js"></script>
<script>
const { $, $$, esc, icon, toast, dialog } = Demo;
Demo.demoTop("lists.html");
/* ---------- 01 项目列表 ---------- */
const PROJ = [
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", regime: "政府采购", lot: "采购包 1", hours: 408, due: "11月20日 09:30 · 剩 17 天", cls: "", status: "已检查", ready: "未就绪", fatal: 3 },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", regime: "政府采购", lot: "全部", hours: 53, due: "剩 2 天 5 小时", cls: "due-soon", status: "已检查", ready: "就绪", fatal: 0 },
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", regime: "招标投标", lot: "标段 1", hours: 6, due: "今天 17:00 截止", cls: "due-today", status: "编制中", ready: "", fatal: 0 },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", regime: "政府采购", lot: "全部", hours: 600, due: "11月28日 10:00 · 剩 25 天", cls: "", status: "跟进中", ready: "", fatal: 0 },
{ name: "某区智慧校园一期", buyer: "某区教育局", regime: "政府采购", lot: "全部", hours: -100, due: "已截止", cls: "due-past", status: "已递交", ready: "", fatal: 0 },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", regime: "政府采购", lot: "采购包 2", hours: -900, due: "已截止", cls: "due-past", status: "已归档", ready: "", fatal: 0 }
];
let pf = { seg: "active", q: "", regime: "", sort: ["hours", "ascending"] };
function prows() {
let rows = PROJ.filter((p) => (pf.seg === "all" || (pf.seg === "active" ? !["已递交", "已归档"].includes(p.status) : p.status === pf.seg))
&& (!pf.regime || p.regime === pf.regime) && (!pf.q || (p.name + p.buyer).includes(pf.q)));
const [k, d] = pf.sort;
rows.sort((a, b) => (k === "name" ? a.name.localeCompare(b.name, "zh") : a[k] - b[k]) * (d === "ascending" ? 1 : -1));
const segName = { active: "进行中", all: "全部", "已递交": "已递交", "已归档": "已归档" }[pf.seg];
$("[data-prows]").innerHTML = rows.map((p) => `<tr>
<td><div class="proj-name">${p.name}</div><div class="proj-sub">${p.buyer} · ${p.regime}</div></td>
<td>${p.lot}</td><td class="nw ${p.cls}">${p.due}</td><td><span class="tag status">${p.status}</span></td>
<td class="nw">${p.ready === "就绪" ? '<span class="tag status" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>' : p.ready || '<span class="muted">—</span>'}</td>
<td class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="muted">—</span>'}</td>
<td class="acts"><button class="icon-btn sm tip-left" aria-label="更多操作" data-tip="更多操作">${icon("more")}</button></td></tr>`).join("")
|| `<tr><td colspan="7" class="empty-cell"><div class="empty sm"><b>没有符合条件的项目</b><p>「${segName}」里没有${pf.regime ? `「${pf.regime}」体系、` : ""}${pf.q ? `名称或采购人含「${esc(pf.q)}」的` : ""}项目。</p><button class="btn sm" data-pclear>清除筛选</button></div></td></tr>`;
}
$("[data-pseg]").addEventListener("change", (e) => { pf.seg = e.detail; prows(); });
$("[data-plot]").addEventListener("change", (e) => { pf.regime = e.detail; prows(); });
$("[data-psearch]").addEventListener("input", (e) => { pf.q = e.target.value.trim(); prows(); });
document.addEventListener("click", (e) => {
if (e.target.closest("[data-pclear]")) { pf = { ...pf, seg: "all", q: "", regime: "" }; $("[data-psearch]").value = ""; $$("[data-pseg] button").forEach((b) => b.setAttribute("aria-pressed", b.dataset.v === "all")); $("[data-plot] .val").textContent = "所有体系"; prows(); }
else if (e.target.closest("[data-prows] tr") && !e.target.closest("button")) toast("(演示)打开项目详情");
});
Demo.sortable($("[data-ptable]"), (k, d) => { pf.sort = [k, d]; prows(); });
$("[data-ptable] th[aria-sort] svg").outerHTML = icon("arrow-up");
prows();
/* ---------- 02 清单列表 ---------- */
const CL = [
{ imp: "★", t: "应用服务器:内存 ≥ 512G DDR4", cats: ["param", "invalid"], lot: "采购包 1", src: "ai", p: 41 },
{ imp: "★", t: "应用服务器:国产自主可控 CPU,提供国产化适配承诺函", cats: ["param", "invalid"], lot: "采购包 1", src: "ai", p: 41 },
{ imp: "★", t: "应用服务器:系统盘 2 块 ≥ 960G SSD", old: "2 块 ≥ 480G SSD", cats: ["param"], lot: "采购包 1", src: "add", p: 1, doc: "补遗 1" },
{ imp: "★", t: "核心交换机:支持硬件 BFD,最小发送间隔 ≤ 50ms,提供证明材料", cats: ["param", "invalid"], lot: "采购包 1", src: "ai", p: 44 },
{ imp: "▲", t: "核心交换机:交换容量 ≥ 19.2Tbps,包转发率 ≥ 2880Mpps", cats: ["param"], lot: "采购包 1", src: "ai", p: 44 },
{ imp: "", t: "营业执照等主体资格证明文件(扫描件)", cats: ["qual", "invalid"], lot: "通用", src: "ai", p: 23 },
{ imp: "", t: "财务状况报告:资产负债表 + 利润表月报表(开标前 6 个月内)", cats: ["qual", "invalid"], lot: "通用", src: "ai", p: 23 },
{ imp: "", t: "不接受联合体投标", cats: ["qual"], lot: "通用", src: "manual", p: 2, edited: true },
{ imp: "", t: "报价不超过采购包 1 最高限价 286.00 万元", cats: ["invalid"], lot: "采购包 1", src: "ai", p: 2 },
{ imp: "", t: "投标有效期不少于 90 天", cats: ["invalid", "date"], lot: "通用", src: "ai", p: 21 },
{ imp: "", t: "提交投标文件截止时间:2026年11月20日 09:30", old: "2026年11月18日 09:30", cats: ["date"], lot: "通用", src: "add", p: 1, doc: "补遗 1", confirmed: true },
{ imp: "", t: "递交方式:电子交易平台在线递交,CA 加密", cats: ["date"], lot: "通用", src: "ai", p: 6 }
];
const CAT = { qual: "资格条件", invalid: "无效条款", param: "技术参数", date: "日期与递交", score: "评分办法" };
let cf = { cat: "", star: false }, cur = 0;
function crows() {
const rows = CL.filter((c) => (!cf.cat || c.cats.includes(cf.cat)) && (!cf.star || c.imp === "★"));
const chips = [cf.cat && `<span class="chip">分类:${CAT[cf.cat]}<button aria-label="移除筛选" data-xc="cat">${icon("x")}</button></span>`, cf.star && `<span class="chip">只看 ★<button aria-label="移除筛选" data-xc="star">${icon("x")}</button></span>`].filter(Boolean);
$("[data-chips]").innerHTML = chips.length ? `<span>筛选:</span>${chips.join("")}<span>· <span class="fig m">${rows.length}</span> 条</span><button class="btn sm text" data-xc="all">清除全部</button>` : "";
cur = Math.min(cur, Math.max(rows.length - 1, 0));
$("[data-clist]").innerHTML = `<div class="lb-h"><span>要求</span><span class="r">来源 · 出处</span></div>` + (rows.map((c, i) => {
const src = c.src === "ai" ? '<span class="src-ai">AI 提取</span>' : c.src === "add" ? c.doc : "人工添加";
const upd = c.old && !c.confirmed ? `<div class="upd"><span class="flag">已被${c.doc} 更新 · 原为 <s>${c.old}</s></span><button class="btn" data-ack="${i}">已核对</button></div>` : "";
return `<div class="item ${i === cur ? "focus" : ""}" data-ci="${i}" tabindex="-1"><span class="imp">${c.imp}</span>
<div><div class="t">${c.t}</div><div class="meta">${c.cats.map((k) => `<span class="tag">${CAT[k]}</span>`).join("")}<span class="tag">${c.lot}</span>${c.edited ? '<span class="flag">已修改 · <a class="plain" href="#">查看记录</a></span>' : ""}</div>${upd}</div>
<div class="side">${src}<br><a class="cite-src" href="#" data-open>${c.doc ? c.doc + " · " : ""}第 <span class="fig">${c.p}</span> 页</a></div></div>`;
}).join("") || `<div class="empty sm"><b>这个分类下没有 ★ 条款</b><p>★ 条款都在「技术参数」和「无效条款」里。</p><button class="btn sm" data-xc="all">清除筛选</button></div>`);
}
$("[data-cseg]").addEventListener("change", (e) => { cf.cat = e.detail; crows(); });
$("[data-star]").addEventListener("change", (e) => { cf.star = e.target.checked; crows(); });
$("[data-clist]").addEventListener("click", (e) => {
const a = e.target.closest("[data-ack]"); if (a) { CL.filter((c) => (!cf.cat || c.cats.includes(cf.cat)) && (!cf.star || c.imp === "★"))[+a.dataset.ack].confirmed = true; crows(); toast("已核对补遗更新,记录已保存"); return; }
const it = e.target.closest("[data-ci]"); if (it) { cur = +it.dataset.ci; crows(); $("[data-clist]").focus(); if (e.target.closest("[data-open]")) { e.preventDefault(); toast("(演示)在原文查看器里打开这一页"); } }
});
document.addEventListener("click", (e) => { const x = e.target.closest("[data-xc]"); if (!x) return; const k = x.dataset.xc; if (k !== "star") { cf.cat = ""; $$("[data-cseg] button").forEach((b) => b.setAttribute("aria-pressed", b.dataset.v === "")); } if (k !== "cat") { cf.star = false; $("[data-star]").checked = false; } crows(); });
$("[data-clist]").addEventListener("keydown", (e) => {
const n = $$("[data-ci]").length; if (!n) return;
if (e.key === "j" || e.key === "k") { e.preventDefault(); cur = Math.max(0, Math.min(n - 1, cur + (e.key === "j" ? 1 : -1))); crows(); $(`[data-ci="${cur}"]`).scrollIntoView({ block: "nearest" }); }
if (e.key === "Enter" || e.key === "o") toast(`(演示)打开出处:${$(`[data-ci="${cur}"] .cite-src`).textContent}`);
});
crows();
/* ---------- 03 问题列表 ---------- */
const FD = [
{ sev: "fatal", t: "报价超过采购包 1 的最高限价", flag: "新增", text: ["开标一览表报价 287.60 万元,采购包 1 最高限价 286.00 万元,超出 1.60 万元。", "这一项会导致投标无效。请核对开标一览表的报价。"],
cites: [["招标文件", 2, "最高限价:418.00万元(", "采购包1:286.00万元", ",采购包2:132.00万元)。"], ["报价册 · 开标一览表", 1, "投标总价(小写):", "2876000.00 元", ""]] },
{ sev: "fatal", t: "★ 条款疑似不满足:应用服务器内存", flag: "沿用 · 上次标记为已修改,问题仍在", text: ["技术偏离表第 12 行响应「内存 256G DDR4」,要求「≥512G DDR4」。", "这一项会导致投标无效。请在技术偏离表第 12 行和对应产品参数中更正内存配置。"],
cites: [["招标文件", 41, "★ 2U机架式服务器,CPU:配置≥2颗海光架构32C 2.7GHz,", "内存: ≥512G DDR4", "……"], ["技术册", 12, "第 12 行 ", "内存:256G DDR4", " 无偏离"]] },
{ sev: "major", t: "缺少 ★ 参数的证明材料:核心交换机硬件 BFD", text: ["已响应「支持硬件 BFD」,但没有找到官网参数截图等证明材料。请在技术册补充证明材料。"],
cites: [["招标文件", 44, "★ 支持硬件BFD,最小报文发送间隔≤50ms,", "提供相关证明材料(包含但不限于产品官网公开参数截图证明等)", "。"]], missing: "证明材料未找到 · 已查找:技术册(全部章节)、商务册(附件)" },
{ sev: "major", t: "资质过期预警:ISO27001 证书在投标截止日前到期", flag: "新增", text: ["证书有效期至 2026年11月15日,早于投标截止日 11月20日。按评分办法,这一项可能不得分。", "请上传换发后的证书,或在资质库更新有效期。"],
cites: [["招标文件", 31, "体系认证(6分):……", "以投标截止日在有效期内为准", ",提供证书扫描件)。"], ["商务册", 38, "ISO/IEC 27001 信息安全管理体系认证证书 ", "有效期至:2026年11月15日", ""]] },
{ sev: "hint", t: "投标文件里出现了其他项目的名称", flag: "新增", text: ["商务册第 27 页出现「某区智慧校园一期项目」及其项目编号。如果是业绩介绍可以忽略;如果是照搬旧标书留下的,请修改。"],
cites: [["商务册", 27, "我公司承担的", "某区智慧校园一期项目(项目编号 QJ-2025-0112)", "……"]] },
{ sev: "hint", t: "一般参数疑似不满足:叫号一体机显示屏尺寸", text: ["技术册第 77 页响应「21英寸」,要求「≥21.5英寸」。一般参数负偏离每项扣 0.5 分。"],
cites: [["招标文件", 47, "", "显示屏尺寸≥21.5英寸", ",分辨率≥1920×1080"], ["技术册", 77, "显示屏:", "21英寸", " LED 背光液晶屏"]] },
{ sev: "hint", t: "投标函落款日期晚于投标截止日", flag: "沿用", text: ["投标函落款「2026年11月25日」,投标截止时间是 2026年11月20日 09:30。"],
cites: [["商务册", 3, "投标人(电子签章):南京某信息技术有限公司  ", "日期:2026年11月25日", ""]], ocr: true }
].map((f, i) => ({ ...f, id: i, st: null, sel: false }));
FD[6].st = { act: "确认无误", at: "11月1日 16:05" };
let ff = "", fcur = 0, foldOpen = false;
const SEV = { fatal: "致命", major: "重要", hint: "提示" };
function citeHTML(c, ocr) { return `<div class="cite"><a class="cite-src" href="#">${c[0]} · <span class="fig">第 ${c[1]} 页</span>${ocr ? '<span class="ocr">OCR</span>' : ""}</a><q>${esc(c[2])}<mark>${esc(c[3])}</mark>${esc(c[4])}</q></div>`; }
function findingHTML(f, focus) {
const ck = f.sev === "hint" && !f.st ? `<label class="ckw" data-tip="批量处理"><input type="checkbox" class="ck" data-fsel="${f.id}" ${f.sel ? "checked" : ""} aria-label="选择这一项"></label>`
: !f.st ? `<span class="ckw" data-tip="只有「提示」级可以批量处理"><input type="checkbox" class="ck" disabled aria-label="不可批量"></span>` : "";
const sign = f.st ? `<div class="sign ${f.st.ign ? "ign" : ""}"><span>${f.st.ign ? "–" : "✓"}</span><b>王敏</b><span class="fig">${f.st.at}</span><span>${f.st.act}</span>${f.st.note ? `<span class="muted">:${esc(f.st.note)}</span>` : ""}<a class="plain undo" href="#" data-undo="${f.id}">撤销</a></div>`
: `<div class="f-actions"><button class="btn" data-act="1" data-tip="我核对过,投标文件这里没问题(例如响应写在别处)">确认无误</button><button class="btn" data-act="2" data-tip="我已经在投标文件里改了,复查时会再查">已修改</button><button class="btn" data-act="3" data-tip="我知道这个问题,决定不处理(必须填原因)">忽略并注明原因</button><a class="plain feedback" href="#">判断有误?</a></div>`;
return `<article class="finding has-ck ${f.sev === "fatal" ? "is-fatal" : ""} ${focus ? "focus" : ""}" data-fid="${f.id}">${ck}
<div><div class="f-head"><span class="sev ${f.sev}">${SEV[f.sev]}</span><span class="f-title">${f.t}</span>${f.flag ? `<span class="flag box">${f.flag}</span>` : ""}</div>
${f.text.map((t) => `<p class="f-text">${t}</p>`).join("")}${sign}</div>
<aside>${f.cites.map((c) => citeHTML(c, f.ocr)).join("")}${f.missing ? `<div class="cite missing"><span class="cite-src">投标文件 · 未找到</span><p>${f.missing}</p></div>` : ""}</aside></article>`;
}
function frows() {
const todo = FD.filter((f) => !f.st && (!ff || f.sev === ff)), done = FD.filter((f) => f.st);
["", "fatal", "major", "hint"].forEach((k) => { $(`[data-c="${k}"]`).textContent = FD.filter((f) => !f.st && (!k || f.sev === k)).length; });
fcur = Math.min(fcur, Math.max(todo.length - 1, 0));
$("[data-flist]").innerHTML = (todo.map((f, i) => findingHTML(f, i === fcur)).join("") || `<div class="empty sm"><b>这个级别的问题都已处理</b><p>已处理的问题在下方「已处理」里,可以撤销。</p></div>`)
+ `<button class="group-h" data-fold aria-expanded="${foldOpen}">${icon(foldOpen ? "chev-down" : "chev-right")}已处理(<span class="fig">${done.length}</span>)</button>`
+ (foldOpen ? done.map((f) => findingHTML(f, false)).join("") : "");
const n = FD.filter((f) => f.sel && !f.st).length;
$("[data-batch]").hidden = !n; $("[data-bn]").textContent = n;
}
const now = () => "11月3日 " + new Date().toTimeString().slice(0, 5);
function act(f, a) {
if (a === 1 && f.sev === "fatal") return dialog({ title: "确认无误:" + f.t, wide: true, ok: "确认无误",
body: `<p>对致命问题说「没问题」,需要写一句说明。说明会同时作为误报反馈提交。</p><div class="field fill"><label for="u4">说明 <small>(必填)</small></label><textarea class="input" id="u4" placeholder="例如:报价以开标一览表大写金额为准,小写是笔误,已修改"></textarea></div>`,
onOk: (s) => { const v = $("#u4", s).value.trim(); if (v.length < 5) { const fl = $("#u4", s).closest(".field"); fl.classList.add("err"); $(".msg", fl)?.remove(); fl.insertAdjacentHTML("beforeend", '<span class="msg"><span class="ico w">!</span>至少写 5 个字</span>'); return false; } done(f, "确认无误", v); } });
if (a === 3) return dialog({ title: "忽略并注明原因", wide: true, ok: "忽略这一项",
body: `<p><span class="sev ${f.sev}">${SEV[f.sev]}</span> <b>${f.t}</b></p><div class="field fill"><label for="ir">原因 <small>(必填,至少 5 个字)</small></label><div class="chips"><button type="button" class="btn" data-chip>招标文件允许</button><button type="button" class="btn" data-chip>已在其他分册响应</button><button type="button" class="btn" data-chip>与本项目无关</button></div><textarea class="input" id="ir"></textarea></div>`,
onOpen: (s) => $$("[data-chip]", s).forEach((c) => c.onclick = () => { $("#ir", s).value = c.textContent; }),
onOk: (s) => { const v = $("#ir", s).value.trim(); if (v.length < 5) { const fl = $("#ir", s).closest(".field"); fl.classList.add("err"); $(".msg", fl)?.remove(); fl.insertAdjacentHTML("beforeend", '<span class="msg"><span class="ico w">!</span>至少写 5 个字,说明为什么不处理</span>'); return false; } done(f, "已忽略", v, true); } });
done(f, a === 1 ? "确认无误" : "已修改");
}
function done(f, actName, note, ign) { f.st = { act: actName, at: now(), note, ign }; f.sel = false; frows(); $("[data-flist]").focus(); toast(`已标记「${actName}」`, { undo: () => { f.st = null; frows(); } }); }
$("[data-fseg]").addEventListener("change", (e) => { ff = e.detail; fcur = 0; frows(); });
$("[data-flist]").addEventListener("click", (e) => {
const t = e.target, fid = t.closest("[data-fid]")?.dataset.fid, f = FD[fid];
if (t.matches("[data-fsel]")) { f.sel = t.checked; frows(); return; }
if (t.closest("[data-fold]")) { foldOpen = !foldOpen; frows(); return; }
if (t.closest("[data-undo]")) { e.preventDefault(); f.st = null; frows(); toast("已撤销", { ok: false }); return; }
if (t.closest("[data-act]")) { act(f, +t.closest("[data-act]").dataset.act); return; }
if (f && !f.st) { fcur = FD.filter((x) => !x.st && (!ff || x.sev === ff)).indexOf(f); frows(); }
});
$("[data-flist]").addEventListener("keydown", (e) => {
if (e.target.matches("input, textarea")) return;
const todo = FD.filter((f) => !f.st && (!ff || f.sev === ff)); if (!todo.length) return;
if (e.key === "j" || e.key === "k") { e.preventDefault(); fcur = Math.max(0, Math.min(todo.length - 1, fcur + (e.key === "j" ? 1 : -1))); frows(); $(".finding.focus").scrollIntoView({ block: "nearest" }); }
if (["1", "2", "3"].includes(e.key)) act(todo[fcur], +e.key);
if (e.key === "?") Demo.keysDialog();
if (e.key === "o" || e.key === "Enter") toast("(演示)在原文查看器里打开当前出处");
});
$("[data-bok]").onclick = () => { const sel = FD.filter((f) => f.sel && !f.st); sel.forEach((f) => { f.st = { act: "确认无误", at: now() }; f.sel = false; }); frows(); toast(`已将 ${sel.length} 项「提示」标记为确认无误`, { undo: () => { sel.forEach((f) => f.st = null); frows(); } }); };
$("[data-bclear]").onclick = () => { FD.forEach((f) => f.sel = false); frows(); };
frows();
/* ---------- 04 流水分页 ---------- */
$("[data-fpager]").innerHTML = `<span class="total">本月 <span class="fig">8</span> 条 · 共 <span class="fig">23</span> 条</span><button class="pg" disabled aria-label="上一页">${icon("chev-left")}</button><button class="pg" aria-current="page">1</button><button class="pg">2</button><button class="pg" aria-label="下一页">${icon("chev-right")}</button>`;
</script>
</body>
</html>

View File

@@ -0,0 +1,226 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>读标 · 标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 本页布局:整屏高度,三栏各自滚动 */
main.main{height:100vh;overflow:hidden}
.cl-bar{display:flex;align-items:center;gap:var(--sp-4);padding:var(--sp-3) var(--sp-8);border-bottom:1px solid var(--border-subtle);background:var(--bg-page)}
.cl-bar h2{font:var(--fw-bold) var(--fs-h2)/var(--lh-h2) var(--font-serif);margin:0;white-space:nowrap}
.cl-bar .status-line{display:flex;gap:var(--sp-2);align-items:center;min-width:0}
.cl-bar .status-line .sign{margin:0;font-size:var(--fs-caption)}
.cl-bar .actions{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center;white-space:nowrap}
.list{overflow:auto;padding:var(--sp-4) var(--sp-5) var(--sp-12)}
.list .toolbar{position:sticky;top:calc(-1 * var(--sp-4));z-index:5;margin:calc(-1 * var(--sp-4)) calc(-1 * var(--sp-5)) var(--sp-3);padding:var(--sp-3) var(--sp-5);background:var(--bg-page);border-bottom:1px solid var(--border-subtle)}
.list .toolbar .search{flex:1;min-width:140px;width:auto}
.group{margin-top:var(--sp-5)}
.group > h3{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0 0 var(--sp-2);display:flex;gap:var(--sp-2);align-items:baseline}
.group > h3 .fig{font-weight:var(--fw-medium);color:var(--text-secondary);font-size:var(--fs-body)}
.items{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm)}
.items .item{padding:var(--sp-3) var(--sp-4)}
.pending .item{padding:var(--sp-3) 0}
.pending .f-actions{margin-top:var(--sp-2)}
.right{border-left:1px solid var(--border-subtle);display:flex;min-height:0;min-width:0}
.right .viewer{flex:1;border:0;border-radius:0;min-width:0}
.lot-note{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);margin:0 0 var(--sp-3)}
.proj-head .tabs{margin-top:var(--sp-3);border-bottom:0}
.proj-head .tabs [role=tab]{margin-bottom:0}
</style>
</head>
<body>
<main>
<div class="proj-head">
<div class="line1">
<div>
<h1>某市政务服务中心智慧大厅建设项目</h1>
<div class="meta"><span>某市政务服务管理办公室</span><span>政府采购 · 公开招标</span><span>投 采购包 1(共 2 个包)</span><span class="fig m">ZC-2026-0418</span></div>
</div>
<div class="facts">
<div><small>投标截止</small><span class="fig m">11月20日 09:30</span> · 剩 <span class="fig m">17</span> 天</div>
<div><small>状态</small><span class="tag status">已检查</span></div>
<div><small>就绪</small>未就绪</div>
</div>
</div>
<nav class="tabs" aria-label="项目"><a role="tab" href="#">概览</a><a role="tab" href="#" aria-selected="true">读标</a><a role="tab" href="#">查标 <span class="sev fatal">致命<span class="fig">3</span></span></a><a role="tab" href="#">报告</a></nav>
</div>
<div class="cl-bar">
<h2>项目检查清单</h2>
<div class="status-line" data-status></div>
<div class="actions">
<a class="plain cap" href="#" data-missing>反馈:有遗漏的要求</a>
<button class="btn" data-add><i data-ic="plus"></i>新增条目</button>
<button class="btn primary" data-confirm>确认清单</button>
</div>
</div>
<div class="split">
<nav class="cats" aria-label="清单分类" data-cats></nav>
<div class="list" data-list tabindex="-1"></div>
<div class="right"><div class="viewer" data-viewer></div></div>
</div>
</main>
<script src="../../prototype/assets/data.js"></script>
<script src="demo.js"></script>
<script>
const { $, $$, esc, icon, toast, dialog } = Demo;
const D = window.PROTO;
Demo.shell({ nav: "workbench", crumbs: [["工作台", "page-workbench.html"], [D.project.name, "#"], ["读标", ""]] });
const items = D.checklist.map((c) => ({ ...c }));
let pending = D.pending.map((q) => ({ ...q }));
let cat = "all", sel = items[0].id, confirmed = false, star = false, kw = "";
/* ---------- 原文查看器 ---------- */
const V = (() => {
const el = $("[data-viewer]");
el.innerHTML = `<div class="viewer-bar">
<span class="doc-name"><b data-vn>原文</b><span class="doc-file" data-vf></span></span>
<div class="pager"><span class="btn-group"><button class="btn sm" data-vp>上一处</button><button class="btn sm" data-vx>下一处</button></span><span class="fig m" data-vpg></span></div></div>
<div class="viewer-body" data-vb></div>`;
let cur = null;
function mark(t, key) { const i = key ? t.indexOf(key) : -1; return i < 0 ? `<span class="hl on">${esc(t)}</span>` : esc(t.slice(0, i)) + `<span class="hl on">${esc(key)}</span>` + esc(t.slice(i + key.length)); }
function open(c) {
cur = c;
const [doc, page, id, key] = c, d = D.docs[doc];
$("[data-vn]", el).textContent = d.name;
$("[data-vf]", el).textContent = d.file;
$("[data-vpg]", el).innerHTML = `第 ${page} / ${d.pages} 页`;
const paras = (D.pages[doc] || {})[page] || [];
$("[data-vb]", el).innerHTML = `<div class="sheet">${paras.map((p) => p.h ? `<p class="h">${esc(p.t)}</p>` : `<p class="${p.faint ? "faint" : ""}">${p.id === id ? mark(p.t, key) : esc(p.t)}</p>`).join("")}<div class="stub"></div><div class="stub"></div><div class="stub s"></div><span class="pno">— ${page} —</span></div>
<p class="viewer-note">页码以规范化 PDF 为准 · 原文只读</p>`;
const on = $(".hl.on", el); if (on) on.scrollIntoView({ block: "center" });
}
const step = (d) => { const v = visible(); const i = v.findIndex((x) => x.id === sel); const n = v[i + d]; if (n) select(n.id, true); };
$("[data-vp]", el).onclick = () => step(-1);
$("[data-vx]", el).onclick = () => step(1);
return { open };
})();
/* ---------- 状态、分类、列表 ---------- */
function status() {
$("[data-status]").innerHTML = confirmed
? `<span class="tag status">已确认 v3</span><span class="sign"><span>✓</span><b>王敏</b><span class="fig">11月3日 ${new Date().toTimeString().slice(0, 5)}</span>确认</span>`
: `<span class="tag">草稿 v3</span><span class="cap muted">补遗 1 并入后待重新确认 · v2 已确认</span>`;
$("[data-confirm]").hidden = confirmed;
}
function cats() {
const n = (id) => items.filter((i) => i.cats.includes(id)).length;
$("[data-cats]").innerHTML = `<button data-cat="all" aria-current="${cat === "all"}">全部<span class="fig">${items.length}</span></button>` +
D.categories.map((c) => `<button data-cat="${c.id}" aria-current="${cat === c.id}">${c.name}<span class="fig">${n(c.id)}</span></button>`).join("") +
`<hr><button class="ai" data-cat="pending" aria-current="${cat === "pending"}">待确认<span class="fig">${pending.length}</span></button>
<hr><p class="hint">一条要求可以属于多个分类,例如 ★ 参数同时在「技术参数」和「无效条款」里。</p>
<hr><p class="hint"><span class="kbd">J</span> <span class="kbd">K</span> 上下移动<br><span class="kbd">Enter</span> 打开原文</p>`;
}
const visible = () => {
const seen = new Set();
const groups = cat === "all" ? D.categories : D.categories.filter((c) => c.id === cat);
const out = [];
groups.forEach((g) => items.filter((i) => i.cats.includes(g.id) && (cat !== "all" || !seen.has(i.id)) && (!star || i.imp === "★") && (!kw || i.title.includes(kw))).forEach((i) => { seen.add(i.id); out.push({ ...i, g: g.id }); }));
return out;
};
function itemRow(i) {
const src = i.src === "addendum" ? i.srcName : i.src === "ai" ? '<span class="src-ai">AI 提取</span>' : "人工添加";
const doc = D.docs[i.cite[0]];
const upd = i.updated ? `<div class="upd"><span class="flag">已被${i.srcName} 更新 · 原为 <s>${esc(i.old)}</s></span><button class="btn" data-ack="${i.id}">已核对</button></div>` : "";
const meta = i.cats.map((c) => `<span class="tag">${D.categories.find((x) => x.id === c).name}</span>`).join("") + `<span class="tag">${i.lot ? "采购包 " + i.lot : "通用"}</span>` + (i.edited ? '<span class="flag">已修改 · <a class="plain" href="#">查看记录</a></span>' : "");
return `<div class="item ${sel === i.id ? "sel" : ""}" data-item="${i.id}" tabindex="-1"><span class="imp">${i.imp || ""}</span>
<div><div class="t">${esc(i.title)}</div><div class="meta">${meta}</div>${upd}</div>
<div class="side">${src}<br><a class="cite-src" href="#">${doc.role === "addendum" ? doc.name + " · " : ""}第 <span class="fig">${i.cite[1]}</span> 页</a></div></div>`;
}
function pendingBox() {
if (!pending.length) return "";
return `<div class="pending" style="margin-bottom:var(--sp-5)"><h4>待确认(<span class="fig">${pending.length}</span>)</h4>
<p>这些结果没有找到可靠出处,或系统把握不大。确认之前,不参与检查。</p>
${pending.map((q) => `<div class="item" data-q="${q.id}"><span class="imp">${q.imp || ""}</span>
<div><div class="t">${esc(q.title)}</div><div class="meta"><span class="src-ai cap">AI 提取 · ${q.reason}</span>${q.cite ? `<a class="cite-src" href="#" data-qopen="${q.id}" style="display:inline">第 <span class="fig">${q.cite[1]}</span> 页</a>` : ""}</div>
<div class="f-actions">${q.cite ? '<button class="btn sm" data-qa="adopt">采纳</button>' : '<button class="btn sm" data-qa="cite">补上出处后采纳</button>'}<button class="btn sm" data-qa="edit">修改后采纳</button><button class="btn sm text" data-qa="drop">丢弃</button></div></div><span></span></div>`).join("")}</div>`;
}
function list() {
const L = $("[data-list]");
const tb = `<div class="toolbar"><label class="search"><i data-ic="search"></i><input class="input" placeholder="在清单里搜索" aria-label="搜索清单" data-kw value="${esc(kw)}"></label><label class="choice cap"><input type="checkbox" class="ck" data-star ${star ? "checked" : ""}>只看 ★</label></div>`;
if (cat === "pending") { L.innerHTML = pendingBox() || '<div class="empty sm"><b>没有待确认的条目</b><p>所有提取结果都有可靠出处。</p></div>'; return; }
const v = visible();
const head = `<p class="lot-note">显示:通用 + 采购包 1 · 采购包 2 的 <span class="fig m">${D.hiddenLotItems}</span> 条已隐藏 · <a class="plain" href="#">更改所投的包</a></p>
${cat === "all" && !kw && !star ? `<div class="alert"><span class="ico i">i</span><div><p>实质性条款的标注规则:<b>打「★」的内容为实质性要求</b>(招标文件 <a class="cite-src inline" href="#" data-rule>第 <span class="fig">24</span> 页</a>)。已识别采购包 1 和通用部分的 ★ 条款 <span class="fig m">4</span> 条,请对照原文核对有无遗漏。</p></div></div><div style="height:var(--sp-4)"></div>${pendingBox()}` : ""}`;
const groups = cat === "all" ? D.categories : D.categories.filter((c) => c.id === cat);
const body = groups.map((g) => { const rows = v.filter((i) => i.g === g.id); return rows.length ? `<section class="group"><h3>${g.name}<span class="fig">${rows.length}</span></h3><div class="items">${rows.map(itemRow).join("")}</div></section>` : ""; }).join("");
const focusKw = document.activeElement?.matches?.("[data-kw]");
L.innerHTML = tb + head + (body || `<div class="empty sm"><b>没有找到「${esc(kw)}」</b><p>搜索只查当前分类里的条目标题。</p><button class="btn sm" data-clear>清除搜索</button></div>`);
Demo.hydrate(L);
if (focusKw) { const i = $("[data-kw]", L); i.focus(); i.setSelectionRange(i.value.length, i.value.length); }
}
function select(id, scroll) {
sel = id; const i = items.find((x) => x.id === id);
$$(".item.sel").forEach((x) => x.classList.remove("sel"));
const el = $(`[data-item="${id}"]`); if (el) { el.classList.add("sel"); if (scroll) el.scrollIntoView({ block: "nearest" }); }
V.open(i.cite);
}
function render() { status(); cats(); list(); }
/* ---------- 交互 ---------- */
document.addEventListener("click", (e) => {
const t = e.target;
if (t.closest("[data-cat]")) { cat = t.closest("[data-cat]").dataset.cat; render(); return; }
if (t.closest("[data-ack]")) { const i = items.find((x) => x.id === t.closest("[data-ack]").dataset.ack); i.updated = false; i.edited = true; list(); toast("已核对补遗更新,记录已保存"); return; }
if (t.closest("[data-rule]")) { e.preventDefault(); V.open(["T", 24, "t24-mark"]); return; }
if (t.closest("[data-qopen]")) { e.preventDefault(); V.open(pending.find((x) => x.id === t.closest("[data-qopen]").dataset.qopen).cite); return; }
if (t.closest("[data-qa]")) { pendingAct(t.closest("[data-q]").dataset.q, t.closest("[data-qa]").dataset.qa); return; }
if (t.closest("[data-clear]")) { kw = ""; list(); return; }
const it = t.closest("[data-item]");
if (it && !t.closest("a.plain")) { e.preventDefault(); select(it.dataset.item); $("[data-list]").focus({ preventScroll: true }); }
});
document.addEventListener("input", (e) => { if (e.target.matches("[data-kw]")) { kw = e.target.value.trim(); list(); } });
document.addEventListener("change", (e) => { if (e.target.matches("[data-star]")) { star = e.target.checked; list(); } });
document.addEventListener("keydown", (e) => {
if (e.target.matches("input, textarea") || $(".scrim")) return;
if (e.key === "?") { Demo.keysDialog(); return; }
if (cat === "pending") return;
const v = visible(), i = v.findIndex((x) => x.id === sel);
if (e.key === "j" || e.key === "k") { e.preventDefault(); const n = v[Math.max(0, Math.min(v.length - 1, (i < 0 ? -1 : i) + (e.key === "j" ? 1 : -1)))]; if (n) select(n.id, true); }
if ((e.key === "Enter" || e.key === "o") && i >= 0) select(sel, true);
});
function pendingAct(id, act) {
const q = pending.find((x) => x.id === id);
if (act === "drop") { pending = pending.filter((x) => x !== q); render(); toast(`已丢弃「${esc(q.title)}」`, { undo: () => { pending.push(q); render(); } }); return; }
if (act === "adopt") { adopt(q, q.cite); return; }
dialog({
title: act === "cite" ? "补上出处后采纳" : "修改后采纳", wide: true, ok: "采纳",
body: `<div class="field fill"><label for="qt">要求 <small>(必填)</small></label><input class="input" id="qt" value="${esc(q.title)}"></div>
<div class="form" style="margin-top:var(--sp-4)"><div class="field"><label for="qp">出处页码 <small>(招标文件)</small></label><div class="input-unit"><input class="input" id="qp" placeholder="例如 26"><span class="unit">/ 186 页</span></div></div>
<div class="field"><label>&nbsp;</label><label class="choice"><input type="checkbox" class="ck" id="qn">找不到出处,按「人工确认、无出处」采纳</label></div></div>`,
onOk: (s) => { const pg = $("#qp", s).value.trim(); if (!pg && !$("#qn", s).checked) { const f = $("#qp", s).closest(".field"); f.classList.add("err"); $(".msg", f)?.remove(); f.insertAdjacentHTML("beforeend", '<span class="msg"><span class="ico w">!</span>填写页码,或勾选「人工确认、无出处」</span>'); return false; } q.title = $("#qt", s).value.trim() || q.title; adopt(q, pg ? ["T", +pg, ""] : null, !pg); }
});
}
function adopt(q, c, noCite) {
pending = pending.filter((x) => x !== q);
items.push({ id: "n" + q.id, cats: ["invalid"], imp: q.imp, title: q.title, lot: 0, src: "manual", edited: true, cite: c || ["T", 1, ""] });
render(); toast(noCite ? "已采纳,标注为「人工确认、无出处」" : "已采纳,进入正式清单");
}
$("[data-confirm]").onclick = () => dialog({
title: "确认清单 v3?", ok: "确认清单",
body: `<p>确认后,查标会以这一版清单为依据。之后再修改清单,会回到草稿并保留历史,已有的检查结果不受影响。</p>${pending.length ? `<div class="alert warn"><span class="ico w">!</span><div><p><b>还有 ${pending.length} 条待确认。</b>它们不会进入这一版清单,也不参与检查。</p></div></div>` : ""}`,
onOk: () => { confirmed = true; status(); toast("清单 v3 已确认"); }
});
$("[data-add]").onclick = () => dialog({
title: "新增条目", wide: true, ok: "添加",
body: `<div class="field fill" id="na-f"><label for="na">要求 <small>(必填)</small></label><textarea class="input" id="na" placeholder="写下招标文件里的要求"></textarea></div>
<div class="form" style="margin-top:var(--sp-4)"><div class="field"><label>分类</label><div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val">无效条款</span>${icon("chev-down")}</button><ul class="menu" role="listbox">${D.categories.map((c) => `<li role="option" ${c.id === "invalid" ? 'aria-selected="true"' : ""}>${c.name}</li>`).join("")}</ul></div></div>
<div class="field"><label for="np">出处页码 <small>(选填)</small></label><div class="input-unit"><input class="input" id="np"><span class="unit">/ 186 页</span></div></div></div>
<p class="cap muted">人工添加的条目可以没有出处,会标注「人工添加」。</p>`,
onOk: (s) => { const v = $("#na", s).value.trim(); const f = $("#na-f", s); if (!v) { f.classList.add("err"); $(".msg", f)?.remove(); f.insertAdjacentHTML("beforeend", '<span class="msg"><span class="ico w">!</span>请写下这条要求</span>'); return false; } items.push({ id: "u" + items.length, cats: ["invalid"], title: v, lot: 0, src: "manual", cite: ["T", +$("#np", s).value || 1, ""] }); render(); toast("已添加到「无效条款」"); }
});
$("[data-missing]").onclick = (e) => { e.preventDefault(); dialog({ title: "反馈:有遗漏的要求", ok: "提交反馈", body: `<div class="field fill"><label for="mi">漏了什么?在第几页?</label><textarea class="input" id="mi" placeholder="例如:第 26 页要求提供原厂授权书,清单里没有"></textarea></div><p class="cap muted" style="margin-top:var(--sp-3)">反馈用于改进提取规则。要把这条加进清单,请用「新增条目」。</p>`, onOk: () => toast("已收到反馈") }); };
render();
select(items.find((i) => i.id === "c19").id, true);
</script>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 本页布局:主列 + 右侧 296px 信息栏 */
.wb{display:grid;grid-template-columns:minmax(0,1fr) 296px;gap:var(--sp-6);align-items:start;margin-top:var(--sp-6)}
.wb .rail > * + *{margin-top:var(--sp-4)}
.wb .rail .card{padding:var(--sp-4) var(--sp-5)}
.wb .rail .card-foot{margin:var(--sp-3) calc(-1 * var(--sp-5)) calc(-1 * var(--sp-4));padding:var(--sp-2) var(--sp-5)}
.log{list-style:none;margin:0;padding:0}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-2);padding:6px 0;border-top:1px solid var(--border-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.log li:first-child{border-top:0}
.log li > span:last-child{color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums;white-space:nowrap}
.log .who{font-weight:var(--fw-bold)}
.log .ok{color:var(--confirmed);white-space:nowrap}
.wb table.data td{vertical-align:middle}
.todo-row{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:var(--sp-3);align-items:baseline;padding:var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5);border-top:1px solid var(--border-subtle)}
.todo-row:first-child{border-top:0}
.todo-row::before{content:"";position:absolute;left:0;top:var(--sp-3);bottom:var(--sp-3);width:3px;background:var(--sev-fatal)}
.todo-row b{display:block;font-weight:var(--fw-bold)}
.todo-row .cite-src{white-space:nowrap}
.wb .proj-sub .fig{font-weight:var(--fw-medium)}
</style>
</head>
<body>
<main>
<div class="content">
<div class="page-title">
<div><h1>工作台</h1><p>11月3日 周二 · 按投标截止时间排序,截止临近的排在前面。</p></div>
<div class="actions"><span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次</span><button class="btn primary" data-new><i data-ic="plus"></i>新建项目</button></div>
</div>
<div class="alert warn"><span class="ico w">!</span><div><p><b>某县数据中心机房改造项目今天 17:00 截止。</b>清单还是草稿,还没有做过检查。</p></div><span class="act"><a class="btn sm" href="page-reading.html">去确认清单</a></span></div>
<div class="stats" style="margin-top:var(--sp-5)">
<div class="stat"><small>进行中的项目</small><span class="big">4</span><p>已递交 1 · 已归档 1</p></div>
<div class="stat"><small>7 天内截止</small><span class="big">2</span><p class="due-today">最近:今天 17:00</p></div>
<div class="stat"><small>待处理的致命问题</small><span class="big">3</span><p>在 1 个项目里</p></div>
<div class="stat"><small>待确认的清单条目</small><span class="big">4</span><p>在 2 个项目里</p></div>
</div>
<div class="wb">
<div>
<div class="toolbar">
<div class="seg" role="group" aria-label="按状态筛选" data-seg><button aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button><button aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button><button aria-pressed="false" data-v="已递交">已递交</button><button aria-pressed="false" data-v="已归档">已归档</button></div>
<div class="right"><label class="search" style="width:240px"><i data-ic="search"></i><input class="input" placeholder="搜索项目、采购人、编号" aria-label="搜索项目" data-q></label></div>
</div>
<table class="data hover">
<thead><tr><th>项目</th><th>投标截止</th><th>状态</th><th>就绪</th><th class="r">致命</th></tr></thead>
<tbody data-rows></tbody>
</table>
<p class="note">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
<div class="section-h"><h2>待处理的致命问题</h2><span class="sev fatal">致命<span class="fig">3</span></span><span class="more"><a class="plain cap" href="#">去查标页处理</a></span></div>
<div class="results">
<div class="todo-row"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="cap muted">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite-src" href="#">报价册 · 第 <span class="fig">1</span> 页</a></div>
<div class="todo-row"><span class="sev fatal">致命</span><div><b>★ 条款疑似不满足:应用服务器内存</b><span class="cap muted">智慧大厅建设项目 · 响应 256G,要求 ≥ 512G · 上次标记为已修改,问题仍在</span></div><a class="cite-src" href="#">技术册 · 第 <span class="fig">12</span> 页</a></div>
<div class="todo-row"><span class="sev fatal">致命</span><div><b>授权委托书的单位签章位置未发现盖章</b><span class="cap muted">智慧大厅建设项目 · 只看有无,不鉴定真伪</span></div><a class="cite-src" href="#">商务册 · 第 <span class="fig">5</span> 页</a></div>
</div>
</div>
<div class="rail">
<div class="card">
<div class="card-h"><h3>正在进行的任务</h3></div>
<div class="d-row" style="gap:var(--sp-2);margin-bottom:var(--sp-1)"><b>解析补遗 2</b><span class="cap muted" style="margin-left:auto">提取清单</span></div>
<div class="cap muted" style="margin-bottom:var(--sp-2)">某市医保信息系统等保测评服务</div>
<div class="progress"><div class="bar"><i style="width:62%"></i></div><span class="fig">62%</span></div>
<div style="display:flex;gap:var(--sp-2);align-items:flex-start;margin-top:var(--sp-3)"><i data-ic="clock" class="muted" style="margin-top:2px"></i><span class="cap muted">检查排队中 · 智慧社区平台运维 · 前面还有 1 个任务</span></div>
<p class="note">可以离开本页,完成后会发站内通知。</p>
</div>
<div class="card quota">
<div class="card-h"><h3>本期额度</h3><span class="more cap muted">至 2027-10-31</span></div>
<div class="q2">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="bar"><i style="width:26%"></i></div></div>
<div><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20</span><div class="bar"><i style="width:40%"></i></div></div>
</div>
<div class="card-foot"><a class="plain cap" href="#">扣点规则</a><span class="r"><a class="plain cap" href="#">查看流水</a></span></div>
</div>
<div class="card">
<div class="card-h"><h3>最近处理</h3><span class="more"><a class="plain cap" href="#">全部记录</a></span></div>
<ul class="log">
<li><span><span class="who">王敏</span> <span class="ok">✓ 已修改</span>「授权委托书签章」</span><span>14:26</span></li>
<li><span><span class="who">王敏</span> 确认了清单 v2 · 智慧大厅</span><span>11-02</span></li>
<li><span><span class="who">系统</span> 完成第 2 次检查 · 致命 3</span><span>11-02</span></li>
<li><span><span class="who">系统</span> 并入补遗 1 · 更新 2 条</span><span>11-02</span></li>
<li><span><span class="who">李工</span> 忽略「其他项目名称」:业绩介绍</span><span>11-01</span></li>
</ul>
</div>
</div>
</div>
</div>
</main>
<script src="demo.js"></script>
<script>
const { $, $$, icon, toast } = Demo;
Demo.shell({ nav: "workbench", crumbs: [["工作台", ""]] });
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: "今天 17:00 截止", cls: "due-today", st: "编制中", ready: "", fatal: 0, href: "page-reading.html" },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", h: 53, due: "剩 2 天 5 小时", cls: "due-soon", st: "已检查", ready: "就绪", fatal: 0 },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, due: "11月20日 09:30 · 剩 17 天", cls: "", st: "已检查", ready: "未就绪", fatal: 3, href: "page-reading.html" },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", h: 600, due: "11月28日 10:00 · 剩 25 天", cls: "", st: "跟进中", ready: "", fatal: 0 },
{ name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, due: "已截止", cls: "due-past", st: "已递交", ready: "", fatal: 0 },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: "已截止", cls: "due-past", st: "已归档", ready: "", fatal: 0 }
];
let seg = "active", q = "";
function render() {
const rows = P.filter((p) => (seg === "all" || (seg === "active" ? !["已递交", "已归档"].includes(p.st) : p.st === seg)) && (!q || (p.name + p.buyer).includes(q)));
$("[data-rows]").innerHTML = rows.map((p) => `<tr data-href="${p.href || ""}">
<td><div class="proj-name">${p.name}</div><div class="proj-sub">${p.buyer} · ${p.lot}</div></td>
<td class="nw ${p.cls}">${p.due}</td><td><span class="tag status">${p.st}</span></td>
<td class="nw">${p.ready === "就绪" ? '<span class="tag status" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>' : p.ready || '<span class="muted">—</span>'}</td>
<td class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="muted">—</span>'}</td></tr>`).join("")
|| `<tr><td colspan="5" class="empty-cell"><div class="empty sm"><b>没有符合条件的项目</b><p>换个关键词,或切到「全部」看看。</p></div></td></tr>`;
}
$("[data-seg]").addEventListener("change", (e) => { seg = e.detail; render(); });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-rows]").addEventListener("click", (e) => { const r = e.target.closest("tr[data-href]"); if (!r) return; if (r.dataset.href) location.href = r.dataset.href; else toast("(演示)只做了「读标」组合页"); });
$("[data-new]").onclick = () => location.href = "forms.html#s-new";
render();
</script>
</body>
</html>