/** * @name 侧边菜单 * * 参考资料: * - /rules/development-guide.md * - /rules/axure-api-guide.md * - /docs/设计规范.UIGuidelines.md */ import './style.css'; import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { ChevronDown, ChevronRight, LayoutDashboard, PanelLeftClose, PanelLeftOpen, Settings, ShoppingBag, User, type LucideIcon, } from 'lucide-react'; import { getNewlyOpenedSubmenuKey } from './side-menu-utils'; type MenuItemInput = { key?: string; label?: string; icon?: string; disabled?: boolean; children?: MenuItemInput[]; }; type SideMenuProps = { title?: string; width?: number; collapsible?: boolean; defaultCollapsed?: boolean; defaultSelectedKey?: string; defaultOpenKeys?: string[]; items?: MenuItemInput[]; onMenuSelect?: (key: string) => void; onCollapseChange?: (collapsed: boolean) => void; }; function resolveIcon(name?: string): LucideIcon | null { switch (name) { case 'dashboard': return LayoutDashboard; case 'shop': return ShoppingBag; case 'user': return User; case 'setting': return Settings; default: return null; } } function normalizeItems(items: any): MenuItemInput[] { if (!Array.isArray(items)) { return []; } return items.map(function (it: any) { return { key: typeof it?.key === 'string' && it.key ? it.key : String(it?.key ?? it?.label ?? ''), label: typeof it?.label === 'string' ? it.label : String(it?.label ?? ''), icon: typeof it?.icon === 'string' ? it.icon : undefined, disabled: it?.disabled === true, children: Array.isArray(it?.children) ? normalizeItems(it.children) : undefined }; }).filter(function (it: MenuItemInput) { return !!it.key; }); } const DEFAULT_ITEMS: MenuItemInput[] = [ { key: 'dashboard', label: '仪表盘', icon: 'dashboard' }, { key: 'orders', label: '订单管理', icon: 'shop', children: [ { key: 'orders_list', label: '订单列表' }, { key: 'orders_refund', label: '退款管理' } ] }, { key: 'users', label: '用户管理', icon: 'user' }, { key: 'settings', label: '系统设置', icon: 'setting' } ]; const Component = function SideMenu(props: SideMenuProps) { const title = typeof props.title === 'string' && props.title ? props.title : 'Axhub'; const width = typeof props.width === 'number' && props.width > 0 ? props.width : 240; const collapsible = props.collapsible !== false; const defaultCollapsed = props.defaultCollapsed === true; const defaultSelectedKey = typeof props.defaultSelectedKey === 'string' && props.defaultSelectedKey ? props.defaultSelectedKey : 'dashboard'; const defaultOpenKeys = Array.isArray(props.defaultOpenKeys) ? props.defaultOpenKeys.map(String).filter(Boolean) : []; const normalizedItems = useMemo(function () { const fromProps = normalizeItems(props.items); return fromProps.length > 0 ? fromProps : DEFAULT_ITEMS; }, [props.items]); const collapsedState = useState(defaultCollapsed); const collapsed = collapsedState[0]; const setCollapsed = collapsedState[1]; const selectedKeyState = useState(defaultSelectedKey); const selectedKey = selectedKeyState[0]; const setSelectedKey = selectedKeyState[1]; const openKeysState = useState(defaultOpenKeys); const openKeys = openKeysState[0]; const setOpenKeys = openKeysState[1]; useEffect(function syncSelectedKey() { setSelectedKey(defaultSelectedKey); }, [defaultSelectedKey]); useEffect(function syncOpenKeys() { setOpenKeys(defaultOpenKeys); }, [defaultOpenKeys]); const handleToggleCollapsed = useCallback(function () { setCollapsed(function (prev) { const next = !prev; if (typeof props.onCollapseChange === 'function') { props.onCollapseChange(next); } return next; }); }, [props]); const handleMenuClick = useCallback(function (info: any) { const key = typeof info?.key === 'string' ? info.key : String(info?.key ?? ''); if (!key) { return; } setSelectedKey(key); if (typeof props.onMenuSelect === 'function') { props.onMenuSelect(key); } }, [props]); const handleOpenChange = useCallback(function (keys: any) { const next = Array.isArray(keys) ? keys.map(String) : []; const openedKey = getNewlyOpenedSubmenuKey(openKeys, next); setOpenKeys(next); if (openedKey && typeof props.onMenuSelect === 'function') { props.onMenuSelect(openedKey); } }, [openKeys, props]); const renderMenuItems = useCallback(function renderMenuItems(items: MenuItemInput[], level = 0): React.ReactNode { return (
    {items.map(function (item) { const hasChildren = Array.isArray(item.children) && item.children.length > 0; const Icon = resolveIcon(item.icon); const isOpen = item.key ? openKeys.includes(item.key) : false; const isSelected = item.key === selectedKey; const itemClassName = [ 'axhub-side-menu__item', isSelected ? 'axhub-side-menu__item--selected' : '', item.disabled ? 'axhub-side-menu__item--disabled' : '', hasChildren ? 'axhub-side-menu__item--parent' : '', ].filter(Boolean).join(' '); return (
  • {!collapsed && hasChildren && isOpen && renderMenuItems(item.children || [], level + 1)}
  • ); })}
); }, [collapsed, handleMenuClick, handleOpenChange, openKeys, selectedKey]); return ( ); }; export default Component;