import React, { useEffect, useState } from 'react'; import { Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography, } from 'antd'; import { MenuOutlined, PlusOutlined, EditOutlined, DeleteOutlined, HomeOutlined, PlayCircleOutlined, WalletOutlined, RobotOutlined, SettingOutlined, BellOutlined, UserOutlined, AppstoreOutlined, FileTextOutlined, StarOutlined, HeartOutlined, CameraOutlined, DashboardOutlined, CalculatorOutlined, DollarOutlined, GiftOutlined, ThunderboltOutlined, FireOutlined, CloudOutlined, SmileOutlined, TrophyOutlined, RocketOutlined, BulbOutlined, CodeOutlined, PictureOutlined, VideoCameraOutlined, AudioOutlined, MailOutlined, PhoneOutlined, GlobalOutlined, ShoppingCartOutlined, TeamOutlined, BarChartOutlined, PieChartOutlined, LineChartOutlined, SecurityScanOutlined, ApiOutlined, DatabaseOutlined, CloudServerOutlined, } from '@ant-design/icons'; import { getMenuConfigs, saveMenuConfig, deleteMenuConfig } from '../api'; const ICON_MAP: Record = { HomeOutlined: , PlayCircleOutlined: , WalletOutlined: , RobotOutlined: , SettingOutlined: , BellOutlined: , UserOutlined: , AppstoreOutlined: , FileTextOutlined: , StarOutlined: , HeartOutlined: , CameraOutlined: , DashboardOutlined: , CalculatorOutlined: , DollarOutlined: , GiftOutlined: , ThunderboltOutlined: , FireOutlined: , CloudOutlined: , SmileOutlined: , TrophyOutlined: , RocketOutlined: , BulbOutlined: , CodeOutlined: , PictureOutlined: , VideoCameraOutlined: , AudioOutlined: , MailOutlined: , PhoneOutlined: , GlobalOutlined: , ShoppingCartOutlined: , TeamOutlined: , BarChartOutlined: , PieChartOutlined: , LineChartOutlined: , SecurityScanOutlined: , ApiOutlined: , DatabaseOutlined: , CloudServerOutlined: , MenuOutlined: , }; const ICON_OPTIONS = Object.keys(ICON_MAP).map(key => ({ value: key, label: {ICON_MAP[key]} {key.replace('Outlined', '')}, })); const TYPE_COLORS: Record = { page: 'blue', group: 'purple' }; const TYPE_LABELS: Record = { page: '页面', group: '分组' }; const AdminMenuConfig: React.FC = () => { const [menus, setMenus] = useState([]); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('frontend'); const [modal, setModal] = useState<{ open: boolean; menu: any | null }>({ open: false, menu: null }); const [form] = Form.useForm(); const load = async () => { setLoading(true); try { const data = await getMenuConfigs(); setMenus(data); } catch { /* auth error handled by client */ } setLoading(false); }; useEffect(() => { load(); }, []); const handleSave = async () => { try { const values = await form.validateFields(); const payload = { label: values.label, path: values.path || '', icon: values.icon || '', sort_order: values.sortOrder ?? 0, is_active: values.isActive ?? true, parent_id: values.parentId || null, menu_type: values.menuType || 'page', menu_target: values.menuTarget || 'frontend', is_default: values.isDefault ?? false, }; if (modal.menu?.id) { await saveMenuConfig({ ...payload, id: modal.menu.id }); } else { await saveMenuConfig(payload); } message.success(modal.menu?.id ? '菜单已更新' : '菜单已添加'); setModal({ open: false, menu: null }); form.resetFields(); load(); } catch { /* validation */ } }; const handleDelete = async (id: string) => { try { await deleteMenuConfig(id); message.success('菜单已删除'); load(); } catch (e: any) { message.error(e?.message || '删除失败'); } }; const openEdit = (menu?: any) => { setModal({ open: true, menu: menu || null }); if (menu) { form.setFieldsValue({ label: menu.label, path: menu.path, icon: menu.icon, sortOrder: menu.sortOrder ?? 0, isActive: menu.isActive ?? true, parentId: menu.parentId ?? '', menuType: menu.menuType ?? 'page', menuTarget: menu.menuTarget ?? 'frontend', isDefault: menu.isDefault ?? false, }); } else { form.resetFields(); form.setFieldsValue({ icon: 'HomeOutlined', sortOrder: menus.length, isActive: true, menuType: 'page', menuTarget: activeTab, isDefault: false }); } }; // Filter menus by active tab const filteredMenus = menus.filter(m => { const target = m.menuTarget ?? 'frontend'; return target === activeTab || target === 'both'; }); // Build tree: groups first, then pages under groups const groups = filteredMenus.filter(m => m.menuType === 'group'); const parentOptions = [ { value: '', label: '顶级菜单' }, ...groups.map(g => ({ value: g.id, label: g.label })), ]; // Build flat display with indentation const displayMenus: any[] = []; const topLevel = filteredMenus.filter(m => !m.parentId); const childMap: Record = {}; filteredMenus.filter(m => m.parentId).forEach(m => { const pid = m.parentId; if (!childMap[pid]) childMap[pid] = []; childMap[pid].push(m); }); topLevel.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)).forEach(m => { displayMenus.push({ ...m, _depth: 0 }); (childMap[m.id] || []).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)).forEach(c => { displayMenus.push({ ...c, _depth: 1 }); }); }); const columns = [ { title: '排序', dataIndex: 'sortOrder', width: 60 }, { title: '菜单名称', key: 'label', width: 180, render: (_: any, r: any) => ( {r._depth === 1 && } {r.label} ), }, { title: '路由路径', dataIndex: 'path', width: 160, render: (v: string) => v || - }, { title: '图标', dataIndex: 'icon', width: 100, render: (v: string) => v && ICON_MAP[v] ? ( {ICON_MAP[v]} ) : '-', }, { title: '类型', dataIndex: 'menuType', width: 80, render: (v: string) => {TYPE_LABELS[v] || v}, }, { title: '状态', dataIndex: 'isActive', width: 70, render: (v: boolean) => ( {v ? '启用' : '停用'} ), }, ...(activeTab === 'frontend' ? [{ title: '默认显示', dataIndex: 'isDefault', width: 80, render: (v: boolean) => v ? 默认 : -, }] : []), { title: '操作', key: 'action', width: 150, render: (_: any, r: any) => ( handleDelete(r.id)}> ), }, ]; return (
菜单配置
{modal.menu?.id ? '编辑菜单' : '添加菜单'}} open={modal.open} onOk={handleSave} onCancel={() => { setModal({ open: false, menu: null }); form.resetFields(); }} okText="确认" cancelText="取消" width={520} >
prev.menuTarget !== cur.menuTarget}> {({ getFieldValue }) => getFieldValue('menuTarget') !== 'admin' ? ( ) : null }
); }; export default AdminMenuConfig;