import { useEffect, useMemo, useRef, useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Layout, Menu, Typography, Button, Space, Badge } from 'antd'; import type { MenuProps } from 'antd'; import { RobotOutlined, DashboardOutlined, UserOutlined, ShoppingOutlined, ShopOutlined, TeamOutlined, GiftOutlined, CarOutlined, SafetyOutlined, LogoutOutlined, CloudUploadOutlined, FileTextOutlined, LockOutlined, SettingOutlined, AccountBookOutlined, ProjectOutlined, } from '@ant-design/icons'; import { hasAnySystemSettingsPermission } from '@dukang/shared-types'; import { clearAuth, request, type HqProfile } from '../lib/api'; import { bindAdminEllipsisTitle } from '../lib/ellipsis-title'; import { AUDIT_NOTICE_CHANGED_EVENT, PACKAGE_AUDIT_CHANGED_EVENT } from '../lib/admin-events'; const { Header, Sider, Content } = Layout; type MenuItem = NonNullable[number]; const MENU_ITEMS: MenuProps['items'] = [ { key: '/', icon: , label: '概览' }, { key: '/users', icon: , label: '用户' }, { key: '/wechat-bindings', icon: , label: '微信绑定' }, { key: 'products-group', icon: , label: '商品', children: [ { key: '/products', label: '商品列表' }, { key: '/product-detail-templates', label: '详情模板' }, ], }, { key: '/orders', icon: , label: '订单' }, { key: '/promo-codes', icon: , label: '推广码' }, { key: 'wecom-group', icon: , label: '企微机器人', children: [ { key: '/wecom/bots', label: '智能机器人' }, { key: '/wecom/pushes', label: '消息推送' }, ], }, { key: '/llm-configs', icon: , label: '语言模型' }, { key: '/knowledge-bases', icon: , label: '知识库' }, { key: 'stores-group', icon: , label: '门店', children: [ { key: '/stores', label: '门店列表' }, { key: '/store-package-audits', label: '审核通知' }, { key: '/store-ratings', label: '门店评价' }, { key: '/store-categories', label: '门店分类' }, { key: '/store-accounts', label: '门店账户' }, { key: '/store-media', label: '门店资源' }, ], }, { key: 'partners-group', icon: , label: '开城', children: [ { key: '/cities', label: '城市' }, { key: '/city-partners', label: '城市合伙人' }, { key: '/city-warehouses', label: '仓库' }, { key: '/fulfillment-providers', label: '仓配管理' }, ], }, { key: 'finance-group', icon: , label: '财务', children: [ { key: '/finance/store-bills', label: '门店账单' }, { key: '/finance/partner-bills', label: '合伙人账单' }, { key: '/finance/winery-bills', label: '酒厂账单' }, { key: '/finance/logistics-bills', label: '物流对账' }, ], }, { key: 'benefit-group', icon: , label: '好客权益', children: [ { key: '/benefit/coupons', label: '权益券' }, { key: '/benefit/ledgers', label: '流水' }, { key: '/redeem-records', label: '核销记录' }, { key: '/redeem-pending', label: '待处理核销' }, { key: '/redeem/debug', label: '核销调试' }, ], }, { key: 'deliveries-group', icon: , label: '配送单', children: [ { key: '/deliveries', label: '配送单列表' }, { key: '/deliveries/xiaofeixia', label: '小飞侠联调' }, ], }, { key: 'tickets-group', icon: , label: '工单', children: [ { key: '/tickets', label: '工单中心' }, { key: '/tickets/support', label: '技术支持' }, ], }, { key: 'dev-plan-group', icon: , label: '开发计划', children: [ { key: '/dev-plan/versions', label: '版本列表' }, { key: '/dev-plan/tasks', label: '任务列表' }, { key: '/dev-plan/settings', label: '开发设置' }, ], }, { key: '/invoices', icon: , label: '发票管理' }, { key: '/resources', icon: , label: 'OSS 资源库' }, { key: 'logs-group', icon: , label: '日志', children: [ { key: '/logs/users', label: '用户日志' }, { key: '/logs/stores', label: '商户日志' }, { key: '/logs/partners', label: '合伙人日志' }, { key: '/logs/hq', label: 'HQ 操作日志' }, { key: '/logs/third-party', label: '第三方日志' }, { key: '/logs/domain-events', label: '领域事件' }, { key: '/logs/wecom-bots', label: '智能机器人日志' }, ], }, { key: '/hq-permissions', icon: , label: '权限分配' }, { key: '/test-whitelist', icon: , label: '白名单管理' }, { key: '/system-settings', icon: , label: '系统设置' }, { key: '/hq-accounts', icon: , label: 'HQ账户' }, ]; function menuAllowed(key: string, permissionKeys: string[]): boolean { if (key === 'tickets-group') { return permissionKeys.includes('tickets') || permissionKeys.includes('tech_support'); } if (key === 'dev-plan-group') { return permissionKeys.includes('dev_plan'); } const map: Record = { '/': 'dashboard', '/users': 'users', '/wechat-bindings': 'wechat_bindings', 'products-group': 'products', '/products': 'products', '/product-detail-templates': 'products', '/orders': 'orders', '/promo-codes': 'promo_codes', '/wecom/bots': 'wecom_bots', '/wecom/pushes': 'wecom_bots', 'wecom-group': 'wecom_bots', '/llm-configs': 'llm_configs', '/knowledge-bases': 'knowledge_bases', 'stores-group': 'stores', '/stores': 'stores', '/store-package-audits': 'stores', '/store-ratings': 'stores', '/store-categories': 'stores', '/store-accounts': 'stores', '/store-media': 'stores', 'partners-group': 'partners', '/cities': 'partners', '/city-partners': 'partners', '/city-warehouses': 'partners', '/fulfillment-providers': 'partners', 'finance-group': 'finance', '/finance/store-bills': 'finance', '/finance/store-withdrawals': 'finance', '/finance/partner-bills': 'finance', '/finance/winery-bills': 'finance', '/finance/logistics-bills': 'finance', 'benefit-group': 'benefit', '/benefit/coupons': 'benefit', '/benefit/ledgers': 'benefit', '/redeem-records': 'benefit', '/redeem-pending': 'benefit', '/redeem/debug': 'benefit', 'deliveries-group': 'deliveries', '/deliveries': 'deliveries', '/deliveries/xiaofeixia': 'deliveries', '/tickets': 'tickets', '/tickets/support': 'tech_support', 'dev-plan-group': 'dev_plan', '/dev-plan/versions': 'dev_plan', '/dev-plan/tasks': 'dev_plan', '/dev-plan/settings': 'dev_plan', '/invoices': 'invoices', '/resources': 'resources', 'logs-group': 'logs', '/logs/users': 'logs', '/logs/wecom-bots': 'logs', '/logs/stores': 'logs', '/logs/partners': 'logs', '/logs/hq': 'logs', '/logs/third-party': 'logs', '/logs/domain-events': 'logs', '/hq-permissions': 'hq_permissions', '/test-whitelist': 'test_whitelist', '/system-settings': 'system_settings_any', '/hq-accounts': 'hq_accounts', }; const need = map[key]; if (!need) return true; if (need === 'system_settings_any') return hasAnySystemSettingsPermission(permissionKeys); return permissionKeys.includes(need); } function filterMenuItems(items: MenuProps['items'], permissionKeys: string[]): MenuProps['items'] { if (!items) return items; return items .map((item) => { if (!item || typeof item !== 'object' || !('key' in item)) return item; const key = String(item.key); if ('children' in item && Array.isArray(item.children)) { if (!menuAllowed(key, permissionKeys)) return null; const children = filterMenuItems(item.children as MenuProps['items'], permissionKeys); if (!children?.length) return null; return { ...item, children } as MenuItem; } return menuAllowed(key, permissionKeys) ? item : null; }) .filter(Boolean) as MenuProps['items']; } function attachAuditBadge(items: MenuProps['items'], pendingCount: number): MenuProps['items'] { if (!items) return items; return items.map((item) => { if (!item || typeof item !== 'object' || !('key' in item)) return item; if ('children' in item && Array.isArray(item.children)) { return { ...item, children: attachAuditBadge(item.children as MenuProps['items'], pendingCount), } as MenuItem; } if (String(item.key) === '/store-package-audits') { return { ...item, label: ( 审核通知 {pendingCount > 0 && } ), } as MenuItem; } return item; }); } const IS_STAGING = import.meta.env.VITE_APP_ENV === 'staging'; export default function AdminLayout() { const navigate = useNavigate(); const location = useLocation(); const contentRef = useRef(null); const [profile, setProfile] = useState(null); const [auditPendingCount, setAuditPendingCount] = useState(0); function refreshAuditPendingCount() { Promise.all([ request<{ pendingCount: number }>('/admin/store-package-audits/summary').catch(() => ({ pendingCount: 0 })), request<{ pendingCount: number; packagePendingCount?: number }>('/admin/store-info-change-requests/summary').catch(() => ({ pendingCount: 0 })), ]) .then(([pkg, info]) => setAuditPendingCount((pkg.pendingCount ?? 0) + (info.pendingCount ?? 0))) .catch(() => {}); } useEffect(() => { request('/admin/auth/me').then(setProfile).catch(() => {}); }, []); useEffect(() => { refreshAuditPendingCount(); }, [location.pathname]); useEffect(() => { const onChanged = () => refreshAuditPendingCount(); window.addEventListener(PACKAGE_AUDIT_CHANGED_EVENT, onChanged); window.addEventListener(AUDIT_NOTICE_CHANGED_EVENT, onChanged); return () => { window.removeEventListener(PACKAGE_AUDIT_CHANGED_EVENT, onChanged); window.removeEventListener(AUDIT_NOTICE_CHANGED_EVENT, onChanged); }; }, []); useEffect(() => { contentRef.current?.scrollTo({ top: 0, left: 0 }); }, [location.pathname]); useEffect(() => { return bindAdminEllipsisTitle(); }, []); function logout() { clearAuth(); navigate('/login'); } const selectedKey = location.pathname.startsWith('/promo-codes') ? '/promo-codes' : location.pathname; const menuItems = useMemo(() => { const base = !profile || profile.adminRole === 'SUPER_ADMIN' ? MENU_ITEMS : filterMenuItems(MENU_ITEMS, profile.permissionKeys ?? []); return attachAuditBadge(base, auditPendingCount); }, [profile, auditPendingCount]); return ( {IS_STAGING ? (
测试环境 · staging · 数据与生产隔离 · Mock 开启
) : null}
杜康 HQ{IS_STAGING ? ' · 测试' : ''}
{ if (key.startsWith('/')) navigate(key); }} />
管理后台 {profile?.name || '—'} {profile?.adminRole}
); }