import { useEffect, useMemo, useState } from 'react'; import { BarChart3, DollarSign, FileCheck2, ShieldCheck } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { Breadcrumb, Button, Modal, MoneyText, Table, Tag, type TableColumn } from '@/components/ui'; import './AdminHome.css'; import { Chart } from '@/components/ui/Chart'; import { adminApi, type DashboardResponse, type SendQualityResponse } from '@/api/adminApi'; import { createDualAxisBarLineOption, createLineOption } from '@/theme/chartOptions'; import { formatAmount, moneyUnitsToYuan } from '@/utils/currency'; type EnterpriseSpendRank = { id: string; enterprise: string; todaySpend: number; balanceStatus: '充足' | '紧张' | '欠费'; availableBalance: number; }; const balanceTone = { 充足: 'success', 紧张: 'warning', 欠费: 'danger', } as const; function formatCurrency(value: number) { return formatAmount(value); } function formatCount(value: number) { return value.toLocaleString('zh-CN'); } export function AdminHome() { const navigate = useNavigate(); const [dashboard, setDashboard] = useState(null); const [quality, setQuality] = useState(null); const [error, setError] = useState(''); const [selectedEnterprise, setSelectedEnterprise] = useState(null); useEffect(() => { Promise.all([adminApi.getDashboard(), adminApi.getSendQuality()]) .then(([nextDashboard, nextQuality]) => { setDashboard(nextDashboard); setQuality(nextQuality); }) .catch((err) => { setError(err instanceof Error ? err.message : '运营看板加载失败'); setDashboard(null); }); }, []); const enterpriseSpendRanks = useMemo(() => { return (dashboard?.enterpriseSpendRanks ?? []).map((account) => { const todaySpend = moneyUnitsToYuan(account.todaySpendCents); const availableBalance = moneyUnitsToYuan(account.balanceCents + account.creditCents); return { id: account.tenantId, enterprise: account.tenantName, todaySpend, availableBalance, balanceStatus: (availableBalance <= 0 ? '欠费' : availableBalance < 100 ? '紧张' : '充足') as EnterpriseSpendRank['balanceStatus'], }; }); }, [dashboard]); const totalSend = dashboard?.today.sent ?? 0; const averageSuccessRate = dashboard?.today.successRate ?? 0; const todaySpend = moneyUnitsToYuan(dashboard?.today.spendCents); const todayReturned = moneyUnitsToYuan(dashboard?.today.returnedCents); const todayBilled = moneyUnitsToYuan(dashboard?.today.billedCents); const todayProfit = moneyUnitsToYuan(dashboard?.today.profitCents); const activeSignatureCount = new Set(quality?.signatures.map((item) => item.signatureId) ?? []).size; const downstreamAlertCount = dashboard?.downstreamDeliverySummary?.alertCount ?? 0; const pendingAudits = dashboard?.pendingAudits ?? { enterpriseCertifications: 0, smsAudits: 0, templates: 0, signatures: 0, drainageInfos: 0, total: 0, }; const sendTrendOption = useMemo( () => createLineOption({ labels: dashboard?.hourlySendTrend.map((item) => item.label) ?? [], series: [ { name: '提交总条数', data: dashboard?.hourlySendTrend.map((item) => item.submittedCount) ?? [] }, { name: '成功条数', data: dashboard?.hourlySendTrend.map((item) => item.successCount) ?? [] }, ], }), [dashboard], ); const auditSpeedOption = useMemo( () => createDualAxisBarLineOption({ labels: dashboard?.auditProcessingSpeed.map((item) => item.label) ?? [], bar: { name: '审核数量', data: dashboard?.auditProcessingSpeed.map((item) => item.count) ?? [], }, line: { name: '平均处理时长(分钟)', data: dashboard?.auditProcessingSpeed.map((item) => item.averageProcessingMs == null ? null : Number((item.averageProcessingMs / 60_000).toFixed(1)), ) ?? [], }, }), [dashboard], ); const enterpriseColumns: Array> = [ { key: 'rank', title: '排名', width: '72px', render: (_record, index) => index + 1 }, { key: 'enterprise', title: '企业名称', render: (record) => (
{record.enterprise}

{record.id}

), }, { key: 'todaySpend', title: '今日消费(元)', align: 'right', render: (record) => ¥{formatCurrency(record.todaySpend)}, }, { key: 'availableBalance', title: '可用余额', align: 'right', render: (record) => formatCount(record.availableBalance), }, { key: 'balanceStatus', title: '余额状态', render: (record) => {record.balanceStatus}, }, { key: 'actions', title: '操作', align: 'right', render: (record) => ( ), }, ]; return (

按业务口径查看平台发送、签名、消费和审核情况。

{[ { label: '今日发送总量', value: formatCount(totalSend), unit: '条', note: '业务短信', group: '发送', icon: , }, { label: '今日消息分片数', value: formatCount(dashboard?.today.segmentCount ?? 0), unit: '片', note: '实际消息分片', group: '发送', }, { label: '总体成功率', value: averageSuccessRate.toFixed(1), unit: '%', note: '送达成功 / 今日总量', group: '质量', icon: , }, { label: '今日到达率', value: (dashboard?.today.arrivalRate ?? 0).toFixed(1), unit: '%', note: '到达分片 / 发送总分片', group: '质量', }, { label: '今日活跃签名', value: formatCount(activeSignatureCount), unit: '个', note: '今日有真实发送记录', group: '发送', }, { label: '今日消费金额', value: formatCurrency(todaySpend), unit: '元', note: '今日消息消费', group: '经营', icon: , }, { label: '今日返还金额', value: formatCurrency(todayReturned), unit: '元', note: '今日返还流水', group: '经营', }, { label: '今日计收金额', value: formatCurrency(todayBilled), unit: '元', note: '成功计费条数 × 客户价', group: '经营', }, { label: '今日利润', value: formatCurrency(todayProfit), unit: '元', note: '计收金额 − 成功分片通道成本', group: '经营', danger: todayProfit < 0, }, { label: '今日利润率', value: (dashboard?.today.profitRate ?? 0).toFixed(1), unit: '%', note: '今日利润 / 今日计收金额', group: '经营', danger: (dashboard?.today.profitRate ?? 0) < 0, }, ].map((metric) => (
{metric.label} {metric.icon} {metric.group}
{dashboard ? metric.value : '—'} {metric.unit}

{dashboard ? metric.note : error ? '数据暂不可用' : '正在加载…'}

))}
{error ?
{error}
: null}

今日发送趋势

按上海时区逐小时展示业务短信提交总条数和最终成功条数。

审核处理速度

展示今日各项已处理审核数量,以及从提交到审核完成的平均时长。

今日企业消费排行

来自真实账户、充值和消息金额聚合。

运营状态

当日关键流程状态汇总。

平均等待 {dashboard?.taskCount ?? 0} 任务 真实批量任务总数。
下游投递告警 {downstreamAlertCount} 条 积压过久或近期失败。
} onClose={() => setSelectedEnterprise(null)} open={Boolean(selectedEnterprise)} title={

企业消费详情

{selectedEnterprise?.id}

} > {selectedEnterprise ? (
企业名称 {selectedEnterprise.enterprise}
企业ID {selectedEnterprise.id}
余额状态 {selectedEnterprise.balanceStatus}
今日消费 ¥{formatCurrency(selectedEnterprise.todaySpend)}
可用余额 {formatCount(selectedEnterprise.availableBalance)}
) : null}
); }