import { useState, useEffect, useCallback } from 'react'; import { Table, Button, Space, Modal, Form, Input, InputNumber, Select, Switch, TreeSelect, Popconfirm, message, Typography, Tag, } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import client from '../../api/client'; // --- Types --- interface MenuItem { id: string; parent_id?: string | null; title: string; path?: string; icon?: string; menu_type: 'directory' | 'menu' | 'button'; sort_order: number; visible: boolean; permission?: string; children?: MenuItem[]; } // --- Helpers --- /** Convert flat menu list to tree structure for Table children prop */ function buildMenuTree(items: MenuItem[]): MenuItem[] { const map = new Map(); const roots: MenuItem[] = []; const withChildren = items.map((item) => ({ ...item, children: [] as MenuItem[], })); withChildren.forEach((item) => map.set(item.id, item)); withChildren.forEach((item) => { if (item.parent_id && map.has(item.parent_id)) { map.get(item.parent_id)!.children!.push(item); } else { roots.push(item); } }); return roots; } /** Convert menu tree to TreeSelect data nodes */ function toTreeSelectData( items: MenuItem[], ): Array<{ title: string; value: string; children?: Array<{ title: string; value: string }> }> { return items.map((item) => ({ title: item.title, value: item.id, children: item.children && item.children.length > 0 ? toTreeSelectData(item.children) : undefined, })); } const menuTypeLabels: Record = { directory: { text: '目录', color: 'blue' }, menu: { text: '菜单', color: 'green' }, button: { text: '按钮', color: 'orange' }, }; // --- Component --- export default function MenuConfig() { const [menus, setMenus] = useState([]); const [menuTree, setMenuTree] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editMenu, setEditMenu] = useState(null); const [form] = Form.useForm(); const fetchMenus = useCallback(async () => { setLoading(true); try { const { data: resp } = await client.get('/config/menus'); const list: MenuItem[] = resp.data ?? resp; setMenus(list); setMenuTree(buildMenuTree(list)); } catch { message.error('加载菜单失败'); } setLoading(false); }, []); useEffect(() => { fetchMenus(); }, [fetchMenus]); const handleSubmit = async (values: { parent_id?: string; title: string; path?: string; icon?: string; menu_type: 'directory' | 'menu' | 'button'; sort_order: number; visible: boolean; permission?: string; }) => { try { if (editMenu) { await client.put(`/config/menus/${editMenu.id}`, values); message.success('菜单更新成功'); } else { await client.post('/config/menus', values); message.success('菜单创建成功'); } closeModal(); fetchMenus(); } catch (err: unknown) { const errorMsg = (err as { response?: { data?: { message?: string } } })?.response?.data ?.message || '操作失败'; message.error(errorMsg); } }; const handleDelete = async (id: string) => { try { await client.delete(`/config/menus/${id}`); message.success('菜单已删除'); fetchMenus(); } catch { message.error('删除失败'); } }; const openCreate = () => { setEditMenu(null); form.resetFields(); form.setFieldsValue({ menu_type: 'menu', sort_order: 0, visible: true, }); setModalOpen(true); }; const openEdit = (menu: MenuItem) => { setEditMenu(menu); form.setFieldsValue({ parent_id: menu.parent_id || undefined, title: menu.title, path: menu.path, icon: menu.icon, menu_type: menu.menu_type, sort_order: menu.sort_order, visible: menu.visible, permission: menu.permission, }); setModalOpen(true); }; const closeModal = () => { setModalOpen(false); setEditMenu(null); form.resetFields(); }; const columns = [ { title: '标题', dataIndex: 'title', key: 'title', width: 200 }, { title: '路径', dataIndex: 'path', key: 'path', ellipsis: true, render: (v?: string) => v || '-', }, { title: '图标', dataIndex: 'icon', key: 'icon', width: 100, render: (v?: string) => v || '-', }, { title: '类型', dataIndex: 'menu_type', key: 'menu_type', width: 90, render: (v: string) => { const info = menuTypeLabels[v] ?? { text: v, color: 'default' }; return {info.text}; }, }, { title: '排序', dataIndex: 'sort_order', key: 'sort_order', width: 80, }, { title: '可见', dataIndex: 'visible', key: 'visible', width: 80, render: (v: boolean) => v ? : , }, { title: '操作', key: 'actions', width: 150, render: (_: unknown, record: MenuItem) => ( handleDelete(record.id)} > ), }, ]; return (
菜单配置
form.submit()} width={560} >
); }