fix: restore daily operations quality metrics

This commit is contained in:
hectorzhao
2026-07-24 18:13:22 +08:00
parent 91f04f5288
commit e12bdf010e
10 changed files with 364 additions and 65 deletions
+48 -43
View File
@@ -3,9 +3,7 @@ import {
BarChart3,
DollarSign,
FileCheck2,
RadioTower,
ShieldCheck,
Users,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import {
@@ -17,7 +15,7 @@ import {
Tag,
type TableColumn,
} from '@/components/ui';
import { adminApi, type DashboardResponse } from '@/api/adminApi';
import { adminApi, type DashboardResponse, type SendQualityResponse, type SignatureQualityStat } from '@/api/adminApi';
import { createBarOption, createLineOption } from '@/theme/chartOptions';
import { formatAmount, moneyUnitsToYuan } from '@/utils/currency';
@@ -46,15 +44,15 @@ function formatCount(value: number) {
export function AdminHome() {
const navigate = useNavigate();
const [dashboard, setDashboard] = useState<DashboardResponse | null>(null);
const [quality, setQuality] = useState<SendQualityResponse | null>(null);
const [error, setError] = useState('');
const [selectedEnterprise, setSelectedEnterprise] = useState<EnterpriseSpendRank | null>(null);
const [channels, setChannels] = useState<Array<{ id: string; name: string; status: string; rateLimitPerSecond: number }>>([]);
useEffect(() => {
Promise.all([adminApi.getDashboard(), adminApi.listChannels()])
.then(([nextDashboard, nextChannels]) => {
Promise.all([adminApi.getDashboard(), adminApi.getSendQuality()])
.then(([nextDashboard, nextQuality]) => {
setDashboard(nextDashboard);
setChannels(nextChannels);
setQuality(nextQuality);
})
.catch((err) => {
setError(err instanceof Error ? err.message : '运营看板加载失败');
@@ -81,7 +79,7 @@ export function AdminHome() {
const totalSend = dashboard?.today.sent ?? 0;
const averageSuccessRate = dashboard?.today.successRate ?? 0;
const todaySpend = moneyUnitsToYuan(dashboard?.today.spendCents);
const activeConnectionCount = dashboard?.gatewayConnections.reduce((sum, item) => sum + (item._sum.currentConnections ?? 0), 0) ?? 0;
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 };
@@ -133,25 +131,32 @@ export function AdminHome() {
},
];
const channelColumns: Array<TableColumn<(typeof channels)[number]>> = [
{ key: 'name', title: '通道名称', render: (record) => <strong>{record.name}</strong> },
{ key: 'status', title: '状态', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'neutral'}>{record.status}</Tag> },
{ key: 'rateLimitPerSecond', title: '限速', align: 'right', render: (record) => `${record.rateLimitPerSecond}/s` },
const signatureColumns: Array<TableColumn<SignatureQualityStat>> = [
{ key: 'rank', title: '排名', width: '72px', render: (_record, index) => index + 1 },
{ key: 'signatureName', title: '签名', render: (record) => <div><strong>{record.signatureName}</strong><p className="text-caption">{record.tenantName}</p></div> },
{ key: 'total', title: '发送总数', align: 'right', render: (record) => formatCount(record.total) },
{ key: 'successCount', title: '成功', align: 'right', render: (record) => formatCount(record.successCount) },
{ key: 'unknownCount', title: '未知', align: 'right', render: (record) => formatCount(record.unknownCount) },
{ key: 'failureCount', title: '失败', align: 'right', render: (record) => formatCount(record.failureCount) },
{ key: 'successRate', title: '成功率', align: 'right', render: (record) => `${record.successRate.toFixed(1)}%` },
{ key: 'averageArrivalMs', title: '平均到达', align: 'right', render: (record) => record.averageArrivalMs === null || record.averageArrivalMs === undefined ? '-' : `${(record.averageArrivalMs / 1000).toFixed(1)}` },
];
const plainSignatureQuality = quality?.signatures.filter((item) => !item.hasDrainage) ?? [];
const drainageSignatureQuality = quality?.signatures.filter((item) => item.hasDrainage) ?? [];
return (
<section className="page-stack admin-dashboard">
<div className="overview-hero admin-dashboard-hero">
<div>
<Breadcrumb items={['数据概览']} />
<p className="muted"></p>
<p className="muted"></p>
</div>
<div className="page-actions">
<Button icon={<FileCheck2 size={16} />} onClick={() => navigate('/admin/templates')} variant="ghost">
</Button>
<Button icon={<RadioTower size={16} />} onClick={() => navigate('/admin/channels')}>
<Button onClick={() => navigate('/admin/monitor')}>
</Button>
</div>
</div>
@@ -173,9 +178,9 @@ export function AdminHome() {
<small></small>
</div>
<div className="surface metric-card">
<span>线</span>
<strong>{activeConnectionCount}</strong>
<small>Gateway </small>
<span></span>
<strong>{activeSignatureCount}</strong>
<small></small>
</div>
</div>
{error ? <div className="surface ui-table__empty">{error}</div> : null}
@@ -193,6 +198,29 @@ export function AdminHome() {
</div>
</div>
<div className="overview-grid admin-signature-rank-grid">
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2> - </h2>
<p className="muted"></p>
</div>
<Tag tone="info">{plainSignatureQuality.length} </Tag>
</div>
<Table columns={signatureColumns} data={plainSignatureQuality} emptyText="今日暂无不含引流的签名发送记录" rowKey="id" />
</div>
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2> - </h2>
<p className="muted"></p>
</div>
<Tag tone="accent">{drainageSignatureQuality.length} </Tag>
</div>
<Table columns={signatureColumns} data={drainageSignatureQuality} emptyText="今日暂无含引流的签名发送记录" rowKey="id" />
</div>
</div>
<div className="surface section-stack">
<div className="section-heading">
<div>
@@ -206,21 +234,7 @@ export function AdminHome() {
<Table columns={enterpriseColumns} data={enterpriseSpendRanks} rowKey="id" />
</div>
<div className="overview-grid">
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2></h2>
<p className="muted"></p>
</div>
<Button onClick={() => navigate('/admin/channels')} size="sm" variant="ghost">
</Button>
</div>
<Table columns={channelColumns} data={channels} rowKey="id" />
</div>
<div className="surface section-stack">
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2></h2>
@@ -263,15 +277,7 @@ export function AdminHome() {
</div>
</div>
<div className="mini-status-card">
<Users size={22} />
<div>
<span></span>
<strong>{activeConnectionCount}</strong>
<small>线</small>
</div>
</div>
<div className="mini-status-card">
<RadioTower size={22} />
<ShieldCheck size={22} />
<div>
<span></span>
<strong>{downstreamAlertCount} </strong>
@@ -279,7 +285,6 @@ export function AdminHome() {
</div>
</div>
</div>
</div>
</div>
<Modal