docs(design): 按 v0.2 重刷全部 UI demo,确认 Q1–Q13 与 U 键规则

- ui-demo:控件、表单、卡片、列表、工作台、读标六页按 v0.2 重刷;侧栏与顶栏框架、项目行、署名行、可键入日期输入、对照布局等收回 demo.css / demo.js 共用
- system:tokens 补悬停 / 按下 / 遮罩色、数字字阶与原文排版变量;components.css 按 v0.2 改写并清除裸色值
- design-system.md:写入 Q1–Q13 答案,新增开关、分页、下拉、日期选择等组件规则,类别标签、待确认区、选中态、对照布局按 v0.2 修订
- Q13:额度流水记录 0 点的「免扣」行,FR-M6-03 与领域模型同步;U 键改为撤销当前条
- decisions.md 记录 Q1–Q12、Q13 与 U 键;AGENTS.md 同步进度

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 16:07:15 +08:00
parent 067cad2120
commit 7f9592ad16
16 changed files with 2625 additions and 1252 deletions

View File

@@ -1,4 +1,9 @@
<!doctype html>
<!--
陈列页 · ③ 卡片(v0.2)
卡片规格以定稿样张 explore/workbench-d5.html 为准:一律用分区卡片 .card.flush + .card-h / .card-b / .card-f。
本页 <style> 只放陈列用的布局和本页特有的小件(任务失败说明、项目信息卡)。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -9,223 +14,295 @@
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 陈列:统计格两种用法上下排 */
.d-sec .stats + .d-label{margin-top:var(--sp-6)}
/* 卡片头右侧的辅助信息 */
.card-h .aside{margin-left:auto;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap}
.card-f .r{margin-left:auto;display:flex;gap:var(--sp-4);align-items:center}
.d-grid.eq > .card{display:flex;flex-direction:column}
.d-grid.eq > .card > .card-f{margin-top:auto}
/* 额度卡:单项额度(80% / 用完) */
.quota .one .big.warn{font-weight:var(--fw-bold)}
.quota .one .bar{margin:var(--sp-2) 0}
.quota p.warn{display:flex;gap:var(--sp-1);align-items:flex-start}
.quota p.warn .ic{flex:none;margin-top:2px;width:14px;height:14px}
.chips{display:flex;flex-wrap:wrap;gap:var(--sp-8);align-items:flex-start}
.chips > div{display:flex;flex-direction:column;gap:var(--sp-2);align-items:flex-start}
.chips .quota-chip{background:var(--bg-surface);border:var(--line) solid var(--border-subtle)}
.chips .tip-hold{padding-bottom:64px}
/* 任务进度卡 */
.file{display:flex;gap:var(--sp-2);align-items:center;margin:0 0 var(--sp-2);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.file .ic{width:14px;height:14px}
.fail-why{display:flex;gap:var(--sp-2);margin-top:var(--sp-3);padding-top:var(--sp-3);border-top:var(--line) solid var(--border-subtle)}
.fail-why > .ic{flex:none;margin-top:3px;color:var(--warn)}
.fail-why b{color:var(--warn-text);font-weight:var(--fw-bold)}
.fail-why p{margin:0}
.fail-why .sub{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.queued{display:flex;gap:var(--sp-2);align-items:center}
.queued .ic{color:var(--text-secondary)}
.queued b{font-weight:var(--fw-bold)}
.queued .aside{margin-left:auto}
/* AI 摘要卡:署名行(共用 .sign.sm)放在 .ai-block 里,和正文左边对齐 */
.ai-card .kv{grid-template-columns:88px minmax(0,1fr)}
.ai-card .kv dd > .fig{font-weight:var(--fw-medium)}
/* 关键日期卡:日期按「11月20日 09:30」写,列放宽 */
.dates li{grid-template-columns:128px minmax(0,1fr) auto}
.dates li > span:nth-child(2) > b{font-weight:var(--fw-bold)}
.dates .left .fig{font-weight:var(--fw-medium)}
/* 项目信息卡(只在项目概览页用) */
.pinfo .kv{grid-template-columns:88px minmax(0,1fr)}
.pinfo .kv dd > .fig{font-weight:var(--fw-medium)}
.pinfo .state{display:flex;gap:var(--sp-2);align-items:center}
.pinfo .flowline{margin-top:var(--sp-4);padding-top:var(--sp-4);border-top:var(--line) solid var(--border-subtle)}
.pinfo .skel + .skel{margin-top:var(--sp-3)}
</style>
</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>
<p>只有一种容器:白卡片,1px 发丝线,圆角 12,无阴影,卡片里不再套面板。工作台只用列表,不做项目卡片视图(Q11)。</p>
<nav class="d-toc" aria-label="本页目录"><a href="#s-stat">01 统计卡</a><a href="#s-quota">02 额度卡</a><a href="#s-task">03 任务进度卡</a><a href="#s-ai">04 AI 摘要卡</a><a href="#s-date">05 关键日期卡</a><a href="#s-info">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 统计格 -->
<!-- 01 统计卡 -->
<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>
<div class="d-sec-h"><span class="no">01</span><h2>统计卡</h2><span class="ref">规范 §4.2 · §9.5</span></div>
<span class="d-label">可点筛选(工作台):按下后标题加粗、底边出墨色线</span>
<section class="stats" aria-label="项目统计,点击可筛选" data-stats>
<button class="stat" aria-pressed="true"><small>进行中的项目</small><span class="big">4</span><i data-ic="filter"></i></button>
<button class="stat" aria-pressed="false"><small>7 天内截止</small><span class="big due">2</span><i data-ic="filter"></i></button>
<button class="stat" aria-pressed="false"><small>待处理的致命问题</small><span class="big fatal">3</span><i data-ic="filter"></i></button>
<button class="stat" aria-pressed="false"><small>待确认的清单条目</small><span class="big">4</span><i data-ic="filter"></i></button>
</section>
<span class="d-label">只读(额度页):数字后可带单位</span>
<section class="stats c3" aria-label="本期用量">
<div class="stat"><small>本期解析</small><span><span class="big">13</span><span class="unit">/ 50 次</span></span></div>
<div class="stat"><small>本期检查</small><span><span class="big">8</span><span class="unit">/ 20 次</span></span></div>
<div class="stat"><small>本期退回</small><span><span class="big">2</span><span class="unit">次</span></span></div>
</section>
<ul class="d-rules"><li>只写标题和数字,不加说明小字(§9.5)。数字 Barlow 28(<code>--fs-figure</code>),单位辅助灰。</li><li>致命数用致命红、7 天内截止用赭色,其余墨色。</li></ul>
</section>
<!-- 03 额度卡 -->
<!-- 02 额度卡 -->
<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 class="d-sec-h"><span class="no">02</span><h2>额度卡</h2><span class="ref">规范 §7.10 · §8.3 · 红线 9</span></div>
<div class="d-grid c3 eq">
<section class="card flush quota" aria-labelledby="q1">
<div class="card-h"><h3 id="q1">本期额度</h3><span class="aside">至 <span class="fig xs m">2027-10-31</span></span></div>
<div class="card-b">
<div class="q2">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="bar" aria-hidden="true"><i style="width:26%"></i></div></div>
<div><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20</span><div class="bar" aria-hidden="true"><i style="width:40%"></i></div></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 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 class="card-f cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card flush quota" aria-labelledby="q2">
<div class="card-h"><h3 id="q2">检查额度</h3><span class="aside">已用 80%</span></div>
<div class="card-b one">
<small>检查剩余</small><span class="big warn">4</span><span class="unit">/ 20</span>
<div class="bar warn" aria-hidden="true"><i style="width:80%"></i></div>
<p class="warn"><i data-ic="alert-circle"></i>同一项目 24 小时内复查不扣额度。</p>
</div>
<div class="card-f cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card flush quota" aria-labelledby="q3">
<div class="card-h"><h3 id="q3">检查额度</h3><span class="aside">已用完</span></div>
<div class="card-b one">
<small>检查剩余</small><span class="big warn">0</span><span class="unit">/ 20</span>
<div class="bar warn" aria-hidden="true"><i style="width:100%"></i></div>
<p class="warn"><i data-ic="alert-circle"></i>本期检查额度已用完,可在额度页提交续费申请,或等下期发放。</p>
</div>
<div class="card-f cap"><button class="btn sm" data-renew>提交续费申请</button><span class="r"><a class="lnk" href="#" data-demo>查看流水</a></span></div>
</section>
</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>
<span class="d-label" style="margin-top:var(--sp-6)">顶栏额度(白底只为陈列;真实顶栏见 <a class="lnk" href="page-workbench.html?quota=warn">工作台 · 80%</a> / <a class="lnk" href="page-workbench.html?quota=out">用完</a>)</span>
<div class="chips">
<div><span class="d-cap">正常</span><a class="quota-chip" href="#" data-demo><span class="q">解析<span class="fig">37</span></span><span class="sep"></span><span class="q">检查<span class="fig">12</span></span></a></div>
<div><span class="d-cap">检查用到 80%</span><a class="quota-chip" href="#" data-demo><span class="q">解析<span class="fig">37</span></span><span class="sep"></span><span class="q warn">检查<span class="fig">4</span></span></a></div>
<div class="tip-hold"><span class="d-cap">检查用完(悬停说明续费)</span><a class="quota-chip tip-below tip-start tip-on" href="#" data-demo data-tip="本期剩余额度:解析 37 / 50 次,检查 0 / 20 次。本期检查额度已用完,可在额度页提交续费申请,或等下期发放" aria-label="本期剩余额度:解析 37 / 50 次,检查 0 / 20 次。本期检查额度已用完,可在额度页提交续费申请,或等下期发放"><span class="q">解析<span class="fig">37</span></span><span class="sep"></span><span class="q warn">检查<span class="fig">0</span></span></a></div>
</div>
<ul class="d-rules"><li>已用部分墨色;用到 80% 起额度条、剩余数和顶栏数字变赭色(加粗),并出现一行提醒;用完时说明续费方式:在额度页提交续费申请(专业版:在线申请 → 对公转账 → 运营开通,决策 G8)。全程不用红(Q9)。</li></ul>
</section>
<!-- 04 任务进度卡 -->
<!-- 03 任务进度卡 -->
<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-sec-h"><span class="no">03</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>
<section class="card flush" aria-labelledby="t1">
<div class="card-h"><h3 id="t1">正在解析招标文件</h3><span class="aside">已用时 1 分 40 秒</span></div>
<div class="card-b">
<p class="file"><i data-ic="file"></i>ZC-2026-0418 采购文件.pdf · <span class="fig xs">186</span> 页</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></li>
<li class="todo"><span class="dot"></span><span>核对出处</span></li>
</ol>
<div class="progress" style="margin-top:var(--sp-2)"><div class="bar" role="progressbar" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-label="解析进度"><i style="width:62%"></i></div><span class="fig">62%</span></div>
<p class="note">可以离开本页,完成后会发站内通知。</p>
</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 class="card-f"><button class="btn sm" data-cancel>取消解析</button><span class="cost">取消后退回 1 次解析</span></div>
</section>
<section class="card flush" aria-labelledby="t2">
<div class="card-h"><h3 id="t2">解析失败</h3><span class="aside">11月2日 <span class="fig xs">14:31</span></span></div>
<div class="card-b">
<p class="file"><i data-ic="file"></i>关于 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="fail-why" role="note"><i data-ic="alert-circle"></i><p><b>文件已加密,无法读取文字。</b>已退回 1 次解析额度。<span class="sub">请上传未加密的版本。</span></p></div>
</div>
<div class="card-f"><button class="btn sm"><i data-ic="upload"></i>重新上传</button><span class="r"><span class="errno">E-20261102-3M8A</span></span></div>
</section>
<div class="stack">
<section class="card flush" aria-labelledby="t3">
<div class="card-h"><h3 id="t3">解析完成</h3><span class="aside">用时 3 分 12 秒</span></div>
<div class="card-b">
<dl class="kv">
<dt>提取要求</dt><dd><span class="fig">26</span> 条 · 其中 ★ <span class="fig">4</span> 条</dd>
<dt>待确认</dt><dd><span class="fig">2</span> 条(没有可靠出处)</dd>
<dt>分包</dt><dd><span class="fig">2</span> 个采购包</dd>
</dl>
</div>
<div class="card-f"><a class="btn sm" href="page-reading.html">去读标</a><span class="r cost">已扣 1 次解析</span></div>
</section>
<section class="card flush" aria-label="检查排队中">
<div class="card-h queued"><i data-ic="clock"></i><b>检查排队中</b><span class="aside">前面 <span class="fig xs">2</span> 个任务 · 约 4 分钟</span></div>
</section>
</div>
</div>
<ul class="d-rules"><li>阶段:✓ 完成(墨色实心)、当前(加粗)、未开始(辅助灰);失败用赭色「!」并说明原因和退回额度,不用红。</li></ul>
</section>
<!-- 05 AI 摘要卡 -->
<!-- 04 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 class="d-sec-h"><span class="no">04</span><h2>AI 摘要卡</h2><span class="ref">规范 §7.16 · §8.1 · 红线 1、2</span></div>
<div class="d-grid c2 eq">
<section class="card flush ai-card" aria-labelledby="a1" data-ai-card>
<div class="card-h"><h3 id="a1">项目要点</h3><span class="tag ai" data-ai-tag>AI 摘要</span></div>
<div class="card-b">
<div class="ai-block" data-ai-block>
<dl class="kv">
<dt>法律体系</dt><dd>政府采购 · 公开招标(按「投标无效」口径检查)</dd>
<dt>预算 / 限价</dt><dd>采购包 1:<span class="fig">286.00</span> 万元(最高限价同预算)<a class="cite-src inline" href="#" data-demo>第 <span class="fig">2</span> 页</a></dd>
<dt>评标方法</dt><dd>综合评分法:价格 10、技术 40、实施方案 20、售后 10、认证 6、业绩 8<a class="cite-src inline" href="#" data-demo>第 <span class="fig">31</span> 页</a></dd>
<dt>联合体</dt><dd>不接受<a class="cite-src inline" href="#" data-demo>第 <span class="fig">2</span> 页</a></dd>
<dt>保证金</dt><dd>不收取<a class="cite-src inline" href="#" data-demo>第 <span class="fig">21</span> 页</a></dd>
<dt>样品</dt><dd class="muted">未找到要求 · 已查找:第二章、第三章、第六章</dd>
</dl>
<div data-ai-sign></div>
</div>
</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 class="card-f" data-ai-foot><button class="btn sm" data-ai-ok>确认无误</button><button class="btn sm text">修改</button><span class="r cap"><a class="lnk" href="#" data-demo>判断有误?</a></span></div>
</section>
<section class="card flush ai-card" aria-labelledby="a2">
<div class="card-h"><h3 id="a2">项目要点</h3></div>
<div class="card-b">
<div class="ai-block done">
<dl class="kv">
<dt>法律体系</dt><dd>政府采购 · 公开招标(按「投标无效」口径检查)</dd>
<dt>预算 / 限价</dt><dd>采购包 1:<span class="fig">286.00</span> 万元(最高限价同预算)<a class="cite-src inline" href="#" data-demo>第 <span class="fig">2</span> 页</a></dd>
<dt>评标方法</dt><dd>综合评分法:价格 10、技术 40、实施方案 20、售后 10、认证 6、业绩 8<a class="cite-src inline" href="#" data-demo>第 <span class="fig">31</span> 页</a></dd>
<dt>联合体</dt><dd>不接受 <span class="flag box">已修改</span><a class="cite-src inline" href="#" data-demo>第 <span class="fig">2</span> 页</a></dd>
<dt>保证金</dt><dd>不收取<a class="cite-src inline" href="#" data-demo>第 <span class="fig">21</span> 页</a></dd>
<dt>样品</dt><dd class="muted">未找到要求 · 已查找:第二章、第三章、第六章</dd>
</dl>
<div class="sign sm"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">11月2日 14:20</span><span>确认无误</span></div>
</div>
</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 class="card-f"><button class="btn sm text"><i data-ic="history"></i>处理记录</button><span class="r"><button class="btn sm text">撤销确认</button></span></div>
</section>
</div>
<ul class="d-rules"><li>确认前:「AI 摘要」蓝铅笔标签 + 左侧虚线;确认后:标签换成绿色署名,虚线变墨色实线,正文本来就是墨色。</li><li>找不到的项写「未找到 · 已查找:…」,不猜测、不留空。胜率之类的数据不足时不展示(红线 5)。</li></ul>
<ul class="d-rules"><li>正文始终墨色。确认前:标题旁「AI 摘要」标签 + 左侧 2px 蓝铅笔虚线;确认后:两者去掉,正文下方出署名行(绿 ✓ + 姓名 + 时间 + 动作),正文位置不动(Q2)。左卡可点「确认无误」试一下。</li><li>出处蓝色,可点开原文核对;找不到的写「未找到 · 已查找:…」,不猜测、不留空(红线 2、5)。</li></ul>
</section>
<!-- 06 关键日期卡 -->
<!-- 05 关键日期卡 -->
<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-sec-h"><span class="no">05</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>
<section class="card flush" aria-labelledby="d1">
<div class="card-h"><h3 id="d1">关键日期</h3><span class="aside">已按补遗 1 更新</span></div>
<div class="card-b">
<ul class="dates">
<li class="past"><span class="fig">10月28日</span><span>采购文件公告发布</span><span></span></li>
<li><span class="fig">11月9日</span><span>询问、质疑截止<span class="left">剩 <span class="fig">6</span> 天</span></span><a class="cite-src" href="#" data-demo>第 <span class="fig">6</span> 页</a></li>
<li class="key"><span class="fig">11月20日 09:30</span><span><b>投标截止 / 开标</b><span class="left">剩 <span class="fig">17</span> 天 · 补遗 1 更新,原为 11月18日 09:30</span></span><a class="cite-src" href="#" data-demo>补遗 1 · 第 <span class="fig">1</span> 页</a></li>
<li><span class="fig">2027年2月18日</span><span>投标有效期至<span class="left">截止日起 <span class="fig">90</span> 天</span></span><a class="cite-src" href="#" data-demo>第 <span class="fig">21</span> 页</a></li>
</ul>
</div>
</section>
<section class="card flush" aria-labelledby="d2">
<div class="card-h"><h3 id="d2">7 天内截止</h3><a class="lnk cap" style="margin-left:auto" href="page-workbench.html">全部项目</a></div>
<div class="card-b">
<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月5日 16:00</span><span><b>某区智慧社区平台运维服务</b><span class="left due-soon">剩 2 天 5 小时 · 已检查</span></span><span class="cap muted">全部</span></li>
</ul>
</div>
</section>
</div>
<ul class="d-rules"><li>日期 Barlow 15(<code>--fs-figure-sm</code>),跨年带年份。倒计时:&gt; 72 小时墨色;≤ 72 小时赭色小字;≤ 24 小时赭色加粗;已过辅助灰。不用红、不闪烁。</li></ul>
</section>
<!-- 06 项目信息卡 -->
<section class="d-sec" id="s-info">
<div class="d-sec-h"><span class="no">06</span><h2>项目信息卡</h2><span class="ref">规范 §7.9 · §9.1</span></div>
<div class="d-grid c3 eq">
<section class="card flush pinfo" aria-labelledby="p1">
<div class="card-h"><h3 id="p1">项目信息</h3><button class="btn sm text" style="margin-left:auto"><i data-ic="pencil"></i>编辑</button></div>
<div class="card-b">
<dl class="kv">
<dt>采购人</dt><dd>某市政务服务管理办公室</dd>
<dt>项目编号</dt><dd><span class="fig">ZC-2026-0418</span></dd>
<dt>体系</dt><dd>政府采购 · 公开招标</dd>
<dt>所投包</dt><dd>采购包 1</dd>
<dt>投标截止</dt><dd>11月20日 <span class="fig">09:30</span> · 剩 <span class="fig">17</span> 天</dd>
<dt>状态</dt><dd class="state"><span class="tag">已检查</span>未就绪</dd>
</dl>
<div class="flowline sm" aria-label="项目状态:已检查"><span class="st done"><i></i>跟进中</span><span class="ln"></span><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></div>
</div>
</section>
<section class="card flush pinfo" aria-labelledby="p2">
<div class="card-h"><h3 id="p2">项目信息</h3><button class="btn sm text" style="margin-left:auto"><i data-ic="pencil"></i>编辑</button></div>
<div class="card-b">
<dl class="kv">
<dt>招标人</dt><dd>某县大数据管理局</dd>
<dt>项目编号</dt><dd><span class="fig">JS-2026-1107</span></dd>
<dt>体系</dt><dd>招标投标 · 公开招标</dd>
<dt>所投标段</dt><dd>标段 1</dd>
<dt>投标截止</dt><dd class="due-today">今天 17:00 截止</dd>
<dt>状态</dt><dd class="state"><span class="tag">编制中</span></dd>
</dl>
<div class="flowline sm" aria-label="项目状态:编制中"><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>
</section>
<section class="card flush pinfo" aria-busy="true" aria-labelledby="p3">
<div class="card-h"><h3 id="p3">项目信息</h3></div>
<div class="card-b">
<span class="skel w60"></span><span class="skel w80"></span><span class="skel w60"></span><span class="skel w30"></span><span class="skel w80"></span>
<p class="d-cap" style="margin:var(--sp-4) 0 0">加载中:骨架屏静止,不做明暗起伏(Q8)</p>
</div>
</section>
</div>
<ul class="d-rules"><li>只在项目概览页右栏用;工作台不做项目卡片,只用列表(见 <a class="lnk" href="page-workbench.html">⑤ 工作台</a>)。</li><li>术语跟着体系走:政府采购写「采购人」「采购包」,招标投标写「招标人」「标段」(§9.1)。</li></ul>
</section>
</main>
@@ -233,8 +310,33 @@
<script>
const { $, toast, dialog } = Demo;
Demo.demoTop("cards.html");
const DEMO = "(演示)这里只做了样式";
document.addEventListener("click", (e) => { if (e.target.closest("[data-demo]")) { e.preventDefault(); toast(DEMO, { ok: false }); } });
/* 统计格:单选,再点一次回到「进行中」 */
$("[data-stats]").addEventListener("click", (e) => {
const b = e.target.closest(".stat"); if (!b) return;
const all = [...$("[data-stats]").children], on = b.getAttribute("aria-pressed") === "true" && b !== all[0];
all.forEach((x) => x.setAttribute("aria-pressed", on ? x === all[0] : x === b));
});
$("[data-cancel]").onclick = () => dialog({ title: "取消解析?", ok: "取消解析", cancel: "继续解析", body: "<p>已完成的阶段不会保留。取消后退回 1 次解析额度,可以稍后重新上传。</p>", onOk: () => toast("已取消解析 · 已退回 1 次解析额度") });
$("[data-ai-ok]").onclick = () => toast("已确认项目要点", { undo: () => toast("已撤销", { ok: false }) });
$("[data-renew]").onclick = () => toast("(演示)打开额度页的续费申请:在线申请 → 对公转账 → 运营开通", { ok: false });
/* AI 摘要卡:确认前 ⇄ 确认后(Q2) */
const card = $("[data-ai-card]"), foot = $("[data-ai-foot]").innerHTML;
function setDone(done) {
$("[data-ai-block]", card).classList.toggle("done", done);
$("[data-ai-tag]", card).hidden = done;
$("[data-ai-sign]", card).innerHTML = done ? '<div class="sign sm"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">11月3日 15:02</span><span>确认无误</span></div>' : "";
$("[data-ai-foot]", card).innerHTML = done
? `<button class="btn sm text">${Demo.icon("history")}处理记录</button><span class="r"><button class="btn sm text" data-ai-undo>撤销确认</button></span>`
: foot;
}
card.addEventListener("click", (e) => {
if (e.target.closest("[data-ai-ok]")) { setDone(true); toast("已确认项目要点", { undo: () => setDone(false) }); $("[data-ai-undo]", card).focus(); }
else if (e.target.closest("[data-ai-undo]")) { setDone(false); toast("已撤销确认", { ok: false }); $("[data-ai-ok]", card).focus(); }
});
</script>
</body>
</html>

View File

@@ -1,4 +1,10 @@
<!doctype html>
<!--
陈列页 · 控件总览(v0.2)
视觉以定稿样张 explore/workbench-d5.html 为准;共用组件来自 components.css 和 demo.css,
本页 <style> 里只放陈列用的小件(主按钮示意、选中态示例的边距、AI 产出示例的切换);日期输入、署名行、菜单、快捷键表都用共用实现。
网址加 ?open=select | date | menu | d1 | d2 | d3 可直接打开对应的浮层或对话框(截图用)。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -9,15 +15,37 @@
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* Q12 示意:浅灰块代表页面,白卡片代表对话框 */
.q12{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:var(--gap-card)}
.q12-page{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-4) var(--sp-5);background:var(--bg-page);border-radius:var(--radius-ctl)}
.q12-page b{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-serif);margin-right:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.q12-dlg{padding:var(--sp-4) var(--sp-5);background:var(--bg-page);border-radius:var(--radius-ctl)}
.q12-dlg > div{padding:var(--sp-4) var(--sp-5);background:var(--bg-surface);border-radius:var(--radius-card);box-shadow:var(--shadow-overlay)}
.q12-dlg b{display:block;margin-bottom:var(--sp-3)}
.q12-dlg .foot{display:flex;justify-content:flex-end;gap:var(--sp-2)}
/* 当前条示例(§7.18):清单项、表格行放在分区卡片里 */
.sel-demo .item{padding:var(--sp-3) var(--card-x)}
.sel-demo .item:first-of-type{border-top:var(--line) solid var(--border-subtle)}
.sel-demo .item:last-child{border-radius:0 0 var(--radius-card) var(--radius-card)}
.sel-demo .item .side .cite-src{white-space:nowrap}
.sel-demo .card-h small{margin-left:auto;font-size:var(--fs-caption);color:var(--text-secondary)}
/* AI 产出示例(Q2) */
.ai-acts{display:flex;gap:var(--sp-2);margin-top:var(--sp-3)}
.ai-block.done .ai-label,.ai-block.done .ai-acts{display:none}
.ai-block:not(.done) .sign{display:none}
</style>
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>① 控件总览</h1>
<p>每个控件按「默认 → 悬停 → 按下 → 聚焦 → 禁用 → 加载中 / 错误」陈列。灰色小字是状态名;标「可以点」的是可交互的。示意数据取自原型的「某市政务服务中心智慧大厅建设项目」。</p>
<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>
<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 标签 · AI 标识</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>
@@ -31,20 +59,19 @@
<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>
<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>
<li>没有红色的危险按钮:删除用次按钮 + 确认对话框。</li>
<li>加载中把文字换成「正在…」,按钮宽度不变,不加转圈。</li>
</ul>
</div>
<div class="d-grid c3" style="margin-top:var(--sp-6)">
<div class="d-grid c3" style="margin-top:var(--gap-card)">
<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 class="d-row"><button class="btn sm">确认无误</button><button class="btn">确认无误</button><button class="btn lg">上传招标文件</button></div>
<ul class="d-rules"><li>32 表格行内 · 36 默认 · 40 登录页、空状态</li></ul>
</div>
<div class="d-box">
<h3>带图标 · 只有图标</h3>
@@ -52,18 +79,28 @@
<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>
<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>
<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">扣 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 class="d-row"><button class="btn" data-nomore>发起检查</button><span class="cost">剩 <span class="fig">0</span> 次 · 仍可点</span></div>
</div>
</div>
<div class="d-box" style="margin-top:var(--gap-card)">
<h3>一屏一个主按钮,按上下文算</h3>
<div class="q12">
<div><span class="d-label">页面级:只有一个,放在页头右侧</span>
<div class="q12-page"><b>智慧大厅建设项目</b><button class="btn">导出报告</button><span class="cost">扣 1 次检查 · 剩 <span class="fig">12</span> 次</span><button class="btn primary">发起检查</button></div></div>
<div><span class="d-label">对话框:独立上下文,可以有自己的主按钮</span>
<div class="q12-dlg"><div><b>确认清单 v3?</b><div class="foot"><button class="btn">取消</button><button class="btn primary">确认清单</button></div></div></div></div>
</div>
<ul class="d-rules"><li>本页和组件预览是陈列页,为了对比会同时出现多个主按钮,不受此限。</li></ul>
</div>
</section>
<!-- 02 输入框 -->
@@ -71,17 +108,17 @@
<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 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 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 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-grid c2" style="margin-top:var(--gap-card)">
<div class="d-box">
<h3>文本域 · 字数 <small>可以输入</small></h3>
<div class="field fill" data-count-field>
@@ -95,75 +132,91 @@
<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>
<ul class="d-rules"><li>高 36,圆角 8;聚焦时边框变墨并加 1px 墨色内圈。占位符写清楚能搜什么。</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-sec-h"><span class="no">03</span><h2>下拉选择</h2><span class="ref">规范 §7.21</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="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" data-select id="sel-demo"><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">公开招标</li><li role="option">邀请招标</li><li role="option" aria-selected="true">竞争性磋商</li><li role="option">竞争性谈判</li><li role="option">询价</li><li class="grp">招标投标</li><li role="option" aria-disabled="true">公开招标(招标投标法)<small>不适用于本项目</small></li></ul></div></div></div>
<div class="d-state d-open"><small>展开</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>
<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>
<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>
<div class="d-state"><small>表格行内(高 32)</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>
<ul class="d-rules"><li>选中项加粗,右侧墨色 ✓;不用蓝色,也不用底色。悬停和键盘移动出浅灰底。</li><li>不可选的项变禁用灰,右侧写原因。键盘:↑ ↓ 移动、Enter 选中、Esc 收起。</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-sec-h"><span class="no">04</span><h2>日期选择</h2><span class="ref">规范 §7.22 · §9.3</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>
<h3>输入框 <small>可以输入、可以点</small></h3>
<div class="field">
<label for="dt1">投标截止 <small>(必填)</small></label>
<div class="dt" data-dt data-time="09:30" data-left data-marks="2026-11-9,2026-11-20" data-req="请填写投标截止时间" id="dt-demo">
<input class="input" id="dt1" value="2026-11-20 09:30" placeholder="例如:2026-11-20 09:30" autocomplete="off">
<button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button>
<div class="cal" role="dialog" aria-label="选择投标截止日期" hidden></div>
</div>
<span class="msg" data-dt-msg>24 小时制</span>
</div>
<div class="field" style="margin-top:var(--sp-4)">
<label for="dt2">证书有效期至</label>
<div class="dt" data-dt>
<input class="input" id="dt2" placeholder="例如:2027-05-14" autocomplete="off">
<button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button>
<div class="cal" role="dialog" aria-label="选择有效期" hidden></div>
</div>
<span class="msg" data-dt-msg>可以键入「2027/5/14」</span>
</div>
<div class="field err" style="margin-top:var(--sp-4)">
<label for="dt3">发证日期</label>
<div class="dt"><input class="input" id="dt3" value="2027-03-01" aria-invalid="true"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" tabindex="-1"><i data-ic="calendar"></i></button></div>
<span class="msg"><span class="ico w">!</span>发证日期晚于有效期,请核对证书</span>
</div>
</div>
<div class="d-box">
<h3>展开</h3>
<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>
<ul class="d-rules"><li>周一开始,非本月的格子留空。今天:控件灰描边;选中:墨底白字;关键日期:下方小圆点。</li><li>时间 24 小时制。</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>
<ul class="d-rules"><li>不可选的日期只变禁用灰,没有悬停,不加删除线。</li></ul>
</div>
</div>
<ul class="d-rules"><li>输入框写「2026-11-20 09:30」,可以直接键入「2026/11/20 9:30」等写法,离开时整理;下方显示「11月20日 09:30 · 剩 17 天」,跨年时带年份。</li></ul>
</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-sec-h"><span class="no">05</span><h2>复选 · 单选 · 开关</h2><span class="ref">规范 §7.19 · §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" 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>
@@ -190,46 +243,52 @@
<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 class="d-state"><small>关</small><button type="button" class="switch" role="switch" aria-checked="false" aria-label="示例"><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" aria-label="示例"><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" aria-label="示例" 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" aria-label="示例" 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 class="choice" style="justify-content:space-between;width:100%;cursor:default"><span id="sw1">额度用到 80% 时站内提醒<small>每期提醒一次</small></span><button type="button" class="switch" role="switch" aria-checked="true" aria-labelledby="sw1" data-sw-toast="额度提醒"><span class="track"></span><span class="st"></span></button></div>
<div class="choice" style="justify-content:space-between;width:100%;cursor:default"><span id="sw2">资质到期前 30 天提醒</span><button type="button" class="switch" role="switch" aria-checked="false" aria-labelledby="sw2" data-sw-toast="资质到期提醒"><span class="track"></span><span class="st"></span></button></div>
</div>
<ul class="d-rules"><li>规范没有定义开关。这里沿用 2px 圆角做成方形滑块(不用胶囊形),旁边写「开 / 关」,不只靠颜色。</li></ul>
<ul class="d-rules"><li>矩形轨道 + 方块滑钮,不用胶囊形;开为墨色,旁边写「开 / 关」。</li><li>只用于立即生效的设置;要提交的表单项用复选框。</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-sec-h"><span class="no">06</span><h2>级别标签 · 中性标签 · AI 标识 · 署名</h2><span class="ref">规范 §3.3 · §7.2 · §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>
<h3 style="margin-top:var(--sp-6)">中性标签(浅灰实底,无描边)</h3>
<div class="d-row"><span class="d-cap">状态</span><span class="tag">跟进中</span><span class="tag">编制中</span><span class="tag">已检查</span><span class="tag">已递交</span><span class="tag">已归档</span></div>
<div class="d-row"><span class="d-cap">就绪</span><span class="tag">已检查</span><span class="hint-u tip-below" tabindex="0" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span><span class="tag" style="margin-left:var(--sp-4)">已检查</span><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></div>
<div class="d-row"><span class="d-cap">复查</span><span class="flag box">新增</span><span class="flag box">沿用</span><span class="flag box">本次已解决</span><span class="d-cap" style="margin-left:var(--sp-4)">清单</span><span class="tag">草稿 v3</span><span class="tag">已确认 v2</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>
<h3>AI 产出 → 人工确认</h3>
<div class="d-row"><span class="tag ai">AI 摘要</span><span class="tag ai">AI 初稿</span><span class="tag ai">AI 提取</span><span class="d-cap" style="margin-left:var(--sp-4)">来源</span><span class="tag ai">AI</span><span class="tag">人工添加</span><span class="tag">补遗 1</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><span class="d-label">确认前:「AI」标签 + 左侧 2px 蓝铅笔虚线,正文墨色 · 可以点</span>
<div class="ai-block" data-ai>
<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 class="ai-acts"><button class="btn sm" data-ai-ok>确认无误</button><button class="btn sm text" data-ai-edit>修改</button></div>
<div class="sign sm"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig" data-ai-time>11月3日 10:05</span><span data-ai-act>确认无误</span><button type="button" class="undo" data-ai-undo>撤销</button></div>
</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 class="sign sm"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">11月2日 14:20</span><span>确认无误</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 class="sign" style="margin:0"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">11月2日 14:20</span><span>已修改</span><button type="button" class="undo" data-undo-demo>撤销</button></div>
<div class="sign ign"><span class="ok" aria-hidden="true">–</span><b>王敏</b><span class="fig">11月2日 14:26</span><span>已忽略</span><button type="button" class="undo" data-undo-demo>撤销</button></div>
<p class="sign-note">原因:招标文件允许</p></div>
</div>
</div>
</div>
@@ -237,51 +296,53 @@
<!-- 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-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>
<span class="d-label" style="margin-top:var(--sp-5)">列表行里的出处链接</span>
<a class="cite-link" href="#"><i data-ic="file"></i>技术册 · 第 <span class="fig">12</span> 页</a>
<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 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>
<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>
<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-sec-h"><span class="no">08</span><h2>提醒</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 class="alert"><i data-ic="info" class="lg"></i><p>清单未确认,检查结果以当前清单为准。</p><button class="btn sm">确认清单</button></div>
<div class="alert warn"><i data-ic="clock" class="lg"></i><p><strong>某县数据中心机房改造项目</strong> <span class="when">今天 17:00 截止</span>,还有 <span class="fig">3</span> 项致命问题待处理。</p></div>
<div class="alert warn"><i data-ic="wallet" class="lg"></i><p>本期检查额度 <span class="when">已用 80%</span>,剩 <span class="fig">4</span> 次。</p><span class="act"><a class="lnk cap" href="#">查看额度</a></span></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 class="alert warn"><i data-ic="alert-circle" class="lg"></i><div><p><span class="when">解析失败:文件已加密。</span>已退回 1 次解析额度。</p><span class="sub">关于 ZC-2026-0418 的更正公告.pdf</span></div><span class="act"><button class="btn sm">重新上传</button></span></div>
<div class="alert"><i data-ic="info" class="lg"></i><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"><i data-ic="check-circle" class="lg"></i><p>报告已导出:智慧大厅建设项目-辅助检查报告-第 2 次.pdf</p></div>
</div>
</div>
<ul class="d-rules"><li>提醒放在相关内容上方,不自动消失。系统错误不用红色,用中性提醒 + 错误编号。</li></ul>
<ul class="d-rules"><li>和卡片同规格的白条,颜色只在图标和关键字上:信息墨色、提醒赭色、完成绿色。截止临近、额度、解析失败都用赭色,不用红。</li><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-sec-h"><span class="no">09</span><h2>轻提示</h2><span class="ref">规范 §7.12</span></div>
<div class="d-grid c2">
<div class="d-box">
<h3>样式</h3>
@@ -294,38 +355,38 @@
<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>
<ul class="d-rules"><li>只用于操作结果,4 秒消失,悬停暂停。可撤销的操作给「撤销」。</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-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>
<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>
<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>
<button class="btn" data-d3>反馈:判断有误</button>
</div>
<ul class="d-rules"><li>宽 480(确认)/ 640(表单),圆角 4。</li><li>标题写要做的事,正文写后果;主操作在右,按钮写动作「删除项目」,不写「确定」。</li><li>删除没有红色:靠输入项目名确认。Esc 关闭,焦点回到触发按钮。</li></ul>
<ul class="d-rules"><li>宽 480(确认)/ 640(表单),圆角 12,遮罩墨色 32%。对话框不裁剪下拉和日期浮层。</li><li>标题写要做的事,正文写后果;主按钮在右,写动作,不写「确定」。对话框可以有自己的主按钮。</li><li>删除不用红色,靠输入项目名确认。Esc 关闭,焦点回到触发按钮。</li></ul>
</div>
</div>
</section>
<!-- 11 标签页 分段 分页 -->
<!-- 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-sec-h"><span class="no">11</span><h2>标签页 · 筛选 · 分页 · 选中</h2><span class="ref">规范 §5.3 · §7.18 · §7.20</span></div>
<div class="d-grid c2">
<div class="d-box">
<h3>标签页 <small>可以点,← → 切换</small></h3>
@@ -336,20 +397,42 @@
<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="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>
<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)">
<div class="d-box" style="margin-top:var(--gap-card)">
<h3>分页 <small>可以点</small></h3>
<div class="pagination" data-pager></div>
<ul class="d-rules"><li>当前页用墨色描边 + 加粗(不填色,避免和主按钮抢)。每页条数固定 20 / 50,不做无限滚动,方便「第几页第几条」沟通。</li></ul>
<ul class="d-rules"><li>当前页浅灰实底 + 加粗,和筛选的按下态一样;只在超过一页时显示。</li></ul>
</div>
<div class="d-grid c2 sel-demo" style="margin-top:var(--gap-card)">
<section class="card flush" aria-labelledby="h-sel1">
<div class="card-h"><h3 id="h-sel1">当前条 · 清单项</h3><small>可以点,↑ ↓ 移动</small></div>
<div data-sel-list role="listbox" aria-label="清单示例">
<div class="item" tabindex="-1" role="option" aria-selected="false"><span class="imp">★</span><div><div class="t">投标保证金人民币 5 万元</div><div class="meta"><span class="tag">资格条件</span><span class="tag">通用</span></div></div><div class="side"><a class="cite-src" href="#">招标文件 · <span class="fig">第 8 页</span></a></div></div>
<div class="item sel" tabindex="0" role="option" aria-selected="true"><span class="imp"></span><div><div class="t">投标有效期 90 天</div><div class="meta"><span class="tag">商务条款</span><span class="tag">通用</span></div></div><div class="side"><a class="cite-src" href="#">招标文件 · <span class="fig">第 21 页</span></a></div></div>
<div class="item" tabindex="-1" role="option" aria-selected="false"><span class="imp">★</span><div><div class="t">应用服务器内存 ≥ 512G DDR4</div><div class="meta"><span class="tag">技术参数</span><span class="tag">采购包 1</span></div></div><div class="side"><a class="cite-src" href="#">招标文件 · <span class="fig">第 41 页</span></a></div></div>
</div>
</section>
<section class="card flush" aria-labelledby="h-sel2">
<div class="card-h"><h3 id="h-sel2">当前条 · 勾选 · 表格行</h3><small>点行定位,点复选框勾选</small></div>
<table class="data hover" data-sel-table>
<thead><tr><th class="ckc"><input type="checkbox" class="ck" aria-label="全选" data-all></th><th>资质</th><th>类型</th><th>有效期至</th></tr></thead>
<tbody>
<tr><td class="ckc mid"><input type="checkbox" class="ck" checked aria-label="选择"></td><td class="mid">ISO/IEC 27001 信息安全管理体系</td><td class="mid"><span class="tag">体系认证</span></td><td class="mid"><span class="num">2027年5月14日</span></td></tr>
<tr class="sel"><td class="ckc mid"><input type="checkbox" class="ck" aria-label="选择"></td><td class="mid">信息系统建设和服务能力等级 CS3</td><td class="mid"><span class="tag">资质证书</span></td><td class="mid"><span class="num">2028年1月9日</span></td></tr>
<tr><td class="ckc mid"><input type="checkbox" class="ck" aria-label="选择"></td><td class="mid">智慧大厅综合管理平台 V2.0</td><td class="mid"><span class="tag">软件著作权</span></td><td class="mid muted">长期</td></tr>
</tbody>
</table>
</section>
</div>
<ul class="d-rules"><li>当前条(清单项、表格行、问题):最浅的灰底 + 左侧 3px 墨色竖条;键盘焦点另加 2px 墨色内框。竖条只表示「当前」,和问题级别无关,致命行不加红竖线。</li><li>勾选(批量处理)只有复选框是选中状态,不加底色和竖条,和当前条可以同时出现。</li><li>筛选、分页、分类导航的「当前项」用浅灰实底 + 加粗。蓝色只留给原文高亮和出处。</li></ul>
</section>
<!-- 12 空状态 骨架屏 进度 -->
@@ -361,14 +444,14 @@
<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>
<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>
<ul class="d-rules"><li>静止的浅灰块,不做明暗起伏或扫光;不用转圈占满屏。</li></ul>
</div>
<div class="d-box">
<h3>分阶段进度</h3>
@@ -383,43 +466,33 @@
<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 class="d-grid c3" style="margin-top:var(--gap-card)">
<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 warn">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 warn">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-sec-h"><span class="no">13</span><h2>提示气泡 · 操作菜单 · 快捷键</h2><span class="ref">规范 §6 · §7.21 · §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" style="padding-bottom:calc(var(--sp-16) + var(--sp-2))"><span class="hint-u 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>
<ul class="d-rules"><li>墨底白字,圆角 8。解释性的话放在这里,不写在界面上。</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>
<h3>操作菜单 <small>可以点</small></h3>
<div class="d-row"><span class="muted">列表行的「更多」</span><button class="icon-btn" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false" data-tip="更多操作" data-more><i data-ic="more"></i></button></div>
<div class="pop d-open" style="margin-top:var(--sp-3)"><div class="menu" role="menu" aria-label="更多操作(静态展开)" style="width:248px;min-width:0" data-menu-static></div></div>
<ul class="d-rules"><li>会扣点的项第二行写扣多少、还剩多少;不可用的项变禁用灰,右侧写原因。</li></ul>
</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 data-keys></div>
<p class="note">按页面只列能用的键:这是查标(问题列表)的一组;读标只有 J / K、Enter、?。「④ 列表与表格」和「⑥ 读标」页可以实际用。</p>
</div>
</div>
</section>
@@ -440,8 +513,50 @@ $$("[data-count-field]").forEach((f) => {
$$("[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-nomore]").onclick = () => dialog({ title: "本期检查额度已用完", ok: "提交续费申请", cancel: "关闭",
body: "<p>这次检查需要 1 次检查额度,本期 20 次已用完。可以在额度页提交续费申请(对公转账后由运营开通),或等下期(2027-11-01)发放。</p><p class='muted cap'>24 小时内对同一项目的复查不扣额度。</p>",
onOk: () => toast("(演示)打开额度页的续费申请", { ok: false }) });
/* 开关:立即生效,给轻提示 */
$$("[data-sw-toast]").forEach((s) => s.addEventListener("click", () => toast(`${s.dataset.swToast}已${s.getAttribute("aria-checked") === "true" ? "打开" : "关闭"}`)));
/* AI 产出 → 确认(Q2):去掉虚线,换成署名行 */
(function () {
const b = $("[data-ai]");
const set = (done, act) => { b.classList.toggle("done", done); if (act) $("[data-ai-act]", b).textContent = act; };
$("[data-ai-ok]", b).onclick = () => { set(true, "确认无误"); toast("已确认", { undo: () => set(false) }); };
$("[data-ai-edit]", b).onclick = () => toast("(演示)修改后同样换成署名行,动作写「已修改」", { ok: false });
$("[data-ai-undo]", b).onclick = (e) => { e.preventDefault(); set(false); $("[data-ai-ok]", b).focus(); };
})();
/* 日期输入(Q10):共用组件 Demo.dateInput(),.dt[data-dt] 自动初始化,这里不用写脚本 */
/* 当前条(§7.18):清单项 */
(function () {
const box = $("[data-sel-list]"), its = $$(".item", box);
const sel = (it, focus) => { its.forEach((x) => { const on = x === it; x.classList.toggle("sel", on); x.setAttribute("aria-selected", on); x.tabIndex = on ? 0 : -1; }); if (focus) it.focus(); };
its.forEach((it) => {
it.addEventListener("click", (e) => { if (!e.target.closest("a")) sel(it); });
it.addEventListener("focus", () => it.classList.toggle("focus", it.matches(":focus-visible")));
it.addEventListener("blur", () => it.classList.remove("focus"));
});
box.addEventListener("keydown", (e) => {
const k = its.indexOf(document.activeElement); if (k < 0) return;
const d = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0; if (!d) return;
e.preventDefault(); sel(its[(k + d + its.length) % its.length], true);
});
$$(".cite-src", box).forEach((a) => a.addEventListener("click", (e) => { e.preventDefault(); toast("(演示)在原文查看器里打开,原句用蓝铅笔浅底高亮", { ok: false }); }));
})();
/* 表格行:点行 = 当前条(tr.sel);复选框 = 勾选,只改复选框本身(§7.18) */
(function () {
const t = $("[data-sel-table]"), all = $("[data-all]", t), cks = $$("tbody .ck", t);
const sync = () => { const n = cks.filter((c) => c.checked).length; all.checked = n === cks.length; all.indeterminate = n > 0 && n < cks.length; };
cks.forEach((c) => c.addEventListener("change", sync));
all.addEventListener("change", () => { cks.forEach((c) => { c.checked = all.checked; }); sync(); });
$$("tbody tr", t).forEach((tr) => tr.addEventListener("click", (e) => { if (e.target.closest("input")) return; $$("tbody tr", t).forEach((x) => x.classList.toggle("sel", x === tr)); }));
sync();
})();
$$("[data-undo-demo]").forEach((b) => b.onclick = () => toast("(演示)撤销后回到处理按钮", { ok: false }));
/* 轻提示 */
$("[data-t1]").onclick = () => toast("已保存「ISO/IEC 27001 信息安全管理体系」");
@@ -449,27 +564,46 @@ $("[data-t2]").onclick = () => toast("已将 3 项「提示」标记为确认无
$("[data-t3]").onclick = () => { toast("清单 v3 已确认"); setTimeout(() => toast("已核对补遗更新"), 300); setTimeout(() => toast("报告已导出"), 600); };
/* 对话框 */
$("[data-d1]").onclick = () => dialog({
const openD1 = () => dialog({
title: "删除项目「某区智慧社区平台运维服务」?", ok: "删除项目",
body: `<p>项目下的招标文件、清单、检查结果和报告会一起删除。删除后 30 天内可以在回收站恢复,之后彻底删除。</p>
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 天内)")
onOk: () => toast("已删除,30 天内可在回收站恢复")
});
$("[data-d2]").onclick = () => dialog({
const openD2 = () => 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>`,
body: `<div class="alert warn"><i data-ic="alert-circle" class="lg"></i><p>还有 <span class="when">3 项致命问题</span>未处理。标记不受影响,处理记录会如实保留。</p></div>
<p style="margin-top:var(--sp-3)">标记后不能再发起检查;清单和检查结果保留,可以导出报告。</p>`,
onOk: () => toast("已标记为已递交")
});
$("[data-d3]").onclick = () => dialog({
title: "忽略并注明原因", wide: true, ok: "忽略这一项",
const openD3 = () => dialog({ // 表单对话框(640):反馈判断有误。忽略原因不用对话框,在问题行内展开(§7.4)
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 }) }); }
<div class="form">
<div class="field"><label id="fb-k">哪里不对 <small>(必填)</small></label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="fb-k"><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="fb-p">投标文件页码 <small>(选填)</small></label><div class="input-unit"><input class="input" id="fb-p" placeholder="例如 3"><span class="unit">/ 120 页</span></div></div>
<div class="field span2"><label for="fb-t">说明 <small>(必填)</small></label><textarea class="input" id="fb-t" placeholder="例如:分项价格表第 3 页有一行是可选项,不计入合计"></textarea><span class="msg">反馈只用于改进检查,不改变这一条的处理状态。</span></div>
</div>`,
onOk: (s) => { const t = $("#fb-t", s); if (!t.value.trim()) { Demo.fieldError(t.closest(".field"), "请写一句哪里不对"); t.focus(); return false; } toast("已收到反馈"); }
});
$("[data-d1]").onclick = openD1;
$("[data-d2]").onclick = openD2;
$("[data-d3]").onclick = openD3;
/* 操作菜单:列表行「更多」,和工作台同一个浮动菜单(Demo.floatMenu);静态展开的那份用 Demo.menuHTML 生成同样的项 */
const MORE = [
{ label: "处理记录" }, { label: "复制为新项目" }, { label: "追加补遗 / 澄清", sub: '扣 1 次解析 · 剩 <span class="fig">37</span> 次' }, "-",
{ label: "标记已递交" }, { label: "归档", disabled: "递交后可用" }, { label: "删除项目" }
];
$("[data-menu-static]").innerHTML = Demo.menuHTML(MORE.map((it, i) => i === 1 ? { ...it, active: true } : it));
$("[data-menu-static]").addEventListener("click", (e) => { const b = e.target.closest("[data-i]"); if (b && !b.hasAttribute("aria-disabled")) toast(`(演示)${b.firstChild.textContent}`, { ok: false }); });
$("[data-keys]").innerHTML = Demo.keysTable(Demo.KEYS.check);
$("[data-more]").addEventListener("click", (e) => {
e.stopPropagation();
Demo.floatMenu(e.currentTarget, MORE, (act) => { if (act === "删除项目") openD1(); else if (act === "标记已递交") openD2(); else toast(`(演示)${act}`, { ok: false }); });
});
/* 分页 */
@@ -477,7 +611,7 @@ $("[data-d3]").onclick = () => dialog({
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>
el.innerHTML = `<span class="total">共 <span class="fig">${total}</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>
@@ -487,6 +621,16 @@ $("[data-d3]").onclick = () => dialog({
el.addEventListener("change", (e) => { const v = +e.target.value; if (v >= 1 && v <= pages) cur = v; render(); });
render();
})();
/* ?open=select | date | menu | d1 | d2 | d3:直接打开浮层(截图用) */
addEventListener("load", () => {
const o = new URLSearchParams(location.search).get("open");
const at = (sel, fn) => { const el = document.querySelector(sel); if (el.getBoundingClientRect().bottom > innerHeight) el.scrollIntoView({ block: "start" }); setTimeout(fn, 50); };
if (o === "select") at("#s-select", () => $("#sel-demo .select").click());
if (o === "date") at("#s-date", () => $("#dt-demo .icon-btn").click());
if (o === "menu") at("#s-tip", () => $("[data-more]").click());
if ({ d1: 1, d2: 1, d3: 1 }[o]) ({ d1: openD1, d2: openD2, d3: openD3 })[o]();
});
</script>
</body>
</html>

View File

@@ -1,47 +1,50 @@
/* 标博士 · UI demo 样式(2026-11-03)
* 依赖 ../system/tokens.css 和 ../system/components.css,只用 tokens.css 的变量,不写裸色值。
/* 标博士 · UI demo 样式 v0.2(2026-11-03)
* 依赖 ../system/tokens.css 和 ../system/components.css,只用 tokens.css 的变量,不写裸色值(Q6)。
* 视觉以定稿样张 explore/workbench-d5.html 为准(决策 U5),Q1–Q12 已于 2026-11-03 确认。
* 分两部分:
* A. 演示页排版:只属于 demo 页面(章节标题、状态陈列格),不进组件库;
* A. 演示页排版:只属于 demo 页面(章节标题、状态陈列格、目录卡),不进组件库;
* B. 新增组件:design-system.md 有描述、但 components.css 里还没有的组件。UI 精修定稿后再并入 components.css。
* 新增组件清单见 index.html。 */
* 新增组件清单见 index.html。类名和 demo.js 的接口沿用 v0.1,页面不用改调用方式。
* 收尾(2026-11-03):各页重刷时的页内绕过已收回这里——项目行(B20)、署名行(B16)、日期输入(B6)、
* 对照布局(B19)、当前条 / 勾选条(B18)、行内说明(B18 .u4);对话框不再裁剪浮层(B11)。 */
/* =====================================================================
* 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{position:sticky;top:0;z-index:30;display:flex;align-items:center;gap:var(--sp-6);height:var(--topbar-h);padding:0 var(--pad-page);background:var(--topbar-glass);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-bottom:var(--line) 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-top .tag{margin-left:calc(-1 * var(--sp-3))}
.d-nav{display:flex;gap:2px;margin-left:auto}
.d-nav a{display:flex;align-items:center;height:var(--control-h-sm);padding:0 10px;border-radius:var(--radius-ctl);color:var(--text-secondary);text-decoration:none;white-space:nowrap;transition:background-color var(--dur-fast) var(--ease)}
.d-nav a:hover{background:var(--c-n100);color:var(--text-primary)}
.d-nav a.on{background:var(--c-n100);color:var(--text-primary);font-weight:var(--fw-bold)}
.d-page{max-width:calc(var(--content-max) + 2 * var(--pad-page));margin:0 auto;padding:var(--sp-10) var(--pad-page) 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{color:var(--text-secondary);text-decoration-color:var(--link-underline);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{display:flex;align-items:baseline;gap:var(--sp-3);border-bottom:var(--line) solid var(--border-subtle);padding-bottom:var(--sp-3);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{display:grid;gap:var(--gap-card);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{background:var(--bg-surface);border:var(--line) solid var(--border-subtle);border-radius:var(--radius-card);padding:var(--sp-5) var(--card-x)}
.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{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap: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{margin:var(--sp-4) 0 0;padding:var(--sp-3) 0 0 1.2em;border-top:var(--line) 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)}
@@ -52,12 +55,18 @@ body.demo{min-width:1280px}
.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)} /* 静态展开的浮层,占位用 */
/* 陈列盒、对话框里的表格:不再带自己的外框(卡片里不套面板),首尾列贴齐容器内边距;没有表头时第一行不画上边线 */
:is(.d-box,.dialog) table.data{border:0;border-radius:0;background:none}
:is(.d-box,.dialog) table.data :is(th,td):first-child{padding-left:0}
:is(.d-box,.dialog) table.data :is(th,td):last-child{padding-right:0}
:is(.d-box,.dialog) table.data:not(:has(thead)) tbody tr:first-child td{border-top:0}
table.data.keys td:first-child{width:1%;padding-right:var(--sp-6)} /* 快捷键表(Demo.keysTable) */
.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-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap-card)}
.d-link{display:block;background:var(--bg-surface);border:var(--line) solid var(--border-subtle);border-radius:var(--radius-card);padding:var(--sp-4) var(--card-x);color:var(--text-primary);text-decoration:none;transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.d-link:hover{background:var(--c-n50);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)}
@@ -66,45 +75,78 @@ body.demo{min-width:1280px}
* B. 新增组件
* ===================================================================== */
/* ---- B1 图标(Lucide 线性,1.5px 描边,16 / 20px,§6) ---- */
/* ---- B1 图标(Lucide 线性,1.5px 描边,16 / 20px,§6;侧栏菜单 22px) ---- */
.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}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
/* ---- 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}
/* ---- B2 页面框架 v0.2(§5.3):96px 墨色渐变侧栏 + 透明顶栏 + 内容区左上角圆角 ----
* 结构由 demo.js 的 Demo.shell() 生成:body.app > aside.sidebar + main.main > header.top + 页面内容 */
body.app{display:grid;grid-template-columns:var(--rail-w) minmax(0,1fr);min-height:100vh;min-width:1280px;background:linear-gradient(180deg,var(--side-top) 0%,var(--side-bottom) 100%) fixed}
.sidebar{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:0 0 var(--sp-4) 10px;color:var(--side-fg);min-width:0}
.sidebar::after{content:"";position:absolute;left:0;right:0;bottom:0;height:260px;pointer-events:none;background-image:radial-gradient(var(--side-dots) 1px,transparent 1.2px);background-size:14px 14px;-webkit-mask-image:linear-gradient(to top,black,transparent);mask-image:linear-gradient(to top,black,transparent)}
.sidebar > *{position:relative;z-index:1}
.sidebar .brand{display:flex;justify-content:center;padding:var(--sp-5) 10px var(--sp-5) 0;border-radius:var(--radius-ctl)}
.sidebar .brand img{width:60px;height:auto;display:block}
.sidebar .nav{display:flex;flex-direction:column;gap:var(--sp-1)}
.sidebar .nav a{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--sp-1);height:62px;margin-right:10px;border-radius:var(--radius-card);font-size:var(--fs-caption);line-height:16px;color:var(--side-fg);text-align:center;text-decoration:none;white-space:nowrap;transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.sidebar .nav a .ic{width:22px;height:22px}
.sidebar .nav a:hover{background:var(--side-hover);color:var(--side-fg-strong)}
.sidebar .nav a[aria-current]{background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold)}
.sidebar .nav .sep{display:block;height:1px;margin:10px 22px 10px 12px;background:var(--side-line)}
.sidebar a:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px}
.sidebar .me{display:flex;flex-direction:column;align-items:center;margin-top:auto;padding:var(--sp-4) 10px var(--sp-1) 0;color:var(--side-muted);text-align:center}
.sidebar .avatar{display:inline-grid;place-items:center;width:44px;height:44px;margin-bottom:var(--sp-2);border-radius:50%;background:var(--side-avatar-bg);box-shadow:0 0 0 1px var(--side-avatar-ring);color:var(--side-fg-strong);font-size:var(--fs-title);font-weight:var(--fw-bold)}
.sidebar .uname{color:var(--side-fg-strong);font-size:var(--fs-body);line-height:var(--lh-body);font-weight:var(--fw-bold)}
/* 套餐标签(§3.1):只在账户区使用;免费版中性、专业版浅金、企业版金色实底 */
.plan{display:inline-block;margin-top:var(--sp-1);padding:0 var(--sp-2);border-radius:var(--radius-xs);font-size:var(--fs-caption);line-height:20px;font-weight:var(--fw-bold);white-space:nowrap}
.plan[data-plan="free"]{background:var(--plan-free-bg);color:var(--side-fg)}
.plan[data-plan="pro"]{background:var(--plan-pro-bg);color:var(--plan-pro-fg);box-shadow:inset 0 0 0 1px var(--plan-pro-line)}
.plan[data-plan="ent"]{background:var(--plan-ent-bg);color:var(--c-ink)}
.main{min-width:0;display:flex;flex-direction:column;min-height:100vh;background:var(--bg-page);border-top-left-radius:var(--radius-card)}
.top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:var(--sp-3);height:var(--topbar-h);padding:0 var(--pad-page);background:transparent;border-bottom:var(--line) solid transparent;border-top-left-radius:var(--radius-card);transition:background-color var(--dur-base) var(--ease),border-color var(--dur-base) var(--ease)}
.top.scrolled{background:var(--topbar-glass);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-bottom-color:var(--border-subtle)}
/* 左侧「公司 / 页面」 */
.crumbs{display:flex;align-items:center;gap:6px;min-width:0;color:var(--text-secondary);white-space:nowrap;overflow:hidden}
.crumbs > *{min-width:0;overflow:hidden;text-overflow:ellipsis}
.crumbs > span{flex:none}
.crumbs .org{flex:none;color:var(--text-primary);font-weight:var(--fw-bold)}
.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)}
.crumbs a:hover{color:var(--text-primary);text-decoration:underline;text-decoration-color:var(--link-underline);text-underline-offset:3px}
.crumbs b{font-weight:var(--fw-regular)}
/* 右侧:搜索、额度、通知 */
.top-search{position:relative;display:flex;align-items:center;flex:none;width:280px;height:var(--control-h);margin-left:auto;border:var(--line) solid var(--border-control);border-radius:var(--radius-ctl);background:var(--bg-surface);transition:width var(--dur-base) var(--ease),border-color var(--dur-fast) var(--ease)}
.top-search:hover{border-color:var(--c-ink)}
.top-search:focus-within{width:360px;border-color:var(--c-ink);box-shadow:0 0 0 1px var(--c-ink)}
.top-search > .ic{position:absolute;left:11px;color:var(--text-secondary);pointer-events:none}
.top-search input{flex:1;min-width:0;height:100%;padding:0 var(--sp-2) 0 34px;border:0;background:transparent;font:inherit;color:var(--text-primary);outline:0}
.top-search input::placeholder{color:var(--text-secondary)}
.top-search input::-webkit-search-cancel-button{display:none}
.top-search .kbd{margin-right:6px}
.top .vsep{flex:none;width:1px;height:20px;margin:0 var(--sp-1);background:var(--border-subtle)}
.top-right{display:flex;align-items:center;gap:var(--sp-1);flex:none}
/* 顶栏额度(§7.10、Q9):用到 ≥ 80% 时该项数字变赭色加粗(.warn);不用红 */
.quota-chip{display:inline-flex;align-items:center;gap:6px;height:var(--control-h-sm);padding:0 10px;border-radius:var(--radius-ctl);color:var(--text-secondary);text-decoration:none;white-space:nowrap;transition:background-color var(--dur-fast) var(--ease)}
.quota-chip:hover{background:var(--c-n100)}
.quota-chip .q{display:inline-flex;align-items:baseline;gap:6px}
.quota-chip .fig{color:var(--text-primary);font-size:var(--fs-figure-sm)}
.quota-chip .q.warn .fig,.quota-chip.warn .fig{color:var(--warn-text);font-weight:var(--fw-bold)}
.quota-chip .sep{width:1px;height:12px;margin:0 var(--sp-1);background:var(--border-subtle)}
.content{width:100%;padding:20px var(--pad-page) var(--sp-12)}
/* 页头:标题 + 一句话;右侧放页面级主按钮(Q12:页面级一个) */
.page-title{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--sp-6);margin-bottom:20px}
.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}
.page-title p{margin:2px 0 0;color:var(--text-secondary)}
.page-title .actions{margin-left:auto;display:flex;gap:var(--sp-3);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}
.stack > * + *{margin-top:var(--gap-card)}
/* 项目页头 */
.proj-head{padding:var(--sp-5) var(--sp-8) 0;border-bottom:1px solid var(--border-subtle);background:var(--bg-page)}
.proj-head{padding:var(--sp-4) var(--pad-page) 0;border-bottom:var(--line) solid var(--border-subtle)}
.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)}
@@ -112,11 +154,12 @@ body.app{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr);min-h
.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-hover{background:var(--c-n50);border-color:var(--c-ink)}
.btn.is-active{background:var(--c-n100);border-color:var(--c-ink)}
.btn.primary.is-hover{background:var(--primary-hover);border-color:var(--primary-hover)}
.btn.primary.is-active{background:var(--primary-active);border-color:var(--primary-active)}
.btn.text.is-hover{background:var(--c-n100);border-color:transparent}
.btn.text:active,.btn.text.is-active{background:var(--c-n100);border-color:transparent}
.btn.is-focus{outline:2px solid var(--focus-ring);outline-offset:2px}
.btn .ic{margin:0 -2px}
/* 加载中:两个文字叠在同一格,按宽的那个定宽,切换时宽度不变 */
@@ -125,114 +168,134 @@ body.app{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr);min-h
.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.primary.is-loading{background:var(--primary-hover);border-color:var(--primary-hover)}
.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)}
/* 只有图标的按钮:36 / 32,悬停出 --c-n100;必须有 aria-label 和悬停提示(§6) */
.icon-btn{position:relative;flex:none;display:inline-grid;place-items:center;width:var(--control-h);height:var(--control-h);padding:0;border:var(--line) solid transparent;border-radius:var(--radius-ctl);background:none;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.icon-btn:hover{background:var(--c-n100)}
.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.bordered:hover{border-color:var(--c-ink);background:var(--c-n50)}
.icon-btn.sm{width:var(--control-h-sm);height:var(--control-h-sm)}
.icon-btn.muted{color:var(--text-secondary)}
.icon-btn .dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--c-ink);box-shadow:0 0 0 2px var(--c-white)}
.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}
.btn-group .btn:hover{position:relative;z-index:1}
/* ---- B4 输入框、文本域、带单位、搜索(§7.13) ---- */
/* ---- B4 输入框、文本域、带单位、搜索(§7.13) ----
* 聚焦:边框变墨 + 1px 墨色内圈(合计 2px,与顶栏搜索一致);错误:赭色边框 + 赭色内圈 */
.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:hover,.input.is-hover{border-color:var(--c-ink)}
.input:focus,.input.is-focus{outline:0;border-color:var(--c-ink);box-shadow:0 0 0 1px var(--c-ink)}
.field.err .input:focus{border-color:var(--warn);box-shadow:0 0 0 1px var(--warn)}
.input[disabled],.input.is-disabled{background:var(--c-n100);border-color:var(--c-n100);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}
.input[readonly]{background:var(--c-n50);border-color:var(--border-subtle)}
.input[readonly]:focus{box-shadow:none;border-color:var(--border-control)}
textarea.input{height:auto;min-height:80px;padding:var(--sp-2) var(--sp-3);resize:vertical;line-height:var(--lh-body)}
select.input{appearance:none;padding-right:32px;cursor:pointer}
.sel-wrap{position:relative;display:block}
.sel-wrap > .ic{position:absolute;right:8px;top:8px;pointer-events:none;color:var(--text-secondary)}
.sel-wrap > .ic{position:absolute;right:10px;top:10px;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 .count{font:var(--fw-medium) var(--fs-figure-xs)/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}
.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:var(--fs-figure-sm)}
.input-unit .unit{display:flex;align-items:center;padding:0 var(--sp-3);border:var(--line) solid var(--border-control);border-left:0;border-radius:0 var(--radius-ctl) var(--radius-ctl) 0;background:var(--c-n50);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}
.search > .ic{position:absolute;left:11px;top:10px;color:var(--text-secondary);pointer-events:none}
.search .input{padding-left:34px}
.search .kbd{position:absolute;right:7px;top:7px}
/* 表单布局 */
.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 + .form-sec{margin-top:var(--sp-8);padding-top:var(--sp-6);border-top:var(--line) 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{display:flex;gap:var(--sp-3);align-items:center;margin-top:var(--sp-8);padding-top:var(--sp-5);border-top:var(--line) 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 只有浮层有阴影) ---- */
/* ---- B5 下拉选择 / 下拉菜单(Q4:选中项加粗,右侧墨色 ✓;浮层圆角 12,只有浮层有阴影) ---- */
.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{display:flex;align-items:center;gap:var(--sp-2);width:100%;height:var(--control-h);padding:0 10px 0 var(--sp-3);border:var(--line) solid var(--border-control);border-radius:var(--radius-ctl);background:var(--bg-surface);font:var(--fs-body) var(--font-sans);color:var(--text-primary);text-align:left;cursor:pointer;transition:border-color var(--dur-fast) var(--ease)}
.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[aria-expanded="true"],.select.is-focus{outline:0;border-color:var(--c-ink);box-shadow:0 0 0 1px 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[disabled]{background:var(--c-n100);border-color:var(--c-n100);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{position:absolute;z-index:45;top:calc(100% + 4px);left:0;min-width:100%;max-height:300px;overflow:auto;margin:0;padding:6px;list-style:none;background:var(--bg-surface);border:var(--line) solid var(--border-subtle);border-radius:var(--radius-card);box-shadow:var(--shadow-overlay)}
.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.float{position:fixed;top:auto;left:auto;min-width:248px} /* 由脚本按触发按钮定位,如列表行的「更多」 */
.menu [role=option],.menu [role=menuitem]{display:flex;align-items:center;gap:var(--sp-2);width:100%;min-height:34px;padding:6px 10px;border:0;border-radius:var(--radius-tag);background:none;font:var(--fs-body)/var(--lh-body) var(--font-sans);color:var(--text-primary);text-align:left;cursor:pointer;white-space:nowrap}
.menu [role=option]:hover,.menu [role=menuitem]:hover,.menu .is-active,.menu [role=menuitem]:focus-visible{background:var(--c-n100);outline:0}
.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 [aria-selected="true"]::after{content:"✓";margin-left:auto;padding-left:var(--sp-4);color:var(--c-ink);font-weight:var(--fw-bold)}
.menu [aria-disabled="true"],.menu [aria-disabled="true"]:hover{color:var(--text-disabled);cursor:not-allowed;background:none}
.menu [aria-disabled="true"]:focus-visible{background:var(--c-n50)}
.menu .grp{padding:var(--sp-2) 10px 2px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.menu hr{border:0;border-top:var(--line) solid var(--border-subtle);margin:6px 0}
.menu small{color:var(--text-secondary);font-size:var(--fs-caption);margin-left:auto;padding-left:var(--sp-4)}
.menu small .fig{color:var(--text-primary);font-size:var(--fs-figure-xs)}
/* 两行菜单项:动作 + 说明(扣点说明写在这里,§8.3) */
.menu .two{flex-direction:column;align-items:flex-start;gap:0}
.menu .two small{display:block;margin:0;padding:0;line-height:var(--lh-caption)}
.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)}
/* ---- B6 日期输入与日期选择(§7.22、§9.3;Q4:不可选日期只变禁用灰;Q10:输入「2026-11-20 09:30」) ----
* 日期输入 .dt[data-dt]:可直接键入(接受 2026/11/20 9:30 等常见写法,离开时整理成标准格式),右侧按钮打开日期选择器;
* 带时间的字段(data-time)在选择器底部填时间,24 小时制;下方 [data-dt-msg] 显示「11月20日 09:30 · 剩 N 天」(data-left)。
* 交互见 demo.js 的 Demo.dateInput(),页面里不用再写脚本。 */
.dt{position:relative;display:block}
.dt > .input{padding-right:40px;font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-size:var(--fs-figure-sm)}
.dt > .input::placeholder{font-family:var(--font-sans);font-size:var(--fs-body)}
.dt > .icon-btn{position:absolute;top:2px;right:2px;color:var(--text-secondary)}
.dt > .icon-btn:hover,.dt > .icon-btn[aria-expanded="true"]{color:var(--text-primary)}
.dt.up > .cal{top:auto;bottom:calc(100% + 4px)}
.dt.d-open > .cal{position:static;margin-top:var(--sp-1)}
.cal{position:absolute;z-index:45;top:calc(100% + 4px);left:0;width:288px;padding:var(--sp-3);background:var(--bg-surface);border:var(--line) solid var(--border-subtle);border-radius:var(--radius-card);box-shadow:var(--shadow-overlay)}
.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-h b .fig{font-size:var(--fs-figure-sm)}
.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{position:relative;height:34px;border:0;border-radius:var(--radius-ctl);background:none;font:var(--fw-medium) var(--fs-figure-sm)/1 var(--font-figures);font-variant-numeric:tabular-nums;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.cal-grid button:not([disabled]):hover{background:var(--c-n100)}
/* 非本月的格子留空(不显示日期),和不可选日期(禁用灰、无悬停)不再混淆 */
.cal-grid .pad{height:34px}
.cal-grid button.today{box-shadow:inset 0 0 0 1px var(--border-control)}
.cal-grid button.mark::after{content:"";position:absolute;left:50%;bottom:4px;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);box-shadow:none}
.cal-grid button[aria-selected="true"]:hover{background:var(--primary-hover)}
.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-grid button[disabled]{color:var(--text-disabled);cursor:not-allowed;background:none;font-weight:var(--fw-regular)}
.cal-grid button[disabled].mark::after{background:var(--text-disabled)}
.cal-f{display:flex;align-items:center;gap:var(--sp-2);margin-top:var(--sp-2);padding-top:var(--sp-2);border-top:var(--line) 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}
.cal-f .time{width:72px;height:var(--control-h-sm);font-family:var(--font-figures);font-size:var(--fs-figure-sm);text-align:center}
/* ---- 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}
/* ---- B7 复选框、单选、开关(§8.2 批量勾选;Q4 开关) ---- */
.ck{appearance:none;flex:none;display:inline-grid;place-content:center;width:16px;height:16px;margin:0;border:var(--line) solid var(--border-control);border-radius:var(--radius-xs);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)}
@@ -241,9 +304,9 @@ select.input{appearance:none;padding-right:28px;cursor:pointer}
.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{background:var(--c-n100);border-color:var(--border-subtle);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{background:var(--c-n100)}
.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)}
@@ -253,132 +316,146 @@ select.input{appearance:none;padding-right:28px;cursor:pointer}
.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{display:flex;gap:var(--sp-3);align-items:flex-start;padding:var(--sp-3) var(--sp-4);border:var(--line) solid var(--border-control);border-radius:var(--radius-ctl);background:var(--bg-surface);cursor:pointer;transition:border-color var(--dur-fast) var(--ease),background-color var(--dur-fast) var(--ease)}
.choice-card:hover{background:var(--c-n50)}
.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) */
/* 开关(Q4):圆角 6 的矩形轨道 + 圆角 4 的方块滑钮;开为墨色;旁边写「开 / 关」;不用胶囊形 */
.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 .track{position:relative;flex:none;width:34px;height:20px;border:var(--line) solid var(--border-control);border-radius:var(--radius-tag);background:var(--bg-surface);transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.switch .track::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:var(--radius-xs);background:var(--c-muted);transition:transform var(--dur-fast) var(--ease),background-color var(--dur-fast) var(--ease)}
.switch:hover .track{border-color:var(--c-ink)}
.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 .st{min-width:2em;font-size:var(--fs-caption);color:var(--text-secondary)}
.switch[aria-checked="true"] .st{color:var(--text-primary)}
.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] .track{background:var(--c-n100);border-color:var(--border-subtle)}
.switch[disabled] .track::after{background:var(--border-subtle)}
.switch[disabled][aria-checked="true"] .track{background:var(--c-muted);border-color:var(--c-muted)}
.switch[disabled][aria-checked="true"] .track::after{background:var(--c-n100)}
.switch[disabled] .st{color:var(--text-disabled)}
/* ---- 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)}
/* ---- B8 筛选(分段)、标签页(§5.3 页签、§7.5 分类) ----
* v0.2 筛选:文字按钮一排,按下 = --c-n100 实底 + 加粗(与工作台项目卡片头一致),不再用墨底反白。 */
.seg{display:inline-flex;align-items:center;gap:2px}
.seg button{display:inline-flex;align-items:center;gap:6px;height:var(--control-h-sm);padding:0 10px;border:0;border-radius:var(--radius-ctl);background:none;font:var(--fs-body) var(--font-sans);color:var(--text-secondary);white-space:nowrap;cursor:pointer;transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.seg button:hover{background:var(--c-n100);color:var(--text-primary)}
.seg button[aria-pressed="true"]{background:var(--c-n100);color:var(--text-primary);font-weight:var(--fw-bold)}
.seg .fig{font-size:var(--fs-figure-sm);color:inherit}
.seg .sev .fig{font-size:var(--fs-figure-xs)}
.tabs{display:flex;gap:var(--sp-6);border-bottom:var(--line) 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)}
.tabs [role=tab] > .fig{font-size:var(--fs-figure-sm)}
.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)}
/* ---- B9 分页(Q4:当前页 --c-n100 实底 + 加粗,与筛选一致) ---- */
.pagination{display:flex;align-items:center;gap:2px;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 .fig{font-size:var(--fs-figure-xs)}
.pagination button.pg{min-width:var(--control-h-sm);height:var(--control-h-sm);padding:0 var(--sp-2);border:0;border-radius:var(--radius-ctl);background:none;font:var(--fw-medium) var(--fs-figure-sm)/1 var(--font-figures);font-variant-numeric:tabular-nums;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.pagination button.pg:hover{background:var(--c-n100)}
.pagination button.pg[aria-current="page"]{background:var(--c-n100);font-weight:var(--fw-bold)}
.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}
.pagination .jump .input{width:52px;height:var(--control-h-sm);text-align:center;font-family:var(--font-figures);font-size:var(--fs-figure-sm)}
/* ---- 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}
.toasts{position:fixed;left:50%;bottom:28px;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-ctl);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 .sep{width:1px;align-self:stretch;margin:2px 0;background:var(--on-dark-line)}
.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)}
/* ---- B11 对话框(§7.14:480 / 640,圆角 12;对话框里可以有自己的主按钮,Q12) ---- */
/* 对话框本身不滚动、不裁剪(overflow 可见),里面的下拉、日期浮层可以伸出对话框;内容比窗口高时由遮罩滚动 */
.scrim{position:fixed;inset:0;z-index:50;display:grid;overflow:auto;padding:var(--sp-8) var(--sp-4);background:var(--scrim)}
.dialog{position:relative;margin:auto;width:480px;max-width:100%;padding:var(--sp-6);background:var(--bg-surface);border-radius:var(--radius-card);box-shadow:var(--shadow-overlay)}
.dialog.wide{width:640px}
.dialog.static{position:static;max-width:none;box-shadow:var(--shadow-overlay)}
.dialog.static{margin:0;max-width:none} /* 陈列用:直接摆在页面里 */
.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}
.dialog table.data td{height:auto}
/* ---- B12 提醒补充(§7.12、§7.15) ---- */
.alert{align-items:flex-start}
/* ---- B12 提醒补充(§7.12、§7.15):右侧操作、副说明、错误编号 ---- */
.alert:has(> .btn,> .act){align-items:center}
.alert:has(> .btn,> .act) > .ic{margin-top:0}
.alert > div{flex:1;min-width:0}
.alert .act{margin-left:auto;flex:none;display:flex;gap:var(--sp-2);align-items:center}
.alert > p{flex:1}
.alert .act{margin-left:auto;flex:none;align-self:center;display:flex;gap:var(--sp-2);align-items:center}
.alert > .btn{flex:none;align-self: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}
.errno{font:var(--fw-medium) var(--fs-figure-xs)/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}
[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% + 8px);left:50%;transform:translateX(-50%);width:max-content;max-width:280px;padding:6px 10px;border-radius:var(--radius-ctl);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;pointer-events:none}
[data-tip].tip-below:hover::after,[data-tip].tip-below:focus-visible::after,[data-tip].tip-below.tip-on::after{bottom:auto;top:calc(100% + 8px)}
[data-tip].tip-start:hover::after,[data-tip].tip-start:focus-visible::after,[data-tip].tip-start.tip-on::after{left:0;transform:none}
[data-tip].tip-left:hover::after,[data-tip].tip-left:focus-visible::after,[data-tip].tip-left.tip-on::after{left:auto;right:0;transform:none}
/* ---- B14 空状态、骨架屏、进度(§7.11、§7.15) ---- */
/* ---- B14 空状态、骨架屏、进度(§7.11、§7.15;Q8 骨架屏静止,不做明暗起伏) ---- */
.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}
.empty.sm b{font-size:var(--fs-body);line-height:var(--lh-body)}
.empty.sm p{margin-bottom:0}
.empty.sm > .btn{margin-top:var(--sp-4)}
.skel{display:block;height:12px;border-radius:var(--radius-xs);background:var(--c-n100)}
.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{display:grid;grid-template-columns:24px minmax(0,1fr) 120px;gap:var(--sp-3);padding:var(--sp-3) 0;border-top:var(--line) 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{display:flex;align-items:center;gap:10px}
.progress .bar{flex:1;margin:0}
.progress .fig{font-weight:var(--fw-medium);min-width:3em;text-align:right}
.progress .fig{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)}
.kbd{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 6px;border-radius:var(--radius-tag);background:var(--c-n100);font:var(--fw-bold) var(--fs-caption)/1 var(--font-figures);color:var(--text-secondary);white-space:nowrap}
.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)}
.fig.sm{font-size:var(--fs-figure-sm)}
.fig.xs{font-size:var(--fs-figure-xs)}
/* 数字默认字号见 components.css(.fig = 15,小字里 13);这里补上 demo 层的 12px 小字容器 */
:is(.d-cap,.d-label,.d-rules,.filters-on,.ai-label,.hint-warn,.sign.sm,.sign-note,.dates .left,.stat p,.quota p,.lb-h) .fig{font-size:var(--fs-figure-xs)}
.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)}
/* 可移除的筛选标签:中性实底 */
.chip{display:inline-flex;align-items:center;gap:var(--sp-1);height:var(--tag-h);padding:0 3px 0 var(--sp-2);border-radius:var(--radius-tag);background:var(--c-n100);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-xs);background:none;color:var(--text-secondary);cursor:pointer}
.chip button:hover{background:var(--border-subtle);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-card);background:var(--bg-surface);padding:var(--sp-8) var(--sp-6);text-align:center;cursor:pointer;transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.drop:hover,.drop.is-over{border-color:var(--c-ink);background:var(--c-n50)}
.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{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:var(--line) solid var(--border-subtle);border-radius:var(--radius-card)}
.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 .ext{font:var(--fw-bold) 11px/1 var(--font-figures);background:var(--c-n100);border-radius:var(--radius-xs);padding:5px 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}
@@ -392,55 +469,74 @@ select.input{appearance:none;padding-right:28px;cursor:pointer}
.flowline.sm{font-size:var(--fs-caption)}
.flowline.sm .st i{width:8px;height:8px}
.flowline.sm .st.now i{border-width:2.5px}
/* 头像(列表、记录里的小头像) */
.av{display:inline-grid;place-items:center;flex:none;width:22px;height:22px;border-radius:50%;background:var(--c-n100);color:var(--text-primary);font-size:var(--fs-caption);font-weight:var(--fw-bold);line-height:1}
/* ---- 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)}
/* ---- B16 AI 产出块(§3.3、§7.16;Q2) ----
* 确认前:「AI」小标签 + 左侧 2px 蓝铅笔虚线;正文始终墨色。
* 确认后(.done):虚线去掉,下方出署名行(.sign:绿 ✓ + 姓名 + 时间)。左内边距保留,正文不跳动。 */
.ai-block{border-left:2px dashed var(--ai);padding:var(--sp-1) 0 var(--sp-1) var(--sp-3);color:var(--text-primary)}
.ai-block.done{border-left-color:transparent}
.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}
.flag.box{background:var(--c-n100);border-radius:var(--radius-tag);padding:0 6px;line-height:var(--tag-h)}
.cite-src.inline{display:inline;margin-left:6px;white-space:nowrap}
/* 署名行(§7.4、§7.16、Q2):确认后的人工签名,绿 ✓ + 姓名 + 时间 + 动作,可撤销。
* <div class="sign"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">11月3日 14:30</span><span>确认无误</span><button type="button" class="undo">撤销</button></div>
* 下方说明(忽略原因、确认说明)用 <p class="sign-note">;「已忽略」不是确认:.sign.ign 用辅助灰和「–」。 */
.sign .fig{font-weight:var(--fw-medium)}
.sign .undo{margin-left:var(--sp-1);padding:0;border:0;background:none;font:var(--fs-caption)/var(--lh-caption) var(--font-sans);color:var(--text-secondary);text-decoration:underline;text-decoration-color:var(--link-underline);text-underline-offset:3px;cursor:pointer}
.sign .undo:hover{color:var(--text-primary);text-decoration-color:var(--c-ink)}
.sign.ign{color:var(--text-secondary)}
.sign.sm{font-size:var(--fs-caption);margin-top:var(--sp-2)}
.sign.sm{font-size:var(--fs-caption);line-height:var(--lh-caption);margin-top:var(--sp-2)}
.sign-note{margin:2px 0 0;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.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}
.more-cites:hover{color:var(--ai-strong);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)}
/* ---- B17 卡片:卡片头、卡片底、统计、额度、关键日期(§7.9、§7.10) ----
* 整块卡片 .card:头用 .card-h,底用 .card-foot(贴边、带发丝线);
* 分区卡片 .card.flush:.card-h / .card-b / .card-f 见 components.css。 */
.card-h{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-3)}
.card-h .more{margin-left:auto;display:flex;gap:var(--sp-3);align-items:center}
.card.tight{padding:var(--sp-3) var(--card-x)}
.card-foot{display:flex;gap:var(--sp-4);align-items:center;margin:var(--sp-4) calc(-1 * var(--card-x)) calc(-1 * var(--card-y));padding:var(--sp-3) var(--card-x);border-top:var(--line) solid var(--border-subtle)}
.card.tight .card-foot{margin-bottom:calc(-1 * var(--sp-3))}
.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)}
/* 项目卡已删除(Q11:MVP 不做项目卡片视图) */
/* 统计(d5):一张卡片四格,竖向发丝线分隔;格子可以是 button(点击筛选,aria-pressed 时底部 2px 墨色边条) */
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));background:var(--bg-surface);border:var(--line) solid var(--border-subtle);border-radius:var(--radius-card);overflow:hidden}
.stats.c3{grid-template-columns:repeat(3,minmax(0,1fr))}
.stat{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0;padding:var(--sp-4) var(--card-x);border:0;border-left:var(--line) solid var(--border-subtle);background:none;font:inherit;color:inherit;text-align:left}
.stat:first-child{border-left:0}
button.stat{cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
button.stat:hover{background:var(--c-n50)}
button.stat:focus-visible{outline-offset:-2px;border-radius:0}
.stat::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--c-ink);transform:scaleY(0);transform-origin:bottom;transition:transform var(--dur-fast) var(--ease)}
.stat[aria-pressed="true"]::after{transform:scaleY(1)}
.stat[aria-pressed="true"] small{color:var(--text-primary);font-weight:var(--fw-bold)}
.stat small{display:block;font-size:var(--fs-body);line-height:var(--lh-body);color:var(--text-secondary)}
.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 .big.fatal{color:var(--sev-fatal)}
.stat .big.due{color:var(--warn-text)}
.stat .unit{color:var(--text-secondary);margin-left:4px;font-family:var(--font-figures)}
.stat > .ic{position:absolute;top:18px;right:var(--card-x);color:var(--text-secondary);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.stat:hover > .ic,.stat:focus-visible > .ic,.stat[aria-pressed="true"] > .ic{opacity:1}
.stat[aria-pressed="true"] > .ic{color:var(--text-primary)}
.stat p{margin: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 .q2 .bar{margin:6px 0 0}
.quota .big.warn{color:var(--warn-text)}
.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{display:grid;grid-template-columns:88px minmax(0,1fr) auto;gap:var(--sp-3);align-items:baseline;padding:var(--sp-2) 0;border-top:var(--line) solid var(--border-subtle)}
.dates li:first-child{border-top:0}
.dates li > .fig{font-weight:var(--fw-medium);font-size:15px}
.dates li > .fig{font-weight:var(--fw-medium);font-size:var(--fs-figure-sm)}
.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)}
@@ -454,71 +550,151 @@ select.input{appearance:none;padding-right:28px;cursor:pointer}
.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{display:flex;align-items:center;gap:4px;width:100%;padding:var(--sp-3) var(--sp-3) var(--sp-2);border:0;background:none;font:inherit;color:inherit;cursor:pointer;text-align:left;white-space:nowrap}
table.data th.sortable:first-child .th-sort{padding-left:var(--card-x)}
table.data th.sortable:last-child .th-sort{padding-right:var(--card-x)}
.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{position:sticky;top:calc(var(--topbar-h) + var(--sp-2));z-index:10;display:flex;align-items:center;gap:var(--sp-3);height:48px;padding:0 var(--sp-2) 0 var(--card-x);margin-bottom:var(--sp-2);background:var(--bg-surface);border:var(--line) solid var(--c-ink);border-radius:var(--radius-card)}
.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{background:var(--bg-surface);border:var(--line) solid var(--border-subtle);border-radius:var(--radius-card);overflow:hidden}
.list-box > .lb-h{display:flex;align-items:center;gap:var(--sp-3);min-height:40px;padding:var(--sp-1) var(--card-x);border-bottom:var(--line) solid var(--border-subtle);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{padding:var(--sp-3) var(--card-x);border-top:var(--line) 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)}
/* 当前条与勾选条(§7.18,v0.2 收尾):
* 当前条(键盘 J / K、点击定位到的那一条)= --c-n50 底 + 左侧 3px 墨色竖条:.item.sel / .finding.sel / tr.sel;
* 键盘操作时再加 2px 墨色内框:条目自身 :focus-visible、加 .focus,或列表容器 .kb-on / :focus-visible;
* 勾选条(批量处理的复选框)只有复选框是选中状态,不加底色和竖条;
* 致命行不加红竖线,竖条只表示「当前」,和级别无关。 */
.finding.sel{background:var(--c-n50);box-shadow:inset 3px 0 0 var(--c-ink)}
.finding{transition:background-color var(--dur-fast) var(--ease)}
:is(.item,.finding).focus,.item[tabindex]:focus-visible{box-shadow:inset 0 0 0 2px var(--focus-ring)}
:is(.item,.finding).sel.focus,.item.sel[tabindex]:focus-visible,:is(.kb-on,:focus-visible) > :is(.item,.finding).sel{box-shadow:inset 3px 0 0 var(--c-ink),inset 0 0 0 2px var(--focus-ring)}
table.data tbody tr.sel.focus td{box-shadow:inset 0 2px 0 var(--focus-ring),inset 0 -2px 0 var(--focus-ring)}
.item[tabindex]{cursor:pointer;outline:none;transition:background-color var(--dur-fast) var(--ease)}
.item[tabindex]:hover{background:var(--c-n50)}
.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)}
.item .upd .btn{height:24px;padding:0 var(--sp-2);font-size:var(--fs-caption);border-radius:var(--radius-tag)}
/* 问题行的紧凑版本(列表页) */
.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)}
/* 问题行带复选框(列表页批量处理只给「提示」) */
.finding .ckw{position:absolute;left:14px;top:calc(var(--sp-5) + 2px)}
.finding.has-ck{padding-left:calc(var(--card-x) + 24px)}
/* 行内说明(§7.4、U4):忽略原因、致命问题「确认无误」的说明,在问题行内展开,替换处理按钮;无底色,不在卡片里再套面板 */
.u4{margin-top:var(--sp-3)}
.u4 .field{max-width:560px}
.u4 .chips .btn{height:26px}
.u4 .row{margin-top:var(--sp-3);gap:var(--sp-2)}
.group-h{display:flex;align-items:center;gap:var(--sp-2);width:100%;padding:var(--sp-3) var(--card-x);border:0;border-top:var(--line) solid var(--border-subtle);background:none;color:var(--text-secondary);font:var(--fs-body) var(--font-sans);cursor:pointer;text-align:left;transition:background-color var(--dur-fast) var(--ease)}
.group-h:hover{background:var(--c-n50);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)}
/* ---- B19 对照布局:分类导航 + 列表 + 原文查看器(§5.3、§7.8;v0.2 收尾,原 .split) ----
* <div class="cmp" data-cmp>
* <section class="card flush cmp-list"><nav class="cats">…</nav><div class="lst">…</div></section>
* <div class="rsz" role="separator" tabindex="0" aria-label="调整原文查看器宽度(← → 键)"></div>
* <section class="viewer">…</section>
* </div>
* 尺寸:分类导航 176(1400 以下 152);列表最小 560;查看器默认 45%,最小 400,拖动或 ← → 调整(Demo.cmp)。
* 1280 宽时:列表 ≥ 560、查看器 ≤ 560,不横向滚动。整屏布局:.cmp 撑满剩余高度,三栏各自滚动。 */
.cmp{--cats-w:176px;--list-min:560px;--viewer-min:400px;flex:1;min-height:0;display:grid;grid-template-columns:minmax(var(--list-min),1fr) var(--gap-card) var(--vw,45%);padding:0 var(--pad-page) var(--sp-4)}
@media (max-width:1399px){.cmp{--cats-w:152px}}
.cmp-list{display:grid;grid-template-columns:var(--cats-w) minmax(0,1fr);min-height:0;overflow:hidden}
.cmp-list > .lst{position:relative;min-height:0;overflow:auto}
.cmp > .viewer{min-width:0}
/* 拖动条:平时不可见,悬停 / 拖动时出一条浅灰细线,键盘聚焦时墨色 */
.rsz{position:relative;cursor:col-resize;outline:0;touch-action:none}
.rsz::after{content:"";position:absolute;top:var(--sp-3);bottom:var(--sp-3);left:50%;width:2px;margin-left:-1px;border-radius:1px;background:transparent;transition:background-color var(--dur-fast) var(--ease)}
.rsz:hover::after,.rsz.on::after{background:var(--c-n300)}
.rsz:focus-visible::after{background:var(--focus-ring)}
body.dragging{cursor:col-resize;user-select:none}
/* 分类导航:在列表卡片左侧;当前分类 = --c-n100 实底 + 加粗(导航的「当前」,不是列表项的「当前条」) */
.cats{display:flex;flex-direction:column;gap:2px;border-right:var(--line) solid var(--border-subtle);padding:var(--sp-3) var(--sp-2);overflow:auto}
.cats button{display:flex;flex:none;width:100%;align-items:center;justify-content:space-between;height:var(--control-h-sm);padding:0 10px;border:0;background:none;border-radius:var(--radius-ctl);font:var(--fs-body) var(--font-sans);color:var(--text-primary);cursor:pointer;text-align:left;transition:background-color var(--dur-fast) var(--ease)}
.cats button:hover{background:var(--c-n100)}
/* 当前分类:和筛选一样用 --c-n100 实底 + 加粗(导航的「当前」,不是列表项的「选中」) */
.cats button[aria-current="true"]{background:var(--c-n100);font-weight:var(--fw-bold)}
.cats button .fig{font-size:var(--fs-figure-sm);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}
.cats hr{width:calc(100% - 20px);border:0;border-top:var(--line) solid var(--border-subtle);margin:var(--sp-2) 10px}
.cats .hint{margin:auto 0 0;padding:var(--sp-3) 10px 0;font-size:var(--fs-caption);line-height:26px;color:var(--text-secondary)}
.cats .hint .kbd{margin-right:2px}
/* 列表里的分组标题;.ai 是清单顶部的「待确认」分组(§7.6 v0.2:蓝色标题,不再用虚线框) */
.grp-h{display:flex;align-items:baseline;gap:var(--sp-2);padding:var(--sp-5) var(--card-x) var(--sp-2)}
.grp-h b{font-weight:var(--fw-bold)}
.grp-h .fig{font-weight:var(--fw-medium);color:var(--text-secondary)}
.grp-h.ai b,.grp-h.ai .fig{color:var(--ai)}
.grp-h p{margin:0;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.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{flex-wrap:nowrap;min-height:48px}
.viewer-bar .doc-name{flex:1;display:flex;gap:var(--sp-2);align-items:baseline;min-width:0}
.viewer-bar .pager{gap:var(--sp-1)}
.viewer-bar .pager > .fig{margin-right:var(--sp-2);font-size:var(--fs-figure-xs);color:var(--text-secondary)}
.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)}
.viewer .sheet{min-height:560px;margin:0 auto;width:calc(100% - 48px);max-width:640px;position:relative;background:var(--bg-surface);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{height:9px;background:var(--c-n100);margin:12px 0;border-radius:var(--radius-xs)}
.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}
/* ---- B20 项目行(§7.9,v0.2 收尾:工作台和列表页共用) ----
* 栅格对齐的列表(不是 table),整行可点:
* <div class="prow-head">项目 · 投标截止 · 状态 · 致命 · 下一步 · (空)</div>
* <ul class="prows"><li class="prow">
* <span class="pt"><a class="pname">…</a><span class="psub">…</span></span>
* <span class="dl"><b>剩 17 天</b><small>11月20日 09:30</small></span>
* <span class="st"><span class="tag">已检查</span>未就绪</span>
* <span class="c">致命标签或 —</span>
* <span><button class="nx"><span>下一步</span>›</button></span>
* <span class="more-cell"><button class="icon-btn sm">⋯</button></span></li></ul>
* 规则:「下一步」表头和内容左对齐,行悬停 / 聚焦时箭头右移 3px;「更多」单独一列常驻,平时辅助灰,行悬停变墨色;「致命」列居中。 */
:root{--prow-cols:minmax(0,1fr) 120px 120px 64px 140px var(--control-h-sm);--prow-gap:var(--sp-4)}
@media (max-width:1399px){:root{--prow-cols:minmax(0,1fr) 108px 108px 56px 132px var(--control-h-sm);--prow-gap:var(--sp-3)}}
.prow-head,.prow{display:grid;grid-template-columns:var(--prow-cols);column-gap:var(--prow-gap);align-items:center;padding:0 var(--card-x)}
.prow-head{padding-bottom:var(--sp-2);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
:is(.prow-head,.prow) > .c{justify-self:center;text-align:center}
.prows{list-style:none;margin:0;padding:0}
.prows > .empty{border-top:var(--line) solid var(--border-subtle)}
.prow{position:relative;min-height:var(--row-h);padding-top:10px;padding-bottom:10px;border-top:var(--line) solid var(--border-subtle);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.prow:hover,.prow:focus-within{background:var(--bg-hover)}
.prow:last-child{border-radius:0 0 var(--radius-card) var(--radius-card)}
.prow:has(.pname:focus-visible){outline:2px solid var(--focus-ring);outline-offset:-2px}
.prow .pt{min-width:0}
.pname{display:block;font-weight:var(--fw-bold);color:var(--text-primary);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;outline:0}
.prow .pname::after{content:"";position:absolute;inset:0}
.psub{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.prow .dl b{display:block;font-weight:var(--fw-regular);white-space:nowrap}
.prow .dl b.due-today{font-weight:var(--fw-bold)}
.prow .dl small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap}
.prow .st{display:flex;align-items:center;gap:var(--sp-2);white-space:nowrap}
.prow .st .hint-u{position:relative;z-index:1}
.prow.arch .pname,.prow.arch .dl b{color:var(--text-secondary);font-weight:var(--fw-regular)}
.dash{color:var(--text-secondary)}
.nx{position:relative;z-index:1;display:inline-flex;align-items:center;gap:var(--sp-1);height:var(--control-h-sm);padding:0;border:0;background:none;font:inherit;color:var(--text-primary);white-space:nowrap;cursor:pointer}
.nx span{text-decoration:underline;text-decoration-color:var(--link-underline);text-underline-offset:3px;transition:text-decoration-color var(--dur-fast) var(--ease)}
.nx:hover span{text-decoration-color:var(--c-ink)}
.nx .ic{color:var(--text-secondary);transition:transform var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.prow:hover .nx .ic,.prow:focus-within .nx .ic{transform:translateX(3px);color:var(--text-primary)}
.prow .more-cell{justify-self:end}
.prow .icon-btn{position:relative;z-index:1;color:var(--text-secondary)}
.prow:hover .icon-btn,.prow:focus-within .icon-btn,.prow .icon-btn[aria-expanded="true"]{color:var(--text-primary)}
/* 可排序的列名(栅格表头版的 .th-sort) */
.prow-head .srt{display:inline-flex;align-items:center;gap:4px;padding:0;border:0;background:none;font:inherit;color:inherit;cursor:pointer;white-space:nowrap}
.prow-head .srt:hover,.prow-head .srt[data-dir]{color:var(--text-primary)}
.prow-head .srt .ic{width:14px;height:14px;color:var(--text-disabled)}
.prow-head .srt[data-dir] .ic{color:var(--text-primary)}

View File

@@ -1,5 +1,8 @@
/* 标博士 · UI demo 公共脚本:图标、页面框架、轻提示、对话框、下拉、日期、开关、标签页、排序。
* 只演示交互,不连后端。正式实现里这些交互件由 Reka UI 打底(AR10)。 */
/* 标博士 · UI demo 公共脚本 v0.2:图标、页面框架、轻提示、对话框、下拉、浮动菜单、日期、开关、标签页、排序。
* 只演示交互,不连后端。正式实现里这些交互件由 Reka UI 打底(AR10)。
* v0.2:Demo.shell() 生成定稿样张 explore/workbench-d5.html 的框架(96px 墨色渐变侧栏 + 透明顶栏 + 内容区圆角)。
* 收尾(2026-11-03):日期输入 Demo.dateInput()(.dt[data-dt] 自动初始化)、字段错误 Demo.fieldError()、
* 浮动菜单支持禁用项、快捷键说明按页面传入(Demo.KEYS)、对照布局拖动宽度 Demo.cmp()(.cmp[data-cmp] 自动初始化)。 */
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
@@ -41,7 +44,14 @@
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"/>'
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"/>',
house: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
award: '<path d="m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526"/><circle cx="12" cy="8" r="6"/>',
wallet: '<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/>',
"alert-circle": '<circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>',
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>',
"check-circle": '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
eye: '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
function hydrate(root = document) {
@@ -61,35 +71,116 @@
<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("");
/* ---------- 产品页面框架 v0.2(组合页) ----------
* opt.nav:当前菜单 workbench / quals / quota
* opt.crumbs:[[文字, 链接], …],最后一项是当前页;顶栏左侧显示「公司 / 页面」
* opt.search:{ placeholder, onInput(q) };不传 onInput 时回车只出演示提示。Ctrl K 聚焦
* opt.quota:{ parse: [剩余, 总数], check: [剩余, 总数] };URL 带 ?quota=warn / ?quota=out 可预览 80% 和用完(Q9)
* opt.plan:free / pro / ent(也可用 ?plan=) */
const ORG = "南京某信息技术有限公司";
function shell(opt = {}) {
const qs = new URLSearchParams(location.search);
const nav = [["page-workbench.html", "house", "工作台", "workbench"], ["#", "award", "资质库", "quals"], ["#", "wallet", "额度", "quota"]]
.map(([h, i, t, k]) => `<a href="${h}" ${opt.nav === k ? 'aria-current="page"' : ""} ${h === "#" ? "data-demo-nav" : ""}>${icon(i)}<span>${t}</span></a>`).join("");
const crumbs = (opt.crumbs || []).map((c, i, a) => i < a.length - 1 && c[1] ? `<a href="${c[1]}" title="${esc(c[0])}">${esc(c[0])}</a><span aria-hidden="true">/</span>` : `<b title="${esc(c[0])}">${esc(c[0])}</b>${i < a.length - 1 ? '<span aria-hidden="true">/</span>' : ""}`).join("");
const PLAN = { free: "免费版", pro: "专业版", ent: "企业版" };
const plan = PLAN[qs.get("plan")] ? qs.get("plan") : (opt.plan || "pro");
const quota = Object.assign({ parse: [37, 50], check: [12, 20] }, opt.quota || {});
if (qs.get("quota") === "warn") quota.check = [3, 20];
if (qs.get("quota") === "out") quota.check = [0, 20];
const qItem = (name, [left, total]) => `<span class="q ${(total - left) / total >= 0.8 ? "warn" : ""}">${name}<span class="fig">${left}</span></span>`;
const out = [["解析", quota.parse], ["检查", quota.check]].filter(([, [l]]) => l <= 0).map(([n]) => n);
const qTip = `本期剩余额度:解析 ${quota.parse[0]} / ${quota.parse[1]} 次,检查 ${quota.check[0]} / ${quota.check[1]} 次` +
(out.length ? `。本期${out.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>`);
<a class="brand" href="page-workbench.html" aria-label="标博士 · 工作台"><img src="../logo/svg/logo-vertical-reverse.svg" alt=""></a>
<nav class="nav" aria-label="主导航">${nav}<span class="sep" role="separator"></span><a href="index.html">${icon("book")}<span>demo 目录</span></a></nav>
<div class="me"><span class="avatar" aria-hidden="true">王</span><b class="uname">王敏</b><span class="plan" data-plan="${plan}">${PLAN[plan]}</span></div></aside>`);
const main = $("main");
main.classList.add("main");
main.insertAdjacentHTML("afterbegin", `<header class="top"><div class="crumbs">${crumbs}</div>
main.insertAdjacentHTML("afterbegin", `<header class="top">
<div class="crumbs"><span class="org">${ORG}</span><span aria-hidden="true">/</span>${crumbs}</div>
<form class="top-search" role="search" data-gsearch>${icon("search")}<input type="search" placeholder="${esc((opt.search && opt.search.placeholder) || "搜索项目、采购人")}" aria-label="${esc((opt.search && opt.search.placeholder) || "搜索项目、采购人")}" aria-keyshortcuts="Control+K" data-gq><span class="kbd" aria-hidden="true">Ctrl K</span></form>
<span class="vsep" aria-hidden="true"></span>
<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>
<a class="quota-chip tip-below tip-left" href="#" data-tip="${qTip}" aria-label="${qTip}" data-demo-nav>${qItem("解析", quota.parse)}<span class="sep"></span>${qItem("检查", quota.check)}</a>
<button class="icon-btn tip-below tip-left" aria-label="通知(1 条未读)" data-tip="通知" data-notice>${icon("bell", "lg")}<span class="dot"></span></button>
</div></header>`);
const top = $(".top", main), gq = $("[data-gq]", main);
const onScroll = () => top.classList.toggle("scrolled", (main.scrollTop || window.scrollY) > 4);
addEventListener("scroll", onScroll, { passive: true }); main.addEventListener("scroll", onScroll, { passive: true }); onScroll();
$("[data-gsearch]", main).addEventListener("submit", (e) => { e.preventDefault(); if (!(opt.search && opt.search.onInput)) toast("(演示)全局搜索:这里只做了样式", { ok: false }); });
if (opt.search && opt.search.onInput) gq.addEventListener("input", () => opt.search.onInput(gq.value.trim()));
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); gq.focus(); }
else if (e.key === "Escape" && document.activeElement === gq) gq.blur();
});
document.addEventListener("click", (e) => { if (e.target.closest("[data-demo-nav]")) { e.preventDefault(); toast("(演示)这一页没有做", { ok: false }); } });
$("[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>` });
/* ---------- 浮动菜单:列表行的「更多」等(role=menu,↑ ↓ 移动,Esc 关闭,焦点回到触发按钮) ----------
* items:[{ label, sub, act, disabled }] 或 "-"(分隔线)
* sub:第二行说明,可以是 HTML(如扣点说明);
* disabled:不可用的原因(如「递交后可用」),该项变禁用灰、右侧写原因,点了不触发;
* onPick(act, item)。Demo.menuHTML(items) 生成同样的菜单项,供陈列页静态展示。 */
function menuHTML(items) {
return items.map((it, i) => it === "-" ? "<hr>" : `<button type="button" role="menuitem" class="${it.sub ? "two" : ""} ${it.active ? "is-active" : ""}" data-i="${i}"${it.disabled ? ' aria-disabled="true"' : ""}>${esc(it.label)}${it.sub ? `<small>${it.sub}</small>` : it.disabled ? `<small>${esc(it.disabled)}</small>` : ""}</button>`).join("");
}
let fm = null;
function closeFloatMenu(focusBack) {
if (!fm) return;
fm.el.remove(); fm.btn.setAttribute("aria-expanded", "false");
if (focusBack) fm.btn.focus();
fm = null;
}
function floatMenu(btn, items, onPick) {
if (fm && fm.btn === btn) return closeFloatMenu();
closeFloatMenu();
const el = document.createElement("div");
el.className = "menu float"; el.setAttribute("role", "menu");
el.innerHTML = menuHTML(items);
document.body.appendChild(el);
const r = btn.getBoundingClientRect();
const below = r.bottom + 4 + el.offsetHeight < innerHeight;
el.style.top = `${below ? r.bottom + 4 : r.top - 4 - el.offsetHeight}px`;
el.style.left = `${Math.max(8, r.right - el.offsetWidth)}px`;
btn.setAttribute("aria-expanded", "true");
fm = { el, btn };
el.addEventListener("click", (e) => { const b = e.target.closest("[data-i]"); if (!b) return; e.stopPropagation(); const it = items[+b.dataset.i]; if (it.disabled) return; closeFloatMenu(true); if (onPick) onPick(it.act || it.label, it); });
el.addEventListener("keydown", (e) => {
const its = $$("[role=menuitem]", el), k = its.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); its[(k + 1) % its.length].focus(); }
if (e.key === "ArrowUp") { e.preventDefault(); its[(k - 1 + its.length) % its.length].focus(); }
if (e.key === "Escape") { e.stopPropagation(); closeFloatMenu(true); }
if (e.key === "Tab") closeFloatMenu();
});
$("[role=menuitem]", el).focus();
}
document.addEventListener("click", (e) => { if (fm && !fm.el.contains(e.target) && !fm.btn.contains(e.target)) closeFloatMenu(); });
addEventListener("scroll", () => closeFloatMenu(), { passive: true, capture: true });
addEventListener("resize", () => closeFloatMenu());
/* ---------- 快捷键说明(§8.2):按页面传入,只列本页能用的键 ----------
* Demo.keysDialog(Demo.KEYS.reading) 读标:J / K、Enter、?
* Demo.keysDialog(Demo.KEYS.check) 查标(问题列表):J / K、O / Enter、1 / 2 / 3、X、U、?
* 也可以传自己的 [[键的 HTML, 说明], …];Demo.keysTable(list) 生成同样的表格供静态展示。 */
const kb = (k) => `<span class="kbd">${k}</span>`;
const KEY = {
jk: [`${kb("J")} / ${kb("K")}`, "下一条 / 上一条"],
enter: [kb("Enter"), "在原文查看器里打开当前条的出处"],
open: [`${kb("O")} 或 ${kb("Enter")}`, "在原文查看器里打开当前出处"],
act: [`${kb("1")} ${kb("2")} ${kb("3")}`, "确认无误 / 已修改 / 忽略(忽略、致命的确认无误在行内写说明)"],
x: [kb("X"), "勾选当前条(只限「提示」)"],
undo: [kb("U"), "撤销刚才的处理"],
help: [kb("?"), "显示本说明"]
};
const KEYS = { reading: [KEY.jk, KEY.enter, KEY.help], check: [KEY.jk, KEY.open, KEY.act, KEY.x, KEY.undo, KEY.help] };
const keysTable = (list) => `<table class="data keys"><tbody>${list.map(([k, t]) => `<tr><td class="nw">${k}</td><td>${t}</td></tr>`).join("")}</tbody></table>`;
function keysDialog(list = KEYS.check) {
dialog({ title: "快捷键", cancel: null, ok: "知道了", body: keysTable(list) });
}
/* ---------- 轻提示:4 秒消失,悬停暂停 ---------- */
@@ -119,7 +210,12 @@
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 === "Escape") {
e.stopPropagation();
const op = $(".pop.open", s); // 先收起对话框里展开的下拉 / 日期选择器
if (op) { closePops(); ($("input, .select", op) || op).focus(); return; }
close();
}
if (e.key === "Tab") { // 焦点锁定在对话框内
const f = $$("button:not([disabled]), input:not([disabled]), textarea, select, a[href]", s);
if (!f.length) return;
@@ -172,54 +268,199 @@
menu.addEventListener("click", (e) => { const o = e.target.closest('[role=option], [role=menuitem]'); if (o && o.getAttribute("aria-disabled") !== "true") { e.stopPropagation(); pick(o); } });
}
/* ---------- 日期选择 ---------- */
/* ---------- 日期选择(§7.22) ----------
* 周一开始;非本月的格子留空;不可选日期(opt.min 之前)禁用灰、无悬停;今天描边;关键日期(opt.marks)下方小圆点。
* 日期键统一写成不补零的「2026-11-3」。 */
const WD = ["一", "二", "三", "四", "五", "六", "日"];
const TODAY = new Date(2026, 10, 3), TODAY_KEY = "2026-11-3"; // 演示里的「今天」
const pad2 = (n) => String(n).padStart(2, "0");
const fig = (v) => `<span class="fig">${v}</span>`;
const keyOf = (d) => `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
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();
const start = (new Date(y, m, 1).getDay() + 6) % 7, days = new Date(y, m + 1, 0).getDate();
let cells = "";
for (let i = 0; i < 42; i++) {
for (let i = 0, n = Math.ceil((start + days) / 7) * 7; i < n; 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;
if (d < 1 || d > days) { cells += '<span class="pad" aria-hidden="true"></span>'; continue; }
const date = new Date(y, m, d), key = keyOf(date);
const cls = [key === (opt.today || TODAY_KEY) ? "today" : "", (opt.marks || []).includes(key) ? "mark" : ""].join(" ").trim();
cells += `<button type="button" class="${cls}" data-d="${key}" aria-selected="${key === sel}" ${opt.min && date < opt.min ? "disabled" : ""} tabindex="-1">${d}</button>`;
}
const note = opt.note !== undefined ? opt.note : (opt.marks || []).length ? "圆点是本项目的关键日期" : "";
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>`;
<div class="cal-f">${opt.time ? `时间 <input class="input time" value="${opt.time}" aria-label="时间(24 小时制)" inputmode="numeric">` : `<span>${note}</span>`}<button type="button" class="btn sm text" data-today>今天</button></div>`;
}
/* 只有日历、没有输入框的陈列(.pop.d-open[data-date]) */
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 cal = $(".cal", p);
let [y, m] = (p.dataset.month || p.dataset.value || "2026-11").split("-").map(Number); m -= 1;
let sel = p.dataset.value ? keyOf(parseDT(p.dataset.value, false).dt) : "";
const opt = { marks: (p.dataset.marks || "").split(",").filter(Boolean).map((k) => keyOf(parseDT(k, false).dt)), time: p.dataset.time, min: p.dataset.min ? parseDT(p.dataset.min, false).dt : 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; }
if (b.dataset.today) { y = 2026; m = 10; sel = TODAY_KEY; }
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) }));
});
}
/* ---------- 日期输入(Q10):.dt[data-dt] ----------
* <div class="field"><label for="x">投标截止 <small>(必填)</small></label>
* <div class="dt" data-dt data-time="09:30" data-left data-req="请填写投标截止时间">
* <input class="input" id="x" placeholder="例如:2026-11-20 09:30" autocomplete="off">
* <button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button>
* <div class="cal" role="dialog" aria-label="选择日期" hidden></div></div>
* <span class="msg" data-dt-msg>24 小时制</span></div>
* data-time:带时间(24 小时制,选择器底部填时间);data-left:下方写「· 剩 N 天」;data-req:必填,空着时的错误说明;
* data-marks / data-min / data-note:关键日期圆点 / 最早可选日期 / 选择器底部的圆点说明;类名加 d-open:选择器常开(陈列用)。
* 可直接键入,接受「2026/11/20 9:30」「2026.11.20 9:30」「2026年11月20日」等写法,离开时整理成「2026-11-20 09:30」,
* 下方 [data-dt-msg] 显示界面写法「11月20日 09:30 · 剩 17 天」。没碰过的字段离开时不报错;报错之后边输入边复查。
* 每次整理后在 .dt 上派发 dt-change(detail:{ ok, empty, val, dt, days });p.dt.commit({ required }) 供提交时调用。 */
function parseDT(raw, withTime) {
const s = String(raw || "").trim().replace(/[\/.年月]/g, "-").replace(/日/g, "").replace(/:/g, ":").replace(/\s+/g, " ");
const eg = withTime ? "2026-11-20 09:30" : "2026-11-20";
const m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?: ?(\d{1,2}):(\d{1,2}))?$/);
if (!m) return { msg: `请按「${eg}」的格式填写` };
const [, y, mo, d, hh, mi] = m.map((v) => v === undefined ? v : +v);
const dt = new Date(y, mo - 1, d);
if (dt.getMonth() !== mo - 1 || dt.getDate() !== d) return { msg: `${y} 年 ${mo} 月没有 ${d} 日,请核对日期` };
if (withTime && hh === undefined) return { msg: `请补上时间,例如「${y}-${pad2(mo)}-${pad2(d)} 09:30」` };
if (withTime && hh > 23) return { msg: "时间用 24 小时制,小时在 00–23 之间" };
if (withTime && mi > 59) return { msg: "分钟在 00–59 之间" };
const time = withTime ? `${pad2(hh)}:${pad2(mi)}` : "";
const val = `${y}-${pad2(mo)}-${pad2(d)}` + (withTime ? ` ${time}` : "");
const days = Math.round((dt - TODAY) / 864e5);
const disp = `${y !== TODAY.getFullYear() ? fig(y) + "年" : ""}${fig(mo)}月${fig(d)}日` + (withTime ? ` ${fig(time)}` : "");
return { val, dt, days, disp, key: keyOf(dt), time };
}
/* 字段错误(§7.13):赭色边框 + 下方「!」说明;其他说明和提醒暂时隐藏。msg 为空时清除。返回是否无错 */
let uid = 0;
function fieldError(f, msg) {
if (!f) return !msg;
f.classList.toggle("err", !!msg);
const inp = $("input:not([type=checkbox]):not([type=radio]), textarea", f); if (inp) inp.toggleAttribute("aria-invalid", !!msg);
let m = $(":scope > .msg.e", f);
if (msg) {
if (!m) { m = document.createElement("span"); m.className = "msg e"; m.id = (f.id || "fld" + ++uid) + "-err"; f.appendChild(m); }
m.innerHTML = `<span class="ico w" aria-hidden="true">!</span>${msg}`;
$$(":scope > .msg:not(.e), :scope > .hint-warn", f).forEach((x) => { x.hidden = true; });
if (inp) inp.setAttribute("aria-describedby", m.id);
} else {
if (m) m.remove();
$$(":scope > .msg, :scope > .hint-warn:not([data-off])", f).forEach((x) => { x.hidden = false; });
const d = $(":scope > .msg[id]", f); if (inp) { if (d) inp.setAttribute("aria-describedby", d.id); else inp.removeAttribute("aria-describedby"); }
}
return !msg;
}
function dateInput(p) {
if (p.dataset.ready) return;
p.dataset.ready = 1; p.classList.add("pop");
const inp = $("input", p), btn = $(":scope > .icon-btn", p), cal = $(".cal", p), f = p.closest(".field");
const msg = f && $("[data-dt-msg]", f), def = msg ? msg.innerHTML : "";
if (msg && !msg.id) msg.id = (inp.id || "dt" + ++uid) + "-msg";
if (msg) inp.setAttribute("aria-describedby", msg.id);
const withTime = p.dataset.time !== undefined, still = p.classList.contains("d-open");
const opt = { marks: (p.dataset.marks || "").split(",").filter(Boolean).map((k) => keyOf(parseDT(k, false).dt)), min: p.dataset.min ? parseDT(p.dataset.min, false).dt : null, note: p.dataset.note };
let y = TODAY.getFullYear(), m = TODAY.getMonth(), sel = "", time = p.dataset.time || "09:00", dirty = false;
const left = (r) => p.dataset.left === undefined ? "" : r.days > 0 ? ` · 剩 ${fig(r.days)} 天` : r.days === 0 ? " · 今天截止" : " · 已截止";
const sync = () => { const r = parseDT(inp.value, withTime); if (r.val) { sel = r.key; y = r.dt.getFullYear(); m = r.dt.getMonth(); if (r.time) time = r.time; } };
const render = () => { cal.innerHTML = calHTML(y, m, sel, { ...opt, time: withTime ? time : undefined }); };
function commit(o = {}) {
if (inp.disabled) { fieldError(f, ""); return true; }
const v = inp.value.trim(), r = v ? parseDT(v, withTime) : {};
let err = "";
if (!v) { err = o.required && p.dataset.req ? p.dataset.req : ""; if (msg) msg.innerHTML = def; }
else if (r.msg) err = r.msg;
else { inp.value = r.val; if (msg) msg.innerHTML = r.disp + left(r); }
fieldError(f, err);
p.dispatchEvent(new CustomEvent("dt-change", { bubbles: true, detail: { ok: !err, empty: !v, ...r } }));
return !err;
}
p.dt = { commit, parse: () => parseDT(inp.value, withTime) };
const close = (focus) => { p.classList.remove("open", "up"); cal.hidden = true; btn.setAttribute("aria-expanded", "false"); if (focus) inp.focus(); };
const open = () => {
closePops(p); closeFloatMenu();
sync(); render(); p.classList.add("open"); cal.hidden = false; btn.setAttribute("aria-expanded", "true");
const r = p.getBoundingClientRect();
p.classList.toggle("up", r.bottom + cal.offsetHeight + 8 > innerHeight && r.top > cal.offsetHeight + 8);
($("[aria-selected=true]", cal) || $(".today", cal) || $(".cal-grid button:not([disabled])", cal)).focus();
};
const write = () => { if (!sel) return; const [a, b, c] = sel.split("-"); inp.value = `${a}-${pad2(b)}-${pad2(c)}` + (withTime ? ` ${time}` : ""); dirty = true; commit(); };
if (still) { sync(); render(); } else cal.hidden = true;
if (inp.value.trim()) commit();
btn.addEventListener("click", (e) => { e.stopPropagation(); if (still) return; p.classList.contains("open") ? close(true) : open(); });
cal.addEventListener("click", (e) => {
e.stopPropagation();
const b = e.target.closest("button"); if (!b || b.disabled) 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 = TODAY.getFullYear(); m = TODAY.getMonth(); sel = TODAY_KEY; }
else if (b.dataset.d) sel = b.dataset.d; else return;
render(); write();
if (!still && !withTime) close(true);
else ($(`[data-d="${sel}"]`, cal) || b).focus();
});
cal.addEventListener("input", (e) => {
if (!e.target.classList.contains("time")) return;
const t = e.target.value.trim().replace(":", ":").match(/^(\d{1,2}):(\d{2})$/);
if (t && +t[1] < 24 && +t[2] < 60) { time = `${pad2(t[1])}:${t[2]}`; write(); }
});
cal.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !still) { e.stopPropagation(); close(true); return; }
if (e.key === "Enter" && e.target.classList.contains("time")) { e.preventDefault(); if (!still) close(true); return; }
const cells = $$(".cal-grid button:not([disabled])", cal), i = cells.indexOf(document.activeElement);
const d = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
if (i >= 0 && d) { e.preventDefault(); (cells[i + d] || cells[i]).focus(); }
});
inp.addEventListener("keydown", (e) => { if (e.key === "ArrowDown" && e.altKey) { e.preventDefault(); open(); } });
inp.addEventListener("input", () => {
dirty = true;
if (f && f.classList.contains("err")) { const v = inp.value.trim(); if (v ? !parseDT(v, withTime).msg : !p.dataset.req) fieldError(f, ""); }
});
inp.addEventListener("blur", (e) => {
if (e.relatedTarget && p.contains(e.relatedTarget)) return; // 焦点进了日期选择器
if (dirty || (f && f.classList.contains("err"))) commit({ required: true });
});
}
/* ---------- 对照布局(§5.3):拖动或用 ← → 调整原文查看器宽度;清单不窄于 --list-min,查看器不窄于 --viewer-min ---------- */
function cmp(el) {
if (el.dataset.ready) return;
el.dataset.ready = 1;
const r = $(":scope > .rsz", el); if (!r) return;
const num = (v) => parseFloat(getComputedStyle(el).getPropertyValue(v)) || 0;
const box = () => { const b = el.getBoundingClientRect(), cs = getComputedStyle(el); return { right: b.right - parseFloat(cs.paddingRight), w: b.width - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight), gap: r.offsetWidth }; };
const cur = () => $(":scope > .viewer", el).offsetWidth;
function setW(px) {
const { w, gap } = box(), max = w - gap - num("--list-min");
px = Math.round(Math.max(num("--viewer-min"), Math.min(max, px)));
el.style.setProperty("--vw", px + "px");
r.setAttribute("aria-valuenow", px);
}
r.setAttribute("aria-valuemin", num("--viewer-min"));
r.addEventListener("pointerdown", (e) => {
e.preventDefault(); r.setPointerCapture(e.pointerId); r.classList.add("on"); document.body.classList.add("dragging");
const mv = (ev) => { const { right, gap } = box(); setW(right - ev.clientX - gap / 2); };
const up = () => { r.classList.remove("on"); document.body.classList.remove("dragging"); r.removeEventListener("pointermove", mv); r.removeEventListener("pointerup", up); };
r.addEventListener("pointermove", mv); r.addEventListener("pointerup", up);
});
r.addEventListener("keydown", (e) => { if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); setW(cur() + (e.key === "ArrowLeft" ? 32 : -32)); } });
addEventListener("resize", () => { if (el.style.getPropertyValue("--vw")) setW(cur()); });
}
/* ---------- 开关、分段、标签页、半选、排序、加载按钮 ---------- */
function init(root = document) {
$$(".pop[data-select], .pop[data-menu]", root).forEach(initSelect);
$$(".pop[data-date]", root).forEach(initDate);
$$(".dt[data-dt]", root).forEach(dateInput);
$$(".cmp[data-cmp]", root).forEach(cmp);
$$(".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" ? "开" : "关"; };
@@ -270,6 +511,6 @@
});
}
window.Demo = { $, $$, esc, icon, hydrate, demoTop, shell, toast, dialog, init, sortable, keysDialog, calHTML };
window.Demo = { $, $$, esc, icon, hydrate, demoTop, shell, toast, dialog, init, sortable, keysDialog, keysTable, KEYS, calHTML, parseDT, fieldError, dateInput, cmp, floatMenu, closeFloatMenu, menuHTML };
document.addEventListener("DOMContentLoaded", () => { hydrate(); init(); });
})();

View File

@@ -1,4 +1,12 @@
<!doctype html>
<!--
陈列页 · 表单(v0.2)
新建项目、资质录入、问题处理时的说明(忽略原因 / 致命问题确认说明 U4)。
日期按 Q10:输入框写「2026-11-20 09:30」,可直接键入,也可用日期选择器;界面显示「11月20日 09:30」。
日期输入、字段错误、署名行、行内说明都是共用组件(demo.js 的 Demo.dateInput / Demo.fieldError,demo.css 的 .dt / .sign / .u4)。
主按钮按 Q12:表单一个、对话框一个;问题处理面板里的提交按钮是次按钮(§7.1「问题处理三按钮」)。
?state=errors 直接显示新建项目的错误汇总(截图用)。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -10,11 +18,24 @@
<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}
.card-b.form-b{padding-top:var(--sp-4);padding-bottom:var(--sp-6)}
.card-f .cost{margin-right:auto}
.card-f .cost .hint-u{margin-left:var(--sp-1)}
.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}
.field.err .drop{border-color:var(--warn)}
/* 错误汇总:卡片头下方的一条,不在卡片里再套框 */
.err-sum{margin:0;border-width:var(--line) 0;border-radius:0;outline-offset:-2px}
.err-sum .ico{margin-top:1px}
.err-sum li{color:var(--text-primary)}
.st6{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:var(--sp-5)}
.st3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-5);align-items:start}
.d-box + .d-box,.d-box + .d-grid{margin-top:var(--gap-card)}
.d-box > .d-label{margin-bottom:var(--sp-4)}
.dialog .form{margin-top:var(--sp-1)}
</style>
</head>
<body class="demo">
@@ -22,50 +43,56 @@
<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>
<p>必填写「(必填)」,错误用赭色;离开输入框时校验,提交时汇总。日期输入「2026-11-20 09:30」,显示「11月20日 09:30」。</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-sec-h"><span class="no">01</span><h2>新建项目</h2><span class="ref">§7.13 · §8.3 · §9.3 · Q10 · Q12</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>
<form class="card flush" novalidate data-new aria-labelledby="h-new">
<div class="card-h"><h3 id="h-new">新建项目</h3></div>
<div class="alert warn err-sum" role="alert" tabindex="-1" data-errsum hidden><span class="ico w" aria-hidden="true">!</span><div><p><b>有 <span data-errn>2</span> 处需要修改</b></p><ul data-errlist></ul></div></div>
<div class="card-b form-b">
<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>解析后自动补全,可以先只填名称</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="例如:某市政务服务中心智慧大厅建设项目" autocomplete="off"><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 id="l-way">采购方式</label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="l-way"><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><li class="grp">招标投标</li><li role="option" data-value="招标-公开">公开招标</li><li role="option" data-value="招标-邀请">邀请招标</li></ul></div></div>
<div class="field" id="f-due"><label for="n-due">投标截止 <small>(必填)</small></label>
<div class="dt" data-dt data-time="09:30" data-left data-req="请填写投标截止时间,例如 2026-11-20 09:30">
<input class="input" id="n-due" placeholder="例如:2026-11-20 09:30" autocomplete="off">
<button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button>
<div class="cal" role="dialog" aria-label="选择投标截止日期" hidden></div>
</div>
<span class="msg" id="n-due-msg" data-dt-msg>24 小时制</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>
<fieldset class="form-sec">
<legend>招标文件<small>PDF、Word 或扫描件;补遗以后可以追加</small></legend>
<div class="field fill" id="f-file"><span class="vh" id="l-file">招标文件</span>
<div class="drop" tabindex="0" role="button" aria-labelledby="l-file" aria-describedby="drop-sub" data-drop><i data-ic="upload" class="lg"></i><b>拖入或点击上传</b><span id="drop-sub">单个文件不超过 200 MB</span></div>
<div data-files></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>
<div class="card-f">
<span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次<span class="hint-u tip-start" tabindex="0" data-tip="解析失败、超时或系统异常不扣额度,已扣的自动退回">失败不扣</span></span>
<button type="button" class="btn" data-cancel>取消</button>
<button type="submit" class="btn primary swap" data-submit><span class="idle">创建并开始解析</span><span class="busy">正在创建…</span></button>
</div>
</form>
@@ -74,19 +101,20 @@
<div class="d-box">
<h3>试一试</h3>
<ol class="d-rules" style="border:0;padding-top:0;margin-top:0">
<li>什么都不填,直接点「创建并开始解析」:顶部汇总 + 每个字段下方说明,点汇总里的链接跳到字段。</li>
<li>在项目名称里输入再清空,离开输入框时就会校验。</li>
<li>直接点「创建并开始解析」:顶部汇总,点汇总里的字段名跳过去。<a class="lnk" href="?state=errors#s-new">直接看汇总</a></li>
<li>投标截止键入「2026/11/20 9:30」,离开后自动整理成标准格式;键入「2026-11-20 25:00」看错误。</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>
<li>必填写「(必填)」,不用红星;选填多时反过来写「(选填)」。</li>
<li>错误说明写怎么改,不写「格式错误」。</li>
<li>没碰过的字段离开时不报错;报错之后边输入边复查,改对立即消失。</li>
<li>会扣点的提交按钮,左侧写扣多少、还剩多少。</li>
<li>扩展名和内容不符只提醒,不拦截。</li>
</ul>
</div>
</div>
@@ -95,121 +123,138 @@
<!-- 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-sec-h"><span class="no">02</span><h2>状态对照</h2><span class="ref">§7.13 · §7.22 · Q10</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>
<span class="d-label">文本输入</span>
<div class="st6">
<div class="d-state"><small>正常</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226IS0458" aria-label="证书编号(正常)"></div></div>
<div class="d-state"><small>悬停</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input is-hover" value="01226IS0458" aria-label="证书编号(悬停)"></div></div>
<div class="d-state"><small>聚焦</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input is-focus" value="01226IS04" aria-label="证书编号(聚焦)"></div></div>
<div class="d-state"><small>错误:要改了才能保存</small><div class="field err"><label>证书编号 <small>(必填)</small></label><input class="input" value="" aria-invalid="true" aria-label="证书编号(错误)"><span class="msg"><span class="ico w" aria-hidden="true">!</span>请填写证书编号</span></div></div>
<div class="d-state"><small>提醒:可以保存</small><div class="field"><label>有效期至</label><input class="input" value="2026-11-15" style="font-family:var(--font-figures);font-size:var(--fs-figure-sm)" aria-label="有效期至(提醒)"><span class="hint-warn"><span class="ico w" aria-hidden="true">!</span><span>早于投标截止 11月20日</span></span></div></div>
<div class="d-state"><small>禁用</small><div class="field dis"><label>证书编号</label><input class="input" value="01226IS0458" disabled aria-label="证书编号(禁用)"><span class="msg">已被已递交项目引用</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 class="d-box">
<span class="d-label">日期时间输入(Q10):输入框写「2026-11-20 09:30」,可直接键入常见写法(左一键入的是「2026/11/20 9:30」),离开时整理;下方显示「11月20日 09:30 · 剩 N 天」</span>
<div class="st3">
<div class="d-state"><small>已填写</small><div class="field"><label for="s-dt1">投标截止 <small>(必填)</small></label>
<div class="dt" data-dt data-time="09:30" data-left><input class="input" id="s-dt1" value="2026/11/20 9:30" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择日期" hidden></div></div>
<span class="msg" data-dt-msg>24 小时制</span></div></div>
<div class="d-state"><small>选择器展开:选日期,底部填时间</small><div class="field"><label for="s-dt2">投标截止 <small>(必填)</small></label>
<div class="dt d-open" data-dt data-time="09:30" data-marks="2026-11-20" data-min="2026-11-03"><input class="input is-focus" id="s-dt2" value="2026-11-20 09:30" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="日期选择器" aria-haspopup="dialog" aria-expanded="true" tabindex="-1"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择日期"></div></div></div></div>
<div class="d-state"><small>错误</small><div class="field err"><label for="s-dt3">投标截止 <small>(必填)</small></label>
<div class="dt" data-dt data-time="09:30" data-left><input class="input" id="s-dt3" value="2026-11-20 25:00" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择日期" hidden></div></div>
<span class="msg" data-dt-msg>24 小时制</span></div></div>
</div>
<div class="card">
<div class="card-h"><h3>提交中</h3><span class="more cap muted">整表禁用,按钮换成「正在…」,宽度不变</span></div>
</div>
<div class="d-grid c2">
<div class="card flush">
<div class="card-h"><h3>提交前</h3></div>
<div class="card-b"><div class="form">
<div class="field"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系" aria-label="名称"></div>
<div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877" aria-label="证书编号"></div>
</div></div>
<div class="card-f"><span class="cost" style="margin-right:auto">不扣额度</span><button class="btn">取消</button><button class="btn primary swap" data-loading="已保存资质"><span class="idle">保存资质</span><span class="busy">正在保存…</span></button></div>
</div>
<div class="card flush">
<div class="card-h"><h3>提交中</h3><span class="cap muted" style="margin-left:auto">整表禁用,按钮宽度不变</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>
<div class="card-b"><div class="form">
<div class="field dis"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系" disabled aria-label="名称"></div>
<div class="field dis"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877" disabled aria-label="证书编号"></div>
</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 class="card-f"><span class="cost" style="margin-right:auto">不扣额度</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-sec-h"><span class="no">03</span><h2>资质录入</h2><span class="ref">最小资质库 · 红线 5 · Q12</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>
<h2 id="q-t">添加资质</h2><button type="button" class="icon-btn sm x" aria-label="关闭"><i data-ic="x"></i></button>
<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>
<div class="field"><label id="l-qtype">类型 <small>(必填)</small></label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="l-qtype"><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 class="field"><label for="q-from">发证日期</label>
<div class="dt" data-dt><input class="input" id="q-from" value="2023-11-16" placeholder="例如:2023-11-16" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择发证日期" hidden></div></div></div>
<div class="field" id="f-qexp"><label for="q-exp">有效期至 <small>(必填)</small></label>
<div class="dt" data-dt data-marks="2026-11-20" data-note="圆点是进行中项目的投标截止日" data-req="请填写有效期,长期有效请打开下方开关"><input class="input" id="q-exp" value="2026-11-15" placeholder="例如:2026-11-15" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择有效期" hidden></div></div>
<span class="hint-warn" data-exp-hint><span class="ico w" aria-hidden="true">!</span><span>早于「智慧大厅建设项目」的投标截止 <span class="fig">11</span>月<span class="fig">20</span>日</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" data-long><span class="track"></span><span class="st"></span></button></div></div>
<div class="field span2"><label>证书扫描件 <small>(可以稍后补录)</small></label>
<div class="file-row"><span class="ext">PDF</span><div><b>ISO27001 证书 2023.pdf</b><span class="cap muted"><span class="fig">2</span> 页 · <span class="fig">1.2</span> 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 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>
<h3>真实对话框 <small>可以点</small></h3>
<button class="btn" data-qadd><i data-ic="plus"></i>添加资质</button>
<p class="cap muted" style="margin:var(--sp-3) 0 0">空表单直接保存看错误;附件可以稍后补录,列表里显示「待补录」,不生成内容。</p>
</div>
<div class="d-box">
<h3>提醒和错误的区别</h3>
<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>
<li><b>错误</b>:赭色边框,改了才能保存,例如漏填证书编号。</li>
<li><b>提醒</b>:只有赭色小字,边框不变,可以保存,例如证书在投标截止前到期。</li>
<li>到期按每个项目的投标截止日判断,不按今天。</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 04 忽略并注明原因 -->
<!-- 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="d-sec-h"><span class="no">04</span><h2>忽略原因 · 致命问题的确认说明</h2><span class="ref">§7.4 · §7.13 · U4 · Q2</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>
<p class="f-text">开标一览表 287.60 万元,分项价格表合计 286.80 万元,相差 0.80 万元。招标文件规定以开标一览表为准,不作无效处理,但会影响价格评审。</p>
<div class="u4" data-u4="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 class="chips" role="group" aria-label="常用原因"><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" aria-describedby="ig1-msg">已核对</textarea>
<div class="msg-row"><span class="msg" id="ig1-msg" data-def="报告和处理记录里会显示这段原因"><span class="ico w" aria-hidden="true">!</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 class="row"><button type="button" class="btn sm" data-ok>忽略这一项</button><button type="button" class="btn sm text" data-x>取消</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>
<div class="cite"><a class="cite-src" href="#" data-demo>报价册 · 第 <span class="fig">1</span> 页</a><q>投标总价(小写):<mark>2876000.00 元</mark></q></div>
<div class="cite"><a class="cite-src" href="#" data-demo>报价册 · 第 <span class="fig">3</span> 页</a><q><mark>合计:2868000.00 元</mark></q></div>
</aside>
</article>
<article class="finding is-fatal">
<article class="finding">
<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">
<p class="f-text">商务册第 5 页《法定代表人授权委托书》的「投标人(电子签章)」位置是空的。只看有无,不鉴定真伪。</p>
<div class="u4" data-u4="ok">
<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>
<textarea class="input" id="ig2" maxlength="200" aria-describedby="ig2-msg">电子签章在递交时由交易平台统一加盖,Word 稿里本来就是空的。</textarea>
<div class="msg-row"><span class="msg" id="ig2-msg" data-def="说明会同时作为误报反馈提交">说明会同时作为误报反馈提交</span><span class="count"><span data-n>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 class="row"><button type="button" class="btn sm swap" data-ok><span class="idle">确认无误</span><span class="busy">正在保存…</span></button><button type="button" class="btn sm text" data-x>取消</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>
<div class="cite"><a class="cite-src" href="#" data-demo>招标文件 · 第 <span class="fig">24</span> 页</a><q>4.《法定代表人授权委托书》(法定代表人签字或盖章并<mark>加盖单位电子签章</mark>)。</q></div>
<div class="cite"><a class="cite-src" href="#" data-demo>商务册 · 第 <span class="fig">5</span> 页</a><q>法定代表人(签字或盖章):李某某  <mark>投标人(电子签章):(签章位置为空)</mark></q></div>
</aside>
</article>
</div>
@@ -221,80 +266,146 @@
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); }
/* 日期输入(Q10)由 demo.js 的 Demo.dateInput() 处理:可直接键入、离开时整理、下方显示「11月20日 09:30 · 剩 N 天」。
* 字段错误用 Demo.fieldError():离开时校验,报错后边输入边复查。 */
const setErr = Demo.fieldError;
const label = (f) => ($("label", f)?.firstChild.textContent || $(".vh", f)?.textContent || "").trim();
/* ---------- 01 新建项目 ---------- */
const F = $("[data-new]"), sum = $("[data-errsum]"), dueDT = $("#f-due [data-dt]");
let files = [];
const RULES = {
"f-name": () => $("#n-name").value.trim() ? "" : "请填写项目名称,可以先写简称",
"f-due": null, // 由日期输入自己校验(必填 + 格式)
"f-file": () => files.length ? "" : "请上传招标文件,至少一份"
};
function check(id) {
const ok = id === "f-due" ? dueDT.dt.commit({ required: true }) : setErr(document.getElementById(id), RULES[id]());
if (!sum.hidden) summary(false);
return ok;
}
function checkName() { setErr("f-name", $("#n-name").value.trim() ? "" : "请填写项目名称,可以先写简称,之后再改"); }
$("#n-name").addEventListener("blur", checkName);
function summary(focus) {
const errs = Object.keys(RULES).map((id) => document.getElementById(id)).filter((f) => f.classList.contains("err"));
sum.hidden = !errs.length;
$("[data-errn]").textContent = errs.length;
$("[data-errlist]").innerHTML = errs.map((f) => `<li><a href="#${f.id}" data-go="${f.id}">${esc(label(f))}</a>:${esc($(".msg.e", f).textContent.replace(/^!/, ""))}</li>`).join("");
if (focus && errs.length) { sum.scrollIntoView({ block: "center" }); sum.focus({ preventScroll: true }); }
}
let nameDirty = false;
$("#n-name").addEventListener("input", () => { nameDirty = true; if ($("#f-name").classList.contains("err") && !RULES["f-name"]()) check("f-name"); });
$("#n-name").addEventListener("blur", () => { if (nameDirty || $("#f-name").classList.contains("err")) check("f-name"); });
F.addEventListener("dt-change", () => { if (!sum.hidden) summary(false); });
sum.addEventListener("click", (e) => { const a = e.target.closest("[data-go]"); if (!a) return; e.preventDefault(); const f = document.getElementById(a.dataset.go); f.scrollIntoView({ block: "center" }); ($("input, [data-drop]", f)).focus({ preventScroll: true }); });
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-files]").innerHTML = files.map((f, i) => `<div class="file-row" style="margin-top:var(--sp-3)"><span class="ext">${f.ext}</span><div><b>${esc(f.name)}</b>${f.warn ? `<span class="hint-warn"><span class="ico w" aria-hidden="true">!</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="移除 ${esc(f.name)}" 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(); } });
const addFiles = () => {
files = [{ ext: "PDF", name: "ZC-2026-0418 采购文件.pdf", meta: `<span class="fig">186</span> 页 · <span class="fig">8.4</span> MB` }, { ext: "DOC", name: "关于 ZC-2026-0418 的更正公告.pdf", warn: "扩展名是 .pdf,内容是 Word 文档,会按 Word 解析" }];
renderFiles(); check("f-file");
};
$("[data-drop]").addEventListener("click", addFiles);
$("[data-drop]").addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); addFiles(); } });
$("[data-files]").addEventListener("click", (e) => { const b = e.target.closest("[data-rm]"); if (b) { files.splice(+b.dataset.rm, 1); renderFiles(); if (!files.length && !sum.hidden) check("f-file"); } });
$("[data-cancel]", F).onclick = () => toast("(演示)取消:回到工作台", { ok: false });
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;
}
const ok = Object.keys(RULES).map(check).every(Boolean);
if (!ok) { summary(true); 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);
const ctl = $$("input, textarea, button:not([data-submit]), .select", F);
ctl.forEach((x) => x.disabled = true);
$$(".field", F).forEach((x) => x.classList.add("dis"));
$("[data-drop]").setAttribute("aria-disabled", "true"); $("[data-drop]").style.pointerEvents = "none";
setTimeout(() => {
b.classList.remove("is-loading"); b.removeAttribute("aria-busy");
ctl.forEach((x) => x.disabled = false); $$(".field.dis", F).forEach((x) => x.classList.remove("dis"));
$("[data-drop]").removeAttribute("aria-disabled"); $("[data-drop]").style.pointerEvents = "";
toast("项目已创建,正在解析招标文件,可以离开本页");
}, 2400);
});
addEventListener("DOMContentLoaded", () => { // 等 demo.js 初始化完日期输入
if (new URLSearchParams(location.search).get("state") === "errors") {
$("#n-due").value = "2026-11-20 25:00";
Object.keys(RULES).forEach(check); summary(false);
}
});
/* ---------- 资质对话框 ---------- */
/* ---------- 03 资质录入:到期按项目的投标截止日判断(智慧大厅 11月20日),只提醒,不拦截 ---------- */
const DUE = new Date(2026, 10, 20);
const expF = $("#f-qexp"), expHint = $("[data-exp-hint]", expF), expDT = $("[data-dt]", expF);
expF.addEventListener("dt-change", (e) => {
const d = e.detail, late = d.ok && !d.empty && d.dt < DUE;
if (late) delete expHint.dataset.off; else expHint.dataset.off = "";
expHint.hidden = !late || expF.classList.contains("err");
});
$("[data-long]").addEventListener("click", () => setTimeout(() => {
const long = $("[data-long]").getAttribute("aria-checked") === "true";
$("#q-exp").disabled = long; $(".icon-btn", expDT).disabled = long;
if (long) { expHint.dataset.off = ""; expHint.hidden = true; setErr(expF, ""); } else expDT.dt.commit({ required: true });
}));
$("[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>`); }
body: `<div class="form">
<div class="field span2" id="qd-name"><label for="qdn">名称 <small>(必填)</small></label><input class="input" id="qdn" placeholder="和证书上的写法一致" autocomplete="off"></div>
<div class="field"><label id="qdt-l">类型 <small>(必填)</small></label><div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="qdt-l"><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" placeholder="例如:CS3、二级"></div>
<div class="field" id="qd-no"><label for="qdo">证书编号 <small>(必填)</small></label><input class="input" id="qdo" autocomplete="off"></div>
<div class="field" id="qd-exp"><label for="qde">有效期至 <small>(必填)</small></label><div class="dt" data-dt data-marks="2026-11-20" data-note="圆点是进行中项目的投标截止日" data-req="请填写有效期"><input class="input" id="qde" placeholder="例如:2026-11-20" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false">${icon("calendar")}</button><div class="cal" role="dialog" aria-label="选择有效期" hidden></div></div></div>
<div class="field span2"><label>证书扫描件 <small>(可以稍后补录)</small></label><div class="drop" tabindex="0" role="button" style="padding:var(--sp-4)">${icon("upload")}<span>拖入或点击上传 PDF / 图片</span></div></div></div>`,
onOpen: (s) => {
const R = {
"qd-name": () => $("#qdn", s).value.trim() ? "" : "请填写资质名称,和证书上的写法一致",
"qd-no": () => $("#qdo", s).value.trim() ? "" : "请填写证书编号,查标时用它核对附件"
};
const exp = $("#qd-exp [data-dt]", s);
s._check = () => [...Object.entries(R).map(([id, fn]) => setErr($("#" + id, s), fn())), exp.dt.commit({ required: true })].every(Boolean);
Object.entries(R).forEach(([id, fn]) => {
const f = $("#" + id, s), inp = $("input", f); let d = false;
inp.addEventListener("input", () => { d = true; if (f.classList.contains("err") && !fn()) setErr(f, ""); });
inp.addEventListener("blur", () => { if (d || f.classList.contains("err")) setErr(f, fn()); });
});
if (bad) return false;
},
onOk: (s) => {
if (!s._check()) { $(".field.err input", s).focus(); 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 }) }); };
/* ---------- 04 忽略原因 / 致命确认说明 ---------- */
const NOW = "11月3日 14:30";
$$("[data-u4]").forEach((box) => {
const kind = box.dataset.u4, ta = $("textarea", box), fld = $(".field", box), msg = $(".msg", fld), def = msg.dataset.def;
const bad = () => ta.value.trim().length < 5;
const show = (on) => { fld.classList.toggle("err", on); ta.toggleAttribute("aria-invalid", on); msg.innerHTML = on ? `<span class="ico w" aria-hidden="true">!</span>${kind === "ign" ? "至少写 5 个字,说明为什么不处理" : "请写一句说明,至少 5 个字"}` : def; };
ta.addEventListener("input", () => { $("[data-n]", box).textContent = ta.value.length; if (fld.classList.contains("err") && !bad()) show(false); });
ta.addEventListener("blur", () => show(bad()));
$$("[data-chip]", box).forEach((c) => c.onclick = () => { ta.value = c.textContent; $("[data-n]", box).textContent = ta.value.length; show(false); ta.focus(); });
$("[data-x]", box).onclick = () => toast("(演示)取消:收起输入框", { ok: false });
const okBtn = $("[data-ok]", box);
okBtn.onclick = () => {
if (bad()) { show(true); ta.focus(); return; }
const done = () => {
const html = kind === "ign" // 署名行(共用 .sign):「已忽略」不是确认,用辅助灰和「–」
? `<div class="sign ign"><span class="ok" aria-hidden="true">–</span><b>王敏</b><span class="fig">${NOW}</span><span>已忽略</span><button type="button" class="undo" data-undo>撤销</button></div><p class="sign-note">原因:${esc(ta.value.trim())}</p>`
: `<div class="sign"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">${NOW}</span><span>确认无误</span><button type="button" class="undo" data-undo>撤销</button></div><p class="sign-note">说明:${esc(ta.value.trim())} · 已作为误报反馈提交</p>`;
box.hidden = true;
box.insertAdjacentHTML("afterend", `<div class="u4-done">${html}</div>`);
const d = box.nextElementSibling;
$("[data-undo]", d).onclick = () => { d.remove(); box.hidden = false; ta.focus(); toast("已撤销", { ok: false }); };
$("[data-undo]", d).focus();
toast(kind === "ign" ? "已忽略 · 原因已记录" : "已确认无误 · 说明已作为反馈提交");
};
if (kind === "ok") { okBtn.classList.add("is-loading"); okBtn.setAttribute("aria-busy", "true"); setTimeout(() => { okBtn.classList.remove("is-loading"); okBtn.removeAttribute("aria-busy"); done(); }, 900); }
else done();
};
});
document.addEventListener("click", (e) => { if (e.target.closest("[data-demo]")) { e.preventDefault(); toast("(演示)在原文查看器里打开这一页", { ok: false }); } });
</script>
</body>
</html>

View File

@@ -11,18 +11,20 @@
<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)}
.qs td:nth-child(2){font-weight:var(--fw-bold);white-space:nowrap}
.qs td:last-child{white-space:nowrap;color:var(--text-secondary)}
.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}
.comp code,.qs code{font:12px/20px ui-monospace,Menlo,Consolas,monospace;color:var(--text-secondary)}
pre.run{margin:var(--sp-3) 0 0;padding:var(--sp-3) var(--sp-4);background:var(--c-n100);border-radius:var(--radius-ctl);font:13px/20px ui-monospace,Menlo,Consolas,monospace;color:var(--text-primary);overflow:auto}
.comp td,.qs td{vertical-align:top}
</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>
<h1>标博士 · UI demo v0.2(2026-11-03)</h1>
<p>按设计规范 v0.2(决策 U5)重刷:白色主调、墨色派生的浅灰、只有一种白卡片容器、圆角三档、96px 墨色渐变侧栏、透明顶栏。<b>v0.2 风格定稿样张见 <a class="lnk" href="explore/workbench-d5.html">explore/workbench-d5.html</a></b>,各页的侧栏、顶栏、卡片、表头、标签、按钮、筛选、行悬停都以它为准。颜色、字号、间距只用 <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>
@@ -30,61 +32,65 @@ pre.run{margin:var(--sp-3) 0 0;padding:var(--sp-3) var(--sp-4);background:var(--
<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>
<a class="d-link" href="page-workbench.html"><b><span class="fig">⑤</span>组合页 · 工作台</b><p>v0.2 框架(侧栏、透明顶栏、搜索、额度、通知);截止提醒、一卡四格统计(可点筛选)、项目列表(下一步 / 更多)、待处理的致命问题,右侧任务、额度、最近处理。</p></a>
<a class="d-link" href="page-reading.html"><b><span class="fig">⑥</span>组合页 · 读标</b><p>项目页头 + 页签、清单状态与确认、分类导航、清单(待确认区、补遗更新、搜索)、原文查看器并排高亮。J / K 逐条看原文。</p></a>
<a class="d-link" href="explore/index.html"><b><span class="fig">★</span>风格探索</b><p>工作台的六轮方案(a–f、d2–d5),d5 为 v0.2 定稿样张。</p></a>
<a class="d-link" href="controls.html"><b><span class="fig">①</span>控件总览</b><p>按钮(类型 × 状态、尺寸、扣点说明)、输入框 / 下拉 / 日期 / 复选 / 单选 / 开关、级别标签、AI 标识与署名、出处、提醒、轻提示、对话框、标签页、筛选、分页、空状态、骨架屏、进度、提示气泡、菜单、快捷键。</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 摘要卡(确认前后)、关键日期卡。MVP 不做项目卡片视图(Q11)。</p></a>
<a class="d-link" href="lists.html"><b><span class="fig">④</span>列表与表格</b><p>项目列表、清单列表(分类、★ 筛选、J / K)、问题列表(级别筛选、只对「提示」批量、1 / 2 / 3)、额度流水、参数对照表(含级别列)、空结果。</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>
<div class="d-sec-h"><span class="no">02</span><h2>新增组件清单</h2><span class="ref">B 部分在 demo.css;精修定稿后并入 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>—</td><td>卡片 · 中性标签 · 链接 · 出处链接 · 数字</td><td><code>.card .card.flush .card-h .card-b .card-f .tag .tag.ai .lnk .hint-u .cite-link .fig .sheet .hl</code></td><td>§4.2 · §5.1 · §7.8 · §7.16</td><td>在 components.css。全页只有一种白卡片;中性标签 --c-n100 实底;<code>.fig</code> 默认 15、12px 小字里 13、统计大数字用 <code>.big</code>(28);原文纸面用 <code>--font-doc</code> <code>--fs-doc</code> <code>--lh-doc</code>,原句高亮 = 蓝铅笔浅底 + 蓝色底边;致命问题行不加红竖线</td></tr>
<tr><td>B1</td><td>图标</td><td><code>.ic .vh</code></td><td>§6 · U3</td><td>Lucide 路径内联,1.5px 描边,16 / 20 / 32;侧栏菜单 22</td></tr>
<tr><td>B2</td><td>页面框架</td><td><code>Demo.shell() · .sidebar .me .plan .main .top .crumbs .top-search .quota-chip .content .page-title .proj-head</code></td><td>§5.3 · U5</td><td>96px 墨色渐变侧栏(上图标下文字、底部点阵、头像 + 套餐);透明顶栏,滚动后毛玻璃;内容区左上角 12 圆角</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>按下变深(--c-ink-active)、加载中定宽换字、只有图标的按钮 36 / 32、按钮组</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>聚焦 = 墨色边框 + 1px 内圈;带单位、搜索、两列表单、错误汇总、字数</td></tr>
<tr><td>B5</td><td>下拉选择 · 下拉菜单 · 浮动菜单</td><td><code>.select .menu .pop .menu.float .menu .two · Demo.floatMenu() Demo.menuHTML()</code></td><td>Q4 · AR10</td><td>选中项加粗 + 右侧墨色 ✓;两行菜单项写扣点说明;禁用项(<code>disabled: "原因"</code>)变禁用灰、右侧写原因;正式实现用 Reka UI Select / DropdownMenu</td></tr>
<tr><td>B6</td><td>日期输入 · 日期选择</td><td><code>.dt[data-dt] .cal · Demo.dateInput() Demo.parseDT() Demo.fieldError()</code></td><td>§7.22 · §9.3 · Q4 · Q10</td><td>可键入「2026-11-20 09:30」及常见写法,离开时整理;选择器带 24 小时时间;下方显示「11月20日 09:30 · 剩 N 天」。日历非本月格子留空,不可选日期禁用灰、无悬停</td></tr>
<tr><td>B7</td><td>复选 · 单选 · 单选卡片 · 开关</td><td><code>.ck .choice .choice-card .switch</code></td><td>§8.2 · Q4</td><td>复选框圆角 4;开关 = 圆角 6 矩形轨道 + 圆角 4 方块滑钮 + 「开 / 关」字</td></tr>
<tr><td>B8</td><td>筛选 · 标签页</td><td><code>.seg .tabs [role=tab]</code></td><td>§5.3 · §7.5</td><td>筛选是一排文字按钮,按下 = --c-n100 实底 + 加粗;页签 ← → 切换</td></tr>
<tr><td>B9</td><td>分页</td><td><code>.pagination</code></td><td>Q4</td><td>当前页 --c-n100 实底 + 加粗,与筛选一致</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>
<tr><td>B11</td><td>对话框</td><td><code>.scrim .dialog .wide</code></td><td>§7.14 · Q12</td><td>480 / 640,圆角 12,遮罩 --scrim;对话框不裁剪浮层(内容过高时遮罩滚动);焦点锁定、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] .tip-below .tip-start .tip-left</code></td><td>§6</td><td>墨底白字、圆角 8;正式实现用 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 · Q8</td><td>骨架屏静止,不做明暗起伏</td></tr>
<tr><td>B15</td><td>杂项 · 快捷键说明</td><td><code>.kbd .kv .chip .drop .file-row .flowline .av · Demo.keysDialog(Demo.KEYS.reading / .check) Demo.keysTable()</code></td><td>§8.2</td><td>快捷键说明按页面传入:读标 J / K、Enter、?;查标 J / K、O、1 / 2 / 3、X、U、?</td></tr>
<tr><td>B16</td><td>AI 产出块 · 署名行</td><td><code>.ai-block .ai-label .sign .sign.sm .sign.ign .sign .undo .sign-note .more-cites</code></td><td>§7.4 · §7.16 · Q2</td><td>正文始终墨色;确认前「AI」标签 + 左侧 2px 蓝铅笔虚线,确认后去掉虚线、出署名行(绿 ✓ + 姓名 + 时间 + 动作 + 撤销),原因 / 说明写在 <code>.sign-note</code></td></tr>
<tr><td>B17</td><td>卡片件</td><td><code>.card-h .card-foot .stats .stat .quota .dates</code></td><td>§7.9 · §7.10 · Q11</td><td>一卡四格统计(格子可做筛选按钮)、额度卡、关键日期;项目卡已删除(Q11)</td></tr>
<tr><td>B18</td><td>表格补充 · 当前条 · 行内说明</td><td><code>.toolbar .th-sort .batchbar .filters-on tr.sel .item.sel .finding.sel .kb-on .u4</code></td><td>§7.4 · §7.7 · §7.18 · §8.2 · U4</td><td>当前条 = --c-n50 底 + 左侧 3px 墨色竖条,键盘再加 2px 内框;勾选只看复选框;忽略原因、致命的确认说明在行内展开(无底色)</td></tr>
<tr><td>B19</td><td>对照布局</td><td><code>.cmp[data-cmp] .cmp-list .cats .lst .rsz .grp-h .grp-h.ai .viewer-body · Demo.cmp()</code></td><td>§5.3 · §7.6 · §7.8</td><td>分类导航 176(1400 以下 152)+ 列表(最小 560)+ 原文查看器(默认 45%、最小 400,可拖动或 ← →);待确认是清单顶部的蓝色标题分组</td></tr>
<tr><td>B20</td><td>项目行</td><td><code>.prow-head .prows .prow .pname .psub .nx .more-cell .srt</code></td><td>§7.9</td><td>工作台和列表页共用:下一步左对齐、行悬停箭头右移 3px;更多单独一列常驻;致命列居中</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>
<section class="d-sec" id="s-q">
<div class="d-sec-h"><span class="no">03</span><h2>Q1–Q13 的答案(2026-11-03 已确认)</h2><span class="ref">Q1–Q12 已写进 design-system.md 对应章节</span></div>
<table class="data qs">
<thead><tr><th>#</th><th>问题</th><th>demo 现在的做法</th><th>需要决定</th></tr></thead>
<thead><tr><th>#</th><th>问题</th><th>结论</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>
<tr><td>Q1</td><td>额度流水的「+」</td><td>改为墨色,靠「+ / −」符号区分,不用绿色</td><td>§3.4 · §7.10</td></tr>
<tr><td>Q2</td><td>AI 产出的正文颜色</td><td>正文始终墨色;AI 身份靠「AI」小标签和左侧 2px 蓝铅笔虚线;确认后去掉虚线,换成署名行(绿 ✓ + 姓名 + 时间)</td><td>§7.16</td></tr>
<tr><td>Q3</td><td>「选中」用什么颜色</td><td>统一为 <code>--c-n50</code> 底 + 左侧 3px 墨色竖条(读标清单、表格选中都一样);蓝色只留给原文高亮和出处链接</td><td>§7.18</td></tr>
<tr><td>Q4</td><td>开关、分页、下拉、日期选择</td><td>开关:圆角 6 矩形轨道 + 圆角 4 方块滑钮,开为墨色,旁边写「开 / 关」。分页:当前页 <code>--c-n100</code> 实底加粗。下拉:选中项加粗、右侧墨色 ✓。日期:不可选只变禁用灰,不加删除线</td><td>§7.19–7.22</td></tr>
<tr><td>Q5</td><td>参数对照表的「级别」列</td><td>保留</td><td>§7.7</td></tr>
<tr><td>Q6</td><td>悬停、按下、遮罩的颜色</td><td>用 tokens.css 的 <code>--c-ink-hover</code> <code>--c-ink-active</code> <code>--scrim</code> 等变量,清掉 <code>#34404D</code> <code>#222</code> <code>#888</code> 这类裸色值</td><td>§3.1</td></tr>
<tr><td>Q7</td><td>字阶外的数字字号</td><td><code>--fs-figure</code> 28、<code>--fs-figure-sm</code> 15、<code>--fs-figure-xs</code> 13</td><td>§4.2</td></tr>
<tr><td>Q8</td><td>骨架屏动不动</td><td>静止,不做明暗起伏</td><td>§7.15</td></tr>
<tr><td>Q9</td><td>顶栏额度 80% 后</td><td>用到 ≥ 80% 时该项数字变赭色(<code>--warn-text</code>)加粗;用完时悬停说明续费方式;不用红</td><td>§7.10</td></tr>
<tr><td>Q10</td><td>日期的输入格式</td><td>输入框「2026-11-20 09:30」(24 小时制,配日期选择器);显示「11月20日 09:30」</td><td>§9.3</td></tr>
<tr><td>Q11</td><td>项目卡片视图</td><td>MVP 不做,工作台只用列表;cards 页的「项目卡」改作他用或删掉</td><td>§7.9</td></tr>
<tr><td>Q12</td><td>「一屏一个主按钮」怎么算</td><td>按上下文算:页面级一个;对话框里可以有自己的主按钮;陈列页不受限</td><td>§7.1</td></tr>
<tr><td>Q13</td><td>流水是否记录 0 点的免扣行</td><td><b>记。</b>24 小时内复查不扣点时,流水记一行「免扣(24 小时内复查)」、数量 0(见 ④ 列表与表格 04),让用户能核对复查确实没扣点(红线 9「可查」)。FR-M6-03 已补「免扣」类型</td><td>§7.10 · FR-M6-03</td></tr>
</tbody>
</table>
</section>

View File

@@ -1,4 +1,9 @@
<!doctype html>
<!--
陈列页 ④ 列表与表格(v0.2)
项目列表的行和 page-workbench.html 共用 demo.css 的 B20;当前条 / 勾选条、行内说明(U4)、署名行都用共用类。
本页 <style> 只放:卡片头里的筛选控件尺寸、清单和问题列表与卡片头的间隔、流水的退回对应行。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -10,9 +15,34 @@
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 卡片头里的筛选控件:统一 32 高(与筛选按钮一致) */
.card-h .seg + .pop,.card-h .seg + .choice{margin-left:var(--sp-2)}
.card-h .pop .select{width:auto;min-width:120px;height:var(--control-h-sm)}
.card-h .r{margin-left:auto;display:flex;gap:var(--sp-3);align-items:center}
.card-h .search{width:240px}
.card-h .search .input{height:var(--control-h-sm)}
.card-h .search > .ic{top:8px}
.card-h .choice{align-items:center;white-space:nowrap}
.card-h .choice .ck{margin-top:0}
.card > .filters-on{margin:0;padding:0 var(--card-x) var(--sp-3)}
/* 02 清单、03 问题:列表放进卡片,第一条也要和卡片头隔开 */
.clist,.flist{outline:0}
.clist .lb-h{display:flex;justify-content:space-between;padding:0 var(--card-x) var(--sp-2);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.clist .item{padding:var(--sp-3) var(--card-x)}
.clist .item:first-of-type,.flist .finding:first-of-type{border-top:var(--line) solid var(--border-subtle)}
.clist .item:last-child{border-radius:0 0 var(--radius-card) var(--radius-card)}
.clist .item .side{display:flex;flex-direction:column;align-items:flex-end;gap:var(--sp-1);white-space:nowrap}
.clist .empty{border-top:var(--line) solid var(--border-subtle)}
.flist > .empty{border-top:var(--line) solid var(--border-subtle)}
/* 04 额度流水 */
.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)}
.card-f .pagination{flex:1;margin:0}
/* 05 参数对照表:点一行 = 当前条(§7.18),在原文查看器里两边对照 */
table.param td:nth-child(4),table.param td:nth-child(5){white-space:nowrap}
</style>
</head>
<body class="demo">
@@ -20,111 +50,118 @@
<main class="d-page">
<div class="d-head">
<h1>④ 列表与表格</h1>
<p>筛选栏 + 表格 / 列表。表头凹陷灰底 + 2px 墨色底线,行高 40;数字用 Barlow 等宽。演示筛选、排序、批量勾选、键盘逐条处理和空结果。</p>
<p>列表和表格都放在白卡片里;筛选在卡片头,表头只有 12px 辅助灰列名,行间发丝线,悬停浅灰。演示筛选、排序、键盘逐条处理、批量勾选、分页和空结果。</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>
<div class="d-sec-h"><span class="no">01</span><h2>项目列表</h2><span class="ref">§7.9 · 行与工作台一致</span></div>
<section class="card flush" aria-labelledby="h-proj">
<div class="card-h">
<h3 id="h-proj">项目</h3>
<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" aria-label="法律体系"><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" aria-selected="false" data-value="政府采购">政府采购</li><li role="option" aria-selected="false" data-value="招标投标">招标投标</li></ul></div>
<div class="r"><label class="search"><i data-ic="search"></i><input class="input" placeholder="搜索项目、采购人" aria-label="搜索项目" data-psearch></label></div>
</div>
<div class="prow-head" data-pcols>
<span><button class="srt" data-sort="name">项目</button></span>
<span><button class="srt" data-sort="h" data-dir="asc">投标截止</button></span>
<span>状态</span>
<span class="c"><button class="srt" data-sort="fatal">致命</button></span>
<span>下一步</span><span></span>
</div>
<ul class="prows" data-prows aria-label="项目列表"></ul>
</section>
</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>
<div class="d-sec-h"><span class="no">02</span><h2>清单列表</h2><span class="ref">§7.5 · §7.18 · 点列表后按 J / K 移动</span></div>
<section class="card flush" aria-label="项目检查清单">
<div class="card-h">
<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="r cap muted"><span><span class="kbd">J</span> <span class="kbd">K</span> 移动</span><span><span class="kbd">Enter</span> 打开出处</span></div>
</div>
<div class="filters-on" data-chips></div>
<div class="clist" data-clist tabindex="0" aria-label="清单,按 J K 移动,Enter 打开出处"></div>
</section>
</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 class="d-sec-h"><span class="no">03</span><h2>问题列表</h2><span class="ref">§7.4 · §7.18 · §8.2 · U4 · 只有「提示」可以批量处理</span></div>
<div class="batchbar" data-batch hidden><b>已选 <span class="fig" data-bn>0</span> 项「提示」</b><span class="r"><button class="btn sm" data-bok>批量确认无误</button><button class="btn sm text" data-bclear>取消选择</button></span></div>
<section class="results card flush" aria-label="检查结果">
<div class="card-h">
<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>
<div class="r"><label class="choice cap" data-allw><input type="checkbox" class="ck" data-fall>全选「提示」</label><button class="btn sm text" onclick="Demo.keysDialog(Demo.KEYS.check)">快捷键 <span class="kbd">?</span></button></div>
</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>
<div class="flist" data-flist tabindex="0" aria-label="问题列表,按 J K 移动,1 2 3 处理"></div>
</section>
<ul class="d-rules"><li>当前条:浅灰底 + 左侧墨色竖条,键盘操作时再加墨色内框;勾选只看复选框,不加底色。致命行不加红竖线,级别只看「致命」标签。</li><li>「忽略」和致命问题的「确认无误」在行内展开说明框,不弹对话框(U4)。</li></ul>
</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>
<div class="d-sec-h"><span class="no">04</span><h2>额度流水</h2><span class="ref">§7.10 · §7.20 · 红线 9 额度即货币</span></div>
<section class="card flush" aria-labelledby="h-flow">
<div class="card-h">
<h3 id="h-flow">本期流水</h3>
<div class="seg" role="group" aria-label="按额度筛选" data-qseg><button aria-pressed="true" data-v="">全部</button><button aria-pressed="false" data-v="解析">解析</button><button aria-pressed="false" data-v="检查">检查</button></div>
<div class="r"><span class="cap muted">2026-11-01 至 2027-10-31</span><button class="btn sm" data-csv><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 data-flow></tbody>
</table>
<div class="card-f" data-ffoot><div class="pagination" data-fpager></div></div>
</section>
<ul class="d-rules"><li>「+」和「−」都用墨色,只靠符号区分,不用绿色(Q1)。退回和对应的预扣相邻显示。期末余额与顶栏一致:解析 37、检查 12。</li><li>24 小时内复查记一行 0 点的「免扣」,这一行是否保留待确认(<a class="lnk" href="index.html#s-q">Q13</a>)。</li><li>分页(Q4):当前页浅灰实底加粗,和筛选一致;只在超过一页时显示。</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">
<div class="d-sec-h"><span class="no">05</span><h2>参数对照表</h2><span class="ref">§7.7 · 点一行在原文查看器里对照</span></div>
<table class="data hover param" data-param>
<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">≥ 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 class="sel"><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><span class="dash">—</span></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><span class="dash">—</span></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><span class="dash">—</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>官网参数截图等</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">已查找:技术册、商务册附件</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="dash">—</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>
<ul class="d-rules"><li>判断只用四个词:满足 / 疑似不满足 / 未找到响应 / 需人工判断。「满足」用墨色普通字,不用绿色。</li><li>保留「级别」列(Q5):标签的实心 / 描边 / 无框给判断词的颜色做形状兜底;「满足」「需人工判断」写「—」。</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-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>
<span class="d-label">筛选后没有结果:写清条件,给「清除筛选」</span>
<section class="card flush" aria-label="空结果示例:项目">
<div class="card-h"><h3>项目</h3><div class="seg" role="group" aria-label="按状态筛选(示例)"><button aria-pressed="false">进行中 <span class="fig">4</span></button><button aria-pressed="false">全部 <span class="fig">6</span></button><button aria-pressed="false">已递交</button><button aria-pressed="true">已归档</button></div></div>
<ul class="prows"><li class="empty sm"><b>没有符合条件的项目</b><p>「已归档」里没有名称或采购人含「医保」的项目。</p><button class="btn sm">清除筛选</button></li></ul>
</section>
</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>
<span class="d-label">问题都处理完:说清楚意味着什么,不庆祝</span>
<section class="results card flush" aria-label="空结果示例:问题">
<div class="empty sm"><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>
</section>
</div>
</div>
</section>
@@ -134,43 +171,83 @@
<script>
const { $, $$, esc, icon, toast, dialog } = Demo;
Demo.demoTop("lists.html");
const kbdMode = (el) => { el.addEventListener("keydown", () => el.classList.add("kb-on")); el.addEventListener("pointerdown", () => el.classList.remove("kb-on")); };
/* ---------- 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 }
/* ---------- 01 项目列表(数据和行结构同 page-workbench.html) ---------- */
const CHECK_COST = `扣 1 次检查 · 剩 <span class="fig">12</span> 次`;
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", regime: "招标投标", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "due-today", st: "编制中", ready: "", fatal: 0,
next: "确认清单", menu: [["上传投标文件", ""], ["发起检查", "cost"], ["导出清单", ""]] },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", regime: "政府采购", lot: "全部", h: 53, due: ["剩 2 天 5 小时", "11月5日 16:00"], cls: "due-soon", st: "已检查", ready: "就绪", fatal: 0,
next: "导出检查报告", menu: [["查看检查结果", ""], ["标记为已递交", ""]] },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", regime: "政府采购", lot: "采购包 1", h: 408, due: ["剩 17 天", "11月20日 09:30"], cls: "", st: "已检查", ready: "未就绪", fatal: 3,
next: "处理致命问题", menu: [["重新检查", "cost"], ["导出检查报告", "致命问题未处理也可以导出"], ["标记为已递交", ""]] },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", regime: "政府采购", lot: "全部", h: 600, due: ["剩 25 天", "11月28日 10:00"], cls: "", st: "跟进中", ready: "", fatal: 0,
next: "确认清单", menu: [["查看解析进度", "补遗 2 解析中 62%"], ["上传补遗", ""]] },
{ name: "某区智慧校园一期", buyer: "某区教育局", regime: "政府采购", lot: "全部", h: -1, due: ["已截止", "已递交"], cls: "due-past", st: "已递交", ready: "", fatal: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", regime: "政府采购", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "due-past", st: "已归档", ready: "", fatal: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
let pf = { seg: "active", q: "", regime: "", sort: ["hours", "ascending"] };
const SEGN = { active: "进行中", all: "全部", "已递交": "已递交", "已归档": "已归档" };
let pf = { seg: "active", q: "", regime: "", sort: ["h", "asc"] };
function pstatus(p) {
const r = p.ready === "就绪" ? '<span class="hint-u tip-below" tabindex="0" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>' : p.ready ? `<span>${p.ready}</span>` : "";
return `<span class="st"><span class="tag">${p.st}</span>${r}</span>`;
}
function prows() {
let rows = PROJ.filter((p) => (pf.seg === "all" || (pf.seg === "active" ? !["已递交", "已归档"].includes(p.status) : p.status === pf.seg))
const rows = P.map((p, i) => [p, i]).filter(([p]) => (pf.seg === "all" || (pf.seg === "active" ? !["已递交", "已归档"].includes(p.st) : p.st === 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>`;
rows.sort(([a], [b]) => (k === "name" ? a.name.localeCompare(b.name, "zh") : a[k] - b[k]) * (d === "asc" ? 1 : -1));
$("[data-prows]").innerHTML = rows.map(([p, i]) => `<li class="prow ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span class="pt"><a class="pname" href="#" title="${esc(p.name)}">${esc(p.name)}</a><span class="psub">${esc(p.buyer)} · ${esc(p.lot)}</span></span>
<span class="dl"><b class="${p.cls}">${p.due[0]}</b><small>${p.due[1]}</small></span>
${pstatus(p)}
<span class="c">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span><button class="nx" data-next><span>${p.next}</span>${icon("chev-right")}</button></span>
<span class="more-cell"><button class="icon-btn sm" aria-label="更多操作:${esc(p.name)}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span></li>`).join("")
|| `<li class="empty sm"><b>没有符合条件的项目</b><p>「${SEGN[pf.seg]}」里没有${pf.regime ? `「${pf.regime}」体系${pf.q ? "、" : ""}` : ""}${pf.q ? `名称或采购人含「${esc(pf.q)}」` : ""}的项目。</p><button class="btn sm" data-pclear>清除筛选</button></li>`;
}
function psortUI() {
$$("[data-pcols] .srt").forEach((b) => {
const on = b.dataset.sort === pf.sort[0];
if (on) b.dataset.dir = pf.sort[1]; else delete b.dataset.dir;
b.querySelector("svg")?.remove();
b.insertAdjacentHTML("beforeend", icon(on ? (pf.sort[1] === "asc" ? "arrow-up" : "arrow-down") : "sort"));
b.setAttribute("aria-label", `按${b.textContent}排序${on ? (pf.sort[1] === "asc" ? ",当前升序" : ",当前降序") : ""}`);
});
}
$("[data-pcols]").addEventListener("click", (e) => {
const b = e.target.closest(".srt"); if (!b) return;
pf.sort = [b.dataset.sort, pf.sort[0] === b.dataset.sort && pf.sort[1] === "asc" ? "desc" : "asc"];
psortUI(); prows();
});
$("[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("(演示)打开项目详情");
$("[data-prows]").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 = "所有体系"; $$("[data-plot] [role=option]").forEach((o) => o.setAttribute("aria-selected", o.dataset.value === ""));
prows(); return;
}
const row = e.target.closest(".prow"); if (!row) return;
e.preventDefault();
const p = P[+row.dataset.i], m = e.target.closest("[data-menu]");
if (m) {
e.stopPropagation();
Demo.floatMenu(m, [{ label: "打开项目" }, "-", ...p.menu.map(([t, s]) => ({ label: t, sub: s === "cost" ? CHECK_COST : s }))], (act) => toast(`(演示)${act}:${p.name}`, { ok: false }));
return;
}
if (e.target.closest(".hint-u")) return;
if (e.target.closest("[data-next]")) return toast(`(演示)${p.next}:${p.name}`, { ok: false });
toast(`(演示)打开项目:${p.name}`, { ok: false });
});
Demo.sortable($("[data-ptable]"), (k, d) => { pf.sort = [k, d]; prows(); });
$("[data-ptable] th[aria-sort] svg").outerHTML = icon("arrow-up");
prows();
psortUI(); prows();
/* ---------- 02 清单列表 ---------- */
/* ---------- 02 清单列表(当前条 = 选中,Q3) ---------- */
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 },
@@ -185,111 +262,249 @@ const CL = [
{ 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: "评分办法" };
const CAT = { qual: "资格条件", invalid: "无效条款", param: "技术参数", date: "日期与递交" };
let cf = { cat: "", star: false }, cur = 0;
const cvis = () => CL.filter((c) => (!cf.cat || c.cats.includes(cf.cat)) && (!cf.star || c.imp === "★"));
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>` : "";
const rows = cvis();
const chips = [cf.cat && `<span class="chip">分类:${CAT[cf.cat]}<button aria-label="移除筛选:${CAT[cf.cat]}" 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>` : "";
$("[data-chips]").hidden = !chips.length;
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 : "人工添加";
$("[data-clist]").innerHTML = `<div class="lb-h"><span>要求</span><span>来源 · 出处</span></div>` + (rows.map((c, i) => {
const src = c.src === "ai" ? '<span class="tag ai">AI</span>' : `<span class="tag">${c.src === "add" ? c.doc : "人工添加"}</span>`;
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>
return `<div class="item ${i === cur ? "sel" : ""}" data-ci="${i}" aria-current="${i === cur}"><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>`);
<div class="side">${src}<a class="cite-src" href="#" data-open>第 <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-cseg]").addEventListener("change", (e) => { cf.cat = e.detail; cur = 0; crows(); });
$("[data-star]").addEventListener("change", (e) => { cf.star = e.target.checked; cur = 0; 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("(演示)在原文查看器里打开这一页"); } }
const a = e.target.closest("[data-ack]"); if (a) { cvis()[+a.dataset.ack].confirmed = true; crows(); toast("已核对补遗更新,记录已保存"); return; }
const it = e.target.closest("[data-ci]"); if (!it) return;
cur = +it.dataset.ci; crows(); $("[data-clist]").focus({ preventScroll: true });
if (e.target.closest("[data-open], .plain")) { e.preventDefault(); toast("(演示)在原文查看器里打开这一页", { ok: false }); }
});
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();
});
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}`);
if (e.key === "Enter" || e.key === "o") toast(`(演示)打开出处:${$(`[data-ci="${cur}"] .cite-src`).textContent}`, { ok: false });
});
kbdMode($("[data-clist]"));
crows();
/* ---------- 03 问题列表 ---------- */
/* ---------- 03 问题列表:当前条 = .finding.sel(浅灰底 + 墨色竖条);勾选只看复选框;批量只给「提示」;忽略和致命的确认无误在行内展开说明(U4) ---------- */
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 行和对应产品参数中更正内存配置。"],
{ 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: [["招标文件", 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 }));
cites: [["商务册", 3, "投标人(电子签章):南京某信息技术有限公司  ", "日期:2026年11月25日", ""]], ocr: true },
{ sev: "hint", t: "目录页码与正文不一致", text: ["技术册目录写「售后服务方案 第 96 页」,正文实际在第 98 页。"],
cites: [["技术册", 2, "七、售后服务方案 ………… ", "96", ""]] }
].map((f, i) => ({ ...f, id: i, st: null, ck: false, open: null, draft: "", err: false }));
FD[6].st = { act: "确认无误", at: "11月1日 16:05" };
let ff = "", fcur = 0, foldOpen = false;
const hist = []; // 处理顺序
/* 本次处理过的问题留在原位(f.session),显示署名行;J / K 可以回到它,U 撤销当前条(规范 §8.2)。
页面加载前已处理的折叠在「已处理」里。 */
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>
const ftodo = () => FD.filter((f) => (!f.st || f.session) && (!ff || f.sev === ff));
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>`; }
/* 行内说明:忽略原因(所有级别)/ 致命问题「确认无误」的说明(U4);替换处理按钮,不弹对话框 */
const U4MSG = { ok: ["说明会同时作为误报反馈提交", "请写一句说明,至少 5 个字"], ign: ["「已忽略」在报告里单独列出", "至少写 5 个字,说明为什么不处理"] };
function u4HTML(f) {
const ok = f.open === "ok", [tip, err] = U4MSG[f.open];
return `<div class="u4" data-u4>
<div class="field ${f.err ? "err" : ""}">
<label for="u4t-${f.id}">${ok ? "确认无误的说明" : "忽略原因"} <small>(必填,至少 5 个字)</small></label>
${ok ? "" : '<div class="chips" role="group" aria-label="常用原因"><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="u4t-${f.id}" maxlength="200" data-u4t ${f.err ? 'aria-invalid="true"' : ""} aria-describedby="u4m-${f.id}">${esc(f.draft)}</textarea>
<div class="msg-row"><span class="msg" id="u4m-${f.id}" data-u4msg>${f.err ? `<span class="ico w" aria-hidden="true">!</span>${err}` : tip}</span><span class="count"><span data-n>${f.draft.length}</span> / 200</span></div>
</div>
<div class="row"><button type="button" class="btn sm" data-u4ok>${ok ? "确认无误" : "忽略这一项"}</button><button type="button" class="btn sm text" data-u4x>取消</button><span class="cap muted">Ctrl Enter 提交 · Esc 取消</span></div></div>`;
}
function findingHTML(f, isCur) {
const ck = f.st ? "" : f.sev === "hint" ? `<label class="ckw tip-start" data-tip="批量处理"><input type="checkbox" class="ck" data-fck="${f.id}" ${f.ck ? "checked" : ""} aria-label="勾选:${esc(f.t)}"></label>`
: `<span class="ckw tip-start" data-tip="只有「提示」可以批量处理"><input type="checkbox" class="ck" disabled aria-label="不可批量"></span>`;
const body = f.st
? `<div class="sign ${f.st.ign ? "ign" : ""}"><span class="ok" aria-hidden="true">${f.st.ign ? "–" : "✓"}</span><b>王敏</b><span class="fig">${f.st.at}</span><span>${f.st.act}</span><button type="button" class="undo" data-undo="${f.id}">撤销</button></div>${f.st.note ? `<p class="sign-note">${f.st.ign ? "原因" : "说明"}:${esc(f.st.note)}${f.st.ign ? "" : " · 已作为误报反馈提交"}</p>` : ""}`
: f.open ? u4HTML(f)
: `<div class="f-actions"><button class="btn sm" data-act="1" data-tip="我核对过,投标文件这里没问题">确认无误</button><button class="btn sm" data-act="2" data-tip="我已经在投标文件里改了,复查时会再查">已修改</button><button class="btn sm" data-act="3" data-tip="决定不处理,必须填原因">忽略并注明原因</button><a class="plain feedback" href="#">判断有误?</a></div>`;
return `<article class="finding has-ck ${isCur ? "sel" : ""}" data-fid="${f.id}" ${isCur ? 'aria-current="true"' : ""}>${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">${f.flag}</span>` : ""}</div>
${f.text.map((t) => `<p class="f-text">${t}</p>`).join("")}${body}</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);
const todo = ftodo(), done = FD.filter((f) => f.st && !f.session);
["", "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>`)
$("[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;
const hints = todo.filter((f) => f.sev === "hint" && !f.st), n = FD.filter((f) => f.ck && !f.st).length, all = $("[data-fall]");
all.checked = hints.length > 0 && hints.every((f) => f.ck); all.indeterminate = !all.checked && hints.some((f) => f.ck); all.disabled = !hints.length;
$("[data-allw]").classList.toggle("dis", !hints.length);
$("[data-batch]").hidden = !n; $("[data-bn]").textContent = n;
}
const now = () => "11月3日 " + new Date().toTimeString().slice(0, 5);
const focusList = () => $("[data-flist]").focus({ preventScroll: true });
function openU4(f, kind) {
f.open = kind; fcur = Math.max(0, ftodo().indexOf(f)); frows();
const ta = $(`#u4t-${f.id}`); ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length);
}
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); } });
if (a === 3) return openU4(f, "ign");
if (a === 1 && f.sev === "fatal") return openU4(f, "ok");
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(); } }); }
function submitU4(f) {
const v = f.draft.trim();
if (v.length < 5) { f.err = true; frows(); $(`#u4t-${f.id}`).focus(); return; }
const ign = f.open === "ign"; f.open = null; f.draft = ""; f.err = false;
done(f, ign ? "已忽略" : "确认无误", v, ign);
}
function done(f, actName, note, ign) {
f.st = { act: actName, at: now(), note, ign }; f.session = true; f.ck = false; hist.push(f);
const vis = ftodo(), nx = vis.findIndex((g, i) => i > vis.indexOf(f) && !g.st); if (nx >= 0) fcur = nx;
frows(); focusList();
toast(`已标记「${actName}」`, { undo: () => undo(f) });
}
function undo(f) { f.st = null; f.session = false; const i = hist.lastIndexOf(f); if (i >= 0) hist.splice(i, 1); frows(); }
$("[data-fseg]").addEventListener("change", (e) => { ff = e.detail; fcur = 0; frows(); });
$("[data-fall]").addEventListener("change", (e) => { ftodo().filter((f) => f.sev === "hint" && !f.st).forEach((f) => { f.ck = e.target.checked; }); 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; }
const t = e.target, f = FD[t.closest("[data-fid]")?.dataset.fid];
if (t.matches("[data-fck]")) { f.ck = 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-undo]")) { undo(f); 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(); }
if (t.closest("[data-chip]")) { const ta = $("[data-u4t]", t.closest("[data-u4]")); ta.value = t.textContent; ta.dispatchEvent(new Event("input", { bubbles: true })); ta.focus(); return; }
if (t.closest("[data-u4ok]")) { submitU4(f); return; }
if (t.closest("[data-u4x]")) { f.open = null; f.err = false; frows(); focusList(); return; }
if (t.closest("[data-u4]")) return;
if (t.closest("a")) { e.preventDefault(); toast(t.closest(".cite-src") ? "(演示)在原文查看器里打开这一处" : "(演示)打开误报反馈", { ok: false }); }
if (f && ftodo().includes(f) && !t.closest(".ckw") && ftodo().indexOf(f) !== fcur) { fcur = ftodo().indexOf(f); frows(); focusList(); }
});
$("[data-flist]").addEventListener("input", (e) => {
if (!e.target.matches("[data-u4t]")) return;
const box = e.target.closest("[data-u4]"), f = FD[e.target.closest("[data-fid]").dataset.fid];
f.draft = e.target.value; $("[data-n]", box).textContent = f.draft.length;
if (f.err && f.draft.trim().length >= 5) { f.err = false; $(".field", box).classList.remove("err"); e.target.removeAttribute("aria-invalid"); $("[data-u4msg]", box).textContent = U4MSG[f.open][0]; }
});
$("[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("(演示)在原文查看器里打开当前出处");
if (e.target.matches("[data-u4t]")) {
const f = FD[e.target.closest("[data-fid]").dataset.fid];
if (e.key === "Escape") { e.preventDefault(); f.open = null; f.err = false; frows(); focusList(); }
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); submitU4(f); }
return;
}
if (e.target.matches("input, textarea") || e.ctrlKey || e.metaKey || e.altKey) return;
const todo = ftodo();
if (e.key === "?") return Demo.keysDialog(Demo.KEYS.check);
if (e.key === "u") { const f = ftodo()[fcur]; if (f && f.st) { undo(f); toast(`已撤销「${f.t}」的处理`, { ok: false }); } else toast("当前这一条还没有处理", { ok: false }); return; }
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.sel").scrollIntoView({ block: "nearest" }); }
if (e.key === "x" && todo[fcur].sev === "hint" && !todo[fcur].st) { todo[fcur].ck = !todo[fcur].ck; frows(); }
if (["1", "2", "3"].includes(e.key)) { e.preventDefault(); if (!todo[fcur].st) act(todo[fcur], +e.key); }
if (e.key === "o" || e.key === "Enter") toast("(演示)在原文查看器里打开当前出处", { ok: false });
});
$("[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(); };
kbdMode($("[data-flist]"));
$("[data-bok]").onclick = () => { const sel = FD.filter((f) => f.ck && !f.st); sel.forEach((f) => { f.st = { act: "确认无误", at: now() }; f.session = true; f.ck = false; hist.push(f); }); frows(); toast(`已将 ${sel.length} 项「提示」标记为确认无误`, { undo: () => sel.forEach(undo) }); };
$("[data-bclear]").onclick = () => { FD.forEach((f) => f.ck = 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>`;
/* ---------- 04 额度流水:按时间先后记账,余额由流水算出(Q1:+ / − 都是墨色) ----------
* [时间, 类型, 额度, 数量, 关联, 说明];退回紧跟在对应的预扣之后。 */
const A = "智慧大厅", B = "智慧社区运维", C = "数据中心机房", D = "医保等保", E = "智慧校园一期", F = "不动产登记平台";
const FREE = "免扣(24 小时内复查)"; // 类型待确认(Q13):FR-M6-03 的类型里没有「免扣」
const LEDGER = [
["11月1日 09:00", "发放", "both", [50, 20], "专业版开通(对公转账)", "开通单 KT-1101"],
["11月1日 09:10", "结算", "检查", -1, `${F} · 第 3 次检查`, ""],
["11月1日 09:20", "结算", "检查", -1, `${E} · 第 1 次检查`, ""],
["11月1日 10:12", "结算", "解析", -1, `${A} · 招标文件`, ""],
["11月1日 10:40", "结算", "解析", -1, `${A} · 招标文件附件`, ""],
["11月1日 11:05", "结算", "解析", -1, `${B} · 招标文件`, ""],
["11月1日 14:02", "结算", "解析", -1, `${C} · 招标文件`, ""],
["11月1日 14:30", "结算", "解析", -1, `${C} · 招标文件附件`, ""],
["11月1日 15:30", "结算", "检查", -1, `${B} · 第 1 次检查`, ""],
["11月1日 16:00", "结算", "检查", -1, `${A} · 第 1 次检查`, ""],
["11月1日 17:20", "结算", "解析", -1, `${D} · 招标文件`, ""],
["11月2日 09:05", "结算", "解析", -1, `${B} · 澄清答复 1`, ""],
["11月2日 09:50", "预扣", "检查", -1, `${E} · 第 2 次检查`, ""],
["11月2日 10:02", "退回", "检查", 1, `${E} · 第 2 次检查`, "检查超时"],
["11月2日 10:05", "结算", "解析", -1, `${A} · 补遗 1`, ""],
["11月2日 10:20", "结算", "检查", -1, `${E} · 第 2 次检查(重试)`, ""],
["11月2日 14:28", "预扣", "解析", -1, `${A} · 补遗 1 附件`, ""],
["11月2日 14:31", "退回", "解析", 1, `${A} · 补遗 1 附件`, "解析失败:文件已加密"],
["11月2日 15:10", "结算", "解析", -1, `${C} · 补遗 1`, ""],
["11月2日 16:20", "结算", "解析", -1, `${D} · 补遗 1`, ""],
["11月2日 16:30", "结算", "检查", -1, `${A} · 第 2 次检查`, "距上次扣点的检查已超过 24 小时"],
["11月2日 16:45", "结算", "检查", -1, `${B} · 第 2 次检查`, ""],
["11月3日 09:15", "结算", "解析", -1, `${B} · 补遗 1`, ""],
["11月3日 10:30", "结算", "解析", -1, `${C} · 澄清答复 1`, ""],
["11月3日 11:00", "结算", "检查", -1, `${E} · 第 3 次检查`, ""],
["11月3日 14:10", "预扣", "解析", -1, `${D} · 补遗 2`, "解析中,完成后结算"],
["11月3日 14:35", FREE, "检查", 0, `${B} · 第 3 次检查`, "排队中 · 不扣额度"]
];
const bal = { 解析: 0, 检查: 0 };
const ROWS = LEDGER.map(([t, type, q, n, ref, note]) => {
if (q === "both") { bal.解析 += n[0]; bal.检查 += n[1]; return { t, type, q: "解析 / 检查", qs: ["解析", "检查"], n: `+${n[0]} / +${n[1]}`, plus: true, bal: `${bal.解析} / ${bal.检查}`, ref, note }; }
bal[q] += n;
return { t, type, q, qs: [q], n: n > 0 ? `+${n}` : n < 0 ? `−${-n}` : "0", plus: n > 0, minus: n < 0, bal: bal[q], ref, note };
}).reverse(); // 新的在上
const PER = 10;
let qf = "", page = 1;
function flow() {
const rows = ROWS.filter((r) => !qf || r.qs.includes(qf)), pages = Math.ceil(rows.length / PER);
page = Math.min(page, pages);
const vis = rows.slice((page - 1) * PER, page * PER);
$("[data-flow]").innerHTML = vis.map((r, i) => {
const sub = r.type === "预扣" && vis[i - 1] && vis[i - 1].type === "退回" && vis[i - 1].ref === r.ref; // 和上一行的退回对应
return `<tr class="${sub ? "flow-sub" : ""}"><td class="num">${sub ? '<span class="fl-link" aria-hidden="true">└</span>' : ""}${r.t}</td><td>${r.type}</td><td>${r.q}</td>
<td class="num r ${r.plus ? "plus" : r.minus ? "minus" : ""}">${r.n}</td><td class="num r">${r.bal}</td><td>${r.ref}</td><td class="muted">${sub ? "对应上一行的退回" : r.note}</td></tr>`;
}).join("");
const pg = (p, label) => `<button class="pg" data-pg="${p}" ${p === page ? 'aria-current="page"' : ""} ${label ? `aria-label="${label}"` : `aria-label="第 ${p} 页"`}>${p}</button>`;
$("[data-ffoot]").hidden = pages < 2; // Q4:只在超过一页时显示
$("[data-fpager]").innerHTML = `<span class="total">共 <span class="fig">${rows.length}</span> 条</span>
<button class="pg" data-pg="${page - 1}" ${page === 1 ? "disabled" : ""} aria-label="上一页">${icon("chev-left")}</button>
${Array.from({ length: pages }, (_, i) => pg(i + 1)).join("")}
<button class="pg" data-pg="${page + 1}" ${page === pages ? "disabled" : ""} aria-label="下一页">${icon("chev-right")}</button>
<label class="jump">跳至 <input class="input" inputmode="numeric" aria-label="跳到第几页" data-jump> 页</label>`;
}
$("[data-qseg]").addEventListener("change", (e) => { qf = e.detail; page = 1; flow(); });
$("[data-fpager]").addEventListener("click", (e) => { const b = e.target.closest("[data-pg]"); if (b && !b.disabled) { page = +b.dataset.pg; flow(); $(`[data-pg="${page}"][aria-current]`)?.focus(); } });
$("[data-fpager]").addEventListener("keydown", (e) => { if (e.key === "Enter" && e.target.matches("[data-jump]")) { const v = parseInt(e.target.value, 10); if (v >= 1) { page = v; flow(); } } });
$("[data-csv]").onclick = () => toast("(演示)已导出本期流水 CSV");
flow();
/* ---------- 05 参数对照表:点一行 = 选中(Q3) ---------- */
$("[data-param]").addEventListener("click", (e) => {
const tr = e.target.closest("tbody tr"); if (!tr) return;
e.preventDefault();
$$("[data-param] tbody tr").forEach((x) => x.classList.toggle("sel", x === tr));
toast(`(演示)原文查看器两边对照:${tr.cells[0].textContent}`, { ok: false });
});
$$("#s-empty button").forEach((b) => b.addEventListener("click", () => toast("(演示)空结果示例", { ok: false })));
</script>
</body>
</html>

View File

@@ -1,4 +1,11 @@
<!doctype html>
<!--
组合页 · 读标(v0.2)
框架来自 Demo.shell(),共用组件来自 components.css 和 demo.css;对照布局(分类导航 + 清单 + 可拖动的原文查看器)是 demo.css 的 B19。
本页 <style> 只放读标特有的:项目头、清单栏、清单工具条与条目细节。
Q2:AI 产出正文墨色,AI 身份靠「AI」标签;待确认条目另加 2px 蓝铅笔虚线。
Q3:清单选中 = --c-n50 底 + 3px 墨色竖条;蓝色只留给原文高亮和出处链接。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -10,59 +17,93 @@
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 本页布局:整屏高度,三栏各自滚动 */
/* 整屏高度:顶栏 + 项目头 + 清单栏固定,下面的对照区(.cmp)三栏各自滚动 */
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}
/* 项目头:名称 + 信息一行;右侧截止、状态、就绪;下方标签页 */
.ph{padding:var(--sp-1) var(--pad-page) 0}
.ph-l1{display:flex;align-items:flex-end;gap:var(--sp-8)}
.ph-l1 > div{min-width:0}
.ph h1{margin:0;font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-serif);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ph-meta{margin:2px 0 0;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.facts{display:flex;gap:var(--sp-8);margin:0 0 0 auto;flex:none}
.facts > div{display:flex;flex-direction:column;gap:2px}
.facts dt{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.facts dd{display:flex;align-items:center;gap:6px;min-height:var(--lh-body);margin:0;white-space:nowrap}
.ph .tabs{margin-top:var(--sp-3)}
.tabs .sev{margin-left:2px}
/* 清单栏:标题 + 清单状态;右侧页面级操作(主按钮一个,Q12) */
.cl-bar{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-4) var(--pad-page) var(--sp-3)}
.cl-bar h2{margin:0;font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);white-space:nowrap}
.cl-bar .status{display:flex;align-items:center;gap:var(--sp-2);min-width:0;white-space:nowrap}
.cl-bar .status .sign{margin:0}
.cl-bar .actions{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center;flex:none}
.cl-bar .actions .lnk{margin-right:var(--sp-2)}
/* 清单 */
.lst{scroll-padding-top:72px}
.lst-tb{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:var(--sp-4);padding:var(--sp-3) var(--card-x);background:var(--bg-surface);border-bottom:var(--line) solid var(--border-subtle)}
.lst-tb .search{flex:1;min-width:0}
.lst-tb .search .input{height:var(--control-h-sm)}
.lst-tb .search > .ic{top:8px}
.lst-tb .choice{flex:none;white-space:nowrap;align-items:center}
.lst-tb .choice .ck{margin-top:0}
.lot-note{margin:0;padding:var(--sp-3) var(--card-x) 0;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.lot-note .fig{font-size:var(--fs-figure-xs)}
.rule{display:flex;gap:var(--sp-2);align-items:flex-start;padding:var(--sp-2) var(--card-x) 0}
.rule > .ic{margin-top:3px;color:var(--text-secondary)}
.rule p{margin:0}
.rule .cite-link{margin:0 -4px;padding:0 4px}
.lst .item{padding:var(--sp-3) var(--card-x) var(--sp-3) var(--sp-3);border-top:var(--line) solid var(--border-subtle)}
.lst .item .t{font-weight:var(--fw-medium)}
.lst .item .side{padding-top:1px}
.lst .item .cite-link{margin:0;padding:0 4px;font-size:var(--fs-caption)}
.lst .item .cite-link .fig{font-size:var(--fs-figure-xs)}
.lst .item .upd .flag b{color:var(--warn-text);font-weight:var(--fw-bold)}
.lst .item .meta .lnk{font-size:var(--fs-caption)}
.lst .pq{cursor:default}
.lst .pq:hover{background:none}
.lst .pq .ai-block{padding-top:0;padding-bottom:0}
.lst .pq .f-actions{margin-top:var(--sp-2)}
.lst .end{height:var(--sp-8)}
</style>
</head>
<body>
<main>
<div class="proj-head">
<div class="line1">
<header class="ph">
<div class="ph-l1">
<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>
<h1 data-pname></h1>
<p class="ph-meta">某市政务服务管理办公室 · 政府采购 · 公开招标 · 投采购包 1(共 2 个包)· <span class="fig">ZC-2026-0418</span></p>
</div>
<dl class="facts">
<div><dt>投标截止</dt><dd><span class="fig m">11月20日 09:30</span><span class="muted">剩 <span class="fig">17</span> 天</span></dd></div>
<div><dt>状态</dt><dd><span class="tag">已检查</span></dd></div>
<div><dt>就绪</dt><dd><span class="hint-u tip-below tip-left" tabindex="0" data-tip="还有 3 个致命问题没处理。不影响导出,决定权在你。">未就绪</span></dd></div>
</dl>
</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>
<nav class="tabs" aria-label="项目"><a role="tab" href="#" data-demo>概览</a><a role="tab" href="#" aria-selected="true">读标</a><a role="tab" href="#" data-demo>查标<span class="sev fatal">致命<span class="fig">3</span></span></a><a role="tab" href="#" data-demo>报告</a></nav>
</header>
<div class="cl-bar">
<h2>项目检查清单</h2>
<div class="status-line" data-status></div>
<div class="status" data-status></div>
<div class="actions">
<a class="plain cap" href="#" data-missing>反馈:有遗漏的要求</a>
<a class="lnk 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 class="cmp" data-cmp>
<section class="card flush cmp-list" aria-label="项目检查清单">
<nav class="cats" aria-label="清单分类" data-cats></nav>
<div class="lst" data-list></div>
</section>
<div class="rsz" role="separator" aria-orientation="vertical" aria-label="调整原文查看器宽度(← → 键)" tabindex="0" data-rsz></div>
<section class="viewer" aria-label="原文查看器" data-viewer></section>
</div>
</main>
@@ -71,122 +112,140 @@ main.main{height:100vh;overflow:hidden}
<script>
const { $, $$, esc, icon, toast, dialog } = Demo;
const D = window.PROTO;
Demo.shell({ nav: "workbench", crumbs: [["工作台", "page-workbench.html"], [D.project.name, "#"], ["读标", ""]] });
const DEMO = "(演示)这里只做了读标页";
/* 面包屑只留「工作台 / 项目名」;当前标签页由项目头的「读标」表示 */
Demo.shell({ nav: "workbench", crumbs: [["工作台", "page-workbench.html"], [D.project.name, ""]], search: { placeholder: "搜索项目、采购人" } });
$("[data-pname]").textContent = D.project.name;
$("[data-pname]").title = 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 = "";
let cat = "all", sel = "c1", confirmed = false, star = false, kw = "";
const catName = (id) => D.categories.find((x) => x.id === id).name;
const page = (c) => `${D.docs[c[0]].role === "addendum" ? D.docs[c[0]].name + " · " : ""}第 <span class="fig">${c[1]}</span> 页`;
/* ---------- 原文查看器 ---------- */
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="pager"><span class="fig" data-vpg></span>
<button class="icon-btn sm tip-below tip-left" aria-label="上一条(K)" data-tip="上一条 K" data-vp>${icon("chev-left")}</button>
<button class="icon-btn sm tip-below tip-left" aria-label="下一条(J)" data-tip="下一条 J" data-vx>${icon("chev-right")}</button></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];
const [doc, pg, 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>
$("[data-vf]", el).title = d.file;
$("[data-vpg]", el).textContent = `第 ${pg} / ${d.pages} 页`;
const paras = (D.pages[doc] || {})[pg] || [];
$("[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">— ${pg} —</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>`;
? `<span class="tag">已确认 v3</span><span class="sign"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">11月3日 ${new Date().toTimeString().slice(0, 5)}</span><span>确认</span></span>`
: `<span class="tag">草稿 v3</span><span class="cap muted">补遗 1 并入后待重新确认</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 b = (id, name, num, cls = "") => `<button class="${cls}" data-cat="${id}" aria-current="${cat === id}">${name}<span class="fig">${num}</span></button>`;
$("[data-cats]").innerHTML = b("all", "全部", items.length) + D.categories.map((c) => b(c.id, c.name, n(c.id))).join("") +
`<hr>${b("pending", "待确认", pending.length, "ai")}
<p class="hint cap"><span class="kbd">J</span><span class="kbd">K</span> 上下条<br><span class="kbd">Enter</span> 看原文<br><span class="kbd">?</span> 快捷键</p>`;
}
const visible = () => {
const seen = new Set();
const seen = new Set(), out = [];
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>
const src = i.src === "ai" ? '<span class="tag ai">AI</span>' : `<span class="tag">${i.src === "addendum" ? i.srcName : "人工添加"}</span>`;
const meta = src + i.cats.map((c) => `<span class="tag">${catName(c)}</span>`).join("") + `<span class="tag">${i.lot ? "采购包 " + i.lot : "通用"}</span>` +
(i.edited ? '<span class="flag">已修改 · <a class="lnk" href="#" data-demo>查看记录</a></span>' : "");
const upd = i.updated ? `<div class="upd"><span class="flag"><b>已被${i.srcName} 更新</b> · 原为 <span class="old">${esc(i.old)}</span></span><button class="btn" data-ack="${i.id}">已核对</button></div>` : "";
return `<div class="item ${sel === i.id ? "sel" : ""}" data-item="${i.id}" tabindex="${sel === i.id ? 0 : -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>`;
<div class="side"><a class="cite-link" href="#" data-cite>${page(i.cite)}</a></div></div>`;
}
/* 待确认(§7.6):放在清单顶部,不和正式清单混排;条目是未确认的 AI 产出,左侧 2px 蓝铅笔虚线(Q2) */
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>`;
return `<section aria-label="待确认"><div class="grp-h ai"><b>待确认</b><span class="fig">${pending.length}</span><p>没找到可靠出处,或把握不大。确认前不参与检查。</p></div>
${pending.map((q) => `<div class="item pq" data-q="${q.id}"><span class="imp">${q.imp || ""}</span>
<div class="ai-block"><div class="t">${esc(q.title)}</div><div class="meta"><span class="tag ai">AI</span><span class="flag">${q.reason}</span></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>
<div class="side">${q.cite ? `<a class="cite-link" href="#" data-qopen>${page(q.cite)}</a>` : '<span class="muted">无出处</span>'}</div></div>`).join("")}</section>`;
}
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>`);
const tb = `<div class="lst-tb"><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></div>') + '<div class="end"></div>';
} else {
const v = visible();
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><div class="grp-h"><b>${g.name}</b><span class="fig">${rows.length}</span></div>${rows.map(itemRow).join("")}</section>` : ""; }).join("");
const head = `<p class="lot-note">通用 + 采购包 1 · 已隐藏采购包 2 的 <span class="fig">${D.hiddenLotItems}</span> 条 · <a class="lnk" href="#" data-demo>更改所投的包</a></p>` +
(cat === "all" && !kw && !star ? `<div class="rule"><i data-ic="info"></i><p>招标文件 <a class="cite-link" href="#" data-rule>第 <span class="fig">24</span> 页</a>:打「★」的为实质性要求。已识别 <span class="fig m">4</span> 条,请对照原文核对有无遗漏。</p></div>${pendingBox()}` : "");
L.innerHTML = tb + head + (body || `<div class="empty sm"><b>没有找到「${esc(kw)}」</b><p>只搜当前分类的条目标题。</p><button class="btn sm" data-clear>清除搜索</button></div>`) + '<div class="end"></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" }); }
function select(id, opt = {}) {
sel = id;
const i = items.find((x) => x.id === id);
$$(".lst .item[data-item]").forEach((x) => { const on = x.dataset.item === id; x.classList.toggle("sel", on); x.tabIndex = on ? 0 : -1; });
const el = $(`[data-item="${id}"]`);
if (el) { if (opt.scroll) el.scrollIntoView({ block: "nearest" }); if (opt.focus) el.focus({ preventScroll: true }); }
V.open(i.cite);
}
function step(d) {
const v = visible(); if (!v.length) return;
const i = v.findIndex((x) => x.id === sel);
const n = v[Math.max(0, Math.min(v.length - 1, (i < 0 ? -1 : i) + d))];
if (n) select(n.id, { scroll: true, focus: true });
}
function render() { status(); cats(); list(); }
/* ---------- 交互 ---------- */
document.addEventListener("click", (e) => {
const t = e.target;
if (t.closest("[data-demo]")) { e.preventDefault(); toast(DEMO, { ok: false }); return; }
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-qopen]")) { e.preventDefault(); V.open(pending.find((x) => x.id === t.closest("[data-q]").dataset.q).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 }); }
if (it && !t.closest("button")) { e.preventDefault(); select(it.dataset.item, { focus: 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 (e.ctrlKey || e.metaKey || e.altKey || e.target.matches("input, textarea") || $(".scrim")) return;
if (e.key === "?") { Demo.keysDialog(Demo.KEYS.reading); 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);
if (e.key === "j" || e.key === "k") { e.preventDefault(); step(e.key === "j" ? 1 : -1); }
if ((e.key === "Enter" || e.key === "o") && (e.target.matches("[data-item]") || e.target === document.body)) { e.preventDefault(); select(sel, { scroll: true, focus: true }); }
});
/* 原文查看器宽度:.cmp[data-cmp] 由 demo.js 的 Demo.cmp() 处理(拖动或 ← →;清单不窄于 560) */
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; }
@@ -206,7 +265,7 @@ function adopt(q, c, 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>` : ""}`,
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({
@@ -214,13 +273,13 @@ $("[data-add]").onclick = () => dialog({
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>`,
<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("已收到反馈") }); };
$("[data-missing]").onclick = (e) => { e.preventDefault(); e.stopPropagation(); 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);
select(sel);
</script>
</body>
</html>

View File

@@ -1,4 +1,9 @@
<!doctype html>
<!--
组合页 · 工作台(v0.2)
内容和交互以定稿样张 explore/workbench-d5.html 为准;框架来自 Demo.shell(),共用组件来自 components.css 和 demo.css,
本页 <style> 里只放工作台特有的布局(主列 + 右栏、待处理问题行);项目行(栅格、「下一步 / 更多」列)是 demo.css 的 B20。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -10,123 +15,212 @@
<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)}
/* 本页布局:主列 + 右侧信息栏(--aside-w),全页一个间距 --gap-card */
.content > .alert,.content > .stats{margin-bottom:var(--gap-card)}
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--aside-w);gap:var(--gap-card);align-items:start}
/* 项目卡片头:筛选 + 已选统计格的清除按钮 */
.card-h .seg{margin-left:var(--sp-2)}
.card-h .clear{margin-left:auto}
/* 待处理的致命问题:不加左侧红竖线,靠实心「致命」标签扫读(d5) */
.todo{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;column-gap:var(--sp-4);align-items:center;padding:var(--sp-3) var(--card-x);border-top:var(--line) solid var(--border-subtle)}
.todo b{display:block;font-weight:var(--fw-bold)}
.todo .cite-link{margin-right:2px}
/* 右栏 */
.wb aside .card-h{min-height:52px;padding-bottom:var(--sp-1)}
.task-h{display:flex;align-items:baseline;gap:var(--sp-2)}
.task-h .cap{margin-left:auto}
.wb .progress{margin-top:10px}
.queue{display:flex;gap:var(--sp-2);align-items:flex-start;margin-top:14px;padding-top:var(--sp-3);border-top:var(--line) solid var(--border-subtle)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.quota .q2 small{font-size:var(--fs-caption)}
.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 li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-2);padding:var(--sp-2) 0;border-top:var(--line) solid var(--border-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.log li:first-child{border-top:0;padding-top:0}
.log li > span:last-child{color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-size:var(--fs-figure-xs);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><h1>工作台</h1><p>下午好,王敏。今天是 11月3日,周二。</p></div>
<div class="actions">
<span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次</span>
<a class="btn primary" href="forms.html#s-new"><i data-ic="plus"></i>新建项目</a>
</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 class="alert warn" role="note">
<i data-ic="clock" class="lg"></i>
<p><strong>某县数据中心机房改造项目</strong> <span class="when">今天 17:00 截止</span>,清单还没确认。</p>
<a class="btn sm" href="page-reading.html"><i data-ic="list"></i>确认清单</a>
</div>
<section class="stats" aria-label="项目统计,点击可筛选项目列表" data-stats>
<button class="stat" data-f="active" aria-pressed="true"><small>进行中的项目</small><span class="big">4</span><i data-ic="filter"></i></button>
<button class="stat" data-f="due7" aria-pressed="false"><small>7 天内截止</small><span class="big due">2</span><i data-ic="filter"></i></button>
<button class="stat" data-f="fatal" aria-pressed="false"><small>待处理的致命问题</small><span class="big fatal">3</span><i data-ic="filter"></i></button>
<button class="stat" data-f="pend" aria-pressed="false"><small>待确认的清单条目</small><span class="big">4</span><i data-ic="filter"></i></button>
</section>
<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 class="stack">
<section class="card flush" aria-labelledby="h-proj">
<div class="card-h">
<h2 id="h-proj">项目</h2>
<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>
<span class="clear" data-chip></span>
</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="prow-head" aria-hidden="true"><span>项目</span><span>投标截止</span><span>状态</span><span class="c">致命</span><span>下一步</span><span></span></div>
<ul class="prows" data-rows aria-label="项目列表"></ul>
</section>
<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>
<section class="card flush" aria-labelledby="h-todo">
<div class="card-h">
<h2 id="h-todo">待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" style="margin-left:auto" href="#" data-demo>查看全部</a>
</div>
<ul style="list-style:none;margin:0;padding:0">
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite-link" href="#" data-demo><i data-ic="file"></i>报价册 · 第 <span class="fig">1</span> 页</a><a class="btn sm" href="page-reading.html">去处理</a></li>
<li class="todo"><span class="sev fatal">致命</span><div><b>★ 条款疑似不满足:应用服务器内存</b><span class="psub">智慧大厅建设项目 · 响应 256G,要求 ≥ 512G · 上次已修改,问题仍在</span></div><a class="cite-link" href="#" data-demo><i data-ic="file"></i>技术册 · 第 <span class="fig">12</span> 页</a><a class="btn sm" href="page-reading.html">去处理</a></li>
<li class="todo"><span class="sev fatal">致命</span><div><b>授权委托书的单位签章位置未发现盖章</b><span class="psub">智慧大厅建设项目 · 只看有无,不鉴定真伪</span></div><a class="cite-link" href="#" data-demo><i data-ic="file"></i>商务册 · 第 <span class="fig">5</span> 页</a><a class="btn sm" href="page-reading.html">去处理</a></li>
</ul>
</div>
</section>
</div>
<aside class="stack" aria-label="任务、额度与记录">
<section class="card flush" aria-labelledby="h-task">
<div class="card-h"><h3 id="h-task">正在进行的任务</h3></div>
<div class="card-b">
<div class="task-h"><b>解析补遗 2</b><span class="cap muted">提取清单</span></div>
<div class="cap muted">某市医保信息系统等保测评服务</div>
<div class="progress"><div class="bar" role="progressbar" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-label="解析补遗 2 的进度"><i style="width:62%"></i></div><span class="fig">62%</span></div>
<div class="queue"><i data-ic="clock"></i><span class="cap muted">检查排队中:智慧社区平台运维,前面 1 个</span></div>
</div>
</section>
<section class="card flush quota" aria-labelledby="h-quota">
<div class="card-h"><h3 id="h-quota">本期额度</h3><span class="cap muted" style="margin-left:auto">至 2027-10-31</span></div>
<div class="card-b">
<div class="q2">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="bar" aria-hidden="true"><i style="width:26%"></i></div></div>
<div><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20</span><div class="bar" aria-hidden="true"><i style="width:40%"></i></div></div>
</div>
</div>
<div class="card-f cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card flush" aria-labelledby="h-log">
<div class="card-h"><h3 id="h-log">最近处理</h3><a class="lnk cap" style="margin-left:auto" href="#" data-demo>全部记录</a></div>
<div class="card-b">
<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>
</section>
</aside>
</div>
</div>
</main>
<script src="demo.js"></script>
<script>
const { $, $$, icon, toast } = Demo;
Demo.shell({ nav: "workbench", crumbs: [["工作台", ""]] });
const { $, $$, icon, esc, toast } = Demo;
const DEMO = "(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页";
/* 数据沿用 explore/workbench-d5.html;pend 为待确认清单条目数。next 为「下一步」,menu 为「更多」里的操作;cost 表示会扣点 */
const CHECK_COST = `扣 1 次检查 · 剩 <span class="fig">12</span> 次`;
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 }
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "due-today", st: "编制中", ready: "", fatal: 0, pend: 3, href: "page-reading.html",
next: "确认清单", menu: [["上传投标文件", ""], ["发起检查", "cost"], ["导出清单", ""]] },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", h: 53, due: ["剩 2 天 5 小时", "11月5日 16:00"], cls: "due-soon", st: "已检查", ready: "就绪", fatal: 0, pend: 0,
next: "导出检查报告", menu: [["查看检查结果", ""], ["标记为已递交", ""]] },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, due: ["剩 17 天", "11月20日 09:30"], cls: "", st: "已检查", ready: "未就绪", fatal: 3, pend: 0, href: "page-reading.html",
next: "处理致命问题", menu: [["重新检查", "cost"], ["导出检查报告", "致命问题未处理也可以导出"], ["标记为已递交", ""]] },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", h: 600, due: ["剩 25 天", "11月28日 10:00"], cls: "", st: "跟进中", ready: "", fatal: 0, pend: 1,
next: "确认清单", menu: [["查看解析进度", "补遗 2 解析中 62%"], ["上传补遗", ""]] },
{ name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, due: ["已截止", "已递交"], cls: "due-past", st: "已递交", ready: "", fatal: 0, pend: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "due-past", st: "已归档", ready: "", fatal: 0, pend: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
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>`;
const STAT = {
active: { label: "进行中", fn: (p) => !["已递交", "已归档"].includes(p.st) },
due7: { label: "7 天内截止", fn: (p) => p.h >= 0 && p.h <= 168 },
fatal: { label: "有致命问题", fn: (p) => p.fatal > 0 },
pend: { label: "有待确认条目", fn: (p) => p.pend > 0 }
};
let seg = "active", stat = "", q = "";
Demo.shell({ nav: "workbench", crumbs: [["工作台", ""]], search: { placeholder: "搜索项目、采购人", onInput: (v) => { q = v; render(); } } });
function status(p) {
let r = "";
if (p.ready === "就绪") r = '<span class="hint-u tip-below" tabindex="0" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>';
else if (p.ready) r = `<span>${p.ready}</span>`;
return `<span class="st"><span class="tag">${p.st}</span>${r}</span>`;
}
$("[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";
function render() {
const rows = P.map((p, i) => [p, i]).filter(([p]) => {
if (stat) { if (!STAT[stat].fn(p)) return false; }
else if (seg !== "all" && !(seg === "active" ? STAT.active.fn(p) : p.st === seg)) return false;
return !q || (p.name + p.buyer).includes(q);
});
$("[data-rows]").innerHTML = rows.map(([p, i]) => `<li class="prow ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span class="pt"><a class="pname" href="${p.href || "#"}" title="${esc(p.name)}">${esc(p.name)}</a><span class="psub">${esc(p.buyer)} · ${esc(p.lot)}</span></span>
<span class="dl"><b class="${p.cls}">${p.due[0]}</b><small>${p.due[1]}</small></span>
${status(p)}
<span class="c">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span><button class="nx" data-next><span>${p.next}</span>${icon("chev-right")}</button></span>
<span class="more-cell"><button class="icon-btn sm" aria-label="更多操作:${esc(p.name)}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span>
</li>`).join("")
|| `<li class="empty sm"><b>没有符合条件的项目</b><p>换个关键词,或切到「全部」看看。</p></li>`;
$$("[data-seg] button").forEach((b) => b.setAttribute("aria-pressed", !stat && b.dataset.v === seg));
$$("[data-stats] .stat").forEach((b) => b.setAttribute("aria-pressed", b.dataset.f === stat || (!stat && b.dataset.f === "active" && seg === "active")));
$("[data-chip]").innerHTML = stat ? `<button class="btn text sm" data-clear>${STAT[stat].label}${icon("x")}<span class="vh">,清除筛选</span></button>` : "";
}
$("[data-rows]").addEventListener("click", (e) => {
const row = e.target.closest(".prow"); if (!row) return;
const p = P[+row.dataset.i];
const m = e.target.closest("[data-menu]");
if (m) {
e.preventDefault(); e.stopPropagation();
const items = [{ label: "打开项目" }, "-", ...p.menu.map(([t, s]) => ({ label: t, sub: s === "cost" ? CHECK_COST : s }))];
Demo.floatMenu(m, items, (act) => { if (act === "打开项目" && p.href) location.href = p.href; else toast(`(演示)${act}:${p.name}`, { ok: false }); });
return;
}
if (e.target.closest(".hint-u")) return;
if (e.target.closest("[data-next]")) { if (p.href) location.href = p.href; else toast(`(演示)${p.next}:${p.name}`, { ok: false }); return; }
if (!p.href) { e.preventDefault(); toast(DEMO, { ok: false }); }
});
$("[data-seg]").addEventListener("change", (e) => { seg = e.detail; stat = ""; render(); });
$("[data-stats]").addEventListener("click", (e) => {
const b = e.target.closest(".stat"); if (!b) return;
const f = b.dataset.f;
if (f === "active") { seg = "active"; stat = ""; }
else if (stat === f) { stat = ""; seg = "active"; }
else stat = f;
render();
});
$("[data-chip]").addEventListener("click", (e) => { if (e.target.closest("[data-clear]")) { stat = ""; seg = "active"; render(); $("[data-seg] button[aria-pressed=true]").focus(); } });
document.addEventListener("click", (e) => { if (e.target.closest("[data-demo]")) { e.preventDefault(); toast(DEMO, { ok: false }); } });
render();
</script>
</body>