236 lines
9.5 KiB
TypeScript
236 lines
9.5 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
BadgeCheck,
|
|
ClipboardList,
|
|
FileText,
|
|
PenLine,
|
|
Plus,
|
|
ReceiptText,
|
|
Send,
|
|
WalletCards,
|
|
} from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Button, Table, Tag, type TableColumn } from '@/components/ui';
|
|
import { Chart } from '@/components/ui/Chart';
|
|
import { clientApi, type DashboardResponse } from '@/api/adminApi';
|
|
import { createLineOption, createPieOption } from '@/theme/chartOptions';
|
|
import { formatDateTime } from '@/utils/dateTime';
|
|
import { formatAmount, formatCents, moneyUnitsToYuan } from '@/utils/currency';
|
|
|
|
type RecentTaskRow = {
|
|
id: string;
|
|
taskNo: string;
|
|
scene: string;
|
|
count: number;
|
|
createdAt: string;
|
|
status: string;
|
|
};
|
|
|
|
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: 'createdAt', title: '创建时间', render: (record) => record.createdAt },
|
|
{ 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 [dashboard, setDashboard] = useState<DashboardResponse | null>(null);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
clientApi.getDashboard()
|
|
.then(setDashboard)
|
|
.catch((err) => {
|
|
setError(err instanceof Error ? err.message : '客户端工作台加载失败');
|
|
setDashboard(null);
|
|
});
|
|
}, []);
|
|
|
|
const account = dashboard?.accounts[0];
|
|
const availableBalance = moneyUnitsToYuan((account?.balanceCents ?? 0) + (account?.creditCents ?? 0));
|
|
const todayRefundCents = Math.max(0, dashboard?.today.returnedCents ?? 0);
|
|
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),
|
|
createdAt: formatDateTime(task.createdAt ? String(task.createdAt) : null),
|
|
status: String(task.status ?? 'unknown'),
|
|
})), [dashboard]);
|
|
const latestRecharge = dashboard?.recentRecharges[0];
|
|
|
|
const sendTrendOption = useMemo(
|
|
() => createLineOption({
|
|
labels: dashboard?.hourlySendTrend.map((item) => item.label) ?? [],
|
|
series: [
|
|
{ name: '提交量', data: dashboard?.hourlySendTrend.map((item) => item.submittedCount) ?? [] },
|
|
{ name: '成功量', data: dashboard?.hourlySendTrend.map((item) => item.successCount) ?? [] },
|
|
],
|
|
}),
|
|
[dashboard],
|
|
);
|
|
|
|
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">
|
|
<div className="overview-hero">
|
|
<div>
|
|
<p className="eyebrow">客户端概览</p>
|
|
<h1>短信服务工作台</h1>
|
|
<p className="muted">查看账户余额、审核进度、发送趋势和常用业务入口。</p>
|
|
</div>
|
|
<div className="page-actions">
|
|
<Button icon={<Plus size={16} />} onClick={() => navigate('/client/templates')} variant="ghost">
|
|
新建模板
|
|
</Button>
|
|
<Button icon={<Send size={16} />} onClick={() => navigate('/client/send')}>发送短信</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="dashboard-grid dashboard-grid--four">
|
|
<div className="surface metric-card metric-card--featured">
|
|
<span>可用发送额度</span>
|
|
<strong>¥{formatAmount(availableBalance)}</strong>
|
|
<small>现金余额 ¥{formatCents(account?.balanceCents)}</small>
|
|
</div>
|
|
<div className="surface metric-card">
|
|
<span>今日发送</span>
|
|
<strong>{(dashboard?.today.sent ?? 0).toLocaleString('zh-CN')}</strong>
|
|
<small>成功率 {dashboard?.today.successRate ?? 0}%</small>
|
|
</div>
|
|
<div className="surface metric-card">
|
|
<span>今日消费金额</span>
|
|
<strong>¥{formatCents(dashboard?.today.spendCents)}</strong>
|
|
<small>企业今日实际消费</small>
|
|
</div>
|
|
<div className="surface metric-card">
|
|
<span>今日返还金额</span>
|
|
<strong>¥{formatCents(todayRefundCents)}</strong>
|
|
<small>异常回执与退费返还</small>
|
|
</div>
|
|
</div>
|
|
{error ? <div className="surface ui-table__empty">{error}</div> : null}
|
|
|
|
<div className="overview-grid">
|
|
<div className="surface section-stack">
|
|
<div className="section-heading">
|
|
<div>
|
|
<h2>快捷操作</h2>
|
|
<p className="muted">覆盖概览页常用入口。</p>
|
|
</div>
|
|
</div>
|
|
<div className="quick-grid">
|
|
<button className="quick-action" onClick={() => navigate('/client/send')} type="button">
|
|
<Send size={20} />
|
|
<span>短信发送</span>
|
|
<small>创建发送批次</small>
|
|
</button>
|
|
<button className="quick-action" onClick={() => navigate('/client/templates')} type="button">
|
|
<FileText size={20} />
|
|
<span>模板管理</span>
|
|
<small>进入模板列表</small>
|
|
</button>
|
|
<button className="quick-action" onClick={() => navigate('/client/signatures')} type="button">
|
|
<PenLine size={20} />
|
|
<span>签名管理</span>
|
|
<small>进入签名列表</small>
|
|
</button>
|
|
<button className="quick-action" onClick={() => navigate('/client/billing')} type="button">
|
|
<WalletCards size={20} />
|
|
<span>账户余额</span>
|
|
<small>查看余额与流水</small>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="surface section-stack">
|
|
<div className="section-heading">
|
|
<div>
|
|
<h2>账户状态</h2>
|
|
<p className="muted">企业认证与资源用量。</p>
|
|
</div>
|
|
<Tag tone={dashboard?.clientOverview?.certificationStatus === 'certified' ? 'success' : 'warning'}>
|
|
{dashboard?.clientOverview?.certificationStatus === 'certified' ? '已认证' : '未认证'}
|
|
</Tag>
|
|
</div>
|
|
<div className="summary-list">
|
|
<div>
|
|
<span>企业主体</span>
|
|
<strong>{dashboard?.clientOverview?.enterpriseName ?? account?.tenant?.name ?? '企业信息未完善'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>签名数量</span>
|
|
<strong>{dashboard?.clientOverview?.signatureCount ?? 0} 个</strong>
|
|
</div>
|
|
<div>
|
|
<span>最近充值</span>
|
|
<strong>{latestRecharge ? `¥${formatCents(latestRecharge.amountCents)}` : '暂无充值'}</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overview-grid overview-grid--three">
|
|
<button className="surface mini-status-card mini-status-card--action" onClick={() => navigate('/client/templates')} type="button">
|
|
<BadgeCheck size={22} />
|
|
<div>
|
|
<span>模板状态</span>
|
|
<strong>{dashboard?.pendingAudits.templates ?? 0} 个待审核</strong>
|
|
<small>点击进入模板明细</small>
|
|
</div>
|
|
</button>
|
|
<button className="surface mini-status-card mini-status-card--action" onClick={() => navigate('/client/signatures')} type="button">
|
|
<PenLine size={22} />
|
|
<div>
|
|
<span>签名状态</span>
|
|
<strong>{dashboard?.pendingAudits.signatures ?? 0} 个待审核</strong>
|
|
<small>点击进入签名明细</small>
|
|
</div>
|
|
</button>
|
|
<button className="surface mini-status-card mini-status-card--action" onClick={() => navigate('/client/batch-tasks')} type="button">
|
|
<ClipboardList size={22} />
|
|
<div>
|
|
<span>批量任务</span>
|
|
<strong>{dashboard?.clientOverview?.pendingBatchTaskCount ?? 0} 个待审核</strong>
|
|
<small>点击进入批量任务菜单</small>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="chart-grid">
|
|
<div className="surface chart-card">
|
|
<h2>今日发送趋势</h2>
|
|
<p className="muted">按北京时间逐小时展示提交量和成功量。</p>
|
|
<Chart height={300} option={sendTrendOption} />
|
|
</div>
|
|
<div className="surface chart-card">
|
|
<h2>运营商占比</h2>
|
|
<p className="muted">按今日提交量估算。</p>
|
|
<Chart height={300} option={channelShareOption} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="surface section-stack">
|
|
<div className="section-heading">
|
|
<div>
|
|
<h2>最近发送批次</h2>
|
|
<p className="muted">跟踪发送状态、通道和创建时间。</p>
|
|
</div>
|
|
<Button icon={<ReceiptText size={16} />} onClick={() => navigate('/client/send-detail')} variant="ghost">
|
|
查看全部
|
|
</Button>
|
|
</div>
|
|
<Table columns={columns} data={recentMessages} rowKey="id" />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|