Files
lingxi/apps/web/src/navigation/nav-config.ts
T
Stev_Wang 61894df401 feat(identity): 完成 A2 角色权限与数据范围全栈落地
背景:A2 开卡前完成 H1 权限门,确认权限点目录、特殊权限、SoD 三对、六类数据范围与超管隐式全集口径(ADR-001)。

改动:
- shared:30 权限点 / 10 模块目录与元数据、特殊权限 6 编码、SoD 三对、角色与权限树契约;/me 改 dataScopes 多角色并集。
- prisma:expand 迁移新增 permissions / data_scopes;种子按原型口径重写角色权限、数据范围与 8 项特殊授予。
- api:角色 CRUD / 复制 / 停用 / 删除、权限点启动同步、SoD 40304、系统角色保护、六类范围 ScopeContext 与 CustomerDirectory 端口、变更审计与 outbox。
- web:方案 B 主从分栏三角色页面(列表 / 编辑器 / 新建),权限树、范围选择、特殊权限二次确认、保存影响、SoD 冲突、只读态与模板预填。
- 测试:角色集成 13 条 + A2 e2e 1 条;A1 全量回归。

验证:format:check / lint / typecheck / test(306)/ test:int(27)/ A2 e2e(1,复跑通过)/ openapi:export 全绿。
2026-11-03 12:26:51 +08:00

415 lines
11 KiB
TypeScript

import { Permission, type PermissionCode } from '@lingxi/shared';
import {
IconAsset,
IconCheck,
IconClock,
IconCopy,
IconDelivery,
IconDocs,
IconExport,
IconFilter,
IconGlobal,
IconGrid,
IconHome,
IconImageAdd,
IconMaterial,
IconPlay,
IconRobot,
IconScript,
IconSettings,
IconShield,
IconStack,
IconTask,
IconUser,
IconVideo,
IconView,
} from '@lingxi/ui/icons';
import type { ComponentType } from 'react';
export type NavIcon = ComponentType<{ size?: number | string; className?: string }>;
export interface NavItem {
key: string;
label: string;
path: string;
icon: NavIcon;
/** 未配置权限点的菜单对所有登录用户可见 */
permission?: PermissionCode;
description?: string;
}
export interface NavModule {
key: string;
label: string;
path: string;
icon: NavIcon;
permission?: PermissionCode;
items: NavItem[];
}
/**
* 顶部模块菜单 + 左侧功能菜单的唯一配置源(A0-FE-03)。
* 新增页面时在这里登记;侧边栏与面包屑都从这里派生,禁止在页面内写死导航。
*/
export const NAV_MODULES: readonly NavModule[] = [
{
key: 'dashboard',
label: '工作台',
path: '/',
icon: IconHome,
items: [
{
key: 'overview',
label: '工作台',
path: '/',
icon: IconHome,
description: '今天的生产任务、待审核脚本与最近素材',
},
],
},
{
key: 'scripts',
label: '脚本',
path: '/scripts/library',
icon: IconScript,
permission: Permission.SCRIPT_READ,
items: [
{
key: 'library',
label: '脚本库',
path: '/scripts/library',
icon: IconDocs,
permission: Permission.SCRIPT_READ,
description: '按客户、行业与状态检索已生成脚本',
},
{
key: 'framework-cards',
label: '要素卡',
path: '/scripts/framework-cards',
icon: IconTask,
permission: Permission.SCRIPT_WRITE,
description: '维护行业包的要素卡字段与填写规则',
},
{
key: 'templates',
label: '脚本模板',
path: '/scripts/templates',
icon: IconStack,
permission: Permission.SCRIPT_WRITE,
description: '结构模板、镜头模板与提示词模板',
},
{
key: 'batch',
label: '批量生成',
path: '/scripts/batch',
icon: IconRobot,
permission: Permission.SCRIPT_WRITE,
description: '按要素卡批量生成脚本并排队审核',
},
{
key: 'review',
label: '脚本审核',
path: '/scripts/review',
icon: IconCheck,
permission: Permission.SCRIPT_REVIEW,
description: '审核生成结果,标记低质量脚本',
},
],
},
{
key: 'material',
label: '素材',
path: '/material/image-compose',
icon: IconMaterial,
permission: Permission.MATERIAL_READ,
items: [
{
key: 'image-compose',
label: '图片合成',
path: '/material/image-compose',
icon: IconImageAdd,
permission: Permission.MATERIAL_RENDER,
description: 'PSD 套版、抠图与确定性合成',
},
{
key: 'video',
label: '视频生产',
path: '/material/video',
icon: IconVideo,
permission: Permission.MATERIAL_RENDER,
description: '逐镜生成、合成与成片预览',
},
{
key: 'tasks',
label: '生产任务',
path: '/material/tasks',
icon: IconTask,
permission: Permission.MATERIAL_READ,
description: '任务进度、失败原因与重试',
},
{
key: 'quick-review',
label: '快筛',
path: '/material/quick-review',
icon: IconFilter,
permission: Permission.MATERIAL_REVIEW,
description: '按快捷键快速筛选候选素材',
},
{
key: 'preview',
label: '成片预览',
path: '/material/preview',
icon: IconPlay,
permission: Permission.MATERIAL_READ,
description: '播放成片并核对规格与标识',
},
],
},
{
key: 'assets',
label: '资产',
path: '/assets/library',
icon: IconAsset,
permission: Permission.ASSET_READ,
items: [
{
key: 'library',
label: '资产库',
path: '/assets/library',
icon: IconAsset,
permission: Permission.ASSET_READ,
description: '图片、视频、音频与模板资产',
},
{
key: 'templates',
label: '模板库',
path: '/assets/templates',
icon: IconCopy,
permission: Permission.ASSET_READ,
description: 'PSD 模板与版式配置',
},
{
key: 'pools',
label: '人物与场景',
path: '/assets/pools',
icon: IconUser,
permission: Permission.ASSET_READ,
description: '人物、场景与风格池',
},
{
key: 'prompts',
label: '提示词模板',
path: '/assets/prompts',
icon: IconRobot,
permission: Permission.ASSET_READ,
description: '视频与图片生成的提示词版本',
},
],
},
{
key: 'delivery',
label: '交付',
path: '/delivery/export',
icon: IconDelivery,
permission: Permission.DELIVERY_READ,
items: [
{
key: 'export',
label: '导出任务',
path: '/delivery/export',
icon: IconExport,
permission: Permission.DELIVERY_EXPORT,
description: '勾选素材导出并按批次归档',
},
{
key: 'records',
label: '导出记录',
path: '/delivery/records',
icon: IconClock,
permission: Permission.DELIVERY_READ,
description: '导出批次、签名链接与归档状态',
},
{
key: 'manifests',
label: '交付清单',
path: '/delivery/manifests',
icon: IconTask,
permission: Permission.DELIVERY_READ,
description: '交付清单、客户签收与留痕',
},
],
},
{
key: 'system',
label: '管理中心',
path: '/system/users',
icon: IconSettings,
permission: Permission.USER_VIEW,
items: [
{
key: 'users',
label: '用户与组织',
path: '/system/users',
icon: IconUser,
permission: Permission.USER_VIEW,
description: '账号、组织与登录状态',
},
{
key: 'roles',
label: '角色权限',
path: '/system/roles',
icon: IconShield,
permission: Permission.ROLE_VIEW,
description: '角色、权限点与职责互斥',
},
{
key: 'customers',
label: '客户',
path: '/system/customers',
icon: IconGlobal,
permission: Permission.CUSTOMER_MANAGE,
description: '客户、行业归属与数据范围',
},
{
key: 'industries',
label: '行业树',
path: '/system/industries',
icon: IconStack,
permission: Permission.INDUSTRY_MANAGE,
description: '行业分类树与行业包绑定',
},
{
key: 'dicts',
label: '字典',
path: '/system/dicts',
icon: IconDocs,
permission: Permission.DICT_MANAGE,
description: '核心字典与业务枚举',
},
{
key: 'audit',
label: '审计日志',
path: '/system/audit-logs',
icon: IconView,
permission: Permission.AUDIT_READ,
description: '关键操作留痕与导出记录',
},
{
key: 'ai',
label: '模型接入',
path: '/system/ai',
icon: IconRobot,
permission: Permission.AI_CONFIGURE,
description: '供应商、路由与预算阈值',
},
{
key: 'settings',
label: '系统配置',
path: '/system/settings',
icon: IconSettings,
permission: Permission.SYSTEM_CONFIGURE,
description: '阈值、上限与功能开关',
},
{
key: 'industry-packs',
label: '行业包',
path: '/system/industry-packs',
icon: IconGrid,
permission: Permission.INDUSTRY_MANAGE,
description: '行业包版本、发布与灰度',
},
{
key: 'forms',
label: '表单设计器',
path: '/system/forms',
icon: IconGrid,
permission: Permission.FORM_DESIGN,
description: '受控字段白名单与表单布局',
},
{
key: 'customer-fields',
label: '客户字段',
path: '/system/customer-fields',
icon: IconTask,
permission: Permission.CUSTOMER_MANAGE,
description: '客户级扩展字段与审批',
},
{
key: 'compliance',
label: '合规规则',
path: '/system/compliance',
icon: IconShield,
permission: Permission.SYSTEM_CONFIGURE,
description: '禁忌词、极限词与合规规则集',
},
],
},
];
export function filterNavByPermissions(
modules: readonly NavModule[],
permissions: ReadonlySet<string>,
): NavModule[] {
return modules
.map((module) => {
if (module.permission && !permissions.has(module.permission)) {
return null;
}
const items = module.items.filter(
(item) => item.permission === undefined || permissions.has(item.permission),
);
return items.length > 0 ? { ...module, items } : null;
})
.filter((module): module is NavModule => module !== null);
}
export function findNavModule(
modules: readonly NavModule[],
moduleKey: string,
): NavModule | undefined {
return modules.find((module) => module.key === moduleKey);
}
export interface ActiveNav {
module: NavModule;
item: NavItem | undefined;
}
export function findActiveNav(modules: readonly NavModule[], pathname: string): ActiveNav | null {
const normalized = pathname !== '/' ? pathname.replace(/\/+$/, '') : pathname;
for (const module of modules) {
const item = module.items.find(
(candidate) =>
candidate.path === normalized ||
(candidate.path !== '/' && normalized.startsWith(`${candidate.path}/`)),
);
if (item) {
return { module, item };
}
}
return null;
}
export interface BreadcrumbEntry {
key: string;
label: string;
path?: string;
}
export function getBreadcrumbs(modules: readonly NavModule[], pathname: string): BreadcrumbEntry[] {
const active = findActiveNav(modules, pathname);
if (!active) {
return [{ key: 'home', label: '工作台', path: '/' }];
}
const entries: BreadcrumbEntry[] = [{ key: 'home', label: '工作台', path: '/' }];
if (active.module.key !== 'dashboard') {
entries.push({ key: active.module.key, label: active.module.label });
}
if (active.item && active.item.path !== '/' && active.item.key !== active.module.items[0]?.key) {
entries.push({ key: active.item.key, label: active.item.label, path: active.item.path });
} else if (active.item && active.item.path !== '/') {
entries.push({ key: active.item.key, label: active.item.label, path: active.item.path });
}
return entries;
}