Files
bid-doctor/docs/design/ui-demo/forms.html
YANG JIANKUAN 7f9592ad16 docs(design): 按 v0.2 重刷全部 UI demo,确认 Q1–Q13 与 U 键规则
- ui-demo:控件、表单、卡片、列表、工作台、读标六页按 v0.2 重刷;侧栏与顶栏框架、项目行、署名行、可键入日期输入、对照布局等收回 demo.css / demo.js 共用
- system:tokens 补悬停 / 按下 / 遮罩色、数字字阶与原文排版变量;components.css 按 v0.2 改写并清除裸色值
- design-system.md:写入 Q1–Q13 答案,新增开关、分页、下拉、日期选择等组件规则,类别标签、待确认区、选中态、对照布局按 v0.2 修订
- Q13:额度流水记录 0 点的「免扣」行,FR-M6-03 与领域模型同步;U 键改为撤销当前条
- decisions.md 记录 Q1–Q12、Q13 与 U 键;AGENTS.md 同步进度

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:07:15 +08:00

412 lines
35 KiB
HTML
Raw Permalink 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>
<!--
陈列页 · 表单(v0.2)
新建项目、资质录入、问题处理时的说明(忽略原因 / 致命问题确认说明 U4)。
日期按 Q10:输入框写「2026-11-20 09:30」,可直接键入,也可用日期选择器;界面显示「11月20日 09:30」。
日期输入、字段错误、署名行、行内说明都是共用组件(demo.js 的 Demo.dateInput / Demo.fieldError,demo.css 的 .dt / .sign / .u4)。
主按钮按 Q12:表单一个、对话框一个;问题处理面板里的提交按钮是次按钮(§7.1「问题处理三按钮」)。
?state=errors 直接显示新建项目的错误汇总(截图用)。
-->
<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)}
.card-b.form-b{padding-top:var(--sp-4);padding-bottom:var(--sp-6)}
.card-f .cost{margin-right:auto}
.card-f .cost .hint-u{margin-left:var(--sp-1)}
.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}
.field.err .drop{border-color:var(--warn)}
/* 错误汇总:卡片头下方的一条,不在卡片里再套框 */
.err-sum{margin:0;border-width:var(--line) 0;border-radius:0;outline-offset:-2px}
.err-sum .ico{margin-top:1px}
.err-sum li{color:var(--text-primary)}
.st6{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:var(--sp-5)}
.st3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-5);align-items:start}
.d-box + .d-box,.d-box + .d-grid{margin-top:var(--gap-card)}
.d-box > .d-label{margin-bottom:var(--sp-4)}
.dialog .form{margin-top:var(--sp-1)}
</style>
</head>
<body class="demo">
<header data-demo-top></header>
<main class="d-page">
<div class="d-head">
<h1>② 表单</h1>
<p>必填写「(必填)」,错误用赭色;离开输入框时校验,提交时汇总。日期输入「2026-11-20 09:30」,显示「11月20日 09:30」。</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 · §9.3 · Q10 · Q12</span></div>
<div class="d-grid c-side">
<form class="card flush" novalidate data-new aria-labelledby="h-new">
<div class="card-h"><h3 id="h-new">新建项目</h3></div>
<div class="alert warn err-sum" role="alert" tabindex="-1" data-errsum hidden><span class="ico w" aria-hidden="true">!</span><div><p><b>有 <span data-errn>2</span> 处需要修改</b></p><ul data-errlist></ul></div></div>
<div class="card-b form-b">
<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="例如:某市政务服务中心智慧大厅建设项目" autocomplete="off"><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 id="l-way">采购方式</label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="l-way"><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><li class="grp">招标投标</li><li role="option" data-value="招标-公开">公开招标</li><li role="option" data-value="招标-邀请">邀请招标</li></ul></div></div>
<div class="field" id="f-due"><label for="n-due">投标截止 <small>(必填)</small></label>
<div class="dt" data-dt data-time="09:30" data-left data-req="请填写投标截止时间,例如 2026-11-20 09:30">
<input class="input" id="n-due" placeholder="例如:2026-11-20 09:30" autocomplete="off">
<button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button>
<div class="cal" role="dialog" aria-label="选择投标截止日期" hidden></div>
</div>
<span class="msg" id="n-due-msg" data-dt-msg>24 小时制</span></div>
</div>
</fieldset>
<fieldset class="form-sec">
<legend>招标文件<small>PDF、Word 或扫描件;补遗以后可以追加</small></legend>
<div class="field fill" id="f-file"><span class="vh" id="l-file">招标文件</span>
<div class="drop" tabindex="0" role="button" aria-labelledby="l-file" aria-describedby="drop-sub" data-drop><i data-ic="upload" class="lg"></i><b>拖入或点击上传</b><span id="drop-sub">单个文件不超过 200 MB</span></div>
<div data-files></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>
<div class="card-f">
<span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次<span class="hint-u tip-start" tabindex="0" data-tip="解析失败、超时或系统异常不扣额度,已扣的自动退回">失败不扣</span></span>
<button type="button" class="btn" data-cancel>取消</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>直接点「创建并开始解析」:顶部汇总,点汇总里的字段名跳过去。<a class="lnk" href="?state=errors#s-new">直接看汇总</a></li>
<li>投标截止键入「2026/11/20 9:30」,离开后自动整理成标准格式;键入「2026-11-20 25:00」看错误。</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>会扣点的提交按钮,左侧写扣多少、还剩多少。</li>
<li>扩展名和内容不符只提醒,不拦截。</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 · §7.22 · Q10</span></div>
<div class="d-box">
<span class="d-label">文本输入</span>
<div class="st6">
<div class="d-state"><small>正常</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226IS0458" aria-label="证书编号(正常)"></div></div>
<div class="d-state"><small>悬停</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input is-hover" value="01226IS0458" aria-label="证书编号(悬停)"></div></div>
<div class="d-state"><small>聚焦</small><div class="field"><label>证书编号 <small>(必填)</small></label><input class="input is-focus" value="01226IS04" aria-label="证书编号(聚焦)"></div></div>
<div class="d-state"><small>错误:要改了才能保存</small><div class="field err"><label>证书编号 <small>(必填)</small></label><input class="input" value="" aria-invalid="true" aria-label="证书编号(错误)"><span class="msg"><span class="ico w" aria-hidden="true">!</span>请填写证书编号</span></div></div>
<div class="d-state"><small>提醒:可以保存</small><div class="field"><label>有效期至</label><input class="input" value="2026-11-15" style="font-family:var(--font-figures);font-size:var(--fs-figure-sm)" aria-label="有效期至(提醒)"><span class="hint-warn"><span class="ico w" aria-hidden="true">!</span><span>早于投标截止 11月20日</span></span></div></div>
<div class="d-state"><small>禁用</small><div class="field dis"><label>证书编号</label><input class="input" value="01226IS0458" disabled aria-label="证书编号(禁用)"><span class="msg">已被已递交项目引用</span></div></div>
</div>
</div>
<div class="d-box">
<span class="d-label">日期时间输入(Q10):输入框写「2026-11-20 09:30」,可直接键入常见写法(左一键入的是「2026/11/20 9:30」),离开时整理;下方显示「11月20日 09:30 · 剩 N 天」</span>
<div class="st3">
<div class="d-state"><small>已填写</small><div class="field"><label for="s-dt1">投标截止 <small>(必填)</small></label>
<div class="dt" data-dt data-time="09:30" data-left><input class="input" id="s-dt1" value="2026/11/20 9:30" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择日期" hidden></div></div>
<span class="msg" data-dt-msg>24 小时制</span></div></div>
<div class="d-state"><small>选择器展开:选日期,底部填时间</small><div class="field"><label for="s-dt2">投标截止 <small>(必填)</small></label>
<div class="dt d-open" data-dt data-time="09:30" data-marks="2026-11-20" data-min="2026-11-03"><input class="input is-focus" id="s-dt2" value="2026-11-20 09:30" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="日期选择器" aria-haspopup="dialog" aria-expanded="true" tabindex="-1"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择日期"></div></div></div></div>
<div class="d-state"><small>错误</small><div class="field err"><label for="s-dt3">投标截止 <small>(必填)</small></label>
<div class="dt" data-dt data-time="09:30" data-left><input class="input" id="s-dt3" value="2026-11-20 25:00" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择日期" hidden></div></div>
<span class="msg" data-dt-msg>24 小时制</span></div></div>
</div>
</div>
<div class="d-grid c2">
<div class="card flush">
<div class="card-h"><h3>提交前</h3></div>
<div class="card-b"><div class="form">
<div class="field"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系" aria-label="名称"></div>
<div class="field"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877" aria-label="证书编号"></div>
</div></div>
<div class="card-f"><span class="cost" style="margin-right:auto">不扣额度</span><button class="btn">取消</button><button class="btn primary swap" data-loading="已保存资质"><span class="idle">保存资质</span><span class="busy">正在保存…</span></button></div>
</div>
<div class="card flush">
<div class="card-h"><h3>提交中</h3><span class="cap muted" style="margin-left:auto">整表禁用,按钮宽度不变</span></div>
<fieldset disabled style="border:0;margin:0;padding:0;min-width:0">
<div class="card-b"><div class="form">
<div class="field dis"><label>名称 <small>(必填)</small></label><input class="input" value="ISO 9001 质量管理体系" disabled aria-label="名称"></div>
<div class="field dis"><label>证书编号 <small>(必填)</small></label><input class="input" value="01226Q31877" disabled aria-label="证书编号"></div>
</div></div>
</fieldset>
<div class="card-f"><span class="cost" style="margin-right:auto">不扣额度</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">最小资质库 · 红线 5 · Q12</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><button type="button" class="icon-btn sm x" aria-label="关闭"><i data-ic="x"></i></button>
<div class="body">
<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 id="l-qtype">类型 <small>(必填)</small></label>
<div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="l-qtype"><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 for="q-from">发证日期</label>
<div class="dt" data-dt><input class="input" id="q-from" value="2023-11-16" placeholder="例如:2023-11-16" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择发证日期" hidden></div></div></div>
<div class="field" id="f-qexp"><label for="q-exp">有效期至 <small>(必填)</small></label>
<div class="dt" data-dt data-marks="2026-11-20" data-note="圆点是进行中项目的投标截止日" data-req="请填写有效期,长期有效请打开下方开关"><input class="input" id="q-exp" value="2026-11-15" placeholder="例如:2026-11-15" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false"><i data-ic="calendar"></i></button><div class="cal" role="dialog" aria-label="选择有效期" hidden></div></div>
<span class="hint-warn" data-exp-hint><span class="ico w" aria-hidden="true">!</span><span>早于「智慧大厅建设项目」的投标截止 <span class="fig">11</span>月<span class="fig">20</span>日</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" data-long><span class="track"></span><span class="st"></span></button></div></div>
<div class="field span2"><label>证书扫描件 <small>(可以稍后补录)</small></label>
<div class="file-row"><span class="ext">PDF</span><div><b>ISO27001 证书 2023.pdf</b><span class="cap muted"><span class="fig">2</span> 页 · <span class="fig">1.2</span> 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" data-qadd><i data-ic="plus"></i>添加资质</button>
<p class="cap muted" style="margin:var(--sp-3) 0 0">空表单直接保存看错误;附件可以稍后补录,列表里显示「待补录」,不生成内容。</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 · Q2</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-u4="ign">
<div class="field err">
<label for="ig1">忽略原因 <small>(必填,至少 5 个字)</small></label>
<div class="chips" role="group" aria-label="常用原因"><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" aria-describedby="ig1-msg">已核对</textarea>
<div class="msg-row"><span class="msg" id="ig1-msg" data-def="报告和处理记录里会显示这段原因"><span class="ico w" aria-hidden="true">!</span>至少写 5 个字,说明为什么不处理</span><span class="count"><span data-n>3</span> / 200</span></div>
</div>
<div class="row"><button type="button" class="btn sm" data-ok>忽略这一项</button><button type="button" class="btn sm text" data-x>取消</button><span class="cap muted">「已忽略」在报告里单独列出</span></div>
</div>
</div>
<aside>
<div class="cite"><a class="cite-src" href="#" data-demo>报价册 · 第 <span class="fig">1</span> 页</a><q>投标总价(小写):<mark>2876000.00 元</mark></q></div>
<div class="cite"><a class="cite-src" href="#" data-demo>报价册 · 第 <span class="fig">3</span> 页</a><q><mark>合计:2868000.00 元</mark></q></div>
</aside>
</article>
<article class="finding">
<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" data-u4="ok">
<div class="field">
<label for="ig2">确认无误的说明 <small>(必填)</small></label>
<textarea class="input" id="ig2" maxlength="200" aria-describedby="ig2-msg">电子签章在递交时由交易平台统一加盖,Word 稿里本来就是空的。</textarea>
<div class="msg-row"><span class="msg" id="ig2-msg" data-def="说明会同时作为误报反馈提交">说明会同时作为误报反馈提交</span><span class="count"><span data-n>30</span> / 200</span></div>
</div>
<div class="row"><button type="button" class="btn sm swap" data-ok><span class="idle">确认无误</span><span class="busy">正在保存…</span></button><button type="button" class="btn sm text" data-x>取消</button></div>
</div>
</div>
<aside>
<div class="cite"><a class="cite-src" href="#" data-demo>招标文件 · 第 <span class="fig">24</span> 页</a><q>4.《法定代表人授权委托书》(法定代表人签字或盖章并<mark>加盖单位电子签章</mark>)。</q></div>
<div class="cite"><a class="cite-src" href="#" data-demo>商务册 · 第 <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");
/* 日期输入(Q10)由 demo.js 的 Demo.dateInput() 处理:可直接键入、离开时整理、下方显示「11月20日 09:30 · 剩 N 天」。
* 字段错误用 Demo.fieldError():离开时校验,报错后边输入边复查。 */
const setErr = Demo.fieldError;
const label = (f) => ($("label", f)?.firstChild.textContent || $(".vh", f)?.textContent || "").trim();
/* ---------- 01 新建项目 ---------- */
const F = $("[data-new]"), sum = $("[data-errsum]"), dueDT = $("#f-due [data-dt]");
let files = [];
const RULES = {
"f-name": () => $("#n-name").value.trim() ? "" : "请填写项目名称,可以先写简称",
"f-due": null, // 由日期输入自己校验(必填 + 格式)
"f-file": () => files.length ? "" : "请上传招标文件,至少一份"
};
function check(id) {
const ok = id === "f-due" ? dueDT.dt.commit({ required: true }) : setErr(document.getElementById(id), RULES[id]());
if (!sum.hidden) summary(false);
return ok;
}
function summary(focus) {
const errs = Object.keys(RULES).map((id) => document.getElementById(id)).filter((f) => f.classList.contains("err"));
sum.hidden = !errs.length;
$("[data-errn]").textContent = errs.length;
$("[data-errlist]").innerHTML = errs.map((f) => `<li><a href="#${f.id}" data-go="${f.id}">${esc(label(f))}</a>:${esc($(".msg.e", f).textContent.replace(/^!/, ""))}</li>`).join("");
if (focus && errs.length) { sum.scrollIntoView({ block: "center" }); sum.focus({ preventScroll: true }); }
}
let nameDirty = false;
$("#n-name").addEventListener("input", () => { nameDirty = true; if ($("#f-name").classList.contains("err") && !RULES["f-name"]()) check("f-name"); });
$("#n-name").addEventListener("blur", () => { if (nameDirty || $("#f-name").classList.contains("err")) check("f-name"); });
F.addEventListener("dt-change", () => { if (!sum.hidden) summary(false); });
sum.addEventListener("click", (e) => { const a = e.target.closest("[data-go]"); if (!a) return; e.preventDefault(); const f = document.getElementById(a.dataset.go); f.scrollIntoView({ block: "center" }); ($("input, [data-drop]", f)).focus({ preventScroll: true }); });
function renderFiles() {
$("[data-files]").innerHTML = files.map((f, i) => `<div class="file-row" style="margin-top:var(--sp-3)"><span class="ext">${f.ext}</span><div><b>${esc(f.name)}</b>${f.warn ? `<span class="hint-warn"><span class="ico w" aria-hidden="true">!</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="移除 ${esc(f.name)}" data-tip="移除" data-rm="${i}">${icon("x")}</button></span></div>`).join("");
}
const addFiles = () => {
files = [{ ext: "PDF", name: "ZC-2026-0418 采购文件.pdf", meta: `<span class="fig">186</span> 页 · <span class="fig">8.4</span> MB` }, { ext: "DOC", name: "关于 ZC-2026-0418 的更正公告.pdf", warn: "扩展名是 .pdf,内容是 Word 文档,会按 Word 解析" }];
renderFiles(); check("f-file");
};
$("[data-drop]").addEventListener("click", addFiles);
$("[data-drop]").addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); addFiles(); } });
$("[data-files]").addEventListener("click", (e) => { const b = e.target.closest("[data-rm]"); if (b) { files.splice(+b.dataset.rm, 1); renderFiles(); if (!files.length && !sum.hidden) check("f-file"); } });
$("[data-cancel]", F).onclick = () => toast("(演示)取消:回到工作台", { ok: false });
F.addEventListener("submit", (e) => {
e.preventDefault();
const ok = Object.keys(RULES).map(check).every(Boolean);
if (!ok) { summary(true); return; }
sum.hidden = true;
const b = $("[data-submit]");
b.classList.add("is-loading"); b.setAttribute("aria-busy", "true");
const ctl = $$("input, textarea, button:not([data-submit]), .select", F);
ctl.forEach((x) => x.disabled = true);
$$(".field", F).forEach((x) => x.classList.add("dis"));
$("[data-drop]").setAttribute("aria-disabled", "true"); $("[data-drop]").style.pointerEvents = "none";
setTimeout(() => {
b.classList.remove("is-loading"); b.removeAttribute("aria-busy");
ctl.forEach((x) => x.disabled = false); $$(".field.dis", F).forEach((x) => x.classList.remove("dis"));
$("[data-drop]").removeAttribute("aria-disabled"); $("[data-drop]").style.pointerEvents = "";
toast("项目已创建,正在解析招标文件,可以离开本页");
}, 2400);
});
addEventListener("DOMContentLoaded", () => { // 等 demo.js 初始化完日期输入
if (new URLSearchParams(location.search).get("state") === "errors") {
$("#n-due").value = "2026-11-20 25:00";
Object.keys(RULES).forEach(check); summary(false);
}
});
/* ---------- 03 资质录入:到期按项目的投标截止日判断(智慧大厅 11月20日),只提醒,不拦截 ---------- */
const DUE = new Date(2026, 10, 20);
const expF = $("#f-qexp"), expHint = $("[data-exp-hint]", expF), expDT = $("[data-dt]", expF);
expF.addEventListener("dt-change", (e) => {
const d = e.detail, late = d.ok && !d.empty && d.dt < DUE;
if (late) delete expHint.dataset.off; else expHint.dataset.off = "";
expHint.hidden = !late || expF.classList.contains("err");
});
$("[data-long]").addEventListener("click", () => setTimeout(() => {
const long = $("[data-long]").getAttribute("aria-checked") === "true";
$("#q-exp").disabled = long; $(".icon-btn", expDT).disabled = long;
if (long) { expHint.dataset.off = ""; expHint.hidden = true; setErr(expF, ""); } else expDT.dt.commit({ required: true });
}));
$("[data-qadd]").onclick = () => dialog({
title: "添加资质", wide: true, ok: "保存资质",
footLeft: '<span class="cap muted">不扣额度</span>',
body: `<div class="form">
<div class="field span2" id="qd-name"><label for="qdn">名称 <small>(必填)</small></label><input class="input" id="qdn" placeholder="和证书上的写法一致" autocomplete="off"></div>
<div class="field"><label id="qdt-l">类型 <small>(必填)</small></label><div class="pop" data-select><button type="button" class="select" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="qdt-l"><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" placeholder="例如:CS3、二级"></div>
<div class="field" id="qd-no"><label for="qdo">证书编号 <small>(必填)</small></label><input class="input" id="qdo" autocomplete="off"></div>
<div class="field" id="qd-exp"><label for="qde">有效期至 <small>(必填)</small></label><div class="dt" data-dt data-marks="2026-11-20" data-note="圆点是进行中项目的投标截止日" data-req="请填写有效期"><input class="input" id="qde" placeholder="例如:2026-11-20" autocomplete="off"><button type="button" class="icon-btn sm" aria-label="打开日期选择器" aria-haspopup="dialog" aria-expanded="false">${icon("calendar")}</button><div class="cal" role="dialog" aria-label="选择有效期" hidden></div></div></div>
<div class="field span2"><label>证书扫描件 <small>(可以稍后补录)</small></label><div class="drop" tabindex="0" role="button" style="padding:var(--sp-4)">${icon("upload")}<span>拖入或点击上传 PDF / 图片</span></div></div></div>`,
onOpen: (s) => {
const R = {
"qd-name": () => $("#qdn", s).value.trim() ? "" : "请填写资质名称,和证书上的写法一致",
"qd-no": () => $("#qdo", s).value.trim() ? "" : "请填写证书编号,查标时用它核对附件"
};
const exp = $("#qd-exp [data-dt]", s);
s._check = () => [...Object.entries(R).map(([id, fn]) => setErr($("#" + id, s), fn())), exp.dt.commit({ required: true })].every(Boolean);
Object.entries(R).forEach(([id, fn]) => {
const f = $("#" + id, s), inp = $("input", f); let d = false;
inp.addEventListener("input", () => { d = true; if (f.classList.contains("err") && !fn()) setErr(f, ""); });
inp.addEventListener("blur", () => { if (d || f.classList.contains("err")) setErr(f, fn()); });
});
},
onOk: (s) => {
if (!s._check()) { $(".field.err input", s).focus(); return false; }
toast("已保存资质");
}
});
/* ---------- 04 忽略原因 / 致命确认说明 ---------- */
const NOW = "11月3日 14:30";
$$("[data-u4]").forEach((box) => {
const kind = box.dataset.u4, ta = $("textarea", box), fld = $(".field", box), msg = $(".msg", fld), def = msg.dataset.def;
const bad = () => ta.value.trim().length < 5;
const show = (on) => { fld.classList.toggle("err", on); ta.toggleAttribute("aria-invalid", on); msg.innerHTML = on ? `<span class="ico w" aria-hidden="true">!</span>${kind === "ign" ? "至少写 5 个字,说明为什么不处理" : "请写一句说明,至少 5 个字"}` : def; };
ta.addEventListener("input", () => { $("[data-n]", box).textContent = ta.value.length; if (fld.classList.contains("err") && !bad()) show(false); });
ta.addEventListener("blur", () => show(bad()));
$$("[data-chip]", box).forEach((c) => c.onclick = () => { ta.value = c.textContent; $("[data-n]", box).textContent = ta.value.length; show(false); ta.focus(); });
$("[data-x]", box).onclick = () => toast("(演示)取消:收起输入框", { ok: false });
const okBtn = $("[data-ok]", box);
okBtn.onclick = () => {
if (bad()) { show(true); ta.focus(); return; }
const done = () => {
const html = kind === "ign" // 署名行(共用 .sign):「已忽略」不是确认,用辅助灰和「–」
? `<div class="sign ign"><span class="ok" aria-hidden="true">–</span><b>王敏</b><span class="fig">${NOW}</span><span>已忽略</span><button type="button" class="undo" data-undo>撤销</button></div><p class="sign-note">原因:${esc(ta.value.trim())}</p>`
: `<div class="sign"><span class="ok" aria-hidden="true">✓</span><b>王敏</b><span class="fig">${NOW}</span><span>确认无误</span><button type="button" class="undo" data-undo>撤销</button></div><p class="sign-note">说明:${esc(ta.value.trim())} · 已作为误报反馈提交</p>`;
box.hidden = true;
box.insertAdjacentHTML("afterend", `<div class="u4-done">${html}</div>`);
const d = box.nextElementSibling;
$("[data-undo]", d).onclick = () => { d.remove(); box.hidden = false; ta.focus(); toast("已撤销", { ok: false }); };
$("[data-undo]", d).focus();
toast(kind === "ign" ? "已忽略 · 原因已记录" : "已确认无误 · 说明已作为反馈提交");
};
if (kind === "ok") { okBtn.classList.add("is-loading"); okBtn.setAttribute("aria-busy", "true"); setTimeout(() => { okBtn.classList.remove("is-loading"); okBtn.removeAttribute("aria-busy"); done(); }, 900); }
else done();
};
});
document.addEventListener("click", (e) => { if (e.target.closest("[data-demo]")) { e.preventDefault(); toast("(演示)在原文查看器里打开这一页", { ok: false }); } });
</script>
</body>
</html>