Files
bid-doctor/docs/design/ui-demo/page-reading.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

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