docs(prototype): 新增 MVP 可点击原型 v0.1

- 13 个页面覆盖 J1–J5:登录、工作台、新建项目、解析、选包、读标、上传投标文件、查标、报告、项目概览、额度、资质库
- 查标页:出处批注与原文抽屉、三种处理、致命项确认需写说明(U4)、忽略原因校验、撤销、批量、键盘逐条处理
- 读标页:分类、待确认区、补遗更新核对、修改记录、原文高亮、确认清单
- 示意场景沿用 02-user-journeys,条款改写自样本 S02、S10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-02 22:02:34 +08:00
parent 700514746f
commit f4dc699041
17 changed files with 1928 additions and 0 deletions

49
docs/prototype/README.md Normal file
View File

@@ -0,0 +1,49 @@
# MVP 可点击原型 v0.1(2026-11-02)
按 `docs/analysis/02-user-journeys.md` 的 J1–J5 做的 HTML 原型,覆盖 MVP 全链路:注册 → 新建项目 → 解析 → 选包 → 读标 → 上传投标文件 → 查标 → 导出报告,以及工作台、项目概览、额度、资质库。
- **保真度**:中保真。直接使用设计规范的变量和组件(`docs/design/system/`),重点验证流程、信息结构和产品红线落地;视觉细节留到 UI 设计阶段精修。
- **示意场景**:某市政务服务中心智慧大厅建设项目(政府采购 · 公开招标 · 2 个采购包,投采购包 1)。条款改写自苏采云样本 S02、S10;项目名、金额、页码、投标响应、人名都是示意。
- **入口**:`index.html`(原型目录,含页面与旅程的对应关系、模拟范围)。
## 运行
```bash
cd docs && python3 -m http.server 8766
# 打开 http://127.0.0.1:8766/prototype/
```
必须从 `docs/` 起服务,因为原型引用了 `../design/system/` 和 `../design/logo/`。宋体和数字字体从 `design/logo/.work/fonts` 加载(不进 git),没有时退回系统字体。设计基准 1440 宽,最小 1280。
## 文件
| 文件 | 内容 |
|---|---|
| `index.html` | 原型目录 |
| `login.html` | 注册 / 登录(含服务协议的围串标禁止条款) |
| `workbench.html` | 工作台 |
| `project-new.html` | 新建项目、上传招标文件;`?addendum=1` 是追加补遗 |
| `parsing.html` | 解析进度、取消、失败演示;`?addendum=1` 是补遗解析 |
| `lots.html` | 选择所投的包 |
| `checklist.html` | 读标:分类、清单、待确认区、补遗更新、修改记录、原文查看器、确认清单 |
| `bid-check.html` | 上传投标文件分册、检查范围、发起检查 |
| `results.html` | 查标:问题处理、出处批注、原文抽屉、键盘操作、批量、需人工核对 |
| `report.html` | 检查报告预览(A4) |
| `project.html` | 项目概览:状态流、AI 摘要、关键日期、文件、检查记录、递交 / 归档 / 删除 |
| `quota.html` | 额度与流水 |
| `qualifications.html` | 资质库 |
| `assets/data.js` | 示意数据(项目、文件页面、清单、检查结果) |
| `assets/app.js` | 页面框架、出处、原文查看器、对话框、轻提示 |
| `assets/app.css` | 页面框架与布局(组件样式在 `design/system/components.css`) |
## 原型阶段发现的问题(2026-11-02 已按建议确认,见 `docs/decisions.md` P1–P2)
| 编号 | 问题 | 结论 |
|---|---|---|
| P1 | 「同一项目 24 小时内复查不扣点」的 24 小时从哪次检查算起?如果从「上一次检查」算,用户每天复查一次就能一直不扣点 | 从**上一次扣点的检查**算起,窗口内的复查不扣点、也不延长窗口 |
| P2 | 「需人工核对」的勾选结果按检查批次存,还是按项目存? | 按项目存,复查时沿用;报告里写勾选人和时间 |
## 和需求、规范的对应
- 每个页面对应的 FR 编号见 `index.html` 的「其他页面」表。
- 产品红线自查:没有出处的结论只出现在「待确认」区;全站无「通过 / 合格 / 保证」字样;红色只用于致命问题;致命问题未处理也能导出和标记已递交;扣点前在按钮旁说明;报告 logo 只在页眉。

View File

@@ -0,0 +1,126 @@
/* 原型专用:页面框架、布局和各页面的局部样式。组件样式在 design/system/components.css。 */
/* ---------- 页面框架 ---------- */
body.app{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr);min-height:100vh;min-width:1280px}
.sidebar{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;border-right:1px solid var(--border-subtle);background:var(--bg-page);padding:var(--sp-5) var(--sp-3)}
.sidebar .brand{display:block;padding:0 var(--sp-2) var(--sp-6)}
.sidebar .brand img{height:30px;display:block}
.nav{display:flex;flex-direction:column;gap:2px}
.nav a{display:flex;align-items:center;gap:var(--sp-3);height:36px;padding:0 var(--sp-3);border-radius:var(--radius-sm);color:var(--text-primary);text-decoration:none}
.nav a:hover{background:var(--bg-hover)}
.nav a.on{background:var(--bg-surface);box-shadow:inset 2px 0 0 var(--c-ink);font-weight:var(--fw-bold)}
.nav svg,.ic{width:18px;height:18px;flex:none;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
.nav .sub{margin:var(--sp-5) var(--sp-3) var(--sp-2);font-size:var(--fs-caption);color:var(--text-secondary)}
.sidebar-foot{margin-top:auto;padding:var(--sp-3);border-top:1px solid var(--border-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.sidebar-foot b{display:block;color:var(--text-primary);font-size:var(--fs-body)}
.main{min-width:0;display:flex;flex-direction:column}
.top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:var(--sp-4);height:52px;padding:0 var(--sp-8);background:var(--bg-page);border-bottom:1px solid var(--border-subtle)}
.crumbs{display:flex;gap:var(--sp-2);color:var(--text-secondary);min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.crumbs a{color:var(--text-secondary);text-decoration:none}
.crumbs a:hover{color:var(--text-primary);text-decoration:underline;text-underline-offset:3px}
.crumbs b{color:var(--text-primary);font-weight:var(--fw-regular);overflow:hidden;text-overflow:ellipsis}
.top-right{margin-left:auto;display:flex;align-items:center;gap:var(--sp-4)}
.quota-chip{display:flex;gap:var(--sp-3);align-items:center;height:28px;padding:0 var(--sp-3);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);color:var(--text-secondary);text-decoration:none;font-size:var(--fs-caption);background:var(--bg-surface)}
.quota-chip .fig{color:var(--text-primary);font-size:14px;margin-left:3px}
.icon-btn{display:inline-grid;place-items:center;width:32px;height:32px;border-radius:var(--radius-sm);color:var(--text-primary);background:none;border:0;cursor:pointer;position:relative}
.icon-btn:hover{background:var(--bg-hover)}
.icon-btn .dot{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%;background:var(--c-ink)}
.content{padding:var(--sp-6) var(--sp-8) var(--sp-16);max-width:calc(var(--content-max) + 64px);width:100%}
.content.wide{max-width:none}
.page-title{display:flex;align-items:flex-end;gap:var(--sp-4);margin-bottom:var(--sp-5)}
.page-title h1{font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-serif);margin:0}
.page-title p{margin:var(--sp-1) 0 0;color:var(--text-secondary)}
.page-title .actions{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
/* 项目页头 + 页签 */
.proj-head{padding:var(--sp-5) var(--sp-8) 0;border-bottom:1px solid var(--border-subtle);background:var(--bg-page)}
.proj-head .line1{display:flex;align-items:flex-start;gap:var(--sp-4)}
.proj-head h1{font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-serif);margin:0}
.proj-head .meta{margin-top:var(--sp-1);color:var(--text-secondary);display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4)}
.proj-head .facts{margin-left:auto;display:flex;gap:var(--sp-6);text-align:right;white-space:nowrap}
.proj-head .facts small{display:block;font-size:var(--fs-caption);color:var(--text-secondary)}
.proj-head .facts .fig{font-size:16px}
.tabs{display:flex;gap:var(--sp-6);margin-top:var(--sp-3)}
.tabs a{padding:var(--sp-2) 0 10px;color:var(--text-secondary);text-decoration:none;border-bottom:2px solid transparent;display:flex;gap:6px;align-items:center}
.tabs a:hover{color:var(--text-primary)}
.tabs a.on{color:var(--text-primary);font-weight:var(--fw-bold);border-bottom-color:var(--c-ink)}
/* ---------- 原文查看器 ---------- */
.viewer{min-height:0}
.viewer-bar{flex-wrap:wrap}
.viewer-bar .doc-name{display:flex;gap:var(--sp-2);align-items:baseline;min-width:0}
.viewer-bar .doc-file{color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}
.viewer-body{flex:1;overflow:auto;padding:var(--sp-4) 0}
.viewer .sheet{min-height:560px;margin:0 auto;width:calc(100% - 48px);max-width:640px;position:relative}
.sheet .pno{position:absolute;bottom:14px;left:0;right:0;text-align:center;font:12px var(--font-figures);color:#999}
.sheet .h{font-weight:700;margin:4px 0 10px}
.sheet .stub{height:9px;background:#EEE;margin:12px 0;border-radius:1px}
.sheet .stub.s{width:60%}
.viewer-empty{color:var(--text-secondary);font-size:var(--fs-caption);text-align:center;margin-top:var(--sp-4)}
.viewer-note{font-size:var(--fs-caption);color:var(--text-secondary);padding:0 var(--sp-6) var(--sp-3)}
/* 抽屉(查标页从右侧滑出原文) */
.drawer{position:fixed;top:0;right:0;bottom:0;width:min(46vw,720px);z-index:40;background:var(--bg-sunken);box-shadow:var(--shadow-overlay);transform:translateX(100%);transition:transform var(--dur-base) var(--ease);display:flex;flex-direction:column}
.drawer.open{transform:none}
.drawer .viewer{flex:1;border:0;border-radius:0}
.drawer-close{margin-left:var(--sp-2)}
/* ---------- 对话框与轻提示 ---------- */
.scrim{position:fixed;inset:0;background:rgba(29,39,51,.32);z-index:50;display:grid;place-items:center}
.dialog{width:480px;max-width:calc(100vw - 32px);background:var(--bg-surface);border-radius:var(--radius-md);box-shadow:var(--shadow-overlay);padding:var(--sp-6)}
.dialog.wide{width:640px}
.dialog h2{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0 0 var(--sp-3)}
.dialog .body > p{margin:0 0 var(--sp-3)}
.dialog .foot{display:flex;justify-content:flex-end;gap:var(--sp-2);margin-top:var(--sp-6)}
.toast{position:fixed;left:50%;bottom:32px;transform:translateX(-50%);z-index:60;background:var(--c-ink);color:#fff;padding:10px 16px;border-radius:var(--radius-sm);box-shadow:var(--shadow-overlay);display:flex;gap:var(--sp-3);align-items:center}
.toast a{color:#fff}
textarea.input{height:auto;min-height:72px;padding:8px 10px;resize:vertical;line-height:var(--lh-body);width:100%}
.field .input{width:100%}
.field{max-width:none}
/* ---------- 通用小件 ---------- */
.muted{color:var(--text-secondary)}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.stack > * + *{margin-top:var(--sp-4)}
.section-h{display:flex;align-items:baseline;gap:var(--sp-3);margin:var(--sp-8) 0 var(--sp-3)}
.section-h h2{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0}
.section-h .more{margin-left:auto}
.kbd{display:inline-block;min-width:20px;padding:0 5px;border:1px solid var(--border-control);border-bottom-width:2px;border-radius:3px;font:12px/18px var(--font-figures);text-align:center;background:var(--bg-surface)}
.seg{display:inline-flex;border:1px solid var(--border-control);border-radius:var(--radius-sm);overflow:hidden;background:var(--bg-surface)}
.seg button{height:28px;padding:0 10px;border:0;background:none;font:var(--fs-body) var(--font-sans);color:var(--text-primary);cursor:pointer;display:flex;gap:6px;align-items:center}
.seg button + button{border-left:1px solid var(--border-subtle)}
.seg button.on{background:var(--c-ink);color:#fff}
.seg button.on .sev.hint{background:#fff}
.chk{display:flex;gap:var(--sp-3);align-items:flex-start;padding:var(--sp-3) 0;border-top:1px solid var(--border-subtle)}
.chk:first-child{border-top:0}
.chk input{width:16px;height:16px;margin:3px 0 0;accent-color:var(--c-ink)}
.free-badge{font-size:var(--fs-caption);border:1px solid var(--border-control);padding:0 6px;border-radius:2px}
.placeholder{border:1px dashed var(--border-control);color:var(--text-secondary);border-radius:var(--radius-sm);padding:var(--sp-4);text-align:center}
/* 状态流 */
.flowline{display:flex;align-items:center;gap:0;margin:var(--sp-2) 0}
.flowline .st{display:flex;align-items:center;gap:var(--sp-2);color:var(--text-secondary);white-space:nowrap}
.flowline .st i{width:10px;height:10px;border-radius:50%;border:1.5px solid var(--border-control);flex:none}
.flowline .st.done{color:var(--text-primary)}
.flowline .st.done i{background:var(--c-ink);border-color:var(--c-ink)}
.flowline .st.now{color:var(--text-primary);font-weight:var(--fw-bold)}
.flowline .st.now i{border:3px solid var(--c-ink)}
.flowline .ln{flex:1;min-width:24px;height:1px;background:var(--border-control);margin:0 var(--sp-3)}
/* 键值列表 */
.kv{display:grid;grid-template-columns:112px 1fr;gap:var(--sp-2) var(--sp-4);margin:0}
.kv dt{color:var(--text-secondary)}
.kv dd{margin:0}
/* 上传区 */
.drop{border:1.5px dashed var(--border-control);border-radius:var(--radius-sm);background:var(--bg-surface);padding:var(--sp-10) var(--sp-6);text-align:center;cursor:pointer}
.drop:hover{border-color:var(--c-ink)}
.drop b{display:block;font-size:var(--fs-title);margin-bottom:var(--sp-1)}
.file-row{display:grid;grid-template-columns:32px minmax(0,1fr) auto auto;gap:var(--sp-3);align-items:center;padding:var(--sp-3) var(--sp-4);background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm)}
.file-row + .file-row{margin-top:var(--sp-2)}
.file-row .ext{font:600 11px/1 var(--font-figures);border:1px solid var(--border-control);border-radius:2px;padding:4px 0;text-align:center}
.file-row select{height:28px;border:1px solid var(--border-control);border-radius:var(--radius-sm);background:#fff;font:var(--fs-body) var(--font-sans);padding:0 6px}
@media (prefers-reduced-motion:reduce){.drawer{transition:none}}

156
docs/prototype/assets/app.js vendored Normal file
View File

@@ -0,0 +1,156 @@
/* 原型公共脚本:页面框架、出处、原文查看器、对话框、轻提示。只模拟交互,不连后端。 */
(function () {
const D = window.PROTO;
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
/* Lucide 图标(ISC 许可)的简化路径 */
const ICONS = {
grid: '<rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="M12 14l4-4"/><path d="M3.3 19a9 9 0 1 1 17.4 0"/>',
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.9 1.9 0 0 0 3.4 0"/>',
help: '<circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01"/>',
x: '<path d="M18 6 6 18M6 6l12 12"/>',
plus: '<path d="M12 5v14M5 12h14"/>',
upload: '<path d="M12 3v12M7 8l5-5 5 5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>',
file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>',
keyboard: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M6 9h.01M10 9h.01M14 9h.01M18 9h.01M7 15h10"/>'
};
const icon = (n, cls = "ic") => `<svg class="${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n]}</svg>`;
/* ---------- 页面框架 ---------- */
function shell(opt) {
const P = D.project;
const nav = [
["workbench.html", "grid", "工作台", "workbench"],
["qualifications.html", "cert", "资质库", "quals"],
["quota.html", "gauge", "额度", "quota"]
].map(([h, i, t, k]) => `<a href="${h}" class="${opt.nav === k ? "on" : ""}">${icon(i)}${t}</a>`).join("");
const side = `<aside class="sidebar">
<a class="brand" href="workbench.html"><img src="../design/logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">${nav}<div class="sub">原型</div><a href="index.html">${icon("help")}原型目录</a></nav>
<div class="sidebar-foot"><b>${D.user.name}</b>${D.user.company}<br>${D.user.plan} · 至 2027-10-31</div>
</aside>`;
const crumbs = (opt.crumbs || []).map((c, i, a) => i < a.length - 1 ? `<a href="${c[1]}">${c[0]}</a><span>/</span>` : `<b>${c[0]}</b>`).join("");
const top = `<header class="top">
<div class="crumbs">${crumbs}</div>
<div class="top-right">
<a class="quota-chip" href="quota.html" title="本期剩余额度">解析<span class="fig">${D.quota.parse.left}</span>检查<span class="fig">${D.quota.check.left}</span></a>
<button class="icon-btn" aria-label="通知" data-notice>${icon("bell")}<span class="dot"></span></button>
</div>
</header>`;
let head = "";
if (opt.tab) {
const tabs = [["project.html", "概览", "overview"], ["checklist.html", "读标", "checklist"], ["results.html", "查标", "results"], ["report.html", "报告", "report"]]
.map(([h, t, k]) => `<a href="${h}" class="${opt.tab === k ? "on" : ""}">${t}${k === "results" ? ' <span class="sev fatal" data-fatal-count>致命<span class="fig">3</span></span>' : ""}</a>`).join("");
head = `<div class="proj-head"><div class="line1"><div>
<h1>${P.name}</h1>
<div class="meta"><span>${P.buyer}</span><span>${P.regime} · ${P.method}</span><span>投 采购包 1(共 2 个包)</span><span class="fig" style="font-weight:500">${P.no}</span></div>
</div>
<div class="facts">
<div><small>投标截止</small><span class="fig">${P.deadlineText}</span> · 剩 <span class="fig">${P.daysLeft}</span> 天</div>
<div><small>状态</small><span class="tag status">${P.status}</span></div>
<div><small>就绪</small><span data-ready>未就绪</span></div>
</div></div>
<nav class="tabs" aria-label="项目">${tabs}</nav></div>`;
}
document.head.insertAdjacentHTML("beforeend", '<link rel="icon" href="../design/logo/favicon.svg">');
document.body.classList.add("app");
const main = $("main");
main.classList.add("main");
document.body.insertAdjacentHTML("afterbegin", side);
main.insertAdjacentHTML("afterbegin", top + head);
$("[data-notice]").addEventListener("click", () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。"));
}
/* ---------- 出处 ---------- */
function citeLabel(c) {
const d = D.docs[c[0]];
return `${d.name} · <span class="fig">第 ${c[1]} 页</span>`;
}
function quote(c) {
const p = (D.pages[c[0]] || {})[c[1]] || [];
const hit = p.find((x) => x.id === c[2]);
return hit ? hit.t : "";
}
/* 段落里只标出关键原句;没有关键原句时整段标出 */
function marked(text, key, tag = "mark") {
const i = key ? text.indexOf(key) : -1;
if (i < 0) return `<${tag}>${esc(text)}</${tag.split(" ")[0]}>`;
return esc(text.slice(0, i)) + `<${tag}>${esc(key)}</${tag.split(" ")[0]}>` + esc(text.slice(i + key.length));
}
function cite(c, opt = {}) {
return `<div class="cite"><a class="cite-src" href="#" data-open="${esc(c.join("|"))}">${citeLabel(c)}</a>${opt.noQuote ? "" : `<q>${marked(quote(c), c[3])}</q>`}</div>`;
}
/* ---------- 原文查看器 ---------- */
function viewer(el, opt = {}) {
let cur = null, list = [];
el.classList.add("viewer");
el.innerHTML = `<div class="viewer-bar">
<span class="doc-name"><b data-v-name>原文</b><span class="doc-file" data-v-file></span></span>
<div class="pager"><button class="btn sm" data-v-prev>上一处</button><span class="fig" data-v-page></span><button class="btn sm" data-v-next>下一处</button>${opt.closable ? `<button class="icon-btn drawer-close" aria-label="关闭原文" data-v-close>${icon("x")}</button>` : ""}</div>
</div>
<div class="viewer-body" data-v-body><p class="viewer-empty">点击清单项或出处,在这里查看原文并高亮原句。</p></div>`;
const body = $("[data-v-body]", el);
function render() {
const [doc, page, hl] = cur;
const d = D.docs[doc];
$("[data-v-name]", el).textContent = d.name;
$("[data-v-file]", el).textContent = d.file;
$("[data-v-page]", el).textContent = `第 ${page} / ${d.pages} 页`;
const paras = (D.pages[doc] || {})[page];
const inner = paras
? paras.map((p) => p.h ? `<p class="h">${esc(p.t)}</p>` : `<p class="${p.faint ? "faint" : ""}">${p.id === hl ? marked(p.t, cur[3], 'span class="hl on"') : esc(p.t)}</p>`).join("")
: '<div class="stub"></div><div class="stub"></div><div class="stub s"></div>';
body.innerHTML = `<div class="sheet">${inner}<div class="stub"></div><div class="stub s"></div><span class="pno">— ${page} —</span></div>
<p class="viewer-note">${doc === "B1" && page === 3 ? "扫描页 · 文字由 OCR 识别,请以原图为准。" : "页码以规范化 PDF 为准(原型只做了被引用的页面)。"}</p>`;
const on = $(".hl.on", body);
if (on) on.scrollIntoView({ block: "center", behavior: "smooth" });
const i = list.findIndex((x) => x.join("|") === cur.join("|"));
$("[data-v-prev]", el).disabled = i <= 0;
$("[data-v-next]", el).disabled = i < 0 || i >= list.length - 1;
}
function open(c, all) {
cur = c; list = all || [c];
render();
if (opt.onOpen) opt.onOpen();
}
$("[data-v-prev]", el).onclick = () => { const i = list.findIndex((x) => x.join("|") === cur.join("|")); if (i > 0) open(list[i - 1], list); };
$("[data-v-next]", el).onclick = () => { const i = list.findIndex((x) => x.join("|") === cur.join("|")); if (i < list.length - 1) open(list[i + 1], list); };
if (opt.closable) $("[data-v-close]", el).onclick = opt.onClose;
return { open };
}
/* ---------- 对话框与轻提示 ---------- */
function dialog({ title, body, ok = "确定", cancel = "取消", wide = false, onOk, onOpen }) {
const s = document.createElement("div");
s.className = "scrim";
s.innerHTML = `<div class="dialog ${wide ? "wide" : ""}" role="dialog" aria-modal="true" aria-labelledby="dlg-t">
<h2 id="dlg-t">${title}</h2><div class="body">${body}</div>
<div class="foot">${cancel ? `<button class="btn" data-cancel>${cancel}</button>` : ""}<button class="btn primary" data-ok>${ok}</button></div></div>`;
document.body.appendChild(s);
const close = () => { s.remove(); document.removeEventListener("keydown", onKey); };
const onKey = (e) => { if (e.key === "Escape") close(); };
document.addEventListener("keydown", onKey);
s.addEventListener("click", (e) => { if (e.target === s) close(); });
if (cancel) $("[data-cancel]", s).onclick = close;
$("[data-ok]", s).onclick = () => { if (!onOk || onOk(s) !== false) close(); };
if (onOpen) onOpen(s);
const f = $("textarea, input, [data-ok]", s);
if (f) f.focus();
return s;
}
let toastTimer;
function toast(msg) {
let t = $(".toast");
if (!t) { t = document.createElement("div"); t.className = "toast"; t.setAttribute("role", "status"); document.body.appendChild(t); }
t.innerHTML = msg;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.remove(), 4000);
}
window.App = { D, $, $$, esc, icon, shell, cite, citeLabel, quote, viewer, dialog, toast };
})();

266
docs/prototype/assets/data.js vendored Normal file
View File

@@ -0,0 +1,266 @@
/* 原型示意数据。场景见 docs/analysis/02-user-journeys.md:
* 某市政务服务中心智慧大厅建设项目(政府采购 · 公开招标 · 2 个采购包,投采购包 1)。
* 条款文字改写自苏采云样本 S02、S10;项目名、金额、页码、投标响应、人名均为示意。 */
window.PROTO = {
today: "2026-11-02",
user: { name: "王敏", company: "南京某信息技术有限公司", plan: "专业版" },
quota: { parse: { left: 37, total: 50 }, check: { left: 12, total: 20 } },
project: {
id: "p1",
name: "某市政务服务中心智慧大厅建设项目",
no: "ZC-2026-0418",
buyer: "某市政务服务管理办公室",
agent: "某市公共资源交易中心",
regime: "政府采购",
method: "公开招标",
deadline: "2026-11-20 09:30",
deadlineText: "11月20日 09:30",
daysLeft: 17,
status: "已检查",
lots: [
{ no: 1, name: "智慧大厅软硬件集成", budget: "286.00", cap: "286.00", bid: true },
{ no: 2, name: "机房网络设备更新", budget: "132.00", cap: "132.00", bid: false }
]
},
/* 文件:role = tender 招标文件 / addendum 补遗 / bid 投标分册 */
docs: {
T: { name: "招标文件", file: "ZC-2026-0418 采购文件.pdf", role: "tender", pages: 186 },
A1: { name: "补遗 1", file: "关于 ZC-2026-0418 的更正公告.pdf", role: "addendum", pages: 2 },
B1: { name: "商务册", file: "商务册-v3.docx", role: "bid", pages: 64 },
B2: { name: "技术册", file: "技术册-v3.docx", role: "bid", pages: 212 },
B3: { name: "报价册", file: "报价册-v2.docx", role: "bid", pages: 8 }
},
/* 原文页面(只做了被引用到的页)。出处格式:[文件, 页码, 段落 id, 关键原句(可选,只高亮这一句)] */
pages: {
T: {
2: [
{ t: "一、项目基本情况", h: 1 },
{ t: "项目编号:ZC-2026-0418" },
{ t: "项目名称:某市政务服务中心智慧大厅建设项目" },
{ t: "采购方式:公开招标" },
{ id: "t2-cap", t: "预算金额:418.00万元。最高限价:418.00万元(采购包1:286.00万元,采购包2:132.00万元)。" },
{ t: "采购需求:详见第六章《采购需求》。" },
{ id: "t2-sme", t: "本项目(是/否)专门面向中小企业采购:否。对小微企业给予价格扣除优惠。" },
{ t: "合同履行期限:合同签订后 90 日历天内完成交付并通过验收。" },
{ id: "t2-joint", t: "本项目(是/否)接受联合体投标:否。" }
],
6: [
{ t: "四、提交投标文件截止时间、开标时间和地点", h: 1 },
{ id: "t6-deadline", t: "1.提交投标文件截止时间、开标时间:2026年11月20日09时30分(北京时间)。" },
{ id: "t6-place", t: "2.投标文件递交方式:通过政府采购电子交易平台在线递交加密的电子投标文件,不接受纸质投标文件。" },
{ t: "3.开标地点:某市公共资源交易中心开标室(远程不见面开标)。" },
{ id: "t6-ask", t: "4.对采购文件的询问或质疑,应在采购文件公告期限届满之日起七个工作日内以书面形式提出。" }
],
18: [
{ t: "34. 投标人存在下列情况之一的,投标无效:", h: 1 },
{ id: "t18-seal", t: "(一)投标文件未按招标文件要求签署、盖章的;" },
{ id: "t18-qual", t: "(二)不具备招标文件中规定的资格要求的;" },
{ id: "t18-cap", t: "(三)报价超过招标文件中规定的预算金额或者最高限价的;" },
{ t: "(四)投标文件含有采购人不能接受的附加条件的;" },
{ id: "t18-valid", t: "(五)投标文件中承诺的投标有效期少于招标文件中载明的投标有效期;" },
{ t: "(六)法律、法规和招标文件规定的其他无效情形。" }
],
21: [
{ t: "投标资料表", h: 1 },
{ id: "t21-valid", t: "投标有效期:90天。" },
{ id: "t21-deposit", t: "投标保证金:本项目不收取投标保证金。" },
{ id: "t21-copies", t: "投标文件份数:电子投标文件1份,须使用CA数字证书加密后上传。" },
{ id: "t21-quote", t: "20.5 投标人只能提交一个投标报价,提交2个以上投标报价的投标文件按照投标无效处理。" }
],
23: [
{ t: "二、资格条件(“1”~“7”项必须提供,否则投标无效)", h: 1 },
{ id: "t23-license", t: "1.投标人合法有效的法人或者其他组织的营业执照等证明文件。以上证明文件提供扫描件。" },
{ id: "t23-fin", t: "2.财务状况报告,至少提供:(1)本项目开标时间前6个月内任一时间截点的资产负债表的扫描件1份;(2)本项目开标时间前6个月内任何1月(不含开标当月)利润表月报表的扫描件1份。" },
{ id: "t23-tax", t: "3.本项目开标时间前6个月内任何1月的依法缴纳税收和社会保障资金的相关材料的扫描件。" },
{ id: "t23-ability", t: "4.《具备履行合同所必需的设备和专业技术能力的书面声明》(加盖电子签章)。" },
{ id: "t23-record", t: "5.参加政府采购活动前3年内在经营活动中没有重大违法记录的书面声明(加盖电子签章)。" },
{ id: "t23-credit", t: "8.投标人无被列入失信被执行人、重大税收违法案件当事人名单、政府采购严重失信行为记录名单的信用记录。" }
],
24: [
{ t: "三、符合条件(必须提供,否则投标无效)", h: 1 },
{ id: "t24-quote", t: "1.报价符合性审查:投标报价不得超过采购包最高限价;投标报价低于采购包最高限价45%的,提供与报价合理性相关的书面说明及必要的证明材料。" },
{ id: "t24-letter", t: "3.《投标函》(加盖电子签章,格式见招标文件第七章)。" },
{ id: "t24-auth", t: "4.《法定代表人授权委托书》(法定代表人签字或盖章并加盖单位电子签章)。" },
{ id: "t24-dev", t: "5.《偏离表》(加盖电子签章,格式见招标文件第七章)。" },
{ id: "t24-mark", t: "本文件中打“★”内容为实质性要求,投标人须全部满足,否则投标无效。" }
],
31: [
{ t: "第四章 评标标准(综合评分法)", h: 1 },
{ id: "t31-price", t: "价格分(10分):满足招标文件要求且投标价格最低的投标报价为评标基准价,其价格分为满分。" },
{ id: "t31-tech", t: "技术规格(40分):完全满足★和▲参数得基础分,▲参数每负偏离一项扣2分,一般参数每负偏离一项扣0.5分。" },
{ id: "t31-plan", t: "项目实施方案(20分):方案内容齐全、符合实际切实可行且无缺陷得20分,每存在一处缺陷扣2分。" },
{ id: "t31-service", t: "售后服务方案(10分):对第六章“四、售后服务要求”进行完善并详细描述,每缺少一项内容扣1分。" },
{ id: "t31-iso", t: "体系认证(6分):投标人具有有效的ISO9001、ISO27001、ISO20000认证证书,每提供一项得2分(以投标截止日在有效期内为准,提供证书扫描件)。" },
{ id: "t31-case", t: "类似业绩(8分):近三年(2023年1月1日以来)承担过政务服务大厅智能化项目的,每提供一个得2分,最多8分(提供合同关键页扫描件)。" }
],
41: [
{ t: "采购包1 · 货物需求一览表", h: 1 },
{ t: "序号 3 应用服务器 2 台", faint: true },
{ id: "t41-server", t: "★ 2U机架式服务器,CPU:配置≥2颗海光架构32C 2.7GHz,内存: ≥512G DDR4,系统盘: 2块≥480G SSD,存储盘: ≥2块8T SATA、≥4块3.84T SSD……" },
{ id: "t41-nic", t: "网络:配置≥4个1GbE 接口,≥4个10GbE 光接口(含光模块),电源:配置2块800W冗余电源。" },
{ id: "t41-cpu", t: "★ 设备采用国产自主可控CPU芯片,投标文件中提供国产化适配承诺函,格式自制。" },
{ t: "备注:加★为核心技术参数;加▲为重要技术参数;其他为通用技术参数。" }
],
44: [
{ t: "序号 5 核心交换机 1 台", faint: true },
{ id: "t44-bfd", t: "★ 支持硬件BFD,最小报文发送间隔≤50ms,提供相关证明材料(包含但不限于产品官网公开参数截图证明等)。" },
{ id: "t44-port", t: "▲ 交换容量≥19.2Tbps,包转发率≥2880Mpps,业务槽位≥4个。" }
],
47: [
{ t: "序号 9 排队叫号一体机 6 台", faint: true },
{ id: "t47-screen", t: "显示屏尺寸≥21.5英寸,分辨率≥1920×1080,支持身份证、社保卡读取。" },
{ id: "t47-ups", t: "▲ 自助服务终端须提供不少于3年原厂质保,提供原厂质保承诺函。" }
]
},
A1: {
1: [
{ t: "关于 ZC-2026-0418 的更正公告(补遗 1)", h: 1 },
{ id: "a1-deadline", t: "一、提交投标文件截止时间、开标时间更正为:2026年11月20日09时30分(北京时间)。原为2026年11月18日09时30分。" },
{ id: "a1-param", t: "二、采购包1 序号3 应用服务器“系统盘”参数更正为:2块≥960G SSD。" }
]
},
B1: {
3: [
{ t: "一、投标函", h: 1 },
{ t: "致:某市公共资源交易中心" },
{ t: "我方已仔细研究了某市政务服务中心智慧大厅建设项目(项目编号:ZC-2026-0418)招标文件的全部内容……" },
{ id: "b1-date", t: "投标人(电子签章):南京某信息技术有限公司  日期:2026年11月25日" }
],
5: [
{ t: "二、法定代表人授权委托书", h: 1 },
{ t: "本授权委托书声明:我李某某系南京某信息技术有限公司的法定代表人,现授权委托王敏为我公司代理人……" },
{ id: "b1-auth-seal", t: "法定代表人(签字或盖章):李某某  投标人(电子签章):(签章位置为空)" }
],
27: [
{ t: "九、类似项目业绩", h: 1 },
{ id: "b1-oldname", t: "我公司承担的某区智慧校园一期项目(项目编号 QJ-2025-0112)已顺利通过验收,本项目将沿用该项目的实施团队……" }
],
38: [
{ t: "十二、体系认证证书", h: 1 },
{ id: "b1-iso", t: "ISO/IEC 27001 信息安全管理体系认证证书 证书编号:01226IS0458 有效期至:2026年11月15日" }
]
},
B2: {
12: [
{ t: "技术偏离表(采购包1)", h: 1 },
{ t: "第 11 行 CPU:2颗海光 7380 32C 2.7GHz 无偏离", faint: true },
{ id: "b2-mem", t: "第 12 行 内存:256G DDR4 无偏离" },
{ t: "第 13 行 系统盘:2块960G SSD 无偏离", faint: true }
],
58: [
{ t: "核心交换机技术响应", h: 1 },
{ id: "b2-bfd", t: "支持硬件BFD,最小报文发送间隔3.3ms,满足要求。" }
],
77: [
{ t: "排队叫号一体机技术响应", h: 1 },
{ id: "b2-screen", t: "显示屏:21英寸 LED 背光液晶屏,分辨率1920×1080。" }
]
},
B3: {
1: [
{ t: "开标一览表", h: 1 },
{ t: "项目名称:某市政务服务中心智慧大厅建设项目 采购包:1" },
{ id: "b3-total", t: "投标总价(小写):2876000.00 元 (大写):贰佰捌拾柒万陆仟元整" }
],
3: [
{ t: "分项价格表(采购包1)", h: 1 },
{ id: "b3-sum", t: "合计:2868000.00 元" }
]
}
},
/* 清单:cat = qual 资格 / invalid 无效条款 / score 评分 / date 日期与递交 / param 技术参数 */
categories: [
{ id: "qual", name: "资格条件" },
{ id: "invalid", name: "无效条款" },
{ id: "score", name: "评分办法" },
{ id: "date", name: "日期与递交" },
{ id: "param", name: "技术参数" }
],
checklist: [
{ id: "c1", cats: ["qual", "invalid"], title: "营业执照等主体资格证明文件(扫描件)", lot: 0, src: "ai", cite: ["T", 23, "t23-license"] },
{ id: "c2", cats: ["qual", "invalid"], title: "财务状况报告:资产负债表 + 利润表月报表(开标前 6 个月内)", lot: 0, src: "ai", cite: ["T", 23, "t23-fin"] },
{ id: "c3", cats: ["qual", "invalid"], title: "依法缴纳税收和社会保障资金的材料(开标前 6 个月内任 1 月)", lot: 0, src: "ai", cite: ["T", 23, "t23-tax"] },
{ id: "c4", cats: ["qual", "invalid"], title: "具备履行合同所必需的设备和专业技术能力的书面声明(加盖电子签章)", lot: 0, src: "ai", cite: ["T", 23, "t23-ability"] },
{ id: "c5", cats: ["qual", "invalid"], title: "前 3 年内无重大违法记录的书面声明(加盖电子签章)", lot: 0, src: "ai", cite: ["T", 23, "t23-record"] },
{ id: "c6", cats: ["qual"], title: "信用记录:未被列入失信被执行人等名单", lot: 0, src: "ai", cite: ["T", 23, "t23-credit"] },
{ id: "c7", cats: ["invalid"], title: "投标文件按要求签署、盖章", lot: 0, src: "ai", cite: ["T", 18, "t18-seal"] },
{ id: "c8", cats: ["invalid"], title: "报价不超过采购包 1 最高限价 286.00 万元", lot: 1, src: "ai", cite: ["T", 2, "t2-cap", "采购包1:286.00万元"] },
{ id: "c9", cats: ["invalid"], title: "只能提交一个投标报价", lot: 0, src: "ai", cite: ["T", 21, "t21-quote"] },
{ id: "c10", cats: ["invalid", "date"], title: "投标有效期不少于 90 天", lot: 0, src: "ai", cite: ["T", 21, "t21-valid"] },
{ id: "c11", cats: ["invalid"], title: "《法定代表人授权委托书》签字或盖章并加盖单位电子签章", lot: 0, src: "ai", cite: ["T", 24, "t24-auth"] },
{ id: "c12", cats: ["score"], title: "技术规格 40 分:▲ 参数每负偏离一项扣 2 分", lot: 0, src: "ai", cite: ["T", 31, "t31-tech"] },
{ id: "c13", cats: ["score"], title: "项目实施方案 20 分", lot: 0, src: "ai", cite: ["T", 31, "t31-plan"] },
{ id: "c14", cats: ["score"], title: "体系认证 6 分:ISO9001 / ISO27001 / ISO20000,投标截止日在有效期内", lot: 0, src: "ai", cite: ["T", 31, "t31-iso"] },
{ id: "c15", cats: ["score"], title: "类似业绩 8 分:2023 年以来政务服务大厅智能化项目", lot: 0, src: "ai", cite: ["T", 31, "t31-case"] },
{ id: "c16", cats: ["date"], title: "提交投标文件截止时间:2026年11月20日 09:30", old: "2026年11月18日 09:30", lot: 0, src: "addendum", srcName: "补遗 1", cite: ["A1", 1, "a1-deadline"], updated: true },
{ id: "c17", cats: ["date"], title: "递交方式:电子交易平台在线递交,CA 加密", lot: 0, src: "ai", cite: ["T", 6, "t6-place"] },
{ id: "c18", cats: ["date"], title: "投标保证金:不收取", lot: 0, src: "ai", cite: ["T", 21, "t21-deposit"] },
{ id: "c19", cats: ["param", "invalid"], imp: "★", title: "应用服务器:内存 ≥ 512G DDR4", lot: 1, src: "ai", cite: ["T", 41, "t41-server", "内存: ≥512G DDR4"] },
{ id: "c20", cats: ["param", "invalid"], imp: "★", title: "应用服务器:国产自主可控 CPU,提供国产化适配承诺函", lot: 1, src: "ai", cite: ["T", 41, "t41-cpu"] },
{ id: "c21", cats: ["param"], imp: "★", title: "应用服务器:系统盘 2 块 ≥ 960G SSD", old: "2 块 ≥ 480G SSD", lot: 1, src: "addendum", srcName: "补遗 1", cite: ["A1", 1, "a1-param"], updated: true },
{ id: "c22", cats: ["param", "invalid"], imp: "★", title: "核心交换机:支持硬件 BFD,最小发送间隔 ≤ 50ms,提供证明材料", lot: 1, src: "ai", cite: ["T", 44, "t44-bfd", "最小报文发送间隔≤50ms,提供相关证明材料"] },
{ id: "c23", cats: ["param"], imp: "▲", title: "核心交换机:交换容量 ≥ 19.2Tbps,包转发率 ≥ 2880Mpps", lot: 1, src: "ai", cite: ["T", 44, "t44-port"] },
{ id: "c24", cats: ["param"], imp: "", title: "排队叫号一体机:显示屏 ≥ 21.5 英寸", lot: 1, src: "ai", cite: ["T", 47, "t47-screen", "显示屏尺寸≥21.5英寸"] },
{ id: "c25", cats: ["param"], imp: "▲", title: "自助服务终端:不少于 3 年原厂质保,提供承诺函", lot: 1, src: "ai", cite: ["T", 47, "t47-ups"] },
{ id: "c26", cats: ["qual"], title: "不接受联合体投标", lot: 0, src: "manual", edited: true, cite: ["T", 2, "t2-joint"] }
],
hiddenLotItems: 9, /* 采购包 2 的条目数(未投,折叠) */
pending: [
{ id: "q1", title: "投标文件须胶装成册并逐页加盖骑缝章", reason: "未找到出处", src: "ai" },
{ id: "q2", imp: "★", title: "核心产品须提供原厂授权书", reason: "把握不大", src: "ai", cite: ["T", 44, "t44-port"] }
],
/* 检查结果(第 2 次检查)。sev = fatal / major / hint;status = todo / ok / fixed / ignored */
check: {
no: 2, time: "11月2日 14:20", rules: "IT 集成 2026.11",
files: "商务册 v3、技术册 v3、报价册 v2",
resolved: 2,
findings: [
{ id: "f1", sev: "fatal", item: "c8", title: "报价超过采购包 1 的最高限价", flag: "新增",
text: ["开标一览表报价 287.60 万元,采购包 1 最高限价 286.00 万元,超出 1.60 万元。", "这一项会导致投标无效。请核对开标一览表的报价。"],
cites: [["T", 2, "t2-cap", "采购包1:286.00万元"], ["B3", 1, "b3-total", "2876000.00 元"]] },
{ id: "f2", sev: "fatal", item: "c19", title: "★ 条款疑似不满足:应用服务器内存", flag: "沿用 · 上次标记为已修改,问题仍在",
text: ["技术偏离表第 12 行响应「内存 256G DDR4」,要求「≥512G DDR4」。", "这一项会导致投标无效。请在技术偏离表第 12 行和对应产品参数中更正内存配置。"],
cites: [["T", 41, "t41-server", "内存: ≥512G DDR4"], ["B2", 12, "b2-mem", "内存:256G DDR4"]] },
{ id: "f3", sev: "fatal", item: "c11", title: "授权委托书的单位签章位置未发现盖章", flag: "新增",
text: ["商务册第 5 页《法定代表人授权委托书》的「投标人(电子签章)」位置是空的。本检查只看有无,不鉴定真伪。", "这一项会导致投标无效。请在递交前补盖单位电子签章。"],
cites: [["T", 24, "t24-auth", "加盖单位电子签章"], ["B1", 5, "b1-auth-seal", "投标人(电子签章):(签章位置为空)"]] },
{ id: "f4", sev: "major", item: "c22", title: "缺少 ★ 参数的证明材料:核心交换机硬件 BFD",
text: ["已响应「支持硬件 BFD」,但没有找到官网参数截图等证明材料。请在技术册补充证明材料。"],
cites: [["T", 44, "t44-bfd", "提供相关证明材料(包含但不限于产品官网公开参数截图证明等)"], ["B2", 58, "b2-bfd", "支持硬件BFD"]], missing: "证明材料未找到 · 已查找:技术册(全部章节)、商务册(附件)" },
{ id: "f5", sev: "major", item: "c8", title: "开标一览表与分项价格表合计不一致",
text: ["开标一览表 287.60 万元,分项价格表合计 286.80 万元,相差 0.80 万元。", "按招标文件规定,两者不一致时以开标一览表为准,不作无效处理;但会影响价格评审。请核对两表。"],
cites: [["B3", 1, "b3-total", "2876000.00 元"], ["B3", 3, "b3-sum", "2868000.00 元"]] },
{ id: "f6", sev: "major", item: "c14", title: "资质过期预警:ISO27001 证书在投标截止日前到期", flag: "新增",
text: ["证书有效期至 2026年11月15日,早于投标截止日 11月20日。按评分办法,这一项可能不得分。", "请上传换发后的证书,或在资质库更新有效期。"],
cites: [["T", 31, "t31-iso", "以投标截止日在有效期内为准"], ["B1", 38, "b1-iso", "有效期至:2026年11月15日"]] },
{ id: "f7", sev: "hint", item: "c10", title: "投标函落款日期晚于投标截止日", flag: "沿用", status: "ok", by: "王敏", at: "11月1日 16:05",
text: ["投标函落款「2026年11月25日」,投标截止时间是 2026年11月20日 09:30。"],
cites: [["B1", 3, "b1-date", "日期:2026年11月25日"]] },
{ id: "f8", sev: "hint", item: "c15", title: "投标文件里出现了其他项目的名称", flag: "新增",
text: ["商务册第 27 页出现「某区智慧校园一期项目」及其项目编号。如果是业绩介绍可以忽略;如果是照搬旧标书留下的,请修改。"],
cites: [["B1", 27, "b1-oldname", "某区智慧校园一期项目(项目编号 QJ-2025-0112)"]] },
{ id: "f9", sev: "hint", item: "c24", title: "一般参数疑似不满足:叫号一体机显示屏尺寸",
text: ["技术册第 77 页响应「21英寸」,要求「≥21.5英寸」。一般参数负偏离每项扣 0.5 分。"],
cites: [["T", 47, "t47-screen", "显示屏尺寸≥21.5英寸"], ["B2", 77, "b2-screen", "21英寸"]] }
],
manual: [
{ id: "m1", title: "电子投标文件已使用 CA 数字证书加密", cite: ["T", 21, "t21-copies"] },
{ id: "m2", title: "在截止时间前通过电子交易平台上传,并取得回执", cite: ["T", 6, "t6-place"] },
{ id: "m3", title: "加密使用的 CA 证书在有效期内,开标时可正常解密", cite: ["T", 21, "t21-copies"] }
]
},
projects: [
{ id: "p1", hoursLeft: 408, name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", due: "11月20日 09:30 · 剩 17 天", dueCls: "", status: "已检查", ready: "未就绪", fatal: 3, href: "results.html" },
{ id: "p2", hoursLeft: 53, name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", due: "剩 2 天 5 小时", dueCls: "due-soon", status: "已检查", ready: "就绪", fatal: 0, href: "project.html" },
{ id: "p3", hoursLeft: 7, name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", due: "今天 17:00 截止", dueCls: "due-today", status: "编制中", ready: "", fatal: 0, href: "checklist.html" },
{ id: "p4", hoursLeft: 600, name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", due: "11月28日 10:00 · 剩 25 天", dueCls: "", status: "跟进中", ready: "", fatal: 0, href: "project-new.html" },
{ id: "p5", hoursLeft: 9999, name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", due: "已截止", dueCls: "due-past", status: "已递交", ready: "", fatal: 0, href: "project.html" },
{ id: "p6", hoursLeft: 9999, name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", due: "已截止", dueCls: "due-past", status: "已归档", ready: "", fatal: 0, href: "project.html" }
]
};

View File

@@ -0,0 +1,82 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>上传投标文件并检查 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.narrow{max-width:860px}
.vol{display:grid;grid-template-columns:40px minmax(0,1fr) auto auto;gap:var(--sp-3);align-items:center;padding:var(--sp-3) var(--sp-4);background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm)}
.vol + .vol{margin-top:var(--sp-2)}
.vol.new{border-color:var(--c-ink)}
.vol.bad{border-color:var(--warn);background:var(--warn-soft)}
.vol .ext{font:600 11px/1 var(--font-figures);border:1px solid var(--border-control);border-radius:2px;padding:4px 0;text-align:center}
.scope{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-4)}
.scope ul{margin:var(--sp-1) 0 0;padding-left:1.2em}
.scope li{margin:2px 0}
.foot-bar{display:flex;align-items:center;gap:var(--sp-3);margin-top:var(--sp-6);padding-top:var(--sp-4);border-top:1px solid var(--border-subtle)}
.foot-bar .cost{margin-right:auto}
</style>
</head>
<body>
<main>
<div class="content narrow">
<div class="page-title"><div><h1>上传投标文件并检查</h1><p>只处理加密前的 Word / PDF 稿。每次重新上传都是一个新版本,检查结果关联到具体版本。</p></div></div>
<div class="alert"><span class="ico i">i</span><p>清单 v3 还是草稿(补遗 1 并入后没有重新确认),检查结果以当前清单为准。<a class="plain" href="checklist.html">去确认清单</a></p></div>
<div class="section-h"><h2>投标文件分册</h2><span class="more"><button class="btn sm" data-add>上传新版本</button></span></div>
<div data-vols></div>
<div class="section-h"><h2>这次会检查什么</h2></div>
<div class="card scope">
<div><b>会查</b><ul><li>清单 <span class="fig">26</span> 条的响应完整性(未响应 / 负偏离未说明 / 疑似不满足)</li><li>形式要件:格式文件是否齐全、签章位置有没有盖章、落款日期、其他项目名称</li><li>资质匹配:按投标截止日判断证书是否过期(用你的资质库)</li><li>报价形式:限价三层、开标一览表与分项合计、大小写</li></ul></div>
<div><b>不查</b><ul><li>份数、密封、CA 加密、递交回执(列为「需人工核对」)</li><li>签章真伪</li><li>报价是否合理、报多少</li><li>评分高低</li></ul></div>
</div>
<div class="foot-bar">
<span class="cost">24 小时内复查不扣额度:从上次扣点的检查(11-01 16:00)算起,至今天 16:00</span>
<a class="btn" href="project.html">取消</a>
<button class="btn primary" data-start>开始检查</button>
</div>
<div data-run></div>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, toast } = App;
App.shell({ crumbs: [["工作台", "workbench.html"], [D.project.name, "project.html"], ["上传投标文件并检查", ""]] });
const vols = [
{ ext: "DOCX", name: "商务册", file: "商务册-v3.docx", ver: "v3", info: "64 页 · 11-02 14:12" },
{ ext: "DOCX", name: "技术册", file: "技术册-v3.docx", ver: "v3", info: "212 页 · 11-02 14:12" },
{ ext: "DOCX", name: "报价册", file: "报价册-v2.docx", ver: "v2", info: "8 页 · 11-01 15:40" }
];
function render() {
$("[data-vols]").innerHTML = vols.map((v) => `<div class="vol ${v.cls || ""}"><span class="ext">${v.ext}</span>
<div><b>${v.name}</b> <span class="tag">${v.ver}</span>${v.cls === "new" ? ' <span class="flag">新版本</span>' : ""}<div class="cap ${v.cls === "bad" ? "" : "muted"}" ${v.cls === "bad" ? 'style="color:var(--warn-text)"' : ""}>${v.file} · ${v.info}</div></div>
<select class="input" style="width:120px;height:28px" aria-label="分册类型" ${v.cls === "bad" ? "disabled" : ""}><option>${v.name}</option><option>商务册</option><option>技术册</option><option>报价册</option><option>资格证明册</option></select>
<button class="btn sm text" data-rm="${v.file}">移除</button></div>`).join("");
}
render();
$("[data-add]").onclick = () => {
vols[2] = { ext: "DOCX", name: "报价册", file: "报价册-v3.docx", ver: "v3", info: "8 页 · 刚刚上传", cls: "new" };
vols.push({ ext: "KEDT", name: "资格证明册", file: "资格证明册.kedt", ver: "—", info: "这是电子投标客户端生成的加密文件,标博士读不了。请上传加密前的 Word 或 PDF 稿。", cls: "bad" });
render(); toast("报价册已更新为 v3;有 1 个文件无法读取");
};
document.addEventListener("click", (e) => { if (e.target.matches("[data-rm]")) { const i = vols.findIndex((v) => v.file === e.target.dataset.rm); vols.splice(i, 1); render(); } });
$("[data-start]").onclick = () => {
$("[data-start]").disabled = true; $("[data-start]").textContent = "正在检查…";
const S = ["解析投标文件分册", "逐条找响应", "形式要件与签章位置", "资质匹配", "报价形式", "沿用上次处理状态"];
let i = 0;
const draw = () => $("[data-run]").innerHTML = `<div class="card" style="margin-top:var(--sp-4)"><ol class="steps">${S.map((s, k) => `<li class="${k < i ? "done" : k === i ? "now" : "todo"}"><span class="dot">${k < i ? "✓" : ""}</span>${s}</li>`).join("")}</ol>
${i >= S.length ? `<div class="alert done" style="margin-top:var(--sp-3)"><span class="ico ok">✓</span><p><b>第 3 次检查完成,未扣额度。</b>致命 3 · 重要 3 · 提示 3;新增 0,沿用 7,本次已解决 2。</p></div><div class="row" style="margin-top:var(--sp-3)"><a class="btn primary" href="results.html">查看检查结果</a></div>` : '<p class="note">可以离开本页,完成后会发站内通知。</p>'}</div>`;
const tick = () => { draw(); if (i++ < S.length) setTimeout(tick, 700); };
tick();
};
</script>
</body>
</html>

View File

@@ -0,0 +1,184 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>读标 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.cl-bar{display:flex;align-items:center;gap:var(--sp-4);padding:var(--sp-4) 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}
.cl-bar .actions{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
main.main{height:100vh;overflow:hidden}
.split{display:grid;grid-template-columns:200px minmax(0,1fr) minmax(420px,45%);flex:1;min-height:0}
.cats{border-right:1px solid var(--border-subtle);padding:var(--sp-4) var(--sp-3);overflow:auto}
.cats button{display:flex;width:100%;align-items:center;justify-content:space-between;height:34px;padding:0 var(--sp-3);border:0;background:none;border-radius:var(--radius-sm);font:var(--fs-body) var(--font-sans);color:var(--text-primary);cursor:pointer;text-align:left}
.cats button:hover{background:var(--bg-hover)}
.cats button.on{background:var(--bg-surface);box-shadow:inset 2px 0 0 var(--c-ink);font-weight:var(--fw-bold)}
.cats .fig{font-weight:500;color:var(--text-secondary)}
.cats hr{border:0;border-top:1px solid var(--border-subtle);margin:var(--sp-3) 0}
.cats .hint{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);padding:0 var(--sp-3)}
.list{overflow:auto;padding:var(--sp-4) var(--sp-6) var(--sp-12)}
.list .alert{margin-bottom:var(--sp-4)}
.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:500;color:var(--text-secondary);font-size:14px}
.items{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:0 var(--sp-4)}
.item{cursor:pointer;margin:0 calc(-1 * var(--sp-4));padding:var(--sp-3) var(--sp-4)}
.item:hover{background:var(--bg-hover)}
.item.sel{background:var(--ai-soft);box-shadow:inset 3px 0 0 var(--ai)}
.item .upd{margin-top:6px;display:flex;gap:var(--sp-2);align-items:center;font-size:var(--fs-caption)}
.item .upd .btn{height:24px;font-size:var(--fs-caption)}
.right{border-left:1px solid var(--border-subtle);display:flex;min-height:0}
.right .viewer{flex:1;border:0;border-radius:0}
.lot-note{font-size:var(--fs-caption);color:var(--text-secondary)}
.status-line{display:flex;gap:var(--sp-2);align-items:center}
.status-line .sign{margin:0;font-size:var(--fs-caption)}
.hist{width:100%;border-collapse:collapse;font-size:var(--fs-body)}
.hist td,.hist th{text-align:left;padding:6px 8px;border-bottom:1px solid var(--border-subtle);vertical-align:top}
.hist th{font-size:var(--fs-caption);color:var(--text-secondary);font-weight:var(--fw-bold)}
</style>
</head>
<body>
<main>
<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>新增条目</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></div>
<div class="right"><div data-viewer></div></div>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, $$, esc, dialog, toast } = App;
App.shell({ crumbs: [["工作台", "workbench.html"], [D.project.name, "project.html"], ["读标", ""]], tab: "checklist" });
const V = App.viewer($("[data-viewer]"));
const items = D.checklist.map((c) => ({ ...c }));
let pending = D.pending.map((q) => ({ ...q }));
let cat = "all", sel = null, confirmed = false;
const SRC = { ai: '<span class="src-ai">AI 提取</span>', manual: "人工添加" };
function status() {
$("[data-status]").innerHTML = confirmed
? `<span class="tag status">已确认 v3</span><span class="sign"><span>✓</span><b>${D.user.name}</b><span class="fig">11月2日 15:12</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" class="${cat === "all" ? "on" : ""}">全部<span class="fig">${items.length}</span></button>` +
D.categories.map((c) => `<button data-cat="${c.id}" class="${cat === c.id ? "on" : ""}">${c.name}<span class="fig">${n(c.id)}</span></button>`).join("") +
`<hr><button data-cat="pending" class="${cat === "pending" ? "on" : ""}" style="color:var(--ai)">待确认<span class="fig" style="color:var(--ai)">${pending.length}</span></button>
<hr><p class="hint">一条要求可以属于多个分类,例如 ★ 参数同时在「技术参数」和「无效条款」里。</p>`;
}
function itemRow(i) {
const src = i.src === "addendum" ? i.srcName : SRC[i.src];
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="#" data-hist="${i.id}">查看记录</a></span>` : "");
return `<div class="item ${sel === i.id ? "sel" : ""}" data-item="${i.id}" tabindex="0">
<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="#">${App.citeLabel(i.cite).replace(/^.*? · /, "")}</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" style="cursor:default" 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 head = `<div class="alert"><span class="ico i">i</span><p>实质性条款的标注规则:<b>打「★」的内容为实质性要求</b>(招标文件 <a class="cite-src" href="#" data-rule style="display:inline">第 <span class="fig">24</span> 页</a>)。文件没有写明 ★ 条款的总数,已识别采购包 1 和通用部分的 ★ 条款 <span class="fig">4</span> 条,请对照原文核对有无遗漏。</p></div>`;
const lot = `<p class="lot-note" style="margin:0 0 var(--sp-3)">显示:通用 + 采购包 1 · 采购包 2 的 <span class="fig" style="font-weight:500">${D.hiddenLotItems}</span> 条已隐藏 · <a class="plain" href="lots.html">更改所投的包</a></p>`;
if (cat === "pending") return $("[data-list]").innerHTML = pendingBox() || '<p class="muted">没有待确认的条目。</p>';
const groups = cat === "all" ? D.categories : D.categories.filter((c) => c.id === cat);
const seen = new Set();
$("[data-list]").innerHTML = lot + (cat === "all" ? head + pendingBox() : "") + groups.map((g) => {
// 「全部」视图下每条只出现一次,放在它的第一个分类里
const rows = items.filter((i) => i.cats.includes(g.id) && (cat !== "all" || !seen.has(i.id)));
rows.forEach((i) => seen.add(i.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("");
}
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.matches("[data-ack]")) { const i = items.find((x) => x.id === t.dataset.ack); i.updated = false; i.edited = true; render(); toast("已核对补遗更新,记录已保存"); return; }
if (t.matches("[data-hist]")) { e.preventDefault(); history(t.dataset.hist); return; }
if (t.closest("[data-rule]")) { e.preventDefault(); V.open(["T", 24, "t24-mark"]); return; }
if (t.closest("[data-qopen]")) { e.preventDefault(); const q = pending.find((x) => x.id === t.closest("[data-qopen]").dataset.qopen); V.open(q.cite); return; }
if (t.matches("[data-qa]")) { pendingAct(t.closest("[data-q]").dataset.q, t.dataset.qa); return; }
const it = t.closest("[data-item]");
if (it) { e.preventDefault(); sel = it.dataset.item; const i = items.find((x) => x.id === sel); $$(".item.sel").forEach((x) => x.classList.remove("sel")); it.classList.add("sel"); V.open(i.cite); }
});
document.addEventListener("keydown", (e) => { if (e.key === "Enter" && e.target.matches("[data-item]")) e.target.click(); });
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)}」 · 可在修改记录中恢复`); return; }
if (act === "adopt") { adopt(q, q.cite, false); return; }
dialog({
title: act === "cite" ? "补上出处后采纳" : "修改后采纳",
body: `<div class="field"><label for="qt">要求</label><input class="input" id="qt" value="${esc(q.title)}"></div>
<div class="field" style="margin-top:var(--sp-4)"><label for="qp">出处页码 <small>(招标文件)</small></label><input class="input" id="qp" placeholder="例如 26" style="width:120px"></div>
<label class="row" style="gap:8px;margin-top:var(--sp-3)"><input type="checkbox" id="qn" style="accent-color:var(--c-ink)"> 找不到出处,按「人工确认、无出处」采纳</label>`,
ok: "采纳",
onOk: (s) => { const pg = $("#qp", s).value.trim(); 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 ? "已采纳,标注为「人工确认、无出处」" : "已采纳,进入正式清单");
}
function history(id) {
const i = items.find((x) => x.id === id);
const u = D.user.name;
const rows = id === "c26"
? [["11-01 10:42", "系统", "AI 提取", "—", "接受联合体投标"], ["11-01 11:05", u, "修改", "接受联合体投标", "不接受联合体投标"], ["11-01 11:06", u, "备注", "—", "原文写的是「否」,AI 读反了"]]
: [["11-01 10:42", "系统", "AI 提取", "—", i.old || i.title], ["11-02 14:31", "系统", "补遗 1 更新", i.old || "—", i.title], ["11-02 15:08", u, "核对补遗更新", "—", "已核对"]];
dialog({ title: "修改记录", cancel: null, ok: "关闭", wide: true,
body: `<p class="muted">${esc(i.title)}</p><table class="hist"><tr><th>时间</th><th>操作人</th><th>操作</th><th>改前</th><th>改后</th></tr>${rows.map((r) => `<tr>${r.map((c, k) => `<td${k ? "" : ' class="fig" style="font-weight:500"'}>${esc(c)}</td>`).join("")}</tr>`).join("")}</table>` });
}
$("[data-confirm]").onclick = () => dialog({
title: "确认清单 v3?",
body: `<p>确认后,查标会以这一版清单为依据。之后再修改清单,会回到草稿并保留历史,已有的检查结果不受影响。</p>${pending.length ? `<div class="alert warn"><span class="ico w">!</span><p><b>还有 ${pending.length} 条待确认。</b>它们不会进入这一版清单,也不参与检查。</p></div>` : ""}`,
ok: "确认清单",
onOk: () => { confirmed = true; render(); toast("清单 v3 已确认"); }
});
$("[data-add]").onclick = () => dialog({
title: "新增条目",
body: `<div class="field"><label for="na">要求</label><textarea class="input" id="na" placeholder="写下招标文件里的要求"></textarea></div>
<div class="field" style="margin-top:var(--sp-4)"><label for="nc">分类</label><select class="input" id="nc">${D.categories.map((c) => `<option value="${c.id}">${c.name}</option>`).join("")}</select></div>
<p class="cap muted">人工添加的条目可以没有出处,会标注「人工添加」。</p>`,
ok: "添加",
onOk: (s) => { const v = $("#na", s).value.trim(); if (!v) return false; items.push({ id: "u" + items.length, cats: [$("#nc", s).value], title: v, lot: 0, src: "manual", cite: ["T", 1, ""] }); render(); toast("已添加"); }
});
$("[data-missing]").onclick = (e) => { e.preventDefault(); dialog({ title: "反馈:有遗漏的要求", body: `<div class="field"><label for="mi">漏了什么?在第几页?</label><textarea class="input" id="mi" placeholder="例如:第 26 页要求提供原厂授权书,清单里没有"></textarea></div><p class="cap muted">反馈用于改进提取规则。如果需要把这条加进清单,请用「新增条目」。</p>`, ok: "提交反馈", onOk: () => toast("已收到反馈,谢谢") }); };
render();
V.open(items[0].cite); sel = items[0].id; list();
</script>
</body>
</html>

82
docs/prototype/index.html Normal file
View File

@@ -0,0 +1,82 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>标博士 原型目录</title>
<link rel="icon" href="../design/logo/favicon.svg">
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.page{max-width:1120px;margin:0 auto;padding:var(--sp-12) var(--sp-6) var(--sp-16)}
.page > header img{height:36px}
.page h1{font:600 32px/40px var(--font-serif);margin:var(--sp-6) 0 var(--sp-2)}
.lede{color:var(--text-secondary);max-width:52em;margin:0}
h2{font:600 var(--fs-h2)/var(--lh-h2) var(--font-serif);margin:var(--sp-12) 0 var(--sp-4);padding-bottom:var(--sp-2);border-bottom:2px solid var(--c-ink)}
.journey{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-3);counter-reset:s}
.journey a{display:block;background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:var(--sp-4);color:inherit;text-decoration:none;counter-increment:s;position:relative}
.journey a:hover{border-color:var(--c-ink)}
.journey a::before{content:counter(s);font:600 22px/1 var(--font-figures);color:var(--text-secondary);display:block;margin-bottom:var(--sp-2)}
.journey b{display:block;font-size:var(--fs-title)}
.journey span{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);display:block;margin-top:4px}
.journey .file{position:absolute;top:var(--sp-4);right:var(--sp-4);font:12px var(--font-figures);color:var(--ai)}
table.data td{height:auto}
.two{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-6)}
.two ul{margin:0;padding-left:1.2em}
.two li{margin:4px 0}
</style>
</head>
<body>
<div class="page">
<header><img src="../design/logo/svg/logo-horizontal.svg" alt="标博士"></header>
<h1>MVP 可点击原型 v0.1</h1>
<p class="lede">按 <code>docs/analysis/02-user-journeys.md</code> 的 J1–J5 走一遍 MVP 链路。示意场景:某市政务服务中心智慧大厅建设项目(政府采购 · 公开招标 · 2 个采购包,投采购包 1)。条款改写自苏采云样本 S02、S10,项目名、金额、页码、投标响应和人名都是示意。直接使用设计规范的变量和组件(<code>design/system/</code>),所以是中保真:重点验证流程、信息结构和红线落地,视觉细节留到 UI 设计阶段精修。</p>
<h2>J1 · 首次体验:从上传到导出报告</h2>
<div class="journey">
<a href="login.html"><span class="file">login</span><b>注册 / 登录</b><span>手机号 + 验证码,填企业名称;协议写明不得用于串通投标</span></a>
<a href="project-new.html"><span class="file">project-new</span><b>新建项目,上传招标文件</b><span>按文件头识别真实格式;加密文件直接说明原因、不扣点</span></a>
<a href="parsing.html"><span class="file">parsing</span><b>等待解析</b><span>分阶段进度,可以离开;失败自动退回额度</span></a>
<a href="lots.html"><span class="file">lots</span><b>选择所投的包</b><span>只保留通用 + 所选包的条目;分包可手工更正</span></a>
<a href="checklist.html"><span class="file">checklist</span><b>读标:浏览清单,看原文</b><span>左分类、中清单、右原文高亮;待确认区;补遗更新;确认清单</span></a>
<a href="bid-check.html"><span class="file">bid-check</span><b>上传投标文件并检查</b><span>多分册、多版本;写明查什么、不查什么;识别加密格式</span></a>
<a href="results.html"><span class="file">results</span><b>查标:逐条处理</b><span>出处批注、三种处理、致命项确认需写说明、键盘逐条处理、批量处理提示级</span></a>
<a href="report.html"><span class="file">report</span><b>导出报告</b><span>首页写覆盖范围和免责声明;logo 只在页眉</span></a>
</div>
<h2>其他页面</h2>
<table class="data">
<thead><tr><th>页面</th><th>对应</th><th>要点</th></tr></thead>
<tbody>
<tr><td><a class="plain" href="workbench.html">工作台</a></td><td>FR-M6-01</td><td>按截止排序;倒计时临近用赭色;就绪只在「已检查」显示;致命计数</td></tr>
<tr><td><a class="plain" href="project.html">项目概览</a></td><td>FR-M6-02、M2-09、J2</td><td>状态流;AI 摘要带出处;关键日期(补遗更新);文件与版本;标记已递交(可撤销)、归档、删除(输入项目名确认)</td></tr>
<tr><td><a class="plain" href="project-new.html?addendum=1">追加补遗</a> → <a class="plain" href="parsing.html?addendum=1">解析补遗</a></td><td>FR-M2-10、J2-1</td><td>补遗单独扣 1 次;并入清单后回到草稿,由用户核对</td></tr>
<tr><td><a class="plain" href="quota.html">额度</a></td><td>FR-M6-03、J3</td><td>扣点规则可见;流水对应到任务;预扣 / 退回成对出现;复查免扣也有记录</td></tr>
<tr><td><a class="plain" href="qualifications.html">资质库</a></td><td>FR-M6-08、J2-3</td><td>到期按项目截止日判断;缺附件显示「待补录」</td></tr>
<tr><td><a class="plain" href="../design/system/components.html">组件预览</a></td><td>设计规范</td><td>原型用到的全部组件</td></tr>
</tbody>
</table>
<h2>模拟了什么、没模拟什么</h2>
<div class="two">
<div class="card"><b>可以点、会变化</b><ul>
<li>查标:处理 / 撤销 / 忽略原因校验 / 致命项确认说明 / 批量 / 快捷键 J K O 1 2 3 U ? / 原文抽屉 / 就绪状态与页签计数联动</li>
<li>读标:分类筛选、点清单项看原文高亮、待确认区采纳与丢弃、补遗更新核对、修改记录、确认清单</li>
<li>解析与检查的进度动画、取消、失败演示</li>
<li>项目状态:标记已递交、撤销、归档、删除确认</li>
<li>报告:勾选附录、出处后实时重排</li></ul></div>
<div class="card"><b>没有做</b><ul>
<li>真实文件上传与解析(点上传区就出示例文件)</li>
<li>数据不保存,刷新后恢复初始状态</li>
<li>原文查看器只做了被引用到的页面</li>
<li>免费版的差异(报告角标、额度用完后的升级引导)只在文字里说明</li>
<li>运营后台(FR-M6-09)、通知中心、账号设置</li></ul></div>
</div>
<h2>运行</h2>
<p>在 <code>docs/</code> 目录运行 <code>python3 -m http.server 8766</code>,打开 <code>http://127.0.0.1:8766/prototype/</code>。宋体和数字字体从 <code>design/logo/.work/fonts</code> 加载,没有该目录时退回系统字体。设计基准 1440 宽,最小 1280。</p>
</div>
</body>
</html>

66
docs/prototype/login.html Normal file
View File

@@ -0,0 +1,66 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录 · 标博士原型</title>
<link rel="icon" href="../design/logo/favicon.svg">
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
body{display:grid;grid-template-columns:minmax(0,1fr) 520px;min-height:100vh;min-width:1280px}
.brand-side{padding:var(--sp-16) var(--sp-16) var(--sp-10);display:flex;flex-direction:column;border-right:1px solid var(--border-subtle)}
.brand-side img{width:200px}
.brand-side .slogan{font:600 32px/1.4 var(--font-serif);margin:var(--sp-12) 0 var(--sp-6)}
.points{list-style:none;margin:0;padding:0;max-width:34em}
.points li{padding:var(--sp-3) 0 var(--sp-3) var(--sp-4);border-left:2px solid var(--ai);margin-bottom:var(--sp-3)}
.points b{display:block;font-size:var(--fs-title)}
.points span{color:var(--text-secondary)}
.brand-side .legal{margin-top:auto;font-size:var(--fs-caption);color:var(--text-secondary)}
.form-side{background:var(--bg-surface);padding:120px var(--sp-12) var(--sp-10);display:flex;flex-direction:column}
.form-side .tabs{margin:0 0 var(--sp-8);border-bottom:1px solid var(--border-subtle)}
.code{display:flex;gap:var(--sp-2)}
.code .input{flex:1}
.agree{display:flex;gap:var(--sp-2);align-items:flex-start;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);margin-top:var(--sp-5)}
.agree input{margin-top:3px;accent-color:var(--c-ink)}
.free{margin-top:var(--sp-5);padding:var(--sp-3) var(--sp-4);background:var(--bg-sunken);border-radius:var(--radius-sm);font-size:var(--fs-caption);line-height:var(--lh-caption)}
</style>
</head>
<body>
<section class="brand-side">
<img src="../design/logo/svg/logo-horizontal-textured.svg" alt="标博士 BIAOBOSHI">
<p class="slogan">标博士,你的AI投标专家</p>
<ul class="points">
<li><b>读标:每一条要求都有出处</b><span>上传招标文件,逐条提取资格、无效条款、评分、日期和 ★ 参数,点一下就跳到原文那一页。</span></li>
<li><b>查标:递交前逐条对一遍</b><span>把投标文件和清单逐条核对,找出没响应、疑似不满足、漏盖章的地方,由你逐条确认。</span></li>
<li><b>报告:谁在什么时候确认了什么</b><span>导出带处理记录的检查报告,写明查了什么、没查什么。</span></li>
</ul>
<p class="legal">辅助检查工具,不替代人工审核,也不对投标结果作任何保证。</p>
</section>
<section class="form-side">
<nav class="tabs" aria-label="登录方式"><a href="#" class="on" data-t="login">登录</a><a href="#" data-t="reg">注册</a></nav>
<div class="stack">
<div class="field"><label for="ph">手机号</label><input class="input" id="ph" inputmode="numeric" placeholder="11 位手机号" value="13800000000"></div>
<div class="field"><label for="cd">验证码</label><div class="code"><input class="input" id="cd" inputmode="numeric" placeholder="6 位数字"><button class="btn" data-send>发送验证码</button></div></div>
<div class="field" data-reg hidden><label for="co">企业名称</label><input class="input" id="co" placeholder="与开票信息一致,例如:南京某信息技术有限公司"><span class="msg">额度和数据归属企业主体,一个手机号只能属于一个企业。</span></div>
</div>
<label class="agree" data-reg hidden><input type="checkbox" id="ag"><span>我已阅读并同意《服务协议》《隐私政策》。服务协议约定:本产品只用于投标人对自己投标文件的投前自查,不得用于串通投标、帮助他人规避雷同等违法用途。</span></label>
<button class="btn primary lg" style="margin-top:var(--sp-6);width:100%;justify-content:center" data-go>登录</button>
<div class="free" data-reg hidden>注册即开通免费版:解读 <b>3</b> 次招标文件,检查 <b>1</b> 个项目(项目内复查不限次数)。不需要上传营业执照。</div>
</section>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { $, $$, toast } = App;
let mode = "login";
$$("[data-t]").forEach((a) => (a.onclick = (e) => { e.preventDefault(); mode = a.dataset.t; $$("[data-t]").forEach((x) => x.classList.toggle("on", x === a)); $$("[data-reg]").forEach((x) => (x.hidden = mode !== "reg")); $("[data-go]").textContent = mode === "reg" ? "注册并开通免费版" : "登录"; }));
$("[data-send]").onclick = (e) => { let n = 60; const b = e.target; b.disabled = true; const t = setInterval(() => { b.textContent = `${--n} 秒后重发`; if (!n) { clearInterval(t); b.disabled = false; b.textContent = "发送验证码"; } }, 1000); toast("验证码已发送(原型:任意 6 位数字)"); };
$("[data-go]").onclick = () => {
if (mode === "reg" && !$("#ag").checked) { toast("请先阅读并同意服务协议"); return; }
location.href = mode === "reg" ? "project-new.html" : "workbench.html";
};
</script>
</body>
</html>

54
docs/prototype/lots.html Normal file
View File

@@ -0,0 +1,54 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>选择所投的包 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.narrow{max-width:760px}
.lot{display:grid;grid-template-columns:20px minmax(0,1fr) auto;gap:var(--sp-4);align-items:start;padding:var(--sp-4) var(--sp-5);background:var(--bg-surface);border:1px solid var(--border-control);border-radius:var(--radius-sm);cursor:pointer}
.lot + .lot{margin-top:var(--sp-3)}
.lot.on{border-color:var(--c-ink);box-shadow:inset 0 0 0 1px var(--c-ink)}
.lot input{width:16px;height:16px;margin:3px 0 0;accent-color:var(--c-ink)}
.lot b{font-size:var(--fs-title)}
.lot .money{text-align:right}
.lot .money .fig{font-size:20px}
.common{margin-top:var(--sp-4);padding:var(--sp-3) var(--sp-5);border:1px dashed var(--border-control);border-radius:var(--radius-sm);color:var(--text-secondary)}
</style>
</head>
<body>
<main>
<div class="content narrow">
<div class="page-title"><div><h1>选择要投的包</h1><p>识别到 2 个采购包。清单只保留通用条目和所选包的条目,没选的包折叠隐藏,以后可以改。</p></div></div>
<label class="lot on"><input type="checkbox" checked data-lot="1"><div><b>采购包 1 · 智慧大厅软硬件集成</b><div class="muted cap">8 条专属要求,其中 ★ 条款 4 条 · <a class="cite-src" href="#" style="display:inline" data-open>招标文件 第 2 页</a></div></div><div class="money"><span class="cap muted">最高限价</span><br><span class="fig">286.00</span> 万元</div></label>
<label class="lot"><input type="checkbox" data-lot="2"><div><b>采购包 2 · 机房网络设备更新</b><div class="muted cap">9 条专属要求,其中 ★ 条款 2 条 · <a class="cite-src" href="#" style="display:inline" data-open>招标文件 第 2 页</a></div></div><div class="money"><span class="cap muted">最高限价</span><br><span class="fig">132.00</span> 万元</div></label>
<div class="common">通用条目 <span class="fig">18</span> 条(资格条件、无效情形、递交要求等),所有包都适用,始终保留。</div>
<div class="row" style="margin-top:var(--sp-6)">
<a class="plain cap" href="#" data-fix>分包识别得不对?手工调整</a>
<span style="margin-left:auto"></span>
<a class="btn primary" href="checklist.html" data-go>查看清单</a>
</div>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, $$, dialog, toast } = App;
App.shell({ crumbs: [["工作台", "workbench.html"], [D.project.name, "project.html"], ["选择所投的包", ""]] });
$$("[data-lot]").forEach((c) => (c.onchange = () => { c.closest(".lot").classList.toggle("on", c.checked); $("[data-go]").classList.toggle("disabled", !$$("[data-lot]:checked").length); }));
$$("[data-open]").forEach((a) => (a.onclick = (e) => { e.preventDefault(); e.stopPropagation(); toast("原型:在读标页的原文查看器里打开第 2 页"); }));
$("[data-go]").onclick = (e) => { if (!$$("[data-lot]:checked").length) { e.preventDefault(); toast("至少选择一个包"); } };
$("[data-fix]").onclick = (e) => { e.preventDefault(); dialog({ title: "手工调整分包", wide: true,
body: `<p class="muted">可以增加、删除、重命名采购包,或把清单条目改到另一个包下(在读标页逐条调整)。</p>
<table class="data"><thead><tr><th>包号</th><th>名称</th><th class="r">最高限价(万元)</th><th></th></tr></thead><tbody>
<tr><td class="num">1</td><td><input class="input" value="智慧大厅软硬件集成"></td><td class="r"><input class="input" value="286.00" style="width:100px;text-align:right"></td><td><button class="btn sm text">删除</button></td></tr>
<tr><td class="num">2</td><td><input class="input" value="机房网络设备更新"></td><td class="r"><input class="input" value="132.00" style="width:100px;text-align:right"></td><td><button class="btn sm text">删除</button></td></tr>
</tbody></table><button class="btn sm" style="margin-top:var(--sp-3)">增加一个包</button>`,
ok: "保存", onOk: () => toast("分包已保存,修改已留痕") }); };
</script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>解析中 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.narrow{max-width:760px}
.steps li{padding:10px 0}
.steps .sub{font-weight:var(--fw-regular);color:var(--text-secondary);font-size:var(--fs-caption);margin-left:auto}
.steps .now .dot{animation:pulse 1.2s ease-in-out infinite}
@keyframes pulse{50%{opacity:.35}}
@media (prefers-reduced-motion:reduce){.steps .now .dot{animation:none}}
.prog{height:4px;background:var(--bg-sunken);margin:var(--sp-4) 0 var(--sp-2)}
.prog i{display:block;height:100%;background:var(--c-ink);transition:width var(--dur-base)}
.done-box{display:none}
.finished .done-box{display:block}
.finished .run-box{display:none}
</style>
</head>
<body>
<main>
<div class="content narrow">
<div class="page-title"><div><h1 data-title>正在解析招标文件</h1><p>ZC-2026-0418 采购文件.doc · 186 页</p></div></div>
<div class="card" data-card>
<div class="run-box">
<ol class="steps" data-steps></ol>
<div class="prog"><i data-prog style="width:0"></i></div>
<p class="cap muted" style="margin:0">300 页以内一般 10 分钟内完成(原型里加速演示)。<b style="color:var(--text-primary)">可以离开本页</b>,完成或失败都会发站内通知。</p>
<div class="row" style="margin-top:var(--sp-5)">
<span class="cost">已预扣 1 次解析,成功后结算;失败、超时或取消会自动退回</span>
<span style="margin-left:auto"></span>
<a class="btn" href="workbench.html">回工作台,完成后通知我</a>
<button class="btn text" data-cancel>取消解析</button>
</div>
</div>
<div class="done-box" data-done></div>
</div>
<p class="note"><a class="plain" href="#" data-fail>演示:解析失败的样子</a></p>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, dialog, toast } = App;
const addendum = new URLSearchParams(location.search).has("addendum");
App.shell({ crumbs: [["工作台", "workbench.html"], [addendum ? D.project.name : "新建项目", addendum ? "project.html" : ""], ["解析", ""]] });
if (addendum) $("[data-title]").textContent = "正在解析补遗 1";
const S = addendum
? [["识别文件格式", "PDF · 2 页"], ["提取文字", ""], ["提取变更内容", ""], ["核对出处", ""], ["并入清单", ""]]
: [["识别文件格式", "实际为 DOCX,保留斜体、下划线等格式"], ["提取文字", "186 页"], ["识别分包与法律体系", "2 个采购包 · 政府采购"], ["提取清单", "资格、无效条款、评分、日期、技术参数"], ["核对出处", "逐条定位页码和原句"]];
let i = 0, timer;
function render() {
$("[data-steps]").innerHTML = S.map(([t, sub], k) => `<li class="${k < i ? "done" : k === i ? "now" : "todo"}"><span class="dot">${k < i ? "✓" : ""}</span>${t}${k <= i && sub ? `<span class="sub">${sub}</span>` : ""}</li>`).join("");
$("[data-prog]").style.width = (i / S.length) * 100 + "%";
}
function tick() {
i++; render();
if (i < S.length) timer = setTimeout(tick, 1100);
else finish();
}
function finish() {
$("[data-card]").classList.add("finished");
$("[data-done]").innerHTML = addendum
? `<div class="alert done"><span class="ico ok">✓</span><p><b>补遗 1 解析完成,已结算 1 次解析。</b>更新了 2 条清单项(投标截止时间、服务器系统盘),新增 0 条。清单已回到草稿,请核对后重新确认。</p></div>
<div class="row" style="margin-top:var(--sp-4)"><a class="btn primary" href="checklist.html">去核对更新</a></div>`
: `<div class="alert done"><span class="ico ok">✓</span><p><b>解析完成,已结算 1 次解析。</b>提取了 <span class="fig">35</span> 条要求,其中 <span class="fig">2</span> 条没找到可靠出处,放在「待确认」里。</p></div>
<dl class="kv" style="margin-top:var(--sp-4)"><dt>项目名称</dt><dd>${D.project.name}</dd><dt>法律体系</dt><dd>政府采购 · 公开招标</dd><dt>采购包</dt><dd>识别到 <span class="fig">2</span> 个采购包,下一步选择要投的包</dd></dl>
<div class="row" style="margin-top:var(--sp-5)"><a class="btn primary" href="lots.html">选择所投的包</a></div>`;
}
render(); timer = setTimeout(tick, 900);
$("[data-cancel]").onclick = () => dialog({ title: "取消解析?", body: "<p>已预扣的 1 次解析会退回。取消后可以重新上传。</p>", ok: "取消解析", cancel: "继续解析",
onOk: () => { clearTimeout(timer); $("[data-card]").classList.add("finished"); $("[data-done]").innerHTML = `<div class="alert"><span class="ico i">i</span><p>已取消,退回 1 次解析额度。</p></div><div class="row" style="margin-top:var(--sp-4)"><a class="btn primary" href="project-new.html">重新上传</a></div>`; } });
$("[data-fail]").onclick = (e) => { e.preventDefault(); clearTimeout(timer); $("[data-card]").classList.add("finished");
$("[data-done]").innerHTML = `<div class="alert warn"><span class="ico w">!</span><p><b>解析失败:第 112–118 页是图片,OCR 没能识别出文字。</b>已退回 1 次解析额度。可以换一份更清晰的扫描件,或者重试。错误编号 PJ-20261102-0417。</p></div>
<div class="row" style="margin-top:var(--sp-4)"><button class="btn primary" onclick="location.reload()">重试(重新预扣 1 次)</button><a class="btn" href="project-new.html">换一个文件</a></div>`; };
</script>
</body>
</html>

View File

@@ -0,0 +1,76 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>新建项目 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.narrow{max-width:760px}
.file-row.bad{border-color:var(--warn);background:var(--warn-soft)}
.file-row .st{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.file-row .st.warn{color:var(--warn-text)}
.foot-bar{display:flex;align-items:center;gap:var(--sp-3);margin-top:var(--sp-6);padding-top:var(--sp-4);border-top:1px solid var(--border-subtle)}
.foot-bar .cost{margin-right:auto}
</style>
</head>
<body>
<main>
<div class="content narrow">
<div class="page-title"><div><h1 data-title>新建项目</h1><p data-sub>上传招标文件,标博士会逐条提取要求,每一条都带原文出处。</p></div></div>
<div class="card stack">
<div class="field" data-name-field><label for="pn">项目名称 <small>(选填,解析后自动填写,可以修改)</small></label><input class="input" id="pn" placeholder="例如:某市政务服务中心智慧大厅建设项目"></div>
<div>
<div class="drop" tabindex="0" role="button" data-drop>
<b>把招标文件拖到这里,或点击选择</b>
<span class="muted">支持 PDF(含扫描件)、Word(DOC / DOCX)、WPS。正文和附件分开的,可以一起上传。</span><br>
<span class="cap muted">专业版单个文件不超过 <span class="fig" style="font-weight:500">500</span> 页、<span class="fig" style="font-weight:500">100</span> MB(示意,以套餐页为准)</span>
</div>
<div data-files style="margin-top:var(--sp-3)"></div>
</div>
</div>
<div class="foot-bar">
<span class="cost" data-cost>选好文件后显示扣点</span>
<a class="btn" href="workbench.html">取消</a>
<button class="btn primary" data-start disabled>开始解析</button>
</div>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, toast } = App;
const addendum = new URLSearchParams(location.search).has("addendum");
App.shell({ nav: addendum ? "" : "workbench", crumbs: addendum ? [["工作台", "workbench.html"], [D.project.name, "project.html"], ["追加补遗", ""]] : [["工作台", "workbench.html"], ["新建项目", ""]] });
if (addendum) {
$("[data-title]").textContent = "追加补遗 / 澄清";
$("[data-sub]").textContent = "补遗单独解析,新提取的条目标注来源「补遗 N」并入清单;日期以最新文件为准,由你核对。MVP 不自动判断哪些旧条目被作废。";
$("[data-name-field]").hidden = true;
}
const files = addendum
? [{ ext: "PDF", name: "关于 ZC-2026-0418 的更正公告.pdf", info: "2 页 · 0.3 MB", type: "补遗", ok: true }]
: [
{ ext: "DOCX", name: "ZC-2026-0418 采购文件.doc", info: "186 页 · 4.2 MB", type: "招标文件", ok: true, note: "扩展名是 .doc,实际是 Word 2007 格式(DOCX),已按实际格式处理。" },
{ ext: "PDF", name: "附件:采购需求-技术参数.pdf", info: "页数无法读取", type: "招标文件", ok: false, note: "文件已加密,无法读取。请上传未加密的文件。这个文件不会创建任务,也不扣额度。" }
];
function render() {
$("[data-files]").innerHTML = files.map((f, i) => `<div class="file-row ${f.ok ? "" : "bad"}">
<span class="ext">${f.ext}</span>
<div><b>${f.name}</b><div class="st ${f.ok ? "muted" : "warn"}">${f.ok ? "" : '<span class="ico w" style="width:14px;height:14px;font-size:10px;display:inline-grid;margin:0 4px -2px 0">!</span>'}${f.info}${f.note ? " · " + f.note : ""}</div></div>
<select aria-label="文件类型" ${f.ok ? "" : "disabled"}><option ${f.type === "招标文件" ? "selected" : ""}>招标文件</option><option ${f.type === "补遗" ? "selected" : ""}>补遗</option><option>澄清答复</option></select>
<button class="btn sm text" data-rm="${i}">移除</button></div>`).join("");
const ok = files.filter((f) => f.ok).length;
$("[data-cost]").innerHTML = ok ? `扣 <span class="fig">${ok}</span> 次解析 · 剩 <span class="fig">${D.quota.parse.left}</span> 次 · 解析失败、超时、取消都不扣` : "选好文件后显示扣点";
$("[data-start]").disabled = !ok;
}
let shown = false;
$("[data-drop]").onclick = () => { if (!shown) { shown = true; render(); } };
$("[data-drop]").onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); $("[data-drop]").click(); } };
document.addEventListener("click", (e) => { if (e.target.matches("[data-rm]")) { files.splice(+e.target.dataset.rm, 1); render(); } });
$("[data-start]").onclick = () => location.href = "parsing.html" + (addendum ? "?addendum=1" : "");
</script>
</body>
</html>

138
docs/prototype/project.html Normal file
View File

@@ -0,0 +1,138 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>项目概览 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.ov{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:var(--sp-6);align-items:start}
.card h3{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0 0 var(--sp-3);display:flex;gap:var(--sp-2);align-items:center}
.card h3 .right{margin-left:auto;font-weight:var(--fw-regular)}
.next{border-left:3px solid var(--c-ink)}
.next p{margin:0 0 var(--sp-3)}
.summary .kv{grid-template-columns:120px 1fr}
.summary dd .cite-src{display:inline;margin-left:6px}
.dates{list-style:none;margin:0;padding:0}
.dates li{display:grid;grid-template-columns:110px minmax(0,1fr) auto;gap:var(--sp-3);padding:var(--sp-2) 0;border-top:1px solid var(--border-subtle);align-items:baseline}
.dates li:first-child{border-top:0}
.dates .fig{font-weight:500}
.dates .past{color:var(--text-secondary)}
.docs .file-row{grid-template-columns:32px minmax(0,1fr) auto}
.runs td{height:auto}
</style>
</head>
<body>
<main>
<div class="content">
<div class="flowline" data-flow></div>
<div class="ov" style="margin-top:var(--sp-5)">
<div class="stack">
<div class="card next">
<h3>下一步</h3>
<p>第 2 次检查还有 <span class="sev fatal">致命<span class="fig">3</span></span> 项待处理。处理完不是递交的前提,决定权在你;报告会如实列出每一项的处理情况。</p>
<div class="row"><a class="btn primary" href="results.html">去处理检查结果</a><a class="btn" href="report.html">导出报告</a></div>
</div>
<div class="card summary">
<h3><span class="tag ai">AI 摘要</span>项目要点<span class="right cap muted">每一条都可以点出处核对</span></h3>
<dl class="kv">
<dt>法律体系</dt><dd>政府采购 · 公开招标(按「投标无效」口径检查)</dd>
<dt>预算 / 限价</dt><dd>采购包 1:<span class="fig">286.00</span> 万元(最高限价同预算)<a class="cite-src" href="#" data-open="T|2|t2-cap|采购包1:286.00万元">第 2 页</a></dd>
<dt>评标方法</dt><dd>综合评分法:价格 10、技术 40、实施方案 20、售后 10、认证 6、业绩 8 <a class="cite-src" href="#" data-open="T|31|t31-price">第 31 页</a></dd>
<dt>联合体</dt><dd>不接受 <span class="flag">已修改</span><a class="cite-src" href="#" data-open="T|2|t2-joint">第 2 页</a></dd>
<dt>中小企业</dt><dd>不专门面向中小企业;小微企业有价格扣除 <a class="cite-src" href="#" data-open="T|2|t2-sme">第 2 页</a></dd>
<dt>保证金</dt><dd>不收取 <a class="cite-src" href="#" data-open="T|21|t21-deposit">第 21 页</a></dd>
<dt>递交</dt><dd>电子交易平台在线递交,CA 加密,不接受纸质 <a class="cite-src" href="#" data-open="T|6|t6-place">第 6 页</a></dd>
<dt>★ 条款</dt><dd>采购包 1 和通用部分共 <span class="fig">4</span> 条 · <a class="plain" href="checklist.html">在清单里查看</a></dd>
</dl>
</div>
<div class="card">
<h3>检查记录<span class="right"><a class="btn sm" href="bid-check.html">上传新版本并检查</a></span></h3>
<table class="data runs">
<thead><tr><th>批次</th><th>时间</th><th>投标文件</th><th>结果</th><th>报告</th></tr></thead>
<tbody>
<tr><td>第 <span class="fig">2</span> 次</td><td class="num">11-02 14:20</td><td>商务册 v3、技术册 v3、报价册 v2</td><td><span class="sev fatal">致命<span class="fig">3</span></span> <span class="sev major">重要<span class="fig">3</span></span> <span class="sev hint">提示<span class="fig">3</span></span></td><td><a class="plain" href="results.html">查看</a></td></tr>
<tr><td>第 <span class="fig">1</span> 次</td><td class="num">11-01 16:00</td><td>商务册 v2、技术册 v2、报价册 v1</td><td><span class="sev fatal">致命<span class="fig">2</span></span> <span class="sev major">重要<span class="fig">4</span></span> <span class="sev hint">提示<span class="fig">2</span></span></td><td><a class="plain" href="report.html">11-01 报告.pdf</a></td></tr>
</tbody>
</table>
</div>
</div>
<div class="stack">
<div class="card">
<h3>关键日期</h3>
<ul class="dates">
<li class="past"><span class="fig">10-28</span><span>采购文件公告发布</span><span></span></li>
<li><span class="fig">11-09</span><span>询问、质疑截止(公告期满后 7 个工作日)</span><a class="cite-src" href="#" data-open="T|6|t6-ask">第 6 页</a></li>
<li><span class="fig">11-20 09:30</span><span><b>投标截止 / 开标</b> · 剩 <span class="fig">17</span> 天<br><span class="flag">已被补遗 1 更新,原为 11-18 09:30</span></span><a class="cite-src" href="#" data-open="A1|1|a1-deadline|2026年11月20日09时30分">补遗 1</a></li>
<li><span class="fig">2027-02-18</span><span>投标有效期至(截止日起 90 天)</span><a class="cite-src" href="#" data-open="T|21|t21-valid">第 21 页</a></li>
</ul>
<p class="note">MVP 只在站内显示倒计时,不发截止提醒。</p>
</div>
<div class="card docs">
<h3>文件<span class="right"><button class="btn sm" data-addendum>追加补遗 / 澄清</button></span></h3>
<p class="cap muted" style="margin:0 0 var(--sp-2)">招标方</p>
<div class="file-row"><span class="ext">PDF</span><div><b>招标文件</b><div class="cap muted">ZC-2026-0418 采购文件.pdf · 186 页 · 解析成功</div></div><a class="plain cap" href="checklist.html">读标</a></div>
<div class="file-row"><span class="ext">PDF</span><div><b>补遗 1</b><div class="cap muted">更正公告 · 2 页 · 新增 0 条、更新 2 条</div></div><a class="plain cap" href="checklist.html">读标</a></div>
<p class="cap muted" style="margin:var(--sp-4) 0 var(--sp-2)">投标文件(加密前的 Word / PDF 稿)</p>
<div class="file-row"><span class="ext">DOCX</span><div><b>商务册 v3</b><div class="cap muted">64 页 · 11-02 14:12 上传</div></div><span></span></div>
<div class="file-row"><span class="ext">DOCX</span><div><b>技术册 v3</b><div class="cap muted">212 页 · 11-02 14:12 上传</div></div><span></span></div>
<div class="file-row"><span class="ext">DOCX</span><div><b>报价册 v2</b><div class="cap muted">8 页 · 11-01 15:40 上传</div></div><span></span></div>
</div>
<div class="card">
<h3>项目操作</h3>
<div class="row"><button class="btn" data-submit>标记为已递交</button><button class="btn" data-archive>归档</button><button class="btn text" data-delete>删除项目</button></div>
<p class="note">标记已递交后项目变为只读,仍可导出报告;可以撤销,撤销会留下记录。</p>
</div>
</div>
</div>
</div>
</main>
<aside class="drawer" aria-label="原文" data-drawer><div data-viewer></div></aside>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, dialog, toast, esc } = App;
App.shell({ crumbs: [["工作台", "workbench.html"], [D.project.name, ""]], tab: "overview" });
const drawer = $("[data-drawer]");
const V = App.viewer($("[data-viewer]"), { closable: true, onClose: () => drawer.classList.remove("open"), onOpen: () => drawer.classList.add("open") });
document.addEventListener("click", (e) => {
const a = e.target.closest("[data-open]");
if (a) { e.preventDefault(); V.open(a.dataset.open.split("|").map((v, i) => i === 1 ? +v : v)); }
});
document.addEventListener("keydown", (e) => { if (e.key === "Escape") drawer.classList.remove("open"); });
let status = "已检查";
function flow() {
const S = ["跟进中", "编制中", "已检查", "已递交", "已归档"];
const i = S.indexOf(status);
$("[data-flow]").innerHTML = S.map((s, k) => `<span class="st ${k < i ? "done" : k === i ? "now" : ""}"><i></i>${s}</span>`).join('<span class="ln"></span>');
document.querySelector(".proj-head .tag.status").textContent = status;
}
flow();
$("[data-submit]").onclick = () => dialog({
title: "标记为已递交?",
body: `<p>标记后项目变为只读:不能再改清单、上传文件或处理问题,仍可以查看和导出报告。</p>
<div class="alert warn"><span class="ico w">!</span><p><b>还有 3 项致命问题待处理。</b>标记已递交不受影响,只是提醒你。</p></div>`,
ok: "标记为已递交",
onOk: () => { status = "已递交"; flow(); toast('已标记为已递交 · <a href="#" data-undo-submit>撤销</a>'); }
});
document.addEventListener("click", (e) => { if (e.target.matches("[data-undo-submit]")) { e.preventDefault(); status = "已检查"; flow(); toast("已撤销递交标记,记录已保存"); } });
$("[data-archive]").onclick = () => { status = "已归档"; flow(); toast("项目已归档,可以随时撤销归档"); };
$("[data-addendum]").onclick = () => location.href = "project-new.html?addendum=1";
$("[data-delete]").onclick = () => dialog({
title: `删除项目「${esc(D.project.name)}」?`,
body: `<p>项目下的招标文件、投标文件、清单、检查结果和报告都会删除。删除后 30 天内可以在回收站恢复,之后彻底清除。</p>
<p class="muted cap">额度流水和一条不含业务内容的审计记录会保留。</p>
<div class="field" data-field><label for="dn">输入项目名称确认</label><input class="input" id="dn" placeholder="${esc(D.project.name)}"><span class="msg"></span></div>`,
ok: "删除项目",
onOk: (s) => {
if ($("#dn", s).value.trim() !== D.project.name) { $("[data-field]", s).classList.add("err"); $(".msg", s).innerHTML = '<span class="ico w">!</span>项目名称不一致'; return false; }
location.href = "workbench.html";
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,51 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>资质库 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
</head>
<body>
<main>
<div class="content">
<div class="page-title"><div><h1>资质库</h1><p>查标时用来核对证书有没有过期、等级够不够。数据只来自你自己录入的内容,系统不从外部补数据。</p></div>
<div class="actions"><button class="btn primary" data-add>添加资质</button></div></div>
<div class="alert warn" style="margin-bottom:var(--sp-5)"><span class="ico w">!</span><p><b>1 张证书会在进行中项目的投标截止日前到期。</b>到期判断以每个项目的投标截止日为准,不以今天为准。</p></div>
<table class="data">
<thead><tr><th>名称</th><th>类型</th><th>等级</th><th>证书编号</th><th>发证机构</th><th>有效期至</th><th>附件</th></tr></thead>
<tbody data-rows></tbody>
</table>
<p class="note">MVP 只有最小资质库;业绩、人员、历史标书等完整资产库在 V1 提供。</p>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { $, dialog, toast } = App;
App.shell({ nav: "quals", crumbs: [["资质库", ""]] });
const Q = [
["ISO/IEC 27001 信息安全管理体系", "体系认证", "—", "01226IS0458", "某认证中心", "2026-11-15", '<span style="color:var(--warn-text)">早于「智慧大厅建设项目」截止日 11-20</span>', "已上传"],
["ISO 9001 质量管理体系", "体系认证", "—", "01226Q31877", "某认证中心", "2028-03-09", "", "已上传"],
["ISO/IEC 20000-1 信息技术服务管理体系", "体系认证", "—", "01226IT0921", "某认证中心", "2027-06-30", "", "已上传"],
["信息系统建设和服务能力等级证书", "资质证书", "CS3", "CS3-3201-000488", "中国电子信息行业联合会", "2027-12-31", "", "已上传"],
["电子与智能化工程专业承包", "资质证书", "二级", "D232045871", "某省住房和城乡建设厅", "2029-05-20", "", '<span class="muted">待补录</span>'],
["政务大厅排队叫号系统 V3.0", "软件著作权", "—", "2024SR0638812", "国家版权局", "长期", "", "已上传"]
];
$("[data-rows]").innerHTML = Q.map((r) => `<tr><td><b>${r[0]}</b></td><td>${r[1]}</td><td>${r[2]}</td><td class="num">${r[3]}</td><td>${r[4]}</td><td><span class="num">${r[5]}</span>${r[6] ? `<div class="cap">${r[6]}</div>` : ""}</td><td>${r[7]}</td></tr>`).join("");
$("[data-add]").onclick = () => dialog({ title: "添加资质", wide: true,
body: `<div class="grid2" style="gap:var(--sp-4)">
<div class="field"><label>名称</label><input class="input" placeholder="例如:信息系统建设和服务能力等级证书"></div>
<div class="field"><label>类型</label><select class="input"><option>资质证书</option><option>体系认证</option><option>软件著作权</option><option>其他</option></select></div>
<div class="field"><label>等级 <small>(选填)</small></label><input class="input"></div>
<div class="field"><label>证书编号</label><input class="input"></div>
<div class="field"><label>发证机构</label><input class="input"></div>
<div class="field"><label>有效期至</label><input class="input" type="date"></div></div>
<div class="drop" style="padding:var(--sp-5);margin-top:var(--sp-4)"><b style="font-size:14px">上传证书扫描件</b><span class="cap muted">PDF 或图片;可以稍后补录</span></div>`,
ok: "保存", onOk: () => toast("已保存") });
</script>
</body>
</html>

64
docs/prototype/quota.html Normal file
View File

@@ -0,0 +1,64 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>额度 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.meters{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-4)}
.meter small{display:block;color:var(--text-secondary);font-size:var(--fs-caption)}
.meter .big{font:600 var(--fs-figure)/var(--lh-figure) var(--font-figures);font-variant-numeric:tabular-nums}
.meter .unit{color:var(--text-secondary);margin-left:4px}
.meter p{margin:0;font-size:var(--fs-caption);color:var(--text-secondary)}
.rules td{height:auto}
td.plus{color:var(--confirmed)}
</style>
</head>
<body>
<main>
<div class="content">
<div class="page-title"><div><h1>额度</h1><p>专业版 · 本期 2026-11-01 至 2027-10-31。额度绑定企业主体,不可转让;期末未用完的额度不结转。</p></div>
<div class="actions"><button class="btn" data-contact>联系客服加额度</button></div></div>
<div class="alert" style="margin-bottom:var(--sp-5)"><span class="ico i">i</span><p>额度用到 80% 和 100% 时会站内提醒(每个周期每个档位只提醒一次),提醒用赭色,不用红色。</p></div>
<div class="meters">
<div class="card meter"><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50 次</span><div class="bar"><i style="width:26%"></i></div><p>已用 13 次 · 招标文件和补遗各算 1 次</p></div>
<div class="card meter"><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20 次</span><div class="bar"><i style="width:40%"></i></div><p>已用 8 次 · 同一项目 24 小时内复查不扣</p></div>
<div class="card meter"><small>按功能分布(本期)</small><p style="margin-top:6px;color:var(--text-primary);font-size:14px;line-height:22px">读标 11 次 · 补遗 2 次<br>查标 8 次 · 复查免扣 5 次</p></div>
</div>
<div class="section-h"><h2>扣点规则</h2><span class="cap muted">所有扣点都能在下方流水里对应到具体项目和任务</span></div>
<table class="data rules">
<thead><tr><th>动作</th><th>扣点</th><th>不扣点</th></tr></thead>
<tbody>
<tr><td>解读一份招标文件或补遗</td><td>1 次解析(发起时预扣,成功后结算)</td><td>解析失败、超时、取消:自动退回</td></tr>
<tr><td>对一个项目做一次全量检查</td><td>1 次检查(投标文件解析含在内)</td><td>检查中断、系统异常;同一项目 24 小时内复查</td></tr>
</tbody>
</table>
<div class="section-h"><h2>流水</h2><span class="more"><div class="seg"><button class="on">全部</button><button>解析</button><button>检查</button></div></span></div>
<table class="data">
<thead><tr><th>时间</th><th>类型</th><th>额度</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>智慧大厅建设项目 · 解析任务 PJ-0417</td><td class="muted">解析失败:OCR 未识别出文字</td></tr>
<tr><td class="num">11-02 14:28</td><td>预扣</td><td>解析</td><td class="num r">−1</td><td>智慧大厅建设项目 · 解析任务 PJ-0417</td><td></td></tr>
<tr><td class="num">11-02 14:20</td><td>—</td><td>检查</td><td class="num r">0</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>智慧大厅建设项目 · 补遗 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>智慧大厅建设项目 · 第 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>智慧大厅建设项目 · 招标文件</td><td></td></tr>
<tr><td class="num">11-01 09:00</td><td>发放</td><td>解析 / 检查</td><td class="num r plus">+50 / +20</td><td>专业版开通(对公转账)</td><td class="muted">运营:开通单 KT-1101</td></tr>
</tbody>
</table>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
App.shell({ nav: "quota", crumbs: [["额度", ""]] });
App.$("[data-contact]").onclick = () => App.dialog({ title: "联系客服加额度", cancel: null, ok: "知道了", body: "<p>MVP 暂不支持线上加量包。请联系客服,确认后由运营在后台调整额度,调整会写进流水并注明原因。</p><dl class='kv'><dt>客服电话</dt><dd>(待定)</dd><dt>官网</dt><dd>www.biaoboshi.cn</dd></dl>" });
</script>
</body>
</html>

102
docs/prototype/report.html Normal file
View File

@@ -0,0 +1,102 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>检查报告 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.rp{display:grid;grid-template-columns:260px minmax(0,1fr);gap:var(--sp-8);align-items:start}
.rp-side{position:sticky;top:76px}
.rp-side .card + .card{margin-top:var(--sp-4)}
.rp-side h3{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:0 0 var(--sp-3)}
.rp-side .chk{padding:var(--sp-2) 0;border:0}
.pages{display:flex;flex-direction:column;align-items:center;gap:var(--sp-6)}
.a4{width:794px;min-height:1123px;background:#fff;box-shadow:0 1px 3px rgba(29,39,51,.18);padding:76px 76px 64px;position:relative;font-size:13.5px;line-height:1.6;color:#1D2733}
.a4 .report-head{margin-bottom:0}
.a4 h2{font:600 26px/1.35 var(--font-serif);margin:40px 0 6px}
.a4 h4{font:600 15px/1.4 var(--font-sans);margin:26px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--c-ink)}
.a4 .sub{color:var(--text-secondary);margin:0}
.a4 dl{display:grid;grid-template-columns:110px 1fr;gap:6px 14px;margin:22px 0}
.a4 dt{color:var(--text-secondary)}
.a4 dd{margin:0}
.a4 .disc{margin-top:18px;padding:12px 14px;background:var(--bg-sunken);font-size:12.5px}
.a4 table{width:100%;border-collapse:collapse;font-size:12.5px}
.a4 th{text-align:left;font-weight:600;color:var(--text-secondary);border-bottom:1.5px solid var(--c-ink);padding:6px 6px}
.a4 td{border-bottom:1px solid var(--border-subtle);padding:7px 6px;vertical-align:top}
.a4 .sumtab td,.a4 .sumtab th{text-align:center}
.a4 .sumtab td:first-child,.a4 .sumtab th:first-child{text-align:left}
.a4 .cites{color:var(--ai);font-size:11.5px}
.a4 .free{position:absolute;top:30px;right:76px;font-size:11px;border:1px solid var(--border-control);padding:1px 6px;color:var(--text-secondary)}
.a4 .report-foot{position:absolute;left:76px;right:76px;bottom:36px}
.a4 .st-todo{font-weight:600}
@media (max-width:1400px){.pages{zoom:.86}}
</style>
</head>
<body>
<main>
<div class="content wide">
<div class="rp">
<div class="rp-side">
<div class="card">
<h3>导出报告</h3>
<label class="chk"><input type="checkbox" checked disabled><span>问题明细(全部级别)</span></label>
<label class="chk"><input type="checkbox" checked data-opt="appendix"><span>附录:需人工核对</span></label>
<label class="chk"><input type="checkbox" checked data-opt="cites"><span>每条附原文出处</span></label>
<div class="alert warn" style="margin:var(--sp-3) 0"><span class="ico w">!</span><p class="cap">还有 <b>3 项致命问题</b>待处理,报告会如实列出。</p></div>
<div class="row"><button class="btn primary" data-dl>下载 PDF</button><button class="btn" onclick="window.print()">打印</button></div>
</div>
<div class="card">
<h3>已导出的报告</h3>
<div class="cap"><a class="plain" href="#">11-01 16:12 · 第 1 次检查.pdf</a><br><span class="muted">王敏 导出 · 12 页</span></div>
<p class="note">报告生成后不再改动,随项目归档;重新导出会生成一份新的。</p>
</div>
</div>
<div class="pages" data-pages></div>
</div>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, $$, esc, toast } = App;
App.shell({ crumbs: [["工作台", "workbench.html"], [D.project.name, "project.html"], ["报告", ""]], tab: "report" });
const C = D.check, SEV = { fatal: "致命", major: "重要", hint: "提示" };
const ST = (f) => f.status === "ok" ? `已确认无误 · ${f.by} ${f.at}` : '<span class="st-todo">待处理</span>';
const head = (n, total) => `<div class="report-head"><img src="../design/logo/svg/logo-horizontal.svg" alt="标博士"><span>投标文件辅助检查报告</span></div>`;
const foot = (n) => `<div class="report-foot"><span>本报告为辅助检查结果,不构成对投标结果的保证</span><span class="fig">${n} / 3</span></div>`;
const cnt = (s, st) => C.findings.filter((f) => f.sev === s && (st ? (f.status || "todo") === st : true)).length;
function render() {
const cites = $("[data-opt=cites]").checked, appx = $("[data-opt=appendix]").checked;
const p1 = `<section class="a4">${head()}
<h2>${D.project.name}</h2><p class="sub">采购包 1 · 智慧大厅软硬件集成</p>
<dl><dt>项目编号</dt><dd>${D.project.no}</dd><dt>采购人</dt><dd>${D.project.buyer}</dd><dt>法律体系</dt><dd>政府采购 · 公开招标</dd>
<dt>投标截止</dt><dd>2026-11-20 09:30</dd><dt>投标文件</dt><dd>${C.files}</dd><dt>检查依据</dt><dd>招标文件 + 补遗 1;清单 v2(王敏 2026-11-01 11:20 确认)</dd>
<dt>检查时间</dt><dd>2026-11-02 14:20(第 ${C.no} 次检查)</dd><dt>规则版本</dt><dd>${C.rules}</dd><dt>导出</dt><dd>王敏 · 2026-11-02 15:20</dd></dl>
<h4>检查覆盖范围</h4>
<p><b>查了:</b>清单 26 条的响应完整性;形式要件(格式文件、签章位置有无、落款日期、其他项目名称);资质匹配(以投标截止日判断有效期);报价形式(限价、开标一览表与分项合计、大小写)。</p>
<p><b>没查:</b>份数、密封、CA 加密、递交回执(见附录「需人工核对」);签章真伪;报价是否合理;评分高低。</p>
<h4>汇总</h4>
<table class="sumtab"><tr><th>级别</th><th>问题数</th><th>待处理</th><th>已确认无误</th><th>已修改</th><th>已忽略</th></tr>
${["fatal", "major", "hint"].map((s) => `<tr><td><span class="sev ${s}">${SEV[s]}</span></td><td class="fig">${cnt(s)}</td><td class="fig">${cnt(s, "todo")}</td><td class="fig">${cnt(s, "ok")}</td><td class="fig">0</td><td class="fig">0</td></tr>`).join("")}</table>
<p class="cap muted" style="margin-top:8px">本次已解决 ${C.resolved} 项(上次检查有、这次没有再发现)。</p>
<div class="disc">本报告由标博士根据招标文件和投标文件自动比对生成,经用户逐项处理后导出,用于投标前自查。检查范围以上方「检查覆盖范围」为准,结论可能存在遗漏或误判,不构成对资格审查、符合性审查或评标结果的任何保证。</div>
${foot(1)}</section>`;
const p2 = `<section class="a4">${head()}<h4 style="margin-top:28px">问题明细</h4>
<table><tr><th style="width:52px">级别</th><th>问题</th><th style="width:150px">处理</th></tr>
${C.findings.map((f) => `<tr><td><span class="sev ${f.sev}">${SEV[f.sev]}</span></td><td><b>${esc(f.title)}</b><br>${esc(f.text[0])}${cites ? `<div class="cites">${f.cites.map((c) => `${D.docs[c[0]].name} 第 ${c[1]} 页${c[3] ? `「${esc(c[3])}」` : ""}`).join(";")}</div>` : ""}</td><td>${ST(f)}</td></tr>`).join("")}</table>${foot(2)}</section>`;
const p3 = appx ? `<section class="a4">${head()}<h4 style="margin-top:28px">附录 · 需人工核对</h4>
<table><tr><th>要求</th><th style="width:130px">出处</th><th style="width:150px">核对</th></tr>
${C.manual.map((m) => `<tr><td>${m.title}</td><td class="cites">${D.docs[m.cite[0]].name} 第 ${m.cite[1]} 页</td><td>未勾选</td></tr>`).join("")}</table>
<p class="cap muted" style="margin-top:10px">「未勾选」表示导出时用户尚未在系统里确认核对。</p>${foot(3)}</section>` : "";
$("[data-pages]").innerHTML = p1 + p2 + p3;
}
$$("[data-opt]").forEach((c) => (c.onchange = render));
$("[data-dl]").onclick = () => toast("报告已导出(原型不生成文件)");
render();
</script>
</body>
</html>

274
docs/prototype/results.html Normal file
View File

@@ -0,0 +1,274 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>查标 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.res-bar{display:flex;align-items:flex-end;gap:var(--sp-4);flex-wrap:wrap}
.res-bar h2{font:var(--fw-bold) var(--fs-h2)/var(--lh-h2) var(--font-serif);margin:0}
.res-bar .actions{margin-left:auto;display:flex;align-items:center;gap:var(--sp-3)}
.filters{display:flex;align-items:center;gap:var(--sp-3);margin:var(--sp-5) 0 var(--sp-3)}
.filters .right{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
.finding{outline:none}
.finding.focus{box-shadow:inset 0 0 0 2px var(--c-ink)}
.finding.leaving{opacity:.3;transition:opacity var(--dur-base)}
.u4{margin-top:var(--sp-3);padding:var(--sp-3) var(--sp-4);background:var(--bg-sunken);border-radius:var(--radius-sm)}
.u4 label{font-weight:var(--fw-bold);display:block;margin-bottom:6px}
.u4 .row{margin-top:var(--sp-2)}
.group-h{display:flex;align-items:center;gap:var(--sp-2);width:100%;padding:var(--sp-3) var(--sp-6);border:0;border-top:1px solid var(--border-subtle);background:var(--bg-sunken);color:var(--text-secondary);font:var(--fs-body) var(--font-sans);cursor:pointer;text-align:left}
.group-h:hover{color:var(--text-primary)}
.group-h .chev{transition:transform var(--dur-fast)}
.group-h[aria-expanded="true"] .chev{transform:rotate(90deg)}
.processed .finding{background:#FCFCFB}
.resolved-row{display:flex;gap:var(--sp-3);padding:var(--sp-3) var(--sp-6);border-top:1px solid var(--border-subtle);color:var(--text-secondary)}
.resolved-row s{color:var(--text-primary)}
.empty-todo{padding:var(--sp-10) var(--sp-6);text-align:center;color:var(--text-secondary)}
.sign .note-text{color:var(--text-secondary)}
</style>
</head>
<body>
<main>
<div class="content wide">
<div class="res-bar">
<div>
<h2>采购包 1 的检查结果</h2>
<p class="muted cap" style="margin:4px 0 0">第 <span class="fig">2</span> 次检查 · 11月2日 14:20 · 商务册 v3、技术册 v3、报价册 v2 · 规则版本 IT 集成 2026.11 · 清单版本 v2(已确认)</p>
</div>
<div class="actions">
<span class="cost">24 小时内复查不扣额度(至今天 16:00)</span>
<a class="btn" href="bid-check.html">重新检查</a>
<a class="btn primary" href="report.html" data-export>导出报告</a>
</div>
</div>
<p class="cap" data-export-note style="text-align:right;margin:6px 0 0;color:var(--text-secondary)"></p>
<div class="filters">
<div class="seg" role="group" aria-label="按级别筛选" data-seg>
<button class="on" data-f="all">全部 <span class="fig" data-c="all"></span></button>
<button data-f="fatal"><span class="sev fatal">致命<span class="fig" data-c="fatal"></span></span></button>
<button data-f="major"><span class="sev major">重要<span class="fig" data-c="major"></span></span></button>
<button data-f="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" data-batch hidden>批量确认无误(<span class="fig" data-batch-n></span>)</button>
<button class="btn sm text" data-keys>快捷键 <span class="kbd">?</span></button>
</div>
</div>
<div class="results" data-list></div>
<div class="section-h"><h2>需人工核对</h2><span class="cap muted">这些要求从文件里看不出来,请递交前自己核对并勾选。勾选结果会写进报告附录。</span></div>
<div class="card" data-manual></div>
</div>
</main>
<aside class="drawer" aria-label="原文" data-drawer><div data-viewer></div></aside>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, $$, esc, cite, dialog, toast } = App;
App.shell({ crumbs: [["工作台", "workbench.html"], [D.project.name, "project.html"], ["查标", ""]], tab: "results" });
const SEV = { fatal: "致命", major: "重要", hint: "提示" };
const ACT = { ok: "确认无误", fixed: "已修改", ignored: "已忽略" };
const F = D.check.findings.map((f) => ({ status: "todo", ...f }));
let filter = "all", focusId = null, lastHandled = null;
/* 原文抽屉 */
const drawer = $("[data-drawer]");
const V = App.viewer($("[data-viewer]"), { closable: true, onClose: () => drawer.classList.remove("open"), onOpen: () => drawer.classList.add("open") });
document.addEventListener("click", (e) => {
const a = e.target.closest("[data-open]");
if (!a) return;
e.preventDefault();
const f = F.find((x) => x.id === a.closest("[data-id]")?.dataset.id);
V.open(a.dataset.open.split("|").map((v, i) => i === 1 ? +v : v), f ? f.cites : null);
$$(".cite.on").forEach((x) => x.classList.remove("on")); a.closest(".cite")?.classList.add("on");
});
function sign(f) {
return `<div class="sign" ${f.status === "ignored" ? 'style="color:var(--text-secondary)"' : ""}><span>${f.status === "ignored" ? "–" : "✓"}</span><b>${f.by}</b><span class="fig">${f.at}</span><span>${f.status === "ok" ? "已确认无误" : f.status === "fixed" ? "已修改" : "已忽略"}</span>${f.note ? `<span class="note-text">· ${esc(f.note)}</span>` : ""}<a class="plain undo" href="#" data-undo>撤销</a></div>`;
}
function row(f) {
const cites = f.cites.map((c) => cite(c)).join("") + (f.missing ? `<div class="cite missing"><span class="cite-src">投标文件 · 未找到</span><p>${f.missing.replace(/^.*?· /, "")}</p></div>` : "");
const actions = f.status === "todo"
? `<div class="f-actions">
<button class="btn ${f.sev === "hint" ? "sm" : ""}" data-act="ok" title="我核对过,投标文件这里没问题">确认无误</button>
<button class="btn ${f.sev === "hint" ? "sm" : ""}" data-act="fixed" title="我已在投标文件里修改,复查时会再查">已修改</button>
<button class="btn ${f.sev === "hint" ? "sm" : ""}" data-act="ignored" title="知道这个问题,决定不处理,需要填原因">忽略并注明原因</button>
<a class="plain feedback" href="#" data-feedback>判断有误?</a></div>`
: sign(f);
return `<article class="finding ${f.sev === "fatal" ? "is-fatal" : ""} ${focusId === f.id ? "focus" : ""}" data-id="${f.id}" tabindex="-1">
<div>
<div class="f-head"><span class="sev ${f.sev}">${SEV[f.sev]}</span><span class="f-title">${esc(f.title)}</span>${f.flag ? `<span class="flag">${f.flag}</span>` : ""}</div>
${f.text.map((t) => `<p class="f-text">${esc(t)}</p>`).join("")}
${actions}
</div>
<aside>${cites}</aside>
</article>`;
}
let openProcessed = false, openResolved = false;
function render() {
const todo = F.filter((f) => f.status === "todo");
const shown = todo.filter((f) => filter === "all" || f.sev === filter);
const done = F.filter((f) => f.status !== "todo");
const n = (s) => todo.filter((f) => f.sev === s).length;
$("[data-c=all]").textContent = todo.length;
["fatal", "major", "hint"].forEach((s) => ($(`[data-c=${s}]`).textContent = n(s)));
$("[data-list]").innerHTML =
(shown.length ? shown.map(row).join("") : `<div class="empty-todo">${filter === "all" ? "全部问题都已处理。报告会列出每条的处理人、时间和原因。" : "这个级别没有待处理的问题。"}</div>`) +
`<button class="group-h" data-toggle="p" aria-expanded="${openProcessed}"><span class="chev">›</span>已处理(<span class="fig">${done.length}</span>)</button>
<div class="processed" ${openProcessed ? "" : "hidden"}>${done.map(row).join("")}</div>
<button class="group-h" data-toggle="r" aria-expanded="${openResolved}"><span class="chev">›</span>本次已解决(<span class="fig">${D.check.resolved}</span>)· 上次检查有、这次没有再发现的问题,自动关闭</button>
<div ${openResolved ? "" : "hidden"}>
<div class="resolved-row"><span class="sev major">重要</span><s>未找到售后服务方案中「响应时间要求」的响应</s><span class="cap">上次:已修改</span></div>
<div class="resolved-row"><span class="sev hint">提示</span><s>投标文件中项目编号写成「ZC-2026-0481」</s><span class="cap">上次:已修改</span></div>
</div>`;
// 就绪状态、页签计数、导出提示、批量
const fatal = n("fatal");
$$("[data-fatal-count]").forEach((el) => (el.outerHTML = fatal ? `<span class="sev fatal" data-fatal-count>致命<span class="fig">${fatal}</span></span>` : `<span data-fatal-count></span>`));
$("[data-ready]").innerHTML = fatal ? "未就绪" : `<span class="tag status" title="只表示致命问题都已处理,不代表投标文件没有问题">就绪</span>`;
$("[data-export-note]").textContent = fatal ? `还有 ${fatal} 项致命问题待处理,报告会如实列出。` : "";
const hints = todo.filter((f) => f.sev === "hint").length;
$("[data-batch]").hidden = !(filter === "hint" && hints > 1);
$("[data-batch-n]").textContent = hints;
}
function handle(id, act, extra = {}) {
const f = F.find((x) => x.id === id);
const el = $(`[data-id="${id}"]`);
const apply = () => {
Object.assign(f, { status: act, by: D.user.name, at: "11月2日 15:02", ...extra });
lastHandled = id;
const next = nextTodo(id);
focusId = next;
render();
toast(`「${esc(f.title)}」已标记为${act === "ok" ? "已确认无误" : act === "fixed" ? "已修改" : "已忽略"} · <a href="#" data-toast-undo>撤销</a>`);
};
if (el) { el.classList.add("leaving"); setTimeout(apply, 160); } else apply();
}
function undo(id) {
const f = F.find((x) => x.id === id);
Object.assign(f, { status: "todo", note: "", by: "", at: "" });
focusId = id;
render();
toast(`已撤销:「${esc(f.title)}」回到待处理`);
}
function nextTodo(id) {
const list = F.filter((f) => f.status === "todo" && f.id !== id && (filter === "all" || f.sev === filter));
const all = F.filter((f) => filter === "all" || f.sev === filter);
const i = all.findIndex((f) => f.id === id);
return (list.find((f) => all.indexOf(f) > i) || list[0] || {}).id || null;
}
function confirmOk(id) {
const f = F.find((x) => x.id === id);
if (f.sev !== "fatal") return handle(id, "ok");
// U4:致命问题确认无误也要写说明
const el = $(`[data-id="${id}"] .f-actions`);
if ($(".u4", el.parentNode)) return;
el.insertAdjacentHTML("afterend", `<div class="u4"><label for="u4-${id}">说明为什么没有问题(必填)</label>
<textarea class="input" id="u4-${id}" placeholder="例如:内存配置在技术册第 15 页的配置清单里已写为 512G,偏离表是笔误,已另行更正"></textarea>
<p class="cap muted" style="margin:6px 0 0">致命问题被确认为没问题,等于系统可能判断错了。这条说明会进入处理记录,也会作为误报反馈发给我们。</p>
<div class="row"><button class="btn primary sm" data-u4-ok>确认无误</button><button class="btn sm" data-u4-cancel>取消</button><span class="cap" data-u4-err></span></div></div>`);
const box = $(".u4", el.parentNode);
$("textarea", box).focus();
$("[data-u4-cancel]", box).onclick = () => box.remove();
$("[data-u4-ok]", box).onclick = () => {
const v = $("textarea", box).value.trim();
if (v.length < 5) {
$("[data-u4-err]", box).innerHTML = '<span style="color:var(--warn-text)">! 请至少写 5 个字</span>';
$("textarea", box).style.borderColor = "var(--warn)";
return;
}
handle(id, "ok", { note: v });
};
}
function ignore(id) {
const f = F.find((x) => x.id === id);
dialog({
title: "忽略这个问题?",
body: `<p>「${esc(f.title)}」</p>
<div class="field" data-field><label for="ig">忽略原因 <small>(必填,至少 5 个字)</small></label>
<textarea class="input" id="ig" placeholder="说明为什么不处理这一项"></textarea>
<div class="chips">${["招标文件允许", "已在其他分册响应", "与本项目无关"].map((c) => `<button class="btn" type="button" data-chip>${c}</button>`).join("")}</div>
<span class="msg">原因会写进处理记录和检查报告。</span></div>`,
ok: "忽略并记录原因",
onOpen: (s) => $$("[data-chip]", s).forEach((b) => (b.onclick = () => { $("#ig", s).value = b.textContent + ":"; $("#ig", s).focus(); })),
onOk: (s) => {
const v = $("#ig", s).value.trim();
if (v.replace(/[::]$/, "").length < 5) {
$("[data-field]", s).classList.add("err");
$(".msg", s).innerHTML = '<span class="ico w">!</span>请写清楚原因,至少 5 个字';
return false;
}
handle(id, "ignored", { note: v });
}
});
}
function feedback(id) {
const f = F.find((x) => x.id === id);
dialog({
title: "反馈:判断有误",
body: `<p class="muted">「${esc(f.title)}」</p>
<div class="stack">${["这不是问题(误报)", "级别不对", "出处定位错了", "其他"].map((t, i) => `<label class="row" style="gap:8px"><input type="radio" name="fb" ${i ? "" : "checked"} style="accent-color:var(--c-ink)"> ${t}</label>`).join("")}</div>
<div class="field" style="margin-top:var(--sp-4)"><label for="fbt">补充说明 <small>(选填)</small></label><textarea class="input" id="fbt"></textarea></div>
<p class="cap muted">反馈只用于改进检查规则,不会改变这条问题的处理状态。</p>`,
ok: "提交反馈",
onOk: () => toast("已收到反馈,谢谢。我们会逐条复盘。")
});
}
document.addEventListener("click", (e) => {
const t = e.target;
const id = t.closest("[data-id]")?.dataset.id;
if (t.matches("[data-act]")) { focusId = id; const a = t.dataset.act; a === "ok" ? confirmOk(id) : a === "fixed" ? handle(id, "fixed") : ignore(id); }
else if (t.matches("[data-undo]")) { e.preventDefault(); undo(id); }
else if (t.matches("[data-toast-undo]")) { e.preventDefault(); $(".toast")?.remove(); undo(lastHandled); }
else if (t.matches("[data-feedback]")) { e.preventDefault(); feedback(id); }
else if (t.closest("[data-toggle]")) { const k = t.closest("[data-toggle]").dataset.toggle; k === "p" ? (openProcessed = !openProcessed) : (openResolved = !openResolved); render(); }
else if (t.closest("[data-f]")) { filter = t.closest("[data-f]").dataset.f; $$("[data-f]").forEach((b) => b.classList.toggle("on", b.dataset.f === filter)); focusId = null; render(); }
else if (t.closest("[data-batch]")) {
const hs = F.filter((f) => f.status === "todo" && f.sev === "hint");
dialog({ title: `批量确认 ${hs.length} 条「提示」?`, body: "<p>这些问题都会标记为「已确认无误」,处理记录里逐条写上你的名字和时间。批量处理只对「提示」级开放。</p>", ok: "批量确认无误",
onOk: () => { hs.forEach((f) => Object.assign(f, { status: "ok", by: D.user.name, at: "11月2日 15:04" })); render(); toast(`已批量确认 ${hs.length} 条提示`); } });
}
else if (t.closest("[data-keys]")) keysHelp();
});
function keysHelp() {
dialog({ title: "快捷键", cancel: null, ok: "知道了",
body: `<dl class="kv">${[["J / K", "下一条 / 上一条"], ["O 或 Enter", "打开当前问题的原文"], ["1", "确认无误"], ["2", "已修改"], ["3", "忽略并注明原因"], ["U", "撤销刚才的处理"], ["Esc", "关闭原文或对话框"]].map(([k, v]) => `<dt><span class="kbd">${k}</span></dt><dd>${v}</dd>`).join("")}</dl>` });
}
document.addEventListener("keydown", (e) => {
if (e.target.matches("input, textarea") || $(".scrim")) return;
const list = $$("[data-list] > .finding");
let i = list.findIndex((el) => el.dataset.id === focusId);
const k = e.key.toLowerCase();
if (k === "j" || k === "k") {
i = k === "j" ? Math.min(i + 1, list.length - 1) : Math.max(i - 1, 0);
focusId = list[i]?.dataset.id; list.forEach((el) => el.classList.toggle("focus", el.dataset.id === focusId));
list[i]?.scrollIntoView({ block: "nearest", behavior: "smooth" });
} else if ((k === "o" || k === "enter") && focusId) { $(`[data-id="${focusId}"] [data-open]`)?.click(); }
else if (k === "1" && focusId) confirmOk(focusId);
else if (k === "2" && focusId) handle(focusId, "fixed");
else if (k === "3" && focusId) ignore(focusId);
else if (k === "u" && lastHandled) undo(lastHandled);
else if (k === "escape") drawer.classList.remove("open");
else if (e.key === "?") keysHelp();
});
/* 需人工核对 */
$("[data-manual]").innerHTML = D.check.manual.map((m) => `<label class="chk"><input type="checkbox" data-m="${m.id}"><div style="flex:1"><div>${m.title}</div><div class="cap muted" data-m-sign></div></div><a class="cite-src" href="#" data-open="${m.cite.join("|")}">${App.citeLabel(m.cite)}</a></label>`).join("");
$$("[data-m]").forEach((c) => (c.onchange = () => { $("[data-m-sign]", c.parentNode).innerHTML = c.checked ? `<span style="color:var(--confirmed)">✓ ${D.user.name} 11月2日 15:06 已核对</span>` : ""; }));
render();
</script>
</body>
</html>

View File

@@ -0,0 +1,75 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 标博士原型</title>
<link rel="stylesheet" href="../design/system/tokens.css">
<link rel="stylesheet" href="../design/system/fonts-local.css">
<link rel="stylesheet" href="../design/system/components.css">
<link rel="stylesheet" href="assets/app.css">
<style>
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-4);margin-bottom:var(--sp-6)}
.stat{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:var(--sp-4) var(--sp-5)}
.stat small{display:block;color:var(--text-secondary);font-size:var(--fs-caption)}
.stat .big{font:600 var(--fs-figure)/var(--lh-figure) var(--font-figures);font-variant-numeric:tabular-nums}
.stat .unit{color:var(--text-secondary);margin-left:4px}
.stat p{margin:var(--sp-1) 0 0;font-size:var(--fs-caption);color:var(--text-secondary)}
.toolbar{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-3)}
.toolbar .right{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center}
table.data tbody tr{cursor:pointer}
table.data tbody tr:hover td{background:var(--bg-hover)}
</style>
</head>
<body>
<main>
<div class="content">
<div class="page-title">
<div><h1>工作台</h1><p>按投标截止时间排序。截止临近的项目排在前面。</p></div>
<div class="actions"><a class="btn primary" href="project-new.html">新建项目</a></div>
</div>
<div class="stats">
<div class="stat"><small>进行中的项目</small><span class="big">4</span><p>已递交 1 · 已归档 1</p></div>
<div class="stat"><small>7 天内截止</small><span class="big">2</span><p class="due-soon" style="color:var(--warn-text)">最近:今天 17:00</p></div>
<div class="stat"><small>待处理的致命问题</small><span class="big">3</span><p>在 1 个项目里</p></div>
<div class="stat"><small>本期额度剩余</small><span class="big">37</span><span class="unit">次解析</span><p>检查 12 次 · <a class="plain" href="quota.html">查看明细</a></p></div>
</div>
<div class="toolbar">
<div class="seg" role="group" aria-label="按状态筛选" data-seg>
<button class="on" data-s="active">进行中</button><button data-s="all">全部</button><button data-s="已递交">已递交</button><button data-s="已归档">已归档</button>
</div>
<div class="right"><input class="input" placeholder="搜索项目名称、采购人、项目编号" style="width:280px"></div>
</div>
<table class="data">
<thead><tr><th>项目</th><th>所投包</th><th>截止</th><th>状态</th><th>就绪</th><th class="r">致命</th></tr></thead>
<tbody data-rows></tbody>
</table>
<p class="note">「就绪」只在「已检查」的项目上显示,只表示致命问题都已处理,不代表投标文件没有问题。截止临近用赭色提示,不用红色。</p>
</div>
</main>
<script src="assets/data.js"></script>
<script src="assets/app.js"></script>
<script>
const { D, $, $$ } = App;
App.shell({ nav: "workbench", crumbs: [["工作台", ""]] });
let s = "active";
function render() {
const rows = [...D.projects].sort((a, b) => a.hoursLeft - b.hoursLeft).filter((p) => s === "all" || (s === "active" ? !["已递交", "已归档"].includes(p.status) : p.status === s));
$("[data-rows]").innerHTML = rows.map((p) => `<tr data-href="${p.href}">
<td><div class="proj-name">${p.name}</div><div class="proj-sub">${p.buyer}</div></td>
<td>${p.lot}</td><td class="${p.dueCls}">${p.due}</td>
<td><span class="tag status">${p.status}</span></td>
<td>${p.ready === "就绪" ? '<span class="tag status" title="只表示致命问题都已处理,不代表投标文件没有问题">就绪</span>' : p.ready || "—"}</td>
<td class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : "—"}</td></tr>`).join("")
|| `<tr><td colspan="6" class="muted" style="text-align:center;padding:40px">没有符合条件的项目。</td></tr>`;
}
document.addEventListener("click", (e) => {
const b = e.target.closest("[data-s]");
if (b) { s = b.dataset.s; $$("[data-s]").forEach((x) => x.classList.toggle("on", x === b)); render(); }
const r = e.target.closest("[data-href]");
if (r) location.href = r.dataset.href;
});
render();
</script>
</body>
</html>