Files
gongxue-base/apps/admin/src/pages/Roles/index.tsx

361 lines
11 KiB
TypeScript

import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Table, Modal, Form, Input, Space, Tag, Popconfirm, Card, Checkbox, Empty } from 'antd';
import { PlusOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
import EditableCell from '../../components/EditableCell';
import { message } from '../../ui/app-message';
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 [saving, setSaving] = useState(false);
const fetchData = useCallback(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: unknown) {
const err = e as { message?: string };
message.error(err?.message || '加载失败,请稍后重试');
}
setLoading(false);
}, []);
useEffect(() => {
fetchData();
}, [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 () => {
setSaving(true);
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 || '操作失败');
} finally {
setSaving(false);
}
};
const handleDisable = 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: '数据面板',
notification: '通知中心',
student: '学生管理',
'student-scope': '学生范围权限',
teacher: '教师管理',
'teacher-workspace': '教师工作台',
room: '宿舍管理',
occupancy: '入住管理',
expense: '费用管理',
bill: '账单管理',
deposit: '押金管理',
wallet: '学生余额',
classroom: '教室管理',
organization: '机构管理',
rental: '租赁订单',
class: '班级管理',
schedule: '排课管理',
attendance: '考勤管理',
'attendance-scope': '考勤范围权限',
log: '操作日志',
user: '用户管理',
role: '角色管理',
sync: '数据同步',
integration: '集成配置',
ai: 'AI 配置',
};
const permissionOptions = useMemo(
() =>
allPerms
.flatMap((group) => group.permissions)
.map((item) => ({ value: item.id, label: `${item.group} · ${item.name}` })),
[allPerms],
);
const saveCell = useCallback(
async (record: RoleItem, field: string, value: unknown) => {
await api.put(`/rbac/roles/${record.id}`, { [field]: value });
message.success('已保存');
await fetchData();
},
[fetchData],
);
const columns = useMemo(
() => [
{ title: 'ID', dataIndex: 'id', width: 80 },
{
title: '名称',
dataIndex: 'name',
width: 120,
render: (v: string, r: RoleItem) => (
<EditableCell
value={v}
required
permission="role:edit"
onSave={(next) => saveCell(r, 'name', next)}
>
{v}
</EditableCell>
),
},
{
title: '描述',
dataIndex: 'description',
width: 200,
render: (v: string, r: RoleItem) => (
<EditableCell
value={v}
editor="textarea"
permission="role:edit"
onSave={(next) => saveCell(r, 'description', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '权限标签',
dataIndex: 'permissions',
width: 150,
render: (perms: PermissionItem[], record: RoleItem) => (
<EditableCell
value={perms?.map((item) => item.id) || []}
editor="multi-select"
options={permissionOptions}
permission="role:edit"
onSave={(next) => saveCell(record, 'permissionIds', next)}
>
{perms?.length > 0 ? (
<Tag color="blue">{perms.length} </Tag>
) : (
<Tag color="default"></Tag>
)}
</EditableCell>
),
},
{
title: '系统',
dataIndex: 'isSystem',
width: 80,
render: (v: boolean) => (v ? <Tag color="orange"></Tag> : null),
},
{
title: '操作',
width: 180,
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 && (
<Popconfirm title="确认停用该角色?" onConfirm={() => handleDisable(record.id)}>
<PermissionButton
permission="role:delete"
type="link"
size="small"
icon={<StopOutlined />}
>
</PermissionButton>
</Popconfirm>
)}
</Space>
),
},
],
[permissionOptions, saveCell],
);
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',
flexWrap: 'wrap',
gap: 8,
}}
>
<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}
locale={{ emptyText: <Empty description="暂无数据" /> }}
scroll={{ x: 900 }}
pagination={false}
/>
<Modal
title={editing ? '编辑角色' : '新增角色'}
open={modalOpen}
onOk={handleSubmit}
onCancel={() => setModalOpen(false)}
width={700}
destroyOnHidden
confirmLoading={saving}
>
<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;