docs(design): 按 v0.2 重刷全部 UI demo,确认 Q1–Q13 与 U 键规则

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 16:07:15 +08:00
parent 067cad2120
commit 7f9592ad16
16 changed files with 2625 additions and 1252 deletions

View File

@@ -1,4 +1,9 @@
<!doctype html>
<!--
组合页 · 工作台(v0.2)
内容和交互以定稿样张 explore/workbench-d5.html 为准;框架来自 Demo.shell(),共用组件来自 components.css 和 demo.css,
本页 <style> 里只放工作台特有的布局(主列 + 右栏、待处理问题行);项目行(栅格、「下一步 / 更多」列)是 demo.css 的 B20。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -10,123 +15,212 @@
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="demo.css">
<style>
/* 本页布局:主列 + 右侧 296px 信息栏 */
.wb{display:grid;grid-template-columns:minmax(0,1fr) 296px;gap:var(--sp-6);align-items:start;margin-top:var(--sp-6)}
.wb .rail > * + *{margin-top:var(--sp-4)}
.wb .rail .card{padding:var(--sp-4) var(--sp-5)}
.wb .rail .card-foot{margin:var(--sp-3) calc(-1 * var(--sp-5)) calc(-1 * var(--sp-4));padding:var(--sp-2) var(--sp-5)}
/* 本页布局:主列 + 右侧信息栏(--aside-w),全页一个间距 --gap-card */
.content > .alert,.content > .stats{margin-bottom:var(--gap-card)}
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--aside-w);gap:var(--gap-card);align-items:start}
/* 项目卡片头:筛选 + 已选统计格的清除按钮 */
.card-h .seg{margin-left:var(--sp-2)}
.card-h .clear{margin-left:auto}
/* 待处理的致命问题:不加左侧红竖线,靠实心「致命」标签扫读(d5) */
.todo{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;column-gap:var(--sp-4);align-items:center;padding:var(--sp-3) var(--card-x);border-top:var(--line) solid var(--border-subtle)}
.todo b{display:block;font-weight:var(--fw-bold)}
.todo .cite-link{margin-right:2px}
/* 右栏 */
.wb aside .card-h{min-height:52px;padding-bottom:var(--sp-1)}
.task-h{display:flex;align-items:baseline;gap:var(--sp-2)}
.task-h .cap{margin-left:auto}
.wb .progress{margin-top:10px}
.queue{display:flex;gap:var(--sp-2);align-items:flex-start;margin-top:14px;padding-top:var(--sp-3);border-top:var(--line) solid var(--border-subtle)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.quota .q2 small{font-size:var(--fs-caption)}
.log{list-style:none;margin:0;padding:0}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-2);padding:6px 0;border-top:1px solid var(--border-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.log li:first-child{border-top:0}
.log li > span:last-child{color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums;white-space:nowrap}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-2);padding:var(--sp-2) 0;border-top:var(--line) solid var(--border-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.log li:first-child{border-top:0;padding-top:0}
.log li > span:last-child{color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-size:var(--fs-figure-xs);white-space:nowrap}
.log .who{font-weight:var(--fw-bold)}
.log .ok{color:var(--confirmed);white-space:nowrap}
.wb table.data td{vertical-align:middle}
.todo-row{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:var(--sp-3);align-items:baseline;padding:var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5);border-top:1px solid var(--border-subtle)}
.todo-row:first-child{border-top:0}
.todo-row::before{content:"";position:absolute;left:0;top:var(--sp-3);bottom:var(--sp-3);width:3px;background:var(--sev-fatal)}
.todo-row b{display:block;font-weight:var(--fw-bold)}
.todo-row .cite-src{white-space:nowrap}
.wb .proj-sub .fig{font-weight:var(--fw-medium)}
</style>
</head>
<body>
<main>
<div class="content">
<div class="page-title">
<div><h1>工作台</h1><p>11月3日 周二 · 按投标截止时间排序,截止临近的排在前面。</p></div>
<div class="actions"><span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次</span><button class="btn primary" data-new><i data-ic="plus"></i>新建项目</button></div>
<div><h1>工作台</h1><p>下午好,王敏。今天是 11月3日,周二。</p></div>
<div class="actions">
<span class="cost">扣 1 次解析 · 剩 <span class="fig">37</span> 次</span>
<a class="btn primary" href="forms.html#s-new"><i data-ic="plus"></i>新建项目</a>
</div>
</div>
<div class="alert warn"><span class="ico w">!</span><div><p><b>某县数据中心机房改造项目今天 17:00 截止。</b>清单还是草稿,还没有做过检查。</p></div><span class="act"><a class="btn sm" href="page-reading.html">去确认清单</a></span></div>
<div class="stats" style="margin-top:var(--sp-5)">
<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-today">最近:今天 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">4</span><p>在 2 个项目里</p></div>
<div class="alert warn" role="note">
<i data-ic="clock" class="lg"></i>
<p><strong>某县数据中心机房改造项目</strong> <span class="when">今天 17:00 截止</span>,清单还没确认。</p>
<a class="btn sm" href="page-reading.html"><i data-ic="list"></i>确认清单</a>
</div>
<section class="stats" aria-label="项目统计,点击可筛选项目列表" data-stats>
<button class="stat" data-f="active" aria-pressed="true"><small>进行中的项目</small><span class="big">4</span><i data-ic="filter"></i></button>
<button class="stat" data-f="due7" aria-pressed="false"><small>7 天内截止</small><span class="big due">2</span><i data-ic="filter"></i></button>
<button class="stat" data-f="fatal" aria-pressed="false"><small>待处理的致命问题</small><span class="big fatal">3</span><i data-ic="filter"></i></button>
<button class="stat" data-f="pend" aria-pressed="false"><small>待确认的清单条目</small><span class="big">4</span><i data-ic="filter"></i></button>
</section>
<div class="wb">
<div>
<div class="toolbar">
<div class="seg" role="group" aria-label="按状态筛选" data-seg><button aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button><button aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button><button aria-pressed="false" data-v="已递交">已递交</button><button aria-pressed="false" data-v="已归档">已归档</button></div>
<div class="right"><label class="search" style="width:240px"><i data-ic="search"></i><input class="input" placeholder="搜索项目、采购人、编号" aria-label="搜索项目" data-q></label></div>
</div>
<table class="data hover">
<thead><tr><th>项目</th><th>投标截止</th><th>状态</th><th>就绪</th><th class="r">致命</th></tr></thead>
<tbody data-rows></tbody>
</table>
<p class="note">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
<div class="section-h"><h2>待处理的致命问题</h2><span class="sev fatal">致命<span class="fig">3</span></span><span class="more"><a class="plain cap" href="#">去查标页处理</a></span></div>
<div class="results">
<div class="todo-row"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="cap muted">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite-src" href="#">报价册 · 第 <span class="fig">1</span> 页</a></div>
<div class="todo-row"><span class="sev fatal">致命</span><div><b>★ 条款疑似不满足:应用服务器内存</b><span class="cap muted">智慧大厅建设项目 · 响应 256G,要求 ≥ 512G · 上次标记为已修改,问题仍在</span></div><a class="cite-src" href="#">技术册 · 第 <span class="fig">12</span> 页</a></div>
<div class="todo-row"><span class="sev fatal">致命</span><div><b>授权委托书的单位签章位置未发现盖章</b><span class="cap muted">智慧大厅建设项目 · 只看有无,不鉴定真伪</span></div><a class="cite-src" href="#">商务册 · 第 <span class="fig">5</span> 页</a></div>
</div>
</div>
<div class="rail">
<div class="card">
<div class="card-h"><h3>正在进行的任务</h3></div>
<div class="d-row" style="gap:var(--sp-2);margin-bottom:var(--sp-1)"><b>解析补遗 2</b><span class="cap muted" style="margin-left:auto">提取清单</span></div>
<div class="cap muted" style="margin-bottom:var(--sp-2)">某市医保信息系统等保测评服务</div>
<div class="progress"><div class="bar"><i style="width:62%"></i></div><span class="fig">62%</span></div>
<div style="display:flex;gap:var(--sp-2);align-items:flex-start;margin-top:var(--sp-3)"><i data-ic="clock" class="muted" style="margin-top:2px"></i><span class="cap muted">检查排队中 · 智慧社区平台运维 · 前面还有 1 个任务</span></div>
<p class="note">可以离开本页,完成后会发站内通知。</p>
</div>
<div class="card quota">
<div class="card-h"><h3>本期额度</h3><span class="more cap muted">至 2027-10-31</span></div>
<div class="q2">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="bar"><i style="width:26%"></i></div></div>
<div><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20</span><div class="bar"><i style="width:40%"></i></div></div>
<div class="stack">
<section class="card flush" aria-labelledby="h-proj">
<div class="card-h">
<h2 id="h-proj">项目</h2>
<div class="seg" role="group" aria-label="按状态筛选" data-seg>
<button aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button>
<button aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button>
<button aria-pressed="false" data-v="已递交">已递交</button>
<button aria-pressed="false" data-v="已归档">已归档</button>
</div>
<span class="clear" data-chip></span>
</div>
<div class="card-foot"><a class="plain cap" href="#">扣点规则</a><span class="r"><a class="plain cap" href="#">查看流水</a></span></div>
</div>
<div class="prow-head" aria-hidden="true"><span>项目</span><span>投标截止</span><span>状态</span><span class="c">致命</span><span>下一步</span><span></span></div>
<ul class="prows" data-rows aria-label="项目列表"></ul>
</section>
<div class="card">
<div class="card-h"><h3>最近处理</h3><span class="more"><a class="plain cap" href="#">全部记录</a></span></div>
<ul class="log">
<li><span><span class="who">王敏</span> <span class="ok">✓ 已修改</span>「授权委托书签章」</span><span>14:26</span></li>
<li><span><span class="who">王敏</span> 确认了清单 v2 · 智慧大厅</span><span>11-02</span></li>
<li><span><span class="who">系统</span> 完成第 2 次检查 · 致命 3</span><span>11-02</span></li>
<li><span><span class="who">系统</span> 并入补遗 1 · 更新 2 条</span><span>11-02</span></li>
<li><span><span class="who">李工</span> 忽略「其他项目名称」:业绩介绍</span><span>11-01</span></li>
<section class="card flush" aria-labelledby="h-todo">
<div class="card-h">
<h2 id="h-todo">待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" style="margin-left:auto" href="#" data-demo>查看全部</a>
</div>
<ul style="list-style:none;margin:0;padding:0">
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite-link" href="#" data-demo><i data-ic="file"></i>报价册 · 第 <span class="fig">1</span> 页</a><a class="btn sm" href="page-reading.html">去处理</a></li>
<li class="todo"><span class="sev fatal">致命</span><div><b>★ 条款疑似不满足:应用服务器内存</b><span class="psub">智慧大厅建设项目 · 响应 256G,要求 ≥ 512G · 上次已修改,问题仍在</span></div><a class="cite-link" href="#" data-demo><i data-ic="file"></i>技术册 · 第 <span class="fig">12</span> 页</a><a class="btn sm" href="page-reading.html">去处理</a></li>
<li class="todo"><span class="sev fatal">致命</span><div><b>授权委托书的单位签章位置未发现盖章</b><span class="psub">智慧大厅建设项目 · 只看有无,不鉴定真伪</span></div><a class="cite-link" href="#" data-demo><i data-ic="file"></i>商务册 · 第 <span class="fig">5</span> 页</a><a class="btn sm" href="page-reading.html">去处理</a></li>
</ul>
</div>
</section>
</div>
<aside class="stack" aria-label="任务、额度与记录">
<section class="card flush" aria-labelledby="h-task">
<div class="card-h"><h3 id="h-task">正在进行的任务</h3></div>
<div class="card-b">
<div class="task-h"><b>解析补遗 2</b><span class="cap muted">提取清单</span></div>
<div class="cap muted">某市医保信息系统等保测评服务</div>
<div class="progress"><div class="bar" role="progressbar" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-label="解析补遗 2 的进度"><i style="width:62%"></i></div><span class="fig">62%</span></div>
<div class="queue"><i data-ic="clock"></i><span class="cap muted">检查排队中:智慧社区平台运维,前面 1 个</span></div>
</div>
</section>
<section class="card flush quota" aria-labelledby="h-quota">
<div class="card-h"><h3 id="h-quota">本期额度</h3><span class="cap muted" style="margin-left:auto">至 2027-10-31</span></div>
<div class="card-b">
<div class="q2">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="bar" aria-hidden="true"><i style="width:26%"></i></div></div>
<div><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20</span><div class="bar" aria-hidden="true"><i style="width:40%"></i></div></div>
</div>
</div>
<div class="card-f cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card flush" aria-labelledby="h-log">
<div class="card-h"><h3 id="h-log">最近处理</h3><a class="lnk cap" style="margin-left:auto" href="#" data-demo>全部记录</a></div>
<div class="card-b">
<ul class="log">
<li><span><span class="who">王敏</span> <span class="ok">✓ 已修改</span>「授权委托书签章」</span><span>14:26</span></li>
<li><span><span class="who">王敏</span> 确认了清单 v2 · 智慧大厅</span><span>11-02</span></li>
<li><span><span class="who">系统</span> 完成第 2 次检查 · 致命 3</span><span>11-02</span></li>
<li><span><span class="who">系统</span> 并入补遗 1 · 更新 2 条</span><span>11-02</span></li>
<li><span><span class="who">李工</span> 忽略「其他项目名称」:业绩介绍</span><span>11-01</span></li>
</ul>
</div>
</section>
</aside>
</div>
</div>
</main>
<script src="demo.js"></script>
<script>
const { $, $$, icon, toast } = Demo;
Demo.shell({ nav: "workbench", crumbs: [["工作台", ""]] });
const { $, $$, icon, esc, toast } = Demo;
const DEMO = "(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页";
/* 数据沿用 explore/workbench-d5.html;pend 为待确认清单条目数。next 为「下一步」,menu 为「更多」里的操作;cost 表示会扣点 */
const CHECK_COST = `扣 1 次检查 · 剩 <span class="fig">12</span> 次`;
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: "今天 17:00 截止", cls: "due-today", st: "编制中", ready: "", fatal: 0, href: "page-reading.html" },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", h: 53, due: "剩 2 天 5 小时", cls: "due-soon", st: "已检查", ready: "就绪", fatal: 0 },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, due: "11月20日 09:30 · 剩 17 天", cls: "", st: "已检查", ready: "未就绪", fatal: 3, href: "page-reading.html" },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", h: 600, due: "11月28日 10:00 · 剩 25 天", cls: "", st: "跟进中", ready: "", fatal: 0 },
{ name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, due: "已截止", cls: "due-past", st: "已递交", ready: "", fatal: 0 },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: "已截止", cls: "due-past", st: "已归档", ready: "", fatal: 0 }
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "due-today", st: "编制中", ready: "", fatal: 0, pend: 3, href: "page-reading.html",
next: "确认清单", menu: [["上传投标文件", ""], ["发起检查", "cost"], ["导出清单", ""]] },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", h: 53, due: ["剩 2 天 5 小时", "11月5日 16:00"], cls: "due-soon", st: "已检查", ready: "就绪", fatal: 0, pend: 0,
next: "导出检查报告", menu: [["查看检查结果", ""], ["标记为已递交", ""]] },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, due: ["剩 17 天", "11月20日 09:30"], cls: "", st: "已检查", ready: "未就绪", fatal: 3, pend: 0, href: "page-reading.html",
next: "处理致命问题", menu: [["重新检查", "cost"], ["导出检查报告", "致命问题未处理也可以导出"], ["标记为已递交", ""]] },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", h: 600, due: ["剩 25 天", "11月28日 10:00"], cls: "", st: "跟进中", ready: "", fatal: 0, pend: 1,
next: "确认清单", menu: [["查看解析进度", "补遗 2 解析中 62%"], ["上传补遗", ""]] },
{ name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, due: ["已截止", "已递交"], cls: "due-past", st: "已递交", ready: "", fatal: 0, pend: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "due-past", st: "已归档", ready: "", fatal: 0, pend: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
let seg = "active", q = "";
function render() {
const rows = P.filter((p) => (seg === "all" || (seg === "active" ? !["已递交", "已归档"].includes(p.st) : p.st === seg)) && (!q || (p.name + p.buyer).includes(q)));
$("[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} · ${p.lot}</div></td>
<td class="nw ${p.cls}">${p.due}</td><td><span class="tag status">${p.st}</span></td>
<td class="nw">${p.ready === "就绪" ? '<span class="tag status" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>' : p.ready || '<span class="muted">—</span>'}</td>
<td class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="muted">—</span>'}</td></tr>`).join("")
|| `<tr><td colspan="5" class="empty-cell"><div class="empty sm"><b>没有符合条件的项目</b><p>换个关键词,或切到「全部」看看。</p></div></td></tr>`;
const STAT = {
active: { label: "进行中", fn: (p) => !["已递交", "已归档"].includes(p.st) },
due7: { label: "7 天内截止", fn: (p) => p.h >= 0 && p.h <= 168 },
fatal: { label: "有致命问题", fn: (p) => p.fatal > 0 },
pend: { label: "有待确认条目", fn: (p) => p.pend > 0 }
};
let seg = "active", stat = "", q = "";
Demo.shell({ nav: "workbench", crumbs: [["工作台", ""]], search: { placeholder: "搜索项目、采购人", onInput: (v) => { q = v; render(); } } });
function status(p) {
let r = "";
if (p.ready === "就绪") r = '<span class="hint-u tip-below" tabindex="0" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>';
else if (p.ready) r = `<span>${p.ready}</span>`;
return `<span class="st"><span class="tag">${p.st}</span>${r}</span>`;
}
$("[data-seg]").addEventListener("change", (e) => { seg = e.detail; render(); });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-rows]").addEventListener("click", (e) => { const r = e.target.closest("tr[data-href]"); if (!r) return; if (r.dataset.href) location.href = r.dataset.href; else toast("(演示)只做了「读标」组合页"); });
$("[data-new]").onclick = () => location.href = "forms.html#s-new";
function render() {
const rows = P.map((p, i) => [p, i]).filter(([p]) => {
if (stat) { if (!STAT[stat].fn(p)) return false; }
else if (seg !== "all" && !(seg === "active" ? STAT.active.fn(p) : p.st === seg)) return false;
return !q || (p.name + p.buyer).includes(q);
});
$("[data-rows]").innerHTML = rows.map(([p, i]) => `<li class="prow ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span class="pt"><a class="pname" href="${p.href || "#"}" title="${esc(p.name)}">${esc(p.name)}</a><span class="psub">${esc(p.buyer)} · ${esc(p.lot)}</span></span>
<span class="dl"><b class="${p.cls}">${p.due[0]}</b><small>${p.due[1]}</small></span>
${status(p)}
<span class="c">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span><button class="nx" data-next><span>${p.next}</span>${icon("chev-right")}</button></span>
<span class="more-cell"><button class="icon-btn sm" aria-label="更多操作:${esc(p.name)}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span>
</li>`).join("")
|| `<li class="empty sm"><b>没有符合条件的项目</b><p>换个关键词,或切到「全部」看看。</p></li>`;
$$("[data-seg] button").forEach((b) => b.setAttribute("aria-pressed", !stat && b.dataset.v === seg));
$$("[data-stats] .stat").forEach((b) => b.setAttribute("aria-pressed", b.dataset.f === stat || (!stat && b.dataset.f === "active" && seg === "active")));
$("[data-chip]").innerHTML = stat ? `<button class="btn text sm" data-clear>${STAT[stat].label}${icon("x")}<span class="vh">,清除筛选</span></button>` : "";
}
$("[data-rows]").addEventListener("click", (e) => {
const row = e.target.closest(".prow"); if (!row) return;
const p = P[+row.dataset.i];
const m = e.target.closest("[data-menu]");
if (m) {
e.preventDefault(); e.stopPropagation();
const items = [{ label: "打开项目" }, "-", ...p.menu.map(([t, s]) => ({ label: t, sub: s === "cost" ? CHECK_COST : s }))];
Demo.floatMenu(m, items, (act) => { if (act === "打开项目" && p.href) location.href = p.href; else toast(`(演示)${act}:${p.name}`, { ok: false }); });
return;
}
if (e.target.closest(".hint-u")) return;
if (e.target.closest("[data-next]")) { if (p.href) location.href = p.href; else toast(`(演示)${p.next}:${p.name}`, { ok: false }); return; }
if (!p.href) { e.preventDefault(); toast(DEMO, { ok: false }); }
});
$("[data-seg]").addEventListener("change", (e) => { seg = e.detail; stat = ""; render(); });
$("[data-stats]").addEventListener("click", (e) => {
const b = e.target.closest(".stat"); if (!b) return;
const f = b.dataset.f;
if (f === "active") { seg = "active"; stat = ""; }
else if (stat === f) { stat = ""; seg = "active"; }
else stat = f;
render();
});
$("[data-chip]").addEventListener("click", (e) => { if (e.target.closest("[data-clear]")) { stat = ""; seg = "active"; render(); $("[data-seg] button[aria-pressed=true]").focus(); } });
document.addEventListener("click", (e) => { if (e.target.closest("[data-demo]")) { e.preventDefault(); toast(DEMO, { ok: false }); } });
render();
</script>
</body>