三面拆分⑫: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 = {
// 设计中心