Initial CMPP frontend prototype
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { BarChart3, CalendarClock, Eye, FileImage, ImageIcon, Search, TrendingUp } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
DateRangeInput,
|
||||
DetailInfoGrid,
|
||||
DetailProgressStats,
|
||||
DetailSection,
|
||||
DetailTitle,
|
||||
getRateTone,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
ProgressBar,
|
||||
RateCard,
|
||||
RateOverview,
|
||||
Select,
|
||||
Table,
|
||||
Tag,
|
||||
type DateRangeValue,
|
||||
type TableColumn,
|
||||
} from '@/components/ui';
|
||||
|
||||
type MmsTaskStatus = 'completed' | 'sending' | 'terminated' | 'failed';
|
||||
type SendType = 'immediate' | 'scheduled';
|
||||
|
||||
type CarrierStat = {
|
||||
name: string;
|
||||
success: number;
|
||||
total: number;
|
||||
rate: number;
|
||||
};
|
||||
|
||||
type CityStat = {
|
||||
city: string;
|
||||
total: number;
|
||||
success: number;
|
||||
};
|
||||
|
||||
type MmsTask = {
|
||||
id: string;
|
||||
enterprise: string;
|
||||
applicationName: string;
|
||||
submittedAt: string;
|
||||
title: string;
|
||||
content: string;
|
||||
image: string;
|
||||
attachment: string;
|
||||
phoneCount: number;
|
||||
sentCount: number;
|
||||
totalCount: number;
|
||||
sendType: SendType;
|
||||
scheduledAt?: string;
|
||||
status: MmsTaskStatus;
|
||||
carrierStats: CarrierStat[];
|
||||
cityStats: CityStat[];
|
||||
};
|
||||
|
||||
const statusToneMap: Record<MmsTaskStatus, 'success' | 'info' | 'neutral' | 'danger'> = {
|
||||
completed: 'success',
|
||||
sending: 'info',
|
||||
terminated: 'neutral',
|
||||
failed: 'danger',
|
||||
};
|
||||
|
||||
const statusLabelMap: Record<MmsTaskStatus, string> = {
|
||||
completed: '已完成',
|
||||
sending: '发送中',
|
||||
terminated: '已终止',
|
||||
failed: '失败',
|
||||
};
|
||||
|
||||
const sendTypeLabels: Record<SendType, string> = {
|
||||
immediate: '立即发送',
|
||||
scheduled: '定时发送',
|
||||
};
|
||||
|
||||
const defaultCarrierStats: CarrierStat[] = [
|
||||
{ name: '中国移动', success: 1450, total: 1502, rate: 96.5 },
|
||||
{ name: '中国联通', success: 1138, total: 1200, rate: 94.8 },
|
||||
{ name: '中国电信', success: 762, total: 800, rate: 95.2 },
|
||||
];
|
||||
|
||||
const defaultCityStats: CityStat[] = [
|
||||
{ city: '成都', total: 1250, success: 1200 },
|
||||
{ city: '重庆', total: 1000, success: 950 },
|
||||
{ city: '绵阳', total: 600, success: 570 },
|
||||
{ city: '泸州', total: 500, success: 475 },
|
||||
{ city: '宜宾', total: 300, success: 285 },
|
||||
];
|
||||
|
||||
const taskSeed: MmsTask[] = [
|
||||
{
|
||||
id: 'MMS202603120001',
|
||||
enterprise: '四川骠骑企业管理',
|
||||
applicationName: '营销应用1',
|
||||
submittedAt: '2026-03-12 09:30:15',
|
||||
title: '春季新品发布会邀请函',
|
||||
content: '【骠骑科技】尊敬的客户,我们诚挚邀请您参加春季新品发布会,现场将展示多款创新产品,精彩活动等您参与!活动时间:3月20日下午2点,期待您的光临!',
|
||||
image: 'https://images.unsplash.com/photo-1519671482749-fd09be7ccebf?auto=format&fit=crop&w=500&q=80',
|
||||
attachment: '3张图片',
|
||||
phoneCount: 5000,
|
||||
sentCount: 3500,
|
||||
totalCount: 5000,
|
||||
sendType: 'immediate',
|
||||
status: 'sending',
|
||||
carrierStats: defaultCarrierStats,
|
||||
cityStats: defaultCityStats,
|
||||
},
|
||||
{
|
||||
id: 'MMS202603120002',
|
||||
enterprise: '重庆进载数智',
|
||||
applicationName: '通知应用',
|
||||
submittedAt: '2026-03-12 10:15:00',
|
||||
title: '会员升级通知',
|
||||
content: '【进载数智】尊敬的VIP会员,恭喜您的会员等级已升级至钻石级别!查看您的专属权益,享受更多优质服务。',
|
||||
image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=500&q=80',
|
||||
attachment: '2张图片',
|
||||
phoneCount: 3000,
|
||||
sentCount: 3000,
|
||||
totalCount: 3000,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-13 08:00:00',
|
||||
status: 'completed',
|
||||
carrierStats: defaultCarrierStats,
|
||||
cityStats: defaultCityStats,
|
||||
},
|
||||
{
|
||||
id: 'MMS202603120003',
|
||||
enterprise: '超感世纪三三网',
|
||||
applicationName: '推广应用2',
|
||||
submittedAt: '2026-03-12 11:20:00',
|
||||
title: '限时优惠活动',
|
||||
content: '【超感世纪】春季大促来袭!全场商品5折起,精选商品低至3折!更有满减优惠,买一送一活动等您参与。',
|
||||
image: 'https://images.unsplash.com/photo-1607083206968-13611e3d76db?auto=format&fit=crop&w=500&q=80',
|
||||
attachment: '4张图片',
|
||||
phoneCount: 8000,
|
||||
sentCount: 6000,
|
||||
totalCount: 8000,
|
||||
sendType: 'immediate',
|
||||
status: 'sending',
|
||||
carrierStats: defaultCarrierStats,
|
||||
cityStats: defaultCityStats,
|
||||
},
|
||||
{
|
||||
id: 'MMS202603120004',
|
||||
enterprise: '重庆香惠慧',
|
||||
applicationName: '客服应用',
|
||||
submittedAt: '2026-03-12 14:05:00',
|
||||
title: '产品使用指南',
|
||||
content: '【香惠慧】感谢您选择我们的产品!为了帮助您更好地了解和使用我们的服务,特为您准备了产品使用指南。',
|
||||
image: 'https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?auto=format&fit=crop&w=500&q=80',
|
||||
attachment: '1张图片',
|
||||
phoneCount: 2000,
|
||||
sentCount: 2000,
|
||||
totalCount: 2000,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-12 16:00:00',
|
||||
status: 'completed',
|
||||
carrierStats: defaultCarrierStats,
|
||||
cityStats: defaultCityStats,
|
||||
},
|
||||
{
|
||||
id: 'MMS202603120005',
|
||||
enterprise: '四川骠骑企业管理',
|
||||
applicationName: '活动推广',
|
||||
submittedAt: '2026-03-12 15:30:00',
|
||||
title: '周末特惠活动',
|
||||
content: '【骠骑科技】周末特惠活动开始啦!精美图片抢先看,超值优惠不容错过!点击查看活动详情。',
|
||||
image: 'https://images.unsplash.com/photo-1607082350899-7e105aa886ae?auto=format&fit=crop&w=500&q=80',
|
||||
attachment: '3张图片',
|
||||
phoneCount: 4000,
|
||||
sentCount: 1000,
|
||||
totalCount: 4000,
|
||||
sendType: 'immediate',
|
||||
status: 'terminated',
|
||||
carrierStats: defaultCarrierStats,
|
||||
cityStats: defaultCityStats,
|
||||
},
|
||||
{
|
||||
id: 'MMS202603110006',
|
||||
enterprise: '重庆进载数智',
|
||||
applicationName: '系统通知',
|
||||
submittedAt: '2026-03-11 17:45:00',
|
||||
title: '系统维护通知',
|
||||
content: '【进载数智】系统维护通知:我们将于今晚进行系统升级,预计耗时2小时。维护期间部分服务可能受影响。',
|
||||
image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=500&q=80',
|
||||
attachment: '1张图片',
|
||||
phoneCount: 6000,
|
||||
sentCount: 4000,
|
||||
totalCount: 6000,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-11 20:00:00',
|
||||
status: 'failed',
|
||||
carrierStats: defaultCarrierStats,
|
||||
cityStats: defaultCityStats,
|
||||
},
|
||||
];
|
||||
|
||||
function formatNumber(value: number) {
|
||||
return value.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getProgress(task: MmsTask) {
|
||||
return Math.round((task.sentCount / task.totalCount) * 100);
|
||||
}
|
||||
|
||||
function getDeliveredCount(task: MmsTask) {
|
||||
if (task.status === 'completed') {
|
||||
return Math.round(task.totalCount * 0.96);
|
||||
}
|
||||
|
||||
if (task.status === 'failed') {
|
||||
return Math.round(task.sentCount * 0.82);
|
||||
}
|
||||
|
||||
return Math.round(task.sentCount * 0.95);
|
||||
}
|
||||
|
||||
function MmsContent({ task }: { task: MmsTask }) {
|
||||
return (
|
||||
<div className="mms-task-content">
|
||||
<img alt={task.title} src={task.image} />
|
||||
<div>
|
||||
<strong>{task.title}</strong>
|
||||
<p>{task.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDetailModal({ task, onClose }: { task: MmsTask; onClose: () => void }) {
|
||||
const progress = getProgress(task);
|
||||
const deliveredCount = getDeliveredCount(task);
|
||||
const overallRate = (deliveredCount / task.totalCount) * 100;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
footer={<Button onClick={onClose}>关闭</Button>}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
title={<DetailTitle title="彩信任务详情" subtitle="查看任务的详细信息和进度。" />}
|
||||
>
|
||||
<div className="task-detail admin-mms-task-detail">
|
||||
<DetailSection title="基本信息" extra={<Tag tone={statusToneMap[task.status]}>{statusLabelMap[task.status]}</Tag>}>
|
||||
<DetailInfoGrid
|
||||
items={[
|
||||
{ label: '任务编号', value: task.id },
|
||||
{ label: '企业名称', value: task.enterprise },
|
||||
{ label: '应用名称', value: task.applicationName },
|
||||
{ label: '提交时间', value: task.submittedAt },
|
||||
{ label: '发送方式', value: sendTypeLabels[task.sendType] },
|
||||
{ label: '号码数', value: `${formatNumber(task.phoneCount)} 个`, tone: 'primary' },
|
||||
{
|
||||
label: '彩信内容',
|
||||
value: (
|
||||
<div className="mms-detail-template">
|
||||
<img alt={task.title} src={task.image} />
|
||||
<div><strong>{task.title}</strong><p>{task.content}</p><small>附件:{task.attachment}</small></div>
|
||||
</div>
|
||||
),
|
||||
full: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="发送进度">
|
||||
<DetailProgressStats
|
||||
label="任务进度"
|
||||
meta={`已发送 ${formatNumber(task.sentCount)} / 总计 ${formatNumber(task.totalCount)}`}
|
||||
percent={progress}
|
||||
status={task.status === 'failed' ? 'terminated' : task.status}
|
||||
stats={[
|
||||
{ label: '提交总数量', value: formatNumber(task.totalCount) },
|
||||
{ label: '已处理数量', value: formatNumber(task.sentCount) },
|
||||
{ label: '发送成功数量', value: formatNumber(deliveredCount) },
|
||||
]}
|
||||
/>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title={<><TrendingUp size={20} /> 按运营商统计成功率</>}>
|
||||
<RateOverview
|
||||
label="总体成功率"
|
||||
metrics={[
|
||||
{ label: '成功总数', value: formatNumber(deliveredCount) },
|
||||
{ label: '总计', value: formatNumber(task.totalCount) },
|
||||
]}
|
||||
rate={overallRate}
|
||||
tone={getRateTone(overallRate)}
|
||||
/>
|
||||
<div className="carrier-rate-grid">
|
||||
{task.carrierStats.map((item) => (
|
||||
<RateCard
|
||||
key={item.name}
|
||||
meta={<><span>{formatNumber(item.success)}</span><span>/ {formatNumber(item.total)}</span></>}
|
||||
rate={item.rate}
|
||||
title={item.name}
|
||||
tone={getRateTone(item.rate)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<h4>按城市统计成功率</h4>
|
||||
<div className="admin-mms-city-list">
|
||||
{task.cityStats.map((item) => {
|
||||
const rate = (item.success / item.total) * 100;
|
||||
return (
|
||||
<div key={item.city}>
|
||||
<strong>{item.city}</strong>
|
||||
<span>{formatNumber(item.success)} / {formatNumber(item.total)}</span>
|
||||
<b>{rate.toFixed(1)}%</b>
|
||||
<ProgressBar percent={rate} tone={getRateTone(rate)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DetailSection>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewModal({ task, onClose }: { task: MmsTask; onClose: () => void }) {
|
||||
return (
|
||||
<Modal
|
||||
footer={<Button onClick={onClose}>关闭</Button>}
|
||||
onClose={onClose}
|
||||
open
|
||||
title={<div className="template-modal-title"><h2>彩信预览</h2><p>{task.id}</p></div>}
|
||||
>
|
||||
<div className="mms-preview">
|
||||
<img alt={task.title} src={task.image} />
|
||||
<h3>{task.title}</h3>
|
||||
<p>{task.content}</p>
|
||||
<div className="mms-preview-frames"><span>{task.attachment}</span><span>图文彩信</span></div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminMmsTaskProgressPage() {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [enterprise, setEnterprise] = useState('all');
|
||||
const [application, setApplication] = useState('all');
|
||||
const [submittedDateRange, setSubmittedDateRange] = useState<DateRangeValue>({});
|
||||
const [selectedTask, setSelectedTask] = useState<MmsTask | null>(null);
|
||||
const [previewTask, setPreviewTask] = useState<MmsTask | null>(null);
|
||||
|
||||
const enterpriseOptions = useMemo(() => {
|
||||
const names = Array.from(new Set(taskSeed.map((item) => item.enterprise)));
|
||||
return [{ label: '全部企业', value: 'all' }, ...names.map((name) => ({ label: name, value: name }))];
|
||||
}, []);
|
||||
|
||||
const applicationOptions = useMemo(() => {
|
||||
const names = Array.from(new Set(taskSeed.filter((item) => enterprise === 'all' || item.enterprise === enterprise).map((item) => item.applicationName)));
|
||||
return [{ label: '全部应用', value: 'all' }, ...names.map((name) => ({ label: name, value: name }))];
|
||||
}, [enterprise]);
|
||||
|
||||
const filteredTasks = useMemo(
|
||||
() => taskSeed.filter((item) => {
|
||||
const submittedDate = item.submittedAt.slice(0, 10);
|
||||
const matchesKeyword = !keyword || item.id.includes(keyword);
|
||||
const matchesEnterprise = enterprise === 'all' || item.enterprise === enterprise;
|
||||
const matchesApplication = application === 'all' || item.applicationName === application;
|
||||
const matchesStartDate = !submittedDateRange.start || submittedDate >= submittedDateRange.start;
|
||||
const matchesEndDate = !submittedDateRange.end || submittedDate <= submittedDateRange.end;
|
||||
return matchesKeyword && matchesEnterprise && matchesApplication && matchesStartDate && matchesEndDate;
|
||||
}),
|
||||
[application, enterprise, keyword, submittedDateRange.end, submittedDateRange.start],
|
||||
);
|
||||
|
||||
function resetFilters() {
|
||||
setKeyword('');
|
||||
setEnterprise('all');
|
||||
setApplication('all');
|
||||
setSubmittedDateRange({});
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<MmsTask>> = [
|
||||
{ key: 'id', title: '任务编号', width: '150px', render: (record) => <strong className="admin-task-id">{record.id}</strong> },
|
||||
{
|
||||
key: 'enterprise',
|
||||
title: '企业/应用',
|
||||
width: '180px',
|
||||
render: (record) => (
|
||||
<div className="admin-task-enterprise">
|
||||
<strong>{record.enterprise}</strong>
|
||||
<span>{record.applicationName}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'submittedAt', title: '提交时间', width: '116px', render: (record) => <span>{record.submittedAt.slice(0, 10)}<br />{record.submittedAt.slice(11, 16)}</span> },
|
||||
{ key: 'content', title: '模板内容', width: '420px', render: (record) => <MmsContent task={record} /> },
|
||||
{ key: 'phoneCount', title: '号码数', align: 'right', width: '90px', render: (record) => <strong>{formatNumber(record.phoneCount)}</strong> },
|
||||
{
|
||||
key: 'sendType',
|
||||
title: '发送方式',
|
||||
width: '150px',
|
||||
render: (record) => (
|
||||
<div className="admin-task-send-type">
|
||||
<Tag tone={record.sendType === 'immediate' ? 'info' : 'warning'}>
|
||||
{record.sendType === 'scheduled' ? <CalendarClock size={13} /> : null}
|
||||
{sendTypeLabels[record.sendType]}
|
||||
</Tag>
|
||||
{record.scheduledAt ? <span>{record.scheduledAt.slice(0, 10)}<br />{record.scheduledAt.slice(11, 16)}</span> : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'progress',
|
||||
title: '进度',
|
||||
width: '180px',
|
||||
render: (record) => {
|
||||
const progress = getProgress(record);
|
||||
return (
|
||||
<div className="batch-progress admin-task-list-progress">
|
||||
<div>
|
||||
<span>{formatNumber(record.sentCount)}/{formatNumber(record.totalCount)}</span>
|
||||
<strong>{progress}%</strong>
|
||||
</div>
|
||||
<div className="batch-progress__track">
|
||||
<span className={`batch-progress__bar batch-progress__bar--${record.status === 'failed' ? 'terminated' : record.status}`} style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: 'status', title: '状态', width: '100px', render: (record) => <Tag tone={statusToneMap[record.status]}>{statusLabelMap[record.status]}</Tag> },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
width: '160px',
|
||||
render: (record) => (
|
||||
<div className="batch-actions mms-task-actions">
|
||||
<Button icon={<Eye size={14} />} onClick={() => setSelectedTask(record)} size="sm" variant="ghost">任务详情</Button>
|
||||
<Button icon={<ImageIcon size={14} />} onClick={() => setPreviewTask(record)} size="sm" variant="ghost">彩信预览</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-mms-task-page">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<div className="breadcrumb-line">发送任务 / <strong>彩信任务进度</strong></div>
|
||||
<h1>彩信任务进度</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-filter">
|
||||
<Input label="任务编号" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入任务编号" value={keyword} />
|
||||
<Select
|
||||
label="选择企业"
|
||||
onChange={(event) => {
|
||||
setEnterprise(event.target.value);
|
||||
setApplication('all');
|
||||
}}
|
||||
options={enterpriseOptions}
|
||||
value={enterprise}
|
||||
/>
|
||||
<Select label="选择应用" onChange={(event) => setApplication(event.target.value)} options={applicationOptions} value={application} />
|
||||
<DateRangeInput label="提交时间" onChange={setSubmittedDateRange} value={submittedDateRange} />
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button icon={<Search size={16} />}>查询</Button>
|
||||
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-table-card admin-mms-task-table-card">
|
||||
<Table columns={columns} data={filteredTasks} rowKey="id" />
|
||||
<Pagination total={filteredTasks.length} />
|
||||
</div>
|
||||
|
||||
{selectedTask ? <TaskDetailModal onClose={() => setSelectedTask(null)} task={selectedTask} /> : null}
|
||||
{previewTask ? <PreviewModal onClose={() => setPreviewTask(null)} task={previewTask} /> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user