import { useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' import { Outlet, useLocation, useNavigate } from 'react-router-dom' import { Layout, Dropdown, Form, Input, Modal, Space, Typography, Avatar, Tag, Button, message, Tooltip, } from 'antd' import { ApiOutlined, AppstoreOutlined, CodeOutlined, DashboardOutlined, DownOutlined, FileTextOutlined, KeyOutlined, LogoutOutlined, MenuOutlined, OrderedListOutlined, ShopOutlined, TeamOutlined, UserOutlined, WalletOutlined, } from '@ant-design/icons' import { useAuth } from '../store/auth' import { authApi } from '../api' import type { MenuProps } from 'antd' const { Header, Sider, Content } = Layout interface SidebarChild { key: string label: string path?: string disabled?: boolean permission?: string feature?: string } interface SidebarSection { key: string label: string icon: ReactNode path?: string disabled?: boolean children?: SidebarChild[] } const adminSections: SidebarSection[] = [ { key: 'dashboard', label: '工作台', icon: , path: '/', }, { key: 'products', label: '商品管理', icon: , children: [ { key: 'merchant-products', label: '商品列表', path: '/merchant-products' }, { key: 'platform-products', label: '渠道与路由配置', path: '/platform-products' }, ], }, { key: 'shops', label: '商户管理', icon: , children: [{ key: 'shop-list', label: '平台商户', path: '/platform-merchants' }], }, { key: 'accounts', label: '账号管理', icon: , children: [{ key: 'platform-users', label: '平台账号', path: '/platform-users' }], }, { key: 'orders', label: '订单管理', icon: , children: [{ key: 'fulfillment-orders', label: '发货订单', path: '/merchant-orders' }], }, { key: 'staff', label: '员工管理', icon: , children: [{ key: 'merchant-members', label: '成员管理', path: '/merchant-members' }], }, { key: 'funds', label: '积分管理', icon: , children: [ { key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet' }, { key: 'merchant-recharge', label: '积分充值', path: '/merchant-recharge' }, { key: 'merchant-alert-settings', label: '余额告警', path: '/merchant-alert-settings' }, ], }, { key: 'open-api', label: '开放 API', icon: , children: [ { key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' }, { key: 'api-callbacks', label: '回调订阅', path: '/merchant-callbacks' }, { key: 'api-docs', label: '对接文档', path: '/open-api' }, { key: 'api-debug', label: '调用调试', path: '/api-debug' }, ], }, ] const merchantSections: SidebarSection[] = [ { key: 'dashboard', label: '工作台', icon: , path: '/', }, { key: 'products', label: '商品管理', icon: , children: [{ key: 'merchant-products', label: '商品列表', path: '/merchant-products', permission: 'products:manage', feature: 'products' }], }, { key: 'orders', label: '订单管理', icon: , children: [{ key: 'fulfillment-orders', label: '发货订单', path: '/merchant-orders', permission: 'orders:manage', feature: 'orders' }], }, { key: 'funds', label: '积分管理', icon: , children: [ { key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet', permission: 'wallet:ledger', feature: 'wallet' }, { key: 'merchant-recharge', label: '积分充值', path: '/merchant-recharge', permission: 'wallet:view', feature: 'wallet' }, { key: 'merchant-alert-settings', label: '余额告警', path: '/merchant-alert-settings', permission: 'recharge:manage', feature: 'wallet' }, ], }, { key: 'staff', label: '员工管理', icon: , children: [{ key: 'merchant-members', label: '成员管理', path: '/merchant-members', permission: 'members:manage' }], }, { key: 'open-api', label: '开放 API', icon: , children: [ { key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys', permission: 'api:manage', feature: 'api' }, { key: 'api-callbacks', label: '回调订阅', path: '/merchant-callbacks', permission: 'callbacks:manage', feature: 'callbacks' }, { key: 'api-docs', label: '对接文档', path: '/open-api', permission: 'api:manage', feature: 'api' }, ], }, ] function getSelectedKey(pathname: string, search: string) { if (pathname === '/') return 'dashboard' if (pathname.startsWith('/merchant-products')) return 'merchant-products' if (pathname.startsWith('/merchant-orders')) return 'fulfillment-orders' if (pathname.startsWith('/merchant-wallet')) return 'merchant-wallet' if (pathname.startsWith('/merchant-recharge')) return 'merchant-recharge' if (pathname.startsWith('/merchant-alert-settings')) return 'merchant-alert-settings' if (pathname.startsWith('/merchant-members')) return 'merchant-members' if (pathname.startsWith('/merchant-callbacks')) return 'api-callbacks' if (pathname.startsWith('/merchant-api-keys')) return 'api-keys' if (pathname.startsWith('/merchant-center')) { const tab = new URLSearchParams(search).get('tab') const tabKeys: Record = { products: 'merchant-products', orders: 'fulfillment-orders', wallet: 'merchant-wallet', members: 'merchant-members', callbacks: 'api-callbacks', } return tab ? tabKeys[tab] || 'merchant-products' : 'merchant-products' } if (pathname.startsWith('/open-api')) return 'api-docs' if (pathname.startsWith('/api-debug')) return 'api-debug' if (pathname.startsWith('/platform-merchants')) return 'shop-list' if (pathname.startsWith('/platform-users')) return 'platform-users' return 'dashboard' } function getInitialOpenKeys(sections: SidebarSection[], selectedKey: string) { return sections .filter((section) => section.children?.some((child) => child.key === selectedKey)) .map((section) => section.key) } const SIDEBAR_OPEN_KEYS_KEY = 'sidebar_open_keys' const SIDEBAR_COLLAPSED_KEY = 'sidebar_collapsed' function loadStoredOpenKeys(sections: SidebarSection[], selectedKey: string): string[] { const initial = getInitialOpenKeys(sections, selectedKey) try { const raw = localStorage.getItem(SIDEBAR_OPEN_KEYS_KEY) if (raw) { const parsed = JSON.parse(raw) if (Array.isArray(parsed)) { return Array.from(new Set([...parsed, ...initial])) } } } catch (e) { // ignore } return initial } export default function MainLayout() { const [collapsed, setCollapsed] = useState(() => { return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === 'true' }) const { user, logout, refreshProfile, isAdmin, merchant, merchantPermissions } = useAuth() const [passwordOpen, setPasswordOpen] = useState(false) const [passwordSaving, setPasswordSaving] = useState(false) const [passwordForm] = Form.useForm() const navigate = useNavigate() const location = useLocation() const isDocsPage = location.pathname.startsWith('/open-api') const sections = useMemo(() => { if (isAdmin) return adminSections const features = new Set((merchant?.features || '').split(/[\s,]+/).filter(Boolean)) return merchantSections.flatMap((section) => { if (!section.children) return [section] const children = section.children.filter((child) => ( (!child.permission || merchantPermissions.includes(child.permission)) && (!child.feature || features.has(child.feature)) )) return children.length > 0 ? [{ ...section, children }] : [] }) }, [isAdmin, merchant?.features, merchantPermissions]) const selectedKey = getSelectedKey(location.pathname, location.search) const [openKeys, setOpenKeys] = useState(() => loadStoredOpenKeys(sections, selectedKey)) useEffect(() => { const nextOpenKeys = getInitialOpenKeys(sections, selectedKey) setOpenKeys((current) => { const merged = Array.from(new Set([...current, ...nextOpenKeys])) localStorage.setItem(SIDEBAR_OPEN_KEYS_KEY, JSON.stringify(merged)) return merged }) }, [sections, selectedKey]) const toggleSection = (section: SidebarSection) => { if (section.disabled) return if (section.path) { navigate(section.path) return } if (collapsed) { const firstEnabledChild = section.children?.find((child) => child.path && !child.disabled) if (firstEnabledChild?.path) navigate(firstEnabledChild.path) return } setOpenKeys((current) => { const next = current.includes(section.key) ? current.filter((key) => key !== section.key) : [...current, section.key] localStorage.setItem(SIDEBAR_OPEN_KEYS_KEY, JSON.stringify(next)) return next }) } const handleToggleCollapse = (next: boolean) => { setCollapsed(next) localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(next)) } const handleChildClick = (child: SidebarChild) => { if (!child.path || child.disabled) return navigate(child.path) } const submitAccountUpdate = async () => { try { const values = await passwordForm.validateFields() setPasswordSaving(true) await authApi.updateCurrentAccount({ current_password: values.current_password, username: values.username, nickname: values.nickname || '', new_password: values.new_password || undefined, }) await refreshProfile() setPasswordOpen(false) passwordForm.resetFields() message.success('账户设置已保存') } catch (e) { message.error(e instanceof Error ? e.message : '保存失败') } finally { setPasswordSaving(false) } } const userMenu: MenuProps['items'] = [ { key: 'user-info', disabled: true, label: (
{user?.nickname || user?.username}
角色:{isAdmin ? '平台超级管理员' : '商户成员'}
), }, { type: 'divider' }, { key: 'account-settings', icon: , label: '账户设置', onClick: () => { passwordForm.setFieldsValue({ username: user?.username || '', nickname: user?.nickname || '' }) setPasswordOpen(true) }, }, { key: 'logout', icon: , label: 退出登录, onClick: () => { logout() navigate('/login') }, }, ] return (
S {!collapsed ? Skin Hub : null}
{isAdmin && ( )}
} /> {user?.nickname || user?.username} {isAdmin ? ( 管理员 ) : ( 商户 )}
{ setPasswordOpen(false) passwordForm.resetFields() }} destroyOnClose >
({ validator: (_, value) => !value || getFieldValue('new_password') === value ? Promise.resolve() : Promise.reject(new Error('两次输入的密码不一致')) }), ]}>
) }