fix: connect operations pages to real backend

This commit is contained in:
hectorzhao
2026-07-02 15:16:34 +08:00
parent ab421cf8a7
commit 321cf716f2
22 changed files with 1255 additions and 429 deletions
+64 -142
View File
@@ -1,11 +1,9 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
BarChart3,
Clock3,
DollarSign,
FileCheck2,
RadioTower,
Send,
ShieldCheck,
Users,
} from 'lucide-react';
@@ -19,59 +17,17 @@ import {
Tag,
type TableColumn,
} from '@/components/ui';
import { auditTrend, hourlySendTrend } from '@/mock/chartData';
import { adminService, type AuditStatus } from '@/mock';
import { createAuditColumns } from '@/apps/admin/auditColumns';
import { adminApi, type DashboardResponse } from '@/api/adminApi';
import { createBarOption, createLineOption } from '@/theme/chartOptions';
type SignatureRank = {
id: string;
signature: string;
customer: string;
type: '不含引流' | '仅引流';
successCount: number;
successRate: number;
averageSeconds: number;
status: '正常' | '关注' | '异常';
};
type EnterpriseSpendRank = {
id: string;
city: string;
enterprise: string;
contact: string;
todaySpend: number;
balanceStatus: '充足' | '紧张' | '欠费';
availableBalance: number;
};
const signatureRanks: SignatureRank[] = [
{ id: 'SIG-001', signature: '[XX银行]', customer: '上海云舟科技', type: '不含引流', successCount: 1278, successRate: 77.8, averageSeconds: 3.7, status: '正常' },
{ id: 'SIG-002', signature: '[XX科技有限公司]', customer: '杭州星澜商贸', type: '不含引流', successCount: 627, successRate: 65.3, averageSeconds: 2.5, status: '关注' },
{ id: 'SIG-003', signature: '[XXAPP]', customer: '深圳北辰出行', type: '不含引流', successCount: 322, successRate: 97.2, averageSeconds: 115.2, status: '关注' },
{ id: 'SIG-004', signature: '[XXXXX公司]', customer: '广州麦芒科技', type: '不含引流', successCount: 125, successRate: 33.2, averageSeconds: 13, status: '异常' },
{ id: 'SIG-005', signature: '[XXXXX公司]', customer: '北京鸣川科技', type: '不含引流', successCount: 45, successRate: 0, averageSeconds: 0.3, status: '异常' },
{ id: 'SIG-101', signature: '[XX银行]', customer: '上海云舟科技', type: '仅引流', successCount: 1278, successRate: 77.8, averageSeconds: 3.7, status: '正常' },
{ id: 'SIG-102', signature: '[XX科技有限公司]', customer: '杭州星澜商贸', type: '仅引流', successCount: 527, successRate: 65.3, averageSeconds: 2.5, status: '关注' },
{ id: 'SIG-103', signature: '[XXAPP]', customer: '深圳北辰出行', type: '仅引流', successCount: 322, successRate: 97.2, averageSeconds: 115.2, status: '关注' },
{ id: 'SIG-104', signature: '[XXXXX公司]', customer: '广州麦芒科技', type: '仅引流', successCount: 125, successRate: 33.2, averageSeconds: 13, status: '异常' },
{ id: 'SIG-105', signature: '[XXXXX公司]', customer: '北京鸣川科技', type: '仅引流', successCount: 45, successRate: 0, averageSeconds: 0.3, status: '异常' },
];
const enterpriseSpendRanks: EnterpriseSpendRank[] = [
{ id: 'ENT-001', city: '上海', enterprise: '上海XXXXX科技有限公司', contact: '赵先生', todaySpend: 1123.4, balanceStatus: '充足', availableBalance: 286420 },
{ id: 'ENT-002', city: '上海', enterprise: '上海云舟科技有限公司', contact: '王女士', todaySpend: 256.3, balanceStatus: '充足', availableBalance: 94220 },
{ id: 'ENT-003', city: '深圳', enterprise: '深圳XXXXX科技有限公司', contact: '陈先生', todaySpend: 97.25, balanceStatus: '紧张', availableBalance: 1200 },
{ id: 'ENT-004', city: '北京', enterprise: '北京XXXXX科技有限公司', contact: '刘女士', todaySpend: 66.2, balanceStatus: '充足', availableBalance: 55200 },
{ id: 'ENT-005', city: '杭州', enterprise: '杭州XXXXX科技有限公司', contact: '周先生', todaySpend: 12, balanceStatus: '欠费', availableBalance: 0 },
];
const rankStatusTone = {
: 'success',
: 'warning',
: 'danger',
} as const;
const balanceTone = {
: 'success',
: 'warning',
@@ -91,57 +47,65 @@ function formatCount(value: number) {
export function AdminHome() {
const navigate = useNavigate();
const overview = adminService.getOverview();
const [audits, setAudits] = useState(() => adminService.getAudits());
const [dashboard, setDashboard] = useState<DashboardResponse | null>(null);
const [error, setError] = useState('');
const [selectedEnterprise, setSelectedEnterprise] = useState<EnterpriseSpendRank | null>(null);
const channels = adminService.getChannels();
const [channels, setChannels] = useState<Array<{ id: string; name: string; status: string; rateLimitPerSecond: number }>>([]);
function updateAuditStatus(id: string, status: AuditStatus) {
setAudits(adminService.updateAuditStatus(id, status));
}
useEffect(() => {
Promise.all([adminApi.getDashboard(), adminApi.listChannels()])
.then(([nextDashboard, nextChannels]) => {
setDashboard(nextDashboard);
setChannels(nextChannels);
})
.catch((err) => {
setError(err instanceof Error ? err.message : '运营看板加载失败');
setDashboard(null);
});
}, []);
const auditColumns = useMemo(() => createAuditColumns(updateAuditStatus), []);
const pendingAudits = audits.filter((item) => item.status === 'pending');
const enterpriseSpendRanks = useMemo<EnterpriseSpendRank[]>(() => {
return (dashboard?.accounts ?? []).map((account) => {
const todaySpend = Math.abs(dashboard?.recentRecharges
.filter((item) => item.tenantId === account.tenantId)
.reduce((sum, item) => sum + item.amountCents, 0) ?? 0) / 100;
const availableBalance = (account.balanceCents + account.creditCents) / 100;
return {
id: account.tenantId,
enterprise: account.tenant?.name ?? account.tenantId,
todaySpend,
availableBalance,
balanceStatus: (availableBalance <= 0 ? '欠费' : availableBalance < 100 ? '紧张' : '充足') as EnterpriseSpendRank['balanceStatus'],
};
}).sort((left, right) => right.todaySpend - left.todaySpend);
}, [dashboard]);
const noDiversionSignatureRanks = signatureRanks.filter((item) => item.type === '不含引流');
const diversionSignatureRanks = signatureRanks.filter((item) => item.type === '仅引流');
const totalSend = signatureRanks.reduce((sum, item) => sum + item.successCount, 0);
const averageSuccessRate = signatureRanks.reduce((sum, item) => sum + item.successRate, 0) / signatureRanks.length;
const todaySpend = enterpriseSpendRanks.reduce((sum, item) => sum + item.todaySpend, 0);
const activeSignatureCount = new Set(signatureRanks.map((item) => item.signature)).size;
const totalSend = dashboard?.today.sent ?? 0;
const averageSuccessRate = dashboard?.today.successRate ?? 0;
const todaySpend = (dashboard?.today.spendCents ?? 0) / 100;
const activeConnectionCount = dashboard?.gatewayConnections.reduce((sum, item) => sum + (item._sum.currentConnections ?? 0), 0) ?? 0;
const sendTrendOption = useMemo(
() => createLineOption({
labels: hourlySendTrend.map((item) => item.time),
labels: ['今日'],
series: [
{ name: '提交量', data: hourlySendTrend.map((item) => item.sent) },
{ name: '成功量', data: hourlySendTrend.map((item) => item.success) },
{ name: '提交量', data: [dashboard?.today.sent ?? 0] },
{ name: '成功量', data: [dashboard?.today.delivered ?? 0] },
],
}),
[],
[dashboard],
);
const auditTrendOption = useMemo(
() => createBarOption({
labels: auditTrend.map((item) => item.day),
labels: ['待审核'],
series: [
{ name: '通过', data: auditTrend.map((item) => item.approved) },
{ name: '驳回', data: auditTrend.map((item) => item.rejected) },
{ name: '待审', data: auditTrend.map((item) => item.pending) },
{ name: '待审', data: [dashboard?.pendingAuditCount ?? 0] },
],
}),
[],
[dashboard],
);
const signatureColumns: Array<TableColumn<SignatureRank>> = [
{ key: 'rank', title: '排名', width: '72px', render: (_record, index) => index + 1 },
{ key: 'signature', title: '签名', render: (record) => <strong>{record.signature}</strong> },
{ key: 'successCount', title: '成功总数', align: 'right', render: (record) => formatCount(record.successCount) },
{ key: 'successRate', title: '成功率', align: 'right', render: (record) => `${record.successRate}%` },
{ key: 'averageSeconds', title: '平均时长(秒)', align: 'right', render: (record) => record.averageSeconds },
];
const enterpriseColumns: Array<TableColumn<EnterpriseSpendRank>> = [
{ key: 'rank', title: '排名', width: '72px', render: (_record, index) => index + 1 },
{
@@ -150,7 +114,7 @@ export function AdminHome() {
render: (record) => (
<div>
<strong>{record.enterprise}</strong>
<p className="text-caption">{record.city} · {record.contact}</p>
<p className="text-caption">{record.id}</p>
</div>
),
},
@@ -169,12 +133,10 @@ export function AdminHome() {
},
];
const channelColumns: Array<TableColumn<ReturnType<typeof adminService.getChannels>[number]>> = [
const channelColumns: Array<TableColumn<(typeof channels)[number]>> = [
{ key: 'name', title: '通道名称', render: (record) => <strong>{record.name}</strong> },
{ key: 'region', title: '区域', render: (record) => <span className="muted">{record.region}</span> },
{ key: 'successRate', title: '成功率', align: 'right', render: (record) => `${record.successRate}%` },
{ key: 'latencyMs', title: '平均延迟', align: 'right', render: (record) => `${record.latencyMs}ms` },
{ key: 'enabled', title: '状态', render: (record) => <Tag tone={record.enabled ? 'success' : 'neutral'}>{record.enabled ? '运行中' : '已停用'}</Tag> },
{ 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` },
];
return (
@@ -197,68 +159,45 @@ export function AdminHome() {
<div className="dashboard-grid admin-metric-grid">
<div className="surface metric-card">
<span></span>
<strong>{(totalSend / 10000).toFixed(4)}</strong>
<small></small>
<strong>{formatCount(totalSend)} </strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{averageSuccessRate.toFixed(1)}%</strong>
<small></small>
<small>delivered / </small>
</div>
<div className="surface metric-card">
<span></span>
<strong>¥{formatCurrency(todaySpend)}</strong>
<small></small>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{activeSignatureCount}</strong>
<small></small>
<span>线</span>
<strong>{activeConnectionCount}</strong>
<small>Gateway </small>
</div>
</div>
{error ? <div className="surface ui-table__empty">{error}</div> : null}
<div className="chart-grid">
<div className="surface chart-card">
<h2></h2>
<p className="muted"> 3 </p>
<p className="muted"></p>
<Chart height={300} option={sendTrendOption} />
</div>
<div className="surface chart-card">
<h2></h2>
<p className="muted"> 7 </p>
<p className="muted"></p>
<Chart height={300} option={auditTrendOption} />
</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">{noDiversionSignatureRanks.length} </Tag>
</div>
<Table columns={signatureColumns} data={noDiversionSignatureRanks} rowKey="id" />
</div>
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2> - </h2>
<p className="muted"></p>
</div>
<Tag tone="info">{diversionSignatureRanks.length} </Tag>
</div>
<Table columns={signatureColumns} data={diversionSignatureRanks} rowKey="id" />
</div>
</div>
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2></h2>
<p className="muted"></p>
<p className="muted"></p>
</div>
<Button icon={<DollarSign size={16} />} size="sm" variant="ghost">
@@ -294,7 +233,7 @@ export function AdminHome() {
<FileCheck2 size={22} />
<div>
<span></span>
<strong>{pendingAudits.length} </strong>
<strong>{dashboard?.pendingAuditCount ?? 0} </strong>
<small></small>
</div>
</div>
@@ -302,35 +241,22 @@ export function AdminHome() {
<ShieldCheck size={22} />
<div>
<span></span>
<strong>{overview.averageWaitMinutes} </strong>
<small></small>
<strong>{dashboard?.taskCount ?? 0} </strong>
<small></small>
</div>
</div>
<div className="mini-status-card">
<Users size={22} />
<div>
<span></span>
<strong>{overview.channelHealth}%</strong>
<small></small>
<strong>{activeConnectionCount}</strong>
<small>线</small>
</div>
</div>
</div>
</div>
</div>
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2></h2>
<p className="muted"></p>
</div>
<Button icon={<Clock3 size={16} />} onClick={() => navigate('/admin/templates')} variant="ghost">
</Button>
</div>
<Table columns={auditColumns} data={pendingAudits} rowKey="id" emptyText="暂无待审核记录" />
</div>
<Modal
footer={(
<>
@@ -354,12 +280,8 @@ export function AdminHome() {
<strong>{selectedEnterprise.enterprise}</strong>
</div>
<div className="ui-detail-info-grid__item">
<span></span>
<strong>{selectedEnterprise.city}</strong>
</div>
<div className="ui-detail-info-grid__item">
<span></span>
<strong>{selectedEnterprise.contact}</strong>
<span>ID</span>
<strong>{selectedEnterprise.id}</strong>
</div>
<div className="ui-detail-info-grid__item">
<span></span>