背景: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 全绿。
704 lines
21 KiB
TypeScript
704 lines
21 KiB
TypeScript
/* eslint-disable max-lines-per-function, complexity -- 编辑器页负责角色加载、草稿编辑、保存编排与离开拦截,集中便于追踪数据流 */
|
||
import { Button } from '@douyinfe/semi-ui-19';
|
||
import {
|
||
Permission,
|
||
type PermissionNode,
|
||
type PermissionTree,
|
||
type RoleDetail,
|
||
type RoleListItem,
|
||
type SodConflict,
|
||
} from '@lingxi/shared';
|
||
import { ErrorState, notifySuccess, notifyWarning, PageSkeleton } from '@lingxi/ui';
|
||
import { IconAlert, IconBack, IconCopy, IconPause, IconPlay, IconShield } from '@lingxi/ui/icons';
|
||
import type { ReactNode } from 'react';
|
||
import { useEffect, useMemo, useState } from 'react';
|
||
import { useBlocker, useNavigate, useParams } from 'react-router';
|
||
|
||
import { ApiError } from '@/api/client';
|
||
import { Can } from '@/components/Can';
|
||
import {
|
||
usePermissionTreeQuery,
|
||
useRoleDetailQuery,
|
||
useRoleListQuery,
|
||
useUpdateRoleMutation,
|
||
} from '@/hooks/queries/use-roles';
|
||
import { usePermission } from '@/hooks/use-permission';
|
||
import { roleDetailPath, roleNewPath, ROUTES } from '@/routes';
|
||
|
||
import {
|
||
DisableRoleDialog,
|
||
LeaveRoleDialog,
|
||
SaveRoleDialog,
|
||
SpecialPermissionDialog,
|
||
} from './role-dialogs';
|
||
import { buildPermissionMetaMap, formatRoleDateTime, scopeLabelOf } from './role-display';
|
||
import {
|
||
conflictsFromErrorDetails,
|
||
type RoleDraftErrors,
|
||
saveResultMessage,
|
||
validateUpdateDraft,
|
||
} from './role-utils';
|
||
import { type RoleEditorTabKey, RoleEditorTabs } from './RoleEditorTabs';
|
||
import { RoleListPanel } from './RoleListPanel';
|
||
import { RoleStatusTag } from './RoleStatusTag';
|
||
import { useRoleEditor } from './use-role-editor';
|
||
|
||
/** 左侧角色切换列表:与列表页同一查询口径,全部状态一次拉取 */
|
||
const PEER_ROLE_QUERY = {
|
||
limit: 100,
|
||
status: 'all',
|
||
sort: 'created_at',
|
||
order: 'desc',
|
||
} as const;
|
||
|
||
interface EditorContentProps {
|
||
detail: RoleDetail;
|
||
tree: PermissionTree | undefined;
|
||
treeLoading: boolean;
|
||
treeError: Error | null;
|
||
onRetryTree: () => void;
|
||
peers: readonly RoleListItem[];
|
||
peersLoading: boolean;
|
||
peersError: Error | null;
|
||
onRetryPeers: () => void;
|
||
onSelectPeer: (roleId: string) => void;
|
||
canManage: boolean;
|
||
}
|
||
|
||
function mergeConflicts(
|
||
local: readonly SodConflict[],
|
||
server: readonly SodConflict[],
|
||
): SodConflict[] {
|
||
const seen = new Set<string>();
|
||
const merged: SodConflict[] = [];
|
||
for (const conflict of [...local, ...server]) {
|
||
if (seen.has(conflict.pairId)) {
|
||
continue;
|
||
}
|
||
seen.add(conflict.pairId);
|
||
merged.push(conflict);
|
||
}
|
||
return merged;
|
||
}
|
||
|
||
function conflictMessages(
|
||
conflicts: readonly SodConflict[],
|
||
meta: ReadonlyMap<string, { name: string }> | null,
|
||
): string {
|
||
return conflicts
|
||
.map((conflict) => {
|
||
const left = meta?.get(conflict.permissions[0])?.name ?? conflict.permissions[0];
|
||
const right = meta?.get(conflict.permissions[1])?.name ?? conflict.permissions[1];
|
||
return `命中职责互斥:${conflict.label}。请移除「${left}」或「${right}」后重试。`;
|
||
})
|
||
.join(' ');
|
||
}
|
||
|
||
function EditorAlert({
|
||
readonly,
|
||
locked,
|
||
conflicts,
|
||
saveError,
|
||
meta,
|
||
}: {
|
||
readonly: boolean;
|
||
locked: boolean;
|
||
conflicts: readonly SodConflict[];
|
||
saveError: string | null;
|
||
meta: ReadonlyMap<string, { name: string }> | null;
|
||
}): ReactNode {
|
||
if (readonly) {
|
||
return (
|
||
<div className="lx-role-editor__alert is-info">
|
||
<IconAlert size={15} />
|
||
<span>你没有角色管理权限(role:manage),当前为只读视图。如需调整请联系平台管理员。</span>
|
||
</div>
|
||
);
|
||
}
|
||
if (locked) {
|
||
return (
|
||
<div className="lx-role-editor__alert is-info">
|
||
<IconAlert size={15} />
|
||
<span>超级管理员为系统内置角色,仅显示名可修改;权限、数据范围与特殊权限由系统保证。</span>
|
||
</div>
|
||
);
|
||
}
|
||
if (conflicts.length > 0) {
|
||
return (
|
||
<div className="lx-role-editor__alert is-danger">
|
||
<IconAlert size={15} />
|
||
<span>{conflictMessages(conflicts, meta)}</span>
|
||
</div>
|
||
);
|
||
}
|
||
if (saveError !== null) {
|
||
return (
|
||
<div className="lx-role-editor__alert is-danger">
|
||
<IconAlert size={15} />
|
||
<span>{saveError}</span>
|
||
</div>
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function EditorFoot({
|
||
readonly,
|
||
locked,
|
||
conflicts,
|
||
editor,
|
||
saving,
|
||
affectedUsers,
|
||
onCancel,
|
||
onSave,
|
||
}: {
|
||
readonly: boolean;
|
||
locked: boolean;
|
||
conflicts: readonly SodConflict[];
|
||
editor: ReturnType<typeof useRoleEditor>;
|
||
saving: boolean;
|
||
affectedUsers: number;
|
||
onCancel: () => void;
|
||
onSave: () => void;
|
||
}): ReactNode {
|
||
if (readonly) {
|
||
return (
|
||
<footer className="lx-role-editor__foot">
|
||
<span className="lx-role-editor__impact is-clean">
|
||
只读视图:可以查看权限配置,但没有修改权限
|
||
</span>
|
||
</footer>
|
||
);
|
||
}
|
||
const dirty = editor.isDirty;
|
||
return (
|
||
<footer className="lx-role-editor__foot">
|
||
<div className="lx-role-editor__impact">
|
||
{conflicts.length > 0 ? (
|
||
<span className="is-blocked">
|
||
<IconAlert size={14} />
|
||
存在职责互斥,保存不可用
|
||
</span>
|
||
) : !dirty ? (
|
||
<span className="is-clean">未做修改</span>
|
||
) : (
|
||
<>
|
||
<span className="is-dirty">未保存</span>
|
||
{editor.diff.totalAdded > 0 ? (
|
||
<span>
|
||
本次新增 <b>{editor.diff.totalAdded}</b>
|
||
</span>
|
||
) : null}
|
||
{editor.diff.totalRemoved > 0 ? (
|
||
<span>
|
||
移除 <b>{editor.diff.totalRemoved}</b>
|
||
</span>
|
||
) : null}
|
||
{editor.diff.nameChanged || editor.diff.descriptionChanged ? (
|
||
<span>基本信息已修改</span>
|
||
) : null}
|
||
{editor.diff.scopeChanged ? <span>数据范围已修改</span> : null}
|
||
<span>
|
||
影响 <b>{affectedUsers}</b> 名用户
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="lx-role-editor__foot-actions">
|
||
<Button onClick={onCancel} disabled={saving}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
theme="solid"
|
||
type="primary"
|
||
loading={saving}
|
||
disabled={conflicts.length > 0 || !dirty}
|
||
onClick={onSave}
|
||
>
|
||
{locked ? '保存名称' : '保存'}
|
||
</Button>
|
||
</div>
|
||
</footer>
|
||
);
|
||
}
|
||
|
||
function EditorContent({
|
||
detail,
|
||
tree,
|
||
treeLoading,
|
||
treeError,
|
||
onRetryTree,
|
||
peers,
|
||
peersLoading,
|
||
peersError,
|
||
onRetryPeers,
|
||
onSelectPeer,
|
||
canManage,
|
||
}: EditorContentProps): ReactNode {
|
||
const navigate = useNavigate();
|
||
const editor = useRoleEditor(detail);
|
||
const updateRole = useUpdateRoleMutation();
|
||
const locked = detail.isSystem;
|
||
const readonly = !canManage;
|
||
const [activeTab, setActiveTab] = useState<RoleEditorTabKey>('basic');
|
||
const [errors, setErrors] = useState<RoleDraftErrors>({});
|
||
const draftFingerprint = useMemo(() => JSON.stringify(editor.draft), [editor.draft]);
|
||
const [serverState, setServerState] = useState<{
|
||
fingerprint: string;
|
||
conflicts: readonly SodConflict[];
|
||
saveError: string | null;
|
||
} | null>(null);
|
||
const serverConflicts = useMemo<readonly SodConflict[]>(
|
||
() =>
|
||
serverState !== null && serverState.fingerprint === draftFingerprint
|
||
? serverState.conflicts
|
||
: [],
|
||
[serverState, draftFingerprint],
|
||
);
|
||
const saveError =
|
||
serverState !== null && serverState.fingerprint === draftFingerprint
|
||
? serverState.saveError
|
||
: null;
|
||
const [saveOpen, setSaveOpen] = useState(false);
|
||
const [specialPrompt, setSpecialPrompt] = useState(false);
|
||
const [specialDialog, setSpecialDialog] = useState<{
|
||
permission: PermissionNode;
|
||
granted: boolean;
|
||
} | null>(null);
|
||
const [disableOpen, setDisableOpen] = useState(false);
|
||
const blocker = useBlocker(editor.isDirty);
|
||
|
||
const conflicts = useMemo(
|
||
() => (detail.isSystem ? [] : mergeConflicts(editor.conflicts, serverConflicts)),
|
||
[detail.isSystem, editor.conflicts, serverConflicts],
|
||
);
|
||
const meta = useMemo(() => (tree === undefined ? null : buildPermissionMetaMap(tree)), [tree]);
|
||
|
||
useEffect(() => {
|
||
if (!editor.isDirty) {
|
||
return undefined;
|
||
}
|
||
const handler = (event: BeforeUnloadEvent): void => {
|
||
event.preventDefault();
|
||
// eslint-disable-next-line @typescript-eslint/no-deprecated -- 浏览器离开提示仍依赖 returnValue
|
||
event.returnValue = '';
|
||
};
|
||
window.addEventListener('beforeunload', handler);
|
||
return () => {
|
||
window.removeEventListener('beforeunload', handler);
|
||
};
|
||
}, [editor.isDirty]);
|
||
|
||
const specialChangedCodes = useMemo(
|
||
() => [...editor.diff.addedSpecial, ...editor.diff.removedSpecial],
|
||
[editor.diff.addedSpecial, editor.diff.removedSpecial],
|
||
);
|
||
|
||
const attemptSave = (): void => {
|
||
if (readonly) {
|
||
return;
|
||
}
|
||
const validation = validateUpdateDraft(detail, editor.draft);
|
||
setErrors(validation.errors);
|
||
if (validation.input === null) {
|
||
setActiveTab('basic');
|
||
notifyWarning(validation.errors.name ?? validation.errors.description ?? '请检查角色信息');
|
||
return;
|
||
}
|
||
if (!editor.isDirty) {
|
||
notifyWarning('未做修改');
|
||
return;
|
||
}
|
||
if (editor.diff.hasSpecialChanges) {
|
||
setSpecialPrompt(true);
|
||
return;
|
||
}
|
||
setSaveOpen(true);
|
||
};
|
||
|
||
const doSave = (): void => {
|
||
const validation = validateUpdateDraft(detail, editor.draft);
|
||
if (validation.input === null) {
|
||
setErrors(validation.errors);
|
||
setSaveOpen(false);
|
||
setActiveTab('basic');
|
||
notifyWarning(validation.errors.name ?? '请检查角色信息');
|
||
return;
|
||
}
|
||
updateRole.mutate(
|
||
{ roleId: detail.id, input: validation.input },
|
||
{
|
||
onSuccess: (result) => {
|
||
setSaveOpen(false);
|
||
setSpecialPrompt(false);
|
||
setErrors({});
|
||
editor.markSaved();
|
||
if (detail.isSystem) {
|
||
notifySuccess('角色名称已更新');
|
||
} else {
|
||
notifySuccess(saveResultMessage(result));
|
||
}
|
||
if (result.warnings.length > 0) {
|
||
notifyWarning(result.warnings.join(';'));
|
||
}
|
||
},
|
||
onError: (error) => {
|
||
setSaveOpen(false);
|
||
setSpecialPrompt(false);
|
||
if (error instanceof ApiError && error.code === 40304) {
|
||
setServerState({
|
||
fingerprint: draftFingerprint,
|
||
conflicts: conflictsFromErrorDetails(error.details),
|
||
saveError: '保存时服务端检测到职责互斥,请调整冲突权限点后重试。',
|
||
});
|
||
setActiveTab('permissions');
|
||
return;
|
||
}
|
||
if (error instanceof ApiError) {
|
||
setServerState({
|
||
fingerprint: draftFingerprint,
|
||
conflicts: [],
|
||
saveError: error.message,
|
||
});
|
||
}
|
||
},
|
||
},
|
||
);
|
||
};
|
||
|
||
const toggleSpecial = (code: string, granted: boolean): void => {
|
||
const permission = tree?.specialPermissions.find((item) => item.code === code);
|
||
if (permission === undefined) {
|
||
return;
|
||
}
|
||
setSpecialDialog({ permission, granted });
|
||
};
|
||
|
||
const headerTitle = editor.draft.name.trim() === '' ? detail.name : editor.draft.name;
|
||
const impactUsers = detail.userCount;
|
||
|
||
return (
|
||
<>
|
||
<header className="lx-roles__header">
|
||
<button
|
||
type="button"
|
||
className="lx-roles__back"
|
||
aria-label="返回角色列表"
|
||
onClick={() => {
|
||
void navigate(ROUTES.system.roles);
|
||
}}
|
||
>
|
||
<IconBack size={18} />
|
||
</button>
|
||
<div className="lx-roles__head-txt">
|
||
<div className="lx-roles__title-line">
|
||
<h1>{headerTitle}</h1>
|
||
<RoleStatusTag status={detail.status} />
|
||
{detail.isSystem ? (
|
||
<span className="lx-role-tag lx-role-tag--builtin">内置</span>
|
||
) : null}
|
||
{readonly ? <span className="lx-role-tag">只读</span> : null}
|
||
</div>
|
||
<p>调整角色权限、数据范围与特殊权限,保存后对绑定用户生效</p>
|
||
</div>
|
||
<div className="lx-roles__header-actions">
|
||
<Can permission={Permission.ROLE_MANAGE}>
|
||
<Button
|
||
icon={<IconCopy size={14} />}
|
||
onClick={() => {
|
||
void navigate(roleNewPath(detail.id));
|
||
}}
|
||
>
|
||
复制
|
||
</Button>
|
||
{detail.isSystem ? null : detail.status === 'active' ? (
|
||
<Button
|
||
type="danger"
|
||
icon={<IconPause size={14} />}
|
||
onClick={() => {
|
||
setDisableOpen(true);
|
||
}}
|
||
>
|
||
停用
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
icon={<IconPlay size={14} />}
|
||
onClick={() => {
|
||
updateRole.mutate(
|
||
{ roleId: detail.id, input: { status: 'enabled' } },
|
||
{
|
||
onSuccess: () => {
|
||
notifySuccess(`「${detail.name}」已启用`);
|
||
},
|
||
},
|
||
);
|
||
}}
|
||
>
|
||
启用
|
||
</Button>
|
||
)}
|
||
</Can>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="lx-roles__split">
|
||
<RoleListPanel
|
||
roles={peers}
|
||
totalCount={peers.length}
|
||
loading={peersLoading}
|
||
error={peersError}
|
||
selectedRoleId={detail.id}
|
||
note={`当前:${detail.name}`}
|
||
footerNote="切换后继续编辑"
|
||
canManage={canManage}
|
||
showRowMenu={false}
|
||
isFiltered={false}
|
||
onSelect={onSelectPeer}
|
||
onRowAction={() => {
|
||
// 编辑器内左侧列表仅用于切换角色,不提供行菜单
|
||
}}
|
||
onCreate={() => {
|
||
void navigate(ROUTES.system.roleNew);
|
||
}}
|
||
onResetFilters={() => {
|
||
// 编辑器内左侧列表不展示筛选
|
||
}}
|
||
onRetry={onRetryPeers}
|
||
/>
|
||
<section className="lx-role-editor" aria-label="角色编辑器">
|
||
<header className="lx-role-editor__head">
|
||
<span className="lx-role-editor__avatar" aria-hidden="true">
|
||
<IconShield size={20} />
|
||
</span>
|
||
<div className="lx-role-editor__ident">
|
||
<div className="lx-role-editor__name">
|
||
<b>{headerTitle}</b>
|
||
<RoleStatusTag status={detail.status} />
|
||
{detail.isSystem ? (
|
||
<span className="lx-role-tag lx-role-tag--builtin">内置</span>
|
||
) : null}
|
||
</div>
|
||
<div className="lx-role-editor__meta">
|
||
<code>{detail.code}</code>
|
||
<i>·</i>
|
||
<span>{detail.userCount} 名用户</span>
|
||
<i>·</i>
|
||
<span>最近更新 {formatRoleDateTime(detail.updatedAt)}</span>
|
||
<i>·</i>
|
||
<span>{scopeLabelOf(detail.dataScope.type)}</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="lx-role-editor__alert-wrap">
|
||
<EditorAlert
|
||
readonly={readonly}
|
||
locked={locked}
|
||
conflicts={conflicts}
|
||
saveError={saveError}
|
||
meta={meta}
|
||
/>
|
||
</div>
|
||
|
||
<RoleEditorTabs
|
||
detail={detail}
|
||
tree={tree}
|
||
treeLoading={treeLoading}
|
||
treeError={treeError}
|
||
onRetryTree={onRetryTree}
|
||
editor={editor}
|
||
locked={locked}
|
||
readonly={readonly}
|
||
activeTab={activeTab}
|
||
onChangeTab={setActiveTab}
|
||
errors={errors}
|
||
onSpecialRequest={toggleSpecial}
|
||
/>
|
||
|
||
<EditorFoot
|
||
readonly={readonly}
|
||
locked={locked}
|
||
conflicts={conflicts}
|
||
editor={editor}
|
||
saving={updateRole.isPending}
|
||
affectedUsers={detail.userCount}
|
||
onCancel={() => {
|
||
void navigate(ROUTES.system.roles);
|
||
}}
|
||
onSave={attemptSave}
|
||
/>
|
||
</section>
|
||
</div>
|
||
|
||
<DisableRoleDialog
|
||
key={`disable-${disableOpen ? detail.id : 'closed'}`}
|
||
visible={disableOpen}
|
||
role={detail}
|
||
loading={updateRole.isPending}
|
||
onCancel={() => {
|
||
setDisableOpen(false);
|
||
}}
|
||
onConfirm={(reason) => {
|
||
updateRole.mutate(
|
||
{
|
||
roleId: detail.id,
|
||
input: { status: 'disabled', ...(reason === '' ? {} : { reason }) },
|
||
},
|
||
{
|
||
onSuccess: () => {
|
||
setDisableOpen(false);
|
||
notifySuccess(`「${detail.name}」已停用,${impactUsers} 名用户下一次请求生效`);
|
||
},
|
||
},
|
||
);
|
||
}}
|
||
/>
|
||
|
||
<SpecialPermissionDialog
|
||
visible={specialDialog !== null}
|
||
permission={specialDialog?.permission ?? null}
|
||
granted={specialDialog?.granted ?? false}
|
||
holderCount={impactUsers}
|
||
onCancel={() => {
|
||
setSpecialDialog(null);
|
||
}}
|
||
onConfirm={() => {
|
||
if (specialDialog === null) {
|
||
return;
|
||
}
|
||
editor.setSpecialPermission(specialDialog.permission.code, specialDialog.granted);
|
||
setSpecialDialog(null);
|
||
notifySuccess(
|
||
specialDialog.granted
|
||
? `已设置「${specialDialog.permission.name}」,保存后生效`
|
||
: `已取消「${specialDialog.permission.name}」,保存后生效`,
|
||
);
|
||
}}
|
||
/>
|
||
|
||
{editor.diff.hasSpecialChanges ? (
|
||
<SpecialPermissionDialog
|
||
visible={specialPrompt}
|
||
permission={
|
||
tree?.specialPermissions.find((item) => item.code === specialChangedCodes[0]) ?? null
|
||
}
|
||
granted={editor.diff.addedSpecial.length > 0}
|
||
holderCount={impactUsers}
|
||
onCancel={() => {
|
||
setSpecialPrompt(false);
|
||
}}
|
||
onConfirm={() => {
|
||
setSpecialPrompt(false);
|
||
setSaveOpen(true);
|
||
}}
|
||
/>
|
||
) : null}
|
||
|
||
<SaveRoleDialog
|
||
visible={saveOpen}
|
||
role={detail}
|
||
diff={editor.diff}
|
||
meta={meta}
|
||
baseName={editor.base.name}
|
||
draftName={editor.draft.name}
|
||
baseScope={scopeLabelOf(editor.base.dataScope.type)}
|
||
draftScope={scopeLabelOf(editor.draft.dataScope.type)}
|
||
loading={updateRole.isPending}
|
||
onCancel={() => {
|
||
setSaveOpen(false);
|
||
}}
|
||
onConfirm={doSave}
|
||
/>
|
||
|
||
<LeaveRoleDialog
|
||
visible={blocker.state === 'blocked'}
|
||
roleName={detail.name}
|
||
onCancel={() => {
|
||
if (blocker.state === 'blocked') {
|
||
blocker.reset();
|
||
}
|
||
}}
|
||
onConfirm={() => {
|
||
if (blocker.state === 'blocked') {
|
||
blocker.proceed();
|
||
}
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function EditorLoading(): ReactNode {
|
||
return (
|
||
<div className="lx-roles__split">
|
||
<section className="lx-role-list lx-role-list--loading">
|
||
<PageSkeleton rows={9} />
|
||
</section>
|
||
<section className="lx-role-editor lx-role-editor--loading">
|
||
<PageSkeleton rows={10} />
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** 角色编辑器页(/system/roles/:id):左列表常驻高亮 + 右 Tab 编辑器 + 底部保存条 */
|
||
export function RoleEditorPage(): ReactNode {
|
||
const params = useParams<{ id: string }>();
|
||
const roleId = params.id ?? null;
|
||
const navigate = useNavigate();
|
||
const canManage = usePermission(Permission.ROLE_MANAGE);
|
||
const tree = usePermissionTreeQuery();
|
||
const peers = useRoleListQuery(PEER_ROLE_QUERY);
|
||
const detail = useRoleDetailQuery(roleId);
|
||
const selectPeer = (nextRoleId: string): void => {
|
||
if (nextRoleId === roleId) {
|
||
return;
|
||
}
|
||
void navigate(roleDetailPath(nextRoleId));
|
||
};
|
||
|
||
const peerRoles: readonly RoleListItem[] = useMemo(() => peers.data?.items ?? [], [peers.data]);
|
||
|
||
if (roleId === null) {
|
||
return <ErrorState title="角色不存在" message="链接缺少角色标识,请返回角色列表重新选择。" />;
|
||
}
|
||
if (detail.isPending) {
|
||
return (
|
||
<div className="lx-roles">
|
||
<EditorLoading />
|
||
</div>
|
||
);
|
||
}
|
||
if (!detail.isSuccess) {
|
||
return (
|
||
<div className="lx-roles">
|
||
<EditorLoading />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="lx-roles">
|
||
<EditorContent
|
||
key={detail.data.id}
|
||
detail={detail.data}
|
||
tree={tree.data}
|
||
treeLoading={tree.isPending}
|
||
treeError={tree.error}
|
||
onRetryTree={() => {
|
||
void tree.refetch();
|
||
}}
|
||
peers={peerRoles}
|
||
peersLoading={peers.isPending}
|
||
peersError={peers.error}
|
||
onRetryPeers={() => {
|
||
void peers.refetch();
|
||
}}
|
||
onSelectPeer={selectPeer}
|
||
canManage={canManage}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|