/** * 治理台外壳 —— 自原型 `Governance.tsx` 第 389–514 行平移。 * * 左轨(九功能)→ 顶栏 → 面包屑 → 区首页(页签 + 主视图)/ 整页下钻。 * 「**不用抽屉、不用弹窗**:每一层内容都有自己的页面。」 * * §9.2「路由状态即应用状态」:当前区 / 页签 / 下钻栈全部编码进 URL 的 hash, * 刷新与深链都能完整还原;前端零过程状态。 */ import { useCallback, useEffect, useState } from 'react'; import { Ico } from '@web-shared/Ico.tsx'; import { TABS, ZM, ZONES, routeLabel } from '@web-shared/gov/gov-route.ts'; import type { GovZone, Nav, Route } from '@web-shared/gov/gov-route.ts'; import { useGovStaff, useGovTasks } from '@web-shared/gov/gov-api.ts'; import { TasksHome, TaskLogPage, TaskPage, WfPage } from '@features/gov-task'; import { EmpPage, PeopleHome, RolePage } from '@features/gov-staff'; import { EdgesPage, PkgPage, WorkflowsHome } from '@features/gov-process'; import { TriggerPage, TriggersHome } from '@features/gov-trigger'; import { ActPage, LibraryHome } from '@features/gov-activity'; import { ToolPage, ToolsHome } from '@features/gov-tool'; import { DataHome, ObjPage } from '@features/gov-data'; import { MemPage, MemoryHome } from '@features/gov-memory'; import { ChanPage, LlmPage, SystemHome } from '@features/gov-system'; /* ── URL 编解码:`#/gov//[/:…]` ──────────────────────── */ type Place = { zone: GovZone; tab: string; stack: Route[] }; function readPlace(): Place { const raw = window.location.hash.replace(/^#\/?/, ''); const parts = raw.split('/').filter((s) => s !== ''); const zone = (ZONES.find((z) => z.key === parts[1])?.key ?? 'tasks') as GovZone; const tabs = TABS[zone]; const tab = tabs.find((t) => t.key === parts[2])?.key ?? tabs[0]!.key; const stack: Route[] = []; for (const seg of parts.slice(3)) { const at = seg.indexOf(':'); if (at <= 0) continue; stack.push({ k: seg.slice(0, at), id: decodeURIComponent(seg.slice(at + 1)) } as Route); } return { zone, tab, stack }; } const writePlace = (p: Place): string => ['#', 'gov', p.zone, p.tab, ...p.stack.map((r) => `${r.k}:${encodeURIComponent(r.id)}`)].join('/'); export function GovShell() { const [place, setPlace] = useState(readPlace); const staff = useGovStaff(); const tasks = useGovTasks(); // 浏览器前进 / 后退即导航 —— 路由状态就是应用状态 useEffect(() => { const onHash = () => setPlace(readPlace()); window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); const go = useCallback((next: Place) => { window.location.hash = writePlace(next); setPlace(next); }, []); const push = useCallback( (r: Route) => go({ ...readPlace(), stack: [...readPlace().stack, r] }), [go], ); const back = useCallback(() => { const p = readPlace(); go({ ...p, stack: p.stack.slice(0, -1) }); }, [go]); const goZone = useCallback((z: GovZone) => go({ zone: z, tab: TABS[z][0]!.key, stack: [] }), [go]); const jump = useCallback((z: GovZone, tab: string, r?: Route) => go({ zone: z, tab, stack: r ? [r] : [] }), [go]); const nav: Nav = { push, jump }; // Esc 退一级 —— 与面包屑同义 useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === 'Escape') back(); }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [back]); const { zone, tab, stack } = place; const meta = ZM[zone]; const top = stack[stack.length - 1]; const me = staff.data?.staff.find((s) => s.account === 'fengbing'); /** 告警计数:异常工作流 + 非放行终态的任务 */ const alerts = (tasks.data?.workflows ?? []).filter((w) => w.state === 'abnormal').length + (tasks.data?.tasks ?? []).filter((t) => t.terminal !== undefined && t.terminal !== 'released').length; /* 面包屑:治理台 › 区 › 页签 › 下钻各级 */ const crumbs: Array<{ label: string; go?: () => void }> = [{ label: '治理台', go: () => goZone(zone) }]; crumbs.push({ label: meta.title, go: () => go({ zone, tab, stack: [] }) }); crumbs.push({ label: TABS[zone].find((t) => t.key === tab)?.label ?? '', go: () => go({ zone, tab, stack: [] }) }); stack.forEach((r, i) => crumbs.push({ label: routeLabel(r), ...(i === stack.length - 1 ? {} : { go: () => go({ zone, tab, stack: stack.slice(0, i + 1) }) }), }), ); return (
{/* ===== 深色左轨 ===== */}
{/* ===== 深色顶栏 ===== */}
JCAI 操作系统单组织 · ≤50 人 · 生产环境
工作台
{/* ===== 工作面 ===== */}
{stack.length > 0 && ( )} {crumbs.map((c, i) => ( {i > 0 && ›} {c.go ? : {c.label}} ))}
{top === undefined ? ( <>

{meta.title}

{meta.desc}

{TABS[zone].map((t) => ( ))}
) : (
)}
); } function ZoneHome({ zone, tab, nav }: { zone: GovZone; tab: string; nav: Nav }) { const P = { tasks: TasksHome, people: PeopleHome, workflows: WorkflowsHome, triggers: TriggersHome, library: LibraryHome, tools: ToolsHome, data: DataHome, memory: MemoryHome, system: SystemHome, }[zone]; return

; } function Detail({ route, nav }: { route: Route; nav: Nav }) { switch (route.k) { case 'wf': case 'apr': return ; case 'task': return ; case 'tasklog': return ; case 'emp': return ; case 'role': return ; case 'pkg': return ; case 'edges': return ; case 'trigger': return ; case 'act': return ; case 'tool': return ; case 'obj': return ; case 'mem': return ; case 'llm': return ; case 'chan': return ; } }