Initial CMPP frontend prototype

This commit is contained in:
hectorzhao
2026-06-30 16:09:46 +08:00
commit 2f3c274a30
98 changed files with 25255 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
import { useMemo } from 'react';
import {
BadgeCheck,
BellRing,
FileText,
PenLine,
Plus,
ReceiptText,
Send,
WalletCards,
} 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 { createLineOption, createPieOption } from '@/theme/chartOptions';
const statusLabelMap: Record<RecentMessage['status'], string> = {
success: '发送完成',
warning: '排队中',
info: '发送中',
danger: '发送失败',
};
const templateStatusLabelMap: Record<TemplateStatus, string> = {
draft: '草稿',
pending: '待审核',
approved: '已通过',
rejected: '已驳回',
};
const columns: Array<TableColumn<RecentMessage>> = [
{ key: 'id', title: '批次编号', render: (record) => record.id },
{ 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> },
];
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 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 quotaTotal = overview.availableMessages + overview.todaySent;
const quotaPercent = Math.round((overview.availableMessages / quotaTotal) * 100);
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 channelShareOption = useMemo(() => createPieOption({ data: channelShare }), []);
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">
<div className="surface metric-card metric-card--featured">
<span></span>
<strong>{overview.availableMessages.toLocaleString('zh-CN')}</strong>
<small> {overview.todaySent.toLocaleString('zh-CN')} </small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{overview.successRate}%</strong>
<small> 24 </small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{pendingTemplates + pendingSignatures}</strong>
<small> {pendingTemplates} {pendingSignatures}</small>
</div>
</div>
<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>{approvedTemplates} </small>
</button>
<button className="quick-action" onClick={() => navigate('/client/signatures')} type="button">
<PenLine size={20} />
<span></span>
<small>{approvedSignatures} </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="success"></Tag>
</div>
<div className="summary-list">
<div>
<span></span>
<strong></strong>
</div>
<div>
<span></span>
<strong></strong>
</div>
<div>
<span></span>
<strong>{latestInvoice.title}</strong>
</div>
</div>
<div>
<div className="progress-heading">
<span></span>
<strong>{quotaPercent}%</strong>
</div>
<div className="progress-track">
<span style={{ width: `${quotaPercent}%` }} />
</div>
</div>
</div>
</div>
<div className="overview-grid overview-grid--three">
<div className="surface mini-status-card">
<BadgeCheck size={22} />
<div>
<span></span>
<strong>{approvedTemplates} </strong>
<small>{templates.map((item) => templateStatusLabelMap[item.status]).join(' / ')}</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>
</div>
</div>
<div className="surface mini-status-card">
<BellRing size={22} />
<div>
<span></span>
<strong></strong>
<small></small>
</div>
</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"></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>
);
}