2026-02-11 05:44:31 +00:00
|
|
|
|
import { Button, Drawer, Form, Input, message, Tag, Typography, Tree } from "antd";
|
|
|
|
|
|
import type { DataNode } from "antd/es/tree";
|
2026-02-10 09:48:44 +00:00
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2026-02-11 05:44:31 +00:00
|
|
|
|
import {
|
|
|
|
|
|
createRole,
|
|
|
|
|
|
listPermissions,
|
|
|
|
|
|
listRolePermissions,
|
|
|
|
|
|
listRoles,
|
|
|
|
|
|
saveRolePermissions,
|
|
|
|
|
|
updateRole
|
|
|
|
|
|
} from "../api";
|
|
|
|
|
|
import type { SysPermission, SysRole } from "../types";
|
2026-02-10 09:48:44 +00:00
|
|
|
|
import { usePermission } from "../hooks/usePermission";
|
2026-02-11 05:44:31 +00:00
|
|
|
|
import { EditOutlined, PlusOutlined, SafetyCertificateOutlined } from "@ant-design/icons";
|
|
|
|
|
|
import "./Roles.css";
|
|
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_STATUS = 1;
|
|
|
|
|
|
|
|
|
|
|
|
type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] };
|
|
|
|
|
|
|
|
|
|
|
|
const buildPermissionTree = (list: SysPermission[]): PermissionNode[] => {
|
|
|
|
|
|
const active = list.filter((p) => p.status !== 0);
|
|
|
|
|
|
const map = new Map<number, PermissionNode>();
|
|
|
|
|
|
const roots: PermissionNode[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
active.forEach((item) => {
|
|
|
|
|
|
map.set(item.permId, { ...item, key: item.permId, children: [] });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
map.forEach((node) => {
|
|
|
|
|
|
if (node.parentId && map.has(node.parentId)) {
|
|
|
|
|
|
map.get(node.parentId)!.children!.push(node);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
roots.push(node);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const sortNodes = (nodes: PermissionNode[]) => {
|
|
|
|
|
|
nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
|
|
|
|
|
nodes.forEach((n) => n.children && sortNodes(n.children));
|
|
|
|
|
|
};
|
|
|
|
|
|
sortNodes(roots);
|
|
|
|
|
|
return roots;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toTreeData = (nodes: PermissionNode[]): DataNode[] =>
|
|
|
|
|
|
nodes.map((node) => ({
|
|
|
|
|
|
key: node.permId,
|
|
|
|
|
|
title: (
|
|
|
|
|
|
<span className="role-permission-node">
|
|
|
|
|
|
<span>{node.name}</span>
|
|
|
|
|
|
{node.permType === "button" && <Tag color="blue">按钮</Tag>}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
children: node.children ? toTreeData(node.children) : undefined
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const generateRoleCode = () => `ROLE-${Date.now().toString(36).toUpperCase()}`;
|
2026-02-10 09:48:44 +00:00
|
|
|
|
|
|
|
|
|
|
export default function Roles() {
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const [saving, setSaving] = useState(false);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
const [data, setData] = useState<SysRole[]>([]);
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const [permissions, setPermissions] = useState<SysPermission[]>([]);
|
|
|
|
|
|
const [rolePermMap, setRolePermMap] = useState<Record<number, number[]>>({});
|
|
|
|
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
const [editing, setEditing] = useState<SysRole | null>(null);
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const [selectedPermIds, setSelectedPermIds] = useState<number[]>([]);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const { can } = usePermission();
|
|
|
|
|
|
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const permissionMap = useMemo(() => {
|
|
|
|
|
|
const map = new Map<number, SysPermission>();
|
|
|
|
|
|
permissions.forEach((p) => map.set(p.permId, p));
|
|
|
|
|
|
return map;
|
|
|
|
|
|
}, [permissions]);
|
|
|
|
|
|
|
|
|
|
|
|
const permissionTreeData = useMemo(
|
|
|
|
|
|
() => toTreeData(buildPermissionTree(permissions)),
|
|
|
|
|
|
[permissions]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const loadPermissions = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const list = await listPermissions();
|
|
|
|
|
|
setPermissions(list || []);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
setPermissions([]);
|
|
|
|
|
|
message.error("加载权限失败,请确认有管理员权限");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadRolePermissions = async (roles: SysRole[]) => {
|
|
|
|
|
|
const entries = await Promise.all(
|
|
|
|
|
|
roles.map(async (role) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ids = await listRolePermissions(role.roleId);
|
|
|
|
|
|
const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id));
|
|
|
|
|
|
return [role.roleId, normalized] as const;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return [role.roleId, []] as const;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
setRolePermMap(Object.fromEntries(entries));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadRoles = async () => {
|
2026-02-10 09:48:44 +00:00
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const list = await listRoles();
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const roles = list || [];
|
|
|
|
|
|
setData(roles);
|
|
|
|
|
|
await loadPermissions();
|
|
|
|
|
|
await loadRolePermissions(roles);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-02-11 05:44:31 +00:00
|
|
|
|
loadRoles();
|
2026-02-10 09:48:44 +00:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const openCreate = () => {
|
|
|
|
|
|
setEditing(null);
|
2026-02-11 05:44:31 +00:00
|
|
|
|
setSelectedPermIds([]);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
form.resetFields();
|
2026-02-11 05:44:31 +00:00
|
|
|
|
setDrawerOpen(true);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openEdit = (record: SysRole) => {
|
|
|
|
|
|
setEditing(record);
|
2026-02-11 05:44:31 +00:00
|
|
|
|
setSelectedPermIds(rolePermMap[record.roleId] || []);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
roleName: record.roleName,
|
|
|
|
|
|
remark: record.remark
|
|
|
|
|
|
});
|
|
|
|
|
|
setDrawerOpen(true);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-11 05:44:31 +00:00
|
|
|
|
useEffect(() => {
|
2026-02-10 09:48:44 +00:00
|
|
|
|
if (editing) {
|
2026-02-11 05:44:31 +00:00
|
|
|
|
setSelectedPermIds(rolePermMap[editing.roleId] || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [editing, rolePermMap]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
|
setDrawerOpen(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
setSaving(true);
|
|
|
|
|
|
const payload: Partial<SysRole> = {
|
|
|
|
|
|
roleCode: editing?.roleCode || generateRoleCode(),
|
|
|
|
|
|
roleName: values.roleName,
|
|
|
|
|
|
remark: values.remark,
|
|
|
|
|
|
status: editing?.status ?? DEFAULT_STATUS
|
|
|
|
|
|
};
|
|
|
|
|
|
let roleId = editing?.roleId;
|
|
|
|
|
|
if (editing) {
|
|
|
|
|
|
await updateRole(editing.roleId, payload);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await createRole(payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const list = await listRoles();
|
|
|
|
|
|
const roles = list || [];
|
|
|
|
|
|
setData(roles);
|
|
|
|
|
|
if (!roleId) {
|
|
|
|
|
|
roleId = roles.find((r) => r.roleCode === payload.roleCode)?.roleId;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (roleId) {
|
|
|
|
|
|
await saveRolePermissions(roleId, selectedPermIds);
|
|
|
|
|
|
}
|
|
|
|
|
|
await loadRolePermissions(roles);
|
|
|
|
|
|
setDrawerOpen(false);
|
|
|
|
|
|
message.success(editing ? "角色已更新" : "角色已创建");
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e instanceof Error && e.message) {
|
|
|
|
|
|
message.error(e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-11 05:44:31 +00:00
|
|
|
|
const renderRolePermissions = (role: SysRole) => {
|
|
|
|
|
|
const permIds = rolePermMap[role.roleId] || [];
|
|
|
|
|
|
const perms = permIds
|
|
|
|
|
|
.map((id) => permissionMap.get(id))
|
|
|
|
|
|
.filter((p): p is SysPermission => Boolean(p));
|
|
|
|
|
|
const preview = perms.slice(0, 3);
|
|
|
|
|
|
const totalCount = permIds.length;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="role-permission-summary">
|
|
|
|
|
|
<span>权限概览</span>
|
|
|
|
|
|
<span className="role-permission-badge">{`${totalCount}个权限`}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="role-permission-tags">
|
|
|
|
|
|
{preview.length ? (
|
|
|
|
|
|
preview.map((p) => (
|
|
|
|
|
|
<Tag key={p.permId} className="role-permission-tag">
|
|
|
|
|
|
{p.name}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Tag className="role-permission-tag">
|
|
|
|
|
|
{totalCount ? "已选权限" : "暂无权限"}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-11 05:44:31 +00:00
|
|
|
|
<div className="roles-page">
|
|
|
|
|
|
<div className="roles-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Title level={4} className="roles-title">
|
|
|
|
|
|
系统角色权限
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<Text type="secondary" className="roles-subtitle">
|
|
|
|
|
|
设置系统中不同角色的访问权限和操作边界
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
2026-02-10 09:48:44 +00:00
|
|
|
|
{can("sys_role:create") && (
|
2026-02-11 05:44:31 +00:00
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
|
|
|
|
添加角色
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="roles-grid">
|
|
|
|
|
|
{data.map((role) => (
|
|
|
|
|
|
<div key={role.roleId} className="role-card">
|
|
|
|
|
|
<div className="role-card-header">
|
|
|
|
|
|
<div className="role-icon">
|
|
|
|
|
|
<SafetyCertificateOutlined />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{can("sys_role:update") && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
className="role-edit-btn"
|
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
|
onClick={() => openEdit(role)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="role-main">
|
|
|
|
|
|
<div className="role-name">{role.roleName}</div>
|
|
|
|
|
|
<div className="role-id">{`ID: ${role.roleCode || role.roleId}`}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{renderRolePermissions(role)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="role-footer">
|
|
|
|
|
|
<span>最后同步</span>
|
|
|
|
|
|
<span>{role.updatedAt ? "刚刚" : "刚刚"}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{!data.length && !loading && (
|
|
|
|
|
|
<div className="roles-empty">暂无角色</div>
|
2026-02-10 09:48:44 +00:00
|
|
|
|
)}
|
2026-02-11 05:44:31 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Drawer
|
|
|
|
|
|
open={drawerOpen}
|
|
|
|
|
|
onClose={handleClose}
|
|
|
|
|
|
width={420}
|
|
|
|
|
|
closable
|
|
|
|
|
|
title={
|
|
|
|
|
|
<div className="role-drawer-title">
|
|
|
|
|
|
<div className="role-drawer-icon">
|
|
|
|
|
|
<SafetyCertificateOutlined />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="role-drawer-heading">
|
|
|
|
|
|
{editing ? "编辑角色" : "创建新角色"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
footer={
|
|
|
|
|
|
<div className="role-drawer-footer">
|
|
|
|
|
|
<Button type="link" className="role-drawer-cancel" onClick={handleClose}>
|
|
|
|
|
|
取消更改
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
className="role-drawer-submit"
|
|
|
|
|
|
loading={saving}
|
|
|
|
|
|
onClick={submit}
|
|
|
|
|
|
>
|
|
|
|
|
|
确认并同步到系统
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2026-02-10 09:48:44 +00:00
|
|
|
|
>
|
2026-02-11 05:44:31 +00:00
|
|
|
|
<Form form={form} layout="vertical" className="role-form">
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="角色名称"
|
|
|
|
|
|
name="roleName"
|
|
|
|
|
|
rules={[{ required: true, message: "请输入角色名称" }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="例如:Auditor" />
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</Form.Item>
|
2026-02-11 05:44:31 +00:00
|
|
|
|
<Form.Item label="描述" name="remark">
|
|
|
|
|
|
<Input placeholder="该角色的职责描述..." />
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
2026-02-11 05:44:31 +00:00
|
|
|
|
|
|
|
|
|
|
<div className="role-permission-section">
|
|
|
|
|
|
<div className="role-permission-group-title">
|
|
|
|
|
|
<span className="role-permission-group-icon">
|
|
|
|
|
|
<SafetyCertificateOutlined />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span>权限选择</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="role-permission-tree">
|
|
|
|
|
|
<Tree
|
|
|
|
|
|
checkable
|
|
|
|
|
|
selectable={false}
|
|
|
|
|
|
checkStrictly={false}
|
|
|
|
|
|
treeData={permissionTreeData}
|
|
|
|
|
|
checkedKeys={selectedPermIds}
|
|
|
|
|
|
onCheck={(keys) => {
|
|
|
|
|
|
const raw = Array.isArray(keys) ? keys : keys.checked;
|
|
|
|
|
|
const normalized = (raw as Array<string | number>).map((k) => Number(k));
|
|
|
|
|
|
setSelectedPermIds(normalized.filter((id) => !Number.isNaN(id)));
|
|
|
|
|
|
}}
|
|
|
|
|
|
defaultExpandAll
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Drawer>
|
2026-02-10 09:48:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|