背景:列表页页头「权限点目录」与详情面板「查看权限点目录」和编辑器权限点 Tab 信息重复,抽屉只读、无搜索、无操作,且页头出现两个并列动作。 改动: - 移除列表页页头目录按钮与 PermissionCatalogDrawer 组件及对应样式; - 详情面板删除「查看权限点目录」入口,保留「已选 n / 30」信息; - 权限查看统一走编辑器「权限点」Tab(只读用户也可进入查看); - 同步 A2 实施报告与设计说明。 验证:pnpm format:check / lint / typecheck / test 全绿;5173 实测页头仅保留「新建角色」,详情面板无目录入口。
This commit is contained in:
1 parent
fe70a2bcee
commit
a3419cf5da
6 files changed
+3
-263
No files matched your search
@@ -1,97 +0,0 @@
|
|||||||
/* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -125,7 +125,6 @@ function renderPanel(overrides: Partial<Parameters<typeof RoleDetailPanel>[0]> =
|
|||||||
onRename={vi.fn()}
|
onRename={vi.fn()}
|
||||||
onDisable={vi.fn()}
|
onDisable={vi.fn()}
|
||||||
onEnable={vi.fn()}
|
onEnable={vi.fn()}
|
||||||
onOpenCatalog={vi.fn()}
|
|
||||||
{...overrides}
|
{...overrides}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export interface RoleDetailPanelProps {
|
|||||||
onRename: (role: RoleDetail) => void;
|
onRename: (role: RoleDetail) => void;
|
||||||
onDisable: (role: RoleDetail) => void;
|
onDisable: (role: RoleDetail) => void;
|
||||||
onEnable: (role: RoleDetail) => void;
|
onEnable: (role: RoleDetail) => void;
|
||||||
onOpenCatalog: (role: RoleDetail) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RoleAvatar({ status }: { status: 'active' | 'disabled' }): ReactNode {
|
function RoleAvatar({ status }: { status: 'active' | 'disabled' }): ReactNode {
|
||||||
@@ -114,11 +113,9 @@ function DetailHead({
|
|||||||
function PermissionSummary({
|
function PermissionSummary({
|
||||||
role,
|
role,
|
||||||
tree,
|
tree,
|
||||||
onOpenCatalog,
|
|
||||||
}: {
|
}: {
|
||||||
role: RoleDetail;
|
role: RoleDetail;
|
||||||
tree: PermissionTree | undefined;
|
tree: PermissionTree | undefined;
|
||||||
onOpenCatalog: () => void;
|
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const selected = [...role.permissions, ...role.specialPermissions];
|
const selected = [...role.permissions, ...role.specialPermissions];
|
||||||
const modules = tree === undefined ? [] : summarizePermissionModules(tree, selected);
|
const modules = tree === undefined ? [] : summarizePermissionModules(tree, selected);
|
||||||
@@ -130,9 +127,6 @@ function PermissionSummary({
|
|||||||
已选 {role.permissionCount + role.specialPermissionCount} /{' '}
|
已选 {role.permissionCount + role.specialPermissionCount} /{' '}
|
||||||
{tree?.totalPermissions ?? '—'}
|
{tree?.totalPermissions ?? '—'}
|
||||||
</span>
|
</span>
|
||||||
<Button theme="borderless" size="small" onClick={onOpenCatalog}>
|
|
||||||
查看权限点目录
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{modules.length === 0 ? (
|
{modules.length === 0 ? (
|
||||||
<p className="lx-role-detail__muted">权限点目录加载中…</p>
|
<p className="lx-role-detail__muted">权限点目录加载中…</p>
|
||||||
@@ -326,13 +320,7 @@ export function RoleDetailPanel(props: RoleDetailPanelProps): ReactNode {
|
|||||||
{formatRoleDateTime(detail.updatedAt)}
|
{formatRoleDateTime(detail.updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<PermissionSummary
|
<PermissionSummary role={detail} tree={props.tree} />
|
||||||
role={detail}
|
|
||||||
tree={props.tree}
|
|
||||||
onOpenCatalog={() => {
|
|
||||||
props.onOpenCatalog(detail);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ScopeAndSpecial role={detail} tree={props.tree} />
|
<ScopeAndSpecial role={detail} tree={props.tree} />
|
||||||
<BoundUsers role={detail} />
|
<BoundUsers role={detail} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Button, Input, Switch } from '@douyinfe/semi-ui-19';
|
import { Button, Input, Switch } from '@douyinfe/semi-ui-19';
|
||||||
import { Permission, type RoleDetail, type RoleListItem } from '@lingxi/shared';
|
import { Permission, type RoleDetail, type RoleListItem } from '@lingxi/shared';
|
||||||
import { notifySuccess } from '@lingxi/ui';
|
import { notifySuccess } from '@lingxi/ui';
|
||||||
import { IconAdd, IconSearch, IconShield } from '@lingxi/ui/icons';
|
import { IconAdd, IconSearch } from '@lingxi/ui/icons';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import { usePermission } from '@/hooks/use-permission';
|
import { usePermission } from '@/hooks/use-permission';
|
||||||
import { roleDetailPath, ROUTES } from '@/routes';
|
import { roleDetailPath, ROUTES } from '@/routes';
|
||||||
|
|
||||||
import { PermissionCatalogDrawer } from './PermissionCatalogDrawer';
|
|
||||||
import {
|
import {
|
||||||
CopyRoleDialog,
|
CopyRoleDialog,
|
||||||
DeleteRoleDialog,
|
DeleteRoleDialog,
|
||||||
@@ -129,7 +128,6 @@ export function RolesPage(): ReactNode {
|
|||||||
const [filters, setFilters] = useState<RoleListFilters>(DEFAULT_ROLE_LIST_FILTERS);
|
const [filters, setFilters] = useState<RoleListFilters>(DEFAULT_ROLE_LIST_FILTERS);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [dialog, setDialog] = useState<DialogState>(null);
|
const [dialog, setDialog] = useState<DialogState>(null);
|
||||||
const [catalogOpen, setCatalogOpen] = useState(false);
|
|
||||||
const filteredRoles = useMemo(() => filterRoles(roles, filters), [roles, filters]);
|
const filteredRoles = useMemo(() => filterRoles(roles, filters), [roles, filters]);
|
||||||
const stats = useMemo(() => summarizeRoles(roles), [roles]);
|
const stats = useMemo(() => summarizeRoles(roles), [roles]);
|
||||||
const effectiveSelectedId = filteredRoles.some((role) => role.id === selectedId)
|
const effectiveSelectedId = filteredRoles.some((role) => role.id === selectedId)
|
||||||
@@ -176,14 +174,6 @@ export function RolesPage(): ReactNode {
|
|||||||
<p>配置功能权限、数据范围与特殊权限,变更即时生效并留痕</p>
|
<p>配置功能权限、数据范围与特殊权限,变更即时生效并留痕</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="lx-roles__header-actions">
|
<div className="lx-roles__header-actions">
|
||||||
<Button
|
|
||||||
icon={<IconShield size={14} />}
|
|
||||||
onClick={() => {
|
|
||||||
setCatalogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
权限点目录
|
|
||||||
</Button>
|
|
||||||
<Can permission={Permission.ROLE_MANAGE}>
|
<Can permission={Permission.ROLE_MANAGE}>
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme="solid"
|
||||||
@@ -265,9 +255,6 @@ export function RolesPage(): ReactNode {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onOpenCatalog={() => {
|
|
||||||
setCatalogOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -383,14 +370,6 @@ export function RolesPage(): ReactNode {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PermissionCatalogDrawer
|
|
||||||
visible={catalogOpen}
|
|
||||||
tree={tree.data}
|
|
||||||
role={selectedDetail ?? null}
|
|
||||||
onClose={() => {
|
|
||||||
setCatalogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1122,135 +1122,6 @@
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 权限点目录抽屉 ---------- */
|
|
||||||
|
|
||||||
.lx-role-catalog {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--lx-space-3);
|
|
||||||
padding-bottom: var(--lx-space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__sub {
|
|
||||||
font-size: var(--lx-font-size-xs);
|
|
||||||
line-height: 18px;
|
|
||||||
color: var(--lx-color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__module {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid var(--lx-color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__module:last-of-type {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__module-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px 0 4px;
|
|
||||||
font-size: var(--lx-font-size-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--lx-color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__module-head span {
|
|
||||||
font-size: var(--lx-font-size-xs);
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--lx-color-text-tertiary);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--lx-space-2);
|
|
||||||
padding: 7px 0;
|
|
||||||
border-bottom: 1px solid var(--lx-color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__row:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__check {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
flex: 0 0 15px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 2px;
|
|
||||||
border: 1px solid var(--lx-color-border-strong);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: var(--lx-color-on-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__row.is-granted .lx-role-catalog__check {
|
|
||||||
border-color: var(--lx-color-brand);
|
|
||||||
background: var(--lx-color-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__copy {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: var(--lx-font-size-sm);
|
|
||||||
color: var(--lx-color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__desc {
|
|
||||||
font-size: var(--lx-font-size-xs);
|
|
||||||
color: var(--lx-color-text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__code {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding-top: 2px;
|
|
||||||
font-family: var(--lx-font-mono);
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--lx-color-text-tertiary);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__note {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--lx-space-2);
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: var(--lx-radius-md);
|
|
||||||
background: var(--lx-color-bg-subtle);
|
|
||||||
font-size: var(--lx-font-size-xs);
|
|
||||||
line-height: 18px;
|
|
||||||
color: var(--lx-color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__foot {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: var(--lx-font-size-xs);
|
|
||||||
color: var(--lx-color-text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lx-role-catalog__empty {
|
|
||||||
padding: var(--lx-space-5);
|
|
||||||
font-size: var(--lx-font-size-xs);
|
|
||||||
color: var(--lx-color-text-tertiary);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 响应式 ---------- */
|
/* ---------- 响应式 ---------- */
|
||||||
|
|
||||||
@media (width <= 1280px) {
|
@media (width <= 1280px) {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ H1 权限门口径(用户确认):权限点保留现有 `read/write` 编码
|
|||||||
|
|
||||||
- 路由:`/system/roles`(`role:view`,左 400 角色列表 + 右详情)、`/system/roles/:id`(编辑器,Tab:基本信息/权限点/数据范围/特殊权限 + 底部唯一保存)、`/system/roles/new`(`role:manage`,左模板 + 右表单);导航 `/system/roles` 权限由 `ROLE_MANAGE` 调整为 `ROLE_VIEW`;面包屑补角色三页。
|
- 路由:`/system/roles`(`role:view`,左 400 角色列表 + 右详情)、`/system/roles/:id`(编辑器,Tab:基本信息/权限点/数据范围/特殊权限 + 底部唯一保存)、`/system/roles/new`(`role:manage`,左模板 + 右表单);导航 `/system/roles` 权限由 `ROLE_MANAGE` 调整为 `ROLE_VIEW`;面包屑补角色三页。
|
||||||
- API/Query:`api/roles.ts`、`hooks/queries/use-roles.ts`、`query-keys`、`endpoints`;全部用 shared Zod schema 解析。
|
- API/Query:`api/roles.ts`、`hooks/queries/use-roles.ts`、`query-keys`、`endpoints`;全部用 shared Zod schema 解析。
|
||||||
- 页面能力:权限树 10 模块/30 点三态 + 搜索 + 展开;特殊权限 6 项二次确认;数据范围六类单选 + 客户组空态;模板预填(特殊不带入);复制/停用/删除/系统角色改名;保存前影响确认;SoD 实时冲突与 40304 回填;未保存离开拦截;权限点目录抽屉;只读态;加载/空/错误态;1024 适配。
|
- 页面能力:权限树 10 模块/30 点三态 + 搜索 + 展开;特殊权限 6 项二次确认;数据范围六类单选 + 客户组空态;模板预填(特殊不带入);复制/停用/删除/系统角色改名;保存前影响确认;SoD 实时冲突与 40304 回填;未保存离开拦截;只读态;加载/空/错误态;1024 适配。
|
||||||
- 样式:`styles/roles.css`、`styles/role-editor.css`,复用 `--lx-*` token 与现有 `.lx-*` 组件类;未新增依赖、未内联 SVG。
|
- 样式:`styles/roles.css`、`styles/role-editor.css`,复用 `--lx-*` token 与现有 `.lx-*` 组件类;未新增依赖、未内联 SVG。
|
||||||
|
|
||||||
### 2.5 UI 原型与决策
|
### 2.5 UI 原型与决策
|
||||||
|
|||||||
Reference in new issue
Block a user