Files
lislgosms/src/apps/admin/AdminMmsTaskProgressPage.tsx
T

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>
);
}