docs(design): UI 风格定稿(U5),设计规范与 tokens 升到 v0.2

- ui-demo/explore/:工作台风格的六轮方案(A–F、D2–D5),D5 为定稿样张
- tokens.css v0.2:墨色派生的中性浅灰作页面底色;圆角三档(6 / 8 / 12);控件高 36;侧栏渐变与套餐标签变量;旧变量保留兼容
- design-system.md v0.2:页面框架(96px 墨色渐变侧栏、透明顶栏、内容区圆角)、只用功能图标、项目行「下一步 / 更多」列、少写说明
- decisions.md 记录 U5;AGENTS.md 与 design/README 同步

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 14:35:08 +08:00
parent 604e889ca4
commit 067cad2120
16 changed files with 5910 additions and 39 deletions

View File

@@ -0,0 +1,638 @@
<!doctype html>
<!--
工作台 · 风格探索方案 F「列表 + 详情」
主从布局:左边项目列表,点一个项目,右边详情卡立即换成它,不离开页面。和读标 / 查标的「清单 + 原文并排」是同一种结构。
内容、数据沿用 ../page-workbench.html 和 ../demo.js;颜色只用 tokens.css 的色值和语义。
本方案相对 design-system.md 改了这些规则:
1. 底色与分隔(§3.1 校样纸底、--c-rule 分隔线):页面、侧栏、顶栏一律白底;浅底和分隔线改用从墨色派生的中性冷灰
--n-50 / --n-100 / --n-line(墨 3% / 6% / 11% 混白)。不再使用 --c-paper、--c-sunken、--c-hover、--c-rule。
2. 容器(§5.1 层次靠分隔线和底色):整页只有两种容器——页面本身和一种白卡片(圆角 12,1px --n-line,无阴影),
白卡片只用于右侧的项目详情。左侧列表、额度、任务都直接放在页面上,不套面板。
3. 圆角(§5.1 控件和卡片 2px、浮层 4px):按钮、输入框、列表项、提醒条 8px;标签 4px;详情卡 12px。
4. 控件(§7.1):按钮高 32 → 36(行内小按钮 28)。主按钮墨底白字;次按钮白底 + 1px --border-control(≥3:1)。
筛选从分段控件改为文字标签页,选中用 2px 墨色下划线。
5. 统计(原型的四张统计卡):去掉统计卡,改成页头下一行内联统计,小号文字 + Barlow 数字,每项都是可点击的筛选。
6. 项目列表(§7.9 项目行):表格改为列表项。项目名 14/22 600(原 16/24);状态改为副行里的纯文字,
详细状态放到详情卡的状态步骤条里。选中项 = --n-50 底 + 左侧 3px 墨色竖条;支持 ↑ / ↓ 切换、Enter 打开。
7. 最近处理、待处理的致命问题、正在进行的任务:从全局区块改为跟着选中项目显示在详情卡里;
额度移到顶栏(小进度条 + 数字),正在进行的任务在列表下方留一个紧凑区块,点了就选中对应项目。
8. 提醒条(§7.12):去掉外框和左竖条,只用赭浅底,圆角 8。
9. 截止倒计时:详情卡里用 Barlow 36 大号数字;颜色规则不变(≤72 小时赭,≤24 小时赭加粗,已截止辅助灰)。
10. 动效(§10):只有详情切换时一次 120ms 交叉淡入(reduced-motion 下关闭),其余只有悬停 / 按下反馈。
问题级别的形状规则、颜色语义、字体分工、最小字号 12、字重 400 / 600 都不变。
-->
<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="data:,">
<link rel="stylesheet" href="../../system/tokens.css">
<link rel="stylesheet" href="../../system/fonts-local.css">
<style>
/* ---------- 本方案派生的变量:全部从墨色混白得到,不引入新色相 ---------- */
:root {
--n-50: color-mix(in srgb, var(--c-ink) 3%, white); /* 悬停、选中底 */
--n-100: color-mix(in srgb, var(--c-ink) 6%, white); /* 侧栏当前项、按下、进度条轨道 */
--n-line: color-mix(in srgb, var(--c-ink) 11%, white); /* 发丝线、详情卡边框 */
--ink-press: color-mix(in srgb, var(--c-ink) 82%, black);
--r-ctl: 8px;
--r-tag: 4px;
--r-card: 12px;
--h-btn: 36px;
--h-btn-sm: 28px;
--top-h: 56px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; min-width: 1280px; background: var(--c-white); color: var(--text-primary);
font: var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);
-webkit-font-smoothing: antialiased;
}
a { color: inherit; }
button, input { font: inherit; color: inherit; }
button { cursor: pointer; }
p { margin: 0; }
.fig { font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-weight: var(--fw-bold); }
.cap { font-size: var(--fs-caption); line-height: var(--lh-caption); }
.sub { color: var(--text-secondary); }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.ic { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.ic.lg { width: 20px; height: 20px; }
:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.lnk { color: var(--text-primary); text-decoration: underline; text-decoration-color: var(--border-control); text-underline-offset: 3px; cursor: pointer; background: none; border: 0; padding: 0; }
.lnk:hover { text-decoration-color: currentColor; }
/* ---------- 按钮:主按钮墨底白字;次按钮白底 + 控件边框 ---------- */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
height: var(--h-btn); padding: 0 16px; border-radius: var(--r-ctl);
border: 1px solid var(--border-control); background: var(--c-white); color: var(--text-primary);
font-weight: var(--fw-bold); white-space: nowrap; text-decoration: none;
transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.btn:hover { background: var(--n-50); border-color: var(--c-ink); }
.btn:active { background: var(--n-100); }
.btn.primary { background: var(--c-ink); border-color: var(--c-ink); color: var(--text-on-dark); }
.btn.primary:hover { background: var(--ink-press); border-color: var(--ink-press); }
.btn.sm { height: var(--h-btn-sm); padding: 0 12px; font-weight: var(--fw-regular); }
.icon-btn { display: inline-grid; place-items: center; width: 36px; height: 36px; border: 0; border-radius: var(--r-ctl); background: transparent; color: var(--text-secondary); position: relative; }
.icon-btn:hover { background: var(--n-50); color: var(--text-primary); }
.cost { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); white-space: nowrap; }
.cost .fig { color: var(--text-primary); }
/* ---------- 标签:一种几何,按级别规则填充 ---------- */
.tag { display: inline-flex; align-items: center; gap: 3px; height: 20px; padding: 0 6px; border-radius: var(--r-tag); font-size: var(--fs-caption); line-height: 20px; font-weight: var(--fw-bold); white-space: nowrap; flex: none; }
.tag.fatal { background: var(--sev-fatal); color: var(--c-white); }
.tag.ready { box-shadow: inset 0 0 0 1px var(--c-ink); color: var(--c-ink); }
/* ---------- 框架:白色侧栏 + 发丝线 ---------- */
.side { position: fixed; inset: 0 auto 0 0; width: var(--sidebar-w); background: var(--c-white); border-right: 1px solid var(--n-line); display: flex; flex-direction: column; padding: 18px 12px 20px; }
.side .brand { display: block; padding: 0 8px 26px; }
.side .brand img { height: 30px; display: block; }
.nav { display: flex; flex-direction: column; gap: 2px; }
.nav a { display: flex; align-items: center; gap: 10px; height: 36px; padding: 0 10px; border-radius: var(--r-ctl); text-decoration: none; color: var(--text-secondary); }
.nav a:hover { background: var(--n-50); color: var(--text-primary); }
.nav a[aria-current="page"] { background: var(--n-100); color: var(--text-primary); font-weight: var(--fw-bold); }
.nav .grp { margin: 20px 10px 6px; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.side-foot { margin-top: auto; padding: 0 10px; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.side-foot b { display: block; color: var(--text-primary); font-size: var(--fs-body); line-height: var(--lh-body); }
.main { margin-left: var(--sidebar-w); min-width: 0; }
.top { position: sticky; top: 0; z-index: 5; height: var(--top-h); display: flex; align-items: center; gap: 16px; padding: 0 24px 0 32px; background: var(--c-white); border-bottom: 1px solid var(--n-line); }
.top .today { color: var(--text-secondary); }
.top .right { margin-left: auto; display: flex; align-items: center; gap: 4px; }
.quota { display: flex; align-items: center; gap: 18px; height: 36px; padding: 0 12px; margin-right: 8px; border-radius: var(--r-ctl); text-decoration: none; }
.quota:hover { background: var(--n-50); }
.quota .q { display: flex; align-items: center; gap: 8px; }
.quota .q > span:first-child { color: var(--text-secondary); }
.meter { width: 56px; height: 4px; border-radius: 2px; background: var(--n-100); overflow: hidden; }
.meter i { display: block; height: 100%; background: var(--c-ink); border-radius: 2px; }
.meter.warn i { background: var(--warn); }
.quota .fig { font-size: 15px; }
.quota .of { color: var(--text-secondary); font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-size: var(--fs-caption); }
.icon-btn .dot { position: absolute; top: 8px; right: 9px; width: 7px; height: 7px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 2px var(--c-white); }
.content { padding: 28px 32px 48px; }
/* ---------- 页头 ---------- */
.head { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; }
h1 { margin: 0; font: var(--fw-bold) 28px/36px var(--font-serif); letter-spacing: .02em; }
.stats { display: flex; flex-wrap: wrap; gap: 4px 6px; margin: 6px 0 0 -8px; }
.stat { display: inline-flex; align-items: baseline; gap: 4px; height: 32px; padding: 0 8px; border: 0; border-radius: 6px; background: transparent; color: var(--text-secondary); }
.stat .fig { font-size: 20px; line-height: 32px; color: var(--text-primary); }
.stat .lb { text-decoration: underline; text-decoration-color: var(--n-line); text-underline-offset: 4px; }
.stat:hover { background: var(--n-50); color: var(--text-primary); }
.stat:hover .lb { text-decoration-color: var(--border-control); }
.stat[aria-pressed="true"] { background: var(--n-100); color: var(--text-primary); }
.stat[aria-pressed="true"] .lb { text-decoration-color: var(--c-ink); }
.head .acts { display: flex; align-items: center; gap: 12px; padding-bottom: 2px; }
.remind { display: flex; align-items: center; gap: 10px; margin-top: 18px; padding: 8px 8px 8px 14px; border-radius: var(--r-ctl); background: var(--warn-soft); color: var(--warn-text); }
.remind .ic { color: var(--warn); }
.remind p { flex: 1; min-width: 0; }
.remind .btn { border-color: color-mix(in srgb, var(--warn) 55%, white); color: var(--warn-text); }
.remind .btn:hover { border-color: var(--warn); background: var(--c-white); }
/* ---------- 主从两栏 ---------- */
.split { display: grid; grid-template-columns: minmax(0, 44fr) minmax(0, 56fr); gap: 32px; align-items: start; margin-top: 24px; }
/* 左栏:标签页 + 搜索 */
.lbar { display: flex; align-items: flex-end; gap: 16px; border-bottom: 1px solid var(--n-line); }
.tabs { display: flex; gap: 20px; flex: none; }
.tabs button { position: relative; height: 40px; padding: 0 1px; border: 0; background: none; color: var(--text-secondary); white-space: nowrap; }
.tabs button .fig { margin-left: 3px; font-weight: var(--fw-regular); }
.tabs button:hover { color: var(--text-primary); }
.tabs button[aria-selected="true"] { color: var(--text-primary); font-weight: var(--fw-bold); }
.tabs button[aria-selected="true"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; border-radius: 1px; background: var(--c-ink); }
.search { flex: 1; min-width: 132px; position: relative; margin-bottom: 6px; }
.search .ic { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); pointer-events: none; }
.search input { width: 100%; height: 32px; padding: 0 10px 0 32px; border: 1px solid var(--border-control); border-radius: var(--r-ctl); background: var(--c-white); }
.search input::placeholder { color: var(--text-secondary); }
.search input:hover { border-color: var(--c-ink); }
.search input:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; border-color: var(--c-ink); }
.qf { display: flex; align-items: center; gap: 12px; padding: 10px 0 0; color: var(--text-secondary); }
.qf[hidden] { display: none; }
/* 左栏:列表项 */
.plist { margin: 8px 0 0 -16px; padding: 0; list-style: none; outline: none; }
.pitem {
position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto 28px; gap: 2px 12px; align-items: center;
padding: 12px 6px 12px 16px; border-radius: var(--r-ctl); cursor: pointer;
transition: background-color var(--dur-fast) var(--ease);
}
.pitem + .pitem { margin-top: 2px; }
.pitem:hover, .pitem[aria-selected="true"] { background: var(--n-50); }
.pitem[aria-selected="true"]::before { content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px; border-radius: 2px; background: var(--c-ink); }
.pitem:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.pitem .nm { font-weight: var(--fw-bold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pitem .meta { grid-column: 1; display: flex; align-items: center; gap: 10px; min-width: 0; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.pitem .meta .by { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.pitem .meta span { flex: none; }
.pitem .meta .by { flex: 0 1 auto; }
.pitem .busy { display: inline-flex; align-items: center; gap: 4px; color: var(--text-primary); }
.pitem .due { grid-column: 2; grid-row: 1; text-align: right; white-space: nowrap; font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-weight: var(--fw-bold); font-size: 15px; }
.pitem .left { grid-column: 2; grid-row: 2; text-align: right; white-space: nowrap; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.pitem .go { grid-column: 3; grid-row: 1 / span 2; width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 6px; background: transparent; color: var(--text-secondary); opacity: 0; }
.pitem:hover .go, .pitem[aria-selected="true"] .go, .pitem .go:focus-visible { opacity: 1; }
.pitem .go:hover { background: var(--c-white); color: var(--text-primary); box-shadow: inset 0 0 0 1px var(--border-control); }
.t-soon { color: var(--warn-text); }
.t-today { color: var(--warn-text); }
.pitem .left.t-soon, .pitem .left.t-today { color: var(--warn-text); font-weight: var(--fw-bold); }
.t-past { color: var(--text-secondary); }
.empty { padding: 32px 16px; text-align: center; color: var(--text-secondary); }
.empty b { display: block; color: var(--text-primary); margin-bottom: 4px; }
.lhint { margin-top: 10px; color: var(--text-secondary); }
.kbd { display: inline-block; min-width: 20px; height: 20px; padding: 0 5px; border-radius: 4px; box-shadow: inset 0 0 0 1px var(--n-line); background: var(--c-white); text-align: center; font-size: var(--fs-caption); line-height: 20px; color: var(--text-primary); }
/* 左栏底部:正在进行的任务 */
.tasks { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--n-line); }
.tasks h2, .sec h3 { margin: 0; font-size: var(--fs-body); line-height: var(--lh-body); font-weight: var(--fw-bold); }
.tasks h2 { display: flex; align-items: baseline; gap: 8px; }
.task { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; gap: 0 10px; align-items: center; width: calc(100% + 16px); margin: 6px 0 0 -16px; padding: 8px 10px 8px 16px; border: 0; border-radius: var(--r-ctl); background: none; text-align: left; }
.task:hover { background: var(--n-50); }
.task .ic { color: var(--text-secondary); }
.task .t2 { grid-column: 2 / span 2; }
.bar { height: 4px; border-radius: 2px; background: var(--n-100); overflow: hidden; }
.bar i { display: block; height: 100%; background: var(--c-ink); border-radius: 2px; }
.task .bar { grid-column: 2 / span 2; margin-top: 6px; }
/* ---------- 右栏:详情卡(唯一的卡片) ---------- */
.detail { position: sticky; top: calc(var(--top-h) + 16px); border: 1px solid var(--n-line); border-radius: var(--r-card); background: var(--c-white); }
.sec { padding: 20px 24px; }
.sec + .sec { border-top: 1px solid var(--n-line); }
.dh { display: flex; align-items: flex-start; gap: 16px; padding-bottom: 18px; }
.dh h2 { margin: 0; font: var(--fw-bold) var(--fs-h2)/var(--lh-h2) var(--font-serif); }
.dh .who { margin-top: 2px; display: flex; gap: 12px; color: var(--text-secondary); }
.dh .btn { margin-left: auto; flex: none; }
/* 状态步骤条 */
.steps { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 18px 0 0; padding: 0; list-style: none; }
.steps li { position: relative; padding-top: 18px; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.steps li::before { content: ""; position: absolute; left: 0; top: 4px; width: 10px; height: 10px; border-radius: 50%; box-sizing: border-box; border: 1.5px solid var(--border-control); background: var(--c-white); z-index: 1; }
.steps li::after { content: ""; position: absolute; left: 14px; right: 4px; top: 8.5px; height: 1.5px; background: var(--n-line); }
.steps li:last-child::after { display: none; }
.steps li.done { color: var(--text-primary); }
.steps li.done::before { background: var(--c-ink); border-color: var(--c-ink); }
.steps li.done::after { background: var(--c-ink); }
.steps li.cur { color: var(--text-primary); font-weight: var(--fw-bold); }
.steps li.cur::before { background: var(--c-ink); border-color: var(--c-ink); box-shadow: 0 0 0 3px var(--n-100); }
.steps li .when { display: block; font-weight: var(--fw-regular); color: var(--text-secondary); font-family: var(--font-figures); font-variant-numeric: tabular-nums; }
/* 截止 + 下一步 */
.dn { display: grid; grid-template-columns: minmax(150px, 34%) minmax(0, 1fr); gap: 24px; }
.lbl { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); margin-bottom: 4px; }
.cd { display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap; }
.cd .n { font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-weight: var(--fw-bold); font-size: 44px; line-height: 48px; }
.cd .u { margin-right: 6px; }
.cd.t-today .u, .cd.t-soon .u { color: var(--warn-text); }
.cd .past { font-size: 20px; line-height: 48px; font-weight: var(--fw-bold); color: var(--text-secondary); }
.cd-at { margin-top: 2px; color: var(--text-secondary); }
.cd-at .fig { font-weight: var(--fw-regular); }
.t-today .n, .t-soon .n { color: var(--warn); }
.situ { margin-bottom: 12px; }
.situ .tag { margin-right: 6px; vertical-align: 1px; }
.acts-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
.note2 { margin-top: 10px; color: var(--text-secondary); display: flex; gap: 6px; align-items: flex-start; }
.note2 .ic { margin-top: 2px; }
/* 详情:致命问题 */
.sec-h { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.sec-h .more { margin-left: auto; }
.issues { list-style: none; margin: 0 -12px; padding: 0; }
.issue { position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0 10px; align-items: start; padding: 10px 12px; border-radius: var(--r-ctl); }
.issue + .issue { margin-top: 2px; }
.issue:hover { background: var(--n-50); }
.issue .tag { margin-top: 1px; }
.issue b { display: block; font-weight: var(--fw-bold); }
.issue .d { color: var(--text-secondary); }
.issue .src { display: inline-flex; align-items: center; gap: 4px; margin-top: 4px; color: var(--ai); text-decoration: none; border-radius: 4px; }
.issue .src .ic { width: 14px; height: 14px; }
.issue .src:hover { color: var(--ai-strong); text-decoration: underline; text-underline-offset: 3px; }
.issue .src .fig { font-weight: var(--fw-regular); }
.issue .btn { opacity: 0; align-self: center; }
.issue:hover .btn, .issue .btn:focus-visible { opacity: 1; }
.none { color: var(--text-secondary); }
/* 详情:清单进度 + 最近处理 */
.two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; }
.prog { display: flex; align-items: baseline; gap: 6px; margin: 6px 0 8px; }
.prog .n { font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-weight: var(--fw-bold); font-size: 20px; line-height: 28px; }
.signed { color: var(--confirmed); }
.log { list-style: none; margin: 6px 0 0; padding: 0; }
.log li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 5px 0; font-size: var(--fs-caption); line-height: var(--lh-caption); }
.log li + li { border-top: 1px solid var(--n-line); }
.log .w { font-weight: var(--fw-bold); }
.log .ok { color: var(--confirmed); }
.log time { color: var(--text-secondary); font-family: var(--font-figures); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dempty { padding: 64px 24px; text-align: center; color: var(--text-secondary); }
/* ---------- 轻提示与悬停说明 ---------- */
.toasts { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 20; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.toast { padding: 10px 16px; border-radius: var(--r-ctl); background: var(--c-ink); color: var(--text-on-dark); box-shadow: var(--shadow-overlay); }
[data-tip] { position: relative; }
[data-tip]:hover::after, [data-tip]:focus-visible::after {
content: attr(data-tip); position: absolute; z-index: 10; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
width: max-content; max-width: 260px; padding: 6px 10px; border-radius: 6px; background: var(--c-ink); color: var(--text-on-dark);
font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: var(--fw-regular); white-space: normal; text-align: left; pointer-events: none;
}
[data-tip].tip-l:hover::after, [data-tip].tip-l:focus-visible::after { left: auto; right: 0; transform: none; }
</style>
</head>
<body>
<aside class="side">
<a class="brand" href="workbench-f.html"><img src="../../logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">
<a href="workbench-f.html" aria-current="page"><svg class="ic lg" viewBox="0 0 24 24" aria-hidden="true"><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"/></svg>工作台</a>
<a href="#" data-demo="资质库"><svg class="ic lg" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/></svg>资质库</a>
<a href="#" data-demo="额度"><svg class="ic lg" viewBox="0 0 24 24" aria-hidden="true"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>额度</a>
<div class="grp">UI demo</div>
<a href="index.html"><svg class="ic lg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>方案目录</a>
</nav>
<div class="side-foot"><b>王敏</b>南京某信息技术有限公司<br>专业版,至 2027-10-31</div>
</aside>
<div class="main">
<header class="top">
<span class="today">11月3日 周二</span>
<div class="right">
<a class="quota tip-l" href="#" data-demo="额度" data-tip="本期剩余额度,至 2027-10-31。点开查看扣点规则和流水。">
<span class="q"><span>解析</span><span class="meter" aria-hidden="true"><i style="width:26%"></i></span><span><span class="fig">37</span><span class="of"> / 50</span></span></span>
<span class="q"><span>检查</span><span class="meter" aria-hidden="true"><i style="width:40%"></i></span><span><span class="fig">12</span><span class="of"> / 20</span></span></span>
</a>
<button class="icon-btn tip-l" aria-label="通知" data-tip="通知" data-notice><svg class="ic lg" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg><span class="dot"></span></button>
<button class="icon-btn tip-l" aria-label="快捷键说明" data-tip="列表里按 ↑ ↓ 切换项目,Enter 打开" data-keys><svg class="ic lg" viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/></svg></button>
</div>
</header>
<main class="content">
<div class="head">
<div>
<h1>工作台</h1>
<div class="stats" role="group" aria-label="快速筛选">
<button class="stat" data-stat="active" aria-pressed="false"><span class="fig">4</span><span class="lb">个进行中</span></button>
<button class="stat" data-stat="due7" aria-pressed="false"><span class="fig">2</span><span class="lb">个 7 天内截止</span></button>
<button class="stat" data-stat="fatal" aria-pressed="false"><span class="fig">3</span><span class="lb">个致命问题待处理</span></button>
<button class="stat" data-stat="pend" aria-pressed="false"><span class="fig">4</span><span class="lb">条清单待确认</span></button>
</div>
</div>
<div class="acts"><span class="cost">扣 <span class="fig">1</span> 次解析,剩 <span class="fig">37</span> 次</span><button class="btn primary" data-new><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>新建项目</button></div>
</div>
<div class="remind" role="status">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
<p><b>某县数据中心机房改造项目今天 17:00 截止。</b>清单还是草稿,还没有做过检查。</p>
<button class="btn sm" data-pick="room">查看项目</button>
</div>
<div class="split">
<section aria-label="项目列表">
<div class="lbar">
<div class="tabs" role="tablist" aria-label="按状态筛选">
<button role="tab" data-tab="active" aria-selected="true">进行中<span class="fig">4</span></button>
<button role="tab" data-tab="all" aria-selected="false" tabindex="-1">全部<span class="fig">6</span></button>
<button role="tab" data-tab="已递交" aria-selected="false" tabindex="-1">已递交<span class="fig">1</span></button>
<button role="tab" data-tab="已归档" aria-selected="false" tabindex="-1">已归档<span class="fig">1</span></button>
</div>
<label class="search"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg><input type="search" placeholder="搜索项目" aria-label="搜索项目名称或采购人" title="可以搜项目名称或采购人" data-q></label>
</div>
<div class="qf cap" data-qf hidden><span data-qf-text></span><button class="lnk cap" data-qf-clear>清除筛选</button></div>
<ul class="plist" role="listbox" aria-label="项目" data-list></ul>
<p class="lhint cap"><span class="kbd">↑</span> <span class="kbd">↓</span> 切换项目,<span class="kbd">Enter</span> 打开</p>
<div class="tasks">
<h2>正在进行<span class="cap sub" style="font-weight:400">可以离开本页,完成后会发站内通知</span></h2>
<button class="task" data-pick="med">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.22-8.56"/></svg>
<span><b>解析补遗 2</b><span class="sub cap"> 提取清单</span></span><span class="fig">62%</span>
<span class="t2 cap sub">某市医保信息系统等保测评服务</span>
<span class="bar"><i style="width:62%"></i></span>
</button>
<button class="task" data-pick="comm">
<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
<span><b>检查排队中</b><span class="sub cap"> 前面还有 1 个任务</span></span><span></span>
<span class="t2 cap sub">某区智慧社区平台运维服务</span>
</button>
</div>
</section>
<section class="detail" aria-live="polite" aria-label="项目详情" data-detail></section>
</div>
</main>
</div>
<script>
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
const IC = {
open: '<path d="M7 7h10v10"/><path d="M7 17 17 7"/>',
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8M16 13H8M16 17H8"/>',
spin: '<path d="M21 12a9 9 0 1 1-6.22-8.56"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>'
};
const ic = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${IC[n]}</svg>`;
const fig = (n) => `<span class="fig">${n}</span>`;
function toast(msg) {
let box = $(".toasts");
if (!box) { box = document.createElement("div"); box.className = "toasts"; box.setAttribute("role", "status"); box.setAttribute("aria-live", "polite"); document.body.appendChild(box); }
const t = document.createElement("div"); t.className = "toast"; t.textContent = msg; box.appendChild(t);
setTimeout(() => t.remove(), 3200);
}
/* 状态序号:跟进中 0 → 编制中 1 → 已检查 2 → 已递交 3;已归档 = 全部完成 */
const STEPS = ["跟进中", "编制中", "已检查", "已递交"];
const CHECK_NOTE = `扣 ${fig(1)} 次检查,剩 ${fig(12)} 次`;
const P = [
{ id: "room", name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, st: "编制中", fatal: 0, pend: 3,
due: "今天 17:00", left: "剩 6 小时", tone: "t-today",
cd: [["6", "小时"]], at: `今天 ${fig("17:00")} 截止`,
stepAt: ["10-28", "11-01", "", ""],
situ: `清单还是草稿,还有 ${fig(3)} 条待确认。`,
acts: [{ t: "确认清单", p: 1, href: "../page-reading.html" }, { t: "发起检查", note: CHECK_NOTE }],
note: `${ic("info")}<span>投标文件已上传。先确认清单再检查,检查会用到这些条目。</span>`,
issuesEmpty: "还没有做过检查。检查后,致命问题会列在这里。",
list: { done: 39, total: 42, line: `还有 ${fig(3)} 条待确认` },
log: [["李工", "上传投标文件(3 册)", "09:40"], ["李工", "确认了 39 条清单条目", "11-02"], ["系统", "解析完成,提取清单 42 条", "11-01"], ["李工", "新建项目,上传招标文件", "11-01"]] },
{ id: "comm", name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", h: 53, st: "已检查", ready: true, fatal: 0, pend: 0,
due: "11月5日 16:00", left: "剩 2 天 5 小时", tone: "t-soon", busy: "检查排队中",
cd: [["2", "天"], ["5", "小时"]], at: `11月5日 ${fig("16:00")} 截止`,
stepAt: ["10-20", "10-24", "11-01", ""],
situ: `<span class="tag ready" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。" tabindex="0">就绪</span>第 1 次检查的致命问题都已处理。`,
acts: [{ t: "标记已递交", p: 1 }, { t: "查看检查结果", href: "../page-reading.html" }],
note: `${ic("clock")}<span>修改后的投标文件正在排队复查,前面还有 ${fig(1)} 个任务。复查完成前也可以标记递交。</span>`,
issuesEmpty: "第 1 次检查的致命问题都已处理。复查结果出来后会更新。",
list: { done: 38, total: 38, sign: "李工 确认了清单", when: "10-24" },
log: [["系统", "第 2 次检查排队中", "09:53"], ["李工", "上传投标文件 v3", "09:52"], ["李工", '<span class="ok">✓ 已修改</span>「业绩证明材料页码」', "11-02"], ["系统", "完成第 1 次检查,致命 1", "11-01"]] },
{ id: "hall", name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, st: "已检查", ready: false, fatal: 3, pend: 0,
due: "11月20日 09:30", left: "剩 17 天", tone: "",
cd: [["17", "天"]], at: `11月20日 ${fig("09:30")} 截止`,
stepAt: ["10-15", "10-22", "11-02", ""],
situ: `未就绪:${fig(3)} 项致命问题待处理。`,
acts: [{ t: "处理致命问题", p: 1, href: "../page-reading.html" }, { t: "导出检查报告" }],
note: `${ic("info")}<span>致命问题没处理完也可以导出,报告会如实列出这 ${fig(3)} 项。</span>`,
issues: [
["报价超过采购包 1 的最高限价", "开标一览表 287.60 万元,限价 286.00 万元", "报价册", 1],
["★ 条款疑似不满足:应用服务器内存", "响应 256G,要求 ≥ 512G。上次标记为已修改,问题仍在", "技术册", 12],
["授权委托书的单位签章位置未发现盖章", "只看有无,不鉴定真伪", "商务册", 5]
],
list: { done: 46, total: 46, sign: "王敏 确认了清单 v2", when: "11-02" },
log: [["王敏", '<span class="ok">✓ 已修改</span>「授权委托书签章」', "14:26"], ["王敏", "确认了清单 v2", "11-02"], ["系统", "完成第 2 次检查,致命 3", "11-02"], ["系统", "并入补遗 1,更新 2 条", "11-02"], ["李工", "忽略「其他项目名称」:业绩介绍", "11-01"]] },
{ id: "med", name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", h: 600, st: "跟进中", fatal: 0, pend: 1,
due: "11月28日 10:00", left: "剩 25 天", tone: "", busy: "解析中 62%",
cd: [["25", "天"]], at: `11月28日 ${fig("10:00")} 截止`,
stepAt: ["10-29", "", "", ""],
situ: `清单还有 ${fig(1)} 条待确认;还没有上传投标文件。`,
acts: [{ t: "确认清单", p: 1, href: "../page-reading.html" }, { t: "上传投标文件" }],
note: `${ic("spin")}<span>补遗 2 正在解析(${fig("62%")}),完成后清单可能新增或更新条目。</span>`,
issuesEmpty: "还没有做过检查。上传投标文件并检查后,致命问题会列在这里。",
list: { done: 35, total: 36, line: `还有 ${fig(1)} 条待确认` },
log: [["李工", "上传补遗 2", "10:12"], ["李工", "确认了 35 条清单条目", "10-30"], ["系统", "解析完成,提取清单 36 条", "10-29"], ["李工", "新建项目,上传招标文件", "10-29"]] },
{ id: "camp", name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, st: "已递交", fatal: 0, pend: 0,
due: "11月1日 09:30", left: "已截止", tone: "t-past",
past: true, at: `11月1日 ${fig("09:30")} 截止`,
stepAt: ["10-08", "10-12", "10-29", "10-31"],
situ: "已递交,等开标结果。",
acts: [{ t: "归档项目", p: 1 }, { t: "导出检查报告" }],
note: "",
issuesEmpty: "最后一次检查(10-29)的致命问题都已处理。",
list: { done: 40, total: 40, sign: "王敏 确认了清单", when: "10-12" },
log: [["李工", "标记已递交", "10-31"], ["王敏", "导出检查报告", "10-30"], ["系统", "完成第 2 次检查,致命 0", "10-29"]] },
{ id: "land", name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, st: "已归档", fatal: 0, pend: 0,
due: "10月20日 09:00", left: "已截止", tone: "t-past",
past: true, at: `10月20日 ${fig("09:00")} 截止`,
stepAt: ["09-21", "09-26", "10-16", "10-19"],
situ: "已归档。归档的项目只读,资料可以复用到新项目。",
acts: [{ t: "查看检查报告", p: 1 }, { t: "取消归档" }],
note: "",
issuesEmpty: "最后一次检查(10-16)的致命问题都已处理。",
list: { done: 51, total: 51, sign: "王敏 确认了清单", when: "09-26" },
log: [["王敏", "归档项目", "10-28"], ["李工", "标记已递交", "10-19"], ["系统", "完成第 1 次检查,致命 0", "10-16"]] }
];
const QF = {
active: { t: "进行中的项目" },
due7: { t: "只看 7 天内截止的项目", f: (p) => p.h >= 0 && p.h <= 168 },
fatal: { t: "只看有待处理致命问题的项目", f: (p) => p.fatal > 0 },
pend: { t: "只看有待确认清单条目的项目", f: (p) => p.pend > 0 }
};
let tab = "active", quick = "", q = "";
const hashSel = (location.hash.match(/p=(\w+)/) || [])[1];
let sel = P.some((p) => p.id === hashSel) ? hashSel : "room";
const isActive = (p) => !["已递交", "已归档"].includes(p.st);
function visible() {
return P.filter((p) => {
if (quick && QF[quick].f) { if (!QF[quick].f(p)) return false; }
else if (!(tab === "all" || (tab === "active" ? isActive(p) : p.st === tab))) return false;
return !q || (p.name + p.buyer).includes(q);
});
}
function renderList() {
const rows = visible();
if (rows.length && !rows.some((p) => p.id === sel)) sel = rows[0].id;
$("[data-list]").innerHTML = rows.map((p) => `
<li class="pitem" role="option" id="pi-${p.id}" data-id="${p.id}" aria-selected="${p.id === sel}" tabindex="${p.id === sel ? 0 : -1}">
<span class="nm" title="${p.name}">${p.name}</span>
<span class="due ${p.tone}">${p.due}</span>
<span class="meta"><span class="by" title="${p.buyer}">${p.buyer}</span><span>${p.lot}</span><span>${p.st}</span>${p.busy ? `<span class="busy">${ic(p.busy.startsWith("解析") ? "spin" : "clock")}${p.busy}</span>` : ""}${p.fatal ? `<span class="tag fatal">致命 ${fig(p.fatal)}</span>` : ""}</span>
<span class="left ${p.tone}">${p.left}</span>
<button class="go" tabindex="-1" aria-label="打开项目" data-open="${p.id}">${ic("open")}</button>
</li>`).join("") || `<li class="empty"><b>没有符合条件的项目</b>换个关键词,或切到「全部」看看。</li>`;
const qf = $("[data-qf]");
qf.hidden = !(quick && QF[quick].f);
if (!qf.hidden) $("[data-qf-text]").textContent = `${QF[quick].t},共 ${rows.length} 个`;
$$("[data-stat]").forEach((b) => b.setAttribute("aria-pressed", b.dataset.stat === quick));
return rows;
}
function renderDetail(fade) {
const box = $("[data-detail]");
const p = P.find((x) => x.id === sel);
if (!p || !visible().some((x) => x.id === sel)) { box.innerHTML = `<div class="dempty">在左侧选一个项目,详情会显示在这里。</div>`; return; }
const si = p.st === "已归档" ? 4 : STEPS.indexOf(p.st);
const steps = STEPS.map((s, i) => `<li class="${i < si ? "done" : i === si ? "cur" : ""}"${i === si ? ' aria-current="step"' : ""}>${s}<span class="when">${p.stepAt[i] || " "}</span></li>`).join("");
const cd = p.past ? `<div class="cd"><span class="past">已截止</span></div>`
: `<div class="cd ${p.tone}">${p.cd.map(([n, u]) => `<span class="n">${n}</span><span class="u">${u}</span>`).join("")}</div>`;
const acts = p.acts.map((a) => `<button class="btn ${a.p ? "primary" : ""}" data-act="${a.t}"${a.href ? ` data-href="${a.href}"` : ""}>${a.t}</button>${a.note ? `<span class="cost">${a.note}</span>` : ""}`).join("");
const issues = p.issues
? `<ul class="issues">${p.issues.map(([t, d, book, pg]) => `<li class="issue">
<span class="tag fatal">致命</span>
<div><b>${t}</b><span class="d cap">${d}</span><br><a class="src cap" href="#" data-src="${book} 第 ${pg} 页">${ic("file")}${book} 第 ${fig(pg)} 页</a></div>
<button class="btn sm" data-href="../page-reading.html">去处理</button></li>`).join("")}</ul>`
: `<p class="none">${p.issuesEmpty}</p>`;
const pct = Math.round((p.list.done / p.list.total) * 100);
const listLine = p.list.sign ? `<span class="signed">✓ ${p.list.sign}</span> <span class="sub fig" style="font-weight:400">${p.list.when}</span>` : `<span class="t-today">${p.list.line}</span>`;
box.innerHTML = `
<div class="sec">
<div class="dh">
<div style="min-width:0"><h2>${p.name}</h2><div class="who cap"><span>${p.buyer}</span><span>${p.lot}</span></div></div>
<button class="btn" data-open="${p.id}">${ic("open")}打开项目</button>
</div>
<ol class="steps" aria-label="项目状态">${steps}</ol>
</div>
<div class="sec dn">
<div><div class="lbl">投标截止</div>${cd}<div class="cd-at cap">${p.at}</div></div>
<div><div class="lbl">下一步</div><p class="situ">${p.situ}</p><div class="acts-row">${acts}</div>${p.note ? `<p class="note2 cap">${p.note}</p>` : ""}</div>
</div>
<div class="sec">
<div class="sec-h"><h3>待处理的致命问题</h3>${p.fatal ? `<span class="tag fatal">致命 ${fig(p.fatal)}</span><a class="lnk cap more" href="../page-reading.html">去查标页处理</a>` : ""}</div>
${issues}
</div>
<div class="sec two">
<div><h3>清单确认</h3>
<div class="prog"><span class="n">${p.list.done}</span><span class="sub">/ ${fig(p.list.total)} 条</span></div>
<div class="bar" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100" aria-label="清单确认进度"><i style="width:${pct}%"></i></div>
<p class="cap" style="margin-top:8px">${listLine}</p>
</div>
<div><div class="sec-h" style="margin-bottom:0"><h3>最近处理</h3><a class="lnk cap more" href="#" data-demo="全部记录">全部记录</a></div>
<ul class="log">${p.log.map(([w, t, tm]) => `<li><span><span class="w">${w}</span> ${t}</span><time>${tm}</time></li>`).join("")}</ul>
</div>
</div>`;
if (fade && !matchMedia("(prefers-reduced-motion: reduce)").matches) box.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 120, easing: "ease-out" });
}
function select(id, { focus = false, fade = true } = {}) {
if (id === sel && !focus) return;
const changed = id !== sel;
sel = id;
$$(".pitem").forEach((li) => { const on = li.dataset.id === id; li.setAttribute("aria-selected", on); li.tabIndex = on ? 0 : -1; });
const li = $(`#pi-${id}`);
if (li && focus) { li.focus(); li.scrollIntoView({ block: "nearest" }); }
if (changed) renderDetail(fade);
}
function openProject(id) {
const p = P.find((x) => x.id === id);
if (id === "room" || id === "hall") location.href = "../page-reading.html";
else toast(`(演示)打开「${p.name}」:只做了「读标」组合页`);
}
function rerender() { renderList(); renderDetail(true); }
/* 列表:点击选中、↑↓ 切换、Enter 打开 */
const list = $("[data-list]");
list.addEventListener("click", (e) => {
const go = e.target.closest("[data-open]");
if (go) { e.stopPropagation(); return openProject(go.dataset.open); }
const li = e.target.closest(".pitem"); if (li) select(li.dataset.id);
});
list.addEventListener("dblclick", (e) => { const li = e.target.closest(".pitem"); if (li) openProject(li.dataset.id); });
list.addEventListener("keydown", (e) => {
const items = $$(".pitem"); const i = items.findIndex((x) => x.dataset.id === sel);
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
const j = Math.max(0, Math.min(items.length - 1, i + (e.key === "ArrowDown" ? 1 : -1)));
if (items[j]) select(items[j].dataset.id, { focus: true });
} else if (e.key === "Home" || e.key === "End") {
e.preventDefault(); const it = items[e.key === "Home" ? 0 : items.length - 1]; if (it) select(it.dataset.id, { focus: true });
} else if (e.key === "Enter") { e.preventDefault(); openProject(sel); }
});
/* 标签页、统计筛选、搜索 */
function setTab(v) {
tab = v; quick = "";
$$("[data-tab]").forEach((b) => { const on = b.dataset.tab === v; b.setAttribute("aria-selected", on); b.tabIndex = on ? 0 : -1; });
rerender();
}
$$("[data-tab]").forEach((b) => b.addEventListener("click", () => setTab(b.dataset.tab)));
$(".tabs").addEventListener("keydown", (e) => {
const tabs = $$("[data-tab]"); const i = tabs.indexOf(document.activeElement); if (i < 0) return;
const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0; if (!d) return;
e.preventDefault(); const t = tabs[(i + d + tabs.length) % tabs.length]; t.focus(); setTab(t.dataset.tab);
});
$$("[data-stat]").forEach((b) => b.addEventListener("click", () => {
const v = b.dataset.stat;
if (v === "active") { setTab("active"); return; }
quick = quick === v ? "" : v;
$$("[data-tab]").forEach((t) => t.setAttribute("aria-selected", !quick && t.dataset.tab === tab));
rerender();
}));
$("[data-qf-clear]").addEventListener("click", () => setTab(tab));
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); rerender(); });
/* 让被选中的项目可见:提醒条、任务 */
$$("[data-pick]").forEach((b) => b.addEventListener("click", () => {
const id = b.dataset.pick, p = P.find((x) => x.id === id);
if (!visible().some((x) => x.id === id)) { tab = isActive(p) ? "active" : "all"; quick = ""; q = ""; $("[data-q]").value = ""; $$("[data-tab]").forEach((t) => t.setAttribute("aria-selected", t.dataset.tab === tab)); renderList(); }
select(id, { focus: true });
}));
/* 详情卡里的操作 */
$("[data-detail]").addEventListener("click", (e) => {
const o = e.target.closest("[data-open]"); if (o) return openProject(o.dataset.open);
const s = e.target.closest("[data-src]"); if (s) { e.preventDefault(); return toast(`(演示)在原文查看器里打开:${s.dataset.src}`); }
const d = e.target.closest("[data-demo]"); if (d) { e.preventDefault(); return toast(`(演示)打开${d.dataset.demo}`); }
const b = e.target.closest("[data-act], [data-href]"); if (!b) return;
if (b.dataset.href) { location.href = b.dataset.href; return; }
const p = P.find((x) => x.id === sel), a = b.dataset.act;
const msg = {
"发起检查": "(演示)发起检查:扣 1 次检查,剩 12 次。可以离开本页,完成后会发站内通知。",
"标记已递交": "(演示)已标记为「已递交」,可以撤销。",
"导出检查报告": p.fatal ? `(演示)导出检查报告:还有 ${p.fatal} 项致命问题待处理,报告会如实列出。` : "(演示)导出检查报告",
"上传投标文件": "(演示)打开上传投标文件对话框",
"归档项目": "(演示)已归档,可以撤销。"
}[a] || `(演示)${a}`;
toast(msg);
});
$$("[data-demo]").forEach((a) => { if (!a.closest("[data-detail]")) a.addEventListener("click", (e) => { e.preventDefault(); toast(`(演示)打开${a.dataset.demo}`); }); });
$("[data-new]").onclick = () => toast("(演示)新建项目:上传招标文件,扣 1 次解析,剩 37 次。解析失败不扣额度。");
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
$("[data-keys]").onclick = () => { const li = $(`#pi-${sel}`); if (li) li.focus(); toast("列表里按 ↑ ↓ 切换项目,Enter 打开。"); };
if (hashSel && !visible().some((p) => p.id === sel)) { tab = "all"; $$("[data-tab]").forEach((t) => { const on = t.dataset.tab === tab; t.setAttribute("aria-selected", on); t.tabIndex = on ? 0 : -1; }); }
renderList(); renderDetail(false);
</script>
</body>
</html>