|
@@ -0,0 +1,440 @@
|
|
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
|
|
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
+import { Card, Table, Button, Modal, Form, Input, Space, Tag, message, Tree, Spin } from 'antd';
|
|
|
|
|
+import { EditOutlined, DeleteOutlined, PlusOutlined, KeyOutlined } from '@ant-design/icons';
|
|
|
|
|
+import { roleClient, permissionClient, rolePermissionClient } from '@/client/api';
|
|
|
|
|
+import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
|
|
+import dayjs from 'dayjs';
|
|
|
|
|
+
|
|
|
|
|
+const { TextArea } = Input;
|
|
|
|
|
+
|
|
|
|
|
+type RoleListResponse = InferResponseType<typeof roleClient.$get, 200>;
|
|
|
|
|
+type RoleItem = RoleListResponse['data'][0];
|
|
|
|
|
+type CreateRoleRequest = InferRequestType<typeof roleClient.$post>['json'];
|
|
|
|
|
+type UpdateRoleRequest = InferRequestType<typeof roleClient[':id']['$put']>['json'];
|
|
|
|
|
+
|
|
|
|
|
+type PermissionListResponse = InferResponseType<typeof permissionClient.$get, 200>;
|
|
|
|
|
+type PermissionItem = PermissionListResponse['data'][0];
|
|
|
|
|
+
|
|
|
|
|
+// React Query hooks
|
|
|
|
|
+const useRoles = () => {
|
|
|
|
|
+ return useQuery({
|
|
|
|
|
+ queryKey: ['roles'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await roleClient.$get({ query: {} });
|
|
|
|
|
+ if (!response.ok) throw new Error('获取角色列表失败');
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ return data.data;
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const usePermissions = () => {
|
|
|
|
|
+ return useQuery({
|
|
|
|
|
+ queryKey: ['permissions'],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await permissionClient.$get({ query: {} });
|
|
|
|
|
+ if (!response.ok) throw new Error('获取权限列表失败');
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ return data.data;
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const useRolePermissions = (roleId: number | null) => {
|
|
|
|
|
+ return useQuery({
|
|
|
|
|
+ queryKey: ['role-permissions', roleId],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ if (!roleId) return [];
|
|
|
|
|
+ const response = await rolePermissionClient.$get({
|
|
|
|
|
+ query: { roleId: roleId as any }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) throw new Error('获取角色权限失败');
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ return data.data.map((item: any) => item.permissionId);
|
|
|
|
|
+ },
|
|
|
|
|
+ enabled: !!roleId,
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const useCreateRole = () => {
|
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
|
+ return useMutation({
|
|
|
|
|
+ mutationFn: async (data: CreateRoleRequest) => {
|
|
|
|
|
+ const response = await roleClient.$post({
|
|
|
|
|
+ json: { ...data, permissions: [] as string[] }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) throw new Error('创建角色失败');
|
|
|
|
|
+ return response.json();
|
|
|
|
|
+ },
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
|
|
|
|
|
+ message.success('创建角色成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error: Error) => {
|
|
|
|
|
+ message.error(error.message);
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const useUpdateRole = () => {
|
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
|
+ return useMutation({
|
|
|
|
|
+ mutationFn: async ({ id, data }: { id: number; data: UpdateRoleRequest }) => {
|
|
|
|
|
+ const response = await roleClient[':id'].$put({
|
|
|
|
|
+ param: { id: id.toString() },
|
|
|
|
|
+ json: data
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) throw new Error('更新角色失败');
|
|
|
|
|
+ return response.json();
|
|
|
|
|
+ },
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
|
|
|
|
|
+ message.success('更新角色成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error: Error) => {
|
|
|
|
|
+ message.error(error.message);
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const useDeleteRole = () => {
|
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
|
+ return useMutation({
|
|
|
|
|
+ mutationFn: async (id: number) => {
|
|
|
|
|
+ const response = await roleClient[':id'].$delete({
|
|
|
|
|
+ param: { id: id.toString() }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) throw new Error('删除角色失败');
|
|
|
|
|
+ return response.json();
|
|
|
|
|
+ },
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
|
|
|
|
|
+ message.success('删除角色成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error: Error) => {
|
|
|
|
|
+ message.error(error.message);
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const useUpdateRolePermissions = () => {
|
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
|
+ return useMutation({
|
|
|
|
|
+ mutationFn: async ({ roleId, permissionIds }: { roleId: number; permissionIds: number[] }) => {
|
|
|
|
|
+ const permissions = permissionIds.map(permissionId => ({
|
|
|
|
|
+ permissionId,
|
|
|
|
|
+ dataScopeType: 'COMPANY' as const,
|
|
|
|
|
+ customDepartments: [] as number[]
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ const response = await rolePermissionClient.batch.$post({
|
|
|
|
|
+ json: { roleId, permissions }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) throw new Error('更新角色权限失败');
|
|
|
|
|
+ return response.json();
|
|
|
|
|
+ },
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ message.success('角色权限更新成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error: Error) => {
|
|
|
|
|
+ message.error(error.message);
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const Roles: React.FC = () => {
|
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
|
+ const [modalVisible, setModalVisible] = useState(false);
|
|
|
|
|
+ const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
|
|
|
|
+ const [editingRole, setEditingRole] = useState<RoleItem | null>(null);
|
|
|
|
|
+ const [selectedRole, setSelectedRole] = useState<RoleItem | null>(null);
|
|
|
|
|
+ const [form] = Form.useForm();
|
|
|
|
|
+ const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+ // React Query hooks
|
|
|
|
|
+ const { data: roles = [], isLoading: rolesLoading } = useRoles();
|
|
|
|
|
+ const { data: permissions = [], isLoading: permissionsLoading } = usePermissions();
|
|
|
|
|
+ const { data: rolePermissions = [], isLoading: rolePermissionsLoading } = useRolePermissions(selectedRole?.id ?? null);
|
|
|
|
|
+
|
|
|
|
|
+ // Mutations
|
|
|
|
|
+ const createRoleMutation = useCreateRole();
|
|
|
|
|
+ const updateRoleMutation = useUpdateRole();
|
|
|
|
|
+ const deleteRoleMutation = useDeleteRole();
|
|
|
|
|
+ const updateRolePermissionsMutation = useUpdateRolePermissions();
|
|
|
|
|
+
|
|
|
|
|
+ // 打开编辑模态框
|
|
|
|
|
+ const handleEdit = (record: RoleItem) => {
|
|
|
|
|
+ setEditingRole(record);
|
|
|
|
|
+ form.setFieldsValue({
|
|
|
|
|
+ name: record.name,
|
|
|
|
|
+ description: record.description
|
|
|
|
|
+ });
|
|
|
|
|
+ setModalVisible(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 打开新建模态框
|
|
|
|
|
+ const handleAdd = () => {
|
|
|
|
|
+ setEditingRole(null);
|
|
|
|
|
+ form.resetFields();
|
|
|
|
|
+ setModalVisible(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 打开权限配置模态框
|
|
|
|
|
+ const handlePermissionConfig = (role: RoleItem) => {
|
|
|
|
|
+ setSelectedRole(role);
|
|
|
|
|
+ setSelectedPermissionIds(rolePermissions);
|
|
|
|
|
+ setPermissionModalVisible(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 保存角色
|
|
|
|
|
+ const handleSave = async (values: any) => {
|
|
|
|
|
+ if (editingRole) {
|
|
|
|
|
+ updateRoleMutation.mutate({ id: editingRole.id, data: values });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ createRoleMutation.mutate(values);
|
|
|
|
|
+ }
|
|
|
|
|
+ setModalVisible(false);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 删除角色
|
|
|
|
|
+ const handleDelete = (record: RoleItem) => {
|
|
|
|
|
+ Modal.confirm({
|
|
|
|
|
+ title: '确认删除',
|
|
|
|
|
+ content: `确定要删除角色 "${record.name}" 吗?`,
|
|
|
|
|
+ onOk: () => {
|
|
|
|
|
+ deleteRoleMutation.mutate(record.id);
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 保存角色权限
|
|
|
|
|
+ const handleSavePermissions = () => {
|
|
|
|
|
+ if (!selectedRole) return;
|
|
|
|
|
+
|
|
|
|
|
+ updateRolePermissionsMutation.mutate(
|
|
|
|
|
+ { roleId: selectedRole.id, permissionIds: selectedPermissionIds },
|
|
|
|
|
+ {
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ setPermissionModalVisible(false);
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 权限数据按模块分组
|
|
|
|
|
+ const permissionTreeData = React.useMemo(() => {
|
|
|
|
|
+ const grouped: Record<string, PermissionItem[]> = {};
|
|
|
|
|
+ permissions.forEach(permission => {
|
|
|
|
|
+ const module = permission.module || '其他';
|
|
|
|
|
+ if (!grouped[module]) {
|
|
|
|
|
+ grouped[module] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ grouped[module].push(permission);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return Object.entries(grouped).map(([module, items]) => ({
|
|
|
|
|
+ title: module,
|
|
|
|
|
+ key: module,
|
|
|
|
|
+ checkable: false,
|
|
|
|
|
+ children: items.map(item => ({
|
|
|
|
|
+ title: `${item.name} (${item.code})`,
|
|
|
|
|
+ key: item.id.toString(),
|
|
|
|
|
+ })),
|
|
|
|
|
+ }));
|
|
|
|
|
+ }, [permissions]);
|
|
|
|
|
+
|
|
|
|
|
+ // 在权限模态框打开时更新选中状态
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (permissionModalVisible && rolePermissions.length > 0) {
|
|
|
|
|
+ setSelectedPermissionIds(rolePermissions);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [permissionModalVisible, rolePermissions]);
|
|
|
|
|
+
|
|
|
|
|
+ const columns = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '角色名称',
|
|
|
|
|
+ dataIndex: 'name',
|
|
|
|
|
+ key: 'name',
|
|
|
|
|
+ width: 150,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '描述',
|
|
|
|
|
+ dataIndex: 'description',
|
|
|
|
|
+ key: 'description',
|
|
|
|
|
+ ellipsis: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '权限数量',
|
|
|
|
|
+ key: 'permissionCount',
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ render: (_: any, record: RoleItem) => (
|
|
|
|
|
+ <Tag color="blue">{record.permissions?.length || 0}</Tag>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '创建时间',
|
|
|
|
|
+ dataIndex: 'createdAt',
|
|
|
|
|
+ key: 'createdAt',
|
|
|
|
|
+ width: 180,
|
|
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
+ sorter: (a: RoleItem, b: RoleItem) =>
|
|
|
|
|
+ dayjs(a.createdAt).valueOf() - dayjs(b.createdAt).valueOf(),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '更新时间',
|
|
|
|
|
+ dataIndex: 'updatedAt',
|
|
|
|
|
+ key: 'updatedAt',
|
|
|
|
|
+ width: 180,
|
|
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
+ sorter: (a: RoleItem, b: RoleItem) =>
|
|
|
|
|
+ dayjs(a.updatedAt).valueOf() - dayjs(b.updatedAt).valueOf(),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '操作',
|
|
|
|
|
+ key: 'action',
|
|
|
|
|
+ width: 200,
|
|
|
|
|
+ fixed: 'right' as const,
|
|
|
|
|
+ render: (_: any, record: RoleItem) => (
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="link"
|
|
|
|
|
+ icon={<EditOutlined />}
|
|
|
|
|
+ onClick={() => handleEdit(record)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 编辑
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="link"
|
|
|
|
|
+ icon={<KeyOutlined />}
|
|
|
|
|
+ onClick={() => handlePermissionConfig(record)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 配置权限
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="link"
|
|
|
|
|
+ danger
|
|
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
|
|
+ onClick={() => handleDelete(record)}
|
|
|
|
|
+ disabled={record.name === 'admin'}
|
|
|
|
|
+ >
|
|
|
|
|
+ 删除
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="p-6">
|
|
|
|
|
+ <Spin spinning={rolesLoading || permissionsLoading}>
|
|
|
|
|
+ <Card
|
|
|
|
|
+ title="角色管理"
|
|
|
|
|
+ extra={
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ icon={<PlusOutlined />}
|
|
|
|
|
+ onClick={handleAdd}
|
|
|
|
|
+ loading={createRoleMutation.isPending}
|
|
|
|
|
+ >
|
|
|
|
|
+ 新建角色
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <Table
|
|
|
|
|
+ columns={columns}
|
|
|
|
|
+ dataSource={roles}
|
|
|
|
|
+ rowKey="id"
|
|
|
|
|
+ scroll={{ x: 1000 }}
|
|
|
|
|
+ pagination={{
|
|
|
|
|
+ showSizeChanger: true,
|
|
|
|
|
+ showQuickJumper: true,
|
|
|
|
|
+ showTotal: (total: number) => `共 ${total} 条记录`,
|
|
|
|
|
+ defaultPageSize: 10,
|
|
|
|
|
+ pageSizeOptions: [10, 20, 50, 100],
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </Spin>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 角色编辑模态框 */}
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={editingRole ? '编辑角色' : '新建角色'}
|
|
|
|
|
+ open={modalVisible}
|
|
|
|
|
+ onCancel={() => setModalVisible(false)}
|
|
|
|
|
+ onOk={() => form.submit()}
|
|
|
|
|
+ okButtonProps={{
|
|
|
|
|
+ loading: editingRole ? updateRoleMutation.isPending : createRoleMutation.isPending
|
|
|
|
|
+ }}
|
|
|
|
|
+ width={500}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form
|
|
|
|
|
+ form={form}
|
|
|
|
|
+ layout="vertical"
|
|
|
|
|
+ onFinish={handleSave}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label="角色名称"
|
|
|
|
|
+ name="name"
|
|
|
|
|
+ rules={[
|
|
|
|
|
+ { required: true, message: '请输入角色名称' },
|
|
|
|
|
+ { min: 2, max: 50, message: '角色名称长度必须在2-50个字符之间' }
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder="请输入角色名称"
|
|
|
|
|
+ maxLength={50}
|
|
|
|
|
+ showCount
|
|
|
|
|
+ />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label="描述"
|
|
|
|
|
+ name="description"
|
|
|
|
|
+ rules={[{ max: 500, message: '描述最多500个字符' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <TextArea
|
|
|
|
|
+ rows={3}
|
|
|
|
|
+ placeholder="请输入角色描述"
|
|
|
|
|
+ maxLength={500}
|
|
|
|
|
+ showCount
|
|
|
|
|
+ />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 权限配置模态框 */}
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={`配置权限 - ${selectedRole?.name}`}
|
|
|
|
|
+ open={permissionModalVisible}
|
|
|
|
|
+ onCancel={() => setPermissionModalVisible(false)}
|
|
|
|
|
+ onOk={handleSavePermissions}
|
|
|
|
|
+ okButtonProps={{ loading: updateRolePermissionsMutation.isPending }}
|
|
|
|
|
+ width={800}
|
|
|
|
|
+ destroyOnClose
|
|
|
|
|
+ >
|
|
|
|
|
+ <Spin spinning={rolePermissionsLoading}>
|
|
|
|
|
+ <Tree
|
|
|
|
|
+ checkable
|
|
|
|
|
+ treeData={permissionTreeData}
|
|
|
|
|
+ checkedKeys={selectedPermissionIds.map(String)}
|
|
|
|
|
+ onCheck={(checkedKeys) => {
|
|
|
|
|
+ const checked = checkedKeys as string[];
|
|
|
|
|
+ setSelectedPermissionIds(checked.map(Number));
|
|
|
|
|
+ }}
|
|
|
|
|
+ height={400}
|
|
|
|
|
+ titleRender={(node) => {
|
|
|
|
|
+ if (node.children) {
|
|
|
|
|
+ return <strong>{node.title}</strong>;
|
|
|
|
|
+ }
|
|
|
|
|
+ return <span>{node.title}</span>;
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Spin>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export default Roles;
|