imeeting/frontend/src/pages/bindings/role-permission/index.tsx

288 lines
9.9 KiB
TypeScript

import {
Button,
Card,
Col,
Empty,
Input,
message,
Row,
Space,
Table,
Tag,
Tree,
Typography
} from "antd";
import type { DataNode } from "antd/es/tree";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ClusterOutlined, KeyOutlined, SafetyCertificateOutlined, SaveOutlined, SearchOutlined } from "@ant-design/icons";
import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "@/api";
import PageContainer from "@/components/shared/PageContainer";
import SectionCard from "@/components/shared/SectionCard";
import { getStandardPagination } from "@/utils/pagination";
import type { SysPermission, SysRole } from "@/types";
import "./index.less";
const { Text } = Typography;
type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] };
function buildPermissionTree(list: SysPermission[]): PermissionNode[] {
if (!list.length) return [];
const active = list.filter((item) => item.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 && 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) => {
if (node.children?.length) {
sortNodes(node.children);
}
});
};
sortNodes(roots);
return roots;
}
function toTreeData(nodes: PermissionNode[]): DataNode[] {
return nodes.map((node) => ({
key: node.permId,
title: (
<Space>
<span>{node.name}</span>
{node.permType === "button" ? (
<Tag color="blue" style={{ marginLeft: 4 }}>
Button
</Tag>
) : null}
</Space>
),
children: node.children?.length ? toTreeData(node.children) : undefined
}));
}
export default function RolePermissionBinding() {
const { t } = useTranslation();
const [roles, setRoles] = useState<SysRole[]>([]);
const [permissions, setPermissions] = useState<SysPermission[]>([]);
const [loadingRoles, setLoadingRoles] = useState(false);
const [loadingPerms, setLoadingPerms] = useState(false);
const [saving, setSaving] = useState(false);
const [selectedRoleId, setSelectedRoleId] = useState<number | null>(null);
const [checkedPermIds, setCheckedPermIds] = useState<number[]>([]);
const [halfCheckedIds, setHalfCheckedIds] = useState<number[]>([]);
const [searchText, setSearchText] = useState("");
const isPlatformMode = useMemo(() => {
const profileStr = sessionStorage.getItem("userProfile");
if (!profileStr) {
return false;
}
const profile = JSON.parse(profileStr);
return !!profile.isPlatformAdmin;
}, []);
const selectedRole = useMemo(
() => roles.find((role) => role.roleId === selectedRoleId) || null,
[roles, selectedRoleId]
);
const filteredRoles = useMemo(() => {
if (!searchText) {
return roles;
}
const lower = searchText.toLowerCase();
return roles.filter((role) => role.roleName.toLowerCase().includes(lower) || role.roleCode.toLowerCase().includes(lower));
}, [roles, searchText]);
const treeData = useMemo(() => toTreeData(buildPermissionTree(permissions)), [permissions]);
const loadRoles = async () => {
setLoadingRoles(true);
try {
const list = await listRoles();
setRoles(list || []);
} finally {
setLoadingRoles(false);
}
};
const loadPermissions = async () => {
setLoadingPerms(true);
try {
const list = await listPermissions();
setPermissions(list || []);
} finally {
setLoadingPerms(false);
}
};
const loadRolePermissions = async (roleId: number) => {
try {
const list = await listRolePermissions(roleId);
const normalized = (list || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id));
const leafIds = normalized.filter((id) => !permissions.some((permission) => permission.parentId === id));
setCheckedPermIds(leafIds);
setHalfCheckedIds([]);
} catch {
setCheckedPermIds([]);
setHalfCheckedIds([]);
}
};
useEffect(() => {
loadRoles();
loadPermissions();
}, []);
useEffect(() => {
if (selectedRoleId && permissions.length > 0) {
loadRolePermissions(selectedRoleId);
} else {
setCheckedPermIds([]);
setHalfCheckedIds([]);
}
}, [selectedRoleId, permissions]);
const handleSave = async () => {
if (!selectedRoleId) {
message.warning(t("rolePerm.selectRole"));
return;
}
setSaving(true);
try {
await saveRolePermissions(selectedRoleId, Array.from(new Set([...checkedPermIds, ...halfCheckedIds])));
message.success(t("common.success"));
} finally {
setSaving(false);
}
};
return (
<PageContainer title={null} className="role-permission-page">
<SectionCard
title={t("rolePerm.title")}
description={t("rolePerm.subtitle")}
>
<div className="app-page__content-toolbar role-permission-toolbar">
<div className="app-page__content-toolbar-actions">
<Button
type="primary"
icon={<SaveOutlined aria-hidden="true" />}
onClick={handleSave}
loading={saving}
disabled={!selectedRoleId || (selectedRole?.roleCode === "TENANT_ADMIN" && !isPlatformMode)}
>
{saving ? t("common.loading") : t("common.save")}
</Button>
</div>
<div className="app-page__content-toolbar-filters" />
</div>
<Row gutter={16} className="role-permission-layout">
<Col xs={24} lg={10} className="role-permission-layout__pane">
<Card title={<Space><SafetyCertificateOutlined aria-hidden="true" /><span>{t("rolePerm.roleList")}</span></Space>} className="app-page__panel-card full-height-card">
<div className="mb-4">
<Input
placeholder={t("rolePerm.searchRole")}
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
value={searchText}
onChange={(event) => setSearchText(event.target.value)}
allowClear
aria-label={t("rolePerm.searchRole")}
/>
</div>
<div style={{ height: "calc(100% - 60px)", overflowY: "auto" }}>
<Table
rowKey="roleId"
size="middle"
loading={loadingRoles}
dataSource={filteredRoles}
rowSelection={{
type: "radio",
selectedRowKeys: selectedRoleId ? [selectedRoleId] : [],
onChange: (keys) => setSelectedRoleId(keys[0] as number)
}}
onRow={(record) => ({
onClick: () => setSelectedRoleId(record.roleId),
className: "cursor-pointer"
})}
pagination={{ ...getStandardPagination(filteredRoles.length, 1, 10, undefined, { showSizeChanger: false }), current: undefined }}
columns={[
{
title: t("roles.roleName"),
key: "role",
render: (_, record: SysRole) => (
<div className="min-w-0">
<div style={{ fontWeight: 500 }} className="truncate">{record.roleName}</div>
<div style={{ fontSize: 12, color: "#8c8c8c" }} className="truncate tabular-nums">{record.roleCode}</div>
</div>
)
},
{
title: t("common.status"),
dataIndex: "status",
width: 80,
render: (value: number) => (value === 1 ? <Tag color="green" className="m-0"></Tag> : <Tag className="m-0"></Tag>)
}
]}
/>
</div>
</Card>
</Col>
<Col xs={24} lg={14} className="role-permission-layout__pane">
<Card
title={<Space><KeyOutlined aria-hidden="true" /><span>{t("rolePerm.permConfig")}</span></Space>}
className="app-page__panel-card full-height-card"
extra={selectedRole ? <Tag color="blue">{t("rolePerm.currentRole")}: {selectedRole.roleName}</Tag> : null}
>
{selectedRoleId ? (
<div className="role-permission-tree-container" style={{ padding: "8px 0", height: "100%", overflowY: "auto" }}>
<Tree
checkable
selectable={false}
checkStrictly={false}
treeData={treeData}
checkedKeys={checkedPermIds}
onCheck={(keys, info) => {
const checked = Array.isArray(keys) ? keys : keys.checked;
const halfChecked = info.halfCheckedKeys || [];
setCheckedPermIds(checked.map((key) => Number(key)));
setHalfCheckedIds(halfChecked.map((key) => Number(key)));
}}
defaultExpandAll
/>
{!permissions.length && !loadingPerms ? <Empty description="暂无权限数据" /> : null}
</div>
) : (
<div className="flex flex-col items-center justify-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200">
<ClusterOutlined style={{ fontSize: 40, color: "#bfbfbf", marginBottom: 16 }} aria-hidden="true" />
<Text type="secondary">{t("rolePerm.selectRole")}</Text>
</div>
)}
</Card>
</Col>
</Row>
</SectionCard>
</PageContainer>
);
}