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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:35:08 +08:00

482 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<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>