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,59 @@
<!doctype html>
<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="../../logo/favicon.svg">
<link rel="stylesheet" href="../../system/tokens.css">
<link rel="stylesheet" href="../../system/fonts-local.css">
<style>
body{margin:0;background:var(--bg-page);color:var(--text-primary);font:var(--fs-body)/var(--lh-body) var(--font-sans)}
main{max-width:960px;margin:0 auto;padding:var(--sp-12) var(--sp-6)}
h1{font:var(--fw-bold) 32px/40px var(--font-serif);margin:0 0 var(--sp-2)}
.lead{color:var(--text-secondary);margin:0 0 var(--sp-8)}
.v{display:grid;grid-template-columns:120px minmax(0,1fr) auto;gap:var(--sp-4);align-items:baseline;padding:var(--sp-5) 0;border-top:1px solid var(--border-subtle)}
.v:last-of-type{border-bottom:1px solid var(--border-subtle)}
.v b{font:var(--fw-bold) 20px/28px var(--font-serif)}
.v p{margin:0}
.v .sub{color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption);margin-top:var(--sp-1)}
.v a{display:inline-flex;align-items:center;height:36px;padding:0 var(--sp-4);border-radius:6px;background:var(--c-ink);color:var(--text-on-dark);text-decoration:none;white-space:nowrap}
.v a.ghost{background:var(--bg-sunken);color:var(--text-primary)}
.v a:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
.round{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans);margin:var(--sp-10) 0 var(--sp-2)}
.note{margin-top:var(--sp-6);color:var(--text-secondary);font-size:var(--fs-caption);line-height:var(--lh-caption)}
</style>
</head>
<body>
<main>
<h1>工作台风格方案</h1>
<p class="lead">同一份内容、同一套颜色、字体和图标,逐轮调整版式和分层方式。<b>D5 已定稿</b>,规则写入设计规范 v0.2 和 tokens.css v0.2。</p>
<h2 class="round">第六轮:窄侧栏,上图标下文字(2026-11-03 定稿,决策 U5)</h2>
<div class="v"><b>D5</b><div><p>侧栏只有一种状态,宽 96px,不再折叠。顶部是竖版 logo(上印章、下字标);菜单改为上图标、下文字,图标换成更精致的 Lucide 线性图标(工作台 house、资质库 award、额度 wallet)。当前项是圆角矩形的半透明浅色底。底部是大头像、名称、套餐和有效期。</p><p class="sub">其余(渐变侧栏、点阵、透明顶栏、精简文案、下一步悬停)同 D4。</p></div><a href="workbench-d5.html">打开</a></div>
<h2 class="round">第五轮:深色侧栏与内容区融合</h2>
<div class="v"><b>D4</b><div><p>侧栏改为双色竖向渐变(墨色到深墨蓝),底部有一片淡淡的点阵;内容区左上角是 12px 圆角,像一张纸铺在深色桌面上;当前菜单项是浅色半透明底,加侧栏左缘一条 3px 白色竖条(?sel=bar 只有竖条,?sel=solid 白色实底)。logo 放大并在侧栏里居中;「下一步」悬停时只让箭头右移;精简了说明性文案。D3 加的装饰图标已全部去掉,只保留原有的功能图标;顶栏和问候语保留 D3 的改法。</p><p class="sub">网址加 ?v=flat 可以看 D3 的平铺深色侧栏作对照;折叠后效果同样成立。</p></div><a href="workbench-d4.html">打开</a></div>
<h2 class="round">第四轮:顶栏优化,加入图标</h2>
<div class="v"><b>D3</b><div><p>顶栏不再是一条白带:平时透明,融进页面底色;往下滚动后变成半透明白(毛玻璃)加一条细线。搜索框靠右,聚焦时变宽。统计、区块标题、项目行、下一步按钮、最近处理都配了图标;图标底块大多是中性浅灰,只有带含义的才着色(截止赭、致命红、待确认的 AI 条目蓝)。页头加了问候语。</p><p class="sub">侧栏同 D2,可以加 ?side=light 看浅灰侧栏。</p></div><a href="workbench-d3.html">打开</a></div>
<h2 class="round">第三轮:在 D 的基础上,侧边栏换底色并支持折叠</h2>
<div class="v"><b>D2 墨色侧栏</b><div><p>侧边栏改为墨色底,logo 用反白版。底部「收起侧栏」按钮(或 Ctrl \)可以折叠到只留图标,折叠后 logo 换成印章,悬停图标有提示;折叠状态会记住。</p><p class="sub">另有浅灰侧栏对照;也可以在网址后加 ?collapsed=1 直接看折叠状态。</p></div><a href="workbench-d2.html">打开</a></div>
<div class="v"><b>D2 浅灰侧栏</b><div><p>同一个页面,侧边栏用从墨色派生的浅灰底,当前项是白色块。比墨色版温和,但和白色内容区的区分弱一些。</p><p class="sub">网址参数 ?side=light。</p></div><a class="ghost" href="workbench-d2.html?side=light">打开</a></div>
<h2 class="round">第二轮(2026-11-03):白色为主、有交互感、整齐</h2>
<div class="v"><b>D 明亮工作区</b><div><p>结构和现状接近,整页只用一种白卡片加发丝线。统计格可以点击筛选;每个项目行有「下一步」,悬停时变成按钮,并出现「更多」菜单(会扣点的操作写明扣多少)。</p><p class="sub">圆角 6 / 8 / 12;浅底改为从墨色派生的冷灰;状态标签改为浅灰实底。</p></div><a href="workbench-d.html">打开</a></div>
<div class="v"><b>E 下一步</b><div><p>页面最上方是按紧急程度排好的待办队列:今天截止 → 有致命问题 → 7 天内 → 其他,每条都带操作按钮,致命那条可以展开问题。项目表放在下面。</p><p class="sub">圆角 4 / 8 / 12;只有待办卡悬停时出现阴影;取消统计卡和单独的提醒条。</p></div><a href="workbench-e.html">打开</a></div>
<div class="v"><b>F 列表 + 详情</b><div><p>左边是项目列表,点击或用 ↑ / ↓ 选中,右边的详情立即切换:状态步骤、倒计时、下一步(带扣点说明)、致命问题、清单确认进度、最近处理。</p><p class="sub">页面全白;圆角 4 / 8 / 12;统计改为页头下的一行;额度移到顶栏。</p></div><a href="workbench-f.html">打开</a></div>
<h2 class="round">第一轮:反馈是 A 像只读报告、B 有点乱、C 排除,底色太绿灰</h2>
<div class="v"><b>现状</b><div><p>描边框、2px 圆角、灰色表头、四张统计卡。</p><p class="sub">用作对照。</p></div><a class="ghost" href="../page-workbench.html">打开</a></div>
<div class="v"><b>A 留白文档</b><div><p>主列是一张白纸,几乎不用框线,靠字号、字重和留白分层。四张统计卡换成一句嵌大数字的概要句,数字可以点击筛选。</p><p class="sub">圆角 6 / 8 / 12;纸面极轻阴影;标题加大到 32;状态改为纯文字。</p></div><a href="workbench-a.html">打开</a></div>
<div class="v"><b>B 工作面板</b><div><p>信息密度不降,层次靠底色分区和圆角:校样纸色面板里放白色列表行。每个项目行带一条四段状态轨。顶栏有全局搜索。</p><p class="sub">圆角 4 / 6 / 8 / 10;不用描边和阴影;页面底色改白;状态标签改实底。</p></div><a href="workbench-b.html">打开</a></div>
<div class="v"><b>C 截止线(已排除)</b><div><p>以时间为主轴:顶部墨色时间带标出未来 30 天的截止点,今天截止的提醒和统计都并进时间带。项目行左侧是大号日期块。</p><p class="sub">圆角 4 / 6 / 10 / 12;新增大面积墨底;项目名用宋体 17。</p></div><a href="workbench-c.html">打开</a></div>
<p class="note">每个方案文件顶部的 HTML 注释里,列出了它相对 design-system.md 改了哪些规则。</p>
</main>
</body>
</html>

View File

@@ -0,0 +1,436 @@
<!doctype html>
<!--
工作台 · 风格探索方案 A「留白文档」
内容、数据、交互沿用 ../page-workbench.html;颜色只用 tokens.css 的色值和语义。
本方案相对 design-system.md 改了这些规则:
1. 圆角(§5.1 控件和卡片 2px、浮层 4px):控件 6px,主列白纸面 12px,侧栏当前项、提醒条、表格行悬停 8px,问题级别标签 4px。
2. 阴影(§5.1 只用于浮层):主列白纸面加一层极轻阴影,只为把白纸从校样纸上托起来;右栏、提醒条、标签都不用阴影。
3. 分隔方式(§5.1 层次靠分隔线和底色):表头去掉凹陷灰底和加粗下边线,只剩小号辅助灰字;表格不画竖线,行间只留一条极浅分隔线;
侧栏不画右边线、顶栏不画下边线;右栏三组内容不加卡片框,只用间距和小标题分组。
4. 字阶(§4.2):H1 24/32 → 32/40;区块标题改用宋体 20/28;项目名保持 16/24;新增「概要句」22/52,句中数字 Barlow 44;
正文仍为 14/22,最小 12px,字重只用 400 / 600。
5. 统计卡(§7.9 / 原型的四张统计卡):换成一句概要句,句中数字可点,点了就筛选下方项目列表。
6. 控件(§7.1):按钮高 32 → 36;分段筛选改为文字标签页,选中用 2px 墨色下划线;搜索框去掉描边,只用校样纸浅底
(放宽了 §3.2「需要看清边界的控件用 --border-control」:靠浅底、放大镜图标和占位符说明这是输入框);提醒条里的次按钮改为无描边白底。
7. 状态 / 就绪(§7.9 中性描边标签):改为纯文字;「就绪」用虚下划线提示可悬停查看说明。问题级别标签的形状规则不变(致命实心、重要描边、提示无框)。
8. 提醒条(§7.12):去掉外框,赭浅底 + 左侧 3px 赭色竖条。
9. 截止时间:日期一行(数字用 Barlow)+ 剩余时间一行,整列右对齐放在最后一列;颜色规则不变(≤72 小时赭小字,≤24 小时加粗,已截止辅助灰)。
-->
<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>
/* ---------- 本方案派生的变量(全部由 tokens.css 现有颜色 color-mix 得到,不引入新色相) ---------- */
:root {
--a-sheet-shadow: 0 1px 2px color-mix(in srgb, var(--c-ink) 5%, transparent),
0 12px 32px -16px color-mix(in srgb, var(--c-ink) 12%, transparent); /* 白纸面的极轻阴影:墨 5% / 12% */
--a-row-rule: color-mix(in srgb, var(--c-rule) 60%, var(--c-white)); /* 表格行间分隔:分隔线 60% 混白,比 --c-rule 更浅 */
--a-nav-hover: color-mix(in srgb, var(--c-white) 55%, var(--c-paper)); /* 侧栏悬停:白与校样纸之间 */
--a-underline: color-mix(in srgb, var(--c-ink) 22%, var(--c-white)); /* 概要句数字的常态下划线:墨 22% */
--a-radius-ctl: 6px; /* 按钮、输入框、图标按钮 */
--a-radius-block: 8px; /* 侧栏当前项、提醒条、行悬停 */
--a-radius-sheet: 12px; /* 主列白纸面 */
--a-ctl-h: 36px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; min-width: 1280px;
background: var(--bg-page); 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 { font: inherit; color: inherit; }
.fig { font-family: var(--font-figures); font-variant-numeric: tabular-nums; }
.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; border-radius: 4px; }
.lnk { color: var(--text-primary); text-decoration: underline; text-decoration-color: var(--a-underline); text-underline-offset: 3px; }
.lnk:hover { text-decoration-color: currentColor; }
/* ---------- 框架 ---------- */
.app { display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); min-height: 100vh; }
.side { position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; padding: var(--sp-6) var(--sp-4) var(--sp-6) var(--sp-5); }
.brand { display: inline-block; padding: 0 var(--sp-2); margin-bottom: var(--sp-10); }
.brand img { display: block; height: 34px; width: auto; }
.nav { display: flex; flex-direction: column; gap: 2px; }
.nav a {
display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 var(--sp-3);
border-radius: var(--a-radius-block); color: var(--text-secondary); text-decoration: none;
}
.nav a:hover { background: var(--a-nav-hover); color: var(--text-primary); }
.nav a[aria-current="page"] { background: var(--bg-surface); color: var(--text-primary); font-weight: var(--fw-bold); }
.nav .demo { margin-top: var(--sp-6); font-size: var(--fs-caption); line-height: var(--lh-caption); height: 32px; }
.side-foot { margin-top: auto; padding: 0 var(--sp-3); font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.side-foot b { display: block; font-size: var(--fs-body); line-height: var(--lh-body); font-weight: var(--fw-bold); color: var(--text-primary); }
.main { padding: 0 var(--sp-10) var(--sp-16) var(--sp-6); min-width: 0; }
.top { display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-1); height: 64px; }
.quota-chip { display: inline-flex; align-items: center; gap: 6px; height: var(--a-ctl-h); padding: 0 var(--sp-3); border-radius: var(--a-radius-ctl); color: var(--text-secondary); text-decoration: none; }
.quota-chip .fig { font-size: 16px; font-weight: var(--fw-bold); color: var(--text-primary); margin-right: 6px; }
.quota-chip:hover, .icon-btn:hover { background: var(--a-nav-hover); }
.icon-btn { position: relative; display: inline-grid; place-items: center; width: var(--a-ctl-h); height: var(--a-ctl-h); border: 0; border-radius: var(--a-radius-ctl); background: none; color: var(--text-secondary); cursor: pointer; }
.icon-btn:hover { color: var(--text-primary); }
.icon-btn .dot { position: absolute; top: 8px; right: 8px; width: 7px; height: 7px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 2px var(--bg-page); }
.layout { display: grid; grid-template-columns: minmax(0, 1fr) clamp(256px, 20.5vw, 296px); gap: clamp(32px, 2.8vw, 48px); align-items: start; max-width: 1240px; }
/* ---------- 白纸面 ---------- */
.sheet { background: var(--bg-surface); border-radius: var(--a-radius-sheet); box-shadow: var(--a-sheet-shadow); padding: var(--sp-10) clamp(36px, 3.3vw, 52px) var(--sp-12); }
.head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-6); }
.head h1 { margin: 0; font: var(--fw-bold) 32px/40px var(--font-serif); letter-spacing: .02em; }
.head .date { margin-left: var(--sp-4); color: var(--text-secondary); font-family: var(--font-sans); font-size: var(--fs-body); font-weight: var(--fw-regular); letter-spacing: 0; }
.head .actions { display: flex; align-items: center; gap: var(--sp-4); flex: none; }
.cost { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: var(--a-ctl-h); padding: 0 var(--sp-4);
border: 0; border-radius: var(--a-radius-ctl); font-weight: var(--fw-bold); text-decoration: none; cursor: pointer; white-space: nowrap;
}
.btn.primary { background: var(--c-ink); color: var(--text-on-dark); }
.btn.primary:hover { background: color-mix(in srgb, var(--c-ink) 92%, var(--c-white)); }
.btn.quiet { height: 32px; padding: 0 var(--sp-3); background: var(--bg-surface); color: var(--text-primary); }
.btn.quiet:hover { background: color-mix(in srgb, var(--c-white) 60%, var(--warn-soft)); }
/* 概要句:本页唯一的「大声」元素 */
.lede { margin: var(--sp-8) 0 0; max-width: 34em; font-size: 22px; line-height: 52px; color: var(--text-primary); }
.lede .half { display: block; white-space: nowrap; } /* 两半句在分号处固定断行 */
.n {
padding: 0 3px; margin: 0 1px; border: 0; background: none; cursor: pointer;
font: var(--fw-bold) 44px/1 var(--font-figures); font-variant-numeric: tabular-nums; color: var(--text-primary);
text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 7px; text-decoration-color: var(--a-underline);
vertical-align: -3px;
}
.n:hover { text-decoration-color: currentColor; }
.n[aria-pressed="true"] { text-decoration-color: currentColor; text-decoration-thickness: 3px; }
.n.due { color: var(--c-ochre); } /* ≥18px,可用 --c-ochre */
.n.fatal { color: var(--sev-fatal); }
.lede-sub { margin: var(--sp-2) 0 0; color: var(--text-secondary); }
/* 提醒条 */
.alert { position: relative; display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-8); padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5); border-radius: var(--a-radius-block); background: var(--warn-soft); overflow: hidden; }
.alert::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--warn); }
.alert > .ic { color: var(--warn); }
.alert p { margin: 0; flex: 1; min-width: 0; }
.alert b { color: var(--warn-text); font-weight: var(--fw-bold); }
/* 筛选 + 搜索 */
.bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-6); margin-top: var(--sp-10); }
.tabs { display: flex; gap: var(--sp-6); }
.tabs button {
height: var(--a-ctl-h); padding: 0 2px; border: 0; background: none; cursor: pointer;
font-size: 15px; color: var(--text-secondary); box-shadow: inset 0 -2px 0 transparent;
}
.tabs button:hover { color: var(--text-primary); }
.tabs button[aria-pressed="true"] { color: var(--text-primary); font-weight: var(--fw-bold); box-shadow: inset 0 -2px 0 var(--c-ink); }
.tabs .fig { margin-left: 4px; font-size: 15px; }
.search { position: relative; display: block; width: 248px; }
.search .ic { position: absolute; left: 12px; top: 10px; color: var(--text-secondary); pointer-events: none; }
.search input {
width: 100%; height: var(--a-ctl-h); padding: 0 var(--sp-3) 0 36px; border: 0; border-radius: var(--a-radius-ctl);
background: var(--bg-page); font: inherit; color: var(--text-primary);
}
.search input::placeholder { color: var(--text-secondary); }
.search input:focus-visible { outline-offset: 0; border-radius: var(--a-radius-ctl); background: var(--bg-surface); }
.filter-note { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); color: var(--text-secondary); }
.filter-note[hidden] { display: none; }
.filter-note b { color: var(--text-primary); font-weight: var(--fw-bold); }
.text-btn { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 var(--sp-2); border: 0; border-radius: var(--a-radius-ctl); background: none; cursor: pointer; color: var(--text-primary); }
.text-btn:hover { background: var(--bg-hover); }
/* 项目列表 */
.projects { width: calc(100% + 24px); margin: var(--sp-5) -12px 0; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
.projects th { padding: 0 12px var(--sp-2); text-align: left; font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: var(--fw-regular); color: var(--text-secondary); white-space: nowrap; }
.projects td { padding: var(--sp-3) 12px; vertical-align: middle; border-top: 1px solid var(--a-row-rule); }
.projects tbody tr:first-child td { border-top-color: transparent; }
.projects tbody tr.row { cursor: pointer; }
.projects tbody tr.row:hover td { background: var(--bg-hover); }
.projects tbody tr.row:hover td:first-child { border-radius: var(--a-radius-block) 0 0 var(--a-radius-block); }
.projects tbody tr.row:hover td:last-child { border-radius: 0 var(--a-radius-block) var(--a-radius-block) 0; }
.projects tbody tr.row:hover td, .projects tbody tr.row:hover + tr td { border-top-color: transparent; }
.projects .r { text-align: right; }
.c-st { width: 72px; } .c-rd { width: 72px; } .c-ft { width: 72px; } .c-due { width: 128px; }
.projects td:nth-child(2), .projects td:nth-child(3) { white-space: nowrap; }
.projects th .ic { vertical-align: -3px; margin-left: 2px; }
.pname { font-size: var(--fs-title); line-height: var(--lh-title); font-weight: var(--fw-bold); text-decoration: none; }
.row:hover .pname { text-decoration: underline; text-underline-offset: 3px; }
.psub { color: var(--text-secondary); }
.psub span + span { margin-left: var(--sp-3); }
.due { white-space: nowrap; }
.due .d1 { display: block; font-size: 15px; }
.due .d1 .fig { font-size: 16px; }
.due .d2 { display: block; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.due.today .d1, .due.today .d2 { color: var(--warn-text); font-weight: var(--fw-bold); }
.due.soon .d1, .due.soon .d2 { color: var(--warn-text); }
.due.past .d1 { color: var(--text-secondary); }
.dash { color: var(--text-secondary); }
.ready { text-decoration: underline dotted var(--text-secondary); text-underline-offset: 4px; cursor: help; position: relative; }
.ready:hover::after, .ready:focus-visible::after {
content: attr(data-tip); position: absolute; left: -8px; top: calc(100% + 8px); z-index: 5; width: 232px; padding: var(--sp-2) var(--sp-3);
border-radius: var(--a-radius-ctl); background: var(--c-ink); color: var(--text-on-dark); font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: var(--fw-regular);
box-shadow: var(--shadow-overlay); white-space: normal; text-align: left;
}
.empty { padding: var(--sp-10) 12px; text-align: center; color: var(--text-secondary); }
.empty b { display: block; margin-bottom: var(--sp-1); font-size: var(--fs-title); line-height: var(--lh-title); color: var(--text-primary); font-weight: var(--fw-bold); }
.note { margin: var(--sp-4) 0 0; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
/* 问题级别标签(形状规则不变) */
.sev { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: 4px; font-size: var(--fs-caption); line-height: 20px; font-weight: var(--fw-bold); white-space: nowrap; }
.sev.fatal { background: var(--sev-fatal); color: var(--text-on-dark); }
.sev .fig { font-size: 13px; }
/* 待处理的致命问题 */
.sec-h { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-12); }
.sec-h h2 { margin: 0; font: var(--fw-bold) var(--fs-h2)/var(--lh-h2) var(--font-serif); }
.sec-h .more { margin-left: auto; }
.issues { list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.issues li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--sp-4); align-items: baseline; padding: var(--sp-3) 0; }
.issues li + li { margin-top: var(--sp-2); }
.issues .sev { position: relative; top: -2px; }
.issues b { display: block; font-size: 15px; line-height: 24px; font-weight: var(--fw-bold); }
.issues p { margin: 2px 0 0; color: var(--text-secondary); }
.cite { display: inline-flex; align-items: center; gap: 6px; color: var(--ai); text-decoration: none; white-space: nowrap; }
.cite:hover { color: var(--ai-strong); text-decoration: underline; text-underline-offset: 3px; }
.cite .fig { font-size: 15px; }
/* ---------- 右栏:直接放在校样纸上 ---------- */
.rail { padding-top: var(--sp-10); }
.rail section + section { margin-top: var(--sp-12); }
.rail h3 { display: flex; align-items: baseline; justify-content: space-between; margin: 0 0 var(--sp-4); font-size: var(--fs-body); line-height: var(--lh-body); font-weight: var(--fw-bold); }
.rail h3 small { font-size: var(--fs-caption); font-weight: var(--fw-regular); color: var(--text-secondary); }
.rail h3 small .fig { font-size: 13px; }
.task-h { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2); }
.task-h span, .task-p { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.progress { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-2); }
.track { flex: 1; height: 4px; border-radius: 2px; background: var(--c-sunken); overflow: hidden; }
.track i { display: block; height: 100%; background: var(--c-ink); border-radius: 2px; }
.progress .fig { font-size: 15px; font-weight: var(--fw-bold); min-width: 34px; text-align: right; }
.queue { display: flex; gap: var(--sp-2); margin-top: var(--sp-4); color: var(--text-secondary); }
.queue .ic { margin-top: 3px; }
.rail .hint { margin: var(--sp-3) 0 0; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.quota { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-6); }
.quota small { display: block; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.quota .big { font: var(--fw-bold) var(--fs-figure)/var(--lh-figure) var(--font-figures); font-variant-numeric: tabular-nums; }
.quota .unit { margin-left: 4px; font-family: var(--font-figures); font-size: 15px; color: var(--text-secondary); }
.quota .track { margin-top: var(--sp-2); }
.rail .links { display: flex; gap: var(--sp-5); margin-top: var(--sp-4); font-size: var(--fs-caption); line-height: var(--lh-caption); }
.log { list-style: none; margin: 0; padding: 0; }
.log li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-3); padding: 6px 0; }
.log .t { font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-size: 13px; color: var(--text-secondary); white-space: nowrap; }
.log .who { font-weight: var(--fw-bold); }
.log .ok { color: var(--confirmed); white-space: nowrap; }
/* 轻提示 */
.toasts { position: fixed; left: 50%; bottom: var(--sp-8); transform: translateX(-50%); display: grid; gap: var(--sp-2); z-index: 20; }
.toast { padding: 10px var(--sp-4); border-radius: var(--a-radius-block); background: var(--c-ink); color: var(--text-on-dark); box-shadow: var(--shadow-overlay); }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
</style>
</head>
<body>
<div class="app">
<aside class="side">
<a class="brand" href="../page-workbench.html"><img src="../../logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">
<a href="#" 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>
<a class="demo" href="../index.html"><svg class="ic" 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>UI demo 目录</a>
</nav>
<div class="side-foot"><b>王敏</b>南京某信息技术有限公司<br>专业版,至 2027-10-31</div>
</aside>
<main class="main">
<header class="top">
<a class="quota-chip" href="#quota" title="本期剩余额度">解析<span class="fig">37</span>检查<span class="fig">12</span></a>
<button class="icon-btn" aria-label="通知(有 1 条未读)" title="通知" 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" aria-label="快捷键说明" title="快捷键说明(?)" 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>
</header>
<div class="layout">
<article class="sheet">
<div class="head">
<h1>工作台<span class="date">11月3日 周二</span></h1>
<div class="actions">
<span class="cost">扣 1 次解析,剩 <span class="fig">37</span> 次</span>
<a class="btn primary" href="../forms.html#s-new"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>新建项目</a>
</div>
</div>
<p class="lede" data-lede>
<span class="half"><button class="n" data-f="active" aria-pressed="false"><span data-to="4">4</span><span class="vh">(只看进行中的项目)</span></button> 个项目进行中,<button class="n due" data-f="due7" aria-pressed="false"><span data-to="2">2</span><span class="vh">(只看 7 天内截止的项目)</span></button> 个 7 天内截止;</span>
<span class="half"><button class="n fatal" data-f="fatal" aria-pressed="false"><span data-to="3">3</span><span class="vh">(只看有致命问题的项目)</span></button> 个致命问题待处理,<button class="n" data-f="pend" aria-pressed="false"><span data-to="4">4</span><span class="vh">(只看有待确认清单条目的项目)</span></button> 条清单待确认。</span>
</p>
<p class="lede-sub">另有 1 个已递交、1 个已归档。致命问题都在 1 个项目里,待确认的清单条目在 2 个项目里。</p>
<div class="alert" role="note">
<svg class="ic lg" 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>
<a class="btn quiet" href="../page-reading.html">去确认清单</a>
</div>
<div class="bar">
<div class="tabs" role="group" aria-label="按状态筛选" data-tabs>
<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>
<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="搜索项目" data-q></label>
</div>
<div class="filter-note" data-fnote hidden><span>正在看:<b data-fname></b></span><button class="text-btn" data-clear><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>清除筛选</button></div>
<table class="projects">
<thead><tr><th scope="col">项目</th><th scope="col" class="c-st">状态</th><th scope="col" class="c-rd">就绪</th><th scope="col" class="c-ft">致命</th><th scope="col" class="c-due r" aria-sort="ascending">投标截止<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg></th></tr></thead>
<tbody data-rows></tbody>
</table>
<p class="note">按投标截止时间排序,截止临近的排在前面。<span id="ready-note">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</span></p>
<section id="fatal" aria-labelledby="fatal-h">
<div class="sec-h"><h2 id="fatal-h">待处理的致命问题</h2><span class="sev fatal">致命<span class="fig">3</span></span><a class="more lnk" href="#" data-demo>去查标页处理</a></div>
<ul class="issues">
<li><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><p>智慧大厅建设项目。开标一览表 287.60 万元,限价 286.00 万元。</p></div><a class="cite" href="#" data-cite>报价册 · 第 <span class="fig">1</span> 页</a></li>
<li><span class="sev fatal">致命</span><div><b>★ 条款疑似不满足:应用服务器内存</b><p>智慧大厅建设项目。响应 256G,要求 ≥ 512G。上次标记为已修改,问题仍在。</p></div><a class="cite" href="#" data-cite>技术册 · 第 <span class="fig">12</span> 页</a></li>
<li><span class="sev fatal">致命</span><div><b>授权委托书的单位签章位置未发现盖章</b><p>智慧大厅建设项目。只看有无,不鉴定真伪。</p></div><a class="cite" href="#" data-cite>商务册 · 第 <span class="fig">5</span> 页</a></li>
</ul>
</section>
</article>
<aside class="rail" aria-label="任务、额度与最近处理">
<section>
<h3>正在进行的任务</h3>
<div class="task-h"><b>解析补遗 2</b><span>提取清单</span></div>
<div class="task-p">某市医保信息系统等保测评服务</div>
<div class="progress"><div class="track" 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"><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>检查排队中:智慧社区平台运维,前面还有 1 个任务</span></div>
<p class="hint">可以离开本页,完成后会发站内通知。</p>
</section>
<section id="quota">
<h3>本期额度<small>至 <span class="fig">2027-10-31</span></small></h3>
<div class="quota">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="track" 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="track" aria-hidden="true"><i style="width:40%"></i></div></div>
</div>
<div class="links"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section>
<h3>最近处理<a class="lnk" href="#" data-demo style="font-size:var(--fs-caption);font-weight:400">全部记录</a></h3>
<ul class="log">
<li><span><span class="who">王敏</span> <span class="ok">✓ 已修改</span>「授权委托书签章」</span><span class="t">14:26</span></li>
<li><span><span class="who">王敏</span> 确认了清单 v2:智慧大厅</span><span class="t">11-02</span></li>
<li><span><span class="who">系统</span> 完成第 2 次检查,致命 3</span><span class="t">11-02</span></li>
<li><span><span class="who">系统</span> 并入补遗 1,更新 2 条</span><span class="t">11-02</span></li>
<li><span><span class="who">李工</span> 忽略「其他项目名称」:业绩介绍</span><span class="t">11-01</span></li>
</ul>
</section>
</aside>
</div>
</main>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
const fig = (s) => String(s).replace(/(\d[\d:.]*)/g, '<span class="fig">$1</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(), 4000);
}
/* 数据沿用 page-workbench.html;pend = 有待确认的清单条目(4 条在 2 个项目里) */
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, d1: "今天 17:00", d2: "剩 6 小时", cls: "today", st: "编制中", ready: "", fatal: 0, pend: 1, href: "../page-reading.html" },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", h: 53, d1: "11月5日 16:00", d2: "剩 2 天 5 小时", cls: "soon", st: "已检查", ready: "就绪", fatal: 0 },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, d1: "11月20日 09:30", d2: "剩 17 天", cls: "", st: "已检查", ready: "未就绪", fatal: 3, href: "../page-reading.html" },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", h: 600, d1: "11月28日 10:00", d2: "剩 25 天", cls: "", st: "跟进中", ready: "", fatal: 0, pend: 1 },
{ name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, d1: "已截止", d2: "", cls: "past", st: "已递交", ready: "", fatal: 0 },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, d1: "已截止", d2: "", cls: "past", st: "已归档", ready: "", fatal: 0 }
];
const DONE = ["已递交", "已归档"];
const LEDE = {
active: ["进行中的项目", (p) => !DONE.includes(p.st)],
due7: ["7 天内截止的项目", (p) => p.h >= 0 && p.h <= 168],
fatal: ["有致命问题待处理的项目", (p) => p.fatal > 0],
pend: ["有清单条目待确认的项目", (p) => p.pend > 0]
};
let seg = "active", lede = "", q = "";
function match(p) {
const bySeg = lede ? LEDE[lede][1](p) : seg === "all" || (seg === "active" ? !DONE.includes(p.st) : p.st === seg);
return bySeg && (!q || (p.name + p.buyer).includes(q));
}
function render() {
const rows = P.filter(match);
$("[data-rows]").innerHTML = rows.map((p, i) => `<tr class="row" data-i="${P.indexOf(p)}">
<td><a class="pname" href="${p.href || "#"}">${p.name}</a><div class="psub"><span>${p.buyer}</span><span>${fig(p.lot)}</span></div></td>
<td>${p.st}</td>
<td>${p.ready === "就绪" ? '<span class="ready" tabindex="0" aria-describedby="ready-note" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>' : p.ready || '<span class="dash" aria-label="无">—</span>'}</td>
<td>${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</td>
<td class="r due ${p.cls}"><span class="d1">${fig(p.d1)}</span>${p.d2 ? `<span class="d2">${p.d2}</span>` : ""}</td></tr>`).join("")
|| `<tr><td colspan="5"><div class="empty"><b>没有符合条件的项目</b>换个关键词,或切到「全部」看看。</div></td></tr>`;
}
function setSeg(v) {
seg = v; lede = "";
$$("[data-tabs] button").forEach((b) => b.setAttribute("aria-pressed", b.dataset.v === v));
$$(".n").forEach((b) => b.setAttribute("aria-pressed", "false"));
$("[data-fnote]").hidden = true;
render();
}
function setLede(f) {
if (lede === f) { setSeg("active"); return; }
lede = f;
$$("[data-tabs] button").forEach((b) => b.setAttribute("aria-pressed", "false"));
$$(".n").forEach((b) => b.setAttribute("aria-pressed", b.dataset.f === f));
$("[data-fname]").textContent = LEDE[f][0];
$("[data-fnote]").hidden = false;
render();
}
$("[data-tabs]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (b) setSeg(b.dataset.v); });
$$(".n").forEach((b) => b.addEventListener("click", () => setLede(b.dataset.f)));
$("[data-clear]").addEventListener("click", () => { setSeg("active"); $("[data-tabs] button").focus(); });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-rows]").addEventListener("click", (e) => {
const r = e.target.closest("tr.row"); if (!r) return;
const p = P[r.dataset.i];
if (!p.href) { e.preventDefault(); toast("(演示)只做了「读标」组合页"); return; }
if (!e.target.closest("a")) location.href = p.href;
});
document.addEventListener("click", (e) => {
const a = e.target.closest("[data-demo], [data-cite]"); if (!a) return;
e.preventDefault();
toast(a.matches("[data-cite]") ? "(演示)出处会在原文查看器里打开" : "(演示)这一页还没做");
});
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
$("[data-keys]").onclick = () => toast("(演示)快捷键:J / K 逐条,1 / 2 / 3 处理,? 显示说明");
render();
/* 唯一的动效:概要句数字载入时从 0 数到实际值一次 */
if (!matchMedia("(prefers-reduced-motion: reduce)").matches) {
const els = $$("[data-to]"), t0 = performance.now(), dur = 700;
const step = (t) => {
const k = Math.min(1, (t - t0) / dur), e = 1 - Math.pow(1 - k, 3);
els.forEach((el) => { el.textContent = Math.round(+el.dataset.to * e); });
if (k < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,469 @@
<!doctype html>
<!--
工作台 · 风格方案 B「工作面板」(探索稿,不是定稿)
数据、文案、交互沿用 ../page-workbench.html。
本方案相对 design-system.md 改了哪些规则:
1. §5.1 圆角:2px 一档 → 分层圆角。面板 10、列表行 8、按钮 / 输入框 / 分段控件 6、标签 4。仍不用胶囊形。
2. §5.1 层次:「靠分隔线和底色」→ 只靠底色。画布白、侧边栏和面板用校样纸色,面板里的行是白底,行与行留 3px 缝,不画分隔线。
3. §3.1 页面底色:校样纸 → 白;校样纸改作侧边栏和面板底色。
4. §7.1 次按钮:白底 + 控件边框 → 浅墨实底、无边框。只有输入框保留控件边框(WCAG 1.4.11)。
5. §7.9 状态标签:中性描边 → 浅灰实底、无描边;「就绪」同样改为实底。
6. §7.9 项目行:「状态」列改为「进度」列,用四段状态轨(跟进中 → 编制中 → 已检查 → 已递交)加当前状态标签;已归档整条变浅。
7. §7.9 倒计时:一行写完 → 两行,上行写剩余时间(≤ 72 小时赭色),下行写截止时刻。
8. §7.7 表头:凹陷灰底 + 2px 墨色底线 → 去掉,只留辅助灰列名。
9. 工作台统计:四张一样的卡片 → 一个面板里四列,数字 32px,每列可点击筛选;致命数字用致命红,截止数字用赭。
10. §4.2 字阶:H1 24/32 → 28/36;统计数字 30/36 → 32/40。其余不变。
11. §7.2 级别标签:圆角 2 → 4,形状规则(致命实心 / 重要描边 / 提示无框)不变。
12. 顶栏:面包屑 → 居中的全局搜索(Ctrl K);额度从文字改为小进度条 + 数字。
新增的派生色全部用 color-mix() 从 tokens.css 的现有色得到,集中声明在 <style> 顶部,没有新色相。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 方案 B 工作面板</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="../../system/tokens.css">
<link rel="stylesheet" href="../../system/fonts-local.css">
<style>
/* ---------- 本方案派生变量(全部从 tokens.css 现有色派生,不引入新色相) ---------- */
:root{
--wb-canvas: var(--c-white); /* 应用画布 */
--wb-side: color-mix(in srgb, var(--c-paper) 55%, var(--c-sunken)); /* 侧边栏:比校样纸略深 */
--wb-panel: var(--c-paper); /* 内容分区面板 */
--wb-fill: color-mix(in srgb, var(--c-ink) 6%, var(--c-white)); /* 次按钮、图标按钮悬停 */
--wb-fill-hover: color-mix(in srgb, var(--c-ink) 11%, var(--c-white)); /* 次按钮悬停 */
--wb-track: color-mix(in srgb, var(--c-ink) 7%, var(--c-paper)); /* 分段控件底、面板内的凹槽 */
--wb-row-hover: color-mix(in srgb, var(--c-ink) 5%, var(--c-white)); /* 列表行悬停 */
--wb-stat-hover: color-mix(in srgb, var(--c-white) 55%, var(--c-paper));/* 统计列悬停 */
--wb-tag: var(--c-sunken); /* 状态标签实底 */
--wb-rail-off: color-mix(in srgb, var(--c-ink) 12%, var(--c-white)); /* 状态轨:未走到的段 */
--wb-rail-on: var(--c-ink); /* 状态轨:已走过 / 当前段 */
--wb-rail-arch: color-mix(in srgb, var(--c-ink) 32%, var(--c-white)); /* 状态轨:已归档整条变浅 */
--wb-primary-hover: color-mix(in srgb, var(--c-ink) 88%, var(--c-white));/* 主按钮悬停:变浅 */
--wb-r-panel: 10px; --wb-r-row: 8px; --wb-r-ctl: 6px; --wb-r-tag: 4px;
--wb-fs-h1: 28px; --wb-lh-h1: 36px;
--wb-fs-stat: 32px; --wb-lh-stat: 40px;
--wb-top-h: 60px;
--wb-rail-w: 296px;
--wb-track-w: 156px;
}
@media (max-width: 1399px){ :root{ --wb-rail-w: 260px; --wb-track-w: 132px; } }
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
body{background:var(--wb-canvas);color:var(--text-primary);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);-webkit-font-smoothing:antialiased;min-width:1280px}
h1,h2,h3,p,ul{margin:0}
ul{padding:0;list-style:none}
a{color:inherit}
button,input{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
.fig{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold)}
.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}
.sec{color:var(--text-secondary)}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
/* ---------- 框架 ---------- */
.app{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr);min-height:100vh}
.side{background:var(--wb-side)}
.side-in{display:flex;flex-direction:column;padding:20px 12px 16px;position:sticky;top:0;height:100vh}
.side .brand{display:block;padding:0 8px 24px}
.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(--wb-r-ctl);text-decoration:none;color:var(--text-secondary)}
.nav a:hover{background:var(--wb-track);color:var(--text-primary)}
.nav a[aria-current="page"]{background:var(--c-white);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:12px 10px 0;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.side-foot b{display:block;font-size:var(--fs-body);line-height:var(--lh-body);color:var(--text-primary);font-weight:var(--fw-bold)}
.main{min-width:0;display:flex;flex-direction:column}
.top{height:var(--wb-top-h);display:grid;grid-template-columns:minmax(max-content,1fr) minmax(280px,540px) minmax(max-content,1fr);align-items:center;gap:24px;padding:0 24px 0 32px;position:sticky;top:0;z-index:20;background:var(--wb-canvas)}
.top .date{color:var(--text-secondary);white-space:nowrap}
.gsearch{position:relative;display:flex;align-items:center}
.gsearch .ic{position:absolute;left:12px;color:var(--text-secondary);pointer-events:none}
.gsearch input{width:100%;height:36px;padding:0 64px 0 38px;border:1px solid var(--border-control);border-radius:var(--wb-r-ctl);background:var(--c-white)}
.gsearch input::placeholder{color:var(--text-secondary)}
.gsearch input:focus-visible{outline:2px solid var(--focus-ring);outline-offset:1px;border-color:var(--c-ink)}
.kbd{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:var(--wb-r-tag);background:var(--wb-tag);color:var(--text-secondary);font:var(--fw-regular) var(--fs-caption)/1 var(--font-sans)}
.gsearch .kbd{position:absolute;right:8px;pointer-events:none}
.top-r{display:flex;align-items:center;justify-content:flex-end;gap:4px}
.qchip{display:flex;align-items:center;gap:16px;height:36px;padding:0 10px;border-radius:var(--wb-r-ctl);text-decoration:none}
.qchip:hover{background:var(--wb-fill)}
.qchip .q{display:flex;align-items:center;gap:8px;color:var(--text-secondary);white-space:nowrap}
.qchip .q .fig{color:var(--text-primary);font-size:16px;min-width:18px}
.mbar{width:40px;height:4px;border-radius:2px;background:var(--wb-rail-off);overflow:hidden}
.mbar i{display:block;height:100%;background:var(--c-ink)}
.ibtn{position:relative;display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:0;border-radius:var(--wb-r-ctl);background:none;cursor:pointer;color:var(--text-primary)}
.ibtn:hover{background:var(--wb-fill)}
.ibtn .dot{position:absolute;top:8px;right:9px;width:6px;height:6px;border-radius:50%;background:var(--c-ink);box-shadow:0 0 0 2px var(--wb-canvas)}
.content{width:100%;max-width:calc(var(--content-max) + 64px);padding:12px 32px 48px}
/* ---------- 页头 ---------- */
.ph{display:flex;align-items:flex-end;gap:24px;margin-bottom:20px}
.ph h1{font:var(--fw-bold) var(--wb-fs-h1)/var(--wb-lh-h1) var(--font-serif);letter-spacing:.02em}
.ph p{margin-top:4px;color:var(--text-secondary)}
.ph .act{margin-left:auto;display:flex;align-items:center;gap:12px}
/* ---------- 按钮 ---------- */
.btn{display:inline-flex;align-items:center;gap:6px;height:34px;padding:0 14px;border:0;border-radius:var(--wb-r-ctl);background:var(--wb-fill);color:var(--text-primary);text-decoration:none;white-space:nowrap;cursor:pointer;transition:background var(--dur-fast) var(--ease)}
.btn:hover{background:var(--wb-fill-hover)}
.btn.primary{background:var(--c-ink);color:var(--text-on-dark);font-weight:var(--fw-bold)}
.btn.primary:hover{background:var(--wb-primary-hover)}
.btn.on-tint{background:var(--c-white)}
.btn.on-tint:hover{background:var(--wb-fill)}
.cost{color:var(--text-secondary);font-size:var(--fs-caption)}
.cost .fig{color:var(--text-primary);font-size:14px}
.lnk{color:var(--text-primary);text-underline-offset:3px}
.lnk:hover{text-decoration-thickness:2px}
/* ---------- 提醒 ---------- */
.alert{display:flex;align-items:center;gap:12px;padding:12px 12px 12px 16px;border-radius:var(--wb-r-panel);background:var(--warn-soft);margin-bottom:16px}
.alert > .ic{color:var(--warn)}
.alert p{flex:1;min-width:0}
.alert b{font-weight:var(--fw-bold)}
/* ---------- 面板 ---------- */
.panel{background:var(--wb-panel);border-radius:var(--wb-r-panel);padding:6px}
.panel-h{display:flex;align-items:center;gap:12px;min-height:44px;padding:4px 10px 6px 12px}
.panel-h h2,.panel-h h3{font:var(--fw-bold) var(--fs-title)/var(--lh-title) var(--font-sans)}
.panel-h .more{margin-left:auto}
.panel-f{padding:8px 12px 6px}
/* ---------- 统计 ---------- */
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin-bottom:16px}
.stat{display:block;text-align:left;border:0;background:none;border-radius:var(--wb-r-row);padding:12px 16px 14px;cursor:pointer;transition:background var(--dur-fast) var(--ease)}
.stat:hover{background:var(--wb-stat-hover)}
.stat[aria-pressed="true"]{background:var(--c-white)}
.stat small{display:flex;align-items:center;gap:6px;font-size:var(--fs-body);line-height:var(--lh-body);color:var(--text-secondary)}
.stat .big{display:block;margin:6px 0 2px;font:var(--fw-bold) var(--wb-fs-stat)/var(--wb-lh-stat) var(--font-figures);font-variant-numeric:tabular-nums;color:var(--text-primary)}
.stat .big.fatal{color:var(--sev-fatal)}
.stat .big.due{color:var(--warn)}
.stat p{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.stat p.due{color:var(--warn-text);font-weight:var(--fw-bold)}
/* ---------- 主体两栏 ---------- */
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--wb-rail-w);gap:16px;align-items:start}
.col{display:flex;flex-direction:column;gap:16px;min-width:0}
/* 分段控件:整体浅底,选中项白底,无描边 */
.seg{display:inline-flex;gap:2px;padding:3px;border-radius:calc(var(--wb-r-ctl) + 3px);background:var(--wb-track)}
.seg button{display:flex;align-items:center;gap:6px;height:28px;padding:0 12px;border:0;border-radius:var(--wb-r-ctl);background:none;color:var(--text-secondary);cursor:pointer;white-space:nowrap;transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.seg button:hover{color:var(--text-primary)}
.seg button[aria-pressed="true"]{background:var(--c-white);color:var(--text-primary);font-weight:var(--fw-bold)}
.seg .fig{font-weight:var(--fw-bold)}
.chip{display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 6px 0 10px;border:0;border-radius:var(--wb-r-ctl);background:var(--c-ink);color:var(--text-on-dark);cursor:pointer;white-space:nowrap}
.chip:hover{background:var(--wb-primary-hover)}
.lsearch{position:relative;margin-left:auto;display:flex;align-items:center}
.lsearch .ic{position:absolute;left:10px;color:var(--text-secondary);pointer-events:none}
.lsearch input{width:220px;height:34px;padding:0 10px 0 32px;border:1px solid var(--border-control);border-radius:var(--wb-r-ctl);background:var(--c-white)}
.lsearch input::placeholder{color:var(--text-secondary)}
input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
/* 嵌入式列表 */
.grid-cols{display:grid;grid-template-columns:minmax(0,1fr) 120px var(--wb-track-w) 56px 64px 16px;gap:16px;align-items:center}
@media (max-width: 1399px){ .grid-cols{grid-template-columns:minmax(0,1fr) 112px var(--wb-track-w) 52px 60px 16px;gap:12px} }
.cols{padding:6px 14px 6px 16px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.cols .r,.row .r{text-align:right;justify-self:end}
.rows{display:flex;flex-direction:column;gap:3px}
.row{padding:12px 14px 12px 16px;min-height:64px;background:var(--c-white);border-radius:var(--wb-r-row);text-decoration:none;color:inherit;cursor:pointer;transition:background var(--dur-fast) var(--ease)}
.row:hover{background:var(--wb-row-hover)}
.row:focus-visible{outline-offset:-2px}
.row .go{color:var(--text-secondary);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.row:hover .go,.row:focus-visible .go{opacity:1}
.pname{display:block;font-weight:var(--fw-bold);font-size:15px;line-height:22px}
.psub{display:block;margin-top:2px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.due b{display:block;font-weight:var(--fw-regular)}
.due b .fig{font-size:15px}
.due small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.due.today b,.due.soon b{color:var(--warn-text)}
.due.today b{font-weight:var(--fw-bold)}
.due.past b{color:var(--text-secondary)}
/* 状态轨:四段 = 跟进中 → 编制中 → 已检查 → 已递交 */
.track{position:relative;width:var(--wb-track-w);padding-bottom:22px}
.track .segs{display:grid;grid-template-columns:repeat(4,1fr);gap:3px}
.track .segs i{height:6px;border-radius:2px;background:var(--wb-rail-off)}
.track .segs i.on{background:var(--wb-rail-on)}
.track .lab{position:absolute;top:12px;white-space:nowrap}
.track.arch .segs i{background:var(--wb-rail-arch)}
.tag{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:var(--wb-r-tag);background:var(--wb-tag);color:var(--text-primary);font-size:var(--fs-caption);line-height:1;white-space:nowrap}
.track.arch .tag{color:var(--text-secondary)}
/* 问题级别标签:致命实心 / 重要描边 / 提示无框 */
.sev{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 6px;border-radius:var(--wb-r-tag);font-size:var(--fs-caption);line-height:1;font-weight:var(--fw-bold);white-space:nowrap}
.sev.fatal{background:var(--sev-fatal);color:var(--text-on-dark)}
.sev .fig{font-size:13px}
.dash{color:var(--text-secondary)}
.empty{padding:28px 16px;background:var(--c-white);border-radius:var(--wb-r-row);text-align:center}
.empty b{display:block;font-weight:var(--fw-bold)}
.empty p{margin-top:4px;color:var(--text-secondary)}
/* 待处理的致命问题 */
.todo{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:baseline;padding:12px 16px 12px 20px;background:var(--c-white);border-radius:var(--wb-r-row)}
.todo::before{content:"";position:absolute;left:7px;top:12px;bottom:12px;width:3px;border-radius:2px;background:var(--sev-fatal)}
.todo b{display:block;font-weight:var(--fw-bold)}
.cite{display:inline-flex;align-items:center;gap:6px;color:var(--ai);text-decoration:none;white-space:nowrap}
.cite:hover{color:var(--ai-strong);text-decoration:underline;text-underline-offset:3px}
/* 右栏 */
.rail .panel{padding:6px 6px 8px}
.rail .body{padding:4px 12px 4px}
.task-h{display:flex;align-items:baseline;gap:8px}
.task-h b{font-weight:var(--fw-bold)}
.task-h span{margin-left:auto}
.pbar{display:flex;align-items:center;gap:10px;margin-top:8px}
.pbar .bar{flex:1;height:6px;border-radius:3px;background:var(--wb-rail-off);overflow:hidden}
.pbar .bar i{display:block;height:100%;background:var(--c-ink);border-radius:3px}
.pbar .fig{font-size:15px;min-width:34px;text-align:right}
.queue{display:flex;gap:8px;align-items:flex-start;margin-top:12px;padding:10px 12px;border-radius:var(--wb-r-row);background:var(--c-white)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.rnote{margin-top:10px}
.q2{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.q2 > div{padding:10px 12px 12px;background:var(--c-white);border-radius:var(--wb-r-row)}
.q2 small{display:block;color:var(--text-secondary)}
.q2 .big{font:var(--fw-bold) 26px/34px var(--font-figures);font-variant-numeric:tabular-nums}
.q2 .unit{margin-left:4px;color:var(--text-secondary);font-family:var(--font-figures);font-size:14px}
.q2 .mbar{width:100%;margin-top:6px}
.links{display:flex;justify-content:space-between;padding:10px 12px 2px}
.log{display:flex;flex-direction:column;gap:2px}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:6px 12px;border-radius:var(--wb-r-row);font-size:var(--fs-caption);line-height:var(--lh-caption)}
.log li > span:last-child{color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-size:13px}
.log .who{font-weight:var(--fw-bold)}
.log .ok{color:var(--confirmed);white-space:nowrap}
/* 悬停说明(浮层,可用阴影) */
[data-tip]{position:relative}
[data-tip]:hover::after,[data-tip]:focus-visible::after{content:attr(data-tip);position:absolute;z-index:30;top:calc(100% + 6px);left:0;width:max-content;max-width:260px;padding:6px 10px;border-radius:var(--wb-r-ctl);background:var(--c-ink);color:var(--text-on-dark);font:var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-sans);box-shadow:var(--shadow-overlay);pointer-events:none;white-space:normal}
[data-tip].tip-r:hover::after,[data-tip].tip-r:focus-visible::after{left:auto;right:0}
.toasts{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:60;display:flex;flex-direction:column;gap:8px}
.toast{padding:10px 16px;border-radius:var(--wb-r-ctl);background:var(--c-ink);color:var(--text-on-dark);box-shadow:var(--shadow-overlay)}
</style>
</head>
<body>
<div class="app">
<aside class="side"><div class="side-in">
<a class="brand" href="workbench-b.html"><img src="../../logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">
<a href="workbench-b.html" aria-current="page"><i data-ic="grid" class="lg"></i>工作台</a>
<a href="#" data-demo><i data-ic="cert" class="lg"></i>资质库</a>
<a href="#" data-demo><i data-ic="gauge" class="lg"></i>额度</a>
<div class="grp">UI demo</div>
<a href="../index.html"><i data-ic="book" class="lg"></i>demo 目录</a>
</nav>
<div class="side-foot"><b>王敏</b>南京某信息技术有限公司<br>专业版 · 至 2027-10-31</div>
</div></aside>
<div class="main">
<header class="top">
<span class="date">11月3日 周二</span>
<form class="gsearch" role="search" data-gsearch>
<i data-ic="search"></i>
<input type="search" placeholder="搜索项目、文件、条款" aria-label="全局搜索:项目、文件、条款" aria-keyshortcuts="Control+K" data-gq>
<span class="kbd" aria-hidden="true">Ctrl K</span>
</form>
<div class="top-r">
<a class="qchip" href="#" data-demo data-tip="本期剩余额度:解析 37 / 50 次,检查 12 / 20 次" aria-label="本期剩余额度:解析 37 次,检查 12 次">
<span class="q">解析<span class="mbar" aria-hidden="true"><i style="width:26%"></i></span><span class="fig">37</span></span>
<span class="q">检查<span class="mbar" aria-hidden="true"><i style="width:40%"></i></span><span class="fig">12</span></span>
</a>
<button class="ibtn tip-r" aria-label="通知(1 条未读)" data-tip="通知" data-notice><i data-ic="bell" class="lg"></i><span class="dot"></span></button>
</div>
</header>
<main class="content">
<div class="ph">
<div><h1>工作台</h1><p>按投标截止时间排序,截止临近的排在前面。</p></div>
<div class="act">
<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" role="note">
<i data-ic="clock" class="lg"></i>
<p><b>某县数据中心机房改造项目今天 17:00 截止。</b>清单还是草稿,还没有做过检查。</p>
<a class="btn on-tint" href="../page-reading.html">去确认清单</a>
</div>
<section class="panel stats" aria-label="项目统计,点击可筛选下方项目列表" data-stats>
<button class="stat" data-f="active" aria-pressed="false"><small>进行中的项目</small><span class="big">4</span><p>已递交 1 · 已归档 1</p></button>
<button class="stat" data-f="due7" aria-pressed="false"><small><i data-ic="clock"></i>7 天内截止</small><span class="big due">2</span><p class="due">最近:今天 17:00</p></button>
<button class="stat" data-f="fatal" aria-pressed="false"><small>待处理的致命问题</small><span class="big fatal">3</span><p>在 1 个项目里</p></button>
<button class="stat" data-f="pend" aria-pressed="false"><small>待确认的清单条目</small><span class="big">4</span><p>在 2 个项目里</p></button>
</section>
<div class="wb">
<div class="col">
<section class="panel" aria-labelledby="h-proj">
<div class="panel-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 data-chip></span>
<label class="lsearch"><i data-ic="search"></i><input type="search" placeholder="搜索项目、采购人" aria-label="在项目列表中搜索" data-q></label>
</div>
<div class="cols grid-cols" aria-hidden="true"><span>项目</span><span>投标截止</span><span>进度</span><span>就绪</span><span class="r">致命</span><span></span></div>
<ul class="rows" data-rows aria-label="项目列表"></ul>
<p class="panel-f cap sec">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
</section>
<section class="panel" aria-labelledby="h-todo">
<div class="panel-h">
<h2 id="h-todo">待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" href="#" data-demo>去查标页处理</a>
</div>
<ul class="rows">
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite" href="#" data-demo><i data-ic="file"></i>报价册 · 第 <span class="fig">1</span> 页</a></li>
<li class="todo"><span class="sev fatal">致命</span><div><b>★ 条款疑似不满足:应用服务器内存</b><span class="psub">智慧大厅建设项目 · 响应 256G,要求 ≥ 512G · 上次标记为已修改,问题仍在</span></div><a class="cite" href="#" data-demo><i data-ic="file"></i>技术册 · 第 <span class="fig">12</span> 页</a></li>
<li class="todo"><span class="sev fatal">致命</span><div><b>授权委托书的单位签章位置未发现盖章</b><span class="psub">智慧大厅建设项目 · 只看有无,不鉴定真伪</span></div><a class="cite" href="#" data-demo><i data-ic="file"></i>商务册 · 第 <span class="fig">5</span> 页</a></li>
</ul>
</section>
</div>
<div class="col rail">
<section class="panel" aria-labelledby="h-task">
<div class="panel-h"><h3 id="h-task">正在进行的任务</h3></div>
<div class="body">
<div class="task-h"><b>解析补遗 2</b><span class="cap sec">提取清单</span></div>
<div class="cap sec">某市医保信息系统等保测评服务</div>
<div class="pbar"><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 sec">检查排队中 · 智慧社区平台运维 · 前面还有 1 个任务</span></div>
<p class="rnote cap sec">可以离开本页,完成后会发站内通知。</p>
</div>
</section>
<section class="panel" aria-labelledby="h-quota">
<div class="panel-h"><h3 id="h-quota">本期额度</h3><span class="more cap sec">至 2027-10-31</span></div>
<div class="q2">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="mbar" 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="mbar" aria-hidden="true"><i style="width:40%"></i></div></div>
</div>
<div class="links"><a class="lnk cap" href="#" data-demo>扣点规则</a><a class="lnk cap" href="#" data-demo>查看流水</a></div>
</section>
<section class="panel" aria-labelledby="h-log">
<div class="panel-h"><h3 id="h-log">最近处理</h3><a class="more lnk cap" href="#" data-demo>全部记录</a></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>
</ul>
</section>
</div>
</div>
</main>
</div>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '<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"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
bell: '<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"/>',
book: '<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"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
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"/>',
"chev-right": '<path d="m9 18 6-6-6-6"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
$$("i[data-ic]").forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
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(), 4000);
}
/* 数据沿用 page-workbench.html;due 拆成两行,pend 为待确认清单条目数(对应统计「在 2 个项目里」) */
const STEPS = ["跟进中", "编制中", "已检查", "已递交"];
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 <span class=fig>17:00</span> 截止", "剩 <span class=fig>6</span> 小时"], cls: "today", st: "编制中", ready: "", fatal: 0, pend: 3, href: "../page-reading.html" },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", h: 53, due: ["剩 <span class=fig>2</span> 天 <span class=fig>5</span> 小时", "11月5日 16:00"], cls: "soon", st: "已检查", ready: "就绪", fatal: 0, pend: 0 },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, due: ["剩 <span class=fig>17</span> 天", "11月20日 09:30"], cls: "", st: "已检查", ready: "未就绪", fatal: 3, pend: 0, href: "../page-reading.html" },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", h: 600, due: ["剩 <span class=fig>25</span> 天", "11月28日 10:00"], cls: "", st: "跟进中", ready: "", fatal: 0, pend: 1 },
{ name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, due: ["已截止", ""], cls: "past", st: "已递交", ready: "", fatal: 0, pend: 0 },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", ""], cls: "past", st: "已归档", ready: "", fatal: 0, pend: 0 }
];
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 = "";
function track(p) {
const arch = p.st === "已归档";
const idx = arch ? 3 : STEPS.indexOf(p.st);
const segs = STEPS.map((s, i) => `<i class="${i <= idx ? "on" : ""}"></i>`).join("");
/* 当前状态标签对齐到它那一段的起点;最后一段右对齐 */
const pos = arch || idx === 3 ? "right:0" : `left:calc(${idx} * (100% + 3px) / 4)`;
const sr = arch ? "进度:已归档(四步都已走完)" : `进度:${p.st},第 ${idx + 1} 步,共 4 步(${STEPS.join("、")})`;
return `<span class="track ${arch ? "arch" : ""}"><span class="vh">${sr}</span><span class="segs" aria-hidden="true">${segs}</span><span class="lab" style="${pos}" aria-hidden="true"><span class="tag">${p.st}</span></span></span>`;
}
function render() {
const rows = P.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) => `<li><a class="row grid-cols" href="${p.href || "#"}" ${p.href ? "" : "data-demo"}>
<span><span class="pname">${p.name}</span><span class="psub">${p.buyer} · ${p.lot}</span></span>
<span class="due ${p.cls}"><b>${p.due[0]}</b>${p.due[1] ? `<small>${p.due[1]}</small>` : ""}</span>
${track(p)}
<span>${p.ready === "就绪" ? '<span class="tag" data-tip="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span>' : p.ready ? p.ready : '<span class="dash" aria-label="不适用">—</span>'}</span>
<span class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span class="go">${icon("chev-right")}</span></a></li>`).join("")
|| `<li class="empty"><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 && stat !== "active" ? `<button class="chip" data-clear aria-label="清除筛选:${STAT[stat].label}">${STAT[stat].label}${icon("x")}</button>` : "";
}
$("[data-seg]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; seg = b.dataset.v; 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(); } });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
document.addEventListener("click", (e) => { const d = e.target.closest("[data-demo]"); if (d) { e.preventDefault(); toast("(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页"); } });
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
$("[data-gsearch]").addEventListener("submit", (e) => { e.preventDefault(); toast("(演示)全局搜索还没有接入,项目可以在下方列表里搜索"); });
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); $("[data-gq]").focus(); }
if (e.key === "Escape" && document.activeElement === $("[data-gq]")) $("[data-gq]").blur();
});
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,481 @@
<!doctype html>
<html lang="zh-CN">
<!--
工作台 · 风格方案 C「截止线」(探索稿,不是定稿)
思路:工作台要回答的是「下一个截止的是谁、它准备好了没有」,所以页面以时间为主轴。
顶部一条墨色时间带画出未来 30 天的截止点,项目列表每行以日期块开头。
本方案相对 design-system.md 改了这些规则:
1. 圆角(§5.1):控件、标签 2px → 控件 6px、级别标签 4px、面板 10px、时间带 12px。仍不用胶囊形。
2. 按钮高度(§7.1):默认 32 → 36。次按钮不画描边,用凹陷底色;分段筛选不画外框,选中项墨底白字。
3. 层次(§5.1「靠分隔线和底色」):区块之间改为只靠间距和白色面板,不用描边和阴影;表格去掉凹陷表头和竖线,行间用极淡分隔线。
4. 颜色(§3.4「不用大面积彩色底」):新增一块大面积墨色底(时间带)。墨不是彩色,但这是规范里没有的用法。
时间带上的文字用白 / 校样纸,次要文字用派生的浅灰(对比 ≥ 7:1);赭色只做图形和带白字的色块(白字压赭 4.8:1),不做小字;
致命在深色底上一律用「红底白字」实心标签。
5. 字阶(§4.2):工作台标题 24 → 28/36;项目名 16 宋体 → 17/26 宋体(规范 §2 说宋体只用于 ≥20px,此处是例外,需在 Windows 上实测);
日期块日数字 Barlow 28;时间带小结数字 Barlow 24。
6. 状态(§7.9):状态不再用中性描边标签,改成文字 + 5 格流程刻度(跟进中 → 已归档);「就绪」改为文字 + 墨点,不再描边。
7. 统计(§4.2 Figure 级):去掉四张统计卡,并入时间带底部一行小结。
8. 提醒条(§7.12):「今天截止」提醒并入时间带头部,不再单独用赭浅底提醒条。
9. 动效:时间带标记载入时依次落位一次(只此一处),减少动态效果时关闭。
-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 标博士</title>
<link rel="icon" href="../../logo/svg/favicon-16.svg">
<link rel="stylesheet" href="../../system/tokens.css">
<link rel="stylesheet" href="../../system/fonts-local.css">
<style>
/* ---------- 本页派生变量:全部从 tokens.css 已有色值混出,不引入新色相 ---------- */
:root {
--band: var(--c-ink); /* 时间带底色:墨 */
--band-raise: color-mix(in srgb, var(--c-white) 11%, var(--c-ink)); /* 时间带上的普通截止标记底 */
--on-band: var(--c-paper); /* 时间带正文:校样纸,约 13:1 */
--on-band-sub: color-mix(in srgb, var(--c-paper) 72%, var(--c-ink)); /* 时间带次要文字,约 7.8:1 */
--band-axis: color-mix(in srgb, var(--c-paper) 48%, var(--c-ink)); /* 时间轴主线(图形,约 4:1) */
--band-tick: color-mix(in srgb, var(--c-paper) 24%, var(--c-ink)); /* 日刻度、分隔(装饰) */
--band-today: color-mix(in srgb, var(--c-paper) 9%, var(--c-ink)); /* 「今天」那一格的底 */
--row-rule: color-mix(in srgb, var(--c-rule) 55%, var(--c-white)); /* 列表行之间的极淡分隔线(装饰) */
--btn-quiet: var(--c-sunken); /* 次按钮底色(沿用凹陷灰) */
--r-ctl: 6px; /* 控件 */
--r-tag: 4px; /* 级别标签、状态小标签 */
--r-panel: 10px; /* 白色面板 */
--r-band: 12px; /* 时间带 */
--ctl-h: 36px;
--rail-w: clamp(296px, 24vw, 336px);
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; min-width: 1280px; background: var(--bg-page); color: var(--text-primary);
font: var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);
display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); min-height: 100vh;
}
h1, h2, h3, p, ul { margin: 0; }
ul { padding: 0; list-style: none; }
button, input { font: inherit; color: inherit; }
a { color: inherit; }
.fig { font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-weight: var(--fw-bold); }
.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; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.band :focus-visible { outline-color: var(--c-white); }
/* ---------- 侧边栏 ---------- */
.sidebar { position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; padding: var(--sp-6) var(--sp-4) var(--sp-5); }
.sidebar .brand { display: block; padding: 0 var(--sp-2) var(--sp-8); }
.sidebar .brand img { height: 30px; display: block; }
.nav { display: flex; flex-direction: column; gap: var(--sp-1); }
.nav a { display: flex; align-items: center; gap: var(--sp-3); height: 40px; padding: 0 var(--sp-3); border-radius: var(--r-ctl); text-decoration: none; color: var(--text-secondary); }
.nav a:hover { background: var(--bg-hover); color: var(--text-primary); }
.nav a[aria-current] { background: var(--bg-surface); color: var(--text-primary); font-weight: var(--fw-bold); }
.sidebar-foot { margin-top: auto; padding: var(--sp-3); font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.sidebar-foot b { display: block; color: var(--text-primary); font-size: var(--fs-body); line-height: var(--lh-body); }
/* ---------- 主区 ---------- */
main { padding: var(--sp-8) var(--sp-10) var(--sp-16); min-width: 0; }
.page-h { display: flex; align-items: flex-end; gap: var(--sp-4); margin-bottom: var(--sp-6); }
.page-h h1 { font: var(--fw-bold) 28px/36px var(--font-serif); letter-spacing: .02em; }
.page-h p { margin-top: var(--sp-1); color: var(--text-secondary); }
.page-h .acts { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); }
.cost { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.cost .fig { color: var(--text-primary); font-size: 14px; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); height: var(--ctl-h); padding: 0 var(--sp-4); border: 0; border-radius: var(--r-ctl); background: var(--btn-quiet); color: var(--text-primary); font-weight: var(--fw-bold); text-decoration: none; cursor: pointer; white-space: nowrap; transition: background var(--dur-fast) var(--ease); }
.btn:hover { background: color-mix(in srgb, var(--c-ink) 8%, var(--c-sunken)); }
.btn.primary { background: var(--c-ink); color: var(--c-white); }
.btn.primary:hover { background: color-mix(in srgb, var(--c-white) 8%, var(--c-ink)); }
.btn.on-band { background: var(--c-paper); color: var(--c-ink); }
.btn.on-band:hover { background: var(--c-white); }
.icon-btn { display: inline-grid; place-items: center; width: var(--ctl-h); height: var(--ctl-h); border: 0; border-radius: var(--r-ctl); background: transparent; color: var(--text-primary); cursor: pointer; position: relative; }
.icon-btn:hover { background: var(--bg-hover); }
.icon-btn .dot { position: absolute; top: 8px; right: 8px; width: 7px; height: 7px; border-radius: 50%; background: var(--c-ochre); box-shadow: 0 0 0 2px var(--bg-page); }
/* 级别标签:致命实心 / 重要描边 / 提示无框(形状规则不变,只是圆角 4) */
.sev { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: var(--r-tag); font-size: 12px; line-height: 20px; font-weight: var(--fw-bold); white-space: nowrap; }
.sev.fatal { background: var(--sev-fatal); color: var(--c-white); }
.sev .fig { font-size: 13px; }
/* ---------- 时间带 ---------- */
.band { background: var(--band); color: var(--on-band); border-radius: var(--r-band); padding: var(--sp-6) var(--sp-8) 0; }
.band-h { display: flex; align-items: center; gap: var(--sp-4); }
.band-h .clock { width: 20px; height: 20px; color: var(--c-ochre); flex: none; }
.band-h .msg b { display: block; font-size: var(--fs-title); line-height: var(--lh-title); font-weight: var(--fw-bold); color: var(--c-white); }
.band-h .msg span { color: var(--on-band-sub); }
.band-h .btn { margin-left: auto; }
.tl { position: relative; height: 212px; margin-top: var(--sp-6); }
.tl-today { position: absolute; top: 0; bottom: 36px; background: var(--band-today); border-radius: 4px 4px 0 0; }
.tl-axis { position: absolute; left: 0; right: 0; bottom: 36px; height: 1px; background: var(--band-axis); }
.tl-72 { position: absolute; left: 0; bottom: 35px; height: 3px; background: var(--c-ochre); border-radius: 0 2px 2px 0; }
.tl-tick { position: absolute; bottom: 36px; width: 1px; height: 6px; background: var(--band-tick); }
.tl-tick.wk { height: 10px; background: var(--band-axis); }
.tl-lab { position: absolute; bottom: 8px; font-size: 12px; line-height: 20px; color: var(--on-band-sub); white-space: nowrap; transform: translateX(-50%); }
.tl-lab.today { transform: none; color: var(--c-white); font-weight: var(--fw-bold); }
.tl-lab.r { transform: translateX(-100%); }
.mk { position: absolute; display: block; text-decoration: none; color: var(--c-white); }
.mk .flag { display: block; padding: 6px 10px 6px 10px; border-radius: 6px; background: var(--band-raise); white-space: nowrap; }
.mk .flag b { display: block; font-size: 13px; line-height: 20px; font-weight: var(--fw-bold); }
.mk .flag small { display: block; font-size: 12px; line-height: 18px; color: var(--on-band-sub); }
.mk.soon .flag { background: var(--c-ochre); } /* 72 小时内:赭色块 + 白字(4.8:1) */
.mk.soon .flag small { color: var(--c-white); }
.mk .hang { display: flex; gap: 6px; margin-top: 4px; }
.mk .ready { display: inline-flex; align-items: center; height: 20px; padding: 0 6px; border-radius: var(--r-tag); box-shadow: inset 0 0 0 1px var(--on-band-sub); font-size: 12px; font-weight: var(--fw-bold); color: var(--on-band); }
.mk:hover .flag { box-shadow: 0 0 0 1px var(--c-paper); }
.mk.flip .hang { justify-content: flex-end; }
.stem { position: absolute; width: 1px; background: var(--band-axis); }
.stem.soon { width: 2px; background: var(--c-ochre); }
.pin { position: absolute; width: 9px; height: 9px; margin: -4px 0 0 -4px; border-radius: 50%; background: var(--c-paper); }
.pin.soon { background: var(--c-ochre); box-shadow: 0 0 0 2px var(--band); }
.band-sum { display: flex; gap: var(--sp-8); padding: var(--sp-4) 0 var(--sp-5); margin-top: var(--sp-2); border-top: 1px solid var(--band-tick); }
.band-sum > div { display: flex; align-items: center; gap: var(--sp-3); white-space: nowrap; }
.band-sum .fig { font-size: 30px; line-height: 36px; color: var(--c-white); }
.band-sum span { display: block; color: var(--on-band); line-height: 20px; }
.band-sum small { display: block; font-size: 12px; line-height: 18px; color: var(--on-band-sub); }
.band-sum .key { margin-left: auto; align-self: center; font-size: 12px; color: var(--on-band-sub); white-space: nowrap; display: flex; align-items: center; gap: var(--sp-2); }
.band-sum .key i { width: 16px; height: 3px; border-radius: 2px; background: var(--c-ochre); display: inline-block; }
/* ---------- 下半部分 ---------- */
.wb { display: grid; grid-template-columns: minmax(0, 1fr) var(--rail-w); gap: var(--sp-8); align-items: start; margin-top: var(--sp-8); }
.toolbar { display: flex; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-3); }
.toolbar h2 { font: var(--fw-bold) 20px/28px var(--font-serif); margin-right: var(--sp-2); }
.seg { display: flex; gap: 2px; }
.seg button { height: 32px; padding: 0 var(--sp-3); border: 0; border-radius: var(--r-ctl); background: transparent; color: var(--text-secondary); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
.seg button:hover { background: var(--bg-hover); color: var(--text-primary); }
.seg button[aria-pressed="true"] { background: var(--c-ink); color: var(--c-white); font-weight: var(--fw-bold); }
.seg .fig { font-weight: var(--fw-bold); }
.search { margin-left: auto; position: relative; width: 248px; }
.search .ic { position: absolute; left: 12px; top: 10px; color: var(--text-secondary); pointer-events: none; }
.search input { width: 100%; height: var(--ctl-h); padding: 0 12px 0 36px; border: 1px solid var(--border-control); border-radius: var(--r-ctl); background: var(--bg-surface); }
.search input::placeholder { color: var(--text-secondary); }
.search input:focus-visible { outline-offset: 1px; }
.list { container: list / inline-size; background: var(--bg-surface); border-radius: var(--r-panel); padding: var(--sp-2) 0; }
.row { display: grid; grid-template-columns: 84px minmax(0, 1fr) 96px 80px; gap: var(--sp-4); align-items: center; padding: var(--sp-4) var(--sp-6) var(--sp-4) var(--sp-5); text-decoration: none; color: inherit; position: relative; cursor: pointer; }
.row + .row::before { content: ""; position: absolute; top: 0; left: calc(var(--sp-5) + 84px + var(--sp-4)); right: var(--sp-6); height: 1px; background: var(--row-rule); }
.row:hover { background: var(--bg-hover); }
.row:hover::before, .row:hover + .row::before { opacity: 0; }
.row:focus-visible { outline-offset: -2px; border-radius: var(--r-ctl); }
.date { text-align: center; line-height: 1; }
.date .m { display: block; font-size: 12px; line-height: 16px; color: var(--text-secondary); }
.date .d { display: block; font-size: 28px; line-height: 32px; margin: 1px 0 2px; }
.date .cd { display: block; font-size: 12px; line-height: 16px; color: var(--text-secondary); white-space: nowrap; }
.row.today .date .d { color: var(--c-ochre); }
.row.today .date .cd, .row.soon .date .cd { color: var(--c-ochre-ink); }
.row.today .date .cd { font-weight: var(--fw-bold); }
.row.past .date .d { color: var(--text-secondary); }
.row .name { display: block; font: var(--fw-bold) 17px/26px var(--font-serif); color: var(--text-primary); text-wrap: pretty; }
.row .sub { font-size: 13px; line-height: 20px; color: var(--text-secondary); margin-top: 2px; }
.row .sub .at { display: none; }
@container list (min-width: 720px) { .row .sub .at { display: inline; } }
.row .when { font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-weight: var(--fw-bold); }
.stage { font-size: 13px; line-height: 20px; }
.pips { display: flex; gap: 3px; margin-top: 6px; }
.pips i { width: 14px; height: 3px; border-radius: 2px; background: var(--c-rule); }
.pips i.on { background: var(--c-ink); }
.ready { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; font-size: 13px; line-height: 20px; }
.ready .ok { display: inline-flex; align-items: center; gap: 6px; font-weight: var(--fw-bold); }
.ready .ok::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--c-ink); }
.ready .no { color: var(--text-secondary); }
.empty { padding: var(--sp-10) var(--sp-6); text-align: center; }
.empty b { display: block; font-size: var(--fs-title); }
.empty p { color: var(--text-secondary); margin-top: var(--sp-1); }
.note { margin-top: var(--sp-3); font-size: 12px; line-height: 20px; color: var(--text-secondary); padding-left: var(--sp-5); }
/* 右侧栏:白色面板,只靠间距分隔 */
.rail { display: flex; flex-direction: column; gap: var(--sp-4); }
.panel { background: var(--bg-surface); border-radius: var(--r-panel); padding: var(--sp-5); }
.panel-h { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.panel-h h3 { font-size: var(--fs-title); line-height: var(--lh-title); font-weight: var(--fw-bold); }
.panel-h .more { margin-left: auto; font-size: 12px; line-height: 20px; color: var(--text-secondary); }
.lnk { color: var(--text-primary); text-decoration: underline; text-decoration-color: var(--c-muted); text-underline-offset: 3px; font-size: 12px; }
.lnk:hover { text-decoration-color: var(--c-ink); }
.cite { display: inline-flex; align-items: center; gap: 4px; color: var(--ai); text-decoration: none; font-size: 12px; line-height: 20px; white-space: nowrap; }
.cite:hover { color: var(--ai-strong); text-decoration: underline; text-underline-offset: 3px; }
.cite .fig { font-size: 13px; }
.todo li { position: relative; padding: var(--sp-2) 0 var(--sp-2) var(--sp-4); }
.todo li + li { margin-top: var(--sp-3); }
.todo li::before { content: ""; position: absolute; left: 0; top: var(--sp-2); bottom: var(--sp-2); width: 3px; border-radius: 2px; background: var(--sev-fatal); }
.todo b { display: block; font-weight: var(--fw-bold); }
.todo p { font-size: 12px; line-height: 20px; color: var(--text-secondary); margin-top: 2px; }
.todo .cite { margin-top: 2px; }
.quota { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); }
.quota small { display: block; font-size: 12px; line-height: 20px; color: var(--text-secondary); }
.quota .big { font-size: 30px; line-height: 36px; }
.quota .unit { font-family: var(--font-figures); font-variant-numeric: tabular-nums; color: var(--text-secondary); margin-left: 4px; }
.bar { height: 4px; border-radius: 2px; background: var(--c-sunken); margin-top: var(--sp-2); overflow: hidden; }
.bar i { display: block; height: 100%; background: var(--c-ink); border-radius: 2px; }
.panel-f { display: flex; gap: var(--sp-4); margin-top: var(--sp-4); }
.task-h { display: flex; align-items: baseline; gap: var(--sp-2); }
.task-h b { font-weight: var(--fw-bold); }
.task-h span { margin-left: auto; font-size: 12px; color: var(--text-secondary); }
.task .cap { font-size: 12px; line-height: 20px; color: var(--text-secondary); }
.prog { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-2); }
.prog .bar { flex: 1; margin: 0; }
.prog .fig { font-size: 14px; }
.queued { display: flex; gap: var(--sp-2); margin-top: var(--sp-4); font-size: 12px; line-height: 20px; color: var(--text-secondary); }
.queued .ic { margin-top: 2px; }
.task .leave { margin-top: var(--sp-3); font-size: 12px; line-height: 20px; color: var(--text-secondary); }
.log li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-3); padding: 6px 0; font-size: 13px; line-height: 20px; }
.log .who { font-weight: var(--fw-bold); }
.log .ok { color: var(--confirmed); white-space: nowrap; }
.log time { font-family: var(--font-figures); font-variant-numeric: tabular-nums; color: var(--text-secondary); font-size: 13px; }
/* 轻提示 */
.toasts { position: fixed; left: 50%; bottom: var(--sp-8); transform: translateX(-50%); display: flex; flex-direction: column; gap: var(--sp-2); z-index: 10; }
.toast { background: var(--c-ink); color: var(--c-white); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-ctl); box-shadow: var(--shadow-overlay); }
/* 唯一的动效:时间带标记依次落位 */
@keyframes drop { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }
@keyframes grow { from { transform: scaleY(0); } to { transform: none; } }
.tl.animate .mk { animation: drop 420ms var(--ease) both; animation-delay: calc(var(--i) * 110ms + 120ms); }
.tl.animate .stem { transform-origin: bottom; animation: grow 320ms var(--ease) both; animation-delay: calc(var(--i) * 110ms); }
@media (prefers-reduced-motion: reduce) {
.tl.animate .mk, .tl.animate .stem { animation: none; }
}
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-grid" viewBox="0 0 24 24"><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"/></symbol>
<symbol id="i-cert" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/></symbol>
<symbol id="i-gauge" viewBox="0 0 24 24"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></symbol>
<symbol id="i-bell" viewBox="0 0 24 24"><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"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></symbol>
<symbol id="i-file" viewBox="0 0 24 24"><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"/></symbol>
</svg>
<aside class="sidebar">
<a class="brand" href="#"><img src="../../logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">
<a href="#" aria-current="page"><svg class="ic lg"><use href="#i-grid"/></svg>工作台</a>
<a href="#" data-demo><svg class="ic lg"><use href="#i-cert"/></svg>资质库</a>
<a href="#" data-demo><svg class="ic lg"><use href="#i-gauge"/></svg>额度</a>
</nav>
<div class="sidebar-foot"><b>王敏</b>南京某信息技术有限公司<br>专业版,至 2027-10-31</div>
</aside>
<main>
<header class="page-h">
<div>
<h1>工作台</h1>
<p>11月3日 周二。按投标截止时间排序,截止临近的排在前面。</p>
</div>
<div class="acts">
<span class="cost">扣 1 次解析,剩 <span class="fig">37</span> 次</span>
<button class="btn primary" data-new><svg class="ic"><use href="#i-plus"/></svg>新建项目</button>
<button class="icon-btn" aria-label="通知" title="通知" data-notice><svg class="ic lg"><use href="#i-bell"/></svg><span class="dot"></span></button>
</div>
</header>
<section class="band" aria-labelledby="band-t">
<h2 class="sr" id="band-t">未来 30 天的投标截止</h2>
<div class="band-h">
<svg class="ic clock" aria-hidden="true"><use href="#i-clock"/></svg>
<div class="msg"><b>某县数据中心机房改造项目今天 17:00 截止。</b><span>清单还是草稿,还没有做过检查。</span></div>
<a class="btn on-band" href="../page-reading.html">去确认清单</a>
</div>
<div class="tl" data-tl aria-label="截止时间轴" role="group"></div>
<div class="band-sum">
<div><span class="fig">4</span><p><span>进行中的项目</span><small>已递交 1,已归档 1</small></p></div>
<div><span class="fig">2</span><p><span>7 天内截止</span><small>最近:今天 17:00</small></p></div>
<div><span class="fig">3</span><p><span>待处理的致命问题</span><small>在 1 个项目里</small></p></div>
<div><span class="fig">4</span><p><span>待确认的清单条目</span><small>在 2 个项目里</small></p></div>
<span class="key"><i></i>72 小时内截止</span>
</div>
</section>
<div class="wb">
<section aria-labelledby="list-t">
<div class="toolbar">
<h2 id="list-t">项目</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>
<label class="search"><svg class="ic" aria-hidden="true"><use href="#i-search"/></svg><input type="search" placeholder="搜索项目、采购人、编号" aria-label="搜索项目" data-q></label>
</div>
<div class="list" data-rows></div>
<p class="note" id="ready-note">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
</section>
<aside class="rail" aria-label="待办与额度">
<section class="panel" aria-labelledby="todo-t">
<div class="panel-h"><h3 id="todo-t">待处理的致命问题</h3><span class="sev fatal">致命 <span class="fig">3</span></span><span class="more"><a class="lnk" href="#" data-demo>去查标页处理</a></span></div>
<ul class="todo">
<li><b>报价超过采购包 1 的最高限价</b><p>智慧大厅建设项目:开标一览表 287.60 万元,限价 286.00 万元</p><a class="cite" href="#" data-demo><svg class="ic"><use href="#i-file"/></svg>报价册 · 第 <span class="fig">1</span> 页</a></li>
<li><b>★ 条款疑似不满足:应用服务器内存</b><p>智慧大厅建设项目:响应 256G,要求 ≥ 512G。上次标记为已修改,问题仍在</p><a class="cite" href="#" data-demo><svg class="ic"><use href="#i-file"/></svg>技术册 · 第 <span class="fig">12</span> 页</a></li>
<li><b>授权委托书的单位签章位置未发现盖章</b><p>智慧大厅建设项目:只看有无,不鉴定真伪</p><a class="cite" href="#" data-demo><svg class="ic"><use href="#i-file"/></svg>商务册 · 第 <span class="fig">5</span> 页</a></li>
</ul>
</section>
<section class="panel" aria-labelledby="quota-t">
<div class="panel-h"><h3 id="quota-t">本期额度</h3><span class="more">至 2027-10-31</span></div>
<div class="quota">
<div><small>解析剩余</small><span class="fig big">37</span><span class="unit">/ 50</span><div class="bar" role="img" aria-label="已用 13 次,共 50 次"><i style="width:26%"></i></div></div>
<div><small>检查剩余</small><span class="fig big">12</span><span class="unit">/ 20</span><div class="bar" role="img" aria-label="已用 8 次,共 20 次"><i style="width:40%"></i></div></div>
</div>
<div class="panel-f"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="panel task" aria-labelledby="task-t">
<div class="panel-h"><h3 id="task-t">正在进行的任务</h3></div>
<div class="task-h"><b>解析补遗 2</b><span>提取清单</span></div>
<div class="cap">某市医保信息系统等保测评服务</div>
<div class="prog"><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="queued"><svg class="ic" aria-hidden="true"><use href="#i-clock"/></svg><span>检查排队中:智慧社区平台运维,前面还有 1 个任务</span></div>
<p class="leave">可以离开本页,完成后会发站内通知。</p>
</section>
<section class="panel" aria-labelledby="log-t">
<div class="panel-h"><h3 id="log-t">最近处理</h3><span class="more"><a class="lnk" href="#" data-demo>全部记录</a></span></div>
<ul class="log">
<li><span><span class="who">王敏</span> <span class="ok">✓ 已修改</span>「授权委托书签章」</span><time>14:26</time></li>
<li><span><span class="who">王敏</span> 确认了清单 v2:智慧大厅</span><time>11-02</time></li>
<li><span><span class="who">系统</span> 完成第 2 次检查,致命 3</span><time>11-02</time></li>
<li><span><span class="who">系统</span> 并入补遗 1,更新 2 条</span><time>11-02</time></li>
<li><span><span class="who">李工</span> 忽略「其他项目名称」:业绩介绍</span><time>11-01</time></li>
</ul>
</section>
</aside>
</div>
</main>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
const STAGES = ["跟进中", "编制中", "已检查", "已递交", "已归档"];
/* 演示数据:现在 = 11月3日 11:00;h = 距投标截止的小时数 */
const P = [
{ id: "p1", name: "某县数据中心机房改造项目", short: "数据中心机房改造", buyer: "某县大数据管理局", lot: "标段 1", h: 6, m: 11, d: 3, cd: "今天 17:00 截止", st: "编制中", ready: "", fatal: 0, href: "../page-reading.html" },
{ id: "p2", name: "某区智慧社区平台运维服务", short: "智慧社区平台运维", buyer: "某区民政局", lot: "全部", h: 53, m: 11, d: 5, cd: "剩 2 天 5 小时", st: "已检查", ready: "就绪", fatal: 0 },
{ id: "p3", name: "某市政务服务中心智慧大厅建设项目", short: "智慧大厅建设", buyer: "某市政务服务管理办公室", lot: "采购包 1", h: 408, m: 11, d: 20, cd: "09:30 · 剩 17 天", st: "已检查", ready: "未就绪", fatal: 3, href: "../page-reading.html" },
{ id: "p4", name: "某市医保信息系统等保测评服务", short: "医保等保测评", buyer: "某市医疗保障局", lot: "全部", h: 600, m: 11, d: 28, cd: "10:00 · 剩 25 天", st: "跟进中", ready: "", fatal: 0 },
{ id: "p5", name: "某区智慧校园一期", short: "智慧校园一期", buyer: "某区教育局", lot: "全部", h: -1, m: 10, d: 30, cd: "已截止", st: "已递交", ready: "", fatal: 0 },
{ id: "p6", name: "某市不动产登记信息平台升级", short: "不动产登记平台", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, m: 10, d: 21, cd: "已截止", st: "已归档", ready: "", fatal: 0 }
];
const SPAN = 30 * 24; // 时间轴:未来 30 天
const TO_MIDNIGHT = 13; // 现在到 11月4日 0:00 的小时数
/* ---------- 时间轴 ---------- */
const tl = $("[data-tl]");
function drawTimeline(animate) {
const W = tl.clientWidth, AXIS = tl.clientHeight - 36;
const x = (h) => Math.round(h / SPAN * W);
let html = `<div class="tl-today" style="left:0;width:${x(TO_MIDNIGHT)}px"></div><div class="tl-axis"></div><div class="tl-72" style="width:${x(72)}px"></div>`;
for (let k = 0; TO_MIDNIGHT + 24 * k < SPAN; k++) {
const hx = x(TO_MIDNIGHT + 24 * k), wk = k % 7 === 6;
html += `<span class="tl-tick ${wk ? "wk" : ""}" style="left:${hx}px"></span>`;
if (wk) { const dt = new Date(2026, 10, 4 + k); html += `<span class="tl-lab" style="left:${hx}px">${dt.getMonth() + 1}月${dt.getDate()}日</span>`; }
}
html += `<span class="tl-lab today" style="left:0">今天</span>`;
tl.innerHTML = html;
// 标记:先放进去量宽度,再分泳道,避免文字互相压住
const live = P.filter((p) => p.h > 0 && p.h <= SPAN).sort((a, b) => a.h - b.h);
const lanes = [0, 76]; // 两条泳道的顶部位置
const laneEnd = [-Infinity, -Infinity];
live.forEach((p, i) => {
const soon = p.h <= 72, px = x(p.h);
const a = document.createElement("a");
a.className = `mk ${soon ? "soon" : ""}`;
a.href = `#${p.id}`;
a.style.setProperty("--i", i);
const due = soon ? p.cd : `${p.m}月${p.d}日 ${p.cd.split(" · ")[0]}`;
a.setAttribute("aria-label", `${p.name},${p.h <= 72 ? p.cd : `${p.m}月${p.d}日 ${p.cd.replace(" · ", ",")}`}${p.fatal ? `,致命问题 ${p.fatal} 项待处理` : ""}${p.ready === "就绪" ? ",就绪" : ""}`);
const hang = [p.fatal ? `<span class="sev fatal">致命 <span class="fig">${p.fatal}</span></span>` : "", p.ready === "就绪" ? `<span class="ready">就绪</span>` : ""].join("");
a.innerHTML = `<span class="flag"><b>${p.short}</b><small>${due}</small></span>${hang ? `<span class="hang">${hang}</span>` : ""}`;
tl.appendChild(a);
const w = a.offsetWidth;
const flip = px + w > W;
const left = flip ? px - w : px, right = flip ? px : px + w;
let lane = laneEnd.findIndex((e) => e + 12 < left);
if (lane < 0) lane = 1;
laneEnd[lane] = right;
if (flip) a.classList.add("flip");
a.style.left = `${left}px`;
a.style.top = `${lanes[lane]}px`;
const stemTop = lanes[lane] + (a.querySelector(".flag").offsetHeight);
const stem = document.createElement("span");
stem.className = `stem ${soon ? "soon" : ""}`;
stem.style.cssText = `left:${px - (soon ? 1 : 0)}px;top:${stemTop}px;height:${AXIS - stemTop}px;--i:${i}`;
const pin = document.createElement("span");
pin.className = `pin ${soon ? "soon" : ""}`;
pin.style.cssText = `left:${px}px;top:${AXIS}px`;
tl.insertBefore(stem, a); tl.appendChild(pin);
});
tl.classList.toggle("animate", !!animate);
}
/* ---------- 项目列表 ---------- */
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) => {
const cls = p.h < 0 ? "past" : p.h <= 24 ? "today" : p.h <= 72 ? "soon" : "";
const si = STAGES.indexOf(p.st);
const ready = p.fatal
? `<span class="sev fatal">致命 <span class="fig">${p.fatal}</span></span><span class="no">未就绪</span>`
: p.ready === "就绪" ? `<span class="ok" title="只表示致命问题都已处理,不代表投标文件没有问题。" aria-describedby="ready-note">就绪</span>` : "";
const cd = p.h > 72 ? `<span class="when">${p.cd.split(" · ")[1]}</span>` : p.cd;
return `<a class="row ${cls}" id="${p.id}" href="${p.href || "#"}" ${p.href ? "" : "data-demo"}>
<span class="date" title="${p.h > 0 ? `投标截止 ${p.m}月${p.d}日${p.h > 72 ? " " + p.cd.split(" · ")[0] : ""}` : "已截止"}"><span class="m">${p.m}月</span><span class="d fig">${p.d}</span><span class="cd">${cd}</span></span>
<span><span class="name">${p.name}</span><span class="sub" style="display:block">${p.buyer},${p.lot}${p.h > 72 ? `<span class="at">,${p.m}月${p.d}日 ${p.cd.split(" · ")[0]} 截止</span>` : ""}</span></span>
<span class="stage">${p.st}<span class="pips" aria-hidden="true">${STAGES.map((_, i) => `<i class="${i <= si ? "on" : ""}"></i>`).join("")}</span></span>
<span class="ready">${ready}</span></a>`;
}).join("") || `<div class="empty"><b>没有符合条件的项目</b><p>换个关键词,或切到「全部」看看。</p></div>`;
}
/* ---------- 轻提示 ---------- */
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(), 4000);
}
$("[data-seg]").addEventListener("click", (e) => {
const b = e.target.closest("button"); if (!b) return;
$$("button", e.currentTarget).forEach((x) => x.setAttribute("aria-pressed", x === b));
seg = b.dataset.v; render();
});
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
document.addEventListener("click", (e) => {
const d = e.target.closest("[data-demo]");
if (d) { e.preventDefault(); toast("(演示)只做了「读标」组合页"); return; }
const mk = e.target.closest(".mk");
if (mk) {
e.preventDefault();
const id = mk.getAttribute("href").slice(1);
if (!$("#" + id)) { seg = "all"; $$("[data-seg] button").forEach((x) => x.setAttribute("aria-pressed", x.dataset.v === "all")); q = ""; $("[data-q]").value = ""; render(); }
const r = $("#" + id); r.scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "center" }); r.focus({ preventScroll: true });
}
});
$("[data-new]").onclick = () => toast("(演示)新建项目");
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
render();
drawTimeline(true);
let rt; addEventListener("resize", () => { clearTimeout(rt); rt = setTimeout(() => drawTimeline(false), 80); });
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,545 @@
<!doctype html>
<!--
工作台 · 风格方案 D「明亮工作区」(探索稿,不是定稿)
数据、文案、交互沿用 ../page-workbench.html 与 ../demo.js。
本方案相对 design-system.md 改了哪些规则:
1. §3.1 页面底色:校样纸 #F5F6F3 → --n-50(墨 3% 混白的中性冷灰);侧边栏、顶栏、卡片纯白。
偏绿的 --c-paper / --c-sunken / --c-hover / --c-rule 一律不用,浅底和分隔线只用下面三个从墨派生的值。
2. §5.1 层次:只有两种容器——页面(--n-50)和白卡片(圆角 12、1px --n-line、无阴影)。卡片里不再套面板。
可点击的整张卡片(本页没有)和浮层才用阴影;行、格的悬停只出 --n-50 底色。
3. §5.1 圆角:2px 一档 → 卡片 12、按钮 / 输入框 / 菜单 8、标签 6。
4. §7.1 按钮:高 32 → 36(主 / 次);表格行内 32。主按钮墨底白字;次按钮白底 + --c-muted 控件边框(3.9:1);
文字按钮悬停出 --n-100 底。普通链接仍是墨色 + 下划线,蓝铅笔只给出处。
5. §7.7 表头:凹陷灰底 + 粗底线 → 无底色,12px 辅助灰列名,无线。行高 64,行间 --n-line 发丝线。
6. §7.9 项目行:
- 「状态」中性描边标签 → --n-100 实底、圆角 6、无描边;全页只有这一种中性标签。
- 「就绪」从描边标签改为墨色普通字 + 虚线下划线(悬停说明不变),和「未就绪」并排写在状态标签后面,省掉一列。
- 「所投包」并入项目名下方的辅助灰行。
- 新增「下一步」列:默认是带箭头的文字链,整行悬停 / 键盘聚焦时变成次按钮,同时浮现「更多」图标按钮。
会扣点的操作(发起检查)放在「更多」里,并写明扣多少、还剩多少;有致命问题时「导出检查报告」仍可用。
7. 工作台统计:四张卡片 → 一张卡片四格,竖向发丝线分隔,数字 30 → 28(Barlow 600)。
每格可点击筛选项目列表,悬停出 --n-50 底,选中格底部 2px 墨色边条。
8. §5.3 顶栏:面包屑位置改为企业主体名;中间加全局搜索(白底 + 控件边框、圆角 8、Ctrl K),本页用它筛选项目列表。
9. §7.12 截止提醒条:赭色浅底条 → 同规格白卡片,赭色图标 + 赭色时间字。
10. §7.4「致命」行左侧 3px 竖线:工作台的待处理列表不用竖线,靠左侧实心「致命」标签扫读。
未改:颜色语义、问题级别形状、字体分工、字阶(正文 14 / 标题 16 / 页标题 24)、4px 间距基准、Lucide 图标。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 方案 D 明亮工作区</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); /* 发丝线、卡片边 */
--ctl-border: var(--c-muted); /* 控件边框 3.9:1 */
--primary-hover: color-mix(in srgb, var(--c-ink) 88%, white);
--r-card: 12px; --r-ctl: 8px; --r-tag: 6px;
--btn-h: 36px; --btn-h-sm: 32px;
--top-h: 56px;
--pad: 32px; /* 页面左右内边距 */
--gap: 16px; /* 卡片之间、主列与右栏之间:全页一个节奏 */
--card-x: 20px; /* 卡片内左右边距:所有卡片统一 */
--rail-w: 296px;
--cols: minmax(0,1fr) 120px 120px 64px 168px; /* 项目行栅格 */
--col-gap: 16px;
}
@media (max-width: 1399px){
:root{ --pad: 24px; --rail-w: 256px; --cols: minmax(0,1fr) 108px 108px 56px 160px; --col-gap: 12px; }
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
body{background:var(--n-50);color:var(--text-primary);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);-webkit-font-smoothing:antialiased;min-width:1280px}
h1,h2,h3,p,ul{margin:0}
ul{padding:0;list-style:none}
a{color:inherit}
button{font:inherit;color:inherit}
.fig{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold)}
.sec{color:var(--text-secondary)}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.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}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px;border-radius:var(--r-ctl)}
/* ---------- 链接与按钮 ---------- */
.lnk{color:var(--text-primary);text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px;transition:text-decoration-color var(--dur-fast) var(--ease)}
.lnk:hover{text-decoration-color:var(--c-ink)}
.cite{display:inline-flex;align-items:center;gap:4px;color:var(--ai);text-decoration:none;white-space:nowrap;border-radius:var(--r-ctl);padding:2px 6px;margin:-2px -6px;transition:background-color var(--dur-fast) var(--ease)}
.cite:hover{background:var(--ai-soft);color:var(--ai-strong)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:var(--btn-h);padding:0 14px;border-radius:var(--r-ctl);border:1px solid var(--ctl-border);background:var(--c-white);color:var(--text-primary);font-size:var(--fs-body);line-height:1;white-space:nowrap;text-decoration:none;cursor:pointer;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(--c-white);font-weight:var(--fw-bold)}
.btn.primary:hover{background:var(--primary-hover)}
.btn.sm{height:var(--btn-h-sm);padding:0 12px}
.tbtn{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;border:0;border-radius:var(--r-ctl);background:transparent;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background-color var(--dur-fast) var(--ease)}
.tbtn:hover{background:var(--n-100)}
.ibtn{position:relative;display:inline-grid;place-items:center;width:var(--btn-h);height:var(--btn-h);border:0;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.ibtn:hover{background:var(--n-100)}
.ibtn.sm{width:var(--btn-h-sm);height:var(--btn-h-sm)}
/* ---------- 标签:中性一种 + 问题级别 ---------- */
.tag{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);white-space:nowrap}
.sev{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 8px;border-radius:var(--r-tag);font-size:var(--fs-caption);font-weight:var(--fw-bold);white-space:nowrap}
.sev.fatal{background:var(--sev-fatal);color:var(--c-white)}
.sev .fig{font-weight:var(--fw-bold)}
.dash{color:var(--text-secondary)}
/* ---------- 应用框架 ---------- */
.app{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr);min-height:100vh}
.side{background:var(--c-white);border-right:1px solid var(--n-line)}
.side-in{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:0 12px 16px}
.brand{display:flex;align-items:center;height:var(--top-h);padding:0 8px;margin-bottom:12px}
.brand img{height:26px;display:block}
.nav{display:flex;flex-direction:column;gap:2px}
.nav a{display:flex;align-items:center;gap:10px;height:var(--btn-h);padding:0 10px;border-radius:var(--r-ctl);text-decoration:none;color:var(--text-secondary);transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.nav a:hover{background:var(--n-50);color:var(--text-primary)}
.nav a[aria-current]{background:var(--n-100);color:var(--text-primary);font-weight:var(--fw-bold)}
.nav .grp{margin:20px 10px 6px;font-size:var(--fs-caption);color:var(--text-secondary)}
.side-foot{margin-top:auto;padding:12px 10px 0;border-top:1px solid var(--n-line);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);font-weight:var(--fw-bold)}
.top{position:sticky;top:0;z-index:5;display:grid;grid-template-columns:1fr minmax(0,400px) 1fr;align-items:center;gap:16px;height:var(--top-h);padding:0 var(--pad);background:var(--c-white);border-bottom:1px solid var(--n-line)}
.org{justify-self:start;display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary)}
.org b{color:var(--text-primary);font-weight:var(--fw-bold)}
.search{position:relative;display:flex;align-items:center;height:var(--btn-h);border:1px solid var(--ctl-border);border-radius:var(--r-ctl);background:var(--c-white);transition:border-color var(--dur-fast) var(--ease)}
.search:hover{border-color:var(--c-ink)}
.search:focus-within{border-color:var(--c-ink);box-shadow:0 0 0 1px var(--c-ink)}
.search > .ic{position:absolute;left:11px;color:var(--text-secondary);pointer-events:none}
.search input{flex:1;min-width:0;height:100%;border:0;background:transparent;padding:0 8px 0 34px;font:inherit;color:var(--text-primary);outline:0}
.search input::placeholder{color:var(--text-secondary)}
.search input::-webkit-search-cancel-button{display:none}
.kbd{flex:none;margin-right:6px;padding:0 6px;height:22px;display:inline-flex;align-items:center;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-secondary);font-size:var(--fs-caption);font-family:var(--font-figures);font-weight:var(--fw-bold)}
.top-r{justify-self:end;display:flex;align-items:center;gap:4px}
.qchip{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;color:var(--text-secondary)}
.qchip .fig{color:var(--text-primary);font-size:15px}
.qchip .sep{width:1px;height:12px;background:var(--n-line);margin:0 4px}
.dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--c-ink);box-shadow:0 0 0 2px var(--c-white)}
.content{padding:28px var(--pad) 48px}
/* ---------- 页头 ---------- */
.ph{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}
.ph h1{font-family:var(--font-serif);font-weight:var(--fw-bold);font-size:var(--fs-h1);line-height:var(--lh-h1)}
.ph p{margin-top:2px;color:var(--text-secondary)}
.ph .act{display:flex;align-items:center;gap:12px}
.cost{font-size:var(--fs-caption);color:var(--text-secondary);white-space:nowrap}
.cost .fig{color:var(--text-primary)}
/* ---------- 唯一的容器:白卡片 ---------- */
.card{background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-card)}
.ch{display:flex;align-items:center;gap:12px;min-height:56px;padding:12px var(--card-x)}
.ch h2,.ch h3{font-size:var(--fs-title);line-height:var(--lh-title);font-weight:var(--fw-bold)}
.ch .more{margin-left:auto}
.cf{padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.stack > * + *{margin-top:var(--gap)}
/* 截止提醒 */
.alert{display:flex;align-items:center;gap:14px;padding:14px var(--card-x);margin-bottom:var(--gap)}
.alert > .ic{color:var(--c-ochre)}
.alert p{flex:1}
.alert .when{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 统计:一张卡片四格 */
.stats{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:var(--gap);overflow:hidden}
.stat{position:relative;display:grid;grid-template-rows:auto auto auto;justify-items:start;gap:2px;padding:16px var(--card-x) 16px;border:0;border-left:1px solid var(--n-line);background:transparent;text-align:left;cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.stat:first-child{border-left:0}
.stat:hover{background:var(--n-50)}
.stat::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--c-ink);transform:scaleY(0);transform-origin:bottom;transition:transform var(--dur-fast) var(--ease)}
.stat[aria-pressed="true"]::after{transform:scaleY(1)}
.stat[aria-pressed="true"] small{color:var(--text-primary);font-weight:var(--fw-bold)}
.stat:focus-visible{outline-offset:-2px;border-radius:0}
.stat small{font-size:var(--fs-body);color:var(--text-secondary)}
.stat .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.stat .big.fatal{color:var(--sev-fatal)}
.stat > .ic{position:absolute;top:18px;right:var(--card-x);color:var(--text-secondary);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.stat:hover > .ic,.stat:focus-visible > .ic,.stat[aria-pressed="true"] > .ic{opacity:1}
.stat[aria-pressed="true"] > .ic{color:var(--text-primary)}
.stat p{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.stat p.due{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 两栏 */
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--rail-w);gap:var(--gap);align-items:start}
/* 项目卡片头:筛选 */
.filters{display:flex;align-items:center;gap:2px;margin-left:8px}
.filters .tbtn{color:var(--text-secondary)}
.filters .tbtn .fig{font-size:15px}
.filters .tbtn[aria-pressed="true"]{background:var(--n-100);color:var(--text-primary);font-weight:var(--fw-bold)}
.clear{margin-left:auto;color:var(--text-primary)}
/* 项目行 */
.grid-cols{display:grid;grid-template-columns:var(--cols);column-gap:var(--col-gap);align-items:center;padding:0 var(--card-x)}
.cols{padding-bottom:8px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.cols .r,.row .r{justify-self:end;text-align:right}
.cols .nx-h{padding-right:48px}
.row{position:relative;min-height:64px;padding-top:10px;padding-bottom:10px;border-top:1px solid var(--n-line);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.row:hover,.row:focus-within{background:var(--n-50)}
.row:has(.pname:focus-visible){outline:2px solid var(--focus-ring);outline-offset:-2px}
.pname{display:block;font-weight:var(--fw-bold);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;outline:0}
.pname::after{content:"";position:absolute;inset:0}
.psub{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.due b{display:block;font-weight:var(--fw-regular);white-space:nowrap}
.due small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap}
.due.today b{color:var(--warn-text);font-weight:var(--fw-bold)}
.due.soon b{color:var(--warn-text)}
.due.past b{color:var(--text-secondary)}
.st{display:flex;align-items:center;gap:8px;white-space:nowrap}
.ready{position:relative;z-index:1;text-decoration:underline dashed;text-decoration-color:color-mix(in srgb, var(--c-ink) 40%, white);text-underline-offset:4px;cursor:help}
.row.arch .pname,.row.arch .due b{color:var(--text-secondary);font-weight:var(--fw-regular)}
/* 下一步:默认文字链,行悬停 / 聚焦变次按钮,同时浮现「更多」 */
.nx-cell{display:flex;align-items:center;justify-content:flex-end;gap:4px}
.nx{position:relative;z-index:1;display:inline-flex;align-items:center;gap:4px;height:var(--btn-h-sm);padding:0 10px 0 12px;border:1px solid transparent;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);white-space:nowrap;cursor:pointer;transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.nx span{text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px}
.nx .ic{color:var(--text-secondary);transition:transform var(--dur-fast) var(--ease)}
.row:hover .nx,.row:focus-within .nx{background:var(--c-white);border-color:var(--ctl-border)}
.row:hover .nx span,.row:focus-within .nx span{text-decoration-color:transparent}
.row .nx:hover{border-color:var(--c-ink)}
.row .nx:hover .ic{transform:translateX(2px);color:var(--text-primary)}
.row .ibtn{position:relative;z-index:1;opacity:0;transition:opacity var(--dur-fast) var(--ease),background-color var(--dur-fast) var(--ease)}
.row:hover .ibtn,.row:focus-within .ibtn,.row .ibtn[aria-expanded="true"]{opacity:1}
.empty{padding:32px var(--card-x);border-top:1px solid var(--n-line);text-align:center}
.empty b{display:block;font-weight:var(--fw-bold)}
.empty p{color:var(--text-secondary);margin-top:4px}
/* 待处理的致命问题 */
.todo{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;column-gap:16px;align-items:center;padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.todo b{display:block;font-weight:var(--fw-bold)}
.todo .psub{white-space:normal}
.todo .cite{margin-right:2px}
/* 右栏 */
.rail .ch{min-height:52px;padding-bottom:4px}
.rbody{padding:4px var(--card-x) 16px}
.task-h{display:flex;align-items:baseline;gap:8px}
.task-h b{font-weight:var(--fw-bold)}
.task-h .cap{margin-left:auto}
.pbar{display:flex;align-items:center;gap:10px;margin-top:10px}
.bar{flex:1;height:6px;border-radius:3px;background:var(--n-100);overflow:hidden}
.bar i{display:block;height:100%;background:var(--c-ink);border-radius:3px}
.queue{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding-top:12px;border-top:1px solid var(--n-line)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.q2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.q2 small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.q2 .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.q2 .unit{margin-left:4px;color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums}
.q2 .bar{margin-top:6px}
.links{display:flex;gap:16px}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:8px 0;border-top:1px solid var(--n-line);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:13px}
.log .who{font-weight:var(--fw-bold)}
.log .ok{color:var(--confirmed);white-space:nowrap}
/* ---------- 浮层:菜单、提示、轻提示 ---------- */
.menu{position:fixed;z-index:20;min-width:248px;padding:6px;background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-ctl);box-shadow:var(--shadow-overlay)}
.menu button{display:block;width:100%;padding:7px 10px;border:0;border-radius:6px;background:transparent;text-align:left;cursor:pointer}
.menu button:hover,.menu button:focus-visible{background:var(--n-100);outline:0}
.menu button small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.menu button small .fig{color:var(--text-primary)}
.menu hr{border:0;border-top:1px solid var(--n-line);margin:6px 0}
[data-tip]{position:relative}
[data-tip]:hover::before,[data-tip]:focus-visible::before{content:attr(data-tip);position:absolute;z-index:30;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:max-content;max-width:260px;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);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-r:hover::before,[data-tip].tip-r:focus-visible::before{left:auto;right:0;transform:none}
.toasts{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:40;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(--c-white);box-shadow:var(--shadow-overlay)}
</style>
</head>
<body>
<div class="app">
<aside class="side"><div class="side-in">
<a class="brand" href="workbench-d.html"><img src="../../logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">
<a href="workbench-d.html" aria-current="page"><i data-ic="grid" class="lg"></i>工作台</a>
<a href="#" data-demo><i data-ic="cert" class="lg"></i>资质库</a>
<a href="#" data-demo><i data-ic="gauge" class="lg"></i>额度</a>
<div class="grp">UI demo</div>
<a href="../index.html"><i data-ic="book" class="lg"></i>demo 目录</a>
</nav>
<div class="side-foot"><b>王敏</b>专业版 · 至 2027-10-31</div>
</div></aside>
<div class="main">
<header class="top">
<span class="org"><b>南京某信息技术有限公司</b><span>/</span>工作台</span>
<form class="search" role="search" data-gsearch>
<i data-ic="search"></i>
<input type="search" placeholder="搜索项目、采购人" aria-label="搜索项目、采购人" aria-keyshortcuts="Control+K" data-q>
<span class="kbd" aria-hidden="true">Ctrl K</span>
</form>
<div class="top-r">
<a class="qchip tbtn" href="#" data-demo data-tip="本期剩余额度:解析 37 / 50 次,检查 12 / 20 次" aria-label="本期剩余额度:解析 37 次,检查 12 次">解析<span class="fig">37</span><span class="sep"></span>检查<span class="fig">12</span></a>
<button class="ibtn tip-r" aria-label="通知(1 条未读)" data-tip="通知" data-notice><i data-ic="bell" class="lg"></i><span class="dot"></span></button>
</div>
</header>
<main class="content">
<div class="ph">
<div><h1>工作台</h1><p>11月3日 周二。项目按投标截止时间排序,截止临近的排在前面。</p></div>
<div class="act">
<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="card alert" role="note">
<i data-ic="clock" class="lg"></i>
<p><b>某县数据中心机房改造项目</b> <span class="when">今天 17:00 截止</span>。清单还是草稿,还没有做过检查。</p>
<a class="btn sm" href="../page-reading.html"><i data-ic="list"></i>确认清单</a>
</div>
<section class="card stats" aria-label="项目统计,点击可筛选项目列表" data-stats>
<button class="stat" data-f="active" aria-pressed="true"><small>进行中的项目</small><span class="big">4</span><p>已递交 1 · 已归档 1</p><i data-ic="filter"></i></button>
<button class="stat" data-f="due7" aria-pressed="false"><small>7 天内截止</small><span class="big">2</span><p class="due">最近:今天 17:00</p><i data-ic="filter"></i></button>
<button class="stat" data-f="fatal" aria-pressed="false"><small>待处理的致命问题</small><span class="big fatal">3</span><p>在 1 个项目里</p><i data-ic="filter"></i></button>
<button class="stat" data-f="pend" aria-pressed="false"><small>待确认的清单条目</small><span class="big">4</span><p>在 2 个项目里</p><i data-ic="filter"></i></button>
</section>
<div class="wb">
<div class="stack">
<section class="card" aria-labelledby="h-proj">
<div class="ch">
<h2 id="h-proj">项目</h2>
<div class="filters" role="group" aria-label="按状态筛选" data-seg>
<button class="tbtn" aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button>
<button class="tbtn" aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button>
<button class="tbtn" aria-pressed="false" data-v="已递交">已递交</button>
<button class="tbtn" aria-pressed="false" data-v="已归档">已归档</button>
</div>
<span class="more" data-chip></span>
</div>
<div class="cols grid-cols" aria-hidden="true"><span>项目</span><span>投标截止</span><span>状态</span><span class="r">致命</span><span class="r nx-h">下一步</span></div>
<ul data-rows aria-label="项目列表"></ul>
<p class="cf cap sec">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
</section>
<section class="card" aria-labelledby="h-todo">
<div class="ch">
<h2 id="h-todo">待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" href="#" data-demo>全部在查标页</a>
</div>
<ul>
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite" 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" 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" 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>
</section>
</div>
<div class="stack rail">
<section class="card" aria-labelledby="h-task">
<div class="ch"><h3 id="h-task">正在进行的任务</h3></div>
<div class="rbody">
<div class="task-h"><b>解析补遗 2</b><span class="cap sec">提取清单</span></div>
<div class="cap sec">某市医保信息系统等保测评服务</div>
<div class="pbar"><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 sec">检查排队中 · 智慧社区平台运维 · 前面还有 1 个任务</span></div>
<p class="cap sec" style="margin-top:8px">可以离开本页,完成后会发站内通知。</p>
</div>
</section>
<section class="card" aria-labelledby="h-quota">
<div class="ch"><h3 id="h-quota">本期额度</h3><span class="more cap sec">至 2027-10-31</span></div>
<div class="rbody">
<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="cf links cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card" aria-labelledby="h-log">
<div class="ch"><h3 id="h-log">最近处理</h3><a class="more lnk cap" href="#" data-demo>全部记录</a></div>
<div class="rbody">
<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>
</div>
</div>
</main>
</div>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '<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"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
bell: '<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"/>',
book: '<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"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
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"/>',
list: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/>',
"chev-right": '<path d="m9 18 6-6-6-6"/>',
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
$$("i[data-ic]").forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
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(), 4000);
}
const DEMO = "(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页";
/* 数据沿用 page-workbench.html;pend 为待确认清单条目数(对应统计「在 2 个项目里」)。
next 为「下一步」,menu 为「更多」里的操作;cost 表示会扣点。 */
const CHECK_COST = { label: "扣 1 次检查 · 剩 ", left: 12, unit: " 次" };
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "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: "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: "past", st: "已递交", ready: "", fatal: 0, pend: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "past", st: "已归档", ready: "", fatal: 0, pend: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
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 = "";
function status(p) {
let r = "";
if (p.ready === "就绪") r = '<span class="ready" 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>`;
}
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="row grid-cols ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span><a class="pname" href="${p.href || "#"}" title="${p.name}" ${p.href ? "" : "data-demo"}>${p.name}</a><span class="psub">${p.buyer} · ${p.lot}</span></span>
<span class="due ${p.cls}"><b>${p.due[0]}</b><small>${p.due[1]}</small></span>
${status(p)}
<span class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span class="nx-cell"><button class="nx" data-next><span>${p.next}</span>${icon("chev-right")}</button><button class="ibtn sm" aria-label="更多操作:${p.name}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span>
</li>`).join("")
|| `<li class="empty"><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="tbtn clear" data-clear>${STAT[stat].label}${icon("x")}<span class="vh">,清除筛选</span></button>` : "";
}
/* 「更多」菜单 */
let menuEl = null, menuBtn = null;
function closeMenu(focusBack) {
if (!menuEl) return;
menuEl.remove(); menuEl = null;
if (menuBtn) { menuBtn.setAttribute("aria-expanded", "false"); if (focusBack) menuBtn.focus(); }
menuBtn = null;
}
function openMenu(btn, p) {
closeMenu();
menuBtn = btn; btn.setAttribute("aria-expanded", "true");
menuEl = document.createElement("div");
menuEl.className = "menu"; menuEl.setAttribute("role", "menu");
const items = [["打开项目", ""], ...p.menu].map(([t, s]) => {
const sub = s === "cost" ? `${CHECK_COST.label}<span class="fig">${CHECK_COST.left}</span>${CHECK_COST.unit}` : s;
return `<button role="menuitem" data-act="${t}">${t}${sub ? `<small>${sub}</small>` : ""}</button>`;
});
items.splice(1, 0, "<hr>");
menuEl.innerHTML = items.join("");
document.body.appendChild(menuEl);
const r = btn.getBoundingClientRect();
menuEl.style.top = `${r.bottom + 4}px`;
menuEl.style.left = `${r.right - menuEl.offsetWidth}px`;
menuEl.addEventListener("click", (e) => {
const it = e.target.closest("[data-act]"); if (!it) return;
const a = it.dataset.act; closeMenu(true);
if (a === "打开项目" && p.href) location.href = p.href;
else toast(`(演示)${a}:${p.name}`);
});
menuEl.addEventListener("keydown", (e) => {
const its = $$("[role=menuitem]", menuEl), k = its.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); its[(k + 1) % its.length].focus(); }
if (e.key === "ArrowUp") { e.preventDefault(); its[(k - 1 + its.length) % its.length].focus(); }
});
$("[role=menuitem]", menuEl).focus();
}
$("[data-rows]").addEventListener("click", (e) => {
const row = e.target.closest(".row"); if (!row) return;
const p = P[+row.dataset.i];
const m = e.target.closest("[data-menu]");
if (m) { e.preventDefault(); e.stopPropagation(); if (menuBtn === m) closeMenu(); else openMenu(m, p); return; }
if (e.target.closest(".ready")) return;
if (e.target.closest("[data-next]")) {
if (p.href) location.href = p.href;
else toast(`(演示)${p.next}:${p.name}`);
return;
}
if (!e.target.closest(".pname")) { if (p.href) location.href = p.href; else toast(DEMO); }
});
document.addEventListener("click", (e) => { if (menuEl && !menuEl.contains(e.target)) closeMenu(); });
window.addEventListener("scroll", () => closeMenu(), { passive: true });
window.addEventListener("resize", () => closeMenu());
$("[data-seg]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; seg = b.dataset.v; 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(); } });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-gsearch]").addEventListener("submit", (e) => e.preventDefault());
document.addEventListener("click", (e) => { const d = e.target.closest("[data-demo]"); if (d) { e.preventDefault(); toast(DEMO); } });
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); $("[data-q]").focus(); }
if (e.key === "Escape") {
if (menuEl) closeMenu(true);
else if (document.activeElement === $("[data-q]")) $("[data-q]").blur();
}
});
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,599 @@
<!doctype html>
<!--
工作台 · 风格方案 D「明亮工作区」(探索稿,不是定稿)
数据、文案、交互沿用 ../page-workbench.html 与 ../demo.js。
本方案相对 design-system.md 改了哪些规则:
0. (d2)侧边栏改为墨色底(--c-ink),logo 用反白版;可折叠到 64px 只留图标,折叠状态记在本地。?side=light 可切换为浅灰侧栏对照。
1. §3.1 页面底色:校样纸 #F5F6F3 → --n-50(墨 3% 混白的中性冷灰);侧边栏、顶栏、卡片纯白。
偏绿的 --c-paper / --c-sunken / --c-hover / --c-rule 一律不用,浅底和分隔线只用下面三个从墨派生的值。
2. §5.1 层次:只有两种容器——页面(--n-50)和白卡片(圆角 12、1px --n-line、无阴影)。卡片里不再套面板。
可点击的整张卡片(本页没有)和浮层才用阴影;行、格的悬停只出 --n-50 底色。
3. §5.1 圆角:2px 一档 → 卡片 12、按钮 / 输入框 / 菜单 8、标签 6。
4. §7.1 按钮:高 32 → 36(主 / 次);表格行内 32。主按钮墨底白字;次按钮白底 + --c-muted 控件边框(3.9:1);
文字按钮悬停出 --n-100 底。普通链接仍是墨色 + 下划线,蓝铅笔只给出处。
5. §7.7 表头:凹陷灰底 + 粗底线 → 无底色,12px 辅助灰列名,无线。行高 64,行间 --n-line 发丝线。
6. §7.9 项目行:
- 「状态」中性描边标签 → --n-100 实底、圆角 6、无描边;全页只有这一种中性标签。
- 「就绪」从描边标签改为墨色普通字 + 虚线下划线(悬停说明不变),和「未就绪」并排写在状态标签后面,省掉一列。
- 「所投包」并入项目名下方的辅助灰行。
- 新增「下一步」列:默认是带箭头的文字链,整行悬停 / 键盘聚焦时变成次按钮,同时浮现「更多」图标按钮。
会扣点的操作(发起检查)放在「更多」里,并写明扣多少、还剩多少;有致命问题时「导出检查报告」仍可用。
7. 工作台统计:四张卡片 → 一张卡片四格,竖向发丝线分隔,数字 30 → 28(Barlow 600)。
每格可点击筛选项目列表,悬停出 --n-50 底,选中格底部 2px 墨色边条。
8. §5.3 顶栏:面包屑位置改为企业主体名;中间加全局搜索(白底 + 控件边框、圆角 8、Ctrl K),本页用它筛选项目列表。
9. §7.12 截止提醒条:赭色浅底条 → 同规格白卡片,赭色图标 + 赭色时间字。
10. §7.4「致命」行左侧 3px 竖线:工作台的待处理列表不用竖线,靠左侧实心「致命」标签扫读。
未改:颜色语义、问题级别形状、字体分工、字阶(正文 14 / 标题 16 / 页标题 24)、4px 间距基准、Lucide 图标。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 方案 D 明亮工作区</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); /* 发丝线、卡片边 */
--ctl-border: var(--c-muted); /* 控件边框 3.9:1 */
--primary-hover: color-mix(in srgb, var(--c-ink) 88%, white);
--r-card: 12px; --r-ctl: 8px; --r-tag: 6px;
--btn-h: 36px; --btn-h-sm: 32px;
--top-h: 56px;
--pad: 32px; /* 页面左右内边距 */
--gap: 16px; /* 卡片之间、主列与右栏之间:全页一个节奏 */
--card-x: 20px; /* 卡片内左右边距:所有卡片统一 */
--rail-w: 296px;
--side-w: var(--sidebar-w); --side-w-min: 64px;
/* 侧栏:墨色底。文字和底色都从墨、白派生,不引入新色相 */
--side-bg: var(--c-ink);
--side-fg: color-mix(in srgb, white 74%, var(--c-ink)); /* 未选中项 约 9:1 */
--side-fg-strong: var(--c-white);
--side-hover: color-mix(in srgb, white 8%, var(--c-ink));
--side-active: color-mix(in srgb, white 14%, var(--c-ink));
--side-line: color-mix(in srgb, white 12%, var(--c-ink));
--side-muted: color-mix(in srgb, white 58%, var(--c-ink)); /* 分组小字、页脚 约 6:1 */
--cols: minmax(0,1fr) 120px 120px 64px 168px; /* 项目行栅格 */
--col-gap: 16px;
}
@media (max-width: 1399px){
:root{ --pad: 24px; --rail-w: 256px; --cols: minmax(0,1fr) 108px 108px 56px 160px; --col-gap: 12px; }
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
body{background:var(--n-50);color:var(--text-primary);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);-webkit-font-smoothing:antialiased;min-width:1280px}
h1,h2,h3,p,ul{margin:0}
ul{padding:0;list-style:none}
a{color:inherit}
button{font:inherit;color:inherit}
.fig{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold)}
.sec{color:var(--text-secondary)}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.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}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px;border-radius:var(--r-ctl)}
/* ---------- 链接与按钮 ---------- */
.lnk{color:var(--text-primary);text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px;transition:text-decoration-color var(--dur-fast) var(--ease)}
.lnk:hover{text-decoration-color:var(--c-ink)}
.cite{display:inline-flex;align-items:center;gap:4px;color:var(--ai);text-decoration:none;white-space:nowrap;border-radius:var(--r-ctl);padding:2px 6px;margin:-2px -6px;transition:background-color var(--dur-fast) var(--ease)}
.cite:hover{background:var(--ai-soft);color:var(--ai-strong)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:var(--btn-h);padding:0 14px;border-radius:var(--r-ctl);border:1px solid var(--ctl-border);background:var(--c-white);color:var(--text-primary);font-size:var(--fs-body);line-height:1;white-space:nowrap;text-decoration:none;cursor:pointer;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(--c-white);font-weight:var(--fw-bold)}
.btn.primary:hover{background:var(--primary-hover)}
.btn.sm{height:var(--btn-h-sm);padding:0 12px}
.tbtn{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;border:0;border-radius:var(--r-ctl);background:transparent;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background-color var(--dur-fast) var(--ease)}
.tbtn:hover{background:var(--n-100)}
.ibtn{position:relative;display:inline-grid;place-items:center;width:var(--btn-h);height:var(--btn-h);border:0;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.ibtn:hover{background:var(--n-100)}
.ibtn.sm{width:var(--btn-h-sm);height:var(--btn-h-sm)}
/* ---------- 标签:中性一种 + 问题级别 ---------- */
.tag{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);white-space:nowrap}
.sev{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 8px;border-radius:var(--r-tag);font-size:var(--fs-caption);font-weight:var(--fw-bold);white-space:nowrap}
.sev.fatal{background:var(--sev-fatal);color:var(--c-white)}
.sev .fig{font-weight:var(--fw-bold)}
.dash{color:var(--text-secondary)}
/* ---------- 应用框架 ---------- */
.app{display:grid;grid-template-columns:var(--side-w) minmax(0,1fr);min-height:100vh;transition:grid-template-columns var(--dur-base) var(--ease)}
.app[data-collapsed]{--side-w:var(--side-w-min)}
.side{background:var(--side-bg);color:var(--side-fg);min-width:0}
/* 浅灰侧栏对照(?side=light) */
.app[data-side="light"]{--side-bg:color-mix(in srgb, var(--c-ink) 9%, white);--side-fg:var(--text-secondary);--side-fg-strong:var(--text-primary);--side-hover:color-mix(in srgb, var(--c-ink) 14%, white);--side-active:var(--c-white);--side-line:var(--n-line);--side-muted:var(--text-secondary)}
.app[data-side="light"] .side{border-right:1px solid var(--n-line)}
.app[data-side="light"] .nav a[aria-current]{box-shadow:0 0 0 1px var(--n-line)}
.brand .mini{display:none;height:28px;width:28px}
.app[data-collapsed] .brand .full{display:none}
.app[data-collapsed] .brand .mini{display:block}
.app[data-side="light"] .brand .full.rev,.app:not([data-side="light"]) .brand .full.std{display:none}
.side-in{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:0 12px 12px;overflow:hidden}
.brand{display:flex;align-items:center;height:var(--top-h);padding:0 8px;margin-bottom:12px;flex:none}
.brand:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px;border-radius:var(--r-ctl)}
.brand img{height:26px;display:block}
.nav{display:flex;flex-direction:column;gap:2px}
.nav a,.side-btn{position:relative;display:flex;align-items:center;gap:10px;height:var(--btn-h);padding:0 10px;border-radius:var(--r-ctl);text-decoration:none;color:var(--side-fg);white-space:nowrap;transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.nav a .ic,.side-btn .ic{flex:none}
.nav a:hover,.side-btn:hover{background:var(--side-hover);color:var(--side-fg-strong)}
.nav a[aria-current]{background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold)}
.nav a:focus-visible,.side-btn:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px}
.nav .grp{margin:20px 10px 6px;height:20px;font-size:var(--fs-caption);line-height:20px;color:var(--side-muted);white-space:nowrap}
.side-btn{width:100%;border:0;background:transparent;font:inherit;cursor:pointer;margin-top:auto}
.side-foot{display:flex;align-items:center;gap:10px;margin-top:8px;padding:12px 6px 0;border-top:1px solid var(--side-line);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--side-muted);white-space:nowrap}
.side-foot b{display:block;color:var(--side-fg-strong);font-size:var(--fs-body);line-height:var(--lh-body);font-weight:var(--fw-bold)}
.avatar{flex:none;display:inline-grid;place-items:center;width:32px;height:32px;border-radius:50%;background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold);font-size:var(--fs-body)}
.app[data-side="light"] .avatar{background:var(--c-ink);color:var(--c-white)}
/* 折叠:只留图标;文字保留给读屏,悬停出提示 */
.app[data-collapsed] .lbl,.app[data-collapsed] .side-foot .who{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.app[data-collapsed] .nav a,.app[data-collapsed] .side-btn{justify-content:center;padding:0;width:40px}
.app[data-collapsed] .nav .grp{margin:20px 4px 6px;font-size:0;border-top:1px solid var(--side-line);height:0}
.app[data-collapsed] .side-foot{justify-content:center;padding:12px 0 0}
.app[data-collapsed] .brand{justify-content:center;padding:0}
.app[data-collapsed] [data-stip]:hover::after,.app[data-collapsed] [data-stip]:focus-visible::after{content:attr(data-stip);position:absolute;z-index:40;left:calc(100% + 12px);top:50%;transform:translateY(-50%);width:max-content;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:var(--fw-regular);box-shadow:var(--shadow-overlay);pointer-events:none}
.side-in:has([data-stip]:hover){overflow:visible}
.top{position:sticky;top:0;z-index:5;display:grid;grid-template-columns:1fr minmax(0,400px) 1fr;align-items:center;gap:16px;height:var(--top-h);padding:0 var(--pad);background:var(--c-white);border-bottom:1px solid var(--n-line)}
.org{justify-self:start;display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary)}
.org b{color:var(--text-primary);font-weight:var(--fw-bold)}
.search{position:relative;display:flex;align-items:center;height:var(--btn-h);border:1px solid var(--ctl-border);border-radius:var(--r-ctl);background:var(--c-white);transition:border-color var(--dur-fast) var(--ease)}
.search:hover{border-color:var(--c-ink)}
.search:focus-within{border-color:var(--c-ink);box-shadow:0 0 0 1px var(--c-ink)}
.search > .ic{position:absolute;left:11px;color:var(--text-secondary);pointer-events:none}
.search input{flex:1;min-width:0;height:100%;border:0;background:transparent;padding:0 8px 0 34px;font:inherit;color:var(--text-primary);outline:0}
.search input::placeholder{color:var(--text-secondary)}
.search input::-webkit-search-cancel-button{display:none}
.kbd{flex:none;margin-right:6px;padding:0 6px;height:22px;display:inline-flex;align-items:center;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-secondary);font-size:var(--fs-caption);font-family:var(--font-figures);font-weight:var(--fw-bold)}
.top-r{justify-self:end;display:flex;align-items:center;gap:4px}
.qchip{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;color:var(--text-secondary)}
.qchip .fig{color:var(--text-primary);font-size:15px}
.qchip .sep{width:1px;height:12px;background:var(--n-line);margin:0 4px}
.dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--c-ink);box-shadow:0 0 0 2px var(--c-white)}
.content{padding:28px var(--pad) 48px}
/* ---------- 页头 ---------- */
.ph{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}
.ph h1{font-family:var(--font-serif);font-weight:var(--fw-bold);font-size:var(--fs-h1);line-height:var(--lh-h1)}
.ph p{margin-top:2px;color:var(--text-secondary)}
.ph .act{display:flex;align-items:center;gap:12px}
.cost{font-size:var(--fs-caption);color:var(--text-secondary);white-space:nowrap}
.cost .fig{color:var(--text-primary)}
/* ---------- 唯一的容器:白卡片 ---------- */
.card{background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-card)}
.ch{display:flex;align-items:center;gap:12px;min-height:56px;padding:12px var(--card-x)}
.ch h2,.ch h3{font-size:var(--fs-title);line-height:var(--lh-title);font-weight:var(--fw-bold)}
.ch .more{margin-left:auto}
.cf{padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.stack > * + *{margin-top:var(--gap)}
/* 截止提醒 */
.alert{display:flex;align-items:center;gap:14px;padding:14px var(--card-x);margin-bottom:var(--gap)}
.alert > .ic{color:var(--c-ochre)}
.alert p{flex:1}
.alert .when{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 统计:一张卡片四格 */
.stats{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:var(--gap);overflow:hidden}
.stat{position:relative;display:grid;grid-template-rows:auto auto auto;justify-items:start;gap:2px;padding:16px var(--card-x) 16px;border:0;border-left:1px solid var(--n-line);background:transparent;text-align:left;cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.stat:first-child{border-left:0}
.stat:hover{background:var(--n-50)}
.stat::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--c-ink);transform:scaleY(0);transform-origin:bottom;transition:transform var(--dur-fast) var(--ease)}
.stat[aria-pressed="true"]::after{transform:scaleY(1)}
.stat[aria-pressed="true"] small{color:var(--text-primary);font-weight:var(--fw-bold)}
.stat:focus-visible{outline-offset:-2px;border-radius:0}
.stat small{font-size:var(--fs-body);color:var(--text-secondary)}
.stat .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.stat .big.fatal{color:var(--sev-fatal)}
.stat > .ic{position:absolute;top:18px;right:var(--card-x);color:var(--text-secondary);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.stat:hover > .ic,.stat:focus-visible > .ic,.stat[aria-pressed="true"] > .ic{opacity:1}
.stat[aria-pressed="true"] > .ic{color:var(--text-primary)}
.stat p{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.stat p.due{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 两栏 */
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--rail-w);gap:var(--gap);align-items:start}
/* 项目卡片头:筛选 */
.filters{display:flex;align-items:center;gap:2px;margin-left:8px}
.filters .tbtn{color:var(--text-secondary)}
.filters .tbtn .fig{font-size:15px}
.filters .tbtn[aria-pressed="true"]{background:var(--n-100);color:var(--text-primary);font-weight:var(--fw-bold)}
.clear{margin-left:auto;color:var(--text-primary)}
/* 项目行 */
.grid-cols{display:grid;grid-template-columns:var(--cols);column-gap:var(--col-gap);align-items:center;padding:0 var(--card-x)}
.cols{padding-bottom:8px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.cols .r,.row .r{justify-self:end;text-align:right}
.cols .nx-h{padding-right:48px}
.row{position:relative;min-height:64px;padding-top:10px;padding-bottom:10px;border-top:1px solid var(--n-line);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.row:hover,.row:focus-within{background:var(--n-50)}
.row:has(.pname:focus-visible){outline:2px solid var(--focus-ring);outline-offset:-2px}
.pname{display:block;font-weight:var(--fw-bold);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;outline:0}
.pname::after{content:"";position:absolute;inset:0}
.psub{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.due b{display:block;font-weight:var(--fw-regular);white-space:nowrap}
.due small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap}
.due.today b{color:var(--warn-text);font-weight:var(--fw-bold)}
.due.soon b{color:var(--warn-text)}
.due.past b{color:var(--text-secondary)}
.st{display:flex;align-items:center;gap:8px;white-space:nowrap}
.ready{position:relative;z-index:1;text-decoration:underline dashed;text-decoration-color:color-mix(in srgb, var(--c-ink) 40%, white);text-underline-offset:4px;cursor:help}
.row.arch .pname,.row.arch .due b{color:var(--text-secondary);font-weight:var(--fw-regular)}
/* 下一步:默认文字链,行悬停 / 聚焦变次按钮,同时浮现「更多」 */
.nx-cell{display:flex;align-items:center;justify-content:flex-end;gap:4px}
.nx{position:relative;z-index:1;display:inline-flex;align-items:center;gap:4px;height:var(--btn-h-sm);padding:0 10px 0 12px;border:1px solid transparent;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);white-space:nowrap;cursor:pointer;transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.nx span{text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px}
.nx .ic{color:var(--text-secondary);transition:transform var(--dur-fast) var(--ease)}
.row:hover .nx,.row:focus-within .nx{background:var(--c-white);border-color:var(--ctl-border)}
.row:hover .nx span,.row:focus-within .nx span{text-decoration-color:transparent}
.row .nx:hover{border-color:var(--c-ink)}
.row .nx:hover .ic{transform:translateX(2px);color:var(--text-primary)}
.row .ibtn{position:relative;z-index:1;opacity:0;transition:opacity var(--dur-fast) var(--ease),background-color var(--dur-fast) var(--ease)}
.row:hover .ibtn,.row:focus-within .ibtn,.row .ibtn[aria-expanded="true"]{opacity:1}
.empty{padding:32px var(--card-x);border-top:1px solid var(--n-line);text-align:center}
.empty b{display:block;font-weight:var(--fw-bold)}
.empty p{color:var(--text-secondary);margin-top:4px}
/* 待处理的致命问题 */
.todo{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;column-gap:16px;align-items:center;padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.todo b{display:block;font-weight:var(--fw-bold)}
.todo .psub{white-space:normal}
.todo .cite{margin-right:2px}
/* 右栏 */
.rail .ch{min-height:52px;padding-bottom:4px}
.rbody{padding:4px var(--card-x) 16px}
.task-h{display:flex;align-items:baseline;gap:8px}
.task-h b{font-weight:var(--fw-bold)}
.task-h .cap{margin-left:auto}
.pbar{display:flex;align-items:center;gap:10px;margin-top:10px}
.bar{flex:1;height:6px;border-radius:3px;background:var(--n-100);overflow:hidden}
.bar i{display:block;height:100%;background:var(--c-ink);border-radius:3px}
.queue{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding-top:12px;border-top:1px solid var(--n-line)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.q2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.q2 small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.q2 .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.q2 .unit{margin-left:4px;color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums}
.q2 .bar{margin-top:6px}
.links{display:flex;gap:16px}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:8px 0;border-top:1px solid var(--n-line);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:13px}
.log .who{font-weight:var(--fw-bold)}
.log .ok{color:var(--confirmed);white-space:nowrap}
/* ---------- 浮层:菜单、提示、轻提示 ---------- */
.menu{position:fixed;z-index:20;min-width:248px;padding:6px;background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-ctl);box-shadow:var(--shadow-overlay)}
.menu button{display:block;width:100%;padding:7px 10px;border:0;border-radius:6px;background:transparent;text-align:left;cursor:pointer}
.menu button:hover,.menu button:focus-visible{background:var(--n-100);outline:0}
.menu button small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.menu button small .fig{color:var(--text-primary)}
.menu hr{border:0;border-top:1px solid var(--n-line);margin:6px 0}
[data-tip]{position:relative}
[data-tip]:hover::before,[data-tip]:focus-visible::before{content:attr(data-tip);position:absolute;z-index:30;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:max-content;max-width:260px;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);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-r:hover::before,[data-tip].tip-r:focus-visible::before{left:auto;right:0;transform:none}
.toasts{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:40;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(--c-white);box-shadow:var(--shadow-overlay)}
</style>
</head>
<body>
<div class="app">
<aside class="side"><div class="side-in">
<a class="brand" href="workbench-d2.html" aria-label="标博士"><img class="full rev" src="../../logo/svg/logo-horizontal-reverse.svg" alt=""><img class="full std" src="../../logo/svg/logo-horizontal.svg" alt=""><img class="mini" src="../../logo/svg/seal-small.svg" alt=""></a>
<nav class="nav" aria-label="主导航">
<a href="workbench-d2.html" aria-current="page" data-stip="工作台"><i data-ic="grid" class="lg"></i><span class="lbl">工作台</span></a>
<a href="#" data-demo data-stip="资质库"><i data-ic="cert" class="lg"></i><span class="lbl">资质库</span></a>
<a href="#" data-demo data-stip="额度"><i data-ic="gauge" class="lg"></i><span class="lbl">额度</span></a>
<div class="grp">UI demo</div>
<a href="index.html" data-stip="方案目录"><i data-ic="book" class="lg"></i><span class="lbl">方案目录</span></a>
</nav>
<button class="side-btn" type="button" data-collapse aria-expanded="true" aria-keyshortcuts="Control+Backslash" data-stip="展开侧栏(Ctrl \)"><i data-ic="panel-close" class="lg"></i><span class="lbl">收起侧栏</span></button>
<div class="side-foot" data-stip="王敏 · 专业版"><span class="avatar" aria-hidden="true">王</span><span class="who"><b>王敏</b>专业版 · 至 2027-10-31</span></div>
</div></aside>
<div class="main">
<header class="top">
<span class="org"><b>南京某信息技术有限公司</b><span>/</span>工作台</span>
<form class="search" role="search" data-gsearch>
<i data-ic="search"></i>
<input type="search" placeholder="搜索项目、采购人" aria-label="搜索项目、采购人" aria-keyshortcuts="Control+K" data-q>
<span class="kbd" aria-hidden="true">Ctrl K</span>
</form>
<div class="top-r">
<a class="qchip tbtn" href="#" data-demo data-tip="本期剩余额度:解析 37 / 50 次,检查 12 / 20 次" aria-label="本期剩余额度:解析 37 次,检查 12 次">解析<span class="fig">37</span><span class="sep"></span>检查<span class="fig">12</span></a>
<button class="ibtn tip-r" aria-label="通知(1 条未读)" data-tip="通知" data-notice><i data-ic="bell" class="lg"></i><span class="dot"></span></button>
</div>
</header>
<main class="content">
<div class="ph">
<div><h1>工作台</h1><p>11月3日 周二。项目按投标截止时间排序,截止临近的排在前面。</p></div>
<div class="act">
<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="card alert" role="note">
<i data-ic="clock" class="lg"></i>
<p><b>某县数据中心机房改造项目</b> <span class="when">今天 17:00 截止</span>。清单还是草稿,还没有做过检查。</p>
<a class="btn sm" href="../page-reading.html"><i data-ic="list"></i>确认清单</a>
</div>
<section class="card stats" aria-label="项目统计,点击可筛选项目列表" data-stats>
<button class="stat" data-f="active" aria-pressed="true"><small>进行中的项目</small><span class="big">4</span><p>已递交 1 · 已归档 1</p><i data-ic="filter"></i></button>
<button class="stat" data-f="due7" aria-pressed="false"><small>7 天内截止</small><span class="big">2</span><p class="due">最近:今天 17:00</p><i data-ic="filter"></i></button>
<button class="stat" data-f="fatal" aria-pressed="false"><small>待处理的致命问题</small><span class="big fatal">3</span><p>在 1 个项目里</p><i data-ic="filter"></i></button>
<button class="stat" data-f="pend" aria-pressed="false"><small>待确认的清单条目</small><span class="big">4</span><p>在 2 个项目里</p><i data-ic="filter"></i></button>
</section>
<div class="wb">
<div class="stack">
<section class="card" aria-labelledby="h-proj">
<div class="ch">
<h2 id="h-proj">项目</h2>
<div class="filters" role="group" aria-label="按状态筛选" data-seg>
<button class="tbtn" aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button>
<button class="tbtn" aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button>
<button class="tbtn" aria-pressed="false" data-v="已递交">已递交</button>
<button class="tbtn" aria-pressed="false" data-v="已归档">已归档</button>
</div>
<span class="more" data-chip></span>
</div>
<div class="cols grid-cols" aria-hidden="true"><span>项目</span><span>投标截止</span><span>状态</span><span class="r">致命</span><span class="r nx-h">下一步</span></div>
<ul data-rows aria-label="项目列表"></ul>
<p class="cf cap sec">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
</section>
<section class="card" aria-labelledby="h-todo">
<div class="ch">
<h2 id="h-todo">待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" href="#" data-demo>全部在查标页</a>
</div>
<ul>
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite" 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" 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" 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>
</section>
</div>
<div class="stack rail">
<section class="card" aria-labelledby="h-task">
<div class="ch"><h3 id="h-task">正在进行的任务</h3></div>
<div class="rbody">
<div class="task-h"><b>解析补遗 2</b><span class="cap sec">提取清单</span></div>
<div class="cap sec">某市医保信息系统等保测评服务</div>
<div class="pbar"><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 sec">检查排队中 · 智慧社区平台运维 · 前面还有 1 个任务</span></div>
<p class="cap sec" style="margin-top:8px">可以离开本页,完成后会发站内通知。</p>
</div>
</section>
<section class="card" aria-labelledby="h-quota">
<div class="ch"><h3 id="h-quota">本期额度</h3><span class="more cap sec">至 2027-10-31</span></div>
<div class="rbody">
<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="cf links cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card" aria-labelledby="h-log">
<div class="ch"><h3 id="h-log">最近处理</h3><a class="more lnk cap" href="#" data-demo>全部记录</a></div>
<div class="rbody">
<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>
</div>
</div>
</main>
</div>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '<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"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
bell: '<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"/>',
book: '<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"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
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"/>',
list: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/>',
"chev-right": '<path d="m9 18 6-6-6-6"/>',
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
"panel-close": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/>',
"panel-open": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
$$("i[data-ic]").forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
/* 侧栏:主题(?side=light 为浅灰对照)与折叠(记在本地,读写失败不影响页面) */
const app = $(".app");
if (new URLSearchParams(location.search).get("side") === "light") app.dataset.side = "light";
const colBtn = $("[data-collapse]");
function setCollapsed(on, save) {
app.toggleAttribute("data-collapsed", on);
colBtn.setAttribute("aria-expanded", String(!on));
$(".lbl", colBtn).textContent = on ? "展开侧栏" : "收起侧栏";
colBtn.dataset.stip = on ? "展开侧栏(Ctrl \\)" : "收起侧栏(Ctrl \\)";
$(".ic", colBtn).outerHTML = icon(on ? "panel-open" : "panel-close", "lg");
if (save) { try { localStorage.setItem("bd-side-collapsed", on ? "1" : "0"); } catch (e) {} }
}
let saved = null; try { saved = localStorage.getItem("bd-side-collapsed"); } catch (e) {}
const forced = new URLSearchParams(location.search).get("collapsed");
setCollapsed(forced ? forced === "1" : saved === "1", false);
colBtn.addEventListener("click", () => setCollapsed(!app.hasAttribute("data-collapsed"), true));
document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.key === "\\") { e.preventDefault(); setCollapsed(!app.hasAttribute("data-collapsed"), true); } });
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(), 4000);
}
const DEMO = "(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页";
/* 数据沿用 page-workbench.html;pend 为待确认清单条目数(对应统计「在 2 个项目里」)。
next 为「下一步」,menu 为「更多」里的操作;cost 表示会扣点。 */
const CHECK_COST = { label: "扣 1 次检查 · 剩 ", left: 12, unit: " 次" };
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "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: "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: "past", st: "已递交", ready: "", fatal: 0, pend: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "past", st: "已归档", ready: "", fatal: 0, pend: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
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 = "";
function status(p) {
let r = "";
if (p.ready === "就绪") r = '<span class="ready" 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>`;
}
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="row grid-cols ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span><a class="pname" href="${p.href || "#"}" title="${p.name}" ${p.href ? "" : "data-demo"}>${p.name}</a><span class="psub">${p.buyer} · ${p.lot}</span></span>
<span class="due ${p.cls}"><b>${p.due[0]}</b><small>${p.due[1]}</small></span>
${status(p)}
<span class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span class="nx-cell"><button class="nx" data-next><span>${p.next}</span>${icon("chev-right")}</button><button class="ibtn sm" aria-label="更多操作:${p.name}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span>
</li>`).join("")
|| `<li class="empty"><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="tbtn clear" data-clear>${STAT[stat].label}${icon("x")}<span class="vh">,清除筛选</span></button>` : "";
}
/* 「更多」菜单 */
let menuEl = null, menuBtn = null;
function closeMenu(focusBack) {
if (!menuEl) return;
menuEl.remove(); menuEl = null;
if (menuBtn) { menuBtn.setAttribute("aria-expanded", "false"); if (focusBack) menuBtn.focus(); }
menuBtn = null;
}
function openMenu(btn, p) {
closeMenu();
menuBtn = btn; btn.setAttribute("aria-expanded", "true");
menuEl = document.createElement("div");
menuEl.className = "menu"; menuEl.setAttribute("role", "menu");
const items = [["打开项目", ""], ...p.menu].map(([t, s]) => {
const sub = s === "cost" ? `${CHECK_COST.label}<span class="fig">${CHECK_COST.left}</span>${CHECK_COST.unit}` : s;
return `<button role="menuitem" data-act="${t}">${t}${sub ? `<small>${sub}</small>` : ""}</button>`;
});
items.splice(1, 0, "<hr>");
menuEl.innerHTML = items.join("");
document.body.appendChild(menuEl);
const r = btn.getBoundingClientRect();
menuEl.style.top = `${r.bottom + 4}px`;
menuEl.style.left = `${r.right - menuEl.offsetWidth}px`;
menuEl.addEventListener("click", (e) => {
const it = e.target.closest("[data-act]"); if (!it) return;
const a = it.dataset.act; closeMenu(true);
if (a === "打开项目" && p.href) location.href = p.href;
else toast(`(演示)${a}:${p.name}`);
});
menuEl.addEventListener("keydown", (e) => {
const its = $$("[role=menuitem]", menuEl), k = its.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); its[(k + 1) % its.length].focus(); }
if (e.key === "ArrowUp") { e.preventDefault(); its[(k - 1 + its.length) % its.length].focus(); }
});
$("[role=menuitem]", menuEl).focus();
}
$("[data-rows]").addEventListener("click", (e) => {
const row = e.target.closest(".row"); if (!row) return;
const p = P[+row.dataset.i];
const m = e.target.closest("[data-menu]");
if (m) { e.preventDefault(); e.stopPropagation(); if (menuBtn === m) closeMenu(); else openMenu(m, p); return; }
if (e.target.closest(".ready")) return;
if (e.target.closest("[data-next]")) {
if (p.href) location.href = p.href;
else toast(`(演示)${p.next}:${p.name}`);
return;
}
if (!e.target.closest(".pname")) { if (p.href) location.href = p.href; else toast(DEMO); }
});
document.addEventListener("click", (e) => { if (menuEl && !menuEl.contains(e.target)) closeMenu(); });
window.addEventListener("scroll", () => closeMenu(), { passive: true });
window.addEventListener("resize", () => closeMenu());
$("[data-seg]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; seg = b.dataset.v; 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(); } });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-gsearch]").addEventListener("submit", (e) => e.preventDefault());
document.addEventListener("click", (e) => { const d = e.target.closest("[data-demo]"); if (d) { e.preventDefault(); toast(DEMO); } });
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); $("[data-q]").focus(); }
if (e.key === "Escape") {
if (menuEl) closeMenu(true);
else if (document.activeElement === $("[data-q]")) $("[data-q]").blur();
}
});
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,647 @@
<!doctype html>
<!--
工作台 · 风格方案 D「明亮工作区」(探索稿,不是定稿)
数据、文案、交互沿用 ../page-workbench.html 与 ../demo.js。
本方案相对 design-system.md 改了哪些规则:
0b. (d3)顶栏:透明落在页面底色上、滚动后变为半透明白 + 毛玻璃 + 发丝线,和墨色侧栏区分;搜索框靠右。图标:统计、区块标题、项目行、下一步、最近处理都加 Lucide 图标;图标底块只用中性浅灰,彩色只给有语义的(致命红、赭、蓝铅笔)。页头加问候语。
0. (d2)侧边栏改为墨色底(--c-ink),logo 用反白版;可折叠到 64px 只留图标,折叠状态记在本地。?side=light 可切换为浅灰侧栏对照。
1. §3.1 页面底色:校样纸 #F5F6F3 → --n-50(墨 3% 混白的中性冷灰);侧边栏、顶栏、卡片纯白。
偏绿的 --c-paper / --c-sunken / --c-hover / --c-rule 一律不用,浅底和分隔线只用下面三个从墨派生的值。
2. §5.1 层次:只有两种容器——页面(--n-50)和白卡片(圆角 12、1px --n-line、无阴影)。卡片里不再套面板。
可点击的整张卡片(本页没有)和浮层才用阴影;行、格的悬停只出 --n-50 底色。
3. §5.1 圆角:2px 一档 → 卡片 12、按钮 / 输入框 / 菜单 8、标签 6。
4. §7.1 按钮:高 32 → 36(主 / 次);表格行内 32。主按钮墨底白字;次按钮白底 + --c-muted 控件边框(3.9:1);
文字按钮悬停出 --n-100 底。普通链接仍是墨色 + 下划线,蓝铅笔只给出处。
5. §7.7 表头:凹陷灰底 + 粗底线 → 无底色,12px 辅助灰列名,无线。行高 64,行间 --n-line 发丝线。
6. §7.9 项目行:
- 「状态」中性描边标签 → --n-100 实底、圆角 6、无描边;全页只有这一种中性标签。
- 「就绪」从描边标签改为墨色普通字 + 虚线下划线(悬停说明不变),和「未就绪」并排写在状态标签后面,省掉一列。
- 「所投包」并入项目名下方的辅助灰行。
- 新增「下一步」列:默认是带箭头的文字链,整行悬停 / 键盘聚焦时变成次按钮,同时浮现「更多」图标按钮。
会扣点的操作(发起检查)放在「更多」里,并写明扣多少、还剩多少;有致命问题时「导出检查报告」仍可用。
7. 工作台统计:四张卡片 → 一张卡片四格,竖向发丝线分隔,数字 30 → 28(Barlow 600)。
每格可点击筛选项目列表,悬停出 --n-50 底,选中格底部 2px 墨色边条。
8. §5.3 顶栏:面包屑位置改为企业主体名;中间加全局搜索(白底 + 控件边框、圆角 8、Ctrl K),本页用它筛选项目列表。
9. §7.12 截止提醒条:赭色浅底条 → 同规格白卡片,赭色图标 + 赭色时间字。
10. §7.4「致命」行左侧 3px 竖线:工作台的待处理列表不用竖线,靠左侧实心「致命」标签扫读。
未改:颜色语义、问题级别形状、字体分工、字阶(正文 14 / 标题 16 / 页标题 24)、4px 间距基准、Lucide 图标。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 方案 D3</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); /* 发丝线、卡片边 */
--ctl-border: var(--c-muted); /* 控件边框 3.9:1 */
--primary-hover: color-mix(in srgb, var(--c-ink) 88%, white);
--r-card: 12px; --r-ctl: 8px; --r-tag: 6px;
--btn-h: 36px; --btn-h-sm: 32px;
--top-h: 56px;
--pad: 32px; /* 页面左右内边距 */
--gap: 16px; /* 卡片之间、主列与右栏之间:全页一个节奏 */
--card-x: 20px; /* 卡片内左右边距:所有卡片统一 */
--rail-w: 296px;
--side-w: var(--sidebar-w); --side-w-min: 64px;
/* 侧栏:墨色底。文字和底色都从墨、白派生,不引入新色相 */
--side-bg: var(--c-ink);
--side-fg: color-mix(in srgb, white 74%, var(--c-ink)); /* 未选中项 约 9:1 */
--side-fg-strong: var(--c-white);
--side-hover: color-mix(in srgb, white 8%, var(--c-ink));
--side-active: color-mix(in srgb, white 14%, var(--c-ink));
--side-line: color-mix(in srgb, white 12%, var(--c-ink));
--side-muted: color-mix(in srgb, white 58%, var(--c-ink)); /* 分组小字、页脚 约 6:1 */
--cols: minmax(0,1fr) 120px 120px 64px 168px; /* 项目行栅格 */
--col-gap: 16px;
}
@media (max-width: 1399px){
:root{ --pad: 24px; --rail-w: 256px; --cols: minmax(0,1fr) 108px 108px 56px 160px; --col-gap: 12px; }
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
body{background:var(--n-50);color:var(--text-primary);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);-webkit-font-smoothing:antialiased;min-width:1280px}
h1,h2,h3,p,ul{margin:0}
ul{padding:0;list-style:none}
a{color:inherit}
button{font:inherit;color:inherit}
.fig{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold)}
.sec{color:var(--text-secondary)}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.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}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px;border-radius:var(--r-ctl)}
/* ---------- 链接与按钮 ---------- */
.lnk{color:var(--text-primary);text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px;transition:text-decoration-color var(--dur-fast) var(--ease)}
.lnk:hover{text-decoration-color:var(--c-ink)}
.cite{display:inline-flex;align-items:center;gap:4px;color:var(--ai);text-decoration:none;white-space:nowrap;border-radius:var(--r-ctl);padding:2px 6px;margin:-2px -6px;transition:background-color var(--dur-fast) var(--ease)}
.cite:hover{background:var(--ai-soft);color:var(--ai-strong)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:var(--btn-h);padding:0 14px;border-radius:var(--r-ctl);border:1px solid var(--ctl-border);background:var(--c-white);color:var(--text-primary);font-size:var(--fs-body);line-height:1;white-space:nowrap;text-decoration:none;cursor:pointer;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(--c-white);font-weight:var(--fw-bold)}
.btn.primary:hover{background:var(--primary-hover)}
.btn.sm{height:var(--btn-h-sm);padding:0 12px}
.tbtn{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;border:0;border-radius:var(--r-ctl);background:transparent;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background-color var(--dur-fast) var(--ease)}
.tbtn:hover{background:var(--n-100)}
.ibtn{position:relative;display:inline-grid;place-items:center;width:var(--btn-h);height:var(--btn-h);border:0;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.ibtn:hover{background:var(--n-100)}
.ibtn.sm{width:var(--btn-h-sm);height:var(--btn-h-sm)}
/* ---------- 标签:中性一种 + 问题级别 ---------- */
.tag{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);white-space:nowrap}
.sev{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 8px;border-radius:var(--r-tag);font-size:var(--fs-caption);font-weight:var(--fw-bold);white-space:nowrap}
.sev.fatal{background:var(--sev-fatal);color:var(--c-white)}
.sev .fig{font-weight:var(--fw-bold)}
.dash{color:var(--text-secondary)}
/* ---------- 应用框架 ---------- */
.app{display:grid;grid-template-columns:var(--side-w) minmax(0,1fr);min-height:100vh;transition:grid-template-columns var(--dur-base) var(--ease)}
.app[data-collapsed]{--side-w:var(--side-w-min)}
.side{background:var(--side-bg);color:var(--side-fg);min-width:0}
/* 浅灰侧栏对照(?side=light) */
.app[data-side="light"]{--side-bg:color-mix(in srgb, var(--c-ink) 9%, white);--side-fg:var(--text-secondary);--side-fg-strong:var(--text-primary);--side-hover:color-mix(in srgb, var(--c-ink) 14%, white);--side-active:var(--c-white);--side-line:var(--n-line);--side-muted:var(--text-secondary)}
.app[data-side="light"] .side{border-right:1px solid var(--n-line)}
.app[data-side="light"] .nav a[aria-current]{box-shadow:0 0 0 1px var(--n-line)}
.brand .mini{display:none;height:28px;width:28px}
.app[data-collapsed] .brand .full{display:none}
.app[data-collapsed] .brand .mini{display:block}
.app[data-side="light"] .brand .full.rev,.app:not([data-side="light"]) .brand .full.std{display:none}
.side-in{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:0 12px 12px;overflow:hidden}
.brand{display:flex;align-items:center;height:var(--top-h);padding:0 8px;margin-bottom:12px;flex:none}
.brand:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px;border-radius:var(--r-ctl)}
.brand img{height:26px;display:block}
.nav{display:flex;flex-direction:column;gap:2px}
.nav a,.side-btn{position:relative;display:flex;align-items:center;gap:10px;height:var(--btn-h);padding:0 10px;border-radius:var(--r-ctl);text-decoration:none;color:var(--side-fg);white-space:nowrap;transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.nav a .ic,.side-btn .ic{flex:none}
.nav a:hover,.side-btn:hover{background:var(--side-hover);color:var(--side-fg-strong)}
.nav a[aria-current]{background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold)}
.nav a:focus-visible,.side-btn:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px}
.nav .grp{margin:20px 10px 6px;height:20px;font-size:var(--fs-caption);line-height:20px;color:var(--side-muted);white-space:nowrap}
.side-btn{width:100%;border:0;background:transparent;font:inherit;cursor:pointer;margin-top:auto}
.side-foot{display:flex;align-items:center;gap:10px;margin-top:8px;padding:12px 6px 0;border-top:1px solid var(--side-line);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--side-muted);white-space:nowrap}
.side-foot b{display:block;color:var(--side-fg-strong);font-size:var(--fs-body);line-height:var(--lh-body);font-weight:var(--fw-bold)}
.avatar{flex:none;display:inline-grid;place-items:center;width:32px;height:32px;border-radius:50%;background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold);font-size:var(--fs-body)}
.app[data-side="light"] .avatar{background:var(--c-ink);color:var(--c-white)}
/* 折叠:只留图标;文字保留给读屏,悬停出提示 */
.app[data-collapsed] .lbl,.app[data-collapsed] .side-foot .who{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.app[data-collapsed] .nav a,.app[data-collapsed] .side-btn{justify-content:center;padding:0;width:40px}
.app[data-collapsed] .nav .grp{margin:20px 4px 6px;font-size:0;border-top:1px solid var(--side-line);height:0}
.app[data-collapsed] .side-foot{justify-content:center;padding:12px 0 0}
.app[data-collapsed] .brand{justify-content:center;padding:0}
.app[data-collapsed] [data-stip]:hover::after,.app[data-collapsed] [data-stip]:focus-visible::after{content:attr(data-stip);position:absolute;z-index:40;left:calc(100% + 12px);top:50%;transform:translateY(-50%);width:max-content;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:var(--fw-regular);box-shadow:var(--shadow-overlay);pointer-events:none}
.side-in:has([data-stip]:hover){overflow:visible}
.top{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:12px;height:var(--top-h);padding:0 var(--pad);background:transparent;border-bottom:1px solid transparent;transition:background-color var(--dur-base) var(--ease),border-color var(--dur-base) var(--ease)}
.top.scrolled{background:color-mix(in srgb, var(--c-white) 82%, transparent);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-bottom-color:var(--n-line)}
.top .search{margin-left:auto;width:280px;transition:width var(--dur-base) var(--ease),border-color var(--dur-fast) var(--ease)}
.top .search:focus-within{width:360px}
.top .vsep{width:1px;height:20px;background:var(--n-line);margin:0 4px}
.org .ic{color:var(--text-secondary)}
.org{justify-self:start;display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary)}
.org b{color:var(--text-primary);font-weight:var(--fw-bold)}
.search{position:relative;display:flex;align-items:center;height:var(--btn-h);border:1px solid var(--ctl-border);border-radius:var(--r-ctl);background:var(--c-white);transition:border-color var(--dur-fast) var(--ease)}
.search:hover{border-color:var(--c-ink)}
.search:focus-within{border-color:var(--c-ink);box-shadow:0 0 0 1px var(--c-ink)}
.search > .ic{position:absolute;left:11px;color:var(--text-secondary);pointer-events:none}
.search input{flex:1;min-width:0;height:100%;border:0;background:transparent;padding:0 8px 0 34px;font:inherit;color:var(--text-primary);outline:0}
.search input::placeholder{color:var(--text-secondary)}
.search input::-webkit-search-cancel-button{display:none}
.kbd{flex:none;margin-right:6px;padding:0 6px;height:22px;display:inline-flex;align-items:center;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-secondary);font-size:var(--fs-caption);font-family:var(--font-figures);font-weight:var(--fw-bold)}
.top-r{justify-self:end;display:flex;align-items:center;gap:4px}
.qchip{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;color:var(--text-secondary)}
.qchip .fig{color:var(--text-primary);font-size:15px}
.qchip .sep{width:1px;height:12px;background:var(--n-line);margin:0 4px}
.dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--c-ink);box-shadow:0 0 0 2px var(--c-white)}
.content{padding:20px var(--pad) 48px}
.tile{flex:none;display:inline-grid;place-items:center;width:36px;height:36px;border-radius:10px;background:var(--n-100);color:var(--text-primary)}
.tile.sm{width:28px;height:28px;border-radius:8px}
.tile.warn{background:var(--warn-soft);color:var(--c-ochre)}
.tile.fatal{background:var(--sev-fatal-soft);color:var(--sev-fatal)}
.tile.ai{background:var(--ai-soft);color:var(--ai)}
.ch h2,.ch h3{display:inline-flex;align-items:center;gap:8px}
.ch h2 > .ic,.ch h3 > .ic{color:var(--text-secondary)}
/* ---------- 页头 ---------- */
.ph{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}
.ph h1{font-family:var(--font-serif);font-weight:var(--fw-bold);font-size:var(--fs-h1);line-height:var(--lh-h1)}
.ph p{margin-top:2px;color:var(--text-secondary)}
.warn-t{color:var(--warn-text);font-weight:var(--fw-bold)}
.ph .act{display:flex;align-items:center;gap:12px}
.cost{font-size:var(--fs-caption);color:var(--text-secondary);white-space:nowrap}
.cost .fig{color:var(--text-primary)}
/* ---------- 唯一的容器:白卡片 ---------- */
.card{background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-card)}
.ch{display:flex;align-items:center;gap:12px;min-height:56px;padding:12px var(--card-x)}
.ch h2,.ch h3{font-size:var(--fs-title);line-height:var(--lh-title);font-weight:var(--fw-bold)}
.ch .more{margin-left:auto}
.cf{padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.stack > * + *{margin-top:var(--gap)}
/* 截止提醒 */
.alert{display:flex;align-items:center;gap:14px;padding:14px var(--card-x);margin-bottom:var(--gap)}
.alert > .ic{color:var(--c-ochre)}
.alert p{flex:1}
.alert .when{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 统计:一张卡片四格 */
.stats{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:var(--gap);overflow:hidden}
.stat{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto auto auto;column-gap:16px;justify-items:start;gap:2px;padding:16px var(--card-x) 16px;border:0;border-left:1px solid var(--n-line);background:transparent;text-align:left;cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.stat:first-child{border-left:0}
.stat:hover{background:var(--n-50)}
.stat::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--c-ink);transform:scaleY(0);transform-origin:bottom;transition:transform var(--dur-fast) var(--ease)}
.stat[aria-pressed="true"]::after{transform:scaleY(1)}
.stat[aria-pressed="true"] small{color:var(--text-primary);font-weight:var(--fw-bold)}
.stat:focus-visible{outline-offset:-2px;border-radius:0}
.stat small{font-size:var(--fs-body);color:var(--text-secondary)}
.stat > .tile{grid-row:1 / span 3;align-self:start;margin-top:2px}
.stat > small,.stat > .big,.stat > p{grid-column:2}
.stat .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.stat .big.fatal{color:var(--sev-fatal)}
.stat > .ic{position:absolute;top:18px;right:var(--card-x);color:var(--text-secondary);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.stat:hover > .ic,.stat:focus-visible > .ic,.stat[aria-pressed="true"] > .ic{opacity:1}
.stat[aria-pressed="true"] > .ic{color:var(--text-primary)}
.stat p{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.stat p.due{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 两栏 */
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--rail-w);gap:var(--gap);align-items:start}
/* 项目卡片头:筛选 */
.filters{display:flex;align-items:center;gap:2px;margin-left:8px}
.filters .tbtn{color:var(--text-secondary)}
.filters .tbtn .fig{font-size:15px}
.filters .tbtn[aria-pressed="true"]{background:var(--n-100);color:var(--text-primary);font-weight:var(--fw-bold)}
.clear{margin-left:auto;color:var(--text-primary)}
/* 项目行 */
.grid-cols{display:grid;grid-template-columns:var(--cols);column-gap:var(--col-gap);align-items:center;padding:0 var(--card-x)}
.cols{padding-bottom:8px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.cols .r,.row .r{justify-self:end;text-align:right}
.cols .nx-h{padding-right:48px}
.row{position:relative;min-height:64px;padding-top:10px;padding-bottom:10px;border-top:1px solid var(--n-line);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.row:hover,.row:focus-within{background:var(--n-50)}
.row:has(.pname:focus-visible){outline:2px solid var(--focus-ring);outline-offset:-2px}
.pcell{display:flex;align-items:center;gap:10px;min-width:0}
.pcell .pt{min-width:0}
.row.arch .tile{color:var(--text-secondary)}
.nx .ic.lead{color:var(--text-secondary)}
.pname{display:block;font-weight:var(--fw-bold);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;outline:0}
.pname::after{content:"";position:absolute;inset:0}
.psub{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.due b{display:block;font-weight:var(--fw-regular);white-space:nowrap}
.due small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap}
.due.today b{color:var(--warn-text);font-weight:var(--fw-bold)}
.due.soon b{color:var(--warn-text)}
.due.past b{color:var(--text-secondary)}
.st{display:flex;align-items:center;gap:8px;white-space:nowrap}
.ready{position:relative;z-index:1;text-decoration:underline dashed;text-decoration-color:color-mix(in srgb, var(--c-ink) 40%, white);text-underline-offset:4px;cursor:help}
.row.arch .pname,.row.arch .due b{color:var(--text-secondary);font-weight:var(--fw-regular)}
/* 下一步:默认文字链,行悬停 / 聚焦变次按钮,同时浮现「更多」 */
.nx-cell{display:flex;align-items:center;justify-content:flex-end;gap:4px}
.nx{position:relative;z-index:1;display:inline-flex;align-items:center;gap:4px;height:var(--btn-h-sm);padding:0 10px 0 12px;border:1px solid transparent;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);white-space:nowrap;cursor:pointer;transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.nx span{text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px}
.nx .ic{color:var(--text-secondary);transition:transform var(--dur-fast) var(--ease)}
.row:hover .nx,.row:focus-within .nx{background:var(--c-white);border-color:var(--ctl-border)}
.row:hover .nx span,.row:focus-within .nx span{text-decoration-color:transparent}
.row .nx:hover{border-color:var(--c-ink)}
.row .nx:hover .ic{transform:translateX(2px);color:var(--text-primary)}
.row .ibtn{position:relative;z-index:1;opacity:0;transition:opacity var(--dur-fast) var(--ease),background-color var(--dur-fast) var(--ease)}
.row:hover .ibtn,.row:focus-within .ibtn,.row .ibtn[aria-expanded="true"]{opacity:1}
.empty{padding:32px var(--card-x);border-top:1px solid var(--n-line);text-align:center}
.empty b{display:block;font-weight:var(--fw-bold)}
.empty p{color:var(--text-secondary);margin-top:4px}
/* 待处理的致命问题 */
.todo{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;column-gap:16px;align-items:center;padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.todo b{display:block;font-weight:var(--fw-bold)}
.todo .psub{white-space:normal}
.todo .cite{margin-right:2px}
/* 右栏 */
.rail .ch{min-height:52px;padding-bottom:4px}
.rbody{padding:4px var(--card-x) 16px}
.task-h{display:flex;align-items:center;gap:10px}
.task-h + .cap{padding-left:38px}
.rbody .pbar{padding-left:38px}
.task-h b{font-weight:var(--fw-bold)}
.task-h .cap{margin-left:auto}
.pbar{display:flex;align-items:center;gap:10px;margin-top:10px}
.bar{flex:1;height:6px;border-radius:3px;background:var(--n-100);overflow:hidden}
.bar i{display:block;height:100%;background:var(--c-ink);border-radius:3px}
.queue{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding-top:12px;border-top:1px solid var(--n-line)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.q2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.q2 small .ic{vertical-align:-3px;margin-right:4px;color:var(--text-secondary)}
.q2 small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.q2 .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.q2 .unit{margin-left:4px;color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums}
.q2 .bar{margin-top:6px}
.links{display:flex;gap:16px}
.log li{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:start;gap:10px;padding:8px 0;border-top:1px solid var(--n-line);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:13px}
.log .who{font-weight:var(--fw-bold)}
.av{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);font-weight:var(--fw-bold);line-height:1}
.av .ic{width:14px;height:14px;color:var(--text-secondary)}
.log .ok{color:var(--confirmed);white-space:nowrap}
/* ---------- 浮层:菜单、提示、轻提示 ---------- */
.menu{position:fixed;z-index:20;min-width:248px;padding:6px;background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-ctl);box-shadow:var(--shadow-overlay)}
.menu button{display:block;width:100%;padding:7px 10px;border:0;border-radius:6px;background:transparent;text-align:left;cursor:pointer}
.menu button:hover,.menu button:focus-visible{background:var(--n-100);outline:0}
.menu button small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.menu button small .fig{color:var(--text-primary)}
.menu hr{border:0;border-top:1px solid var(--n-line);margin:6px 0}
[data-tip]{position:relative}
[data-tip]:hover::before,[data-tip]:focus-visible::before{content:attr(data-tip);position:absolute;z-index:30;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:max-content;max-width:260px;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);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-r:hover::before,[data-tip].tip-r:focus-visible::before{left:auto;right:0;transform:none}
.toasts{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:40;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(--c-white);box-shadow:var(--shadow-overlay)}
</style>
</head>
<body>
<div class="app">
<aside class="side"><div class="side-in">
<a class="brand" href="workbench-d3.html" aria-label="标博士"><img class="full rev" src="../../logo/svg/logo-horizontal-reverse.svg" alt=""><img class="full std" src="../../logo/svg/logo-horizontal.svg" alt=""><img class="mini" src="../../logo/svg/seal-small.svg" alt=""></a>
<nav class="nav" aria-label="主导航">
<a href="workbench-d3.html" aria-current="page" data-stip="工作台"><i data-ic="grid" class="lg"></i><span class="lbl">工作台</span></a>
<a href="#" data-demo data-stip="资质库"><i data-ic="cert" class="lg"></i><span class="lbl">资质库</span></a>
<a href="#" data-demo data-stip="额度"><i data-ic="gauge" class="lg"></i><span class="lbl">额度</span></a>
<div class="grp">UI demo</div>
<a href="index.html" data-stip="方案目录"><i data-ic="book" class="lg"></i><span class="lbl">方案目录</span></a>
</nav>
<button class="side-btn" type="button" data-collapse aria-expanded="true" aria-keyshortcuts="Control+Backslash" data-stip="展开侧栏(Ctrl \)"><i data-ic="panel-close" class="lg"></i><span class="lbl">收起侧栏</span></button>
<div class="side-foot" data-stip="王敏 · 专业版"><span class="avatar" aria-hidden="true">王</span><span class="who"><b>王敏</b>专业版 · 至 2027-10-31</span></div>
</div></aside>
<div class="main">
<header class="top">
<span class="org"><i data-ic="building"></i><b>南京某信息技术有限公司</b><span>/</span>工作台</span>
<form class="search" role="search" data-gsearch>
<i data-ic="search"></i>
<input type="search" placeholder="搜索项目、采购人" aria-label="搜索项目、采购人" aria-keyshortcuts="Control+K" data-q>
<span class="kbd" aria-hidden="true">Ctrl K</span>
</form>
<span class="vsep" aria-hidden="true"></span>
<div class="top-r">
<a class="qchip tbtn" href="#" data-demo data-tip="本期剩余额度:解析 37 / 50 次,检查 12 / 20 次" aria-label="本期剩余额度:解析 37 次,检查 12 次"><i data-ic="gauge"></i>解析<span class="fig">37</span><span class="sep"></span>检查<span class="fig">12</span></a>
<button class="ibtn tip-r" aria-label="通知(1 条未读)" data-tip="通知" data-notice><i data-ic="bell" class="lg"></i><span class="dot"></span></button>
</div>
</header>
<main class="content">
<div class="ph">
<div><h1>工作台</h1><p>下午好,王敏。今天是 11月3日 周二,有 <b class="warn-t">1 个项目今天截止</b>。</p></div>
<div class="act">
<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="card alert" role="note">
<span class="tile warn"><i data-ic="clock" class="lg"></i></span>
<p><b>某县数据中心机房改造项目</b> <span class="when">今天 17:00 截止</span>。清单还是草稿,还没有做过检查。</p>
<a class="btn sm" href="../page-reading.html"><i data-ic="list"></i>确认清单</a>
</div>
<section class="card stats" aria-label="项目统计,点击可筛选项目列表" data-stats>
<button class="stat" data-f="active" aria-pressed="true"><span class="tile"><i data-ic="folders" class="lg"></i></span><small>进行中的项目</small><span class="big">4</span><p>已递交 1 · 已归档 1</p><i data-ic="filter"></i></button>
<button class="stat" data-f="due7" aria-pressed="false"><span class="tile warn"><i data-ic="calendar-clock" class="lg"></i></span><small>7 天内截止</small><span class="big">2</span><p class="due">最近:今天 17:00</p><i data-ic="filter"></i></button>
<button class="stat" data-f="fatal" aria-pressed="false"><span class="tile fatal"><i data-ic="octagon-alert" class="lg"></i></span><small>待处理的致命问题</small><span class="big fatal">3</span><p>在 1 个项目里</p><i data-ic="filter"></i></button>
<button class="stat" data-f="pend" aria-pressed="false"><span class="tile ai"><i data-ic="list" class="lg"></i></span><small>待确认的清单条目</small><span class="big">4</span><p>在 2 个项目里</p><i data-ic="filter"></i></button>
</section>
<div class="wb">
<div class="stack">
<section class="card" aria-labelledby="h-proj">
<div class="ch">
<h2 id="h-proj"><i data-ic="folders"></i>项目</h2>
<div class="filters" role="group" aria-label="按状态筛选" data-seg>
<button class="tbtn" aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button>
<button class="tbtn" aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button>
<button class="tbtn" aria-pressed="false" data-v="已递交">已递交</button>
<button class="tbtn" aria-pressed="false" data-v="已归档">已归档</button>
</div>
<span class="more" data-chip></span>
</div>
<div class="cols grid-cols" aria-hidden="true"><span>项目</span><span>投标截止</span><span>状态</span><span class="r">致命</span><span class="r nx-h">下一步</span></div>
<ul data-rows aria-label="项目列表"></ul>
<p class="cf cap sec">「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
</section>
<section class="card" aria-labelledby="h-todo">
<div class="ch">
<h2 id="h-todo"><i data-ic="octagon-alert"></i>待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" href="#" data-demo>全部在查标页</a>
</div>
<ul>
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite" 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" 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" 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>
</section>
</div>
<div class="stack rail">
<section class="card" aria-labelledby="h-task">
<div class="ch"><h3 id="h-task"><i data-ic="activity"></i>正在进行的任务</h3></div>
<div class="rbody">
<div class="task-h"><span class="tile sm ai"><i data-ic="loader"></i></span><b>解析补遗 2</b><span class="cap sec">提取清单</span></div>
<div class="cap sec">某市医保信息系统等保测评服务</div>
<div class="pbar"><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 sec">检查排队中 · 智慧社区平台运维 · 前面还有 1 个任务</span></div>
<p class="cap sec" style="margin-top:8px">可以离开本页,完成后会发站内通知。</p>
</div>
</section>
<section class="card" aria-labelledby="h-quota">
<div class="ch"><h3 id="h-quota"><i data-ic="gauge"></i>本期额度</h3><span class="more cap sec">至 2027-10-31</span></div>
<div class="rbody">
<div class="q2">
<div><small><i data-ic="file-search"></i>解析剩余</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><i data-ic="scan"></i>检查剩余</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="cf links cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card" aria-labelledby="h-log">
<div class="ch"><h3 id="h-log"><i data-ic="history"></i>最近处理</h3><a class="more lnk cap" href="#" data-demo>全部记录</a></div>
<div class="rbody">
<ul class="log">
<li><span class="av">王</span><span><span class="who">王敏</span> <span class="ok">✓ 已修改</span>「授权委托书签章」</span><span>14:26</span></li>
<li><span class="av">王</span><span><span class="who">王敏</span> 确认了清单 v2 · 智慧大厅</span><span>11-02</span></li>
<li><span class="av sys"><i data-ic="cpu"></i></span><span><span class="who">系统</span> 完成第 2 次检查 · 致命 3</span><span>11-02</span></li>
<li><span class="av sys"><i data-ic="cpu"></i></span><span><span class="who">系统</span> 并入补遗 1 · 更新 2 条</span><span>11-02</span></li>
<li><span class="av">李</span><span><span class="who">李工</span> 忽略「其他项目名称」:业绩介绍</span><span>11-01</span></li>
</ul>
</div>
</section>
</div>
</div>
</main>
</div>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '<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"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
bell: '<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"/>',
book: '<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"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
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"/>',
list: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/>',
"chev-right": '<path d="m9 18 6-6-6-6"/>',
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
building: '<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4M10 10h4M10 14h4M10 18h4"/>',
folders: '<path d="M20 17a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3.9a2 2 0 0 1-1.69-.9l-.81-1.2a2 2 0 0 0-1.67-.9H8a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2Z"/><path d="M2 8v11a2 2 0 0 0 2 2h14"/>',
"calendar-clock": '<path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M16 2v4M8 2v4M3 10h5"/><path d="M17.5 17.5 16 16.3V14"/><circle cx="16" cy="16" r="6"/>',
"octagon-alert": '<path d="M12 16h.01M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/>',
activity: '<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/>',
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
loader: '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
"file-search": '<path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"/><path d="m9 18-1.5-1.5"/><circle cx="5" cy="14" r="3"/>',
scan: '<path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>',
bookmark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
pen: '<path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/>',
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
archive: '<rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>',
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
wrench: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
cpu: '<rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2M15 20v2M2 15h2M2 9h2M20 15h2M20 9h2M9 2v2M9 20v2"/>',
"panel-close": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/>',
"panel-open": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
$$("i[data-ic]").forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
/* 顶栏:滚动后变为半透明白 + 毛玻璃 */
const topEl = $(".top"); const onScroll = () => topEl.classList.toggle("scrolled", scrollY > 4); addEventListener("scroll", onScroll, { passive: true }); onScroll();
/* 侧栏:主题(?side=light 为浅灰对照)与折叠(记在本地,读写失败不影响页面) */
const app = $(".app");
if (new URLSearchParams(location.search).get("side") === "light") app.dataset.side = "light";
const colBtn = $("[data-collapse]");
function setCollapsed(on, save) {
app.toggleAttribute("data-collapsed", on);
colBtn.setAttribute("aria-expanded", String(!on));
$(".lbl", colBtn).textContent = on ? "展开侧栏" : "收起侧栏";
colBtn.dataset.stip = on ? "展开侧栏(Ctrl \\)" : "收起侧栏(Ctrl \\)";
$(".ic", colBtn).outerHTML = icon(on ? "panel-open" : "panel-close", "lg");
if (save) { try { localStorage.setItem("bd-side-collapsed", on ? "1" : "0"); } catch (e) {} }
}
let saved = null; try { saved = localStorage.getItem("bd-side-collapsed"); } catch (e) {}
const forced = new URLSearchParams(location.search).get("collapsed");
setCollapsed(forced ? forced === "1" : saved === "1", false);
colBtn.addEventListener("click", () => setCollapsed(!app.hasAttribute("data-collapsed"), true));
document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.key === "\\") { e.preventDefault(); setCollapsed(!app.hasAttribute("data-collapsed"), true); } });
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(), 4000);
}
const DEMO = "(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页";
/* 数据沿用 page-workbench.html;pend 为待确认清单条目数(对应统计「在 2 个项目里」)。
next 为「下一步」,menu 为「更多」里的操作;cost 表示会扣点。 */
const CHECK_COST = { label: "扣 1 次检查 · 剩 ", left: 12, unit: " 次" };
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "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: "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: "past", st: "已递交", ready: "", fatal: 0, pend: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "past", st: "已归档", ready: "", fatal: 0, pend: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
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 = "";
const ST_IC = { "跟进中": "bookmark", "编制中": "pen", "已检查": "scan", "已递交": "send", "已归档": "archive" };
const NX_IC = { "确认清单": "list", "导出检查报告": "download", "处理致命问题": "wrench", "上传投标文件": "upload", "查看检查报告": "file", "发起检查": "scan", "查看解析进度": "loader", "归档项目": "archive" };
function status(p) {
let r = "";
if (p.ready === "就绪") r = '<span class="ready" 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>`;
}
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="row grid-cols ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span class="pcell"><span class="tile sm" title="${p.st}">${icon(ST_IC[p.st] || "folders")}</span><span class="pt"><a class="pname" href="${p.href || "#"}" title="${p.name}" ${p.href ? "" : "data-demo"}>${p.name}</a><span class="psub">${p.buyer} · ${p.lot}</span></span></span>
<span class="due ${p.cls}"><b>${p.due[0]}</b><small>${p.due[1]}</small></span>
${status(p)}
<span class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span class="nx-cell"><button class="nx" data-next>${icon(NX_IC[p.next] || "chev-right", "lead")}<span>${p.next}</span>${icon("chev-right")}</button><button class="ibtn sm" aria-label="更多操作:${p.name}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span>
</li>`).join("")
|| `<li class="empty"><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="tbtn clear" data-clear>${STAT[stat].label}${icon("x")}<span class="vh">,清除筛选</span></button>` : "";
}
/* 「更多」菜单 */
let menuEl = null, menuBtn = null;
function closeMenu(focusBack) {
if (!menuEl) return;
menuEl.remove(); menuEl = null;
if (menuBtn) { menuBtn.setAttribute("aria-expanded", "false"); if (focusBack) menuBtn.focus(); }
menuBtn = null;
}
function openMenu(btn, p) {
closeMenu();
menuBtn = btn; btn.setAttribute("aria-expanded", "true");
menuEl = document.createElement("div");
menuEl.className = "menu"; menuEl.setAttribute("role", "menu");
const items = [["打开项目", ""], ...p.menu].map(([t, s]) => {
const sub = s === "cost" ? `${CHECK_COST.label}<span class="fig">${CHECK_COST.left}</span>${CHECK_COST.unit}` : s;
return `<button role="menuitem" data-act="${t}">${t}${sub ? `<small>${sub}</small>` : ""}</button>`;
});
items.splice(1, 0, "<hr>");
menuEl.innerHTML = items.join("");
document.body.appendChild(menuEl);
const r = btn.getBoundingClientRect();
menuEl.style.top = `${r.bottom + 4}px`;
menuEl.style.left = `${r.right - menuEl.offsetWidth}px`;
menuEl.addEventListener("click", (e) => {
const it = e.target.closest("[data-act]"); if (!it) return;
const a = it.dataset.act; closeMenu(true);
if (a === "打开项目" && p.href) location.href = p.href;
else toast(`(演示)${a}:${p.name}`);
});
menuEl.addEventListener("keydown", (e) => {
const its = $$("[role=menuitem]", menuEl), k = its.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); its[(k + 1) % its.length].focus(); }
if (e.key === "ArrowUp") { e.preventDefault(); its[(k - 1 + its.length) % its.length].focus(); }
});
$("[role=menuitem]", menuEl).focus();
}
$("[data-rows]").addEventListener("click", (e) => {
const row = e.target.closest(".row"); if (!row) return;
const p = P[+row.dataset.i];
const m = e.target.closest("[data-menu]");
if (m) { e.preventDefault(); e.stopPropagation(); if (menuBtn === m) closeMenu(); else openMenu(m, p); return; }
if (e.target.closest(".ready")) return;
if (e.target.closest("[data-next]")) {
if (p.href) location.href = p.href;
else toast(`(演示)${p.next}:${p.name}`);
return;
}
if (!e.target.closest(".pname")) { if (p.href) location.href = p.href; else toast(DEMO); }
});
document.addEventListener("click", (e) => { if (menuEl && !menuEl.contains(e.target)) closeMenu(); });
window.addEventListener("scroll", () => closeMenu(), { passive: true });
window.addEventListener("resize", () => closeMenu());
$("[data-seg]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; seg = b.dataset.v; 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(); } });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-gsearch]").addEventListener("submit", (e) => e.preventDefault());
document.addEventListener("click", (e) => { const d = e.target.closest("[data-demo]"); if (d) { e.preventDefault(); toast(DEMO); } });
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); $("[data-q]").focus(); }
if (e.key === "Escape") {
if (menuEl) closeMenu(true);
else if (document.activeElement === $("[data-q]")) $("[data-q]").blur();
}
});
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,654 @@
<!doctype html>
<!--
工作台 · 风格方案 D「明亮工作区」(探索稿,不是定稿)
数据、文案、交互沿用 ../page-workbench.html 与 ../demo.js。
本方案相对 design-system.md 改了哪些规则:
0d. (d4 修订)按用户意见去掉 d3 加的装饰图标(统计、区块标题、下一步、最近处理、顶栏),只保留原有的功能图标;问候语保留。
0c. (d4)侧栏与内容区融合:侧栏双色竖向渐变(墨 → 墨混 18% 蓝铅笔的深墨蓝,只作底色、不承担 AI 语义)+ 底部淡点阵;内容区左上角 12px 圆角压在深色上;当前菜单项:浅色半透明底 + 侧栏左缘 3px 白色竖条(?sel=bar 只有竖条,?sel=solid 白色实底),不再连进内容区(折叠动画会穿帮)。项目行去掉状态图标列。?v=flat 看 D3 的平铺侧栏对照。
0b. (d3)顶栏:透明落在页面底色上、滚动后变为半透明白 + 毛玻璃 + 发丝线,和墨色侧栏区分;搜索框靠右。图标:统计、区块标题、项目行、下一步、最近处理都加 Lucide 图标;图标底块只用中性浅灰,彩色只给有语义的(致命红、赭、蓝铅笔)。页头加问候语。
0. (d2)侧边栏改为墨色底(--c-ink),logo 用反白版;可折叠到 64px 只留图标,折叠状态记在本地。?side=light 可切换为浅灰侧栏对照。
1. §3.1 页面底色:校样纸 #F5F6F3 → --n-50(墨 3% 混白的中性冷灰);侧边栏、顶栏、卡片纯白。
偏绿的 --c-paper / --c-sunken / --c-hover / --c-rule 一律不用,浅底和分隔线只用下面三个从墨派生的值。
2. §5.1 层次:只有两种容器——页面(--n-50)和白卡片(圆角 12、1px --n-line、无阴影)。卡片里不再套面板。
可点击的整张卡片(本页没有)和浮层才用阴影;行、格的悬停只出 --n-50 底色。
3. §5.1 圆角:2px 一档 → 卡片 12、按钮 / 输入框 / 菜单 8、标签 6。
4. §7.1 按钮:高 32 → 36(主 / 次);表格行内 32。主按钮墨底白字;次按钮白底 + --c-muted 控件边框(3.9:1);
文字按钮悬停出 --n-100 底。普通链接仍是墨色 + 下划线,蓝铅笔只给出处。
5. §7.7 表头:凹陷灰底 + 粗底线 → 无底色,12px 辅助灰列名,无线。行高 64,行间 --n-line 发丝线。
6. §7.9 项目行:
- 「状态」中性描边标签 → --n-100 实底、圆角 6、无描边;全页只有这一种中性标签。
- 「就绪」从描边标签改为墨色普通字 + 虚线下划线(悬停说明不变),和「未就绪」并排写在状态标签后面,省掉一列。
- 「所投包」并入项目名下方的辅助灰行。
- 新增「下一步」列:默认是带箭头的文字链,整行悬停 / 键盘聚焦时变成次按钮,同时浮现「更多」图标按钮。
会扣点的操作(发起检查)放在「更多」里,并写明扣多少、还剩多少;有致命问题时「导出检查报告」仍可用。
7. 工作台统计:四张卡片 → 一张卡片四格,竖向发丝线分隔,数字 30 → 28(Barlow 600)。
每格可点击筛选项目列表,悬停出 --n-50 底,选中格底部 2px 墨色边条。
8. §5.3 顶栏:面包屑位置改为企业主体名;中间加全局搜索(白底 + 控件边框、圆角 8、Ctrl K),本页用它筛选项目列表。
9. §7.12 截止提醒条:赭色浅底条 → 同规格白卡片,赭色图标 + 赭色时间字。
10. §7.4「致命」行左侧 3px 竖线:工作台的待处理列表不用竖线,靠左侧实心「致命」标签扫读。
未改:颜色语义、问题级别形状、字体分工、字阶(正文 14 / 标题 16 / 页标题 24)、4px 间距基准、Lucide 图标。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 方案 D4</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); /* 发丝线、卡片边 */
--ctl-border: var(--c-muted); /* 控件边框 3.9:1 */
--primary-hover: color-mix(in srgb, var(--c-ink) 88%, white);
--r-card: 12px; --r-ctl: 8px; --r-tag: 6px;
--btn-h: 36px; --btn-h-sm: 32px;
--top-h: 56px;
--pad: 32px; /* 页面左右内边距 */
--gap: 16px; /* 卡片之间、主列与右栏之间:全页一个节奏 */
--card-x: 20px; /* 卡片内左右边距:所有卡片统一 */
--rail-w: 296px;
--side-w: var(--sidebar-w); --side-w-min: 64px;
/* 侧栏:墨色底。文字和底色都从墨、白派生,不引入新色相 */
--side-bg: var(--c-ink);
--side-fg: color-mix(in srgb, white 74%, var(--c-ink)); /* 未选中项 约 9:1 */
--side-fg-strong: var(--c-white);
--side-hover: color-mix(in srgb, white 7%, transparent); /* 半透明:在渐变上各处一致 */
--side-active: color-mix(in srgb, white 12%, transparent);
--side-line: color-mix(in srgb, white 12%, var(--c-ink));
--side-muted: color-mix(in srgb, white 58%, var(--c-ink)); /* 分组小字、页脚 约 6:1 */
--cols: minmax(0,1fr) 120px 120px 64px 168px; /* 项目行栅格 */
--col-gap: 16px;
}
@media (max-width: 1399px){
:root{ --pad: 24px; --rail-w: 256px; --cols: minmax(0,1fr) 108px 108px 56px 160px; --col-gap: 12px; }
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
body{background:var(--n-50);color:var(--text-primary);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);-webkit-font-smoothing:antialiased;min-width:1280px}
h1,h2,h3,p,ul{margin:0}
ul{padding:0;list-style:none}
a{color:inherit}
button{font:inherit;color:inherit}
.fig{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold)}
.sec{color:var(--text-secondary)}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.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}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px;border-radius:var(--r-ctl)}
/* ---------- 链接与按钮 ---------- */
.lnk{color:var(--text-primary);text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px;transition:text-decoration-color var(--dur-fast) var(--ease)}
.lnk:hover{text-decoration-color:var(--c-ink)}
.cite{display:inline-flex;align-items:center;gap:4px;color:var(--ai);text-decoration:none;white-space:nowrap;border-radius:var(--r-ctl);padding:2px 6px;margin:-2px -6px;transition:background-color var(--dur-fast) var(--ease)}
.cite:hover{background:var(--ai-soft);color:var(--ai-strong)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:var(--btn-h);padding:0 14px;border-radius:var(--r-ctl);border:1px solid var(--ctl-border);background:var(--c-white);color:var(--text-primary);font-size:var(--fs-body);line-height:1;white-space:nowrap;text-decoration:none;cursor:pointer;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(--c-white);font-weight:var(--fw-bold)}
.btn.primary:hover{background:var(--primary-hover)}
.btn.sm{height:var(--btn-h-sm);padding:0 12px}
.tbtn{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;border:0;border-radius:var(--r-ctl);background:transparent;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background-color var(--dur-fast) var(--ease)}
.tbtn:hover{background:var(--n-100)}
.ibtn{position:relative;display:inline-grid;place-items:center;width:var(--btn-h);height:var(--btn-h);border:0;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.ibtn:hover{background:var(--n-100)}
.ibtn.sm{width:var(--btn-h-sm);height:var(--btn-h-sm)}
/* ---------- 标签:中性一种 + 问题级别 ---------- */
.tag{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);white-space:nowrap}
.sev{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 8px;border-radius:var(--r-tag);font-size:var(--fs-caption);font-weight:var(--fw-bold);white-space:nowrap}
.sev.fatal{background:var(--sev-fatal);color:var(--c-white)}
.sev .fig{font-weight:var(--fw-bold)}
.dash{color:var(--text-secondary)}
/* ---------- 应用框架 ---------- */
.app{display:grid;grid-template-columns:var(--side-w) minmax(0,1fr);min-height:100vh;transition:grid-template-columns var(--dur-base) var(--ease)}
.app[data-collapsed]{--side-w:var(--side-w-min)}
.side{background:var(--side-bg);color:var(--side-fg);min-width:0}
/* ---- d4:侧栏与内容区融合 ---- */
.app:not([data-side="light"]):not([data-v="flat"]){
--side-deep: color-mix(in srgb, var(--c-ink) 82%, var(--c-pencil)); /* 渐变下端:深墨蓝 */
background:linear-gradient(180deg, var(--c-ink) 0%, var(--side-deep) 100%) fixed;
}
.app:not([data-side="light"]):not([data-v="flat"]) .side{background:transparent;position:relative}
.app:not([data-side="light"]):not([data-v="flat"]) .side-in::after{content:"";position:absolute;left:0;right:0;bottom:0;height:260px;pointer-events:none;
background-image:radial-gradient(color-mix(in srgb, white 14%, transparent) 1px, transparent 1.2px);background-size:14px 14px;
-webkit-mask-image:linear-gradient(to top, black, transparent);mask-image:linear-gradient(to top, black, transparent)}
.app:not([data-side="light"]):not([data-v="flat"]) .side-in > *{position:relative;z-index:1}
.app:not([data-side="light"]):not([data-v="flat"]) .main{background:var(--n-50);border-top-left-radius:12px;min-height:100vh}
.app:not([data-side="light"]):not([data-v="flat"]) .top{border-top-left-radius:12px}
/* 浅灰侧栏对照(?side=light) */
.app[data-side="light"]{--side-bg:color-mix(in srgb, var(--c-ink) 9%, white);--side-fg:var(--text-secondary);--side-fg-strong:var(--text-primary);--side-hover:color-mix(in srgb, var(--c-ink) 14%, white);--side-active:var(--c-white);--side-line:var(--n-line);--side-muted:var(--text-secondary)}
.app[data-side="light"] .side{border-right:1px solid var(--n-line)}
.app[data-side="light"] .nav a[aria-current]{box-shadow:0 0 0 1px var(--n-line)}
.brand .mini{display:none;height:32px;width:32px}
.app[data-collapsed] .brand .full{display:none}
.app[data-collapsed] .brand .mini{display:block}
.app[data-side="light"] .brand .full.rev,.app:not([data-side="light"]) .brand .full.std{display:none}
.side-in{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:0 12px 12px;overflow:hidden}
.brand{display:flex;align-items:center;justify-content:center;height:72px;padding:0;margin:0 0 8px;flex:none}
.brand:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px;border-radius:var(--r-ctl)}
.brand img{height:34px;display:block}
.nav{display:flex;flex-direction:column;gap:2px}
.nav a,.side-btn{position:relative;display:flex;align-items:center;gap:10px;height:var(--btn-h);padding:0 10px;border-radius:var(--r-ctl);text-decoration:none;color:var(--side-fg);white-space:nowrap;transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.nav a .ic,.side-btn .ic{flex:none}
.nav a:hover,.side-btn:hover{background:var(--side-hover);color:var(--side-fg-strong)}
.nav a[aria-current]{background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold)}
/* 选中样式(d4 修订):选中效果全部留在侧栏内,折叠动画中不会露出。?sel=bar / solid 切换对照 */
.nav a[aria-current]::before{content:"";position:absolute;left:-12px;top:8px;bottom:8px;width:3px;border-radius:0 3px 3px 0;background:var(--side-fg-strong)}
.app[data-sel="bar"] .nav a[aria-current]{background:transparent}
.app[data-sel="bar"] .nav a[aria-current]:hover{background:var(--side-hover)}
.app[data-sel="solid"] .nav a[aria-current]{background:var(--c-white);color:var(--text-primary)}
.app[data-sel="solid"] .nav a[aria-current]::before{display:none}
.app[data-side="light"] .nav a[aria-current]::before{background:var(--c-ink)}
.nav a:focus-visible,.side-btn:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px}
.nav .grp{margin:20px 10px 6px;height:20px;font-size:var(--fs-caption);line-height:20px;color:var(--side-muted);white-space:nowrap}
.side-btn{width:100%;border:0;background:transparent;font:inherit;cursor:pointer;margin-top:auto}
.side-foot{display:flex;align-items:center;gap:10px;margin-top:8px;padding:12px 6px 0;border-top:1px solid var(--side-line);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--side-muted);white-space:nowrap}
.side-foot b{display:block;color:var(--side-fg-strong);font-size:var(--fs-body);line-height:var(--lh-body);font-weight:var(--fw-bold)}
.avatar{flex:none;display:inline-grid;place-items:center;width:32px;height:32px;border-radius:50%;background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold);font-size:var(--fs-body)}
.app[data-side="light"] .avatar{background:var(--c-ink);color:var(--c-white)}
/* 折叠:只留图标;文字保留给读屏,悬停出提示 */
.app[data-collapsed] .lbl,.app[data-collapsed] .side-foot .who{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.app[data-collapsed] .nav a,.app[data-collapsed] .side-btn{justify-content:center;padding:0;width:40px}
.app[data-collapsed] .nav .grp{margin:20px 4px 6px;font-size:0;border-top:1px solid var(--side-line);height:0}
.app[data-collapsed] .side-foot{justify-content:center;padding:12px 0 0}
.app[data-collapsed] .brand{justify-content:center;padding:0}
.app[data-collapsed] [data-stip]:hover::after,.app[data-collapsed] [data-stip]:focus-visible::after{content:attr(data-stip);position:absolute;z-index:40;left:calc(100% + 12px);top:50%;transform:translateY(-50%);width:max-content;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:var(--fw-regular);box-shadow:var(--shadow-overlay);pointer-events:none}
.side-in:has([data-stip]:hover){overflow:visible}
.top{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:12px;height:var(--top-h);padding:0 var(--pad);background:transparent;border-bottom:1px solid transparent;transition:background-color var(--dur-base) var(--ease),border-color var(--dur-base) var(--ease)}
.top.scrolled{background:color-mix(in srgb, var(--c-white) 82%, transparent);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-bottom-color:var(--n-line)}
.top .search{margin-left:auto;width:280px;transition:width var(--dur-base) var(--ease),border-color var(--dur-fast) var(--ease)}
.top .search:focus-within{width:360px}
.top .vsep{width:1px;height:20px;background:var(--n-line);margin:0 4px}
.org .ic{color:var(--text-secondary)}
.org{justify-self:start;display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary)}
.org b{color:var(--text-primary);font-weight:var(--fw-bold)}
.search{position:relative;display:flex;align-items:center;height:var(--btn-h);border:1px solid var(--ctl-border);border-radius:var(--r-ctl);background:var(--c-white);transition:border-color var(--dur-fast) var(--ease)}
.search:hover{border-color:var(--c-ink)}
.search:focus-within{border-color:var(--c-ink);box-shadow:0 0 0 1px var(--c-ink)}
.search > .ic{position:absolute;left:11px;color:var(--text-secondary);pointer-events:none}
.search input{flex:1;min-width:0;height:100%;border:0;background:transparent;padding:0 8px 0 34px;font:inherit;color:var(--text-primary);outline:0}
.search input::placeholder{color:var(--text-secondary)}
.search input::-webkit-search-cancel-button{display:none}
.kbd{flex:none;margin-right:6px;padding:0 6px;height:22px;display:inline-flex;align-items:center;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-secondary);font-size:var(--fs-caption);font-family:var(--font-figures);font-weight:var(--fw-bold)}
.top-r{justify-self:end;display:flex;align-items:center;gap:4px}
.qchip{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;color:var(--text-secondary)}
.qchip .fig{color:var(--text-primary);font-size:15px}
.qchip .sep{width:1px;height:12px;background:var(--n-line);margin:0 4px}
.dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--c-ink);box-shadow:0 0 0 2px var(--c-white)}
.content{padding:20px var(--pad) 48px}
.ch h2,.ch h3{display:inline-flex;align-items:center;gap:8px}
.ch h2 > .ic,.ch h3 > .ic{color:var(--text-secondary)}
/* ---------- 页头 ---------- */
.ph{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}
.ph h1{font-family:var(--font-serif);font-weight:var(--fw-bold);font-size:var(--fs-h1);line-height:var(--lh-h1)}
.ph p{margin-top:2px;color:var(--text-secondary)}
.warn-t{color:var(--warn-text);font-weight:var(--fw-bold)}
.ph .act{display:flex;align-items:center;gap:12px}
.cost{font-size:var(--fs-caption);color:var(--text-secondary);white-space:nowrap}
.cost .fig{color:var(--text-primary)}
/* ---------- 唯一的容器:白卡片 ---------- */
.card{background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-card)}
.ch{display:flex;align-items:center;gap:12px;min-height:56px;padding:12px var(--card-x)}
.ch h2,.ch h3{font-size:var(--fs-title);line-height:var(--lh-title);font-weight:var(--fw-bold)}
.ch .more{margin-left:auto}
.cf{padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.stack > * + *{margin-top:var(--gap)}
/* 截止提醒 */
.alert{display:flex;align-items:center;gap:14px;padding:14px var(--card-x);margin-bottom:var(--gap)}
.alert > .ic{color:var(--c-ochre)}
.alert p{flex:1}
.alert .when{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 统计:一张卡片四格 */
.stats{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:var(--gap);overflow:hidden}
.stat{position:relative;display:grid;grid-template-rows:auto auto auto;justify-items:start;gap:2px;padding:16px var(--card-x) 16px;border:0;border-left:1px solid var(--n-line);background:transparent;text-align:left;cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.stat:first-child{border-left:0}
.stat:hover{background:var(--n-50)}
.stat::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--c-ink);transform:scaleY(0);transform-origin:bottom;transition:transform var(--dur-fast) var(--ease)}
.stat[aria-pressed="true"]::after{transform:scaleY(1)}
.stat[aria-pressed="true"] small{color:var(--text-primary);font-weight:var(--fw-bold)}
.stat:focus-visible{outline-offset:-2px;border-radius:0}
.stat small{font-size:var(--fs-body);color:var(--text-secondary)}
.stat .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.stat .big.fatal{color:var(--sev-fatal)}
.stat .big.due{color:var(--warn-text)}
.stat > .ic{position:absolute;top:18px;right:var(--card-x);color:var(--text-secondary);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.stat:hover > .ic,.stat:focus-visible > .ic,.stat[aria-pressed="true"] > .ic{opacity:1}
.stat[aria-pressed="true"] > .ic{color:var(--text-primary)}
.stat p{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.stat p.due{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 两栏 */
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--rail-w);gap:var(--gap);align-items:start}
/* 项目卡片头:筛选 */
.filters{display:flex;align-items:center;gap:2px;margin-left:8px}
.filters .tbtn{color:var(--text-secondary)}
.filters .tbtn .fig{font-size:15px}
.filters .tbtn[aria-pressed="true"]{background:var(--n-100);color:var(--text-primary);font-weight:var(--fw-bold)}
.clear{margin-left:auto;color:var(--text-primary)}
/* 项目行 */
.grid-cols{display:grid;grid-template-columns:var(--cols);column-gap:var(--col-gap);align-items:center;padding:0 var(--card-x)}
.cols{padding-bottom:8px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.cols .r,.row .r{justify-self:end;text-align:right}
.cols .nx-h{padding-right:48px}
.row{position:relative;min-height:64px;padding-top:10px;padding-bottom:10px;border-top:1px solid var(--n-line);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.row:hover,.row:focus-within{background:var(--n-50)}
.row:has(.pname:focus-visible){outline:2px solid var(--focus-ring);outline-offset:-2px}
.pcell{display:flex;align-items:center;gap:10px;min-width:0}
.pcell .pt{min-width:0}
.pname{display:block;font-weight:var(--fw-bold);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;outline:0}
.pname::after{content:"";position:absolute;inset:0}
.psub{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.due b{display:block;font-weight:var(--fw-regular);white-space:nowrap}
.due small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap}
.due.today b{color:var(--warn-text);font-weight:var(--fw-bold)}
.due.soon b{color:var(--warn-text)}
.due.past b{color:var(--text-secondary)}
.st{display:flex;align-items:center;gap:8px;white-space:nowrap}
.ready{position:relative;z-index:1;text-decoration:underline dashed;text-decoration-color:color-mix(in srgb, var(--c-ink) 40%, white);text-underline-offset:4px;cursor:help}
.row.arch .pname,.row.arch .due b{color:var(--text-secondary);font-weight:var(--fw-regular)}
/* 下一步:默认文字链,行悬停 / 聚焦变次按钮,同时浮现「更多」 */
.nx-cell{display:flex;align-items:center;justify-content:flex-end;gap:4px}
.nx{position:relative;z-index:1;display:inline-flex;align-items:center;gap:4px;height:var(--btn-h-sm);padding:0 10px 0 12px;border:1px solid transparent;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);white-space:nowrap;cursor:pointer;transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.nx span{text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px}
.nx .ic{color:var(--text-secondary);transition:transform var(--dur-fast) var(--ease)}
.row:hover .nx .ic,.row:focus-within .nx .ic{transform:translateX(3px);color:var(--text-primary)}
.row .nx:hover span{text-decoration-color:var(--c-ink)}
.row .ibtn{position:relative;z-index:1;opacity:0;transition:opacity var(--dur-fast) var(--ease),background-color var(--dur-fast) var(--ease)}
.row:hover .ibtn,.row:focus-within .ibtn,.row .ibtn[aria-expanded="true"]{opacity:1}
.empty{padding:32px var(--card-x);border-top:1px solid var(--n-line);text-align:center}
.empty b{display:block;font-weight:var(--fw-bold)}
.empty p{color:var(--text-secondary);margin-top:4px}
/* 待处理的致命问题 */
.todo{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;column-gap:16px;align-items:center;padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.todo b{display:block;font-weight:var(--fw-bold)}
.todo .psub{white-space:normal}
.todo .cite{margin-right:2px}
/* 右栏 */
.rail .ch{min-height:52px;padding-bottom:4px}
.rbody{padding:4px var(--card-x) 16px}
.task-h{display:flex;align-items:baseline;gap:8px}
.task-h b{font-weight:var(--fw-bold)}
.task-h .cap{margin-left:auto}
.pbar{display:flex;align-items:center;gap:10px;margin-top:10px}
.bar{flex:1;height:6px;border-radius:3px;background:var(--n-100);overflow:hidden}
.bar i{display:block;height:100%;background:var(--c-ink);border-radius:3px}
.queue{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding-top:12px;border-top:1px solid var(--n-line)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.q2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.q2 small .ic{vertical-align:-3px;margin-right:4px;color:var(--text-secondary)}
.q2 small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.q2 .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.q2 .unit{margin-left:4px;color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums}
.q2 .bar{margin-top:6px}
.links{display:flex;gap:16px}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:8px 0;border-top:1px solid var(--n-line);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:13px}
.log .who{font-weight:var(--fw-bold)}
.av{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);font-weight:var(--fw-bold);line-height:1}
.av .ic{width:14px;height:14px;color:var(--text-secondary)}
.log .ok{color:var(--confirmed);white-space:nowrap}
/* ---------- 浮层:菜单、提示、轻提示 ---------- */
.menu{position:fixed;z-index:20;min-width:248px;padding:6px;background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-ctl);box-shadow:var(--shadow-overlay)}
.menu button{display:block;width:100%;padding:7px 10px;border:0;border-radius:6px;background:transparent;text-align:left;cursor:pointer}
.menu button:hover,.menu button:focus-visible{background:var(--n-100);outline:0}
.menu button small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.menu button small .fig{color:var(--text-primary)}
.menu hr{border:0;border-top:1px solid var(--n-line);margin:6px 0}
[data-tip]{position:relative}
[data-tip]:hover::before,[data-tip]:focus-visible::before{content:attr(data-tip);position:absolute;z-index:30;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:max-content;max-width:260px;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);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-r:hover::before,[data-tip].tip-r:focus-visible::before{left:auto;right:0;transform:none}
.toasts{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:40;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(--c-white);box-shadow:var(--shadow-overlay)}
</style>
</head>
<body>
<div class="app">
<aside class="side"><div class="side-in">
<a class="brand" href="workbench-d4.html" aria-label="标博士"><img class="full rev" src="../../logo/svg/logo-horizontal-reverse.svg" alt=""><img class="full std" src="../../logo/svg/logo-horizontal.svg" alt=""><img class="mini" src="../../logo/svg/seal-small.svg" alt=""></a>
<nav class="nav" aria-label="主导航">
<a href="workbench-d4.html" aria-current="page" data-stip="工作台"><i data-ic="grid" class="lg"></i><span class="lbl">工作台</span></a>
<a href="#" data-demo data-stip="资质库"><i data-ic="cert" class="lg"></i><span class="lbl">资质库</span></a>
<a href="#" data-demo data-stip="额度"><i data-ic="gauge" class="lg"></i><span class="lbl">额度</span></a>
<div class="grp">UI demo</div>
<a href="index.html" data-stip="方案目录"><i data-ic="book" class="lg"></i><span class="lbl">方案目录</span></a>
</nav>
<button class="side-btn" type="button" data-collapse aria-expanded="true" aria-keyshortcuts="Control+Backslash" data-stip="展开侧栏(Ctrl \)"><i data-ic="panel-close" class="lg"></i><span class="lbl">收起侧栏</span></button>
<div class="side-foot" data-stip="王敏 · 专业版"><span class="avatar" aria-hidden="true">王</span><span class="who"><b>王敏</b>专业版 · 至 2027-10-31</span></div>
</div></aside>
<div class="main">
<header class="top">
<span class="org"><b>南京某信息技术有限公司</b><span>/</span>工作台</span>
<form class="search" role="search" data-gsearch>
<i data-ic="search"></i>
<input type="search" placeholder="搜索项目、采购人" aria-label="搜索项目、采购人" aria-keyshortcuts="Control+K" data-q>
<span class="kbd" aria-hidden="true">Ctrl K</span>
</form>
<span class="vsep" aria-hidden="true"></span>
<div class="top-r">
<a class="qchip tbtn" href="#" data-demo data-tip="本期剩余额度:解析 37 / 50 次,检查 12 / 20 次" aria-label="本期剩余额度:解析 37 次,检查 12 次">解析<span class="fig">37</span><span class="sep"></span>检查<span class="fig">12</span></a>
<button class="ibtn tip-r" aria-label="通知(1 条未读)" data-tip="通知" data-notice><i data-ic="bell" class="lg"></i><span class="dot"></span></button>
</div>
</header>
<main class="content">
<div class="ph">
<div><h1>工作台</h1><p>下午好,王敏。今天是 11月3日,周二。</p></div>
<div class="act">
<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="card alert" role="note">
<i data-ic="clock" class="lg"></i>
<p><b>某县数据中心机房改造项目</b> <span class="when">今天 17:00 截止</span>,清单还没确认。</p>
<a class="btn sm" href="../page-reading.html"><i data-ic="list"></i>确认清单</a>
</div>
<section class="card 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 class="stack">
<section class="card" aria-labelledby="h-proj">
<div class="ch">
<h2 id="h-proj">项目</h2>
<div class="filters" role="group" aria-label="按状态筛选" data-seg>
<button class="tbtn" aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button>
<button class="tbtn" aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button>
<button class="tbtn" aria-pressed="false" data-v="已递交">已递交</button>
<button class="tbtn" aria-pressed="false" data-v="已归档">已归档</button>
</div>
<span class="more" data-chip></span>
</div>
<div class="cols grid-cols" aria-hidden="true"><span>项目</span><span>投标截止</span><span>状态</span><span class="r">致命</span><span class="r nx-h">下一步</span></div>
<ul data-rows aria-label="项目列表"></ul>
</section>
<section class="card" aria-labelledby="h-todo">
<div class="ch">
<h2 id="h-todo">待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" href="#" data-demo>查看全部</a>
</div>
<ul>
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite" 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" 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" 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>
</section>
</div>
<div class="stack rail">
<section class="card" aria-labelledby="h-task">
<div class="ch"><h3 id="h-task">正在进行的任务</h3></div>
<div class="rbody">
<div class="task-h"><b>解析补遗 2</b><span class="cap sec">提取清单</span></div>
<div class="cap sec">某市医保信息系统等保测评服务</div>
<div class="pbar"><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 sec">检查排队中:智慧社区平台运维,前面 1 个</span></div>
</div>
</section>
<section class="card" aria-labelledby="h-quota">
<div class="ch"><h3 id="h-quota">本期额度</h3><span class="more cap sec">至 2027-10-31</span></div>
<div class="rbody">
<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="cf links cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card" aria-labelledby="h-log">
<div class="ch"><h3 id="h-log">最近处理</h3><a class="more lnk cap" href="#" data-demo>全部记录</a></div>
<div class="rbody">
<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>
</div>
</div>
</main>
</div>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '<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"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
bell: '<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"/>',
book: '<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"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
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"/>',
list: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/>',
"chev-right": '<path d="m9 18 6-6-6-6"/>',
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
building: '<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4M10 10h4M10 14h4M10 18h4"/>',
folders: '<path d="M20 17a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3.9a2 2 0 0 1-1.69-.9l-.81-1.2a2 2 0 0 0-1.67-.9H8a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2Z"/><path d="M2 8v11a2 2 0 0 0 2 2h14"/>',
"calendar-clock": '<path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M16 2v4M8 2v4M3 10h5"/><path d="M17.5 17.5 16 16.3V14"/><circle cx="16" cy="16" r="6"/>',
"octagon-alert": '<path d="M12 16h.01M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/>',
activity: '<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/>',
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
loader: '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
"file-search": '<path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"/><path d="m9 18-1.5-1.5"/><circle cx="5" cy="14" r="3"/>',
scan: '<path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>',
bookmark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
pen: '<path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/>',
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
archive: '<rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>',
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
wrench: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
cpu: '<rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2M15 20v2M2 15h2M2 9h2M20 15h2M20 9h2M9 2v2M9 20v2"/>',
"panel-close": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/>',
"panel-open": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
$$("i[data-ic]").forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
/* 顶栏:滚动后变为半透明白 + 毛玻璃 */
const topEl = $(".top"); const onScroll = () => topEl.classList.toggle("scrolled", scrollY > 4); addEventListener("scroll", onScroll, { passive: true }); onScroll();
/* 侧栏:主题(?side=light 为浅灰对照)与折叠(记在本地,读写失败不影响页面) */
const app = $(".app");
if (new URLSearchParams(location.search).get("side") === "light") app.dataset.side = "light";
if (new URLSearchParams(location.search).get("v") === "flat") app.dataset.v = "flat";
const sel = new URLSearchParams(location.search).get("sel"); if (sel === "bar" || sel === "solid") app.dataset.sel = sel;
const colBtn = $("[data-collapse]");
function setCollapsed(on, save) {
app.toggleAttribute("data-collapsed", on);
colBtn.setAttribute("aria-expanded", String(!on));
$(".lbl", colBtn).textContent = on ? "展开侧栏" : "收起侧栏";
colBtn.dataset.stip = on ? "展开侧栏(Ctrl \\)" : "收起侧栏(Ctrl \\)";
$(".ic", colBtn).outerHTML = icon(on ? "panel-open" : "panel-close", "lg");
if (save) { try { localStorage.setItem("bd-side-collapsed", on ? "1" : "0"); } catch (e) {} }
}
let saved = null; try { saved = localStorage.getItem("bd-side-collapsed"); } catch (e) {}
const forced = new URLSearchParams(location.search).get("collapsed");
setCollapsed(forced ? forced === "1" : saved === "1", false);
colBtn.addEventListener("click", () => setCollapsed(!app.hasAttribute("data-collapsed"), true));
document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.key === "\\") { e.preventDefault(); setCollapsed(!app.hasAttribute("data-collapsed"), true); } });
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(), 4000);
}
const DEMO = "(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页";
/* 数据沿用 page-workbench.html;pend 为待确认清单条目数(对应统计「在 2 个项目里」)。
next 为「下一步」,menu 为「更多」里的操作;cost 表示会扣点。 */
const CHECK_COST = { label: "扣 1 次检查 · 剩 ", left: 12, unit: " 次" };
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "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: "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: "past", st: "已递交", ready: "", fatal: 0, pend: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "past", st: "已归档", ready: "", fatal: 0, pend: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
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 = "";
function status(p) {
let r = "";
if (p.ready === "就绪") r = '<span class="ready" 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>`;
}
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="row grid-cols ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span class="pcell"><span class="pt"><a class="pname" href="${p.href || "#"}" title="${p.name}" ${p.href ? "" : "data-demo"}>${p.name}</a><span class="psub">${p.buyer} · ${p.lot}</span></span></span>
<span class="due ${p.cls}"><b>${p.due[0]}</b><small>${p.due[1]}</small></span>
${status(p)}
<span class="r">${p.fatal ? `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>` : '<span class="dash" aria-label="无">—</span>'}</span>
<span class="nx-cell"><button class="nx" data-next><span>${p.next}</span>${icon("chev-right")}</button><button class="ibtn sm" aria-label="更多操作:${p.name}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span>
</li>`).join("")
|| `<li class="empty"><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="tbtn clear" data-clear>${STAT[stat].label}${icon("x")}<span class="vh">,清除筛选</span></button>` : "";
}
/* 「更多」菜单 */
let menuEl = null, menuBtn = null;
function closeMenu(focusBack) {
if (!menuEl) return;
menuEl.remove(); menuEl = null;
if (menuBtn) { menuBtn.setAttribute("aria-expanded", "false"); if (focusBack) menuBtn.focus(); }
menuBtn = null;
}
function openMenu(btn, p) {
closeMenu();
menuBtn = btn; btn.setAttribute("aria-expanded", "true");
menuEl = document.createElement("div");
menuEl.className = "menu"; menuEl.setAttribute("role", "menu");
const items = [["打开项目", ""], ...p.menu].map(([t, s]) => {
const sub = s === "cost" ? `${CHECK_COST.label}<span class="fig">${CHECK_COST.left}</span>${CHECK_COST.unit}` : s;
return `<button role="menuitem" data-act="${t}">${t}${sub ? `<small>${sub}</small>` : ""}</button>`;
});
items.splice(1, 0, "<hr>");
menuEl.innerHTML = items.join("");
document.body.appendChild(menuEl);
const r = btn.getBoundingClientRect();
menuEl.style.top = `${r.bottom + 4}px`;
menuEl.style.left = `${r.right - menuEl.offsetWidth}px`;
menuEl.addEventListener("click", (e) => {
const it = e.target.closest("[data-act]"); if (!it) return;
const a = it.dataset.act; closeMenu(true);
if (a === "打开项目" && p.href) location.href = p.href;
else toast(`(演示)${a}:${p.name}`);
});
menuEl.addEventListener("keydown", (e) => {
const its = $$("[role=menuitem]", menuEl), k = its.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); its[(k + 1) % its.length].focus(); }
if (e.key === "ArrowUp") { e.preventDefault(); its[(k - 1 + its.length) % its.length].focus(); }
});
$("[role=menuitem]", menuEl).focus();
}
$("[data-rows]").addEventListener("click", (e) => {
const row = e.target.closest(".row"); if (!row) return;
const p = P[+row.dataset.i];
const m = e.target.closest("[data-menu]");
if (m) { e.preventDefault(); e.stopPropagation(); if (menuBtn === m) closeMenu(); else openMenu(m, p); return; }
if (e.target.closest(".ready")) return;
if (e.target.closest("[data-next]")) {
if (p.href) location.href = p.href;
else toast(`(演示)${p.next}:${p.name}`);
return;
}
if (!e.target.closest(".pname")) { if (p.href) location.href = p.href; else toast(DEMO); }
});
document.addEventListener("click", (e) => { if (menuEl && !menuEl.contains(e.target)) closeMenu(); });
window.addEventListener("scroll", () => closeMenu(), { passive: true });
window.addEventListener("resize", () => closeMenu());
$("[data-seg]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; seg = b.dataset.v; 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(); } });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-gsearch]").addEventListener("submit", (e) => e.preventDefault());
document.addEventListener("click", (e) => { const d = e.target.closest("[data-demo]"); if (d) { e.preventDefault(); toast(DEMO); } });
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); $("[data-q]").focus(); }
if (e.key === "Escape") {
if (menuEl) closeMenu(true);
else if (document.activeElement === $("[data-q]")) $("[data-q]").blur();
}
});
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,658 @@
<!doctype html>
<!--
工作台 · 风格方案 D「明亮工作区」(探索稿,不是定稿)
数据、文案、交互沿用 ../page-workbench.html 与 ../demo.js。
本方案相对 design-system.md 改了哪些规则:
0e. (d5)侧栏改为单一状态的窄栏(96px,不再折叠):竖版 logo;菜单上图标、下文字;当前项用圆角矩形的半透明浅色底 + 白色加粗文字,不再连进内容区。图标换为 Lucide house / award / wallet。
0d. (d4 修订)按用户意见去掉 d3 加的装饰图标(统计、区块标题、下一步、最近处理、顶栏),只保留原有的功能图标;问候语保留。
0c. (d4)侧栏与内容区融合:侧栏双色竖向渐变(墨 → 墨混 18% 蓝铅笔的深墨蓝,只作底色、不承担 AI 语义)+ 底部淡点阵;内容区左上角 12px 圆角压在深色上;当前菜单项:浅色半透明底 + 侧栏左缘 3px 白色竖条(?sel=bar 只有竖条,?sel=solid 白色实底),不再连进内容区(折叠动画会穿帮)。项目行去掉状态图标列。?v=flat 看 D3 的平铺侧栏对照。
0b. (d3)顶栏:透明落在页面底色上、滚动后变为半透明白 + 毛玻璃 + 发丝线,和墨色侧栏区分;搜索框靠右。图标:统计、区块标题、项目行、下一步、最近处理都加 Lucide 图标;图标底块只用中性浅灰,彩色只给有语义的(致命红、赭、蓝铅笔)。页头加问候语。
0. (d2)侧边栏改为墨色底(--c-ink),logo 用反白版;可折叠到 64px 只留图标,折叠状态记在本地。?side=light 可切换为浅灰侧栏对照。
1. §3.1 页面底色:校样纸 #F5F6F3 → --n-50(墨 3% 混白的中性冷灰);侧边栏、顶栏、卡片纯白。
偏绿的 --c-paper / --c-sunken / --c-hover / --c-rule 一律不用,浅底和分隔线只用下面三个从墨派生的值。
2. §5.1 层次:只有两种容器——页面(--n-50)和白卡片(圆角 12、1px --n-line、无阴影)。卡片里不再套面板。
可点击的整张卡片(本页没有)和浮层才用阴影;行、格的悬停只出 --n-50 底色。
3. §5.1 圆角:2px 一档 → 卡片 12、按钮 / 输入框 / 菜单 8、标签 6。
4. §7.1 按钮:高 32 → 36(主 / 次);表格行内 32。主按钮墨底白字;次按钮白底 + --c-muted 控件边框(3.9:1);
文字按钮悬停出 --n-100 底。普通链接仍是墨色 + 下划线,蓝铅笔只给出处。
5. §7.7 表头:凹陷灰底 + 粗底线 → 无底色,12px 辅助灰列名,无线。行高 64,行间 --n-line 发丝线。
6. §7.9 项目行:
- 「状态」中性描边标签 → --n-100 实底、圆角 6、无描边;全页只有这一种中性标签。
- 「就绪」从描边标签改为墨色普通字 + 虚线下划线(悬停说明不变),和「未就绪」并排写在状态标签后面,省掉一列。
- 「所投包」并入项目名下方的辅助灰行。
- 新增「下一步」列:默认是带箭头的文字链,整行悬停 / 键盘聚焦时变成次按钮,同时浮现「更多」图标按钮。
会扣点的操作(发起检查)放在「更多」里,并写明扣多少、还剩多少;有致命问题时「导出检查报告」仍可用。
7. 工作台统计:四张卡片 → 一张卡片四格,竖向发丝线分隔,数字 30 → 28(Barlow 600)。
每格可点击筛选项目列表,悬停出 --n-50 底,选中格底部 2px 墨色边条。
8. §5.3 顶栏:面包屑位置改为企业主体名;中间加全局搜索(白底 + 控件边框、圆角 8、Ctrl K),本页用它筛选项目列表。
9. §7.12 截止提醒条:赭色浅底条 → 同规格白卡片,赭色图标 + 赭色时间字。
10. §7.4「致命」行左侧 3px 竖线:工作台的待处理列表不用竖线,靠左侧实心「致命」标签扫读。
未改:颜色语义、问题级别形状、字体分工、字阶(正文 14 / 标题 16 / 页标题 24)、4px 间距基准、Lucide 图标。
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>工作台 · 方案 D5</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); /* 发丝线、卡片边 */
--ctl-border: var(--c-muted); /* 控件边框 3.9:1 */
--primary-hover: color-mix(in srgb, var(--c-ink) 88%, white);
--r-card: 12px; --r-ctl: 8px; --r-tag: 6px;
--btn-h: 36px; --btn-h-sm: 32px;
--top-h: 56px;
--pad: 32px; /* 页面左右内边距 */
--gap: 16px; /* 卡片之间、主列与右栏之间:全页一个节奏 */
--card-x: 20px; /* 卡片内左右边距:所有卡片统一 */
--rail-w: 296px;
--side-w: var(--sidebar-w); --side-w-min: 64px;
/* 侧栏:墨色底。文字和底色都从墨、白派生,不引入新色相 */
--side-bg: var(--c-ink);
--side-fg: color-mix(in srgb, white 74%, var(--c-ink)); /* 未选中项 约 9:1 */
--side-fg-strong: var(--c-white);
--side-hover: color-mix(in srgb, white 7%, transparent); /* 半透明:在渐变上各处一致 */
--side-active: color-mix(in srgb, white 14%, transparent);
--side-line: color-mix(in srgb, white 12%, var(--c-ink));
--side-muted: color-mix(in srgb, white 58%, var(--c-ink)); /* 分组小字、页脚 约 6:1 */
--cols: minmax(0,1fr) 120px 120px 64px 140px 32px; /* 项目行栅格 */
--col-gap: 16px;
}
@media (max-width: 1399px){
:root{ --pad: 24px; --rail-w: 256px; --cols: minmax(0,1fr) 108px 108px 56px 132px 32px; --col-gap: 12px; }
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
body{background:var(--n-50);color:var(--text-primary);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);-webkit-font-smoothing:antialiased;min-width:1280px}
h1,h2,h3,p,ul{margin:0}
ul{padding:0;list-style:none}
a{color:inherit}
button{font:inherit;color:inherit}
.fig{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold)}
.sec{color:var(--text-secondary)}
.cap{font-size:var(--fs-caption);line-height:var(--lh-caption)}
.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}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px;border-radius:var(--r-ctl)}
/* ---------- 链接与按钮 ---------- */
.lnk{color:var(--text-primary);text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px;transition:text-decoration-color var(--dur-fast) var(--ease)}
.lnk:hover{text-decoration-color:var(--c-ink)}
.cite{display:inline-flex;align-items:center;gap:4px;color:var(--ai);text-decoration:none;white-space:nowrap;border-radius:var(--r-ctl);padding:2px 6px;margin:-2px -6px;transition:background-color var(--dur-fast) var(--ease)}
.cite:hover{background:var(--ai-soft);color:var(--ai-strong)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:var(--btn-h);padding:0 14px;border-radius:var(--r-ctl);border:1px solid var(--ctl-border);background:var(--c-white);color:var(--text-primary);font-size:var(--fs-body);line-height:1;white-space:nowrap;text-decoration:none;cursor:pointer;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(--c-white);font-weight:var(--fw-bold)}
.btn.primary:hover{background:var(--primary-hover)}
.btn.sm{height:var(--btn-h-sm);padding:0 12px}
.tbtn{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;border:0;border-radius:var(--r-ctl);background:transparent;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background-color var(--dur-fast) var(--ease)}
.tbtn:hover{background:var(--n-100)}
.ibtn{position:relative;display:inline-grid;place-items:center;width:var(--btn-h);height:var(--btn-h);border:0;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.ibtn:hover{background:var(--n-100)}
.ibtn.sm{width:var(--btn-h-sm);height:var(--btn-h-sm)}
/* ---------- 标签:中性一种 + 问题级别 ---------- */
.tag{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);white-space:nowrap}
.sev{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 8px;border-radius:var(--r-tag);font-size:var(--fs-caption);font-weight:var(--fw-bold);white-space:nowrap}
.sev.fatal{background:var(--sev-fatal);color:var(--c-white)}
.sev .fig{font-weight:var(--fw-bold)}
.dash{color:var(--text-secondary)}
/* ---------- 应用框架 ---------- */
.app{display:grid;grid-template-columns:var(--side-w) minmax(0,1fr);min-height:100vh;transition:grid-template-columns var(--dur-base) var(--ease)}
.side{background:var(--side-bg);color:var(--side-fg);min-width:0}
/* ---- d4:侧栏与内容区融合 ---- */
.app:not([data-side="light"]):not([data-v="flat"]){
--side-deep: color-mix(in srgb, var(--c-ink) 82%, var(--c-pencil)); /* 渐变下端:深墨蓝 */
background:linear-gradient(180deg, var(--c-ink) 0%, var(--side-deep) 100%) fixed;
}
.app:not([data-side="light"]):not([data-v="flat"]) .side{background:transparent;position:relative}
.app:not([data-side="light"]):not([data-v="flat"]) .side-in::after{content:"";position:absolute;left:0;right:0;bottom:0;height:260px;pointer-events:none;
background-image:radial-gradient(color-mix(in srgb, white 14%, transparent) 1px, transparent 1.2px);background-size:14px 14px;
-webkit-mask-image:linear-gradient(to top, black, transparent);mask-image:linear-gradient(to top, black, transparent)}
.app:not([data-side="light"]):not([data-v="flat"]) .side-in > *{position:relative;z-index:1}
.app:not([data-side="light"]):not([data-v="flat"]) .main{background:var(--n-50);border-top-left-radius:12px;min-height:100vh}
.app:not([data-side="light"]):not([data-v="flat"]) .top{border-top-left-radius:12px}
/* 浅灰侧栏对照(?side=light) */
.app[data-side="light"]{--side-bg:color-mix(in srgb, var(--c-ink) 9%, white);--side-fg:var(--text-secondary);--side-fg-strong:var(--text-primary);--side-hover:color-mix(in srgb, var(--c-ink) 14%, white);--side-active:var(--c-white);--side-line:var(--n-line);--side-muted:var(--text-secondary)}
.app[data-side="light"] .side{border-right:1px solid var(--n-line)}
.app[data-side="light"] .nav a[aria-current]{box-shadow:0 0 0 1px var(--n-line)}
.brand .mini{display:none;height:32px;width:32px}
.app[data-side="light"] .brand .full.rev,.app:not([data-side="light"]) .brand .full.std{display:none}
.side-in{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;padding:0 12px 12px;overflow:hidden}
.brand{display:flex;align-items:center;justify-content:center;height:72px;padding:0;margin:0 0 8px;flex:none}
.brand:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px;border-radius:var(--r-ctl)}
.brand img{height:34px;display:block}
.nav{display:flex;flex-direction:column;gap:2px}
.nav a,.side-btn{position:relative;display:flex;align-items:center;gap:10px;height:var(--btn-h);padding:0 10px;border-radius:var(--r-ctl);text-decoration:none;color:var(--side-fg);white-space:nowrap;transition:background-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.nav a .ic,.side-btn .ic{flex:none}
.nav a:hover,.side-btn:hover{background:var(--side-hover);color:var(--side-fg-strong)}
.nav a[aria-current]{background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold)}
/* 选中样式(d4 修订):选中效果全部留在侧栏内,折叠动画中不会露出。?sel=bar / solid 切换对照 */
.nav a[aria-current]::before{content:"";position:absolute;left:-12px;top:8px;bottom:8px;width:3px;border-radius:0 3px 3px 0;background:var(--side-fg-strong)}
.app[data-sel="bar"] .nav a[aria-current]{background:transparent}
.app[data-sel="bar"] .nav a[aria-current]:hover{background:var(--side-hover)}
.app[data-sel="solid"] .nav a[aria-current]{background:var(--c-white);color:var(--text-primary)}
.app[data-sel="solid"] .nav a[aria-current]::before{display:none}
.app[data-side="light"] .nav a[aria-current]::before{background:var(--c-ink)}
.nav a:focus-visible,.side-btn:focus-visible{outline:2px solid var(--side-fg-strong);outline-offset:-2px}
.nav .grp{margin:20px 10px 6px;height:20px;font-size:var(--fs-caption);line-height:20px;color:var(--side-muted);white-space:nowrap}
.side-btn{width:100%;border:0;background:transparent;font:inherit;cursor:pointer;margin-top:auto}
.side-foot{display:flex;align-items:center;gap:10px;margin-top:8px;padding:12px 6px 0;border-top:1px solid var(--side-line);font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--side-muted);white-space:nowrap}
.side-foot b{display:block;color:var(--side-fg-strong);font-size:var(--fs-body);line-height:var(--lh-body);font-weight:var(--fw-bold)}
.avatar{flex:none;display:inline-grid;place-items:center;width:32px;height:32px;border-radius:50%;background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold);font-size:var(--fs-body)}
.app[data-side="light"] .avatar{background:var(--c-ink);color:var(--c-white)}
/* 折叠:只留图标;文字保留给读屏,悬停出提示 */
.side-in:has([data-stip]:hover){overflow:visible}
.top{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:12px;height:var(--top-h);padding:0 var(--pad);background:transparent;border-bottom:1px solid transparent;transition:background-color var(--dur-base) var(--ease),border-color var(--dur-base) var(--ease)}
.top.scrolled{background:color-mix(in srgb, var(--c-white) 82%, transparent);-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);border-bottom-color:var(--n-line)}
.top .search{margin-left:auto;width:280px;transition:width var(--dur-base) var(--ease),border-color var(--dur-fast) var(--ease)}
.top .search:focus-within{width:360px}
.top .vsep{width:1px;height:20px;background:var(--n-line);margin:0 4px}
.org .ic{color:var(--text-secondary)}
.org{justify-self:start;display:inline-flex;align-items:center;gap:6px;color:var(--text-secondary)}
.org b{color:var(--text-primary);font-weight:var(--fw-bold)}
.search{position:relative;display:flex;align-items:center;height:var(--btn-h);border:1px solid var(--ctl-border);border-radius:var(--r-ctl);background:var(--c-white);transition:border-color var(--dur-fast) var(--ease)}
.search:hover{border-color:var(--c-ink)}
.search:focus-within{border-color:var(--c-ink);box-shadow:0 0 0 1px var(--c-ink)}
.search > .ic{position:absolute;left:11px;color:var(--text-secondary);pointer-events:none}
.search input{flex:1;min-width:0;height:100%;border:0;background:transparent;padding:0 8px 0 34px;font:inherit;color:var(--text-primary);outline:0}
.search input::placeholder{color:var(--text-secondary)}
.search input::-webkit-search-cancel-button{display:none}
.kbd{flex:none;margin-right:6px;padding:0 6px;height:22px;display:inline-flex;align-items:center;border-radius:var(--r-tag);background:var(--n-100);color:var(--text-secondary);font-size:var(--fs-caption);font-family:var(--font-figures);font-weight:var(--fw-bold)}
.top-r{justify-self:end;display:flex;align-items:center;gap:4px}
.qchip{display:inline-flex;align-items:center;gap:6px;height:var(--btn-h-sm);padding:0 10px;color:var(--text-secondary)}
.qchip .fig{color:var(--text-primary);font-size:15px}
.qchip .sep{width:1px;height:12px;background:var(--n-line);margin:0 4px}
.dot{position:absolute;top:8px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--c-ink);box-shadow:0 0 0 2px var(--c-white)}
.content{padding:20px var(--pad) 48px}
.ch h2,.ch h3{display:inline-flex;align-items:center;gap:8px}
.ch h2 > .ic,.ch h3 > .ic{color:var(--text-secondary)}
/* ---------- 页头 ---------- */
.ph{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}
.ph h1{font-family:var(--font-serif);font-weight:var(--fw-bold);font-size:var(--fs-h1);line-height:var(--lh-h1)}
.ph p{margin-top:2px;color:var(--text-secondary)}
.warn-t{color:var(--warn-text);font-weight:var(--fw-bold)}
.ph .act{display:flex;align-items:center;gap:12px}
.cost{font-size:var(--fs-caption);color:var(--text-secondary);white-space:nowrap}
.cost .fig{color:var(--text-primary)}
/* ---------- 唯一的容器:白卡片 ---------- */
.card{background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-card)}
.ch{display:flex;align-items:center;gap:12px;min-height:56px;padding:12px var(--card-x)}
.ch h2,.ch h3{font-size:var(--fs-title);line-height:var(--lh-title);font-weight:var(--fw-bold)}
.ch .more{margin-left:auto}
.cf{padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.stack > * + *{margin-top:var(--gap)}
/* 截止提醒 */
.alert{display:flex;align-items:center;gap:14px;padding:14px var(--card-x);margin-bottom:var(--gap)}
.alert > .ic{color:var(--c-ochre)}
.alert p{flex:1}
.alert .when{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 统计:一张卡片四格 */
.stats{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:var(--gap);overflow:hidden}
.stat{position:relative;display:grid;grid-template-rows:auto auto auto;justify-items:start;gap:2px;padding:16px var(--card-x) 16px;border:0;border-left:1px solid var(--n-line);background:transparent;text-align:left;cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.stat:first-child{border-left:0}
.stat:hover{background:var(--n-50)}
.stat::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--c-ink);transform:scaleY(0);transform-origin:bottom;transition:transform var(--dur-fast) var(--ease)}
.stat[aria-pressed="true"]::after{transform:scaleY(1)}
.stat[aria-pressed="true"] small{color:var(--text-primary);font-weight:var(--fw-bold)}
.stat:focus-visible{outline-offset:-2px;border-radius:0}
.stat small{font-size:var(--fs-body);color:var(--text-secondary)}
.stat .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.stat .big.fatal{color:var(--sev-fatal)}
.stat .big.due{color:var(--warn-text)}
.stat > .ic{position:absolute;top:18px;right:var(--card-x);color:var(--text-secondary);opacity:0;transition:opacity var(--dur-fast) var(--ease)}
.stat:hover > .ic,.stat:focus-visible > .ic,.stat[aria-pressed="true"] > .ic{opacity:1}
.stat[aria-pressed="true"] > .ic{color:var(--text-primary)}
.stat p{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.stat p.due{color:var(--warn-text);font-weight:var(--fw-bold)}
/* 两栏 */
.wb{display:grid;grid-template-columns:minmax(0,1fr) var(--rail-w);gap:var(--gap);align-items:start}
/* 项目卡片头:筛选 */
.filters{display:flex;align-items:center;gap:2px;margin-left:8px}
.filters .tbtn{color:var(--text-secondary)}
.filters .tbtn .fig{font-size:15px}
.filters .tbtn[aria-pressed="true"]{background:var(--n-100);color:var(--text-primary);font-weight:var(--fw-bold)}
.clear{margin-left:auto;color:var(--text-primary)}
/* 项目行 */
.grid-cols{display:grid;grid-template-columns:var(--cols);column-gap:var(--col-gap);align-items:center;padding:0 var(--card-x)}
.cols{padding-bottom:8px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.cols .r,.row .r{justify-self:end;text-align:right}
.row .more-cell{justify-self:end}
.cols .c,.row .c{justify-self:center;text-align:center}
.row{position:relative;min-height:64px;padding-top:10px;padding-bottom:10px;border-top:1px solid var(--n-line);cursor:pointer;transition:background-color var(--dur-fast) var(--ease)}
.row:hover,.row:focus-within{background:var(--n-50)}
.row:has(.pname:focus-visible){outline:2px solid var(--focus-ring);outline-offset:-2px}
.pcell{display:flex;align-items:center;gap:10px;min-width:0}
.pcell .pt{min-width:0}
.pname{display:block;font-weight:var(--fw-bold);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;outline:0}
.pname::after{content:"";position:absolute;inset:0}
.psub{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.due b{display:block;font-weight:var(--fw-regular);white-space:nowrap}
.due small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary);white-space:nowrap}
.due.today b{color:var(--warn-text);font-weight:var(--fw-bold)}
.due.soon b{color:var(--warn-text)}
.due.past b{color:var(--text-secondary)}
.st{display:flex;align-items:center;gap:8px;white-space:nowrap}
.ready{position:relative;z-index:1;text-decoration:underline dashed;text-decoration-color:color-mix(in srgb, var(--c-ink) 40%, white);text-underline-offset:4px;cursor:help}
.row.arch .pname,.row.arch .due b{color:var(--text-secondary);font-weight:var(--fw-regular)}
/* 下一步:默认文字链,行悬停 / 聚焦变次按钮,同时浮现「更多」 */
.nx-cell{display:flex;align-items:center;justify-content:flex-start;min-width:0}
.nx{position:relative;z-index:1;display:inline-flex;align-items:center;gap:4px;height:var(--btn-h-sm);padding:0;border:0;border-radius:var(--r-ctl);background:transparent;color:var(--text-primary);white-space:nowrap;cursor:pointer;transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.nx span{text-decoration:underline;text-decoration-color:color-mix(in srgb, var(--c-ink) 30%, white);text-underline-offset:3px}
.nx .ic{color:var(--text-secondary);transition:transform var(--dur-fast) var(--ease)}
.row:hover .nx .ic,.row:focus-within .nx .ic{transform:translateX(3px);color:var(--text-primary)}
.row .nx:hover span{text-decoration-color:var(--c-ink)}
.row .ibtn{position:relative;z-index:1;color:var(--text-secondary);transition:color var(--dur-fast) var(--ease),background-color var(--dur-fast) var(--ease)}
.row:hover .ibtn,.row:focus-within .ibtn,.row .ibtn[aria-expanded="true"]{color:var(--text-primary)}
.empty{padding:32px var(--card-x);border-top:1px solid var(--n-line);text-align:center}
.empty b{display:block;font-weight:var(--fw-bold)}
.empty p{color:var(--text-secondary);margin-top:4px}
/* 待处理的致命问题 */
.todo{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;column-gap:16px;align-items:center;padding:12px var(--card-x);border-top:1px solid var(--n-line)}
.todo b{display:block;font-weight:var(--fw-bold)}
.todo .psub{white-space:normal}
.todo .cite{margin-right:2px}
/* 右栏 */
.rail .ch{min-height:52px;padding-bottom:4px}
.rbody{padding:4px var(--card-x) 16px}
.task-h{display:flex;align-items:baseline;gap:8px}
.task-h b{font-weight:var(--fw-bold)}
.task-h .cap{margin-left:auto}
.pbar{display:flex;align-items:center;gap:10px;margin-top:10px}
.bar{flex:1;height:6px;border-radius:3px;background:var(--n-100);overflow:hidden}
.bar i{display:block;height:100%;background:var(--c-ink);border-radius:3px}
.queue{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding-top:12px;border-top:1px solid var(--n-line)}
.queue .ic{margin-top:2px;color:var(--text-secondary)}
.q2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.q2 small .ic{vertical-align:-3px;margin-right:4px;color:var(--text-secondary)}
.q2 small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.q2 .big{font-family:var(--font-figures);font-variant-numeric:tabular-nums;font-weight:var(--fw-bold);font-size:28px;line-height:36px}
.q2 .unit{margin-left:4px;color:var(--text-secondary);font-family:var(--font-figures);font-variant-numeric:tabular-nums}
.q2 .bar{margin-top:6px}
.links{display:flex;gap:16px}
.log li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:8px 0;border-top:1px solid var(--n-line);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:13px}
.log .who{font-weight:var(--fw-bold)}
.av{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--n-100);color:var(--text-primary);font-size:var(--fs-caption);font-weight:var(--fw-bold);line-height:1}
.av .ic{width:14px;height:14px;color:var(--text-secondary)}
.log .ok{color:var(--confirmed);white-space:nowrap}
/* ---------- 浮层:菜单、提示、轻提示 ---------- */
.menu{position:fixed;z-index:20;min-width:248px;padding:6px;background:var(--c-white);border:1px solid var(--n-line);border-radius:var(--r-ctl);box-shadow:var(--shadow-overlay)}
.menu button{display:block;width:100%;padding:7px 10px;border:0;border-radius:6px;background:transparent;text-align:left;cursor:pointer}
.menu button:hover,.menu button:focus-visible{background:var(--n-100);outline:0}
.menu button small{display:block;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--text-secondary)}
.menu button small .fig{color:var(--text-primary)}
.menu hr{border:0;border-top:1px solid var(--n-line);margin:6px 0}
[data-tip]{position:relative}
[data-tip]:hover::before,[data-tip]:focus-visible::before{content:attr(data-tip);position:absolute;z-index:30;top:calc(100% + 8px);left:50%;transform:translateX(-50%);width:max-content;max-width:260px;padding:6px 10px;border-radius:var(--r-ctl);background:var(--c-ink);color:var(--c-white);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-r:hover::before,[data-tip].tip-r:focus-visible::before{left:auto;right:0;transform:none}
.toasts{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:40;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(--c-white);box-shadow:var(--shadow-overlay)}
/* ---------- d5:单一状态窄侧栏 ---------- */
.app{--side-w:96px}
.side .side-in{padding:0 0 16px 10px;overflow:visible}
.side .brand{height:auto;justify-content:center;padding:20px 10px 20px 0;margin:0}
.side .brand img{width:60px;height:auto}
.side .nav{gap:4px}
.side .nav a{flex-direction:column;justify-content:center;gap:4px;height:62px;padding:0;margin-right:10px;border-radius:10px;font-size:var(--fs-caption);line-height:16px;color:var(--side-fg);font-weight:var(--fw-regular);text-align:center}
.side .nav a .ic{width:22px;height:22px}
.side .nav a:hover{background:var(--side-hover);color:var(--side-fg-strong)}
.side .nav a[aria-current]{background:var(--side-active);color:var(--side-fg-strong);font-weight:var(--fw-bold)}
.side .nav a[aria-current]::before{display:none}
.side .nav .grp{display:block;height:1px;margin:10px 22px 10px 12px;background:var(--side-line)}
.side .side-foot{flex-direction:column;justify-content:center;gap:0;margin-top:auto;padding:16px 10px 4px 0;border-top:0;color:var(--side-muted);text-align:center;white-space:normal}
.side .side-foot .avatar{width:44px;height:44px;font-size:var(--fs-title);margin-bottom:8px;background:color-mix(in srgb, white 16%, transparent);box-shadow:0 0 0 1px color-mix(in srgb, white 22%, transparent)}
.side .side-foot .uname{color:var(--side-fg-strong);font-size:var(--fs-body);line-height:var(--lh-body);font-weight:var(--fw-bold)}
.side .side-foot .plan{margin-top:4px;padding:0 8px;border-radius:4px;font-size:var(--fs-caption);line-height:20px;font-weight:var(--fw-bold)}
/* 套餐标签:只在账户区使用的「金色」阶梯,从赭色派生,不引入新色相;
免费版中性、专业版浅金底、企业版金色实底,靠深浅区分,不靠多种颜色。?plan=free / ent 预览 */
.plan[data-plan="free"]{background:color-mix(in srgb, white 12%, transparent);color:var(--side-fg)}
.plan[data-plan="pro"]{background:color-mix(in srgb, var(--c-ochre) 42%, transparent);color:color-mix(in srgb, var(--c-ochre) 12%, white);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--c-ochre) 70%, white 10%)}
.plan[data-plan="ent"]{background:color-mix(in srgb, var(--c-ochre) 55%, white);color:var(--c-ink)}
</style>
</head>
<body>
<div class="app">
<aside class="side"><div class="side-in">
<a class="brand" href="workbench-d5.html" aria-label="标博士"><img src="../../logo/svg/logo-vertical-reverse.svg" alt=""></a>
<nav class="nav" aria-label="主导航">
<a href="workbench-d5.html" aria-current="page"><i data-ic="house"></i><span>工作台</span></a>
<a href="#" data-demo><i data-ic="award"></i><span>资质库</span></a>
<a href="#" data-demo><i data-ic="wallet"></i><span>额度</span></a>
<span class="grp" role="separator"></span>
<a href="index.html"><i data-ic="book"></i><span>方案目录</span></a>
</nav>
<div class="side-foot"><span class="avatar" aria-hidden="true">王</span><b class="uname">王敏</b><span class="plan" data-plan="pro">专业版</span></div>
</div></aside>
<div class="main">
<header class="top">
<span class="org"><b>南京某信息技术有限公司</b><span>/</span>工作台</span>
<form class="search" role="search" data-gsearch>
<i data-ic="search"></i>
<input type="search" placeholder="搜索项目、采购人" aria-label="搜索项目、采购人" aria-keyshortcuts="Control+K" data-q>
<span class="kbd" aria-hidden="true">Ctrl K</span>
</form>
<span class="vsep" aria-hidden="true"></span>
<div class="top-r">
<a class="qchip tbtn" href="#" data-demo data-tip="本期剩余额度:解析 37 / 50 次,检查 12 / 20 次" aria-label="本期剩余额度:解析 37 次,检查 12 次">解析<span class="fig">37</span><span class="sep"></span>检查<span class="fig">12</span></a>
<button class="ibtn tip-r" aria-label="通知(1 条未读)" data-tip="通知" data-notice><i data-ic="bell" class="lg"></i><span class="dot"></span></button>
</div>
</header>
<main class="content">
<div class="ph">
<div><h1>工作台</h1><p>下午好,王敏。今天是 11月3日,周二。</p></div>
<div class="act">
<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="card alert" role="note">
<i data-ic="clock" class="lg"></i>
<p><b>某县数据中心机房改造项目</b> <span class="when">今天 17:00 截止</span>,清单还没确认。</p>
<a class="btn sm" href="../page-reading.html"><i data-ic="list"></i>确认清单</a>
</div>
<section class="card 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 class="stack">
<section class="card" aria-labelledby="h-proj">
<div class="ch">
<h2 id="h-proj">项目</h2>
<div class="filters" role="group" aria-label="按状态筛选" data-seg>
<button class="tbtn" aria-pressed="true" data-v="active">进行中 <span class="fig">4</span></button>
<button class="tbtn" aria-pressed="false" data-v="all">全部 <span class="fig">6</span></button>
<button class="tbtn" aria-pressed="false" data-v="已递交">已递交</button>
<button class="tbtn" aria-pressed="false" data-v="已归档">已归档</button>
</div>
<span class="more" data-chip></span>
</div>
<div class="cols grid-cols" aria-hidden="true"><span>项目</span><span>投标截止</span><span>状态</span><span class="c">致命</span><span>下一步</span><span></span></div>
<ul data-rows aria-label="项目列表"></ul>
</section>
<section class="card" aria-labelledby="h-todo">
<div class="ch">
<h2 id="h-todo">待处理的致命问题</h2>
<span class="sev fatal">致命<span class="fig">3</span></span>
<a class="more lnk cap" href="#" data-demo>查看全部</a>
</div>
<ul>
<li class="todo"><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><span class="psub">智慧大厅建设项目 · 开标一览表 287.60 万元,限价 286.00 万元</span></div><a class="cite" 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" 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" 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>
</section>
</div>
<div class="stack rail">
<section class="card" aria-labelledby="h-task">
<div class="ch"><h3 id="h-task">正在进行的任务</h3></div>
<div class="rbody">
<div class="task-h"><b>解析补遗 2</b><span class="cap sec">提取清单</span></div>
<div class="cap sec">某市医保信息系统等保测评服务</div>
<div class="pbar"><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 sec">检查排队中:智慧社区平台运维,前面 1 个</span></div>
</div>
</section>
<section class="card" aria-labelledby="h-quota">
<div class="ch"><h3 id="h-quota">本期额度</h3><span class="more cap sec">至 2027-10-31</span></div>
<div class="rbody">
<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="cf links cap"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card" aria-labelledby="h-log">
<div class="ch"><h3 id="h-log">最近处理</h3><a class="more lnk cap" href="#" data-demo>全部记录</a></div>
<div class="rbody">
<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>
</div>
</div>
</main>
</div>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* Lucide 图标(ISC 许可)路径 */
const ICONS = {
grid: '<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"/>',
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
bell: '<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"/>',
book: '<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"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
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"/>',
list: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/>',
"chev-right": '<path d="m9 18 6-6-6-6"/>',
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
building: '<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4M10 10h4M10 14h4M10 18h4"/>',
folders: '<path d="M20 17a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3.9a2 2 0 0 1-1.69-.9l-.81-1.2a2 2 0 0 0-1.67-.9H8a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2Z"/><path d="M2 8v11a2 2 0 0 0 2 2h14"/>',
"calendar-clock": '<path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M16 2v4M8 2v4M3 10h5"/><path d="M17.5 17.5 16 16.3V14"/><circle cx="16" cy="16" r="6"/>',
"octagon-alert": '<path d="M12 16h.01M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/>',
activity: '<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/>',
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
loader: '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
"file-search": '<path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"/><path d="m9 18-1.5-1.5"/><circle cx="5" cy="14" r="3"/>',
scan: '<path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>',
bookmark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
pen: '<path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/>',
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
archive: '<rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>',
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
wrench: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
cpu: '<rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2M15 20v2M2 15h2M2 9h2M20 15h2M20 9h2M9 2v2M9 20v2"/>',
house: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
award: '<path d="m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526"/><circle cx="12" cy="8" r="6"/>',
wallet: '<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/>',
"panel-close": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/>',
"panel-open": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/>'
};
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
$$("i[data-ic]").forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
/* 顶栏:滚动后变为半透明白 + 毛玻璃 */
const topEl = $(".top"); const onScroll = () => topEl.classList.toggle("scrolled", scrollY > 4); addEventListener("scroll", onScroll, { passive: true }); onScroll();
/* 侧栏:主题(?side=light 为浅灰对照)与折叠(记在本地,读写失败不影响页面) */
const app = $(".app");
if (new URLSearchParams(location.search).get("side") === "light") app.dataset.side = "light";
if (new URLSearchParams(location.search).get("v") === "flat") app.dataset.v = "flat";
const plan = new URLSearchParams(location.search).get("plan"); const PLAN = { free: "免费版", pro: "专业版", ent: "企业版" };
if (PLAN[plan]) { const el = $(".plan"); el.dataset.plan = plan; el.textContent = PLAN[plan]; }
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(), 4000);
}
const DEMO = "(演示)这里只做了工作台,可以打开「某县数据中心」或「智慧大厅」看读标页";
/* 数据沿用 page-workbench.html;pend 为待确认清单条目数(对应统计「在 2 个项目里」)。
next 为「下一步」,menu 为「更多」里的操作;cost 表示会扣点。 */
const CHECK_COST = { label: "扣 1 次检查 · 剩 ", left: 12, unit: " 次" };
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", h: 6, due: ["今天 17:00 截止", "剩 6 小时"], cls: "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: "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: "past", st: "已递交", ready: "", fatal: 0, pend: 0,
next: "归档项目", menu: [["导出检查报告", ""]] },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", h: -2, due: ["已截止", "已归档"], cls: "past", st: "已归档", ready: "", fatal: 0, pend: 0,
next: "查看项目", menu: [["恢复为进行中", ""]] }
];
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 = "";
function status(p) {
let r = "";
if (p.ready === "就绪") r = '<span class="ready" 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>`;
}
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="row grid-cols ${p.st === "已归档" ? "arch" : ""}" data-i="${i}">
<span class="pcell"><span class="pt"><a class="pname" href="${p.href || "#"}" title="${p.name}" ${p.href ? "" : "data-demo"}>${p.name}</a><span class="psub">${p.buyer} · ${p.lot}</span></span></span>
<span class="due ${p.cls}"><b>${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 class="nx-cell"><button class="nx" data-next><span>${p.next}</span>${icon("chev-right")}</button></span><span class="more-cell"><button class="ibtn sm" aria-label="更多操作:${p.name}" aria-haspopup="menu" aria-expanded="false" data-menu>${icon("more")}</button></span>
</li>`).join("")
|| `<li class="empty"><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="tbtn clear" data-clear>${STAT[stat].label}${icon("x")}<span class="vh">,清除筛选</span></button>` : "";
}
/* 「更多」菜单 */
let menuEl = null, menuBtn = null;
function closeMenu(focusBack) {
if (!menuEl) return;
menuEl.remove(); menuEl = null;
if (menuBtn) { menuBtn.setAttribute("aria-expanded", "false"); if (focusBack) menuBtn.focus(); }
menuBtn = null;
}
function openMenu(btn, p) {
closeMenu();
menuBtn = btn; btn.setAttribute("aria-expanded", "true");
menuEl = document.createElement("div");
menuEl.className = "menu"; menuEl.setAttribute("role", "menu");
const items = [["打开项目", ""], ...p.menu].map(([t, s]) => {
const sub = s === "cost" ? `${CHECK_COST.label}<span class="fig">${CHECK_COST.left}</span>${CHECK_COST.unit}` : s;
return `<button role="menuitem" data-act="${t}">${t}${sub ? `<small>${sub}</small>` : ""}</button>`;
});
items.splice(1, 0, "<hr>");
menuEl.innerHTML = items.join("");
document.body.appendChild(menuEl);
const r = btn.getBoundingClientRect();
menuEl.style.top = `${r.bottom + 4}px`;
menuEl.style.left = `${r.right - menuEl.offsetWidth}px`;
menuEl.addEventListener("click", (e) => {
const it = e.target.closest("[data-act]"); if (!it) return;
const a = it.dataset.act; closeMenu(true);
if (a === "打开项目" && p.href) location.href = p.href;
else toast(`(演示)${a}:${p.name}`);
});
menuEl.addEventListener("keydown", (e) => {
const its = $$("[role=menuitem]", menuEl), k = its.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); its[(k + 1) % its.length].focus(); }
if (e.key === "ArrowUp") { e.preventDefault(); its[(k - 1 + its.length) % its.length].focus(); }
});
$("[role=menuitem]", menuEl).focus();
}
$("[data-rows]").addEventListener("click", (e) => {
const row = e.target.closest(".row"); if (!row) return;
const p = P[+row.dataset.i];
const m = e.target.closest("[data-menu]");
if (m) { e.preventDefault(); e.stopPropagation(); if (menuBtn === m) closeMenu(); else openMenu(m, p); return; }
if (e.target.closest(".ready")) return;
if (e.target.closest("[data-next]")) {
if (p.href) location.href = p.href;
else toast(`(演示)${p.next}:${p.name}`);
return;
}
if (!e.target.closest(".pname")) { if (p.href) location.href = p.href; else toast(DEMO); }
});
document.addEventListener("click", (e) => { if (menuEl && !menuEl.contains(e.target)) closeMenu(); });
window.addEventListener("scroll", () => closeMenu(), { passive: true });
window.addEventListener("resize", () => closeMenu());
$("[data-seg]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; seg = b.dataset.v; 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(); } });
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-gsearch]").addEventListener("submit", (e) => e.preventDefault());
document.addEventListener("click", (e) => { const d = e.target.closest("[data-demo]"); if (d) { e.preventDefault(); toast(DEMO); } });
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); $("[data-q]").focus(); }
if (e.key === "Escape") {
if (menuEl) closeMenu(true);
else if (document.activeElement === $("[data-q]")) $("[data-q]").blur();
}
});
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,544 @@
<!doctype html>
<!--
工作台 · 风格探索第二轮 方案 E「下一步」
概念:工作台是「今天该做什么」的行动清单,不是报表。最上方是按紧急程度排好的待办队列,每条都能直接动手;项目全貌放在下面。
内容、数据沿用 ../page-workbench.html;颜色只用 tokens.css 的色值和语义,浅底与分隔线全部由墨色派生。
本方案相对 design-system.md 改了这些规则:
1. 底色与分隔线(§3.1 / §3.2):页面底色 --c-paper 改为 --n-50(墨 3% 混白);分隔线 --c-rule 改为 --n-line(墨 11% 混白);
行悬停 --c-hover 改为 --n-50;进度条轨道、侧栏当前项用 --n-100(墨 6%)。不再使用 --c-paper / --c-sunken / --c-hover / --c-rule。
2. 圆角(§5.1 控件和卡片 2px、浮层 4px):卡片 12px;按钮、输入框、侧栏当前项、轻提示 8px;问题级别标签 4px。
3. 阴影(§5.1 只用于浮层):只有可点击的待办卡在悬停 / 键盘聚焦时出现一层极轻阴影并加深边框,作为「可以点」的提示;静止时所有卡片都不带阴影。
4. 按钮(§7.1):默认高度 32 → 36;次按钮白底 + 1px --border-control;表格行内按钮 28 不变。
5. 统计卡(原型四张统计卡):取消。数字并入区块标题(待办 4、项目 4)和待办标题右侧的一行概要。
6. 提醒条(§7.12):取消,并入待办队列。今天截止的那条用左侧 3px 赭色竖条 + 赭色时间文字。
7. 筛选(§7.1 分段控件):改为文字标签页,选中项墨色 2px 下划线。
8. 表格(§7.8):表头去掉凹陷底色和加粗下边线,只留小号辅助灰字;行间发丝线;「状态」「就绪」「致命」合并为一列;
状态改为纯文字,全页只保留问题级别一种标签(形状规则不变:致命实心、重要描边、提示无框)。新增「下一步」列,每行一个可直接点的操作。
9. 页面框架(§5.3):去掉顶栏,通知、快捷键和新建项目并入页头一行;侧栏白底,与内容之间一条 --n-line 发丝线;额度只在右栏出现一次。
10. 区块标题:16/24 黑体 600(宋体只用于 H1「工作台」)。
-->
<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); /* 卡片边框、发丝线 */
--n-line-strong: color-mix(in srgb, var(--c-ink) 24%, white); /* 待办卡悬停边框 */
--e-lift: 0 1px 2px color-mix(in srgb, var(--c-ink) 6%, transparent),
0 6px 16px -6px color-mix(in srgb, var(--c-ink) 14%, transparent);
--e-r-card: 12px;
--e-r-ctl: 8px;
--e-h: 36px;
--e-h-sm: 28px;
--e-gap: 16px; /* 卡片之间、卡片内分组之间的统一间距 */
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; min-width: 1280px;
background: var(--n-50); 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 { font: inherit; color: inherit; }
h1, h2, h3, p { margin: 0; }
.fig { font-family: var(--font-figures); font-variant-numeric: tabular-nums; }
.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; border-radius: 4px; }
.sub { color: var(--text-secondary); }
.cap { font-size: var(--fs-caption); line-height: var(--lh-caption); }
.lnk { color: var(--text-primary); text-decoration: underline; text-decoration-color: var(--n-line-strong); text-underline-offset: 3px; cursor: pointer; }
.lnk:hover { text-decoration-color: currentColor; }
/* ---------- 按钮:一种主按钮、一种次按钮、一种图标按钮 ---------- */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
height: var(--e-h); padding: 0 var(--sp-4); border: 1px solid transparent; border-radius: var(--e-r-ctl);
font-weight: var(--fw-bold); text-decoration: none; white-space: nowrap; cursor: pointer;
transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.btn.primary { background: var(--c-ink); color: var(--text-on-dark); }
.btn.primary:hover { background: color-mix(in srgb, var(--c-ink) 86%, white); }
.btn.secondary { background: var(--c-white); border-color: var(--border-control); color: var(--text-primary); }
.btn.secondary:hover { background: var(--n-50); border-color: var(--c-ink); }
.btn.sm { height: var(--e-h-sm); padding: 0 var(--sp-3); font-weight: var(--fw-regular); }
.icon-btn {
position: relative; display: inline-grid; place-items: center; width: var(--e-h); height: var(--e-h);
border: 0; border-radius: var(--e-r-ctl); background: none; color: var(--text-secondary); cursor: pointer;
}
.icon-btn:hover { background: var(--n-100); color: var(--text-primary); }
.icon-btn.sm { width: var(--e-h-sm); height: var(--e-h-sm); }
.icon-btn .dot { position: absolute; top: 8px; right: 8px; width: 7px; height: 7px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 2px var(--n-50); }
/* ---------- 唯一的标签:问题级别 ---------- */
.sev { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: 4px; font-size: var(--fs-caption); line-height: 20px; font-weight: var(--fw-bold); white-space: nowrap; }
.sev.fatal { background: var(--sev-fatal); color: var(--text-on-dark); }
.sev .fig { font-size: 13px; }
/* ---------- 框架 ---------- */
.app { display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); min-height: 100vh; }
.side {
position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column;
padding: var(--sp-6) var(--sp-3); background: var(--c-white); border-right: 1px solid var(--n-line);
}
.brand { display: inline-block; padding: 0 var(--sp-3); margin-bottom: var(--sp-8); }
.brand img { display: block; height: 32px; width: auto; }
.nav { display: flex; flex-direction: column; gap: 2px; }
.nav a { display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 var(--sp-3); border-radius: var(--e-r-ctl); color: var(--text-secondary); text-decoration: none; }
.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 .demo { margin-top: var(--sp-6); height: 32px; font-size: var(--fs-caption); }
.side-foot { margin-top: auto; padding: var(--sp-4) var(--sp-3) 0; border-top: 1px solid var(--n-line); font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.side-foot b { display: block; font-size: var(--fs-body); line-height: var(--lh-body); font-weight: var(--fw-bold); color: var(--text-primary); }
.main { min-width: 0; padding: var(--sp-6) var(--sp-6) var(--sp-16); }
.wrap { max-width: 1320px; }
/* 页头 */
.head { display: flex; align-items: center; gap: var(--sp-4); min-height: 48px; }
.head h1 { font: var(--fw-bold) 28px/36px var(--font-serif); letter-spacing: .02em; }
.head .date { color: var(--text-secondary); padding-top: 6px; }
.head .tools { margin-left: auto; display: flex; align-items: center; gap: var(--sp-1); }
.head .cost { margin: 0 var(--sp-3) 0 var(--sp-4); color: var(--text-secondary); }
.head .cost .fig { color: var(--text-primary); font-weight: var(--fw-bold); font-size: 13px; }
.layout { display: grid; grid-template-columns: minmax(0, 1fr) clamp(272px, 20.5vw, 304px); gap: var(--sp-6); align-items: start; margin-top: var(--sp-6); }
/* 区块标题:标题 + 小号数字 + 右侧补充 */
.sec-h { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-3); min-height: 28px; }
.sec-h h2 { font-size: var(--fs-title); line-height: var(--lh-title); font-weight: var(--fw-bold); }
.sec-h .count { font-family: var(--font-figures); font-variant-numeric: tabular-nums; font-size: 16px; color: var(--text-secondary); }
.sec-h .aside { margin-left: auto; display: flex; gap: var(--sp-4); color: var(--text-secondary); font-size: var(--fs-caption); line-height: var(--lh-caption); }
.sec-h .aside .fig { font-size: 14px; font-weight: var(--fw-bold); color: var(--text-primary); margin: 0 2px; }
.sec-h .aside .fig.ochre { color: var(--warn-text); }
.sec-h .aside .fig.fatal { color: var(--sev-fatal); }
/* ---------- 唯一的容器:白卡片 ---------- */
.card { background: var(--c-white); border: 1px solid var(--n-line); border-radius: var(--e-r-card); }
/* ---------- 待办队列(主角) ---------- */
.queue { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.todo {
position: relative; overflow: hidden; cursor: pointer;
transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-base) var(--ease), transform var(--dur-base) var(--ease),
opacity 220ms var(--ease), height 260ms var(--ease) 120ms, margin 260ms var(--ease) 120ms;
}
.todo:hover, .todo:focus-within { border-color: var(--n-line-strong); box-shadow: var(--e-lift); transform: translateY(-1px); }
.todo.today::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--warn); }
.todo-row {
display: grid; align-items: center; column-gap: var(--sp-4);
grid-template-columns: 20px minmax(0, 1fr) 136px 160px 28px;
padding: var(--sp-4) var(--sp-3) var(--sp-4) var(--sp-5);
}
.todo .kind { align-self: start; margin-top: 2px; color: var(--text-secondary); }
.todo.today .kind { color: var(--warn); }
.todo.fatal .kind { color: var(--sev-fatal); }
.todo h3 { font-size: 15px; line-height: 24px; font-weight: var(--fw-bold); }
.todo h3 .proj { font-weight: var(--fw-bold); }
.todo .why { margin-top: 2px; color: var(--text-secondary); }
.todo .why b { font-weight: var(--fw-bold); color: var(--warn-text); }
.todo .why .sev { margin-right: 4px; position: relative; top: -1px; }
.todo .meta { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; white-space: nowrap; }
.todo .meta .d1 { font-size: 14px; line-height: 22px; }
.todo .meta .d1 .fig { font-size: 15px; }
.todo .meta .d2 { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.todo.today .meta .d1, .todo.today .meta .d2 { color: var(--warn-text); font-weight: var(--fw-bold); }
.todo.soon .meta .d1, .todo.soon .meta .d2 { color: var(--warn-text); }
.todo .go { width: 100%; }
@media (max-width: 1400px) { .todo-row { column-gap: var(--sp-3); grid-template-columns: 20px minmax(0, 1fr) 120px 148px 28px; } .todo .go { padding: 0 var(--sp-3); } }
.todo .snooze { opacity: 0; transition: opacity var(--dur-fast) var(--ease); }
.todo:hover .snooze, .todo:focus-within .snooze, .todo .snooze:focus-visible { opacity: 1; }
/* 致命问题展开 */
.toggle { display: inline-flex; align-items: center; gap: 4px; margin-top: var(--sp-2); padding: 0 6px 0 0; height: 24px; border: 0; background: none; color: var(--text-primary); cursor: pointer; text-decoration: underline; text-decoration-color: var(--n-line-strong); text-underline-offset: 3px; }
.toggle:hover { text-decoration-color: currentColor; }
.toggle .ic { transition: transform var(--dur-base) var(--ease); }
.toggle[aria-expanded="true"] .ic { transform: rotate(180deg); }
.issues { list-style: none; margin: 0; padding: 0 var(--sp-5) var(--sp-2) calc(var(--sp-5) + 20px + var(--sp-4)); cursor: default; }
.issues[hidden] { display: none; }
.issues li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--sp-3); align-items: baseline; padding: var(--sp-3) 0; border-top: 1px solid var(--n-line); }
.issues .sev { position: relative; top: -1px; }
.issues b { display: block; font-weight: var(--fw-bold); }
.issues p { color: var(--text-secondary); font-size: var(--fs-caption); line-height: var(--lh-caption); margin-top: 2px; }
.cite { display: inline-flex; align-items: center; gap: 4px; color: var(--ai); text-decoration: none; white-space: nowrap; font-size: var(--fs-caption); line-height: var(--lh-caption); }
.cite:hover { color: var(--ai-strong); text-decoration: underline; text-underline-offset: 3px; }
.cite .fig { font-size: 13px; }
.issues .foot { display: block; border-top: 1px solid var(--n-line); padding: var(--sp-3) 0 var(--sp-1); color: var(--text-secondary); font-size: var(--fs-caption); line-height: var(--lh-caption); }
.queue-done { padding: var(--sp-6) var(--sp-5); text-align: center; color: var(--text-secondary); }
.queue-done b { display: block; color: var(--text-primary); font-weight: var(--fw-bold); }
/* ---------- 项目列表 ---------- */
.projects-sec { margin-top: var(--sp-10); }
.list-card { overflow: hidden; }
.bar { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-4); padding: var(--sp-2) var(--sp-5) 0; border-bottom: 1px solid var(--n-line); }
.tabs { display: flex; gap: var(--sp-5); }
.tabs button {
height: 44px; padding: 0 2px; border: 0; background: none; cursor: pointer; color: var(--text-secondary);
box-shadow: inset 0 -2px 0 transparent; transition: color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.tabs button:hover { color: var(--text-primary); box-shadow: inset 0 -2px 0 var(--n-line-strong); }
.tabs button[aria-selected="true"] { color: var(--text-primary); font-weight: var(--fw-bold); box-shadow: inset 0 -2px 0 var(--c-ink); }
.tabs .fig { margin-left: 4px; }
.search { position: relative; display: block; width: 240px; margin-bottom: var(--sp-2); }
.search .ic { position: absolute; left: 10px; top: 8px; color: var(--text-secondary); pointer-events: none; }
.search input {
width: 100%; height: 32px; padding: 0 var(--sp-3) 0 34px; border: 1px solid var(--border-control); border-radius: var(--e-r-ctl);
background: var(--c-white); font: inherit; color: var(--text-primary);
}
.search input::placeholder { color: var(--text-secondary); }
.search input:hover { border-color: var(--c-ink); }
.search input:focus-visible { outline-offset: 1px; border-color: var(--c-ink); }
.projects { width: 100%; border-collapse: collapse; table-layout: fixed; }
.projects th { padding: var(--sp-3) 10px var(--sp-2); text-align: left; font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: var(--fw-regular); color: var(--text-secondary); white-space: nowrap; }
.projects th:first-child, .projects td:first-child { padding-left: var(--sp-5); }
.projects th:last-child, .projects td:last-child { padding-right: var(--sp-4); }
.projects td { padding: var(--sp-3) 10px; border-top: 1px solid var(--n-line); vertical-align: middle; }
.projects tbody tr.row { cursor: pointer; transition: background var(--dur-fast) var(--ease); }
.projects tbody tr.row:hover, .projects tbody tr.row:focus-within { background: var(--n-50); }
.c-st { width: 104px; } .c-due { width: 124px; } .c-next { width: 172px; }
.pname { font-weight: var(--fw-bold); text-decoration: none; }
.row:hover .pname { text-decoration: underline; text-decoration-color: var(--n-line-strong); text-underline-offset: 3px; }
.psub { display: flex; gap: var(--sp-3); color: var(--text-secondary); font-size: var(--fs-caption); line-height: var(--lh-caption); }
.st { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.st .r2 { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.ready { text-decoration: underline dotted var(--text-secondary); text-underline-offset: 3px; cursor: help; }
.due { white-space: nowrap; }
.due .d1 { display: block; }
.due .d2 { display: block; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.due.today .d1, .due.today .d2 { color: var(--warn-text); font-weight: var(--fw-bold); }
.due.soon .d1, .due.soon .d2 { color: var(--warn-text); }
.due.past .d1 { color: var(--text-secondary); }
.next { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
.next .more { opacity: 0; transition: opacity var(--dur-fast) var(--ease); }
.row:hover .more, .row:focus-within .more { opacity: 1; }
.row:hover .btn.secondary { border-color: var(--c-ink); }
.empty { padding: var(--sp-10) var(--sp-5); text-align: center; color: var(--text-secondary); }
.empty b { display: block; margin-bottom: var(--sp-1); color: var(--text-primary); font-weight: var(--fw-bold); }
.list-note { padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--n-line); color: var(--text-secondary); font-size: var(--fs-caption); line-height: var(--lh-caption); }
/* ---------- 右栏:同一种白卡片 ---------- */
.rail { display: grid; gap: var(--e-gap); padding-top: calc(28px + var(--sp-3)); }
.rail .card { padding: var(--sp-4) var(--sp-5) var(--sp-5); }
.rail h3 { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-3); font-size: var(--fs-title); line-height: var(--lh-title); font-weight: var(--fw-bold); }
.rail h3 .r { margin-left: auto; font-size: var(--fs-caption); line-height: var(--lh-caption); font-weight: var(--fw-regular); color: var(--text-secondary); }
.quota { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); }
.quota small { display: block; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.quota .big { font: var(--fw-bold) var(--fs-figure)/var(--lh-figure) var(--font-figures); font-variant-numeric: tabular-nums; }
.quota .unit { margin-left: 4px; font-family: var(--font-figures); font-size: 15px; color: var(--text-secondary); }
.track { height: 4px; border-radius: 2px; background: var(--n-100); overflow: hidden; margin-top: var(--sp-2); }
.track i { display: block; height: 100%; background: var(--c-ink); border-radius: 2px; }
.links { display: flex; gap: var(--sp-5); margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--n-line); font-size: var(--fs-caption); line-height: var(--lh-caption); }
.task-h { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2); }
.task-h span { font-size: var(--fs-caption); color: var(--text-secondary); }
.progress { display: flex; align-items: center; gap: var(--sp-3); }
.progress .track { flex: 1; margin: 0; }
.progress .fig { font-size: 15px; font-weight: var(--fw-bold); min-width: 32px; text-align: right; }
.task + .task { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--n-line); }
.task .wait { display: flex; gap: var(--sp-2); color: var(--text-secondary); }
.task .wait .ic { margin-top: 3px; }
.rail .hint { margin-top: var(--sp-3); font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--text-secondary); }
.log { list-style: none; margin: 0; padding: 0; }
.log li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-3); padding: 6px 0; font-size: 13px; line-height: 20px; }
.log li + li { border-top: 1px solid var(--n-line); }
.log .t { font-family: var(--font-figures); font-variant-numeric: tabular-nums; color: var(--text-secondary); white-space: nowrap; }
.log .who { font-weight: var(--fw-bold); }
.log .ok { color: var(--confirmed); white-space: nowrap; }
/* 轻提示 */
.toasts { position: fixed; left: 50%; bottom: var(--sp-8); transform: translateX(-50%); display: grid; gap: var(--sp-2); justify-items: center; z-index: 20; }
.toast { display: flex; align-items: center; gap: var(--sp-3); min-height: 40px; padding: var(--sp-2) var(--sp-4); border-radius: var(--e-r-ctl); background: var(--c-ink); color: var(--text-on-dark); box-shadow: var(--shadow-overlay); white-space: nowrap; }
.toast button { padding: 0; border: 0; background: none; color: var(--text-on-dark); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.toast button:focus-visible { outline-color: var(--c-white); }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } .todo:hover, .todo:focus-within { transform: none; } }
</style>
</head>
<body>
<div class="app">
<aside class="side">
<a class="brand" href="../page-workbench.html"><img src="../../logo/svg/logo-horizontal.svg" alt="标博士"></a>
<nav class="nav" aria-label="主导航">
<a href="#" 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="#quota"><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>
<a class="demo" href="index.html"><svg class="ic" 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>
<main class="main">
<div class="wrap">
<header class="head">
<h1>工作台</h1>
<span class="date">11月3日 周二</span>
<div class="tools">
<button class="icon-btn" aria-label="通知(有 1 条未读)" title="通知" 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" aria-label="快捷键说明" title="快捷键说明(?)" 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>
<span class="cost cap">扣 1 次解析,剩 <span class="fig">37</span> 次</span>
<a class="btn primary" href="../forms.html#s-new"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>新建项目</a>
</div>
</header>
<div class="layout">
<div>
<!-- 待办队列 -->
<section aria-labelledby="todo-h">
<div class="sec-h">
<h2 id="todo-h">待办</h2><span class="count" data-todo-count>4</span>
<div class="aside"><span>7 天内截止<span class="fig ochre">2</span>个项目</span><span>致命问题<span class="fig fatal">3</span>个</span><span>清单待确认<span class="fig">4</span>条,在 2 个项目里</span></div>
</div>
<ol class="queue" data-queue>
<li class="card todo today" data-go="../page-reading.html">
<div class="todo-row">
<svg class="ic lg kind" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/></svg>
<div><h3>确认清单:<span class="proj">某县数据中心机房改造项目</span></h3><p class="why"><b>今天 17:00 截止</b>,清单还是草稿,还没做过检查。确认后上传投标文件就能发起检查。</p></div>
<div class="meta"><span class="d1">今天 <span class="fig">17:00</span></span><span class="d2">剩 6 小时</span></div>
<a class="btn primary go" href="../page-reading.html">确认清单</a>
<button class="icon-btn sm snooze" aria-label="稍后提醒:确认清单" title="稍后提醒" data-snooze><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.1 2.18a9.93 9.93 0 0 1 3.8 0"/><path d="M17.6 3.71a9.95 9.95 0 0 1 2.69 2.7"/><path d="M21.82 10.1a9.93 9.93 0 0 1 0 3.8"/><path d="M20.29 17.6a9.95 9.95 0 0 1-2.7 2.69"/><path d="M13.9 21.82a9.94 9.94 0 0 1-3.8 0"/><path d="M6.4 20.29a9.95 9.95 0 0 1-2.69-2.7"/><path d="M2.18 13.9a9.93 9.93 0 0 1 0-3.8"/><path d="M3.71 6.4a9.95 9.95 0 0 1 2.7-2.69"/><path d="M12 7v5l3 2"/></svg></button>
</div>
</li>
<li class="card todo fatal" data-go="../page-reading.html">
<div class="todo-row">
<svg class="ic lg kind" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 16h.01"/><path d="M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/></svg>
<div><h3>致命问题:<span class="proj">某市政务服务中心智慧大厅建设项目</span></h3><p class="why"><span class="sev fatal">致命<span class="fig">3</span></span> 第 2 次检查发现 3 个致命问题。没处理完也能导出,项目会保持「未就绪」。</p>
<button class="toggle cap" aria-expanded="false" aria-controls="fatal-list" data-toggle>看看是哪 3 个<svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button></div>
<div class="meta"><span class="d1"><span class="fig">11</span>月<span class="fig">20</span>日 <span class="fig">09:30</span></span><span class="d2">剩 17 天</span></div>
<a class="btn primary go" href="../page-reading.html">处理 3 个致命问题</a>
<button class="icon-btn sm snooze" aria-label="稍后提醒:致命问题" title="稍后提醒" data-snooze><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.1 2.18a9.93 9.93 0 0 1 3.8 0"/><path d="M17.6 3.71a9.95 9.95 0 0 1 2.69 2.7"/><path d="M21.82 10.1a9.93 9.93 0 0 1 0 3.8"/><path d="M20.29 17.6a9.95 9.95 0 0 1-2.7 2.69"/><path d="M13.9 21.82a9.94 9.94 0 0 1-3.8 0"/><path d="M6.4 20.29a9.95 9.95 0 0 1-2.69-2.7"/><path d="M2.18 13.9a9.93 9.93 0 0 1 0-3.8"/><path d="M3.71 6.4a9.95 9.95 0 0 1 2.7-2.69"/><path d="M12 7v5l3 2"/></svg></button>
</div>
<ul class="issues" id="fatal-list" hidden>
<li><span class="sev fatal">致命</span><div><b>报价超过采购包 1 的最高限价</b><p>开标一览表 287.60 万元,限价 286.00 万元</p></div><a class="cite" href="#" data-cite>报价册 第 <span class="fig">1</span> 页</a></li>
<li><span class="sev fatal">致命</span><div><b>★ 条款疑似不满足:应用服务器内存</b><p>响应 256G,要求 ≥ 512G。上次标记为已修改,问题仍在</p></div><a class="cite" href="#" data-cite>技术册 第 <span class="fig">12</span> 页</a></li>
<li><span class="sev fatal">致命</span><div><b>授权委托书的单位签章位置未发现盖章</b><p>只看有无,不鉴定真伪</p></div><a class="cite" href="#" data-cite>商务册 第 <span class="fig">5</span> 页</a></li>
</ul>
</li>
<li class="card todo soon" data-go="">
<div class="todo-row">
<svg class="ic lg kind" viewBox="0 0 24 24" aria-hidden="true"><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="m9 15 2 2 4-4"/></svg>
<div><h3>递交前复核:<span class="proj">某区智慧社区平台运维服务</span></h3><p class="why">已就绪。新上传的版本在排队检查,前面还有 1 个任务,出结果后会通知你。</p></div>
<div class="meta"><span class="d1"><span class="fig">11</span>月<span class="fig">5</span>日 <span class="fig">16:00</span></span><span class="d2">剩 2 天 5 小时</span></div>
<button class="btn secondary go" data-demo-go="(演示)会打开该项目上一次的检查结果">查看检查结果</button>
<button class="icon-btn sm snooze" aria-label="稍后提醒:递交前复核" title="稍后提醒" data-snooze><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.1 2.18a9.93 9.93 0 0 1 3.8 0"/><path d="M17.6 3.71a9.95 9.95 0 0 1 2.69 2.7"/><path d="M21.82 10.1a9.93 9.93 0 0 1 0 3.8"/><path d="M20.29 17.6a9.95 9.95 0 0 1-2.7 2.69"/><path d="M13.9 21.82a9.94 9.94 0 0 1-3.8 0"/><path d="M6.4 20.29a9.95 9.95 0 0 1-2.69-2.7"/><path d="M2.18 13.9a9.93 9.93 0 0 1 0-3.8"/><path d="M3.71 6.4a9.95 9.95 0 0 1 2.7-2.69"/><path d="M12 7v5l3 2"/></svg></button>
</div>
</li>
<li class="card todo" data-go="">
<div class="todo-row">
<svg class="ic lg kind" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
<div><h3>补遗解析中:<span class="proj">某市医保信息系统等保测评服务</span></h3><p class="why">补遗 2 正在提取清单,已完成 62%。完成后需要确认补遗带来的变更。</p></div>
<div class="meta"><span class="d1"><span class="fig">11</span>月<span class="fig">28</span>日 <span class="fig">10:00</span></span><span class="d2">剩 25 天</span></div>
<button class="btn secondary go" data-demo-go="(演示)会打开解析进度:补遗 2,提取清单 62%">查看解析进度</button>
<button class="icon-btn sm snooze" aria-label="稍后提醒:补遗解析中" title="稍后提醒" data-snooze><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.1 2.18a9.93 9.93 0 0 1 3.8 0"/><path d="M17.6 3.71a9.95 9.95 0 0 1 2.69 2.7"/><path d="M21.82 10.1a9.93 9.93 0 0 1 0 3.8"/><path d="M20.29 17.6a9.95 9.95 0 0 1-2.7 2.69"/><path d="M13.9 21.82a9.94 9.94 0 0 1-3.8 0"/><path d="M6.4 20.29a9.95 9.95 0 0 1-2.69-2.7"/><path d="M2.18 13.9a9.93 9.93 0 0 1 0-3.8"/><path d="M3.71 6.4a9.95 9.95 0 0 1 2.7-2.69"/><path d="M12 7v5l3 2"/></svg></button>
</div>
</li>
</ol>
</section>
<!-- 项目列表 -->
<section class="projects-sec" aria-labelledby="proj-h">
<div class="sec-h"><h2 id="proj-h">项目</h2><span class="count" data-proj-count>4</span></div>
<div class="card list-card">
<div class="bar">
<div class="tabs" role="tablist" aria-label="按状态筛选" data-tabs>
<button role="tab" aria-selected="true" data-v="active">进行中<span class="fig">4</span></button>
<button role="tab" aria-selected="false" data-v="all" tabindex="-1">全部<span class="fig">6</span></button>
<button role="tab" aria-selected="false" data-v="已递交" tabindex="-1">已递交<span class="fig">1</span></button>
<button role="tab" aria-selected="false" data-v="已归档" 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="搜索项目" data-q></label>
</div>
<table class="projects">
<thead><tr><th scope="col">项目</th><th scope="col" class="c-st">状态</th><th scope="col" class="c-due" aria-sort="ascending">投标截止</th><th scope="col" class="c-next"><span class="vh">下一步</span></th></tr></thead>
<tbody data-rows></tbody>
</table>
<p class="list-note" id="ready-note">按投标截止时间排序。「就绪」只表示致命问题都已处理,不代表投标文件没有问题。</p>
</div>
</section>
</div>
<!-- 右栏 -->
<aside class="rail" aria-label="额度、任务与最近处理">
<section class="card" id="quota">
<h3>本期额度<span class="r">至 <span class="fig">2027-10-31</span></span></h3>
<div class="quota">
<div><small>解析剩余</small><span class="big">37</span><span class="unit">/ 50</span><div class="track" aria-hidden="true"><i style="width:74%"></i></div></div>
<div><small>检查剩余</small><span class="big">12</span><span class="unit">/ 20</span><div class="track" aria-hidden="true"><i style="width:60%"></i></div></div>
</div>
<div class="links"><a class="lnk" href="#" data-demo>扣点规则</a><a class="lnk" href="#" data-demo>查看流水</a></div>
</section>
<section class="card">
<h3>正在进行的任务<span class="count fig sub">2</span></h3>
<div class="task">
<div class="task-h"><b>解析补遗 2</b><span>提取清单</span></div>
<div class="cap sub">某市医保信息系统等保测评服务</div>
<div class="progress"><div class="track" 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>
<div class="task">
<div class="wait"><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>检查排队中:某区智慧社区平台运维服务,前面还有 1 个任务</span></div>
</div>
<p class="hint">可以离开本页,完成后会发站内通知。</p>
</section>
<section class="card">
<h3>最近处理<a class="r lnk" href="#" data-demo>全部记录</a></h3>
<ul class="log">
<li><span><span class="who">王敏</span> <span class="ok">✓ 已修改</span>「授权委托书签章」</span><span class="t">14:26</span></li>
<li><span><span class="who">王敏</span> 确认了清单 v2:智慧大厅</span><span class="t">11-02</span></li>
<li><span><span class="who">系统</span> 完成第 2 次检查,致命 3</span><span class="t">11-02</span></li>
<li><span><span class="who">系统</span> 并入补遗 1,更新 2 条</span><span class="t">11-02</span></li>
<li><span><span class="who">李工</span> 忽略「其他项目名称」:业绩介绍</span><span class="t">11-01</span></li>
</ul>
</section>
</aside>
</div>
</div>
</main>
</div>
<script>
(function () {
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
const fig = (s) => String(s).replace(/(\d[\d:.]*)/g, '<span class="fig">$1</span>');
function toast(msg, undo) {
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.innerHTML = `<span></span>${undo ? "<button type=\"button\">撤销</button>" : ""}`;
t.firstChild.textContent = msg;
if (undo) t.querySelector("button").onclick = () => { t.remove(); undo(); };
box.appendChild(t);
setTimeout(() => t.remove(), 5000);
}
/* ---------- 待办队列 ---------- */
const queue = $("[data-queue]");
const countTodo = () => { $("[data-todo-count]").textContent = $$(".todo:not(.leaving)", queue).length; };
queue.addEventListener("click", (e) => {
const card = e.target.closest(".todo"); if (!card) return;
if (e.target.closest("[data-toggle]")) {
const b = e.target.closest("[data-toggle]"), open = b.getAttribute("aria-expanded") !== "true";
b.setAttribute("aria-expanded", open);
b.firstChild.textContent = open ? "收起" : "看看是哪 3 个";
$("#" + b.getAttribute("aria-controls")).hidden = !open;
return;
}
if (e.target.closest("[data-cite]")) { e.preventDefault(); toast("(演示)出处会在原文查看器里打开"); return; }
if (e.target.closest(".issues")) return;
if (e.target.closest("[data-snooze]")) { snooze(card); return; }
const demo = card.querySelector("[data-demo-go]");
if (!card.dataset.go) { e.preventDefault(); toast(demo ? demo.dataset.demoGo : "(演示)这一页还没做"); return; }
if (!e.target.closest("a")) location.href = card.dataset.go;
});
/* 唯一的演示动效:推迟一条待办后,该条淡出、队列上移 */
function snooze(card) {
const title = card.querySelector("h3").textContent;
const h = card.offsetHeight;
card.style.height = h + "px";
card.classList.add("leaving");
card.offsetHeight; // reflow
card.style.opacity = "0";
card.style.height = "0px";
card.style.marginBottom = "-12px";
card.style.borderWidth = "0";
countTodo();
const done = () => { card.hidden = true; card.removeEventListener("transitionend", onEnd); emptyCheck(); };
const onEnd = (ev) => { if (ev.propertyName === "height") done(); };
card.addEventListener("transitionend", onEnd);
if (matchMedia("(prefers-reduced-motion: reduce)").matches) done();
toast(`「${title}」已推迟,明天 9:00 再提醒`, () => {
card.hidden = false; card.classList.remove("leaving");
card.style.height = h + "px"; card.style.borderWidth = ""; card.style.marginBottom = "";
card.offsetHeight; card.style.opacity = "";
setTimeout(() => { card.style.height = ""; }, 400);
countTodo(); emptyCheck();
});
}
function emptyCheck() {
let e = $(".queue-done");
const left = $$(".todo:not(.leaving)", queue).length;
if (!left && !e) { queue.insertAdjacentHTML("afterend", '<div class="card queue-done"><b>今天的待办都安排好了</b>被推迟的事项明天 9:00 会回到这里。</div>'); }
if (left && e) e.remove();
}
/* ---------- 项目列表 ---------- */
const P = [
{ name: "某县数据中心机房改造项目", buyer: "某县大数据管理局", lot: "标段 1", d1: "今天 17:00", d2: "剩 6 小时", cls: "today", st: "编制中", ready: "清单草稿", fatal: 0, next: "确认清单", href: "../page-reading.html" },
{ name: "某区智慧社区平台运维服务", buyer: "某区民政局", lot: "全部", d1: "11月5日 16:00", d2: "剩 2 天 5 小时", cls: "soon", st: "已检查", ready: "就绪", fatal: 0, next: "查看检查结果" },
{ name: "某市政务服务中心智慧大厅建设项目", buyer: "某市政务服务管理办公室", lot: "采购包 1", d1: "11月20日 09:30", d2: "剩 17 天", cls: "", st: "已检查", ready: "未就绪", fatal: 3, next: "处理致命问题", href: "../page-reading.html" },
{ name: "某市医保信息系统等保测评服务", buyer: "某市医疗保障局", lot: "全部", d1: "11月28日 10:00", d2: "剩 25 天", cls: "", st: "跟进中", ready: "补遗解析中", fatal: 0, next: "查看解析进度" },
{ name: "某区智慧校园一期", buyer: "某区教育局", lot: "全部", d1: "已截止", d2: "", cls: "past", st: "已递交", ready: "", fatal: 0, next: "查看检查报告" },
{ name: "某市不动产登记信息平台升级", buyer: "某市自然资源和规划局", lot: "采购包 2", d1: "已截止", d2: "", cls: "past", st: "已归档", ready: "", fatal: 0, next: "查看项目" }
];
const DONE = ["已递交", "已归档"];
let seg = "active", q = "";
function status(p) {
let r2 = "";
if (p.fatal) r2 = `<span class="sev fatal">致命<span class="fig">${p.fatal}</span></span>`;
else if (p.ready === "就绪") r2 = '<span class="r2"><span class="ready" tabindex="0" aria-describedby="ready-note" title="只表示致命问题都已处理,不代表投标文件没有问题。">就绪</span></span>';
else if (p.ready) r2 = `<span class="r2">${p.ready}</span>`;
return `<div class="st"><span>${p.st}</span>${r2}</div>`;
}
function render() {
const rows = P.filter((p) => (seg === "all" || (seg === "active" ? !DONE.includes(p.st) : p.st === seg)) && (!q || (p.name + p.buyer).includes(q)));
$("[data-proj-count]").textContent = rows.length;
$("[data-rows]").innerHTML = rows.map((p) => `<tr class="row" data-i="${P.indexOf(p)}">
<td><a class="pname" href="${p.href || "#"}">${p.name}</a><div class="psub"><span>${p.buyer}</span><span>${fig(p.lot)}</span></div></td>
<td>${status(p)}</td>
<td class="due ${p.cls}"><span class="d1">${fig(p.d1)}</span>${p.d2 ? `<span class="d2">${p.d2}</span>` : ""}</td>
<td><div class="next"><button class="btn sm secondary" data-next>${p.next}</button><button class="icon-btn sm more" aria-label="更多操作:${p.name}" title="更多操作" data-more><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg></button></div></td></tr>`).join("")
|| `<tr><td colspan="4"><div class="empty"><b>没有符合条件的项目</b>换个关键词,或切到「全部」看看。</div></td></tr>`;
}
function setSeg(btn) {
seg = btn.dataset.v;
$$("[data-tabs] button").forEach((b) => { const on = b === btn; b.setAttribute("aria-selected", on); b.tabIndex = on ? 0 : -1; });
render();
}
$("[data-tabs]").addEventListener("click", (e) => { const b = e.target.closest("button"); if (b) setSeg(b); });
$("[data-tabs]").addEventListener("keydown", (e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
const bs = $$("[data-tabs] button"), i = bs.indexOf(document.activeElement);
const n = bs[(i + (e.key === "ArrowRight" ? 1 : -1) + bs.length) % bs.length];
n.focus(); setSeg(n);
});
$("[data-q]").addEventListener("input", (e) => { q = e.target.value.trim(); render(); });
$("[data-rows]").addEventListener("click", (e) => {
const r = e.target.closest("tr.row"); if (!r) return;
const p = P[r.dataset.i];
if (e.target.closest("[data-more]")) { toast("(演示)更多操作:改状态、归档、删除项目"); return; }
if (e.target.closest("[data-next]") && p.href) { location.href = p.href; return; }
if (!p.href) { e.preventDefault(); toast(`(演示)会打开「${p.name}」:${p.next}`); return; }
if (!e.target.closest("a")) location.href = p.href;
});
document.addEventListener("click", (e) => {
const a = e.target.closest("[data-demo]"); if (!a) return;
e.preventDefault(); toast("(演示)这一页还没做");
});
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
$("[data-keys]").onclick = () => toast("(演示)快捷键:J / K 逐条,1 / 2 / 3 处理,? 显示说明");
render();
})();
</script>
</body>
</html>

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>