feat(frontend): add Roles management page and Permissions overview page

This commit is contained in:
2026-07-02 12:04:35 +08:00
parent a84a0ed153
commit 6a93967271
2 changed files with 283 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import { Card, Tag, Input, Space, Spin } from 'antd';
import api from '../../api';
interface PermissionItem {
id: number;
code: string;
name: string;
group: string;
description: string;
}
const PermissionsPage: React.FC = () => {
const [permTree, setPermTree] = useState<{ group: string; permissions: PermissionItem[] }[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
const groupNames: Record<string, string> = {
dashboard: '数据面板', student: '学生管理', room: '宿舍管理', occupancy: '入住管理',
expense: '费用管理', bill: '账单管理', deposit: '押金管理',
classroom: '教室管理', tenant: '租赁方', rental: '租赁订单',
log: '操作日志', user: '用户管理', role: '角色管理',
};
useEffect(() => {
setLoading(true);
api.get('/rbac/permissions/tree')
.then((res: any) => setPermTree(res))
.catch(console.error)
.finally(() => setLoading(false));
}, []);
const filteredTree = search
? permTree.map(g => ({
...g,
permissions: g.permissions.filter(p =>
p.name.includes(search) || p.code.includes(search)
),
})).filter(g => g.permissions.length > 0)
: permTree;
if (loading) return <Spin style={{ display: 'block', margin: '40px auto' }} />;
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ margin: 0 }}></h2>
<Input.Search
placeholder="搜索权限名称或编码"
allowClear
style={{ width: 280 }}
onSearch={setSearch}
onChange={e => !e.target.value && setSearch('')}
/>
</div>
<Space direction="vertical" style={{ width: '100%' }} size={16}>
{filteredTree.map(group => (
<Card
key={group.group}
title={<span style={{ fontWeight: 600 }}>{groupNames[group.group] || group.group} ({group.permissions.length})</span>}
size="small"
>
<Space wrap>
{group.permissions.map(p => (
<Tag key={p.id} color="blue" style={{ marginBottom: 8 }}>
{p.name} <Tag color="geekblue" style={{ marginLeft: 4 }}>{p.code}</Tag>
</Tag>
))}
</Space>
</Card>
))}
</Space>
</div>
);
};
export default PermissionsPage;

View File

@@ -0,0 +1,206 @@
import React, { useEffect, useState } from 'react';
import { Table, Button, Modal, Form, Input, Space, Tag, Popconfirm, message, Card, Checkbox } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
interface PermissionItem {
id: number;
code: string;
name: string;
group: string;
}
interface RoleItem {
id: number;
name: string;
description: string;
isSystem: boolean;
status: number;
permissions: PermissionItem[];
}
const RolesPage: React.FC = () => {
const [data, setData] = useState<RoleItem[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<RoleItem | null>(null);
const [allPerms, setAllPerms] = useState<{ group: string; permissions: PermissionItem[] }[]>([]);
const [form] = Form.useForm();
const [selectedPermIds, setSelectedPermIds] = useState<number[]>([]);
const fetchData = async () => {
setLoading(true);
try {
const [roles, permTree] = await Promise.all([
api.get('/rbac/roles') as Promise<RoleItem[]>,
api.get('/rbac/permissions/tree') as Promise<{ group: string; permissions: PermissionItem[] }[]>,
]);
setData(roles);
setAllPerms(permTree);
} catch (e) { console.error(e); }
setLoading(false);
};
useEffect(() => { fetchData(); }, []);
const handleAdd = () => {
setEditing(null);
form.resetFields();
setSelectedPermIds([]);
setModalOpen(true);
};
const handleEdit = (record: RoleItem) => {
setEditing(record);
form.setFieldsValue({ name: record.name, description: record.description });
setSelectedPermIds(record.permissions.map(p => p.id));
setModalOpen(true);
};
const handleSubmit = async () => {
const values = await form.validateFields();
try {
if (editing) {
await api.put(`/rbac/roles/${editing.id}`, { name: values.name, description: values.description, permissionIds: selectedPermIds });
message.success('角色更新成功');
} else {
await api.post('/rbac/roles', { name: values.name, description: values.description, permissionIds: selectedPermIds });
message.success('角色创建成功');
}
setModalOpen(false);
fetchData();
} catch (e: any) { message.error(e.message || '操作失败'); }
};
const handleDelete = async (id: number) => {
try {
await api.delete(`/rbac/roles/${id}`);
message.success('角色已删除');
fetchData();
} catch (e: any) { message.error(e.message || '删除失败'); }
};
const groupNames: Record<string, string> = {
dashboard: '数据面板', student: '学生管理', room: '宿舍管理', occupancy: '入住管理',
expense: '费用管理', bill: '账单管理', deposit: '押金管理',
classroom: '教室管理', tenant: '租赁方', rental: '租赁订单',
log: '操作日志', user: '用户管理', role: '角色管理',
};
const columns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '名称', dataIndex: 'name', width: 120 },
{ title: '描述', dataIndex: 'description', width: 200, ellipsis: true },
{
title: '权限标签', dataIndex: 'permissions', width: 150, ellipsis: true,
render: (perms: PermissionItem[]) => perms?.length > 0
? <Tag color="blue">{perms.length} </Tag>
: <Tag color="default"></Tag>,
},
{
title: '系统', dataIndex: 'isSystem', width: 70,
render: (v: boolean) => v ? <Tag color="orange"></Tag> : null,
},
{
title: '操作', width: 160, fixed: 'right' as const,
render: (_: any, record: RoleItem) => (
<Space>
<PermissionButton permission="role:edit" type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
</PermissionButton>
{!record.isSystem && (
<PermissionButton permission="role:delete">
<Popconfirm title="确认删除该角色?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" size="small" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</PermissionButton>
)}
</Space>
),
},
];
const handleGroupCheckAll = (group: string, checked: boolean) => {
const groupPermIds = allPerms.find(g => g.group === group)?.permissions.map(p => p.id) || [];
if (checked) {
setSelectedPermIds(prev => [...new Set([...prev, ...groupPermIds])]);
} else {
setSelectedPermIds(prev => prev.filter(id => !groupPermIds.includes(id)));
}
};
const isGroupAllChecked = (group: string) => {
const groupPermIds = allPerms.find(g => g.group === group)?.permissions.map(p => p.id) || [];
return groupPermIds.length > 0 && groupPermIds.every(id => selectedPermIds.includes(id));
};
const isGroupIndeterminate = (group: string) => {
const groupPermIds = allPerms.find(g => g.group === group)?.permissions.map(p => p.id) || [];
const checkedCount = groupPermIds.filter(id => selectedPermIds.includes(id)).length;
return checkedCount > 0 && checkedCount < groupPermIds.length;
};
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 style={{ margin: 0 }}></h2>
<PermissionButton permission="role:create" type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</PermissionButton>
</div>
<Table columns={columns} dataSource={data} rowKey="id" loading={loading} scroll={{ x: 800 }} pagination={false} />
<Modal
title={editing ? '编辑角色' : '新增角色'}
open={modalOpen}
onOk={handleSubmit}
onCancel={() => setModalOpen(false)}
width={700}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="角色名称" rules={[{ required: true, message: '请输入角色名称' }]}>
<Input disabled={editing?.isSystem} />
</Form.Item>
<Form.Item name="description" label="角色描述">
<Input.TextArea rows={2} />
</Form.Item>
<Form.Item label="权限分配">
<div style={{ maxHeight: 400, overflow: 'auto' }}>
{allPerms.map(group => (
<Card
key={group.group}
size="small"
title={
<Checkbox
checked={isGroupAllChecked(group.group)}
indeterminate={isGroupIndeterminate(group.group)}
onChange={e => handleGroupCheckAll(group.group, e.target.checked)}
>
{groupNames[group.group] || group.group}
</Checkbox>
}
style={{ marginBottom: 8 }}
>
<Checkbox.Group
value={selectedPermIds}
onChange={vals => setSelectedPermIds(vals as number[])}
>
<Space wrap>
{group.permissions.map(p => (
<Checkbox key={p.id} value={p.id}>{p.name}</Checkbox>
))}
</Space>
</Checkbox.Group>
</Card>
))}
</div>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default RolesPage;