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
+66 -50
View File
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
BadgeCheck,
BellRing,
@@ -11,61 +11,76 @@ import {
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
import { channelShare, hourlySendTrend } from '@/mock/chartData';
import { clientService, type RecentMessage, type TemplateStatus } from '@/mock';
import { clientApi, type DashboardResponse } from '@/api/adminApi';
import { createLineOption, createPieOption } from '@/theme/chartOptions';
const statusLabelMap: Record<RecentMessage['status'], string> = {
success: '发送完成',
warning: '排队中',
info: '发送中',
danger: '发送失败',
type RecentTaskRow = {
id: string;
taskNo: string;
scene: string;
count: number;
channel: string;
createdAt: string;
status: string;
};
const templateStatusLabelMap: Record<TemplateStatus, string> = {
draft: '草稿',
pending: '待审核',
approved: '已通过',
rejected: '已驳回',
};
const columns: Array<TableColumn<RecentMessage>> = [
{ key: 'id', title: '批次编号', render: (record) => record.id },
const columns: Array<TableColumn<RecentTaskRow>> = [
{ key: 'taskNo', title: '批次编号', render: (record) => record.taskNo },
{ key: 'scene', title: '发送场景', render: (record) => record.scene },
{ key: 'count', title: '发送量', render: (record) => `${record.count.toLocaleString('zh-CN')}` },
{ key: 'channel', title: '通道', render: (record) => record.channel },
{ key: 'createdAt', title: '创建时间', render: (record) => record.createdAt },
{ key: 'status', title: '状态', render: (record) => <Tag tone={record.status}>{statusLabelMap[record.status]}</Tag> },
{ key: 'status', title: '状态', render: (record) => <Tag tone={record.status === 'completed' ? 'success' : record.status === 'failed' ? 'danger' : 'info'}>{record.status}</Tag> },
];
export function ClientHome() {
const navigate = useNavigate();
const overview = clientService.getOverview();
const recentMessages = clientService.getRecentMessages();
const templates = clientService.getTemplates();
const signatures = clientService.getSignatures();
const invoices = clientService.getInvoices();
const [dashboard, setDashboard] = useState<DashboardResponse | null>(null);
const [error, setError] = useState('');
const approvedTemplates = templates.filter((item) => item.status === 'approved').length;
const approvedSignatures = signatures.filter((item) => item.status === 'approved').length;
const pendingTemplates = templates.filter((item) => item.status === 'pending').length;
const pendingSignatures = signatures.filter((item) => item.status === 'pending').length;
const latestInvoice = invoices[0];
const balanceBaseline = overview.availableBalance + overview.todaySpend - overview.todayRefund;
const balancePercent = Math.min(100, Math.round((overview.availableBalance / balanceBaseline) * 100));
useEffect(() => {
clientApi.getDashboard()
.then(setDashboard)
.catch((err) => {
setError(err instanceof Error ? err.message : '客户端工作台加载失败');
setDashboard(null);
});
}, []);
const account = dashboard?.accounts[0];
const availableBalance = ((account?.balanceCents ?? 0) + (account?.creditCents ?? 0)) / 100;
const todaySpend = (dashboard?.today.spendCents ?? 0) / 100;
const todayRefund = Math.abs((dashboard?.transactions._sum.amountCents ?? 0) < 0 ? 0 : dashboard?.transactions._sum.amountCents ?? 0) / 100;
const balanceBaseline = Math.max(availableBalance + todaySpend - todayRefund, availableBalance, 1);
const balancePercent = Math.min(100, Math.round((availableBalance / balanceBaseline) * 100));
const recentMessages = useMemo<RecentTaskRow[]>(() => (dashboard?.recentTasks ?? []).map((task) => ({
id: String(task.id ?? task.taskNo),
taskNo: String(task.taskNo ?? task.id),
scene: String(task.category ?? task.content ?? '短信发送'),
count: Number(task.phoneTotal ?? task.progressTotal ?? 0),
channel: Array.isArray(task.messages) && task.messages[0]?.channel?.name ? String(task.messages[0].channel.name) : '待路由',
createdAt: task.createdAt ? new Date(String(task.createdAt)).toLocaleString('zh-CN') : '',
status: String(task.status ?? 'unknown'),
})), [dashboard]);
const latestRecharge = dashboard?.recentRecharges[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 channelShareOption = useMemo(() => createPieOption({ data: channelShare }), []);
const channelShareOption = useMemo(() => createPieOption({
data: (dashboard?.gatewayConnections ?? []).map((item) => ({
name: item.status,
value: item._sum.currentConnections ?? item._count._all,
})),
}), [dashboard]);
return (
<section className="page-stack">
@@ -86,20 +101,21 @@ export function ClientHome() {
<div className="dashboard-grid">
<div className="surface metric-card metric-card--featured">
<span></span>
<strong>¥{overview.availableBalance.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
<small> ¥{overview.todaySpend.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</small>
<strong>¥{availableBalance.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
<small> ¥{todaySpend.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{overview.todaySent.toLocaleString('zh-CN')}</strong>
<small> {overview.todaySuccessRate}%</small>
<strong>{(dashboard?.today.sent ?? 0).toLocaleString('zh-CN')}</strong>
<small> {dashboard?.today.successRate ?? 0}%</small>
</div>
<div className="surface metric-card">
<span></span>
<strong>¥{overview.todayRefund.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
<strong>¥{todayRefund.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
<small>退</small>
</div>
</div>
{error ? <div className="surface ui-table__empty">{error}</div> : null}
<div className="overview-grid">
<div className="surface section-stack">
@@ -118,12 +134,12 @@ export function ClientHome() {
<button className="quick-action" onClick={() => navigate('/client/templates')} type="button">
<FileText size={20} />
<span></span>
<small>{approvedTemplates} </small>
<small></small>
</button>
<button className="quick-action" onClick={() => navigate('/client/signatures')} type="button">
<PenLine size={20} />
<span></span>
<small>{approvedSignatures} </small>
<small></small>
</button>
<button className="quick-action" onClick={() => navigate('/client/billing')} type="button">
<WalletCards size={20} />
@@ -139,20 +155,20 @@ export function ClientHome() {
<h2></h2>
<p className="muted"></p>
</div>
<Tag tone="success"></Tag>
<Tag tone={account?.status === 'active' ? 'success' : 'warning'}>{account?.status ?? '未知'}</Tag>
</div>
<div className="summary-list">
<div>
<span></span>
<strong></strong>
<strong>{account?.tenant?.name ?? account?.tenantId ?? '当前租户'}</strong>
</div>
<div>
<span></span>
<strong></strong>
<strong> API </strong>
</div>
<div>
<span></span>
<strong>{latestInvoice.title}</strong>
<strong>{latestRecharge ? `¥${(latestRecharge.amountCents / 100).toFixed(2)}` : '暂无充值'}</strong>
</div>
</div>
<div>
@@ -172,16 +188,16 @@ export function ClientHome() {
<BadgeCheck size={22} />
<div>
<span></span>
<strong>{approvedTemplates} </strong>
<small>{templates.map((item) => templateStatusLabelMap[item.status]).join(' / ')}</small>
<strong>{dashboard?.pendingAuditCount ?? 0} </strong>
<small></small>
</div>
</div>
<div className="surface mini-status-card">
<PenLine size={22} />
<div>
<span></span>
<strong>{approvedSignatures} </strong>
<small> {pendingSignatures} {signatures.filter((item) => item.status === 'rejected').length}</small>
<strong> API</strong>
<small></small>
</div>
</div>
<div className="surface mini-status-card">