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
+325
View File
@@ -0,0 +1,325 @@
import { Fragment, useMemo, useState } from 'react';
import { FileText, Search, Smartphone } from 'lucide-react';
import {
DateRangeInput,
Input,
QueryPanel,
Select,
Tag,
type DateRangeValue,
} from '@/components/ui';
type SendStatus = 'success' | 'unknown' | 'failed';
type SmsSendDetail = {
id: string;
applicationName: string;
sentAt: string;
content: string;
wordCount: number;
billingCount: number;
phone: string;
carrier: '中国移动' | '中国联通' | '中国电信';
region: string;
status: SendStatus;
receipt: 'DELIVRD' | 'UNKNOWN' | 'UNDELIV';
receiptAt?: string;
};
const statusLabelMap: Record<SendStatus, string> = {
success: '成功',
unknown: '未知',
failed: '失败',
};
const statusToneMap: Record<SendStatus, 'success' | 'info' | 'danger'> = {
success: 'success',
unknown: 'info',
failed: 'danger',
};
const sendDetailRows: SmsSendDetail[] = [
{
id: 'SMSD20260316001',
applicationName: '营销推广平台',
sentAt: '2026-03-16 10:30:15',
content: '【启瑞物业】尊敬的业主,您本月物业费500元,请及时缴纳。感谢您的配合!',
wordCount: 45,
billingCount: 1,
phone: '13800138000',
carrier: '中国移动',
region: '北京市',
status: 'success',
receipt: 'DELIVRD',
receiptAt: '2026-03-16 10:30:18',
},
{
id: 'SMSD20260316002',
applicationName: '客服系统',
sentAt: '2026-03-16 10:32:20',
content: '【客服中心】尊敬的张先生,您已成功预约上门维修服务,时间:2026-03-18 14:00。',
wordCount: 48,
billingCount: 1,
phone: '13900139000',
carrier: '中国联通',
region: '上海市',
status: 'success',
receipt: 'DELIVRD',
receiptAt: '2026-03-16 10:32:25',
},
{
id: 'SMSD20260316003',
applicationName: '验证码服务',
sentAt: '2026-03-16 10:35:40',
content: '【验证码】您的验证码是123456,5分钟内有效,请勿泄露给他人。',
wordCount: 34,
billingCount: 1,
phone: '13700137000',
carrier: '中国电信',
region: '深圳市',
status: 'success',
receipt: 'DELIVRD',
receiptAt: '2026-03-16 10:35:43',
},
{
id: 'SMSD20260316004',
applicationName: '营销推广平台',
sentAt: '2026-03-16 10:38:10',
content: '【启瑞物业】您好!春季业主大会将于2026-03-20在小区会议室举行,欢迎参加。',
wordCount: 46,
billingCount: 1,
phone: '13600136000',
carrier: '中国移动',
region: '广州市',
status: 'unknown',
receipt: 'UNKNOWN',
},
{
id: 'SMSD20260316005',
applicationName: '验证码服务',
sentAt: '2026-03-16 10:40:30',
content: '【验证码】您的验证码是654321,5分钟内有效,请勿泄露给他人。',
wordCount: 34,
billingCount: 1,
phone: '13500135000',
carrier: '中国联通',
region: '杭州市',
status: 'success',
receipt: 'DELIVRD',
receiptAt: '2026-03-16 10:40:33',
},
{
id: 'SMSD20260316006',
applicationName: '订单通知系统',
sentAt: '2026-03-16 10:45:12',
content: '【订单通知】您的订单已发货,请留意物流信息。',
wordCount: 28,
billingCount: 1,
phone: '18800188000',
carrier: '中国电信',
region: '成都市',
status: 'failed',
receipt: 'UNDELIV',
},
{
id: 'SMSD20260316007',
applicationName: '营销推广平台',
sentAt: '2026-03-16 10:48:01',
content: '【启瑞物业】尊敬的客户,值此佳节之际,祝您节日快乐,阖家幸福。',
wordCount: 39,
billingCount: 1,
phone: '15900159000',
carrier: '中国移动',
region: '武汉市',
status: 'success',
receipt: 'DELIVRD',
receiptAt: '2026-03-16 10:48:06',
},
{
id: 'SMSD20260316008',
applicationName: '客服系统',
sentAt: '2026-03-16 10:51:36',
content: '【客服中心】您的服务工单已受理,工作人员将在24小时内联系您。',
wordCount: 36,
billingCount: 1,
phone: '15000150000',
carrier: '中国联通',
region: '南京市',
status: 'unknown',
receipt: 'UNKNOWN',
},
{
id: 'SMSD20260316009',
applicationName: '订单通知系统',
sentAt: '2026-03-16 10:55:44',
content: '【订单通知】您的退款申请已提交,预计1-3个工作日内到账。',
wordCount: 35,
billingCount: 1,
phone: '18900189000',
carrier: '中国电信',
region: '西安市',
status: 'success',
receipt: 'DELIVRD',
receiptAt: '2026-03-16 10:55:49',
},
{
id: 'SMSD20260316010',
applicationName: '验证码服务',
sentAt: '2026-03-16 10:59:18',
content: '【验证码】您的登录验证码为908172,请在5分钟内完成验证。',
wordCount: 33,
billingCount: 1,
phone: '13200132000',
carrier: '中国移动',
region: '重庆市',
status: 'success',
receipt: 'DELIVRD',
receiptAt: '2026-03-16 10:59:21',
},
];
function getDate(value: string) {
return value.slice(0, 10);
}
export function ClientSendDetailPage() {
const [applicationName, setApplicationName] = useState('all');
const [status, setStatus] = useState('all');
const [dateRange, setDateRange] = useState<DateRangeValue>({});
const [contentKeyword, setContentKeyword] = useState('');
const [phoneKeyword, setPhoneKeyword] = useState('');
const applicationOptions = useMemo(() => {
const applications = Array.from(new Set(sendDetailRows.map((item) => item.applicationName)));
return [
{ label: '全部应用', value: 'all' },
...applications.map((item) => ({ label: item, value: item })),
];
}, []);
const filteredRows = sendDetailRows.filter((item) => {
const matchesApplication = applicationName === 'all' || item.applicationName === applicationName;
const matchesStatus = status === 'all' || item.status === status;
const sentDate = getDate(item.sentAt);
const matchesStartDate = !dateRange.start || sentDate >= dateRange.start;
const matchesEndDate = !dateRange.end || sentDate <= dateRange.end;
const matchesContent = !contentKeyword || item.content.includes(contentKeyword);
const matchesPhone = !phoneKeyword || item.phone.includes(phoneKeyword);
return matchesApplication && matchesStatus && matchesStartDate && matchesEndDate && matchesContent && matchesPhone;
});
return (
<section className="page-stack">
<div className="sms-send-title">
<span className="sms-send-title__icon">
<FileText size={22} />
</span>
<h1></h1>
</div>
<QueryPanel
title="查询条件"
summary={<> <strong>{filteredRows.length}</strong> </>}
>
<Select label="应用名称" onChange={(event) => setApplicationName(event.target.value)} options={applicationOptions} value={applicationName} />
<DateRangeInput label="发送时间" onChange={setDateRange} value={dateRange} />
<Select
label="发送状态"
onChange={(event) => setStatus(event.target.value)}
options={[
{ label: '全部', value: 'all' },
{ label: '成功', value: 'success' },
{ label: '未知', value: 'unknown' },
{ label: '失败', value: 'failed' },
]}
value={status}
/>
<Input
label="短信内容"
onChange={(event) => setContentKeyword(event.target.value)}
placeholder="输入关键词搜索"
prefix={<Search size={16} />}
value={contentKeyword}
/>
<Input
label="手机号码"
onChange={(event) => setPhoneKeyword(event.target.value)}
placeholder="输入手机号搜索"
prefix={<Smartphone size={16} />}
value={phoneKeyword}
/>
</QueryPanel>
<div className="surface send-detail-table-card">
<div className="ui-table-wrap">
<table className="ui-table send-detail-table">
<thead>
<tr>
<th style={{ width: '112px' }}></th>
<th style={{ width: '128px' }}></th>
<th style={{ width: '90px', textAlign: 'center' }}>/</th>
<th style={{ width: '130px' }}></th>
<th style={{ width: '90px' }}></th>
<th style={{ width: '90px' }}></th>
<th style={{ width: '96px', textAlign: 'center' }}></th>
<th style={{ width: '95px', textAlign: 'center' }}></th>
<th style={{ width: '128px' }}></th>
</tr>
</thead>
<tbody>
{filteredRows.length === 0 ? (
<tr>
<td className="ui-table__empty" colSpan={9}></td>
</tr>
) : filteredRows.map((record) => (
<Fragment key={record.id}>
<tr className="send-detail-main-row">
<td><strong className="send-detail-app-name">{record.applicationName}</strong></td>
<td>
<span className="send-detail-time">
{record.sentAt.slice(0, 10)}
<small>{record.sentAt.slice(11)}</small>
</span>
</td>
<td style={{ textAlign: 'center' }}>
<span className="send-detail-count">
<strong>{record.wordCount}</strong>
<small>{record.billingCount}</small>
</span>
</td>
<td><strong>{record.phone}</strong></td>
<td><strong className="send-detail-carrier">{record.carrier}</strong></td>
<td>
<span className="send-detail-region">
{record.region.slice(0, 2)}
<small>{record.region.slice(2)}</small>
</span>
</td>
<td style={{ textAlign: 'center' }}><Tag tone={statusToneMap[record.status]}>{statusLabelMap[record.status]}</Tag></td>
<td style={{ textAlign: 'center' }}><strong className="send-detail-receipt-code">{record.receipt}</strong></td>
<td>
{record.receiptAt ? (
<span className="send-detail-time">
{record.receiptAt.slice(0, 10)}
<small>{record.receiptAt.slice(11)}</small>
</span>
) : <span className="muted">-</span>}
</td>
</tr>
<tr className="send-detail-content-row">
<td colSpan={9}>
<div className="send-detail-content-block">
<span></span>
<p>{record.content}</p>
</div>
</td>
</tr>
</Fragment>
))}
</tbody>
</table>
</div>
</div>
</section>
);
}