Files
lingxi/apps/web/src/pages/system/roles/RoleDetailPanel.tsx
T
Stev_Wang f4da1f3c02
ci / 质量门禁 (push) Canceled after 0s
refactor(web): 编辑角色主操作移入详情页头并移除页脚
背景:角色详情页把「编辑角色」放在右下角页脚,与页头的复制/停用分离,主操作位置突兀;页脚只剩一句备注也浪费垂直空间。

改动:
- 页头操作顺序调整为:复制 → 停用/启用(系统角色为改名)→ 编辑角色(实心主按钮);
- 只读用户显示「查看角色」,系统角色也显示「查看角色」(仅名称可改,避免编辑语义过强);
- 移除详情页脚及编辑按钮,权限摘要与绑定用户获得更多高度;
- 更新只读态单测断言。

验证:format:check / lint / typecheck / test 全绿;5173 实测页头操作与主按钮位置正确。
2026-11-03 14:16:39 +08:00

366 lines
11 KiB
TypeScript

/* eslint-disable max-lines-per-function -- 详情面板按头部/摘要/范围/特殊/绑定用户分区,集中便于对齐原型 */
import { Button } from '@douyinfe/semi-ui-19';
import type { PermissionTree, RoleDetail } from '@lingxi/shared';
import { ErrorState, PageEmpty, PageSkeleton } from '@lingxi/ui';
import {
IconCopy,
IconEdit,
IconPause,
IconPlay,
IconShield,
IconStar,
IconView,
} from '@lingxi/ui/icons';
import type { ReactNode } from 'react';
import { useState } from 'react';
import {
buildPermissionMetaMap,
formatRoleDateTime,
permissionNameOf,
scopeDescriptionOf,
scopeLabelOf,
summarizePermissionModules,
} from './role-display';
import { RoleStatusTag } from './RoleStatusTag';
export interface RoleDetailPanelProps {
roleId: string | null;
detail: RoleDetail | undefined;
tree: PermissionTree | undefined;
loading: boolean;
error: Error | null;
canManage: boolean;
onRetry: () => void;
onEdit: (roleId: string) => void;
onCopy: (role: RoleDetail) => void;
onRename: (role: RoleDetail) => void;
onDisable: (role: RoleDetail) => void;
onEnable: (role: RoleDetail) => void;
}
function RoleAvatar({ status }: { status: 'active' | 'disabled' }): ReactNode {
return (
<span
className={
status === 'active' ? 'lx-role-detail__avatar' : 'lx-role-detail__avatar is-disabled'
}
aria-hidden="true"
>
<IconShield size={22} />
</span>
);
}
function DetailHead({
role,
canManage,
onEdit,
onCopy,
onRename,
onDisable,
onEnable,
}: Pick<
RoleDetailPanelProps,
'canManage' | 'onEdit' | 'onCopy' | 'onRename' | 'onDisable' | 'onEnable'
> & {
role: RoleDetail;
}): ReactNode {
return (
<header className="lx-role-detail__head">
<RoleAvatar status={role.status} />
<div className="lx-role-detail__ident">
<div className="lx-role-detail__name">
<h2>{role.name}</h2>
<RoleStatusTag status={role.status} />
{role.isSystem ? <span className="lx-role-tag lx-role-tag--builtin">内置</span> : null}
</div>
<p className="lx-role-detail__desc">{role.description}</p>
</div>
<div className="lx-role-detail__actions">
{canManage ? (
<>
<Button
icon={<IconCopy size={14} />}
onClick={() => {
onCopy(role);
}}
>
复制
</Button>
{role.isSystem ? (
<Button
icon={<IconEdit size={14} />}
onClick={() => {
onRename(role);
}}
>
改名
</Button>
) : role.status === 'active' ? (
<Button
type="danger"
icon={<IconPause size={14} />}
onClick={() => {
onDisable(role);
}}
>
停用
</Button>
) : (
<Button
icon={<IconPlay size={14} />}
onClick={() => {
onEnable(role);
}}
>
启用
</Button>
)}
</>
) : null}
<Button
theme="solid"
type="primary"
icon={canManage && !role.isSystem ? <IconEdit size={14} /> : <IconView size={14} />}
onClick={() => {
onEdit(role.id);
}}
>
{canManage && !role.isSystem ? '编辑角色' : '查看角色'}
</Button>
</div>
</header>
);
}
function PermissionSummary({
role,
tree,
}: {
role: RoleDetail;
tree: PermissionTree | undefined;
}): ReactNode {
const selected = [...role.permissions, ...role.specialPermissions];
const modules = tree === undefined ? [] : summarizePermissionModules(tree, selected);
return (
<section className="lx-role-detail__section">
<div className="lx-role-detail__section-head">
<h3>权限点摘要</h3>
<span className="lx-role-detail__count">
已选 {role.permissionCount + role.specialPermissionCount} /{' '}
{tree?.totalPermissions ?? '—'}
</span>
</div>
{modules.length === 0 ? (
<p className="lx-role-detail__muted">权限点目录加载中…</p>
) : (
<div className="lx-role-detail__modules">
{modules.map((module) => (
<div
key={module.module}
className={
module.selected === 0 ? 'lx-role-detail__module is-zero' : 'lx-role-detail__module'
}
>
<span className="lx-role-detail__module-name">{module.moduleLabel}</span>
<span className="lx-role-detail__module-count">
<b>{module.selected}</b> / {module.total}
</span>
</div>
))}
</div>
)}
</section>
);
}
function ScopeAndSpecial({
role,
tree,
}: {
role: RoleDetail;
tree: PermissionTree | undefined;
}): ReactNode {
const meta = tree === undefined ? undefined : buildPermissionMetaMap(tree);
return (
<section className="lx-role-detail__section">
<div className="lx-role-detail__cols">
<div className="lx-role-detail__col">
<div className="lx-role-detail__section-head">
<h3>数据范围</h3>
</div>
<p className="lx-role-detail__scope">
<b>{scopeLabelOf(role.dataScope.type)}</b>
<span>{scopeDescriptionOf(role.dataScope.type)}</span>
</p>
{role.dataScope.type === 'customer_groups' ? (
<p className="lx-role-detail__muted">
{(role.dataScope.targetIds ?? []).length === 0
? '暂未指定客户组,客户组将在客户模块上线后可选择。'
: `已指定 ${String((role.dataScope.targetIds ?? []).length)} 个客户组,客户组名称将在客户模块上线后展示。`}
</p>
) : null}
</div>
<div className="lx-role-detail__col">
<div className="lx-role-detail__section-head">
<h3>特殊权限</h3>
<span className="lx-role-detail__count">{role.specialPermissions.length} / 6</span>
</div>
{role.specialPermissions.length === 0 ? (
<p className="lx-role-detail__muted">
未授予特殊权限,6 项均默认关闭,需单独授予并记录审计。
</p>
) : (
<div className="lx-role-detail__special-chips">
{role.specialPermissions.map((code) => (
<span key={code} className="lx-role-detail__special-chip" title={code}>
<IconStar size={12} />
{meta === undefined ? code : permissionNameOf(meta, code)}
</span>
))}
</div>
)}
</div>
</div>
</section>
);
}
function BoundUsers({ role }: { role: RoleDetail }): ReactNode {
const [expanded, setExpanded] = useState(false);
const shown = expanded ? role.boundUsers : role.boundUsers.slice(0, 3);
return (
<section className="lx-role-detail__section">
<div className="lx-role-detail__section-head">
<h3>绑定用户</h3>
{role.boundUsers.length > 3 ? (
<Button
theme="borderless"
size="small"
onClick={() => {
setExpanded((value) => !value);
}}
>
{expanded ? '收起' : `查看全部 ${String(role.boundUsers.length)} 名`}
</Button>
) : null}
</div>
{role.boundUsers.length === 0 ? (
<p className="lx-role-detail__muted">暂未绑定用户,可停用或删除该角色。</p>
) : (
<ul className="lx-role-detail__users">
{shown.map((user) => (
<li key={user.id} className="lx-role-detail__user">
<span
className={
user.status === 'disabled'
? 'lx-role-detail__user-avatar is-disabled'
: 'lx-role-detail__user-avatar'
}
>
{user.name.slice(0, 1)}
</span>
<span className="lx-role-detail__user-name">{user.name}</span>
<span className="lx-role-detail__user-account">{user.account}</span>
<span className="lx-role-detail__user-dept">{user.departmentName}</span>
<span
className={
user.status === 'active' ? 'lx-status-tag is-active' : 'lx-status-tag is-disabled'
}
>
{user.status === 'active' ? '在职' : '停用'}
</span>
</li>
))}
</ul>
)}
</section>
);
}
function DetailState({
roleId,
loading,
error,
onRetry,
}: Pick<RoleDetailPanelProps, 'roleId' | 'loading' | 'error' | 'onRetry'>): ReactNode {
if (roleId === null) {
return (
<PageEmpty
title="请选择一个角色"
description="左侧列表支持搜索与状态筛选,选中角色后在此查看权限详情。"
/>
);
}
if (loading) {
return (
<div className="lx-role-detail__state">
<PageSkeleton rows={8} />
</div>
);
}
if (error !== null) {
return (
<ErrorState
className="lx-role-detail__state"
title="角色详情加载失败"
message={error.message}
onRetry={onRetry}
/>
);
}
return null;
}
/** 右侧角色详情面板(只读):权限摘要、数据范围、特殊权限、绑定用户与编辑入口 */
export function RoleDetailPanel(props: RoleDetailPanelProps): ReactNode {
const ready = props.roleId !== null && props.detail !== undefined && props.error === null;
const detail = props.detail;
return (
<section className="lx-role-detail" aria-label="角色详情">
{ready && detail !== undefined ? (
<>
<DetailHead
role={detail}
canManage={props.canManage}
onEdit={props.onEdit}
onCopy={props.onCopy}
onRename={props.onRename}
onDisable={props.onDisable}
onEnable={props.onEnable}
/>
<div className="lx-role-detail__body">
<div className="lx-role-detail__meta">
<span>
<em>编码</em>
<code>{detail.code}</code>
</span>
<span>
<em>绑定用户</em>
{detail.userCount} 名
</span>
<span>
<em>最近更新</em>
{formatRoleDateTime(detail.updatedAt)}
</span>
</div>
<PermissionSummary role={detail} tree={props.tree} />
<ScopeAndSpecial role={detail} tree={props.tree} />
<BoundUsers role={detail} />
</div>
</>
) : (
<div className="lx-role-detail__empty">
<DetailState
roleId={props.roleId}
loading={props.loading}
error={props.error}
onRetry={props.onRetry}
/>
</div>
)}
</section>
);
}