import { Avatar, Button, Card, Col, Drawer, Empty, Form, Input, List, Pagination, Radio, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row } from "antd"; import type { DataNode } from "antd/es/tree"; import { useEffect, useMemo, useState } from "react"; import { ApartmentOutlined, CheckCircleFilled, DeleteOutlined, EditOutlined, FilterOutlined, KeyOutlined, PlusOutlined, SafetyCertificateOutlined, SaveOutlined, SearchOutlined, TeamOutlined, UserAddOutlined, UserOutlined } from "@ant-design/icons"; import { bindUsersToRole, createRole, deleteRole, fetchUsersByRoleId, getRoleDataScope, listOrgs, listPermissions, listRolePermissions, listTenants, listUsers, pageRoles, saveRoleDataScope, saveRolePermissions, unbindUserFromRole, updateRole } from "@/api"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; import PageHeader from "@/components/shared/PageHeader"; import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types"; import "./index.less"; const { Text, Title } = Typography; type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] }; type OrgTreeNode = SysOrg & { key: number; children?: OrgTreeNode[] }; type RoleTabKey = "permissions" | "dataScope" | "users"; const DEFAULT_STATUS = 1; const DEFAULT_ROLE_PAGE_SIZE = 10; const BUTTON_SHORT_LABEL = "按钮"; const DATA_SCOPE_OPTIONS = [ { label: "全部", value: "ALL" }, { label: "个人", value: "SELF" }, { label: "本部门", value: "DEPT" }, { label: "本部门及下级部门", value: "DEPT_AND_CHILD" }, { label: "自定义部门", value: "CUSTOM" } ] as const; function normalizeNumber(value: unknown): number | undefined { if (typeof value === "number") { return Number.isFinite(value) ? value : undefined; } if (typeof value === "string" && value.trim() !== "") { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } if (value && typeof value === "object" && "value" in value) { return normalizeNumber((value as { value?: unknown }).value); } return undefined; } function buildPermissionTree(list: SysPermission[]): PermissionNode[] { const active = (list || []).filter((permission) => permission.status !== 0); const map = new Map(); const roots: PermissionNode[] = []; active.forEach((item) => { map.set(item.permId, { ...item, key: item.permId, children: [] }); }); map.forEach((node) => { if (node.parentId && node.parentId !== 0) { const parent = map.get(node.parentId); if (parent) { parent.children!.push(node); } } else { roots.push(node); } }); const sortNodes = (nodes: PermissionNode[]) => { nodes.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)); nodes.forEach((node) => node.children && sortNodes(node.children)); }; sortNodes(roots); return roots; } function toPermissionTreeData(nodes: PermissionNode[], buttonShortLabel: string): DataNode[] { return nodes.map((node) => ({ key: node.permId, title: ( {node.name} {node.permType === "button" ? {buttonShortLabel} : null} ), children: node.children?.length ? toPermissionTreeData(node.children, buttonShortLabel) : undefined })); } function buildOrgTree(list: SysOrg[]): OrgTreeNode[] { const map = new Map(); const roots: OrgTreeNode[] = []; list.forEach((item) => { map.set(item.id, { ...item, key: item.id, 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: OrgTreeNode[]) => { nodes.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)); nodes.forEach((node) => node.children && sortNodes(node.children)); }; sortNodes(roots); return roots; } function toOrgTreeData(nodes: OrgTreeNode[]): DataNode[] { return nodes.map((node) => ({ key: node.id, title: node.orgName, children: node.children?.length ? toOrgTreeData(node.children) : undefined })); } function getDataScopeDescription(scopeType: string) { switch (scopeType) { case "ALL": return "当前角色可访问当前租户全部数据。"; case "SELF": return "当前角色仅可访问本人数据。"; case "DEPT": return "当前角色可访问本人所在部门的数据。"; case "DEPT_AND_CHILD": return "当前角色可访问本人所在部门及所有下级部门的数据。"; case "CUSTOM": return "当前角色可访问选中部门的数据。"; default: return ""; } } const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`; export default function Roles() { const { can } = usePermission(); const { items: statusDict } = useDict("sys_common_status"); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [data, setData] = useState([]); const [permissions, setPermissions] = useState([]); const [selectedRole, setSelectedRole] = useState(null); const [selectedPermIds, setSelectedPermIds] = useState([]); const [halfCheckedIds, setHalfCheckedIds] = useState([]); const [roleUsers, setRoleUsers] = useState([]); const [loadingUsers, setLoadingUsers] = useState(false); const [allUsers, setAllUsers] = useState([]); const [userModalOpen, setUserModalOpen] = useState(false); const [selectedUserKeys, setSelectedUserKeys] = useState([]); const [userSearchText, setUserSearchText] = useState(""); const [searchText, setSearchText] = useState(""); const [rolePage, setRolePage] = useState({ current: 1, size: DEFAULT_ROLE_PAGE_SIZE, total: 0 }); const [filterTenantId, setFilterTenantId] = useState(undefined); const [drawerOpen, setDrawerOpen] = useState(false); const [editing, setEditing] = useState(null); const [tenants, setTenants] = useState([]); const [activeTab, setActiveTab] = useState("permissions"); const [dataScopeType, setDataScopeType] = useState("SELF"); const [scopeOrgIds, setScopeOrgIds] = useState([]); const [scopeOrgTree, setScopeOrgTree] = useState([]); const [form] = Form.useForm(); const isPlatformMode = useMemo(() => { const profileStr = sessionStorage.getItem("userProfile"); if (!profileStr) return false; const profile = JSON.parse(profileStr); return profile.isPlatformAdmin && localStorage.getItem("activeTenantId") === "0"; }, []); const activeTenantId = useMemo(() => normalizeNumber(localStorage.getItem("activeTenantId")) ?? 0, []); const permissionTreeData = useMemo(() => toPermissionTreeData(buildPermissionTree(permissions), BUTTON_SHORT_LABEL), [permissions]); const filteredModalUsers = useMemo(() => { const existingIds = new Set(roleUsers.map((user) => user.userId)); return allUsers.filter( (user) => !existingIds.has(user.userId) && (user.username.toLowerCase().includes(userSearchText.toLowerCase()) || user.displayName.toLowerCase().includes(userSearchText.toLowerCase())) ); }, [allUsers, roleUsers, userSearchText]); useEffect(() => { if (!isPlatformMode) return; listTenants({ current: 1, size: 100 }).then((response) => setTenants(response.records || [])).catch(() => {}); }, [isPlatformMode]); const loadPermissions = async () => { try { const list = await listPermissions(); setPermissions(list || []); return list || []; } catch { setPermissions([]); return [] as SysPermission[]; } }; const selectRole = async (role: SysRole, permissionList: SysPermission[] = permissions) => { setSelectedRole(role); setLoadingUsers(true); try { const [ids, users, dataScope, orgs] = await Promise.all([ listRolePermissions(role.roleId), fetchUsersByRoleId(role.roleId), getRoleDataScope(role.roleId), listOrgs(role.tenantId) ]); const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)); const leafIds = normalized.filter((id) => !permissionList.some((permission) => permission.parentId === id)); setSelectedPermIds(leafIds); setHalfCheckedIds([]); setRoleUsers(users || []); setDataScopeType(dataScope?.scopeType || role.dataScopeType || "SELF"); setScopeOrgIds((dataScope?.orgIds || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id))); setScopeOrgTree(toOrgTreeData(buildOrgTree(orgs || []))); } finally { setLoadingUsers(false); } }; const loadRoles = async (page = rolePage.current, size = rolePage.size) => { setLoading(true); try { const permissionList = await loadPermissions(); const response = await pageRoles({ current: page, size, tenantId: isPlatformMode ? filterTenantId : activeTenantId, keyword: searchText || undefined }); const roles = response?.records || []; setRolePage({ current: page, size, total: response?.total || 0 }); setData(roles); if (roles.length === 0) { setSelectedRole(null); setRoleUsers([]); setSelectedPermIds([]); setHalfCheckedIds([]); setDataScopeType("SELF"); setScopeOrgIds([]); setScopeOrgTree([]); } else if (!selectedRole) { await selectRole(roles[0], permissionList); } else { const updated = roles.find((role) => role.roleId === selectedRole.roleId); if (updated) { await selectRole(updated, permissionList); } else { await selectRole(roles[0], permissionList); } } } finally { setLoading(false); } }; useEffect(() => { void loadRoles(rolePage.current, rolePage.size); }, [filterTenantId, rolePage.current, rolePage.size, searchText]); const loadAllUsers = async () => { try { const list = await listUsers(); setAllUsers(list || []); } catch { setAllUsers([]); } }; const openUserModal = () => { void loadAllUsers(); setSelectedUserKeys([]); setUserModalOpen(true); }; const handleAddUsers = async () => { if (!selectedRole || selectedUserKeys.length === 0) return; await bindUsersToRole(selectedRole.roleId, selectedUserKeys); message.success("操作成功"); setUserModalOpen(false); await selectRole(selectedRole); }; const handleUnbindUser = async (userId: number) => { if (!selectedRole) return; if (selectedRole.roleCode === "TENANT_ADMIN" && roleUsers.length <= 1) { message.warning("租户管理员角色至少需要保留一个绑定用户"); return; } await unbindUserFromRole(selectedRole.roleId, userId); message.success("操作成功"); await selectRole(selectedRole); }; const openCreate = () => { setEditing(null); form.resetFields(); form.setFieldsValue({ status: 1, tenantId: isPlatformMode ? undefined : activeTenantId }); setDrawerOpen(true); }; const openEditBasic = (event: React.MouseEvent, record: SysRole) => { event.stopPropagation(); setEditing(record); form.setFieldsValue(record); setDrawerOpen(true); }; const handleRemove = async (event: React.MouseEvent, id: number) => { event.stopPropagation(); await deleteRole(id); message.success("操作成功"); if (selectedRole?.roleId === id) setSelectedRole(null); await loadRoles(rolePage.current, rolePage.size); }; const submitBasic = async () => { const values = await form.validateFields(); setSaving(true); try { const payload: Partial = { roleCode: editing?.roleCode || values.roleCode || generateRoleCode(), roleName: values.roleName, remark: values.remark, status: values.status ?? DEFAULT_STATUS, tenantId: values.tenantId, dataScopeType: editing?.dataScopeType || "SELF" }; if (editing) { await updateRole(editing.roleId, payload); } else { await createRole(payload); } message.success("操作成功"); setDrawerOpen(false); await loadRoles(rolePage.current, rolePage.size); } finally { setSaving(false); } }; const handleRolePageChange = (page: number, pageSize: number) => { setRolePage((prev) => ({ ...prev, current: page, size: pageSize })); }; const savePermissions = async () => { if (!selectedRole) return; setSaving(true); try { await saveRolePermissions(selectedRole.roleId, Array.from(new Set([...selectedPermIds, ...halfCheckedIds]))); message.success("操作成功"); } finally { setSaving(false); } }; const saveDataScope = async () => { if (!selectedRole) return; if (dataScopeType === "CUSTOM" && scopeOrgIds.length === 0) { message.warning("请选择至少一个部门"); return; } setSaving(true); try { const payload: RoleDataScope = { roleId: selectedRole.roleId, scopeType: dataScopeType, orgIds: dataScopeType === "CUSTOM" ? scopeOrgIds : [] }; await saveRoleDataScope(selectedRole.roleId, payload); message.success("操作成功"); setSelectedRole((prev) => (prev ? { ...prev, dataScopeType } : prev)); setData((prev) => prev.map((item) => item.roleId === selectedRole.roleId ? { ...item, dataScopeType } : item)); } finally { setSaving(false); } }; const handlePrimarySave = () => { if (activeTab === "permissions") { void savePermissions(); return; } if (activeTab === "dataScope") { void saveDataScope(); } }; const saveDisabled = !selectedRole || activeTab === "users" || (activeTab === "permissions" && !can("sys:role:permission:save")) || (activeTab === "dataScope" && !can("sys:role:update")); const saveLabel = activeTab === "dataScope" ? "保存数据权限" : "保存"; return (
{can("sys:role:create") && }
{"角色列表"}} bordered={false} className="app-page__panel-card roles-side-card">
{isPlatformMode && ( } value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear />
}} renderItem={(item) => (
void selectRole(item)}>
{item.roleName} {isPlatformMode && {item.tenantId === 0 ? "平台租户" : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `租户:${item.tenantId}`}} {item.status === 0 && {"停用"}}
{item.roleCode}
{selectedRole?.roleId === item.roleId ? ( ) : null}
)} />
{selectedRole ? (
{selectedRole.roleName}
{selectedRole.roleCode}
} extra={} > setActiveTab(key as RoleTabKey)} className="role-detail-tabs"> {"功能权限"}} key="permissions">
{ const checked = Array.isArray(keys) ? keys : keys.checked; const halfChecked = info.halfCheckedKeys || []; setSelectedPermIds(checked.map((key) => Number(key))); setHalfCheckedIds(halfChecked.map((key) => Number(key))); }} defaultExpandAll />
{"数据权限"}} key="dataScope">
setDataScopeType(event.target.value)} optionType="button" buttonStyle="solid"> {DATA_SCOPE_OPTIONS.map((item) => ( {item.label} ))}
{getDataScopeDescription(dataScopeType)}
{dataScopeType === "CUSTOM" ? (
{ const checked = Array.isArray(keys) ? keys : keys.checked; setScopeOrgIds(checked.map((key) => Number(key))); }} defaultExpandAll />
) : ( )}
{`成员管理 (${roleUsers.length})`}} key="users">
{"已绑定用户"}
( } style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
{user.displayName}
@{user.username}
) }, { title: "手机号", dataIndex: "phone", className: "tabular-nums" }, { title: "状态", dataIndex: "status", width: 80, render: (status: number) => {status === 1 ? "启用" : "停用"} }, { title: "操作", key: "action", width: 80, render: (_: unknown, user: SysUser) => ( void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} /> setDrawerOpen(false)} width={420} destroyOnClose footer={
}>