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)
项目列表的行和 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>