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,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>