三面拆分②:三章标签与入口路由,/gov 深链整链改写

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 20:53:43 +08:00
co-authored by Claude Fable 5
parent e578b5bc34
commit cbae9b1284
2 changed files with 62 additions and 22 deletions
+27 -10
View File
@@ -1,13 +1,14 @@
/** /**
* apps/web 唯一组装层(编码规范 §9):样式装载、Provider、鉴权壳、SSE 连接。 * apps/web 唯一组装层(编码规范 §9):样式装载、Provider、鉴权壳、SSE 连接。
* *
* **两台面各自装自己的皮肤。** * **三面各自装自己的皮肤。**
* 两份原型本是两个独立应用,各自只加载自己那两份 CSS: * 两份原型本是两个独立应用,各自只加载自己那两份 CSS:
* 工作台 = `proto.vendor.css`(原型皮肤)+ `app.css`(补充,作用域 `.proto-root`) * 工作台 = `proto.vendor.css`(原型皮肤)+ `app.css`(补充,作用域 `.proto-root`)
* 治理台 = `theme.css`(令牌 + 页面基座)+ `console.css`(gov-* 组件) * 治理面 = `theme.css`(令牌 + 页面基座)+ `console.css`(gov-* 组件)
* 两份基座都写了 `body { min-width / background }` 这类**全局**规则,谁后加载谁赢 —— * 两份基座都写了 `body { min-width / background }` 这类**全局**规则,谁后加载谁赢 ——
* 四份一起塞进同一个文档,后加载的治理台基座会把工作台的壳整个盖掉(实测:布局全崩)。 * 四份一起塞进同一个文档,后加载的治理台基座会把工作台的壳整个盖掉(实测:布局全崩)。
* 所以按台面**分别装载**,这也正是 1:1 平移该有的样子:每台面拿到的样式集与它的原型一致。 * 所以按台面**分别装载**,这也正是 1:1 平移该有的样子:每台面拿到的样式集与它的原型一致。
* 设计中心与运营中心是治理的两个台面,共用同一套皮肤,只是区表与首页不同。
*/ */
import { StrictMode, useEffect } from 'react'; import { StrictMode, useEffect } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
@@ -21,24 +22,40 @@ import { LoginPage } from '@features/auth';
import { ApiCallError, call } from '@web-shared/typed-fetch.ts'; import { ApiCallError, call } from '@web-shared/typed-fetch.ts';
import { queryClient, queryKeys } from './query.ts'; import { queryClient, queryKeys } from './query.ts';
import { applyTabIdentity } from './tab-identity.ts'; import { applyTabIdentity } from './tab-identity.ts';
import type { TabSurface } from './tab-identity.ts';
import { SURFACE_META, legacyGovTarget } from '@web-shared/gov/surfaces.ts';
import type { Surface } from '@web-shared/gov/surfaces.ts';
const container = document.getElementById('root'); const container = document.getElementById('root');
if (!container) throw new Error('#root 缺失'); if (!container) throw new Error('#root 缺失');
/** 路径或 hash 决定进哪台 —— 路由状态即应用状态,可深链(§9.2) */ /*
const isGovernance = * 旧 `/gov` 链接一条不死:带着深链原样改写到新台面(replace —— 不在历史里留一个回不去的地址)。
window.location.pathname.startsWith('/gov') || window.location.hash.startsWith('#/gov'); * 治理中心一分为二后,`/gov` 只剩这一个职责。
*/
const legacy = window.location.pathname.startsWith('/gov')
? (legacyGovTarget(window.location.hash) ?? `${SURFACE_META.ops.path}#/ops/cockpit/dash`)
: undefined;
if (legacy !== undefined) window.location.replace(legacy);
/** 路径决定进哪一面 —— 路由状态即应用状态,可深链(§9.2) */
const surface: TabSurface = window.location.pathname.startsWith(SURFACE_META.design.path)
? 'design'
: window.location.pathname.startsWith(SURFACE_META.ops.path)
? 'ops'
: 'desk';
const govSurface: Surface | undefined = surface === 'desk' ? undefined : surface;
// 网页标签的名与章 —— 样式装载前就挂上,标签从第一帧起就叫对名字 // 网页标签的名与章 —— 样式装载前就挂上,标签从第一帧起就叫对名字
applyTabIdentity(isGovernance); applyTabIdentity(surface);
/* /*
* 样式先装、再渲染:CSS 落地后才挂 DOM,避免无样式闪烁。 * 样式先装、再渲染:CSS 落地后才挂 DOM,避免无样式闪烁。
* 台面切换靠整页导航(两台面的 body 基座互斥,不做客户端软切换)。 * 台面切换靠整页导航(工作台与治理面的 body 基座互斥,不做客户端软切换)。
* 登录页两台面共用,故恒装 —— 它通篇作用域在 `.jc-login`,不与任一基座相争。 * 登录页三面共用,故恒装 —— 它通篇作用域在 `.jc-login`,不与任一基座相争。
*/ */
await import('./styles/login.css'); await import('./styles/login.css');
if (isGovernance) { if (govSurface !== undefined) {
await import('./styles/theme.css'); await import('./styles/theme.css');
await import('./styles/console.css'); await import('./styles/console.css');
} else { } else {
@@ -79,7 +96,7 @@ function Gate() {
return <LoginPage onLogin={(s) => client.setQueryData(queryKeys.session, s)} />; return <LoginPage onLogin={(s) => client.setQueryData(queryKeys.session, s)} />;
} }
return isGovernance ? ( return govSurface !== undefined ? (
<GovShell /> <GovShell />
) : ( ) : (
// .proto-root 是工作台补充样式的作用域(原型 app.css 通篇以它起头); // .proto-root 是工作台补充样式的作用域(原型 app.css 通篇以它起头);
+35 -12
View File
@@ -1,9 +1,10 @@
/** /**
* 网页标签的名与章 —— 两台面各有一套,多标签并开时一眼可辨。 * 网页标签的名与章 —— 三面各有一套,多标签并开时一眼可辨「我在定义、在监控、还是在干活」。
* *
* 章随台面的**皮肤基调**走,不另造视觉: * 章随台面的**皮肤基调**走,不另造视觉:
* 工作台 = 品牌蓝渐变圆角方 + 白色机器人(与左轨品牌章、登录页方章同源); * 工作台 = 品牌蓝渐变圆角方 + 白色机器人(与左轨品牌章、登录页方章同源);
* 治理中心 = 深色底(治理台顶栏的那种深)+ 盾形勾(管的是规则与放行)。 * 设计中心 = 深靛底 + 路网(定义企业的流转 —— 与流程包区的靛色同源);
* 运营中心 = 深色底 + 仪表(看企业今天跑得怎样 —— 驾驶舱的那支指针)。
* SVG 以 data: URI 内联 —— 不添静态资产,构建零配置。 * SVG 以 data: URI 内联 —— 不添静态资产,构建零配置。
*/ */
@@ -24,17 +25,39 @@ const DESK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
</g> </g>
</svg>`; </svg>`;
const GOV_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> /** 设计中心:深靛底 + 路网 —— 线接着点,正是「连线即映射」 */
<rect width="32" height="32" rx="8" fill="#141b2e"/> const DESIGN_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g transform="translate(4,4)" fill="none" stroke="#8fb0ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <rect width="32" height="32" rx="8" fill="#232b6e"/>
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/> <g transform="translate(4,4)" fill="none" stroke="#9db1ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 12 2 2 4-4" stroke="#fff"/> <path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" stroke="#fff"/>
<circle cx="6" cy="19" r="2.6" fill="#232b6e"/>
<circle cx="18" cy="5" r="2.6" fill="#232b6e"/>
</g> </g>
</svg>`; </svg>`;
/** 进哪台就挂哪套 —— 登录页也按 URL 归台,标签在登录前就叫对名字 */ /** 运营中心:深色底 + 仪表 —— 指针指着「今天跑得怎样」 */
export function applyTabIdentity(isGovernance: boolean): void { const OPS_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
document.title = isGovernance ? 'JCAI 治理中心' : 'JCAI 工作台'; <rect width="32" height="32" rx="8" fill="#141b2e"/>
<g transform="translate(4,5)" fill="none" stroke="#5fdcb4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3.34 18a10 10 0 1 1 17.32 0"/>
<path d="m12 13 4.2-4.2" stroke="#fff"/>
<circle cx="12" cy="18" r="1.6" fill="#fff" stroke="none"/>
</g>
</svg>`;
/** 三面各一套章名 —— `desk` 是工作台,另两个是治理的两个台面 */
export type TabSurface = 'desk' | 'design' | 'ops';
const IDENTITY: Readonly<Record<TabSurface, { title: string; icon: string }>> = {
desk: { title: 'JCAI 工作台', icon: DESK_ICON },
design: { title: 'JCAI 设计中心', icon: DESIGN_ICON },
ops: { title: 'JCAI 运营中心', icon: OPS_ICON },
};
/** 进哪面就挂哪套 —— 登录页也按 URL 归面,标签在登录前就叫对名字 */
export function applyTabIdentity(surface: TabSurface): void {
const { title, icon } = IDENTITY[surface];
document.title = title;
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]'); let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (link === null) { if (link === null) {
@@ -43,5 +66,5 @@ export function applyTabIdentity(isGovernance: boolean): void {
document.head.appendChild(link); document.head.appendChild(link);
} }
link.type = 'image/svg+xml'; link.type = 'image/svg+xml';
link.href = 'data:image/svg+xml;utf8,' + encodeURIComponent(isGovernance ? GOV_ICON : DESK_ICON); link.href = 'data:image/svg+xml;utf8,' + encodeURIComponent(icon);
} }