Files
bid-doctor/docs/design/ui-demo/explore/workbench-f.html
YANG JIANKUAN 067cad2120 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>
2026-11-03 14:35:08 +08:00

639 lines
46 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
<!--
工作台 · 风格探索方案 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>