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

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

301 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>表单 · 标博士 UI demo</title>
<link rel="icon" href="../logo/favicon.svg">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/fonts-local.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 只属于本页的排版 */
.lot-radios{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3)}
.f-card{max-width:none}
.hint-warn{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--warn-text);display:flex;gap:6px;align-items:flex-start}
.hint-warn .ico{width:14px;height:14px;font-size:10px;margin:3px 0 0}
</style>
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>② 表单</h1>
<p>三张真实业务表单:新建项目、资质录入、处理问题时的「忽略并注明原因」。标签在上,必填写「(必填)」,错误用赭色边框 + 下方赭色小字 + 「!」图标,离开输入框时校验、提交时汇总。</p>
<nav class="d-toc" aria-label="本页目录"><a href="#s-new">01 新建项目</a><a href="#s-states">02 状态对照</a><a href="#s-qual">03 资质录入</a><a href="#s-ignore">04 忽略并注明原因 · 致命确认说明</a></nav>
</div>
<!-- 01 新建项目 -->
<section class="d-sec" id="s-new">
<div class="d-sec-h"><span class="no">01</span><h2>新建项目</h2><span class="ref">规范 §7.13 · §8.3 · FR-M2-01</span></div>
<div class="d-grid c-side">
<form class="card f-card" novalidate data-new>
<div class="alert warn err-sum" data-errsum hidden><span class="ico w">!</span><div><p><b>有 <span data-errn>2</span> 处需要修改</b></p><ul data-errlist></ul></div></div>
<fieldset class="form-sec">
<legend>项目信息<small>招标文件解析后会自动补全,可以先只填名称</small></legend>
<div class="form">
<div class="field span2" id="f-name"><label for="n-name">项目名称 <small>(必填)</small></label><input class="input" id="n-name" placeholder="例如:某市政务服务中心智慧大厅建设项目"><span class="msg">只在你的工作台里显示,可以用简称</span></div>
<div class="field"><label for="n-no">项目编号</label><input class="input" id="n-no" placeholder="招标文件封面上的编号"></div>
<div class="field"><label for="n-buyer">采购人</label><input class="input" id="n-buyer" placeholder="例如:某市政务服务管理办公室"></div>
<div class="field span2"><label id="l-regime">法律体系</label>
<div class="lot-radios" role="radiogroup" aria-labelledby="l-regime">
<label class="choice-card"><input type="radio" class="ck" name="regime" checked><span><b>让标博士识别</b><small>按招标文件的依据和格式判断</small></span></label>
<label class="choice-card"><input type="radio" class="ck" name="regime"><span><b>政府采购</b><small>结论用「投标无效」</small></span></label>
<label class="choice-card"><input type="radio" class="ck" name="regime"><span><b>招标投标</b><small>结论用「否决投标」</small></span></label>
</div></div>
<div class="field"><label>采购方式</label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val"><span class="ph">解析后识别</span></span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option" aria-selected="true" data-label='<span class="ph">解析后识别</span>'>解析后识别</li><li class="grp">政府采购</li><li role="option">公开招标</li><li role="option">邀请招标</li><li role="option">竞争性磋商</li><li role="option">竞争性谈判</li><li role="option">询价</li></ul></div></div>
<div class="field" id="f-due"><label>投标截止 <small>(必填)</small></label>
<div class="dt-pair"><div class="pop" data-date data-marks="2026-11-20" data-new-date><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val"><span class="ph">选择日期</span></span><i data-ic="calendar"></i></button><div class="cal"></div></div><input class="input date-trigger" value="09:30" aria-label="截止时间"></div>
<span class="msg">用于工作台的倒计时;补遗更正后会提示你核对</span></div>
</div>
</fieldset>
<fieldset class="form-sec">
<legend>招标文件<small>PDF、Word 或扫描件;补遗可以以后追加</small></legend>
<div class="field fill" id="f-file">
<div class="drop" tabindex="0" role="button" aria-label="上传招标文件" data-drop><i data-ic="upload" class="lg"></i><b>拖入或点击上传招标文件</b><span>单个文件不超过 200 MB · 扩展名不可信时以文件内容为准</span></div>
<div data-files style="margin-top:var(--sp-3)"></div>
</div>
<div class="field fill" style="margin-top:var(--sp-5)"><label for="n-note">备注 <small>(选填)</small></label><textarea class="input" id="n-note" placeholder="例如:只投采购包 1,联合体伙伴待定"></textarea></div>
</fieldset>
<div class="form-foot">
<span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次 · 解析失败、超时自动退回</span>
<button type="button" class="btn">取消</button>
<button type="submit" class="btn primary swap" data-submit><span class="idle">创建并开始解析</span><span class="busy">正在创建…</span></button>
</div>
</form>
<div class="d-stack">
<div class="d-box">
<h3>试一试</h3>
<ol class="d-rules" style="border:0;padding-top:0;margin-top:0">
<li>什么都不填,直接点「创建并开始解析」:顶部汇总 + 每个字段下方说明,点汇总里的链接跳到字段。</li>
<li>在项目名称里输入再清空,离开输入框时就会校验。</li>
<li>点上传区,会加入两个示意文件,其中一个扩展名和内容不符。</li>
<li>全部填好后提交:按钮变成「正在创建…」,宽度不变,表单整体禁用。</li>
</ol>
</div>
<div class="d-box">
<h3>规则</h3>
<ul class="d-rules" style="border:0;padding-top:0;margin-top:0">
<li>必填写「(必填)」,不用红色星号;选填项多的表单反过来写「(选填)」。</li>
<li>错误说明写清楚怎么改,不写「格式错误」。</li>
<li>会扣点的提交按钮,左侧直接写扣多少、还剩多少。</li>
<li>文件扩展名和内容不符只提醒,不拦截(样本分析 07 的结论)。</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 02 状态对照 -->
<section class="d-sec" id="s-states">
<div class="d-sec-h"><span class="no">02</span><h2>同一字段的状态对照</h2><span class="ref">规范 §7.13</span></div>
<div class="d-box">
<div class="d-states wide">
<div class="d-state"><small>正常</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226IS0458"></div></div>
<div class="d-state"><small>聚焦</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input is-focus" value="01226IS04"></div></div>
<div class="d-state"><small>错误</small><div class="field err"><label>证书编号 <small>(必填)</small></label><input class="input" value="" aria-invalid="true"><span class="msg"><span class="ico w">!</span>请填写证书编号,查标时用它核对附件</span></div></div>
<div class="d-state"><small>禁用</small><div class="field dis"><label>证书编号</label><input class="input" value="01226IS0458" disabled><span class="msg">已被 2 个已递交项目引用,不能修改</span></div></div>
</div>
</div>
<div class="d-grid c2" style="margin-top:var(--sp-6)">
<div class="card">
<div class="card-h"><h3>填写中</h3><span class="more cap muted">正常状态</span></div>
<div class="form">
<div class="field"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系"></div>
<div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877"></div>
</div>
<div class="form-foot"><span class="cost">不扣额度</span><button class="btn">取消</button><button class="btn primary swap"><span class="idle">保存资质</span><span class="busy">正在保存…</span></button></div>
</div>
<div class="card">
<div class="card-h"><h3>提交中</h3><span class="more cap muted">整表禁用,按钮换成「正在…」,宽度不变</span></div>
<fieldset disabled style="border:0;margin:0;padding:0;min-width:0">
<div class="form">
<div class="field dis"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系" disabled></div>
<div class="field dis"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877" disabled></div>
</div>
</fieldset>
<div class="form-foot"><span class="cost">不扣额度</span><button class="btn" disabled>取消</button><button class="btn primary swap is-loading" aria-busy="true"><span class="idle">保存资质</span><span class="busy">正在保存…</span></button></div>
</div>
</div>
</section>
<!-- 03 资质录入 -->
<section class="d-sec" id="s-qual">
<div class="d-sec-h"><span class="no">03</span><h2>资质录入</h2><span class="ref">FR-M6 最小资质库 · 红线 5 不编造</span></div>
<div class="d-grid c-side">
<div class="dialog wide static" role="dialog" aria-labelledby="q-t" style="width:auto">
<h2 id="q-t">添加资质</h2>
<div class="body">
<p class="muted">查标时用来核对证书有没有过期、等级够不够。数据只来自你录入的内容,系统不从外部补数据。</p>
<div class="form">
<div class="field span2"><label for="q-name">名称 <small>(必填)</small></label><input class="input" id="q-name" value="ISO/IEC 27001 信息安全管理体系认证"></div>
<div class="field"><label>类型 <small>(必填)</small></label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val">体系认证</span><i data-ic="chev-down"></i></button>
<ul class="menu" role="listbox"><li role="option">资质证书</li><li role="option" aria-selected="true">体系认证</li><li role="option">软件著作权</li><li role="option">其他</li></ul></div></div>
<div class="field"><label for="q-lv">等级 <small>(选填)</small></label><input class="input" id="q-lv" placeholder="例如:CS3、二级"></div>
<div class="field"><label for="q-no">证书编号 <small>(必填)</small></label><input class="input" id="q-no" value="01226IS0458"></div>
<div class="field"><label for="q-org">发证机构</label><input class="input" id="q-org" value="某认证中心"></div>
<div class="field"><label>发证日期</label><div class="pop" data-date data-value="2023-11-16" data-month="2023-11"><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val">2023-11-16</span><i data-ic="calendar"></i></button><div class="cal"></div></div></div>
<div class="field"><label>有效期至 <small>(必填)</small></label><div class="pop" data-date data-value="2026-11-15" data-marks="2026-11-20"><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val">2026-11-15</span><i data-ic="calendar"></i></button><div class="cal"></div></div>
<span class="hint-warn"><span class="ico w">!</span><span>早于进行中项目「智慧大厅建设项目」的投标截止日 11月20日</span></span></div>
<div class="field span2"><div class="d-row" style="gap:var(--sp-3)"><span id="q-long">长期有效</span><button type="button" class="switch" role="switch" aria-checked="false" aria-labelledby="q-long"><span class="track"></span><span class="st"></span></button><span class="cap muted">打开后不检查有效期</span></div></div>
<div class="field span2"><label>证书扫描件</label>
<div class="file-row"><span class="ext">PDF</span><div><b>ISO27001 证书 2023.pdf</b><span class="cap muted">2 页 · 1.2 MB</span></div><span class="acts"><button type="button" class="btn sm text">替换</button><button type="button" class="icon-btn sm" aria-label="移除附件" data-tip="移除附件"><i data-ic="x"></i></button></span></div>
</div>
</div>
</div>
<div class="foot"><span class="left cap muted">保存后,进行中项目的查标会用新信息复查</span><button class="btn">取消</button><button class="btn primary">保存资质</button></div>
</div>
<div class="d-stack">
<div class="d-box">
<h3>打开真实对话框 <small>可以点</small></h3>
<button class="btn primary" data-qadd><i data-ic="plus"></i>添加资质</button>
<p class="note">空表单直接保存会看到错误状态;「附件」可以稍后补录,列表里显示「待补录」占位,不生成内容。</p>
</div>
<div class="d-box">
<h3>提醒和错误的区别</h3>
<ul class="d-rules" style="border:0;padding-top:0;margin-top:0">
<li><b>错误</b>(赭色边框):必须改了才能保存,例如漏填证书编号。</li>
<li><b>提醒</b>(只有赭色小字,边框不变):可以保存,但查标会提示,例如证书在投标截止日前到期。</li>
<li>到期判断以每个项目的投标截止日为准,不以今天为准。</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 04 忽略并注明原因 -->
<section class="d-sec" id="s-ignore">
<div class="d-sec-h"><span class="no">04</span><h2>忽略并注明原因 · 致命问题的确认说明</h2><span class="ref">规范 §7.4 · §7.13 · U4</span></div>
<div class="results">
<article class="finding">
<div>
<div class="f-head"><span class="sev major">重要</span><span class="f-title">开标一览表与分项价格表合计不一致</span></div>
<p class="f-text">开标一览表 287.60 万元,分项价格表合计 286.80 万元,相差 0.80 万元。按招标文件规定,两者不一致时以开标一览表为准,不作无效处理;但会影响价格评审。</p>
<div class="u4" data-ign>
<div class="field err">
<label for="ig1">忽略原因 <small>(必填,至少 5 个字)</small></label>
<div class="chips"><button type="button" class="btn" data-chip>招标文件允许</button><button type="button" class="btn" data-chip>已在其他分册响应</button><button type="button" class="btn" data-chip>与本项目无关</button></div>
<textarea class="input" id="ig1" maxlength="200" aria-invalid="true">已核对</textarea>
<div class="msg-row"><span class="msg"><span class="ico w">!</span>至少写 5 个字,说明为什么不处理</span><span class="count"><span data-n>3</span> / 200</span></div>
</div>
<div class="row"><button class="btn primary" data-ign-ok>忽略这一项</button><button class="btn text" data-ign-cancel>取消</button><span class="cap muted">「已忽略」不等于确认无误,报告里单独列出</span></div>
</div>
</div>
<aside>
<div class="cite"><a class="cite-src" href="#">报价册 · <span class="fig">第 1 页</span></a><q>投标总价(小写):<mark>2876000.00 元</mark></q></div>
<div class="cite"><a class="cite-src" href="#">报价册 · <span class="fig">第 3 页</span></a><q><mark>合计:2868000.00 元</mark></q></div>
</aside>
</article>
<article class="finding is-fatal">
<div>
<div class="f-head"><span class="sev fatal">致命</span><span class="f-title">授权委托书的单位签章位置未发现盖章</span><span class="flag box">新增</span></div>
<p class="f-text">商务册第 5 页《法定代表人授权委托书》的「投标人(电子签章)」位置是空的。本检查只看有无,不鉴定真伪。</p>
<div class="u4">
<div class="field">
<label for="ig2">确认无误的说明 <small>(必填)</small></label>
<textarea class="input" id="ig2">电子签章在递交时由交易平台统一加盖,Word 稿里本来就是空的。</textarea>
<div class="msg-row"><span class="msg">对致命问题说「没问题」,需要写一句说明;说明会同时作为误报反馈提交。</span><span class="count"><span>30</span> / 200</span></div>
</div>
<div class="row"><button class="btn primary swap" data-loading="已确认无误 · 说明已作为反馈提交"><span class="idle">确认无误</span><span class="busy">正在保存…</span></button><button class="btn text">取消</button></div>
</div>
</div>
<aside>
<div class="cite"><a class="cite-src" href="#">招标文件 · <span class="fig">第 24 页</span></a><q>4.《法定代表人授权委托书》(法定代表人签字或盖章并<mark>加盖单位电子签章</mark>)。</q></div>
<div class="cite"><a class="cite-src" href="#">商务册 · <span class="fig">第 5 页</span></a><q>法定代表人(签字或盖章):李某某  <mark>投标人(电子签章):(签章位置为空)</mark></q></div>
</aside>
</article>
</div>
</section>
</main>
<script src="demo.js"></script>
<script>
const { $, $$, toast, dialog, icon, esc } = Demo;
Demo.demoTop("forms.html");
/* ---------- 新建项目 ---------- */
const F = $("[data-new]");
let files = [], dateVal = "";
$("[data-new-date]").addEventListener("change", (e) => { dateVal = e.detail; setErr("f-due", ""); });
function setErr(id, msg) {
const f = document.getElementById(id);
f.classList.toggle("err", !!msg);
let m = $(":scope > .msg.e", f);
if (msg) {
if (!m) { m = document.createElement("span"); m.className = "msg e"; f.appendChild(m); }
m.innerHTML = `<span class="ico w">!</span>${msg}`;
$$(":scope > .msg:not(.e)", f).forEach((x) => x.hidden = true);
} else { if (m) m.remove(); $$(":scope > .msg", f).forEach((x) => x.hidden = false); }
}
function checkName() { setErr("f-name", $("#n-name").value.trim() ? "" : "请填写项目名称,可以先写简称,之后再改"); }
$("#n-name").addEventListener("blur", checkName);
function renderFiles() {
$("[data-files]").innerHTML = files.map((f, i) => `<div class="file-row"><span class="ext">${f.ext}</span><div><b>${esc(f.name)}</b>${f.warn ? `<span class="hint-warn"><span class="ico w">!</span><span>${f.warn}</span></span>` : `<span class="cap muted">${f.meta}</span>`}</div><span class="acts"><button type="button" class="icon-btn sm" aria-label="移除" data-tip="移除" data-rm="${i}">${icon("x")}</button></span></div>`).join("");
}
$("[data-drop]").addEventListener("click", () => {
files = [{ ext: "PDF", name: "ZC-2026-0418 采购文件.pdf", meta: "186 页 · 8.4 MB" }, { ext: "DOC", name: "关于 ZC-2026-0418 的更正公告.pdf", warn: "扩展名是 .pdf,内容是 Word 文档,会按 Word 解析" }];
renderFiles(); setErr("f-file", "");
});
$("[data-files]").addEventListener("click", (e) => { const b = e.target.closest("[data-rm]"); if (b) { files.splice(+b.dataset.rm, 1); renderFiles(); } });
F.addEventListener("submit", (e) => {
e.preventDefault();
checkName();
setErr("f-due", dateVal ? "" : "请选择投标截止日期,用于倒计时");
setErr("f-file", files.length ? "" : "请上传招标文件,至少一份");
const errs = $$(".field.err", F);
const sum = $("[data-errsum]");
if (errs.length) {
$("[data-errn]").textContent = errs.length;
$("[data-errlist]").innerHTML = errs.map((f) => `<li><a href="#${f.id}">${$("label", f)?.firstChild.textContent.trim() || "招标文件"}</a>:${$(".msg.e", f).textContent.replace(/^!/, "")}</li>`).join("");
sum.hidden = false; sum.scrollIntoView({ block: "center", behavior: "smooth" });
return;
}
sum.hidden = true;
const b = $("[data-submit]");
b.classList.add("is-loading"); b.setAttribute("aria-busy", "true");
$$("input, textarea, button:not([data-submit]), .drop", F).forEach((x) => { x.disabled = true; x.closest(".field")?.classList.add("dis"); });
setTimeout(() => { b.classList.remove("is-loading"); b.removeAttribute("aria-busy"); $$("input, textarea, button", F).forEach((x) => x.disabled = false); $$(".field.dis", F).forEach((x) => x.classList.remove("dis")); toast("项目已创建,正在解析招标文件 · 可以离开本页"); }, 2400);
});
/* ---------- 资质对话框 ---------- */
$("[data-qadd]").onclick = () => dialog({
title: "添加资质", wide: true, ok: "保存资质",
footLeft: '<span class="cap muted">不扣额度</span>',
body: `<p class="muted">数据只来自你录入的内容,系统不从外部补数据。</p><div class="form">
<div class="field span2" id="qd-name"><label for="qdn">名称 <small>(必填)</small></label><input class="input" id="qdn" placeholder="例如:信息系统建设和服务能力等级证书"></div>
<div class="field"><label>类型 <small>(必填)</small></label><div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false"><span class="val">资质证书</span>${icon("chev-down")}</button><ul class="menu" role="listbox"><li role="option" aria-selected="true">资质证书</li><li role="option">体系认证</li><li role="option">软件著作权</li><li role="option">其他</li></ul></div></div>
<div class="field"><label for="qdl">等级 <small>(选填)</small></label><input class="input" id="qdl"></div>
<div class="field" id="qd-no"><label for="qdo">证书编号 <small>(必填)</small></label><input class="input" id="qdo"></div>
<div class="field"><label>有效期至</label><div class="pop" data-date><button type="button" class="select date-trigger" aria-haspopup="dialog" aria-expanded="false"><span class="val"><span class="ph">选择日期</span></span>${icon("calendar")}</button><div class="cal"></div></div></div>
<div class="field span2"><label>证书扫描件 <small>(可以稍后补录)</small></label><div class="drop" style="padding:var(--sp-4)">${icon("upload")}<span>拖入或点击上传 PDF / 图片</span></div></div></div>`,
onOk: (s) => {
let bad = 0;
[["qd-name", "#qdn", "请填写资质名称,和证书上的写法一致"], ["qd-no", "#qdo", "请填写证书编号,查标时用它核对附件"]].forEach(([id, sel, msg]) => {
const f = $("#" + id, s), v = $(sel, s).value.trim();
f.classList.toggle("err", !v); $(".msg", f)?.remove();
if (!v) { bad++; f.insertAdjacentHTML("beforeend", `<span class="msg"><span class="ico w">!</span>${msg}</span>`); }
});
if (bad) return false;
toast("已保存资质");
}
});
/* ---------- 忽略原因 ---------- */
const ig = $("[data-ign]"), ta = $("#ig1", ig), fld = $(".field", ig);
const validate = (show) => { const n = ta.value.trim().length; $("[data-n]", ig).textContent = ta.value.length; const bad = n < 5; if (show || !bad) { fld.classList.toggle("err", bad); $(".msg", fld).innerHTML = bad ? '<span class="ico w">!</span>至少写 5 个字,说明为什么不处理' : "报告和处理记录里会显示这段原因"; } return !bad; };
ta.addEventListener("input", () => validate(false));
ta.addEventListener("blur", () => validate(true));
$$("[data-chip]", ig).forEach((c) => c.onclick = () => { ta.value = c.textContent; validate(true); ta.focus(); });
$("[data-ign-ok]").onclick = () => { if (validate(true)) toast("已忽略 · 原因已记录", { undo: () => toast("已撤销", { ok: false }) }); };
</script>
</body>
</html>