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 全绿。
This commit is contained in:
Stev_Wang committed 2026-11-03 12:26:51 +08:00
1 parent f97f97b0cd
commit 61894df401
80 files changed
+14380 -237

No files matched your search

@@ -0,0 +1,97 @@
/* eslint-disable max-lines-per-function -- 单组件按原型区块展开,拆分反而降低可读性 */
import { Button, SideSheet } from '@douyinfe/semi-ui-19';
import type { PermissionTree, RoleDetail } from '@lingxi/shared';
import { IconCheck, IconStar } from '@lingxi/ui/icons';
import type { ReactNode } from 'react';
import { scopeLabelOf } from './role-display';
export interface PermissionCatalogDrawerProps {
visible: boolean;
tree: PermissionTree | undefined;
role: RoleDetail | null;
onClose: () => void;
}
/** 权限点目录抽屉:只读展示 30 个权限点与当前角色的持有情况 */
export function PermissionCatalogDrawer({
visible,
tree,
role,
onClose,
}: PermissionCatalogDrawerProps): ReactNode {
const granted = new Set<string>([
...(role?.permissions ?? []),
...(role?.specialPermissions ?? []),
]);
return (
<SideSheet
visible={visible}
title="权限点目录"
width={480}
placement="right"
onCancel={onClose}
footer={
<div className="lx-role-catalog__foot">
<span>权限点变更即时对绑定用户生效</span>
<Button onClick={onClose}>关闭</Button>
</div>
}
>
<div className="lx-role-catalog">
<p className="lx-role-catalog__sub">
{tree?.totalPermissions ?? 30} 个权限点 · {tree?.specialPermissions.length ?? 6}{' '}
个特殊权限;★ 默认关闭、需单独授予。
{role === null
? ' 当前角色尚未加载。'
: ` 当前勾选为「${role.name}」持有(${String(role.permissionCount + role.specialPermissionCount)} 个)。`}
</p>
{tree === undefined ? (
<p className="lx-role-catalog__empty">权限点目录加载中…</p>
) : (
tree.modules.map((module) => (
<section key={module.module} className="lx-role-catalog__module">
<header className="lx-role-catalog__module-head">
<b>{module.moduleLabel}</b>
<span>
{module.permissions.filter((permission) => granted.has(permission.code)).length} /{' '}
{module.permissions.length}
</span>
</header>
{module.permissions.map((permission) => {
const owned = granted.has(permission.code);
return (
<div
key={permission.code}
className={owned ? 'lx-role-catalog__row is-granted' : 'lx-role-catalog__row'}
>
<span className="lx-role-catalog__check" aria-hidden="true">
{owned ? <IconCheck size={12} /> : null}
</span>
<span className="lx-role-catalog__copy">
<span className="lx-role-catalog__name">
{permission.name}
{permission.isSpecial ? (
<span className="lx-role-tag lx-role-tag--special">★ 特殊</span>
) : null}
</span>
<span className="lx-role-catalog__desc">{permission.description}</span>
</span>
<code className="lx-role-catalog__code">{permission.code}</code>
</div>
);
})}
</section>
))
)}
<div className="lx-role-catalog__note">
<IconStar size={14} />
<span>
特殊权限 6 项与普通权限分开授予,不随角色复制带入;当前角色数据范围为「
{role === null ? '—' : scopeLabelOf(role.dataScope.type)}」。
</span>
</div>
</div>
</SideSheet>
);
}