Files
hms/apps/web/src/pages/Roles.tsx
iven d436888ca5
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
refactor(web): 系统设置模块页面表单一致性重构
- 新增 useCrudDrawer hook 封装 CRUD Drawer 通用模式(状态管理/提交/错误处理)
- 新增 useListData hook 封装非分页列表数据获取
- 11 个页面统一迁移到 DrawerForm + 共享 hooks,消除重复代码
- 错误处理统一使用 useApiRequest.execute(),移除内联 try-catch
- Modal 全部替换为 DrawerForm,保持 UI 一致性
- 净减少 ~1300 行代码(858 增 / 2136 删)
2026-05-04 11:57:38 +08:00

214 lines
7.6 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Form,
Input,
Tag,
Popconfirm,
Checkbox,
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import {
listRoles,
createRole,
updateRole,
deleteRole,
assignPermissions,
getRolePermissions,
listPermissions,
type RoleInfo,
type PermissionInfo,
} from '../api/roles';
import { PageContainer } from '../components/PageContainer';
import { DrawerForm } from '../components/DrawerForm';
import { useCrudDrawer } from '../hooks/useCrudDrawer';
import { useApiRequest } from '../hooks/useApiRequest';
import { useThemeMode } from '../hooks/useThemeMode';
import { useListData } from '../hooks/useListData';
export default function Roles() {
const isDark = useThemeMode();
const { execute } = useApiRequest();
const { data: roles, loading, refresh } = useListData<RoleInfo>(async () => {
const result = await listRoles();
return result.data;
});
const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
const [permDrawerOpen, setPermDrawerOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]);
useEffect(() => {
listPermissions().then(setPermissions).catch(() => {});
}, []);
const roleDrawer = useCrudDrawer<RoleInfo>({
getId: (r) => r.id,
onCreate: async (values) => {
await createRole(values as unknown as { name: string; code: string; description?: string });
},
onUpdate: async (id, values) => {
await updateRole(id, values as unknown as { name: string; code: string; description?: string; version: number });
},
onSuccess: refresh,
});
const handleDelete = async (id: string) => {
await execute(() => deleteRole(id), '角色已删除');
refresh();
};
const openPermDrawer = async (role: RoleInfo) => {
setSelectedRole(role);
try {
const rolePerms = await getRolePermissions(role.id);
setSelectedPermIds(rolePerms.map((p) => p.id));
} catch {
setSelectedPermIds([]);
}
setPermDrawerOpen(true);
};
const savePermissions = async () => {
if (!selectedRole) return;
await execute(() => assignPermissions(selectedRole.id, selectedPermIds), '权限分配成功');
setPermDrawerOpen(false);
};
const columns = [
{
title: '角色名称', dataIndex: 'name', key: 'name',
render: (v: string, record: RoleInfo) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: record.is_system ? 'linear-gradient(135deg, #2563eb, #60a5fa)' : isDark ? '#0f172a' : '#f8fafc',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: record.is_system ? '#fff' : isDark ? '#94a3b8' : '#475569', fontSize: 14,
}}>
<SafetyCertificateOutlined />
</div>
<span style={{ fontWeight: 500 }}>{v}</span>
</div>
),
},
{
title: '编码', dataIndex: 'code', key: 'code',
render: (v: string) => <Tag style={{ background: isDark ? '#0f172a' : '#f8fafc', border: 'none', color: isDark ? '#94a3b8' : '#475569', fontFamily: 'monospace', fontSize: 12 }}>{v}</Tag>,
},
{
title: '描述', dataIndex: 'description', key: 'description', ellipsis: true,
render: (v?: string) => <span style={{ color: isDark ? '#475569' : '#94a3b8' }}>{v || '-'}</span>,
},
{
title: '类型', dataIndex: 'is_system', key: 'is_system', width: 100,
render: (v: boolean) => (
<Tag style={{ color: v ? '#2563eb' : isDark ? '#94a3b8' : '#475569', background: v ? '#eff6ff' : isDark ? '#0f172a' : '#f8fafc', border: 'none', fontWeight: 500 }}>
{v ? '系统' : '自定义'}
</Tag>
),
},
{
title: '操作', key: 'actions', width: 180,
render: (_: unknown, record: RoleInfo) => (
<Space size={4}>
<Button size="small" type="text" icon={<SafetyCertificateOutlined />} onClick={() => openPermDrawer(record)} style={{ color: '#2563eb' }}></Button>
{!record.is_system && (
<>
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => roleDrawer.openEdit(record, (r) => ({
name: r.name, code: r.code, description: r.description,
}))} style={{ color: isDark ? '#94a3b8' : '#475569' }} />
<Popconfirm title="确定删除此角色?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
</Popconfirm>
</>
)}
</Space>
),
},
];
const groupedPermissions = permissions.reduce<Record<string, PermissionInfo[]>>((acc, p) => {
if (!acc[p.resource]) acc[p.resource] = [];
acc[p.resource].push(p);
return acc;
}, {});
return (
<PageContainer
title="角色管理"
subtitle="管理系统角色和权限分配"
actions={<Button type="primary" icon={<PlusOutlined />} onClick={() => roleDrawer.openCreate()}></Button>}
>
<Table
columns={columns}
dataSource={roles}
rowKey="id"
loading={loading}
pagination={{ pageSize: 20, showTotal: (t) => `${t} 条记录` }}
/>
{/* 新建/编辑角色 Drawer */}
<DrawerForm
title={roleDrawer.editingRecord ? '编辑角色' : '新建角色'}
open={roleDrawer.open}
onClose={roleDrawer.close}
onSubmit={roleDrawer.handleSubmit}
initialValues={roleDrawer.initialValues}
loading={roleDrawer.loading}
width={480}
columns={1}
>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入角色名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="编码" rules={[{ required: true, message: '请输入角色编码' }]}>
<Input disabled={!!roleDrawer.editingRecord} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
</DrawerForm>
{/* 权限分配 Drawer */}
<DrawerForm
title={`权限分配 - ${selectedRole?.name || ''}`}
open={permDrawerOpen}
onClose={() => setPermDrawerOpen(false)}
onSubmit={savePermissions}
initialValues={{}}
loading={false}
width={600}
columns={1}
>
<div style={{ marginTop: 8 }}>
{Object.entries(groupedPermissions).map(([resource, perms]) => (
<div key={resource} style={{
marginBottom: 16, padding: 16, borderRadius: 10,
border: `1px solid ${isDark ? '#0f172a' : '#E2E8F0'}`,
background: isDark ? '#0B0F1A' : '#f1f5f9',
}}>
<div style={{ fontWeight: 600, marginBottom: 12, textTransform: 'capitalize', color: isDark ? '#E2E8F0' : '#334155', fontSize: 14 }}>
{resource}
</div>
<Checkbox.Group
value={selectedPermIds}
onChange={(values) => setSelectedPermIds(values as string[])}
style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}
>
{perms.map((p) => (
<Checkbox key={p.id} value={p.id} style={{ marginRight: 0 }}>{p.name}</Checkbox>
))}
</Checkbox.Group>
</div>
))}
</div>
</DrawerForm>
</PageContainer>
);
}