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>
This commit is contained in:
226
docs/design/ui-demo/page-reading.html
Normal file
226
docs/design/ui-demo/page-reading.html
Normal file
@@ -0,0 +1,226 @@
|
||||
<!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> </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>
|
||||
Reference in New Issue
Block a user