forked from ChaiHao/BPOS-MVP
三面拆分①:surface 配置与区表 —— 归属/亮灯/旧链改写皆纯函数先测后写
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 三面骨架的唯一事实源(docs/产品设计.md §1 / §6)。
|
||||
*
|
||||
* 产品分三面:设计中心定义企业、运营中心看企业跑、工作台在企业里干活。
|
||||
* 区归属、入口亮灯、旧链接改写都从这里出 —— 壳层只消费,不各写各的。
|
||||
*/
|
||||
import type { GovernanceFunction, Session } from '@contracts';
|
||||
import { GOV_ZONES } from './gov-route.ts';
|
||||
import type { GovZone } from './gov-route.ts';
|
||||
|
||||
/** 两个治理台面 —— 工作台是第三面,但它不吃这套区/页签骨架 */
|
||||
export type Surface = 'design' | 'ops';
|
||||
|
||||
export const SURFACE_META: Readonly<Record<Surface, { path: string; brand: string; searchHint: string }>> = {
|
||||
design: {
|
||||
path: '/design',
|
||||
brand: '设计中心',
|
||||
searchHint: '检索定义:流程包 / 活动 / 工具 / 数据域 / 调起规则…',
|
||||
},
|
||||
ops: {
|
||||
path: '/ops',
|
||||
brand: '运营中心',
|
||||
searchHint: '检索运行:工作流 / 任务 / 员工 / 台账…',
|
||||
},
|
||||
};
|
||||
|
||||
/** 区归属:设计五区 + 运营六区,恰好铺满 `GOV_ZONES`(由测试守住) */
|
||||
export const SURFACE_ZONES: Readonly<Record<Surface, readonly GovZone[]>> = {
|
||||
design: ['blueprint', 'workflows', 'library', 'tools', 'data'],
|
||||
ops: ['cockpit', 'tasks', 'people', 'ledgers', 'knowledge', 'system'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 亮灯功能集 —— 授权模型不动(仍是九功能 × 读/写/治),这里只说「哪个功能属哪一面」。
|
||||
*
|
||||
* `trigger` 与 `data` 两面都算:触发器**定义归设计、启停归运营**;
|
||||
* 数据**结构归设计、行数据归运营**。同一功能的两种动作各归其位,不拆授权。
|
||||
*/
|
||||
export const SURFACE_FNS: Readonly<Record<Surface, readonly GovernanceFunction[]>> = {
|
||||
design: ['process', 'activity', 'tool', 'data', 'trigger'],
|
||||
ops: ['task', 'staff', 'memory', 'system', 'trigger', 'data'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 台面入口是否亮灯。
|
||||
*
|
||||
* 判据是「持有该面任一功能的**任意**授权」—— 动作蕴含 govern ⊃ write ⊃ read,
|
||||
* 有 grant 即至少可读,入口就该看得见;进去之后动作仍逐功能校验。
|
||||
*/
|
||||
export function canSeeSurface(session: Session | undefined, surface: Surface): boolean {
|
||||
if (session === undefined) return false;
|
||||
const fns = SURFACE_FNS[surface];
|
||||
return session.grants.some((g) => g.object.kind === 'governance' && fns.includes(g.object.function));
|
||||
}
|
||||
|
||||
export function surfaceOfZone(zone: GovZone): Surface {
|
||||
return SURFACE_ZONES.design.includes(zone) ? 'design' : 'ops';
|
||||
}
|
||||
|
||||
function isGovZone(value: string): value is GovZone {
|
||||
return (GOV_ZONES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已删区的改嫁表:键是 `区` 或 `区/页签`,值是新台面下的 `台面/区/页签`。
|
||||
* 精确到页签的先匹配 —— 记忆的两个旧页签各归其位(度量升驾驶舱,活动记忆归知识库)。
|
||||
*/
|
||||
const LEGACY_MOVES: Readonly<Record<string, string>> = {
|
||||
triggers: 'design/workflows/rules',
|
||||
memory: 'ops/cockpit/dash',
|
||||
'memory/layers': 'ops/knowledge/layers',
|
||||
};
|
||||
|
||||
/** 运营首页 —— 认不出的旧链接一律落这儿,不给白屏 */
|
||||
const OPS_HOME = 'ops/cockpit/dash';
|
||||
|
||||
function toUrl(place: string): string {
|
||||
const head = place.split('/')[0];
|
||||
const surface: Surface = head === 'design' ? 'design' : 'ops';
|
||||
return `${SURFACE_META[surface].path}#/${place}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧 `#/gov/<区>/…` 深链 → 新台面的完整 URL(pathname + hash)。
|
||||
* 非 gov 哈希返回 `undefined`(不是我们的事,别乱改人家的地址)。
|
||||
*/
|
||||
export function legacyGovTarget(hash: string): string | undefined {
|
||||
const parts = hash
|
||||
.replace(/^#\/?/, '')
|
||||
.split('/')
|
||||
.filter((s) => s !== '');
|
||||
if (parts[0] !== 'gov') return undefined;
|
||||
|
||||
const zone = parts[1];
|
||||
if (zone === undefined) return toUrl(OPS_HOME);
|
||||
|
||||
const moved = LEGACY_MOVES[`${zone}/${parts[2] ?? ''}`] ?? LEGACY_MOVES[zone];
|
||||
if (moved !== undefined) return toUrl(moved);
|
||||
if (!isGovZone(zone)) return toUrl(OPS_HOME);
|
||||
|
||||
// 仍在的区:原样带着页签与下钻栈搬到新台面
|
||||
return toUrl([surfaceOfZone(zone), ...parts.slice(1)].join('/'));
|
||||
}
|
||||
Reference in New Issue
Block a user