fix: connect operations pages to real backend
This commit is contained in:
+64
-142
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user