fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { BarChart3, CalendarClock, Eye, MapPin, Search, Send, Smartphone, StopCircle, TrendingUp } from 'lucide-react';
|
||||
import { adminApi, type SmsBatchTask, type SmsMessageRecord } from '@/api/adminApi';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
@@ -24,15 +25,15 @@ type CarrierStat = {
|
||||
tone: 'mobile' | 'unicom' | 'telecom';
|
||||
};
|
||||
|
||||
type CityStat = {
|
||||
city: string;
|
||||
province: string;
|
||||
type RegionStat = {
|
||||
region: string;
|
||||
total: number;
|
||||
success: number;
|
||||
};
|
||||
|
||||
type SmsTask = {
|
||||
id: string;
|
||||
backendId: string;
|
||||
enterprise: string;
|
||||
application: string;
|
||||
submittedAt: string;
|
||||
@@ -41,14 +42,16 @@ type SmsTask = {
|
||||
wordCount: number;
|
||||
billingCount: number;
|
||||
sendType: SendType;
|
||||
scheduledAt?: string;
|
||||
scheduledAt?: string | null;
|
||||
submittedCount: number;
|
||||
submittedSuccess: number;
|
||||
sentCount: number;
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
status: TaskStatus;
|
||||
rawStatus: string;
|
||||
carriers: CarrierStat[];
|
||||
cities: CityStat[];
|
||||
regions: RegionStat[];
|
||||
};
|
||||
|
||||
const statusLabels: Record<TaskStatus, string> = {
|
||||
@@ -70,188 +73,102 @@ const sendTypeLabels: Record<SendType, string> = {
|
||||
scheduled: '定时发送',
|
||||
};
|
||||
|
||||
const taskData: SmsTask[] = [
|
||||
{
|
||||
id: 'TASK202603120001',
|
||||
enterprise: '四川骠骑企业管理',
|
||||
application: '营销应用1',
|
||||
submittedAt: '2026-03-12 09:30:15',
|
||||
templateContent: '【骠骑科技】尊敬的{name},您有一笔{amount}元的订单已确认,预计{date}送达。',
|
||||
phoneCount: 10000,
|
||||
wordCount: 68,
|
||||
billingCount: 14250,
|
||||
sendType: 'immediate',
|
||||
submittedCount: 10000,
|
||||
submittedSuccess: 7500,
|
||||
sentCount: 7500,
|
||||
successCount: 7125,
|
||||
status: 'sending',
|
||||
carriers: [
|
||||
{ name: '中国移动', total: 6000, success: 5760, tone: 'mobile' },
|
||||
{ name: '中国联通', total: 2500, success: 2350, tone: 'unicom' },
|
||||
{ name: '中国电信', total: 1500, success: 1425, tone: 'telecom' },
|
||||
],
|
||||
cities: [
|
||||
{ city: '成都市', province: '四川省', total: 1500, success: 1440 },
|
||||
{ city: '重庆市', province: '重庆市', total: 1200, success: 1140 },
|
||||
{ city: '北京市', province: '北京市', total: 1000, success: 970 },
|
||||
{ city: '上海市', province: '上海市', total: 1000, success: 960 },
|
||||
{ city: '深圳市', province: '广东省', total: 800, success: 760 },
|
||||
{ city: '广州市', province: '广东省', total: 700, success: 658 },
|
||||
{ city: '杭州市', province: '浙江省', total: 600, success: 576 },
|
||||
{ city: '南京市', province: '江苏省', total: 500, success: 475 },
|
||||
{ city: '武汉市', province: '湖北省', total: 500, success: 470 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'TASK202603120002',
|
||||
enterprise: '重庆进载数智',
|
||||
application: '通知应用',
|
||||
submittedAt: '2026-03-12 10:15:00',
|
||||
templateContent: '【进载数智】亲爱的用户,您的会员即将到期,请及时续费。',
|
||||
phoneCount: 5000,
|
||||
wordCount: 52,
|
||||
billingCount: 5000,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-13 08:00:00',
|
||||
submittedCount: 5000,
|
||||
submittedSuccess: 5000,
|
||||
sentCount: 5000,
|
||||
successCount: 4910,
|
||||
status: 'completed',
|
||||
carriers: [
|
||||
{ name: '中国移动', total: 3000, success: 2955, tone: 'mobile' },
|
||||
{ name: '中国联通', total: 1200, success: 1170, tone: 'unicom' },
|
||||
{ name: '中国电信', total: 800, success: 785, tone: 'telecom' },
|
||||
],
|
||||
cities: [
|
||||
{ city: '重庆市', province: '重庆市', total: 1200, success: 1180 },
|
||||
{ city: '成都市', province: '四川省', total: 900, success: 884 },
|
||||
{ city: '西安市', province: '陕西省', total: 700, success: 688 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'TASK202603120003',
|
||||
enterprise: '超感世纪三三网',
|
||||
application: '推广应用2',
|
||||
submittedAt: '2026-03-12 11:20:00',
|
||||
templateContent: '【超感世纪】新用户专享优惠,限时抢购中!点击链接查看详情:...',
|
||||
phoneCount: 20000,
|
||||
wordCount: 85,
|
||||
billingCount: 40000,
|
||||
sendType: 'immediate',
|
||||
submittedCount: 20000,
|
||||
submittedSuccess: 12000,
|
||||
sentCount: 12000,
|
||||
successCount: 11160,
|
||||
status: 'sending',
|
||||
carriers: [
|
||||
{ name: '中国移动', total: 12000, success: 11160, tone: 'mobile' },
|
||||
{ name: '中国联通', total: 5000, success: 4600, tone: 'unicom' },
|
||||
{ name: '中国电信', total: 3000, success: 2820, tone: 'telecom' },
|
||||
],
|
||||
cities: [
|
||||
{ city: '北京市', province: '北京市', total: 3000, success: 2820 },
|
||||
{ city: '上海市', province: '上海市', total: 2600, success: 2418 },
|
||||
{ city: '广州市', province: '广东省', total: 2300, success: 2139 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'TASK202603120004',
|
||||
enterprise: '重庆香惠慧',
|
||||
application: '客服应用',
|
||||
submittedAt: '2026-03-12 14:05:00',
|
||||
templateContent: '【香惠慧】您的验证码是{code},请在5分钟内完成验证。',
|
||||
phoneCount: 3000,
|
||||
wordCount: 45,
|
||||
billingCount: 3000,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-12 16:00:00',
|
||||
submittedCount: 3000,
|
||||
submittedSuccess: 3000,
|
||||
sentCount: 3000,
|
||||
successCount: 2962,
|
||||
status: 'completed',
|
||||
carriers: [
|
||||
{ name: '中国移动', total: 1700, success: 1682, tone: 'mobile' },
|
||||
{ name: '中国联通', total: 800, success: 790, tone: 'unicom' },
|
||||
{ name: '中国电信', total: 500, success: 490, tone: 'telecom' },
|
||||
],
|
||||
cities: [
|
||||
{ city: '重庆市', province: '重庆市', total: 1600, success: 1584 },
|
||||
{ city: '成都市', province: '四川省', total: 800, success: 786 },
|
||||
{ city: '贵阳市', province: '贵州省', total: 600, success: 592 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'TASK202603120005',
|
||||
enterprise: '四川骠骑企业管理',
|
||||
application: '活动推广',
|
||||
submittedAt: '2026-03-12 15:30:00',
|
||||
templateContent: '【骠骑科技】周末特惠活动开始啦!全场商品8折起,更多优惠请访问官网。',
|
||||
phoneCount: 8000,
|
||||
wordCount: 72,
|
||||
billingCount: 16000,
|
||||
sendType: 'immediate',
|
||||
submittedCount: 8000,
|
||||
submittedSuccess: 2000,
|
||||
sentCount: 2000,
|
||||
successCount: 1860,
|
||||
status: 'terminated',
|
||||
carriers: [
|
||||
{ name: '中国移动', total: 4800, success: 4464, tone: 'mobile' },
|
||||
{ name: '中国联通', total: 2000, success: 1840, tone: 'unicom' },
|
||||
{ name: '中国电信', total: 1200, success: 1116, tone: 'telecom' },
|
||||
],
|
||||
cities: [
|
||||
{ city: '成都市', province: '四川省', total: 1600, success: 1488 },
|
||||
{ city: '绵阳市', province: '四川省', total: 900, success: 837 },
|
||||
{ city: '德阳市', province: '四川省', total: 700, success: 651 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'TASK202603110006',
|
||||
enterprise: '重庆进载数智',
|
||||
application: '系统通知',
|
||||
submittedAt: '2026-03-11 17:45:00',
|
||||
templateContent: '【进载数智】系统维护通知:我们将于{date}进行系统升级,预计耗时2小时。',
|
||||
phoneCount: 15000,
|
||||
wordCount: 63,
|
||||
billingCount: 15000,
|
||||
sendType: 'scheduled',
|
||||
scheduledAt: '2026-03-11 20:00:00',
|
||||
submittedCount: 15000,
|
||||
submittedSuccess: 10000,
|
||||
sentCount: 10000,
|
||||
successCount: 8200,
|
||||
status: 'failed',
|
||||
carriers: [
|
||||
{ name: '中国移动', total: 9000, success: 7380, tone: 'mobile' },
|
||||
{ name: '中国联通', total: 3600, success: 2952, tone: 'unicom' },
|
||||
{ name: '中国电信', total: 2400, success: 1968, tone: 'telecom' },
|
||||
],
|
||||
cities: [
|
||||
{ city: '重庆市', province: '重庆市', total: 2600, success: 2132 },
|
||||
{ city: '成都市', province: '四川省', total: 1800, success: 1476 },
|
||||
{ city: '昆明市', province: '云南省', total: 1200, success: 984 },
|
||||
],
|
||||
},
|
||||
];
|
||||
const carrierLabels: Record<string, { label: string; tone: CarrierStat['tone'] }> = {
|
||||
mobile: { label: '中国移动', tone: 'mobile' },
|
||||
unicom: { label: '中国联通', tone: 'unicom' },
|
||||
telecom: { label: '中国电信', tone: 'telecom' },
|
||||
all: { label: '三网通道', tone: 'mobile' },
|
||||
};
|
||||
|
||||
function formatNumber(value: number) {
|
||||
return value.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatTime(value?: string | null) {
|
||||
return value ? `${value.slice(0, 10)} ${value.slice(11, 16)}` : '-';
|
||||
}
|
||||
|
||||
function normalizeTaskStatus(status: string): TaskStatus {
|
||||
if (['finished', 'completed', 'done'].includes(status)) return 'completed';
|
||||
if (['canceled', 'cancelled', 'terminated'].includes(status)) return 'terminated';
|
||||
if (['failed', 'rejected'].includes(status)) return 'failed';
|
||||
return 'sending';
|
||||
}
|
||||
|
||||
function countMessages(messages: SmsMessageRecord[] | undefined, statuses: string[]) {
|
||||
return (messages ?? []).filter((message) => statuses.includes(message.status)).length;
|
||||
}
|
||||
|
||||
function buildCarrierStats(messages: SmsMessageRecord[] | undefined): CarrierStat[] {
|
||||
const stats = new Map<string, CarrierStat>();
|
||||
(messages ?? []).forEach((message) => {
|
||||
const carrier = message.channel?.carrier ?? 'unknown';
|
||||
const meta = carrierLabels[carrier] ?? { label: carrier || '未知通道', tone: 'mobile' as const };
|
||||
const current = stats.get(carrier) ?? { name: meta.label, total: 0, success: 0, tone: meta.tone };
|
||||
current.total += 1;
|
||||
if (message.status === 'delivered') current.success += 1;
|
||||
stats.set(carrier, current);
|
||||
});
|
||||
return Array.from(stats.values());
|
||||
}
|
||||
|
||||
function buildRegionStats(messages: SmsMessageRecord[] | undefined): RegionStat[] {
|
||||
const stats = new Map<string, RegionStat>();
|
||||
(messages ?? []).forEach((message) => {
|
||||
const region = message.channel?.sendRegion ?? '未分配通道';
|
||||
const current = stats.get(region) ?? { region, total: 0, success: 0 };
|
||||
current.total += 1;
|
||||
if (message.status === 'delivered') current.success += 1;
|
||||
stats.set(region, current);
|
||||
});
|
||||
return Array.from(stats.values()).sort((a, b) => b.total - a.total);
|
||||
}
|
||||
|
||||
function mapTask(task: SmsBatchTask): SmsTask {
|
||||
const messages = task.messages ?? [];
|
||||
const submittedStatuses = ['submitted', 'delivered', 'failed', 'unknown', 'timeout', 'submit_failed'];
|
||||
const failedStatuses = ['failed', 'submit_failed', 'rejected', 'timeout'];
|
||||
const submittedCount = task.submittedTotal ?? countMessages(messages, submittedStatuses);
|
||||
const successCount = task.successTotal ?? countMessages(messages, ['delivered']);
|
||||
const failedCount = task.failedTotal ?? countMessages(messages, failedStatuses);
|
||||
const processedCount = submittedCount + (task.unknownTotal ?? 0) + (task.timeoutTotal ?? 0);
|
||||
const billingCount = messages.reduce((sum, message) => sum + (message.billingUnits ?? 0), 0)
|
||||
|| task.phoneTotal * (task.template?.billingUnits ?? Math.max(1, Math.ceil([...task.content].length / 67)));
|
||||
|
||||
return {
|
||||
id: task.taskNo || task.id,
|
||||
backendId: task.id,
|
||||
enterprise: task.tenant?.name ?? task.tenantId,
|
||||
application: task.application?.name ?? task.applicationId ?? '未绑定应用',
|
||||
submittedAt: task.createdAt,
|
||||
templateContent: task.content,
|
||||
phoneCount: task.phoneTotal,
|
||||
wordCount: [...task.content].length,
|
||||
billingCount,
|
||||
sendType: task.scheduledAt ? 'scheduled' : 'immediate',
|
||||
scheduledAt: task.scheduledAt,
|
||||
submittedCount,
|
||||
submittedSuccess: submittedCount,
|
||||
sentCount: Math.max(processedCount, successCount + failedCount),
|
||||
successCount,
|
||||
failedCount,
|
||||
status: normalizeTaskStatus(task.status),
|
||||
rawStatus: task.status,
|
||||
carriers: buildCarrierStats(messages),
|
||||
regions: buildRegionStats(messages),
|
||||
};
|
||||
}
|
||||
|
||||
function getProgress(task: SmsTask) {
|
||||
return Math.round((task.sentCount / task.phoneCount) * 100);
|
||||
return task.phoneCount > 0 ? Math.min(100, Math.round((task.sentCount / task.phoneCount) * 100)) : 0;
|
||||
}
|
||||
|
||||
function getSuccessRate(task: SmsTask) {
|
||||
return (task.successCount / task.submittedCount) * 100;
|
||||
return task.submittedCount > 0 ? (task.successCount / task.submittedCount) * 100 : 0;
|
||||
}
|
||||
|
||||
function getCityRate(city: CityStat) {
|
||||
return (city.success / city.total) * 100;
|
||||
function getRegionRate(region: RegionStat) {
|
||||
return region.total > 0 ? (region.success / region.total) * 100 : 0;
|
||||
}
|
||||
|
||||
function splitSignature(content: string) {
|
||||
@@ -286,6 +203,7 @@ function MetricCard({ label, value, tone }: { label: string; value: string; tone
|
||||
function TaskDetailModal({ task, onClose }: { task: SmsTask; onClose: () => void }) {
|
||||
const progress = getProgress(task);
|
||||
const successRate = getSuccessRate(task);
|
||||
const perPhoneBillingUnits = Math.max(1, Math.ceil(task.wordCount / 67));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -314,7 +232,7 @@ function TaskDetailModal({ task, onClose }: { task: SmsTask; onClose: () => void
|
||||
</div>
|
||||
<div>
|
||||
<dt>提交时间</dt>
|
||||
<dd>{task.submittedAt}</dd>
|
||||
<dd>{formatTime(task.submittedAt)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>发送方式</dt>
|
||||
@@ -327,7 +245,7 @@ function TaskDetailModal({ task, onClose }: { task: SmsTask; onClose: () => void
|
||||
<h3><TrendingUp size={18} />发送进度</h3>
|
||||
<div className="admin-task-progress-card">
|
||||
<div>
|
||||
<span>已发送 {formatNumber(task.sentCount)} / 总计 {formatNumber(task.phoneCount)}</span>
|
||||
<span>已处理 {formatNumber(task.sentCount)} / 总计 {formatNumber(task.phoneCount)}</span>
|
||||
<strong>{progress}%</strong>
|
||||
</div>
|
||||
<div className="batch-progress__track">
|
||||
@@ -347,52 +265,56 @@ function TaskDetailModal({ task, onClose }: { task: SmsTask; onClose: () => void
|
||||
<p className="admin-task-template">{task.templateContent}</p>
|
||||
</div>
|
||||
<dl className="admin-task-template-meta">
|
||||
<div><dt>字符数/计费条数</dt><dd>{task.wordCount} 字符 <b>·</b> {Math.max(1, Math.ceil(task.wordCount / 67))} 条/号码</dd></div>
|
||||
<div><dt>字符数/计费条数</dt><dd>{task.wordCount} 字符 <b>·</b> {perPhoneBillingUnits} 条/号码</dd></div>
|
||||
<div><dt>发送号码数</dt><dd>{formatNumber(task.phoneCount)} 个</dd></div>
|
||||
</dl>
|
||||
<div className="admin-task-billing-note">
|
||||
<span>计费规则:每 67 字为 1 条短信。本次任务单号码 {Math.max(1, Math.ceil(task.wordCount / 67))} 条,预计总计费 {formatNumber(task.billingCount)} 条</span>
|
||||
<span>计费规则:每 67 字为 1 条短信。本次任务单号码 {perPhoneBillingUnits} 条,预计总计费 {formatNumber(task.billingCount)} 条</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="admin-task-card admin-task-card--full">
|
||||
<h3><Smartphone size={18} />运营商分布</h3>
|
||||
<div className="admin-carrier-grid">
|
||||
{task.carriers.map((carrier) => {
|
||||
const rate = (carrier.success / carrier.total) * 100;
|
||||
return (
|
||||
<article className={`admin-carrier-card admin-carrier-card--${carrier.tone}`} key={carrier.name}>
|
||||
<strong>{carrier.name}</strong>
|
||||
<p><span>总数</span><b>{formatNumber(carrier.total)}</b></p>
|
||||
<p><span>成功</span><b>{formatNumber(carrier.success)}</b></p>
|
||||
<div>
|
||||
<em>{rate.toFixed(1)}%</em>
|
||||
<span>成功率</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<h3><Smartphone size={18} />通道运营商分布</h3>
|
||||
{task.carriers.length === 0 ? (
|
||||
<div className="admin-uplink-empty-match">暂无已分配通道记录</div>
|
||||
) : (
|
||||
<div className="admin-carrier-grid">
|
||||
{task.carriers.map((carrier) => {
|
||||
const rate = carrier.total > 0 ? (carrier.success / carrier.total) * 100 : 0;
|
||||
return (
|
||||
<article className={`admin-carrier-card admin-carrier-card--${carrier.tone}`} key={carrier.name}>
|
||||
<strong>{carrier.name}</strong>
|
||||
<p><span>总数</span><b>{formatNumber(carrier.total)}</b></p>
|
||||
<p><span>成功</span><b>{formatNumber(carrier.success)}</b></p>
|
||||
<div>
|
||||
<em>{rate.toFixed(1)}%</em>
|
||||
<span>成功率</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-task-card admin-task-card--full">
|
||||
<h3><MapPin size={18} />城市分布</h3>
|
||||
<h3><MapPin size={18} />发送地区分布</h3>
|
||||
<Table
|
||||
columns={[
|
||||
{ key: 'city', title: '城市', render: (record: CityStat) => <strong>{record.city}</strong> },
|
||||
{ key: 'province', title: '省份', render: (record: CityStat) => <span className="muted">{record.province}</span> },
|
||||
{ key: 'total', title: '总数', align: 'right', render: (record: CityStat) => formatNumber(record.total) },
|
||||
{ key: 'success', title: '成功', align: 'right', render: (record: CityStat) => <span className="admin-success-text">{formatNumber(record.success)}</span> },
|
||||
{ key: 'region', title: '发送地区', render: (record: RegionStat) => <strong>{record.region}</strong> },
|
||||
{ key: 'total', title: '总数', align: 'right', render: (record: RegionStat) => formatNumber(record.total) },
|
||||
{ key: 'success', title: '成功', align: 'right', render: (record: RegionStat) => <span className="admin-success-text">{formatNumber(record.success)}</span> },
|
||||
{
|
||||
key: 'rate',
|
||||
title: '成功率',
|
||||
align: 'right',
|
||||
render: (record: CityStat) => <Tag tone={getCityRate(record) >= 95 ? 'success' : 'info'}>{getCityRate(record).toFixed(1)}%</Tag>,
|
||||
render: (record: RegionStat) => <Tag tone={getRegionRate(record) >= 95 ? 'success' : 'info'}>{getRegionRate(record).toFixed(1)}%</Tag>,
|
||||
},
|
||||
]}
|
||||
data={task.cities}
|
||||
rowKey={(record) => record.city}
|
||||
data={task.regions}
|
||||
emptyText="暂无已分配通道记录"
|
||||
rowKey={(record) => record.region}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -401,7 +323,7 @@ function TaskDetailModal({ task, onClose }: { task: SmsTask; onClose: () => void
|
||||
}
|
||||
|
||||
export function AdminSmsTaskProgressPage() {
|
||||
const [tasks, setTasks] = useState(taskData);
|
||||
const [tasks, setTasks] = useState<SmsTask[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [enterprise, setEnterprise] = useState('all');
|
||||
const [application, setApplication] = useState('all');
|
||||
@@ -409,6 +331,23 @@ export function AdminSmsTaskProgressPage() {
|
||||
const [hoveredTaskId, setHoveredTaskId] = useState<string | null>(null);
|
||||
const [selectedTask, setSelectedTask] = useState<SmsTask | null>(null);
|
||||
const [terminateTarget, setTerminateTarget] = useState<SmsTask | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
function loadTasks() {
|
||||
setLoading(true);
|
||||
adminApi.listAdminBatchTasks()
|
||||
.then((items) => {
|
||||
setTasks(items.map(mapTask));
|
||||
setError('');
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '短信任务进度加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, []);
|
||||
|
||||
const enterpriseOptions = useMemo(() => {
|
||||
const names = Array.from(new Set(tasks.map((item) => item.enterprise)));
|
||||
@@ -441,10 +380,13 @@ export function AdminSmsTaskProgressPage() {
|
||||
}
|
||||
|
||||
function terminateTask(taskId: string) {
|
||||
setTasks((current) => current.map((task) => (
|
||||
task.id === taskId ? { ...task, status: 'terminated' } : task
|
||||
)));
|
||||
setTerminateTarget(null);
|
||||
adminApi.terminateAdminBatchTask(taskId)
|
||||
.then(() => {
|
||||
setTerminateTarget(null);
|
||||
setSelectedTask(null);
|
||||
loadTasks();
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '任务终止失败'));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -470,11 +412,13 @@ export function AdminSmsTaskProgressPage() {
|
||||
<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 icon={<Search size={16} />} onClick={loadTasks}>查询</Button>
|
||||
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<div className="surface admin-task-table-card">
|
||||
<div className="ui-table-wrap">
|
||||
<table className="ui-table batch-table admin-task-table">
|
||||
@@ -486,22 +430,22 @@ export function AdminSmsTaskProgressPage() {
|
||||
<th style={{ width: '130px' }}>号码数/字符数</th>
|
||||
<th style={{ width: '150px' }}>发送方式</th>
|
||||
<th style={{ width: '190px' }}>进度</th>
|
||||
<th style={{ width: '100px' }}>状态</th>
|
||||
<th style={{ width: '130px' }}>状态</th>
|
||||
<th style={{ textAlign: 'right', width: '170px' }}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<tr>
|
||||
<td className="ui-table__empty" colSpan={8}>暂无数据</td>
|
||||
</tr>
|
||||
{loading ? (
|
||||
<tr><td className="ui-table__empty" colSpan={8}>正在加载真实短信任务...</td></tr>
|
||||
) : filteredTasks.length === 0 ? (
|
||||
<tr><td className="ui-table__empty" colSpan={8}>暂无短信任务</td></tr>
|
||||
) : filteredTasks.map((record) => {
|
||||
const progress = getProgress(record);
|
||||
const { signature, content } = splitSignature(record.templateContent);
|
||||
const rowClass = hoveredTaskId === record.id ? 'batch-row--hovered' : '';
|
||||
|
||||
return (
|
||||
<Fragment key={record.id}>
|
||||
<Fragment key={record.backendId}>
|
||||
<tr
|
||||
className={['batch-main-row', rowClass].filter(Boolean).join(' ')}
|
||||
onMouseEnter={() => setHoveredTaskId(record.id)}
|
||||
@@ -583,7 +527,7 @@ export function AdminSmsTaskProgressPage() {
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={() => setTerminateTarget(null)} variant="ghost">取消</Button>
|
||||
<Button onClick={() => terminateTask(terminateTarget.id)} variant="danger">确认终止</Button>
|
||||
<Button onClick={() => terminateTask(terminateTarget.backendId)} variant="danger">确认终止</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={() => setTerminateTarget(null)}
|
||||
|
||||
Reference in New Issue
Block a user