import { useCallback, useState } from 'react'; import { Activity, AlertTriangle, BarChart3, Building2, FileCheck2, FileText, FileSpreadsheet, FilePenLine, Gauge, Hash, ImageIcon, ListChecks, Layers3, MessageSquare, Phone, RefreshCw, TrendingUp, RadioTower, ReceiptText, ClipboardList, Send, ServerCog, ScanSearch, Settings, Shield, ShieldOff, ShieldCheck, Users, UserX, } from 'lucide-react'; import { adminApi, type PendingAuditCounts } from '@/api/adminApi'; import { getLastUserActivityAt, readSession, type LoginSession } from '@/api/session'; import { AppShell } from '@/layouts/AppShell'; import { PortalSessionBoundary } from '@/layouts/PortalSessionBoundary'; import { request } from '@/api/core/httpClient'; import { settleNotificationRequest, useNotificationPolling } from './useNotificationPolling'; const EMPTY_PENDING_AUDITS: Omit = { enterpriseCertifications: 0, smsAudits: 0, templates: 0, signatures: 0, drainageInfos: 0, }; type NotificationValue = { value: T; status: 'loading' | 'ready' | 'unavailable'; hasValue: boolean }; function initialNotification(value: T): NotificationValue { return { value, status: 'loading', hasValue: false }; } function updateNotification( previous: NotificationValue, result: PromiseSettledResult, available = true, ): NotificationValue { return result.status === 'fulfilled' && available ? { value: result.value, status: 'ready', hasValue: true } : { ...previous, status: 'unavailable' }; } function notificationDescription(notification: NotificationValue, description: string) { if (notification.status === 'loading') return '计数加载中'; if (notification.status === 'unavailable') { return notification.hasValue ? '计数暂不可用,显示上次结果' : '计数暂不可用,尚未取得结果'; } return description; } function canPollNotifications(userId: string) { const session = readSession('admin'); return Boolean( session && session.user.id === userId && !session.locked && Date.now() - getLastUserActivityAt() < session.idleTimeoutSeconds * 1000, ); } export function AdminLayout() { return ( {(session) => } ); } function AdminAuthenticatedLayout({ session }: { session: LoginSession }) { const [pendingAudits, setPendingAudits] = useState(() => initialNotification(EMPTY_PENDING_AUDITS)); const [retirementSummary, setRetirementSummary] = useState(() => initialNotification({ count: 0 })); const [reportSummary, setReportSummary] = useState(() => initialNotification({ count: 0 })); const [monitorSummary, setMonitorSummary] = useState(() => initialNotification({ count: 0 })); const [securityAlertSummary, setSecurityAlertSummary] = useState(() => initialNotification({ count: 0, criticalCount: 0 }), ); const [infrastructureAlertSummary, setInfrastructureAlertSummary] = useState(() => initialNotification({ count: 0, criticalCount: 0 }), ); const [sessionLocked, setSessionLocked] = useState(Boolean(session.locked)); const canPoll = useCallback(() => canPollNotifications(session.user.id), [session.user.id]); const loadPendingAuditCount = useCallback(async (signal: AbortSignal, isCurrent: () => boolean) => { // This runs globally and on a timer, so it must not fan out through the full dashboard aggregation. const results = await Promise.allSettled([ settleNotificationRequest(adminApi.getPendingAudits(undefined, signal), signal), settleNotificationRequest(adminApi.getSignatureRetirementUnreadCount(signal), signal), settleNotificationRequest(adminApi.getSecurityNotificationSummary(signal), signal), settleNotificationRequest(adminApi.getInfrastructureMonitoringNotificationSummary(signal), signal), settleNotificationRequest(request<{ count: number }>('/admin/report-notifications/summary', { signal }), signal), settleNotificationRequest( request<{ count: number; unavailable?: boolean }>('/admin/sending-monitor/notification-summary', { signal }), signal, ), ]); if (!isCurrent()) return false; const [audits, retirement, security, infrastructure, reporting, monitor] = results; const monitorAvailable = monitor.status === 'fulfilled' && !monitor.value.unavailable; setPendingAudits((previous) => updateNotification(previous, audits)); setRetirementSummary((previous) => updateNotification(previous, retirement)); setSecurityAlertSummary((previous) => updateNotification(previous, security)); setInfrastructureAlertSummary((previous) => updateNotification(previous, infrastructure)); setReportSummary((previous) => updateNotification(previous, reporting)); setMonitorSummary((previous) => updateNotification(previous, monitor, monitorAvailable)); return results.every((result) => result.status === 'fulfilled') && monitorAvailable; }, []); useNotificationPolling({ enabled: session.portal === 'admin' && !sessionLocked, canPoll, poll: loadPendingAuditCount, }); const auditLabel = (label: string) => pendingAudits.status === 'ready' ? label : `${label}(${pendingAudits.status === 'loading' ? '计数加载中' : '计数暂不可用'})`; return ( 0 ? `${securityAlertSummary.value.criticalCount} 条严重告警待处置` : '待处置安全告警', ), to: '/admin/security-detection', }, { label: '系统监控告警', count: infrastructureAlertSummary.value.count, description: notificationDescription( infrastructureAlertSummary, infrastructureAlertSummary.value.criticalCount > 0 ? `${infrastructureAlertSummary.value.criticalCount} 条 Prometheus 严重告警` : 'Prometheus 活动告警', ), to: '/admin/system-monitoring#active-alerts', }, ]} auditNotifications={[ { label: auditLabel('企业认证待审'), count: pendingAudits.value.enterpriseCertifications, to: '/admin/enterprise-audit', }, { label: auditLabel('短信审核待审'), count: pendingAudits.value.smsAudits, to: '/admin/sms-audit' }, { label: auditLabel('模板待审'), count: pendingAudits.value.templates, to: '/admin/templates' }, { label: auditLabel('签名待审'), count: pendingAudits.value.signatures, to: '/admin/signatures' }, { label: auditLabel('签名导入待审'), count: pendingAudits.value.signatureImports ?? 0, to: '/admin/signatures?tab=import', }, { label: auditLabel('引流信息待审'), count: pendingAudits.value.drainageInfos, to: '/admin/drainage-audits' }, ]} navSections={[ { title: '运营概览', icon: Gauge, items: [ { label: '运营看板', to: '/admin', icon: Gauge }, { label: '发送监控', to: '/admin/monitor', icon: Activity }, { label: '网关异常', to: '/admin/gateway-submit-exceptions', icon: AlertTriangle }, { label: '签名质量检测', to: '/admin/analytics', icon: BarChart3 }, ], }, { title: '客户管理', icon: Building2, items: [ { label: '企业管理', to: '/admin/customer-enterprises', icon: Building2 }, { label: '企业应用管理', to: '/admin/enterprise-applications', icon: Layers3 }, { label: '企业签名管理', to: '/admin/enterprise-signatures', icon: FilePenLine }, { label: '企业模板管理', to: '/admin/enterprise-templates', icon: FileCheck2 }, { label: 'HTTP签名计算', to: '/admin/http-signature', icon: Hash }, ], }, { title: '审核中心', icon: FileCheck2, items: [ { label: '企业认证审核', to: '/admin/enterprise-audit', icon: ShieldCheck }, { label: '短信审核', to: '/admin/sms-audit', icon: MessageSquare }, { label: '短信模板审核', to: '/admin/templates', icon: FileCheck2 }, { label: '短信签名审核', to: '/admin/signatures', icon: FilePenLine }, { label: '引流信息审核', to: '/admin/drainage-audits', icon: FilePenLine }, ], }, { title: '通道管理', icon: RadioTower, items: [ { label: '短信通道管理', to: '/admin/channels', icon: RadioTower }, { label: '彩信通道管理', to: '/admin/mms-channels', icon: ImageIcon, pending: true }, { label: '短信通道组管理', to: '/admin/channel-groups', icon: Layers3 }, ], }, { title: '报备工作台', icon: ClipboardList, items: [ { label: '报备资料池', to: '/admin/report-materials', icon: FileSpreadsheet }, { label: '报备批次', to: '/admin/report-batches', icon: Layers3 }, { label: '通道报备明细', to: '/admin/report-tasks', icon: ClipboardList }, { label: '状态记录', to: '/admin/report-records', icon: ListChecks }, ], }, { title: '发送任务', icon: Send, items: [ { label: '短信任务进度', to: '/admin/sms-task-progress', icon: TrendingUp }, { label: '彩信任务进度', to: '/admin/mms-task-progress', icon: BarChart3, pending: true }, ], }, { title: '数据详单', icon: ReceiptText, items: [ { label: '短信记录', to: '/admin/sms-records', icon: MessageSquare }, { label: '彩信记录', to: '/admin/mms-records', icon: ImageIcon, pending: true }, { label: '短信上行记录', to: '/admin/sms-uplink-records', icon: MessageSquare }, { label: '下游投递记录', to: '/admin/downstream-deliveries', icon: Send }, { label: '恢复状态管理', to: '/admin/downstream-recovery-statuses', icon: RefreshCw }, { label: '充值记录', to: '/admin/recharge-records', icon: ReceiptText }, ], }, { title: '报表对账', icon: BarChart3, items: [ { label: '对账单', to: '/admin/reconciliation-reports', icon: ReceiptText }, { label: '利润报表', to: '/admin/profit-reports', icon: TrendingUp }, { label: '发送质量报表', to: '/admin/quality-reports', icon: BarChart3 }, ], }, { title: '安全控制', icon: Shield, items: [ { label: '风控规则', to: '/admin/risk-rules', icon: Shield }, { label: '安全检测与封禁', to: '/admin/security-detection', icon: ShieldCheck }, { label: '签名清退预警', to: '/admin/signature-retirement', icon: AlertTriangle }, { label: '企业黑名单', to: '/admin/enterprise-blacklist', icon: UserX }, { label: '全局黑名单', to: '/admin/global-blacklist', icon: ShieldOff }, { label: '敏感词管理', to: '/admin/sensitive-words', icon: Shield }, ], }, { title: '系统管理', icon: Settings, items: [ { label: '用户管理', to: '/admin/users', icon: Users }, { label: '手机号段库', to: '/admin/phone-segments', icon: Phone }, { label: '报备字段库', to: '/admin/drainage-fields', icon: Hash }, { label: '引流识别规则', to: '/admin/drainage-detection-rules', icon: ScanSearch }, { label: '系统日志', to: '/admin/system-logs', icon: FileText }, { label: '系统监控', to: '/admin/system-monitoring', icon: ServerCog }, ], }, ]} /> ); }