483 lines
17 KiB
TypeScript
483 lines
17 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { BarChart3, CalendarClock, Eye, FileImage, ImageIcon, Search, TrendingUp } from 'lucide-react';
|
|
import {
|
|
Breadcrumb,
|
|
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>
|
|
<Breadcrumb items={['发送任务', '彩信任务进度']} />
|
|
<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>
|
|
);
|
|
}
|