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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:46:27 +08:00

296 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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