288 lines
9.9 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|