Initial CMPP frontend prototype
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
BarChart3,
|
||||
Clock3,
|
||||
DollarSign,
|
||||
FileCheck2,
|
||||
RadioTower,
|
||||
Send,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Chart,
|
||||
Modal,
|
||||
Table,
|
||||
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 { 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',
|
||||
欠费: 'danger',
|
||||
} as const;
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return value.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
function formatCount(value: number) {
|
||||
return value.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
export function AdminHome() {
|
||||
const navigate = useNavigate();
|
||||
const overview = adminService.getOverview();
|
||||
const [audits, setAudits] = useState(() => adminService.getAudits());
|
||||
const [selectedEnterprise, setSelectedEnterprise] = useState<EnterpriseSpendRank | null>(null);
|
||||
const channels = adminService.getChannels();
|
||||
|
||||
function updateAuditStatus(id: string, status: AuditStatus) {
|
||||
setAudits(adminService.updateAuditStatus(id, status));
|
||||
}
|
||||
|
||||
const auditColumns = useMemo(() => createAuditColumns(updateAuditStatus), []);
|
||||
const pendingAudits = audits.filter((item) => item.status === 'pending');
|
||||
|
||||
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 sendTrendOption = useMemo(
|
||||
() => createLineOption({
|
||||
labels: hourlySendTrend.map((item) => item.time),
|
||||
series: [
|
||||
{ name: '提交量', data: hourlySendTrend.map((item) => item.sent) },
|
||||
{ name: '成功量', data: hourlySendTrend.map((item) => item.success) },
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const auditTrendOption = useMemo(
|
||||
() => createBarOption({
|
||||
labels: auditTrend.map((item) => item.day),
|
||||
series: [
|
||||
{ name: '通过', data: auditTrend.map((item) => item.approved) },
|
||||
{ name: '驳回', data: auditTrend.map((item) => item.rejected) },
|
||||
{ name: '待审', data: auditTrend.map((item) => item.pending) },
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
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 },
|
||||
{
|
||||
key: 'enterprise',
|
||||
title: '企业名称',
|
||||
render: (record) => (
|
||||
<div>
|
||||
<strong>{record.enterprise}</strong>
|
||||
<p className="text-caption">{record.city} · {record.contact}</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ 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) => <Tag tone={balanceTone[record.balanceStatus]}>{record.balanceStatus}</Tag> },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
render: (record) => (
|
||||
<Button onClick={() => setSelectedEnterprise(record)} size="sm" variant="ghost">
|
||||
查看详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const channelColumns: Array<TableColumn<ReturnType<typeof adminService.getChannels>[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> },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-dashboard">
|
||||
<div className="overview-hero admin-dashboard-hero">
|
||||
<div>
|
||||
<p className="eyebrow">运营端</p>
|
||||
<h1>数据概览</h1>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-grid admin-metric-grid">
|
||||
<div className="surface metric-card">
|
||||
<span>今日发送总量</span>
|
||||
<strong>{(totalSend / 10000).toFixed(4)}万条</strong>
|
||||
<small>基于签名发送排行汇总</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>总体成功率</span>
|
||||
<strong>{averageSuccessRate.toFixed(1)}%</strong>
|
||||
<small>包含不含引流与仅引流口径</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>今日消费</span>
|
||||
<strong>¥{formatCurrency(todaySpend)}</strong>
|
||||
<small>企业消费排行汇总</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>活跃签名数量</span>
|
||||
<strong>{activeSignatureCount}</strong>
|
||||
<small>今日有成功发送记录的签名</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="chart-grid">
|
||||
<div className="surface chart-card">
|
||||
<h2>今日发送趋势</h2>
|
||||
<p className="muted">按 3 小时聚合平台提交量和成功量。</p>
|
||||
<Chart height={300} option={sendTrendOption} />
|
||||
</div>
|
||||
<div className="surface chart-card">
|
||||
<h2>审核处理趋势</h2>
|
||||
<p className="muted">近 7 天模板、签名审核处理情况。</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>
|
||||
</div>
|
||||
<Button icon={<DollarSign size={16} />} size="sm" variant="ghost">
|
||||
导出排行
|
||||
</Button>
|
||||
</div>
|
||||
<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="section-heading">
|
||||
<div>
|
||||
<h2>运营状态</h2>
|
||||
<p className="muted">当日关键流程状态汇总。</p>
|
||||
</div>
|
||||
<BarChart3 size={20} className="status-info" />
|
||||
</div>
|
||||
<div className="overview-grid overview-grid--three">
|
||||
<div className="mini-status-card">
|
||||
<FileCheck2 size={22} />
|
||||
<div>
|
||||
<span>待审核</span>
|
||||
<strong>{pendingAudits.length} 条</strong>
|
||||
<small>模板、签名和企业认证。</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mini-status-card">
|
||||
<ShieldCheck size={22} />
|
||||
<div>
|
||||
<span>平均等待</span>
|
||||
<strong>{overview.averageWaitMinutes} 分钟</strong>
|
||||
<small>高风险内容优先处理。</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mini-status-card">
|
||||
<Users size={22} />
|
||||
<div>
|
||||
<span>平台健康度</span>
|
||||
<strong>{overview.channelHealth}%</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={(
|
||||
<>
|
||||
<Button onClick={() => setSelectedEnterprise(null)} variant="ghost">关闭</Button>
|
||||
<Button onClick={() => navigate('/admin/billing')}>查看账单流水</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={() => setSelectedEnterprise(null)}
|
||||
open={Boolean(selectedEnterprise)}
|
||||
title={(
|
||||
<div className="ui-detail-title">
|
||||
<h2>企业消费详情</h2>
|
||||
<p>{selectedEnterprise?.id}</p>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{selectedEnterprise ? (
|
||||
<div className="ui-detail-info-grid">
|
||||
<div className="ui-detail-info-grid__item">
|
||||
<span>企业名称</span>
|
||||
<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>
|
||||
</div>
|
||||
<div className="ui-detail-info-grid__item">
|
||||
<span>余额状态</span>
|
||||
<strong>
|
||||
<Tag tone={balanceTone[selectedEnterprise.balanceStatus]}>{selectedEnterprise.balanceStatus}</Tag>
|
||||
</strong>
|
||||
</div>
|
||||
<div className="ui-detail-info-grid__item">
|
||||
<span>今日消费</span>
|
||||
<strong>¥{formatCurrency(selectedEnterprise.todaySpend)}</strong>
|
||||
</div>
|
||||
<div className="ui-detail-info-grid__item">
|
||||
<span>可用余额</span>
|
||||
<strong>{formatCount(selectedEnterprise.availableBalance)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user