Files
lislgosms/src/layouts/AdminLayout.tsx
T

321 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PendingAuditCounts, 'total'> = {
enterpriseCertifications: 0,
smsAudits: 0,
templates: 0,
signatures: 0,
drainageInfos: 0,
};
type NotificationValue<T> = { value: T; status: 'loading' | 'ready' | 'unavailable'; hasValue: boolean };
function initialNotification<T>(value: T): NotificationValue<T> {
return { value, status: 'loading', hasValue: false };
}
function updateNotification<T>(
previous: NotificationValue<T>,
result: PromiseSettledResult<T>,
available = true,
): NotificationValue<T> {
return result.status === 'fulfilled' && available
? { value: result.value, status: 'ready', hasValue: true }
: { ...previous, status: 'unavailable' };
}
function notificationDescription<T>(notification: NotificationValue<T>, 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 (
<PortalSessionBoundary portal="admin">
{(session) => <AdminAuthenticatedLayout key={session.user.id} session={session} />}
</PortalSessionBoundary>
);
}
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 (
<AppShell
title="CMPP 运营端"
subtitle="平台运营管理中心"
workspaceName="平台运营工作区"
loginPath="/admin/login"
portal="admin"
userName={session.user.displayName}
userRole="平台管理员"
onSessionLockedChange={setSessionLocked}
reportingNotifications={[
{
label: '报备状态变化通知',
count: reportSummary.value.count,
description: notificationDescription(reportSummary, '按企业与小时汇总的未读消息'),
to: '/admin/report-records?tab=readiness',
},
{
label: '签名清退预警',
count: retirementSummary.value.count,
description: notificationDescription(retirementSummary, '今日未读且未抑制'),
to: '/admin/signature-retirement',
},
{
label: '报备进度提醒',
description: '报备进度通知功能尚未开放',
pending: true,
},
]}
alertNotifications={[
{
label: '发送质量告警',
count: monitorSummary.value.count,
description: notificationDescription(monitorSummary, '未读活动发送质量告警'),
to: '/admin/monitor?tab=alerts',
},
{
label: '安全检测与封禁',
count: securityAlertSummary.value.count,
description: notificationDescription(
securityAlertSummary,
securityAlertSummary.value.criticalCount > 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 },
],
},
{
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 },
],
},
]}
/>
);
}