'use client'; import { useEffect, useRef, useState } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { BarChartOutlined, DashboardOutlined, DatabaseOutlined, FileSearchOutlined, FlagOutlined, GiftOutlined, HeartOutlined, LineChartOutlined, LogoutOutlined, MessageOutlined, MoneyCollectOutlined, NotificationOutlined, ProfileOutlined, SafetyCertificateOutlined, SettingOutlined, ShareAltOutlined, TeamOutlined, UserOutlined, } from '@ant-design/icons'; import { Avatar, Dropdown, Layout, Tooltip } from 'antd'; import { api } from '@/lib/api'; import { clearAuth, getAdmin, getToken, setAuth } from '@/lib/auth'; import { REVIEW_BADGE_REFRESH_EVENT, type ReviewBadgeKey, } from '@/lib/reviewBadge'; import type { AdminInfo, FeedbackSummary, PriceReportSummary, WithdrawSummary, } from '@/lib/types'; const { Sider, Header, Content } = Layout; type NavItem = { key: string; icon: React.ReactNode; label: string; }; type NavGroup = | (NavItem & { children?: never }) | { key: string; icon: React.ReactNode; label: string; children: NavItem[]; }; const hasChildren = (group: NavGroup): group is NavGroup & { children: NavItem[] } => Array.isArray(group.children); const NAV_GROUPS: NavGroup[] = [ { key: 'dashboard', icon: , label: '看板', children: [ { key: '/dashboard', icon: , label: '数据大盘' }, { key: '/ad-revenue-report', icon: , label: '广告收益' }, { key: '/comparison-records', icon: , label: '比价记录' }, { key: '/coupon-data', icon: , label: '领券记录' }, { key: '/cps', icon: , label: 'CPS收益' }, ], }, { key: 'reward-review', icon: , label: '奖励审核', children: [ { key: '/withdraws', icon: , label: '提现审核' }, { key: '/price-reports', icon: , label: '低价审核' }, { key: '/feedbacks', icon: , label: '用户反馈' }, ], }, { key: 'data-config', icon: , label: '数据配置', children: [ { key: '/config', icon: , label: '系统配置' }, { key: '/ad-revenue', icon: , label: '广告配置' }, { key: '/huawei-review', icon: , label: '华为审核开关' }, { key: '/users', icon: , label: '用户管理' }, ], }, { key: 'monitoring-audit', icon: , label: '监控审计', children: [ { key: '/device-liveness', icon: , label: '设备存活' }, { key: '/analytics-health', icon: , label: '埋点成功率' }, { key: '/event-logs', icon: , label: '埋点日志' }, { key: '/audit-logs', icon: , label: '审计日志' }, ], }, { key: '/admins', icon: , label: '权限管理' }, ]; // 导航项 key(/dashboard)→ 权限页 key(dashboard),与后端 permissions.py 目录对齐 const permOf = (key: string) => key.replace(/^\//, ''); export default function MainLayout({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const [admin, setAdmin] = useState(null); const [collapsed, setCollapsed] = useState(false); const [pendingReviewCounts, setPendingReviewCounts] = useState>>({}); const reviewRefreshVersion = useRef>>({}); useEffect(() => { const token = getToken(); if (!token) { router.replace('/login'); return; } setAdmin(getAdmin()); // 先用本地存的(快、不闪) // 再拉 /me 刷新有效可见页:超管改过该角色权限也能及时反映到左侧导航 api .get('/admin/api/auth/me') .then(({ data }) => { setAdmin(data); setAuth(token, data); }) .catch(() => {}); }, [router]); useEffect(() => { if (!admin) return; const canAccess = (page: string) => admin.role === 'super_admin' || !admin.pages || admin.pages.includes(page); const refresh = async (key: ReviewBadgeKey) => { if (!canAccess(key.slice(1))) return; const version = (reviewRefreshVersion.current[key] ?? 0) + 1; reviewRefreshVersion.current[key] = version; let count: number; if (key === '/withdraws') { count = (await api.get('/admin/api/withdraws/summary')).data.reviewing_count; } else if (key === '/price-reports') { count = (await api.get('/admin/api/price-reports/summary')).data.pending; } else { count = (await api.get('/admin/api/feedbacks/summary')).data.pending; } if (reviewRefreshVersion.current[key] !== version) return; setPendingReviewCounts((current) => ({ ...current, [key]: count })); }; const refreshSafely = (key: ReviewBadgeKey) => refresh(key).catch(() => {}); void Promise.all([ refreshSafely('/withdraws'), refreshSafely('/price-reports'), refreshSafely('/feedbacks'), ]); const onReviewCompleted = (event: Event) => { const key = (event as CustomEvent).detail; if (key) void refreshSafely(key); }; window.addEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); return () => { window.removeEventListener(REVIEW_BADGE_REFRESH_EVENT, onReviewCompleted); }; }, [admin]); if (!admin) return null; // 守卫期间不闪烁内容 // 选中态:取路径一级(/users/123 -> /users) const selectedKey = '/' + (pathname.split('/')[1] || 'dashboard'); // 叶子导航项按当前角色的有效可见页过滤:super_admin 恒可见;缺 pages 信息(旧登录)兜底全显示不锁死。 const isSuper = admin.role === 'super_admin'; const canShowLeaf = (item: { key: string }) => isSuper || !admin.pages || admin.pages.includes(permOf(item.key)); const visibleGroups = NAV_GROUPS .map((group) => { if (!hasChildren(group)) return group; return { ...group, children: group.children.filter(canShowLeaf) }; }) .filter((group) => (hasChildren(group) ? group.children.length > 0 : canShowLeaf(group))); const flatNavItems = visibleGroups.flatMap((group) => hasChildren(group) ? group.children : [group], ); const reviewBadge = (key: string) => { const count = pendingReviewCounts[key as ReviewBadgeKey] ?? 0; if (count <= 0) return null; return ( {count} ); }; const logout = () => { clearAuth(); router.replace('/login'); }; return ( {collapsed ? flatNavItems.map((item) => ( router.push(item.key)} aria-label={item.label} > {item.icon} {reviewBadge(item.key)} )) : visibleGroups.map((group) => { const isGroup = hasChildren(group); const groupSelected = isGroup ? group.children.some((child) => child.key === selectedKey) : selectedKey === group.key; if (!isGroup) { return ( router.push(group.key)} > {group.icon} {group.label} {reviewBadge(group.key)} ); } return ( {group.icon} {group.label} {group.children.map((child) => ( router.push(child.key)} > {child.icon} {child.label} {reviewBadge(child.key)} ))} ); })} , label: '退出登录', onClick: logout }, ], }} > } /> {admin.username}({admin.role}) {children} ); }