import { useEffect, useMemo, useState } from 'react'; import { BarChart3, Eye, Search, X } from 'lucide-react'; import { adminApi, type SendQualityResponse, type SignatureChannelCarrierQualityStat, type SignatureChannelQualityItem, type SignatureChannelQualityResponse, } from '@/api/adminApi'; import { Breadcrumb, Button, Chart, Input, Pagination, Table, Tag, type TableColumn } from '@/components/ui'; import { createBarOption, createPieOption } from '@/theme/chartOptions'; import { successRateClassName } from '@/utils/successRate'; const carrierOrder = ['mobile', 'unicom', 'telecom', 'unknown']; const majorCarrierOrder = ['mobile', 'unicom', 'telecom'] as const; const drainageStates = [ { value: 'with', label: '含引流' }, { value: 'without', label: '不含引流' }, { value: 'unknown', label: '未检测' }, ] as const; const carrierLabels: Record = { mobile: '移动', unicom: '联通', telecom: '电信', unknown: '未知', }; export function AdminAnalyticsPage() { const [statisticsDate, setStatisticsDate] = useState(() => shanghaiDateKey()); const [quality, setQuality] = useState(null); const [signatureQuality, setSignatureQuality] = useState(null); const [signatureKeyword, setSignatureKeyword] = useState(''); const [appliedKeyword, setAppliedKeyword] = useState(''); const [selectedSignature, setSelectedSignature] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); async function loadData(page = 1, keyword = appliedKeyword) { setLoading(true); try { const [qualityData, signatureData] = await Promise.all([ adminApi.getSendQuality(statisticsDate), adminApi.getSignatureQuality({ date: statisticsDate, keyword: keyword || undefined, page, pageSize: 10, }), ]); setQuality(qualityData); setSignatureQuality(signatureData); setAppliedKeyword(keyword); setSelectedSignature((current) => current ? signatureData.items.find((item) => item.signatureId === current.signatureId) ?? null : null); setError(''); } catch (failure) { setError(failure instanceof Error ? failure.message : '统计数据加载失败'); } finally { setLoading(false); } } useEffect(() => { void loadData(1, ''); }, []); useEffect(() => { if (!selectedSignature) return undefined; const previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setSelectedSignature(null); }; document.addEventListener('keydown', handleKeyDown); return () => { document.body.style.overflow = previousOverflow; document.removeEventListener('keydown', handleKeyDown); }; }, [selectedSignature]); const applicationOption = useMemo(() => createBarOption({ labels: quality?.applications.map((item) => item.applicationName) ?? [], series: [{ name: '发送量', data: quality?.applications.map((item) => item.total) ?? [] }], }), [quality]); const channelOption = useMemo(() => createPieOption({ data: quality?.channels.map((item) => ({ name: item.channelName || item.channelId, value: item.total })) ?? [], }), [quality]); const effectiveDate = quality?.date ?? statisticsDate; const signatureColumns: Array> = [ { key: 'signature', title: '短信签名', width: '260px', render: (record) => (
{record.signatureName} {record.tenantName} {record.applicationNames || '全部企业应用'}
), }, { key: 'businessTotal', title: '业务短信', width: '110px', align: 'right', render: (record) => record.total.toLocaleString('zh-CN'), }, { key: 'channelSubmitTotal', title: '通道提交', width: '110px', align: 'right', render: (record) => record.channelSubmitTotal.toLocaleString('zh-CN'), }, { key: 'successCount', title: '送达成功', width: '110px', align: 'right', render: (record) => {record.successCount.toLocaleString('zh-CN')}, }, { key: 'failureCount', title: '送达失败', width: '110px', align: 'right', render: (record) => {record.failureCount.toLocaleString('zh-CN')}, }, { key: 'submitFailureCount', title: '提交失败', width: '110px', align: 'right', render: (record) => {record.submitFailureCount.toLocaleString('zh-CN')}, }, { key: 'successRate', title: '成功率', width: '170px', render: (record) => , }, { key: 'averageArrivalMs', title: '平均到达时间', width: '140px', align: 'right', render: (record) => formatDuration(record.averageArrivalMs), }, { key: 'actions', title: '操作', width: '110px', align: 'right', render: (record) => ( ), }, ]; function queryStatistics() { void loadData(1, signatureKeyword.trim()); } function changeSignaturePage(page: number) { void loadData(page, appliedKeyword); } return (
setStatisticsDate(event.target.value)} type="date" value={statisticsDate} />
{error ?

{error}

: null}
{effectiveDate} 发送量 {quality?.summary.total.toLocaleString('zh-CN') ?? 0} 所选日期真实消息记录
{effectiveDate} 成功率 {(quality?.summary.successRate ?? 0).toFixed(1)}% {quality?.summary.successCount.toLocaleString('zh-CN') ?? 0} 条已送达 / {quality?.summary.total.toLocaleString('zh-CN') ?? 0} 条发送

企业应用发送排行

{effectiveDate} 当天按企业应用名称聚合真实短信消息记录。

企业应用

通道占比

{effectiveDate} 当天按真实通道提交及回执聚合。

通道

签名通道发送质量

已登记签名

{signatureQuality?.date ?? effectiveDate} 按签名查看业务结果,明细按真实通道提交尝试拆分运营商与通道。

setSignatureKeyword(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') queryStatistics(); }} placeholder="搜索签名、企业或应用" value={signatureKeyword} />
统计说明: 业务短信按消息记录去重;发生补发时会产生多次通道提交,因此“通道提交”可能大于“业务短信”。
{(signatureQuality?.total ?? 0) > 0 ? ( = Math.ceil((signatureQuality?.total ?? 0) / (signatureQuality?.pageSize ?? 10))} onNext={() => changeSignaturePage((signatureQuality?.page ?? 1) + 1)} onPageChange={changeSignaturePage} onPrevious={() => changeSignaturePage((signatureQuality?.page ?? 1) - 1)} page={signatureQuality?.page ?? 1} previousDisabled={(signatureQuality?.page ?? 1) <= 1} total={signatureQuality?.total ?? 0} totalPages={Math.ceil((signatureQuality?.total ?? 0) / (signatureQuality?.pageSize ?? 10))} /> ) : null} {selectedSignature ? ( setSelectedSignature(null)} /> ) : null} ); } function SignatureQualityDrawer({ date, item, onClose, }: { date: string; item: SignatureChannelQualityItem; onClose: () => void; }) { const [matrixMode, setMatrixMode] = useState<'overall' | 'drainage'>('overall'); const carriers = item.carrierOverview .map((carrier) => ({ ...carrier, channelCount: new Set( item.breakdowns .filter((entry) => normalizeCarrier(entry.carrier) === normalizeCarrier(carrier.carrier)) .map((entry) => entry.channelId), ).size, })) // Database aggregation order is not a display contract; keep the three major carriers stable. .sort((left, right) => { const leftRank = carrierOrder.indexOf(normalizeCarrier(left.carrier)); const rightRank = carrierOrder.indexOf(normalizeCarrier(right.carrier)); return (leftRank < 0 ? carrierOrder.length : leftRank) - (rightRank < 0 ? carrierOrder.length : rightRank); }); const channels = [...new Map([...item.breakdowns, ...item.drainageBreakdowns] .map((entry) => [entry.channelId, entry.channelName])).entries()] .map(([channelId, channelName]) => ({ channelId, channelName })); const visibleCarriers = carrierOrder.filter((carrier) => item.breakdowns.some((entry) => normalizeCarrier(entry.carrier) === carrier)); return (
{ if (event.target === event.currentTarget) onClose(); }}>
{matrixMode === 'overall' ? ( {visibleCarriers.map((carrier) => )} ) : ( {majorCarrierOrder.map((carrier) => )} )} {matrixMode === 'overall' ? channels.map((channel) => ( {visibleCarriers.map((carrier) => { const metric = item.breakdowns.find((entry) => ( entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier )); return ( ); })} )) : channels.flatMap((channel) => drainageStates.map((state, stateIndex) => ( {stateIndex === 0 ? : null} {majorCarrierOrder.map((carrier) => { const metric = item.drainageBreakdowns.find((entry) => ( entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier && entry.drainageState === state.value )); return ; })} )))}
通道名称{carrierLabel(carrier)}
通道名称 引流类型{carrierLabel(carrier)}
{channel.channelName} {metric ? : }
{channel.channelName}{state.label}

平均到达时间从该通道提交受理开始计算,到该通道全部成功回执完成为止,仅统计成功送达的提交尝试。

); } function QualityMetric({ label, value, valueClassName }: { label: string; value: string; valueClassName?: string }) { return (
{label} {value}
); } function MatrixMetric({ metric, zeroWhenEmpty = false }: { metric?: SignatureChannelCarrierQualityStat; zeroWhenEmpty?: boolean }) { const total = metric?.total ?? 0; const successRate = metric?.successRate ?? 0; if (zeroWhenEmpty && total === 0) return 0; return (
{total.toLocaleString('zh-CN')} 次 {successRate.toFixed(1)}% {formatDuration(metric?.averageArrivalMs)} {(metric?.submitFailureCount ?? 0) > 0 ? 提交失败 {metric?.submitFailureCount} : null}
); } function QualityRate({ value }: { value: number }) { return (
{value.toFixed(1)}%
); } function normalizeCarrier(value: string) { const normalized = value.toLowerCase(); if (['mobile', 'cmcc', '移动'].includes(normalized)) return 'mobile'; if (['unicom', 'cucc', '联通'].includes(normalized)) return 'unicom'; if (['telecom', 'ctcc', '电信'].includes(normalized)) return 'telecom'; return 'unknown'; } function carrierLabel(value: string) { return carrierLabels[normalizeCarrier(value)] ?? '未知'; } function carrierTagTone(value: string): 'info' | 'accent' | 'warning' | 'neutral' { const carrier = normalizeCarrier(value); if (carrier === 'mobile') return 'info'; if (carrier === 'unicom') return 'accent'; if (carrier === 'telecom') return 'warning'; return 'neutral'; } function formatDuration(value?: number | null) { if (value == null) return '—'; if (value < 1000) return `${Math.round(value)} 毫秒`; return `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)} 秒`; } function shanghaiDateKey(value = new Date()) { const parts = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(value); const byType = new Map(parts.map((part) => [part.type, part.value])); return `${byType.get('year')}-${byType.get('month')}-${byType.get('day')}`; }