412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
Table,
|
||
|
|
Button,
|
||
|
|
Modal,
|
||
|
|
Form,
|
||
|
|
Input,
|
||
|
|
Select,
|
||
|
|
Checkbox,
|
||
|
|
Tree,
|
||
|
|
message,
|
||
|
|
Space,
|
||
|
|
Tag,
|
||
|
|
Tooltip,
|
||
|
|
Alert,
|
||
|
|
Typography,
|
||
|
|
Divider,
|
||
|
|
Collapse,
|
||
|
|
} from 'antd';
|
||
|
|
import { PlusOutlined, EditOutlined, DeleteOutlined, SaveOutlined, CloseOutlined, KeyOutlined, LockOutlined, DownOutlined, UpOutlined, LineChartOutlined } from '@ant-design/icons';
|
||
|
|
import { useRequest } from 'umi';
|
||
|
|
import {
|
||
|
|
createRole as createRoleAPI,
|
||
|
|
updateRole as updateRoleAPI,
|
||
|
|
deleteRole as deleteRoleAPI,
|
||
|
|
getAllRoles as getAllRolesAPI,
|
||
|
|
getPermissionDefinitions as getPermissionsAPI,
|
||
|
|
} from '@/services/roleDataSource';
|
||
|
|
|
||
|
|
const { Title, Text } = Typography;
|
||
|
|
const { Option } = Select;
|
||
|
|
const { TreeNode } = Tree;
|
||
|
|
const { Panel } = Collapse;
|
||
|
|
|
||
|
|
const RoleManagement: React.FC = () => {
|
||
|
|
const [roles, setRoles] = useState<any[]>([]);
|
||
|
|
const [permissions, setPermissions] = useState<any[]>([]);
|
||
|
|
const [selectedRole, setSelectedRole] = useState<any>(null);
|
||
|
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||
|
|
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||
|
|
const [form] = Form.useForm();
|
||
|
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||
|
|
const [deletingRole, setDeletingRole] = useState<string>('');
|
||
|
|
|
||
|
|
// 加载角色列表
|
||
|
|
const { run: loadRoles, loading: loadingRoles } = useRequest(getAllRolesAPI, {
|
||
|
|
onSuccess: (data) => {
|
||
|
|
setRoles(data);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// 加载权限列表
|
||
|
|
const { run: loadPermissions, loading: loadingPermissions } = useRequest(getPermissionsAPI, {
|
||
|
|
onSuccess: (data) => {
|
||
|
|
setPermissions(data);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadRoles();
|
||
|
|
loadPermissions();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 打开创建/编辑角色模态框
|
||
|
|
const showModal = (role?: any) => {
|
||
|
|
if (role) {
|
||
|
|
setSelectedRole(role);
|
||
|
|
form.setFieldsValue({
|
||
|
|
roleCode: role.code,
|
||
|
|
roleName: role.name,
|
||
|
|
permissions: role.permissions || [],
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
setSelectedRole(null);
|
||
|
|
form.resetFields();
|
||
|
|
}
|
||
|
|
setIsModalVisible(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 关闭模态框
|
||
|
|
const handleCancel = () => {
|
||
|
|
setIsModalVisible(false);
|
||
|
|
setSelectedRole(null);
|
||
|
|
form.resetFields();
|
||
|
|
};
|
||
|
|
|
||
|
|
// 提交角色表单
|
||
|
|
const handleSubmit = async () => {
|
||
|
|
try {
|
||
|
|
const values = await form.validateFields();
|
||
|
|
setConfirmLoading(true);
|
||
|
|
|
||
|
|
if (selectedRole) {
|
||
|
|
// 更新角色
|
||
|
|
await updateRoleAPI(values.roleCode, values.roleName, values.permissions);
|
||
|
|
message.success('Role updated successfully');
|
||
|
|
} else {
|
||
|
|
// 创建角色
|
||
|
|
await createRoleAPI(values.roleCode, values.roleName, values.permissions);
|
||
|
|
message.success('Role created successfully');
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsModalVisible(false);
|
||
|
|
form.resetFields();
|
||
|
|
loadRoles();
|
||
|
|
} catch (error) {
|
||
|
|
message.error('Failed to save role');
|
||
|
|
} finally {
|
||
|
|
setConfirmLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 打开删除确认模态框
|
||
|
|
const showDeleteConfirm = (roleCode: string) => {
|
||
|
|
setDeletingRole(roleCode);
|
||
|
|
setIsDeleteModalVisible(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 确认删除角色
|
||
|
|
const handleDelete = async () => {
|
||
|
|
try {
|
||
|
|
setConfirmLoading(true);
|
||
|
|
await deleteRoleAPI(deletingRole);
|
||
|
|
message.success('Role deleted successfully');
|
||
|
|
setIsDeleteModalVisible(false);
|
||
|
|
loadRoles();
|
||
|
|
} catch (error) {
|
||
|
|
message.error('Failed to delete role');
|
||
|
|
} finally {
|
||
|
|
setConfirmLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 构建权限树
|
||
|
|
const buildPermissionTree = () => {
|
||
|
|
const modules = new Map<string, any[]>();
|
||
|
|
|
||
|
|
// 按模块分组权限
|
||
|
|
permissions.forEach(permission => {
|
||
|
|
if (!modules.has(permission.module)) {
|
||
|
|
modules.set(permission.module, []);
|
||
|
|
}
|
||
|
|
modules.get(permission.module)?.push(permission);
|
||
|
|
});
|
||
|
|
|
||
|
|
return Array.from(modules.entries()).map(([module, perms]) => (
|
||
|
|
<TreeNode key={module} title={module}>
|
||
|
|
{perms.map(perm => (
|
||
|
|
<TreeNode
|
||
|
|
key={perm.id}
|
||
|
|
title={
|
||
|
|
<Space size="small">
|
||
|
|
<Text>{perm.name}</Text>
|
||
|
|
<Tooltip title={perm.description}>
|
||
|
|
<Tag size="small" color="blue">{perm.action}</Tag>
|
||
|
|
</Tooltip>
|
||
|
|
</Space>
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</TreeNode>
|
||
|
|
));
|
||
|
|
};
|
||
|
|
|
||
|
|
// 检查角色是否为系统预设角色
|
||
|
|
const isSystemRole = (roleCode: string) => {
|
||
|
|
const systemRoles = ['ADMIN', 'MANAGER', 'OPERATOR', 'FINANCE', 'SOURCING', 'LOGISTICS', 'ANALYST'];
|
||
|
|
return systemRoles.includes(roleCode);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 计算权限继承关系
|
||
|
|
const calculatePermissionInheritance = (role: any) => {
|
||
|
|
const inheritedPermissions = new Set<string>();
|
||
|
|
const roleHierarchy = {
|
||
|
|
'ADMIN': [],
|
||
|
|
'MANAGER': ['OPERATOR'],
|
||
|
|
'OPERATOR': [],
|
||
|
|
'FINANCE': [],
|
||
|
|
'SOURCING': [],
|
||
|
|
'LOGISTICS': [],
|
||
|
|
'ANALYST': [],
|
||
|
|
};
|
||
|
|
|
||
|
|
// 递归计算继承的权限
|
||
|
|
const calculateInheritance = (roleCode: string) => {
|
||
|
|
const childRoles = roleHierarchy[roleCode as keyof typeof roleHierarchy] || [];
|
||
|
|
childRoles.forEach(childRoleCode => {
|
||
|
|
const childRole = roles.find(r => r.code === childRoleCode);
|
||
|
|
if (childRole) {
|
||
|
|
childRole.permissions?.forEach((perm: string) => {
|
||
|
|
inheritedPermissions.add(perm);
|
||
|
|
});
|
||
|
|
calculateInheritance(childRoleCode);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
calculateInheritance(role.code);
|
||
|
|
return Array.from(inheritedPermissions);
|
||
|
|
};
|
||
|
|
|
||
|
|
const columns = [
|
||
|
|
{
|
||
|
|
title: 'Role Code',
|
||
|
|
dataIndex: 'code',
|
||
|
|
key: 'code',
|
||
|
|
render: (code: string) => (
|
||
|
|
<Space>
|
||
|
|
<KeyOutlined />
|
||
|
|
<Text strong>{code}</Text>
|
||
|
|
{isSystemRole(code) && <Tag color="green">System</Tag>}
|
||
|
|
</Space>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Role Name',
|
||
|
|
dataIndex: 'name',
|
||
|
|
key: 'name',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Permissions',
|
||
|
|
dataIndex: 'permissions',
|
||
|
|
key: 'permissions',
|
||
|
|
render: (permissions: string[]) => (
|
||
|
|
<Tag color="blue">{permissions?.length || 0} permissions</Tag>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Actions',
|
||
|
|
key: 'actions',
|
||
|
|
render: (_: any, record: any) => (
|
||
|
|
<Space size="middle">
|
||
|
|
<Button
|
||
|
|
type="primary"
|
||
|
|
icon={<EditOutlined />}
|
||
|
|
onClick={() => showModal(record)}
|
||
|
|
disabled={isSystemRole(record.code)}
|
||
|
|
>
|
||
|
|
Edit
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
danger
|
||
|
|
icon={<DeleteOutlined />}
|
||
|
|
onClick={() => showDeleteConfirm(record.code)}
|
||
|
|
disabled={isSystemRole(record.code)}
|
||
|
|
>
|
||
|
|
Delete
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ padding: 24 }}>
|
||
|
|
<Card
|
||
|
|
title={
|
||
|
|
<Space>
|
||
|
|
<LockOutlined />
|
||
|
|
<Title level={4}>Role Management</Title>
|
||
|
|
</Space>
|
||
|
|
}
|
||
|
|
extra={
|
||
|
|
<Button
|
||
|
|
type="primary"
|
||
|
|
icon={<PlusOutlined />}
|
||
|
|
onClick={() => showModal()}
|
||
|
|
>
|
||
|
|
Create Role
|
||
|
|
</Button>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<Alert
|
||
|
|
message="Role Management"
|
||
|
|
description="Create and manage custom roles with specific permissions. System roles cannot be modified."
|
||
|
|
type="info"
|
||
|
|
style={{ marginBottom: 24 }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Table
|
||
|
|
columns={columns}
|
||
|
|
dataSource={roles}
|
||
|
|
rowKey="code"
|
||
|
|
loading={loadingRoles}
|
||
|
|
pagination={{ pageSize: 10 }}
|
||
|
|
expandable={{
|
||
|
|
expandedRowRender: (record) => (
|
||
|
|
<div style={{ padding: 20 }}>
|
||
|
|
<Collapse defaultActiveKey={['permissions', 'inheritance']}>
|
||
|
|
<Panel
|
||
|
|
header={
|
||
|
|
<Space>
|
||
|
|
<LockOutlined />
|
||
|
|
<Text strong>Assigned Permissions</Text>
|
||
|
|
</Space>
|
||
|
|
}
|
||
|
|
key="permissions"
|
||
|
|
>
|
||
|
|
<Space wrap>
|
||
|
|
{record.permissions?.map((perm: string) => {
|
||
|
|
const permInfo = permissions.find(p => p.id === perm);
|
||
|
|
return (
|
||
|
|
<Tag key={perm} color="blue">
|
||
|
|
{permInfo?.name || perm}
|
||
|
|
</Tag>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</Space>
|
||
|
|
</Panel>
|
||
|
|
<Panel
|
||
|
|
header={
|
||
|
|
<Space>
|
||
|
|
<LineChartOutlined />
|
||
|
|
<Text strong>Permission Inheritance</Text>
|
||
|
|
</Space>
|
||
|
|
}
|
||
|
|
key="inheritance"
|
||
|
|
>
|
||
|
|
<div>
|
||
|
|
<Text>Role Hierarchy:</Text>
|
||
|
|
<div style={{ marginLeft: 20, marginTop: 10 }}>
|
||
|
|
{calculatePermissionInheritance(record).length > 0 ? (
|
||
|
|
<Space wrap>
|
||
|
|
{calculatePermissionInheritance(record).map((perm: string) => {
|
||
|
|
const permInfo = permissions.find(p => p.id === perm);
|
||
|
|
return (
|
||
|
|
<Tag key={perm} color="green">
|
||
|
|
{permInfo?.name || perm}
|
||
|
|
</Tag>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</Space>
|
||
|
|
) : (
|
||
|
|
<Text type="secondary">No inherited permissions</Text>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Panel>
|
||
|
|
</Collapse>
|
||
|
|
</div>
|
||
|
|
),
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Create/Edit Role Modal */}
|
||
|
|
<Modal
|
||
|
|
title={selectedRole ? 'Edit Role' : 'Create Role'}
|
||
|
|
open={isModalVisible}
|
||
|
|
onOk={handleSubmit}
|
||
|
|
onCancel={handleCancel}
|
||
|
|
confirmLoading={confirmLoading}
|
||
|
|
width={800}
|
||
|
|
>
|
||
|
|
<Form
|
||
|
|
form={form}
|
||
|
|
layout="vertical"
|
||
|
|
>
|
||
|
|
<Form.Item
|
||
|
|
name="roleCode"
|
||
|
|
label="Role Code"
|
||
|
|
rules={[
|
||
|
|
{ required: true, message: 'Please enter role code' },
|
||
|
|
{ pattern: /^[A-Z_]+$/, message: 'Role code must be uppercase with underscores' },
|
||
|
|
]}
|
||
|
|
>
|
||
|
|
<Input placeholder="e.g., SALES_MANAGER" disabled={!!selectedRole} />
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item
|
||
|
|
name="roleName"
|
||
|
|
label="Role Name"
|
||
|
|
rules={[{ required: true, message: 'Please enter role name' }]}
|
||
|
|
>
|
||
|
|
<Input placeholder="e.g., Sales Manager" />
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item
|
||
|
|
name="permissions"
|
||
|
|
label="Permissions"
|
||
|
|
rules={[{ required: true, message: 'Please select permissions' }]}
|
||
|
|
>
|
||
|
|
<Tree
|
||
|
|
checkable
|
||
|
|
treeData={buildPermissionTree()}
|
||
|
|
placeholder="Select permissions"
|
||
|
|
/>
|
||
|
|
</Form.Item>
|
||
|
|
</Form>
|
||
|
|
</Modal>
|
||
|
|
|
||
|
|
{/* Delete Role Modal */}
|
||
|
|
<Modal
|
||
|
|
title="Delete Role"
|
||
|
|
open={isDeleteModalVisible}
|
||
|
|
onOk={handleDelete}
|
||
|
|
onCancel={() => setIsDeleteModalVisible(false)}
|
||
|
|
confirmLoading={confirmLoading}
|
||
|
|
okText="Delete"
|
||
|
|
cancelText="Cancel"
|
||
|
|
okType="danger"
|
||
|
|
>
|
||
|
|
<p>Are you sure you want to delete the role <strong>{deletingRole}</strong>?</p>
|
||
|
|
<p>All users assigned to this role will lose these permissions.</p>
|
||
|
|
</Modal>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default RoleManagement;
|