docs(design): 按 v0.2 重刷全部 UI demo,确认 Q1–Q13 与 U 键规则
- ui-demo:控件、表单、卡片、列表、工作台、读标六页按 v0.2 重刷;侧栏与顶栏框架、项目行、署名行、可键入日期输入、对照布局等收回 demo.css / demo.js 共用 - system:tokens 补悬停 / 按下 / 遮罩色、数字字阶与原文排版变量;components.css 按 v0.2 改写并清除裸色值 - design-system.md:写入 Q1–Q13 答案,新增开关、分页、下拉、日期选择等组件规则,类别标签、待确认区、选中态、对照布局按 v0.2 修订 - Q13:额度流水记录 0 点的「免扣」行,FR-M6-03 与领域模型同步;U 键改为撤销当前条 - decisions.md 记录 Q1–Q12、Q13 与 U 键;AGENTS.md 同步进度 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
组合页 · 读标(v0.2)
|
||||
框架来自 Demo.shell(),共用组件来自 components.css 和 demo.css;对照布局(分类导航 + 清单 + 可拖动的原文查看器)是 demo.css 的 B19。
|
||||
本页 <style> 只放读标特有的:项目头、清单栏、清单工具条与条目细节。
|
||||
Q2:AI 产出正文墨色,AI 身份靠「AI」标签;待确认条目另加 2px 蓝铅笔虚线。
|
||||
Q3:清单选中 = --c-n50 底 + 3px 墨色竖条;蓝色只留给原文高亮和出处链接。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -10,59 +17,93 @@
|
||||
<link rel="stylesheet" href="../system/components.css">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<style>
|
||||
/* 本页布局:整屏高度,三栏各自滚动 */
|
||||
/* 整屏高度:顶栏 + 项目头 + 清单栏固定,下面的对照区(.cmp)三栏各自滚动 */
|
||||
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}
|
||||
|
||||
/* 项目头:名称 + 信息一行;右侧截止、状态、就绪;下方标签页 */
|
||||
.ph{padding:var(--sp-1) var(--pad-page) 0}
|
||||
.ph-l1{display:flex;align-items:flex-end;gap:var(--sp-8)}
|
||||
.ph-l1 > div{min-width:0}
|
||||
.ph h1{margin:0;font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-serif);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.ph-meta{margin:2px 0 0;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.facts{display:flex;gap:var(--sp-8);margin:0 0 0 auto;flex:none}
|
||||
.facts > div{display:flex;flex-direction:column;gap:2px}
|
||||
.facts dt{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
|
||||
.facts dd{display:flex;align-items:center;gap:6px;min-height:var(--lh-body);margin:0;white-space:nowrap}
|
||||
.ph .tabs{margin-top:var(--sp-3)}
|
||||
.tabs .sev{margin-left:2px}
|
||||
|
||||
/* 清单栏:标题 + 清单状态;右侧页面级操作(主按钮一个,Q12) */
|
||||
.cl-bar{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-4) var(--pad-page) var(--sp-3)}
|
||||
.cl-bar h2{margin:0;font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);white-space:nowrap}
|
||||
.cl-bar .status{display:flex;align-items:center;gap:var(--sp-2);min-width:0;white-space:nowrap}
|
||||
.cl-bar .status .sign{margin:0}
|
||||
.cl-bar .actions{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center;flex:none}
|
||||
.cl-bar .actions .lnk{margin-right:var(--sp-2)}
|
||||
|
||||
/* 清单 */
|
||||
.lst{scroll-padding-top:72px}
|
||||
.lst-tb{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:var(--sp-4);padding:var(--sp-3) var(--card-x);background:var(--bg-surface);border-bottom:var(--line) solid var(--border-subtle)}
|
||||
.lst-tb .search{flex:1;min-width:0}
|
||||
.lst-tb .search .input{height:var(--control-h-sm)}
|
||||
.lst-tb .search > .ic{top:8px}
|
||||
.lst-tb .choice{flex:none;white-space:nowrap;align-items:center}
|
||||
.lst-tb .choice .ck{margin-top:0}
|
||||
.lot-note{margin:0;padding:var(--sp-3) var(--card-x) 0;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
|
||||
.lot-note .fig{font-size:var(--fs-figure-xs)}
|
||||
.rule{display:flex;gap:var(--sp-2);align-items:flex-start;padding:var(--sp-2) var(--card-x) 0}
|
||||
.rule > .ic{margin-top:3px;color:var(--text-secondary)}
|
||||
.rule p{margin:0}
|
||||
.rule .cite-link{margin:0 -4px;padding:0 4px}
|
||||
.lst .item{padding:var(--sp-3) var(--card-x) var(--sp-3) var(--sp-3);border-top:var(--line) solid var(--border-subtle)}
|
||||
.lst .item .t{font-weight:var(--fw-medium)}
|
||||
.lst .item .side{padding-top:1px}
|
||||
.lst .item .cite-link{margin:0;padding:0 4px;font-size:var(--fs-caption)}
|
||||
.lst .item .cite-link .fig{font-size:var(--fs-figure-xs)}
|
||||
.lst .item .upd .flag b{color:var(--warn-text);font-weight:var(--fw-bold)}
|
||||
.lst .item .meta .lnk{font-size:var(--fs-caption)}
|
||||
.lst .pq{cursor:default}
|
||||
.lst .pq:hover{background:none}
|
||||
.lst .pq .ai-block{padding-top:0;padding-bottom:0}
|
||||
.lst .pq .f-actions{margin-top:var(--sp-2)}
|
||||
.lst .end{height:var(--sp-8)}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="proj-head">
|
||||
<div class="line1">
|
||||
<header class="ph">
|
||||
<div class="ph-l1">
|
||||
<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>
|
||||
<h1 data-pname></h1>
|
||||
<p class="ph-meta">某市政务服务管理办公室 · 政府采购 · 公开招标 · 投采购包 1(共 2 个包)· <span class="fig">ZC-2026-0418</span></p>
|
||||
</div>
|
||||
<dl class="facts">
|
||||
<div><dt>投标截止</dt><dd><span class="fig m">11月20日 09:30</span><span class="muted">剩 <span class="fig">17</span> 天</span></dd></div>
|
||||
<div><dt>状态</dt><dd><span class="tag">已检查</span></dd></div>
|
||||
<div><dt>就绪</dt><dd><span class="hint-u tip-below tip-left" tabindex="0" data-tip="还有 3 个致命问题没处理。不影响导出,决定权在你。">未就绪</span></dd></div>
|
||||
</dl>
|
||||
</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>
|
||||
<nav class="tabs" aria-label="项目"><a role="tab" href="#" data-demo>概览</a><a role="tab" href="#" aria-selected="true">读标</a><a role="tab" href="#" data-demo>查标<span class="sev fatal">致命<span class="fig">3</span></span></a><a role="tab" href="#" data-demo>报告</a></nav>
|
||||
</header>
|
||||
|
||||
<div class="cl-bar">
|
||||
<h2>项目检查清单</h2>
|
||||
<div class="status-line" data-status></div>
|
||||
<div class="status" data-status></div>
|
||||
<div class="actions">
|
||||
<a class="plain cap" href="#" data-missing>反馈:有遗漏的要求</a>
|
||||
<a class="lnk 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 class="cmp" data-cmp>
|
||||
<section class="card flush cmp-list" aria-label="项目检查清单">
|
||||
<nav class="cats" aria-label="清单分类" data-cats></nav>
|
||||
<div class="lst" data-list></div>
|
||||
</section>
|
||||
<div class="rsz" role="separator" aria-orientation="vertical" aria-label="调整原文查看器宽度(← → 键)" tabindex="0" data-rsz></div>
|
||||
<section class="viewer" aria-label="原文查看器" data-viewer></section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -71,122 +112,140 @@ main.main{height:100vh;overflow:hidden}
|
||||
<script>
|
||||
const { $, $$, esc, icon, toast, dialog } = Demo;
|
||||
const D = window.PROTO;
|
||||
Demo.shell({ nav: "workbench", crumbs: [["工作台", "page-workbench.html"], [D.project.name, "#"], ["读标", ""]] });
|
||||
const DEMO = "(演示)这里只做了读标页";
|
||||
/* 面包屑只留「工作台 / 项目名」;当前标签页由项目头的「读标」表示 */
|
||||
Demo.shell({ nav: "workbench", crumbs: [["工作台", "page-workbench.html"], [D.project.name, ""]], search: { placeholder: "搜索项目、采购人" } });
|
||||
$("[data-pname]").textContent = D.project.name;
|
||||
$("[data-pname]").title = 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 = "";
|
||||
let cat = "all", sel = "c1", confirmed = false, star = false, kw = "";
|
||||
const catName = (id) => D.categories.find((x) => x.id === id).name;
|
||||
const page = (c) => `${D.docs[c[0]].role === "addendum" ? D.docs[c[0]].name + " · " : ""}第 <span class="fig">${c[1]}</span> 页`;
|
||||
|
||||
/* ---------- 原文查看器 ---------- */
|
||||
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="pager"><span class="fig" data-vpg></span>
|
||||
<button class="icon-btn sm tip-below tip-left" aria-label="上一条(K)" data-tip="上一条 K" data-vp>${icon("chev-left")}</button>
|
||||
<button class="icon-btn sm tip-below tip-left" aria-label="下一条(J)" data-tip="下一条 J" data-vx>${icon("chev-right")}</button></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];
|
||||
const [doc, pg, 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>
|
||||
$("[data-vf]", el).title = d.file;
|
||||
$("[data-vpg]", el).textContent = `第 ${pg} / ${d.pages} 页`;
|
||||
const paras = (D.pages[doc] || {})[pg] || [];
|
||||
$("[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">— ${pg} —</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>`;
|
||||
? `<span class="tag">已确认 v3</span><span class="sign"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">11月3日 ${new Date().toTimeString().slice(0, 5)}</span><span>确认</span></span>`
|
||||
: `<span class="tag">草稿 v3</span><span class="cap muted">补遗 1 并入后待重新确认</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 b = (id, name, num, cls = "") => `<button class="${cls}" data-cat="${id}" aria-current="${cat === id}">${name}<span class="fig">${num}</span></button>`;
|
||||
$("[data-cats]").innerHTML = b("all", "全部", items.length) + D.categories.map((c) => b(c.id, c.name, n(c.id))).join("") +
|
||||
`<hr>${b("pending", "待确认", pending.length, "ai")}
|
||||
<p class="hint cap"><span class="kbd">J</span><span class="kbd">K</span> 上下条<br><span class="kbd">Enter</span> 看原文<br><span class="kbd">?</span> 快捷键</p>`;
|
||||
}
|
||||
const visible = () => {
|
||||
const seen = new Set();
|
||||
const seen = new Set(), out = [];
|
||||
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>
|
||||
const src = i.src === "ai" ? '<span class="tag ai">AI</span>' : `<span class="tag">${i.src === "addendum" ? i.srcName : "人工添加"}</span>`;
|
||||
const meta = src + i.cats.map((c) => `<span class="tag">${catName(c)}</span>`).join("") + `<span class="tag">${i.lot ? "采购包 " + i.lot : "通用"}</span>` +
|
||||
(i.edited ? '<span class="flag">已修改 · <a class="lnk" href="#" data-demo>查看记录</a></span>' : "");
|
||||
const upd = i.updated ? `<div class="upd"><span class="flag"><b>已被${i.srcName} 更新</b> · 原为 <span class="old">${esc(i.old)}</span></span><button class="btn" data-ack="${i.id}">已核对</button></div>` : "";
|
||||
return `<div class="item ${sel === i.id ? "sel" : ""}" data-item="${i.id}" tabindex="${sel === i.id ? 0 : -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>`;
|
||||
<div class="side"><a class="cite-link" href="#" data-cite>${page(i.cite)}</a></div></div>`;
|
||||
}
|
||||
/* 待确认(§7.6):放在清单顶部,不和正式清单混排;条目是未确认的 AI 产出,左侧 2px 蓝铅笔虚线(Q2) */
|
||||
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>`;
|
||||
return `<section aria-label="待确认"><div class="grp-h ai"><b>待确认</b><span class="fig">${pending.length}</span><p>没找到可靠出处,或把握不大。确认前不参与检查。</p></div>
|
||||
${pending.map((q) => `<div class="item pq" data-q="${q.id}"><span class="imp">${q.imp || ""}</span>
|
||||
<div class="ai-block"><div class="t">${esc(q.title)}</div><div class="meta"><span class="tag ai">AI</span><span class="flag">${q.reason}</span></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>
|
||||
<div class="side">${q.cite ? `<a class="cite-link" href="#" data-qopen>${page(q.cite)}</a>` : '<span class="muted">无出处</span>'}</div></div>`).join("")}</section>`;
|
||||
}
|
||||
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>`);
|
||||
const tb = `<div class="lst-tb"><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></div>') + '<div class="end"></div>';
|
||||
} else {
|
||||
const v = visible();
|
||||
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><div class="grp-h"><b>${g.name}</b><span class="fig">${rows.length}</span></div>${rows.map(itemRow).join("")}</section>` : ""; }).join("");
|
||||
const head = `<p class="lot-note">通用 + 采购包 1 · 已隐藏采购包 2 的 <span class="fig">${D.hiddenLotItems}</span> 条 · <a class="lnk" href="#" data-demo>更改所投的包</a></p>` +
|
||||
(cat === "all" && !kw && !star ? `<div class="rule"><i data-ic="info"></i><p>招标文件 <a class="cite-link" href="#" data-rule>第 <span class="fig">24</span> 页</a>:打「★」的为实质性要求。已识别 <span class="fig m">4</span> 条,请对照原文核对有无遗漏。</p></div>${pendingBox()}` : "");
|
||||
L.innerHTML = tb + head + (body || `<div class="empty sm"><b>没有找到「${esc(kw)}」</b><p>只搜当前分类的条目标题。</p><button class="btn sm" data-clear>清除搜索</button></div>`) + '<div class="end"></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" }); }
|
||||
function select(id, opt = {}) {
|
||||
sel = id;
|
||||
const i = items.find((x) => x.id === id);
|
||||
$$(".lst .item[data-item]").forEach((x) => { const on = x.dataset.item === id; x.classList.toggle("sel", on); x.tabIndex = on ? 0 : -1; });
|
||||
const el = $(`[data-item="${id}"]`);
|
||||
if (el) { if (opt.scroll) el.scrollIntoView({ block: "nearest" }); if (opt.focus) el.focus({ preventScroll: true }); }
|
||||
V.open(i.cite);
|
||||
}
|
||||
function step(d) {
|
||||
const v = visible(); if (!v.length) return;
|
||||
const i = v.findIndex((x) => x.id === sel);
|
||||
const n = v[Math.max(0, Math.min(v.length - 1, (i < 0 ? -1 : i) + d))];
|
||||
if (n) select(n.id, { scroll: true, focus: true });
|
||||
}
|
||||
function render() { status(); cats(); list(); }
|
||||
|
||||
/* ---------- 交互 ---------- */
|
||||
document.addEventListener("click", (e) => {
|
||||
const t = e.target;
|
||||
if (t.closest("[data-demo]")) { e.preventDefault(); toast(DEMO, { ok: false }); return; }
|
||||
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-qopen]")) { e.preventDefault(); V.open(pending.find((x) => x.id === t.closest("[data-q]").dataset.q).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 }); }
|
||||
if (it && !t.closest("button")) { e.preventDefault(); select(it.dataset.item, { focus: 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 (e.ctrlKey || e.metaKey || e.altKey || e.target.matches("input, textarea") || $(".scrim")) return;
|
||||
if (e.key === "?") { Demo.keysDialog(Demo.KEYS.reading); 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);
|
||||
if (e.key === "j" || e.key === "k") { e.preventDefault(); step(e.key === "j" ? 1 : -1); }
|
||||
if ((e.key === "Enter" || e.key === "o") && (e.target.matches("[data-item]") || e.target === document.body)) { e.preventDefault(); select(sel, { scroll: true, focus: true }); }
|
||||
});
|
||||
|
||||
/* 原文查看器宽度:.cmp[data-cmp] 由 demo.js 的 Demo.cmp() 处理(拖动或 ← →;清单不窄于 560) */
|
||||
|
||||
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; }
|
||||
@@ -206,7 +265,7 @@ function adopt(q, c, 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>` : ""}`,
|
||||
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({
|
||||
@@ -214,13 +273,13 @@ $("[data-add]").onclick = () => dialog({
|
||||
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>`,
|
||||
<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("已收到反馈") }); };
|
||||
$("[data-missing]").onclick = (e) => { e.preventDefault(); e.stopPropagation(); 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);
|
||||
select(sel);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user