三面拆分⑫:UI 打磨 —— 补 10 枚缺失图标,蓝图改因果链,台账收成一个模块

图标注册表缺 arrow-left-right/columns/table/tag 等 10 名,用到即渲染成空 SVG —— 补齐;
蓝图四库改真箭头相连的因果链节点卡(卡即入口,删重复的入库段);
驾驶舱终态分布改五色堆叠条;运营数据收成左轨一枚「台账」,域下沉为 2-3 字短名页签
(受理→寻源→定价→议价→替代→共享,按流程序),进模块自动落首域。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 22:02:49 +08:00
co-authored by Claude Fable 5
parent 4a19690389
commit 34e207c3e8
11 changed files with 266 additions and 170 deletions
+39 -84
View File
@@ -21,6 +21,7 @@ import type { GovZone, Nav, Route } from '@web-shared/gov/gov-route.ts';
import { SURFACE_META, SURFACE_ZONES, canSeeSurface } from '@web-shared/gov/surfaces.ts';
import type { Surface } from '@web-shared/gov/surfaces.ts';
import { useGovObjects, useGovTasks } from '@web-shared/gov/gov-api.ts';
import { domainShort, sortDomains } from '@web-shared/gov/domains.ts';
import { queryKeys } from './query.ts';
import { BlueprintHome } from '@features/gov-blueprint';
import { TasksHome, TaskLogPage, TaskPage, WfPage } from '@features/gov-task';
@@ -126,17 +127,19 @@ export function GovShell({ surface }: { surface: Surface }) {
(tasks.data?.workflows ?? []).filter((w) => w.state === 'abnormal').length +
(tasks.data?.tasks ?? []).filter((t) => t.terminal !== undefined && t.terminal !== 'released').length;
/** 台账的导航项按**已有数据域**长出来 —— 设计侧一发布新域,运营侧自动多一位 */
const domains = [...new Set((objects.data?.objects ?? []).map((o) => o.domain))].sort();
/** 台账的域页签按**已有域**长出来 —— 设计侧一发布新域,台账里自动多一个页签 */
const domains = sortDomains([...new Set((objects.data?.objects ?? []).map((o) => o.domain))]);
// 进台账没带域(左轨点进来)时落到首域 —— 页签与内容同一判定
const effectiveTab = zone === 'ledgers' && tab === '' ? (domains[0] ?? '') : tab;
/* 面包屑:台面 › 区 › 页签 › 下钻各级 */
const crumbs: Array<{ label: string; go?: () => void }> = [
{ label: SURFACE_META[surface].brand, go: () => goZone(zone) },
];
crumbs.push({ label: meta.title, go: () => go({ zone, tab, stack: [] }) });
// 台账区的页签名就是域名 —— 静态表里查不到,直接用它本身
// 台账区的页签名就是域键 —— 静态表里查不到,呈现走人读短名
crumbs.push({
label: TABS[zone].find((t) => t.key === tab)?.label ?? tab,
label: TABS[zone].find((t) => t.key === tab)?.label ?? (zone === 'ledgers' ? domainShort(effectiveTab) : tab),
go: () => go({ zone, tab, stack: [] }),
});
stack.forEach((r, i) =>
@@ -154,24 +157,14 @@ export function GovShell({ surface }: { surface: Surface }) {
<Ico name={surface === 'design' ? 'route' : 'gauge'} s={20} />
</div>
<div className="g-nav-brand">{SURFACE_META[surface].brand}</div>
{ZONES.filter((z) => SURFACE_ZONES[surface].includes(z.key)).map((z) =>
// 台账不是一个固定位,是一段**随数据域生长**的动态位 —— 每域一枚
z.key === 'ledgers' ? (
<LedgerRail
key={z.key}
domains={domains}
active={zone === 'ledgers' ? tab : undefined}
onPick={(domain) => go({ zone: 'ledgers', tab: domain, stack: [] })}
/>
) : (
<button key={z.key} className={'g-nav-b' + (zone === z.key ? ' on' : '')} onClick={() => goZone(z.key)}>
<span className="g-nav-ico">
<Ico name={z.icon} s={18} />
</span>
<span>{z.label}</span>
</button>
),
)}
{ZONES.filter((z) => SURFACE_ZONES[surface].includes(z.key)).map((z) => (
<button key={z.key} className={'g-nav-b' + (zone === z.key ? ' on' : '')} onClick={() => goZone(z.key)}>
<span className="g-nav-ico">
<Ico name={z.icon} s={18} />
</span>
<span>{z.label}</span>
</button>
))}
<div className="g-nav-foot">
<span className="g-nav-ava">{(me?.name ?? '—').slice(0, 1)}</span>
<span>{me?.name ?? '未登录'}</span>
@@ -242,18 +235,31 @@ export function GovShell({ surface }: { surface: Surface }) {
</div>
</div>
<div className="g-tabs">
{TABS[zone].map((t) => (
<button
key={t.key}
className={'g-tab' + (tab === t.key ? ' on' : '')}
onClick={() => go({ zone, tab: t.key, stack: [] })}
>
<Ico name={t.ico} s={13} />
{t.label}
</button>
))}
{zone === 'ledgers'
? // 台账的页签是**数据域**(短名 2-3 字),随发布生长 —— 设计产生运营的形状,落在页签上
domains.map((d) => (
<button
key={d}
className={'g-tab' + (effectiveTab === d ? ' on' : '')}
title={`数据域 · ${d}`}
onClick={() => go({ zone, tab: d, stack: [] })}
>
<Ico name="table" s={13} />
{domainShort(d)}
</button>
))
: TABS[zone].map((t) => (
<button
key={t.key}
className={'g-tab' + (tab === t.key ? ' on' : '')}
onClick={() => go({ zone, tab: t.key, stack: [] })}
>
<Ico name={t.ico} s={13} />
{t.label}
</button>
))}
</div>
<ZoneHome zone={zone} tab={tab} nav={nav} />
<ZoneHome zone={zone} tab={effectiveTab} nav={nav} />
</>
) : (
<div className="g-detail" key={`${stack.length}:${top.k}:${top.id}`}>
@@ -266,57 +272,6 @@ export function GovShell({ surface }: { surface: Surface }) {
);
}
/**
* 台账段 —— 左轨里唯一的**动态位**:一域一枚,随数据域发布而生长。
*
* 这是「设计产生运营的形状」最直观的一处:导航本身就是被设计出来的。
*/
function LedgerRail({
domains,
active,
onPick,
}: {
domains: readonly string[];
active: string | undefined;
onPick: (domain: string) => void;
}) {
return (
<>
<div className="g-nav-brand" style={{ marginTop: 8, marginBottom: 4 }}>
台账
</div>
{domains.length === 0 ? (
<div className="g-nav-brand" style={{ fontWeight: 600, letterSpacing: 0, lineHeight: 1.5 }}>
随数据域
<br />
发布生长
</div>
) : (
domains.map((d) => (
<button
key={d}
className={'g-nav-b' + (active === d ? ' on' : '')}
onClick={() => onPick(d)}
title={`台账 · ${d}`}
>
<span className="g-nav-ico">
<Ico name="database" s={18} />
</span>
{/* 域名是 `活动_环节` 这种复合键,60px 的轨放不下一行 —— 按下划线断行,窄字号铺开 */}
<span style={{ fontSize: 10, lineHeight: 1.25, wordBreak: 'break-word' }}>
{d.split('_').map((seg, i) => (
<span key={seg} style={{ display: 'block' }}>
{i === 0 && d.includes('_') ? `${seg}_` : seg}
</span>
))}
</span>
</button>
))
)}
</>
);
}
function ZoneHome({ zone, tab, nav }: { zone: GovZone; tab: string; nav: Nav }) {
const P = {
// 设计中心
+32
View File
@@ -458,3 +458,35 @@
.g-dbpage button:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
.g-dbpage button:disabled { opacity: .35; cursor: default; }
.g-dbpage b { min-width: 36px; text-align: center; font-size: 11px; color: var(--ink-2); }
/* ═══════════ 三面补充(非原型)—— 蓝图因果链 / 终态堆叠条 / 数据域紧凑轨钮 ═══════════ */
/* 蓝图因果链:四库节点卡以真箭头相连 —— 箭头编码真实依赖(流程包→活动→工具→数据),节点即入口 */
.g-chain { display: flex; align-items: stretch; gap: 0; margin-bottom: 14px; }
.g-chain-node { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; background: var(--card); border: 1px solid var(--cline); border-radius: 13px; box-shadow: var(--cshadow); text-align: left; font: inherit; color: var(--ink); cursor: pointer; transition: border-color var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast); }
.g-chain-node:hover { border-color: var(--brand); transform: translateY(-1px); box-shadow: 0 4px 14px -6px rgba(51, 112, 255, .35); }
.g-chain-node:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.g-chain-k { display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 600; color: var(--ink-3); }
.g-chain-k .ic { width: 22px; height: 22px; border-radius: 7px; display: grid; place-items: center; flex: none; background: var(--fill-2, #eef1f5); color: var(--ink-3); }
.g-chain-node.t-brand .ic { background: var(--brand-soft); color: var(--brand-deep); }
.g-chain-node.t-auto .ic { background: #e6f7f4; color: #0a6a64; }
.g-chain-node.t-collab .ic { background: #f0e9ff; color: #5b21cf; }
.g-chain-node.t-ok .ic { background: var(--ok-soft); color: var(--ok); }
.g-chain-v { font-size: 24px; font-weight: 800; font-family: var(--mono); letter-spacing: -.03em; line-height: 1; }
.g-chain-node.t-brand .g-chain-v { color: var(--brand-deep); }
.g-chain-node.t-auto .g-chain-v { color: #0a6a64; }
.g-chain-node.t-collab .g-chain-v { color: #5b21cf; }
.g-chain-node.t-ok .g-chain-v { color: var(--ok); }
.g-chain-h { font-size: 11px; line-height: 1.5; color: var(--ink-4); }
.g-chain-arr { flex: none; width: 26px; display: grid; place-items: center; color: var(--ink-4); }
@media (max-width: 1100px) {
.g-chain { flex-direction: column; }
.g-chain-arr { transform: rotate(90deg); width: auto; padding: 2px 0; }
}
/* 终态堆叠条:五终态五色一条,比例即信息 —— 装在 .g-kpi 卡里 */
.g-mixbar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--fill-2, #eef1f5); }
.g-mixbar i { display: block; min-width: 2px; }
.g-mixbar-legend { display: flex; flex-wrap: wrap; gap: 3px 10px; font-size: 10px; color: var(--ink-3); }
.g-mixbar-legend b { font-family: var(--mono); font-weight: 700; color: var(--ink-2); }
.g-mixbar-legend i { display: inline-block; width: 7px; height: 7px; border-radius: 2px; margin-right: 4px; }