import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button, Card, Form, Input, Tag, Select, InputNumber, Space, Popconfirm, Popover, Tooltip, TreeSelect } from "antd"; import { PlusOutlined, EditOutlined, DeleteOutlined, AppstoreOutlined, CaretRightOutlined, CaretDownOutlined } from "@ant-design/icons"; import ListTable from "../components/ListTable/ListTable"; import DetailDrawer from "../components/DetailDrawer/DetailDrawer"; import PageHeader from "../components/PageHeader/PageHeader"; import Toast from "../components/Toast/Toast"; import { api } from "../api"; import { clearTokens } from "../auth"; import { ICON_LIST, getIcon } from "../utils/icons"; interface PermItem { perm_id: number; parent_id: number | null; name: string; code: string; perm_type: string; level: number; path?: string | null; icon?: string | null; sort_order?: number; status?: number; children?: PermItem[]; } type DrawerMode = "create" | "edit"; export default function PermissionTreePage() { const navigate = useNavigate(); const [perms, setPerms] = useState([]); const [loading, setLoading] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); const [drawerMode, setDrawerMode] = useState("create"); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); const [iconPopoverOpen, setIconPopoverOpen] = useState(false); const [selectedIcon, setSelectedIcon] = useState(null); const load = async () => { try { setLoading(true); const res = await api.listPermissions(); const tree = buildTree(res as any); setPerms(tree); } catch { clearTokens(); navigate("/"); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const openCreate = (parentId: number | null = null) => { setDrawerMode("create"); setEditing(null); form.resetFields(); setSelectedIcon(null); form.setFieldsValue({ parent_id: parentId, level: parentId ? 2 : 1, perm_type: parentId ? 'menu' : 'menu', status: 1 }); setDrawerOpen(true); }; const openEdit = (row: PermItem) => { setDrawerMode("edit"); setEditing(row); setSelectedIcon(row.icon || null); form.setFieldsValue({ parent_id: row.parent_id, name: row.name, code: row.code, perm_type: row.perm_type, level: row.level, path: row.path || "", icon: row.icon || "", sort_order: row.sort_order || 0, status: row.status || 1, }); setDrawerOpen(true); }; const notifyMenuRefresh = () => { window.dispatchEvent(new CustomEvent('menu-refresh')); }; const submit = async () => { try { const values = await form.validateFields(); if (drawerMode === "create") { await api.createPermission(values); Toast.success("已创建"); } else if (editing) { await api.updatePermission(editing.perm_id, values); Toast.success("已更新"); } setDrawerOpen(false); load(); notifyMenuRefresh(); } catch (e) {} }; const remove = async (row: PermItem) => { await api.deletePermission(row.perm_id); Toast.success("已删除"); load(); notifyMenuRefresh(); }; const handleIconSelect = (iconName: string) => { form.setFieldsValue({ icon: iconName }); setSelectedIcon(iconName); setIconPopoverOpen(false); }; const iconContent = (
{ICON_LIST.map((item) => (
handleIconSelect(item.name)} style={{ cursor: 'pointer', padding: '8px 0', textAlign: 'center', borderRadius: 4, background: selectedIcon === item.name ? '#f0f7ff' : 'transparent', border: selectedIcon === item.name ? '1px solid #1890ff' : '1px solid transparent', fontSize: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s' }} onMouseEnter={(e) => { if (selectedIcon !== item.name) { e.currentTarget.style.backgroundColor = '#f5f5f5'; } }} onMouseLeave={(e) => { if (selectedIcon !== item.name) { e.currentTarget.style.backgroundColor = 'transparent'; } }} > {item.icon}
))}
); const columns = useMemo(() => [ { title: "名称", dataIndex: "name", key: "name" }, { title: "图标", dataIndex: "icon", key: "icon", width: 80, render: (v: string) => v ? {getIcon(v)} : '-' }, { title: "编码", dataIndex: "code", key: "code" }, { title: "类型", dataIndex: "perm_type", key: "perm_type", render: (v: string) => {v === 'menu' ? '菜单' : '按钮'} }, { title: "路径", dataIndex: "path", key: "path", render: (v: string | null) => v || "-" }, { title: "操作", key: "action", width: 200, render: (_: any, record: PermItem) => ( {record.level < 3 && ( )} remove(record)}> ) } ], [perms]); const treeData = useMemo(() => { const loop = (data: PermItem[], disabled = false): any[] => data.map((item) => { const isDisabled = disabled || editing?.perm_id === item.perm_id; return { title: item.name, value: item.perm_id, key: item.perm_id, disabled: isDisabled, children: item.children ? loop(item.children, isDisabled) : [], }; }); return [ { title: '无 (顶级菜单)', value: null, key: 'root', children: [] }, ...loop(perms) ]; }, [perms, editing]); return (
} onClick={() => openCreate()}> 新增权限 } />
columns={columns} dataSource={perms} rowKey="perm_id" loading={loading} pagination={false} scroll={{ x: 1000 }} expandable={{ expandIcon: ({ expanded, onExpand, record }) => { if (record.children && record.children.length > 0) { return expanded ? ( onExpand(record, e)} style={{ marginRight: 8, cursor: 'pointer', color: '#666', fontSize: '12px' }} /> ) : ( onExpand(record, e)} style={{ marginRight: 8, cursor: 'pointer', color: '#666', fontSize: '12px' }} /> ); } return ; } }} />
setDrawerOpen(false)} title={{ text: drawerMode === "create" ? "新增权限" : "编辑权限" }} headerActions={[ { key: "cancel", label: "取消", onClick: () => setDrawerOpen(false) }, { key: "submit", label: "保存", type: "primary", onClick: submit }, ]} width={450} >
} style={{ cursor: 'pointer' }} /> {selectedIcon && ( { e.stopPropagation(); handleIconSelect(''); }} > × )}
); } function buildTree(items: any[]): PermItem[] { const map = new Map(); const roots: PermItem[] = []; const rawItems = items.map(it => ({...it, children: []})); rawItems.forEach((it) => map.set(it.perm_id, it)); rawItems.forEach((it) => { if (it.parent_id) { const parent = map.get(it.parent_id); if (parent) { if (!parent.children) parent.children = []; parent.children.push(it); } else { roots.push(it); } } else { roots.push(it); } }); // Sort children by sort_order const sortFunc = (a: any, b: any) => (a.sort_order || 0) - (b.sort_order || 0); rawItems.forEach(it => { if (it.children && it.children.length > 0) { it.children.sort(sortFunc); } }); roots.sort(sortFunc); return roots; }