- 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>
511 lines
46 KiB
HTML
511 lines
46 KiB
HTML
<!doctype html>
|
||
<!--
|
||
陈列页 ④ 列表与表格(v0.2)
|
||
项目列表的行和 page-workbench.html 共用 demo.css 的 B20;当前条 / 勾选条、行内说明(U4)、署名行都用共用类。
|
||
本页 <style> 只放:卡片头里的筛选控件尺寸、清单和问题列表与卡片头的间隔、流水的退回对应行。
|
||
-->
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>列表与表格 · 标博士 UI demo</title>
|
||
<link rel="icon" href="../logo/favicon.svg">
|
||
<link rel="stylesheet" href="../system/tokens.css">
|
||
<link rel="stylesheet" href="../system/fonts-local.css">
|
||
<link rel="stylesheet" href="../system/components.css">
|
||
<link rel="stylesheet" href="demo.css">
|
||
<style>
|
||
/* 卡片头里的筛选控件:统一 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}
|
||
.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">
|
||
<header data-demo-top></header>
|
||
<main class="d-page">
|
||
<div class="d-head">
|
||
<h1>④ 列表与表格</h1>
|
||
<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>
|
||
<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 · §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 · §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>
|
||
<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 · §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 hover param" data-param>
|
||
<thead><tr><th>参数项</th><th>要求</th><th>投标响应</th><th>判断</th><th>级别</th><th>出处</th></tr></thead>
|
||
<tbody>
|
||
<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>保留「级别」列(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-grid c2">
|
||
<div>
|
||
<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>
|
||
<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>
|
||
</main>
|
||
|
||
<script src="demo.js"></script>
|
||
<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 项目列表(数据和行结构同 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: [["恢复为进行中", ""]] }
|
||
];
|
||
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() {
|
||
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 === "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(); });
|
||
$("[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 });
|
||
});
|
||
psortUI(); prows();
|
||
|
||
/* ---------- 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 },
|
||
{ imp: "★", t: "应用服务器:系统盘 2 块 ≥ 960G SSD", old: "2 块 ≥ 480G SSD", cats: ["param"], lot: "采购包 1", src: "add", p: 1, doc: "补遗 1" },
|
||
{ imp: "★", t: "核心交换机:支持硬件 BFD,最小发送间隔 ≤ 50ms,提供证明材料", cats: ["param", "invalid"], lot: "采购包 1", src: "ai", p: 44 },
|
||
{ imp: "▲", t: "核心交换机:交换容量 ≥ 19.2Tbps,包转发率 ≥ 2880Mpps", cats: ["param"], lot: "采购包 1", src: "ai", p: 44 },
|
||
{ imp: "", t: "营业执照等主体资格证明文件(扫描件)", cats: ["qual", "invalid"], lot: "通用", src: "ai", p: 23 },
|
||
{ imp: "", t: "财务状况报告:资产负债表 + 利润表月报表(开标前 6 个月内)", cats: ["qual", "invalid"], lot: "通用", src: "ai", p: 23 },
|
||
{ imp: "", t: "不接受联合体投标", cats: ["qual"], lot: "通用", src: "manual", p: 2, edited: true },
|
||
{ imp: "", t: "报价不超过采购包 1 最高限价 286.00 万元", cats: ["invalid"], lot: "采购包 1", src: "ai", p: 2 },
|
||
{ imp: "", t: "投标有效期不少于 90 天", cats: ["invalid", "date"], lot: "通用", src: "ai", p: 21 },
|
||
{ imp: "", t: "提交投标文件截止时间:2026年11月20日 09:30", old: "2026年11月18日 09:30", cats: ["date"], lot: "通用", src: "add", p: 1, doc: "补遗 1", confirmed: true },
|
||
{ imp: "", t: "递交方式:电子交易平台在线递交,CA 加密", cats: ["date"], lot: "通用", src: "ai", p: 6 }
|
||
];
|
||
const CAT = { qual: "资格条件", invalid: "无效条款", param: "技术参数", date: "日期与递交" };
|
||
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 = 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>来源 · 出处</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 ? "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}<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; 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) { 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();
|
||
});
|
||
$("[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}`, { ok: false });
|
||
});
|
||
kbdMode($("[data-clist]"));
|
||
crows();
|
||
|
||
/* ---------- 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 行和对应产品参数中更正内存配置。"],
|
||
cites: [["招标文件", 41, "★ 2U机架式服务器,CPU:配置≥2颗海光架构32C 2.7GHz,", "内存: ≥512G DDR4", "……"], ["技术册", 12, "第 12 行 ", "内存:256G DDR4", " 无偏离"]] },
|
||
{ sev: "major", t: "缺少 ★ 参数的证明材料:核心交换机硬件 BFD", text: ["已响应「支持硬件 BFD」,但没有找到官网参数截图等证明材料。请在技术册补充证明材料。"],
|
||
cites: [["招标文件", 44, "★ 支持硬件BFD,最小报文发送间隔≤50ms,", "提供相关证明材料(包含但不限于产品官网公开参数截图证明等)", "。"]], missing: "已查找:技术册(全部章节)、商务册(附件)" },
|
||
{ sev: "major", t: "ISO27001 证书在投标截止日前到期", flag: "新增", text: ["证书有效期至 2026年11月15日,早于投标截止日 11月20日。按评分办法,这一项可能不得分。", "请上传换发后的证书,或在资质库更新有效期。"],
|
||
cites: [["招标文件", 31, "体系认证(6分):……", "以投标截止日在有效期内为准", ",提供证书扫描件)。"], ["商务册", 38, "ISO/IEC 27001 信息安全管理体系认证证书 ", "有效期至:2026年11月15日", ""]] },
|
||
{ sev: "hint", t: "投标文件里出现了其他项目的名称", flag: "新增", text: ["商务册第 27 页出现「某区智慧校园一期项目」及其项目编号。如果是业绩介绍可以忽略;如果是照搬旧标书留下的,请修改。"],
|
||
cites: [["商务册", 27, "我公司承担的", "某区智慧校园一期项目(项目编号 QJ-2025-0112)", "……"]] },
|
||
{ sev: "hint", t: "一般参数疑似不满足:叫号一体机显示屏尺寸", text: ["技术册第 77 页响应「21英寸」,要求「≥21.5英寸」。一般参数负偏离每项扣 0.5 分。"],
|
||
cites: [["招标文件", 47, "", "显示屏尺寸≥21.5英寸", ",分辨率≥1920×1080"], ["技术册", 77, "显示屏:", "21英寸", " LED 背光液晶屏"]] },
|
||
{ sev: "hint", t: "投标函落款日期晚于投标截止日", flag: "沿用", text: ["投标函落款「2026年11月25日」,投标截止时间是 2026年11月20日 09:30。"],
|
||
cites: [["商务册", 3, "投标人(电子签章):南京某信息技术有限公司 ", "日期:2026年11月25日", ""]], ocr: true },
|
||
{ 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: "提示" };
|
||
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 = 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>`)
|
||
+ `<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 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 === 3) return openU4(f, "ign");
|
||
if (a === 1 && f.sev === "fatal") return openU4(f, "ok");
|
||
done(f, a === 1 ? "确认无误" : "已修改");
|
||
}
|
||
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, 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]")) { undo(f); toast("已撤销", { ok: false }); return; }
|
||
if (t.closest("[data-act]")) { act(f, +t.closest("[data-act]").dataset.act); return; }
|
||
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("[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 });
|
||
});
|
||
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 额度流水:按时间先后记账,余额由流水算出(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>
|