import { Fragment, useEffect, useState } from 'react'; import { Clock3, Eye, FileText, Search, StopCircle } from 'lucide-react'; import { useSearchParams } from 'react-router-dom'; import { Button, DateRangeInput, DetailInfoGrid, DetailProgressStats, DetailSection, DetailTitle, InlineTextPreview, Input, Modal, Pagination, QueryPanel, QueryButtons, Select, Tag, type DateRangeValue, type TableColumn, } from '@/components/ui'; import { clientApi, type SmsBatchTask } from '@/api/adminApi'; import { formatDateTime, recentBeijingDateRange } from '@/utils/dateTime'; import { batchTaskStatusMeta, canClientCancelBatchTask, normalizeBatchTaskStatus, type BatchTaskDisplayStatus } from '@/utils/batchTaskStatus'; type BatchTask = { id: string; backendId: string; applicationName: string; submittedAt: string; phoneCount: number; wordCount: number; sendType: 'immediate' | 'scheduled'; scheduledAt?: string | null; sentCount: number; deliveredCount: number; failedCount: number; totalCount: number; templateContent: string; status: BatchTaskDisplayStatus; rawStatus: string; reviewReason?: string | null; rejectReason?: string | null; riskTaskId?: string | null; }; function splitSignature(content: string) { const match = content.match(/^【(.+?)】(.+)$/); return { signature: match?.[1], content: match?.[2] ?? content, }; } function getProgress(task: BatchTask) { return task.totalCount > 0 ? Math.round((task.sentCount / task.totalCount) * 100) : 0; } function getBillingCount(task: BatchTask) { return getDeliveredCount(task); } function getDeliveredCount(task: BatchTask) { return task.deliveredCount; } function mapTask(task: SmsBatchTask): BatchTask { const displayStatus = normalizeBatchTaskStatus(task.status); return { id: task.taskNo || task.id, backendId: task.id, applicationName: task.application?.name ?? task.applicationId ?? '未绑定应用', submittedAt: task.createdAt, phoneCount: task.phoneTotal, wordCount: [...task.content].length, sendType: task.scheduledAt ? 'scheduled' : 'immediate', scheduledAt: task.scheduledAt, sentCount: displayStatus === 'pending_review' ? 0 : task.progressSent ?? task.submittedTotal ?? 0, deliveredCount: task.progressDelivered ?? task.successTotal ?? 0, failedCount: task.progressFailed ?? task.failedTotal ?? 0, totalCount: task.progressTotal || task.phoneTotal, templateContent: task.content, status: displayStatus, rawStatus: task.status, reviewReason: task.reviewReason, rejectReason: task.rejectReason, riskTaskId: task.riskTaskId, }; } export function ClientBatchTasksPage() { const [searchParams] = useSearchParams(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [total, setTotal] = useState(0); const [applications, setApplications] = useState>([]); const [keyword, setKeyword] = useState(() => searchParams.get('taskNo') ?? ''); const [application, setApplication] = useState('all'); const [submittedDateRange, setSubmittedDateRange] = useState(() => recentBeijingDateRange(7)); const [applied, setApplied] = useState(() => ({ keyword, application, submittedDateRange })); const [hoveredTaskId, setHoveredTaskId] = useState(null); const [page, setPage] = useState(1); const [selectedTask, setSelectedTask] = useState(null); const pageSize = 10; function loadTasks(targetPage = page) { setLoading(true); clientApi.listBatchTasksPage({ keyword: applied.keyword.trim() || undefined, applicationKeyword: applied.application === 'all' ? undefined : applied.application, createdAtFrom: applied.submittedDateRange.start, createdAtTo: applied.submittedDateRange.end, page: targetPage, pageSize, }) .then((result) => { setTasks(result.items.map(mapTask)); setTotal(result.total); setError(''); }) .catch((reason: Error) => setError(reason.message || '批量任务加载失败')) .finally(() => setLoading(false)); } useEffect(() => { loadTasks(page); }, [applied, page]); function query() { setPage(1); setApplied({ keyword, application, submittedDateRange }); } function reset() { const defaults = { keyword: '', application: 'all', submittedDateRange: recentBeijingDateRange(7) }; setKeyword(''); setApplication('all'); setSubmittedDateRange(defaults.submittedDateRange); setPage(1); setApplied(defaults); } useEffect(() => { clientApi.listApplicationOptions() .then((items) => setApplications(items.map((item) => ({ id: item.id, name: item.name })))) .catch(() => undefined); }, []); const applicationOptions = [ { label: '全部应用', value: 'all' }, ...applications.map((item) => ({ label: item.name, value: item.name })), ]; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const currentPage = Math.min(page, totalPages); const visibleTasks = tasks; function terminateTask(id: string) { const source = tasks.find((item) => item.id === id); if (!source) return; clientApi.cancelBatchTask(source.backendId) .then(() => loadTasks(page)) .catch((reason: Error) => setError(reason.message || '发送批次终止失败')); } const columns: Array> = [ { key: 'id', title: '发送批次号', width: '140px', render: (record) => (
{record.id} {batchTaskStatusMeta[record.status].label}{record.status === 'unknown' ? `(${record.rawStatus})` : ''}
), }, { key: 'applicationName', title: '应用名称', width: '150px', render: (record) => record.applicationName }, { key: 'submittedAt', title: '提交时间', width: '150px', render: (record) => formatDateTime(record.submittedAt) }, { key: 'phoneCount', title: '发送号码数', width: '120px', render: (record) => record.phoneCount.toLocaleString('zh-CN') }, { key: 'wordCount', title: '单号码字数', width: '120px', render: (record) => {record.wordCount} 字 }, { key: 'sendType', title: '发送时间', width: '130px', render: (record) => (
{record.sendType === 'immediate' ? '立即发送' : '定时发送'} {record.scheduledAt ? {formatDateTime(record.scheduledAt)} : null}
), }, { key: 'progress', title: '发送进度', width: '180px', render: (record) => { const percent = getProgress(record); return (
{record.sentCount.toLocaleString('zh-CN')} / {record.totalCount.toLocaleString('zh-CN')} {percent}%
); }, }, { key: 'actions', title: '操作', align: 'right', width: '120px', render: (record) => (
{canClientCancelBatchTask(record.rawStatus) ? : null}
), }, ]; return (

查看批量任务

共找到 {total} 个发送批次} > setKeyword(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { query(); } }} placeholder="输入发送批次号搜索" prefix={} value={keyword} />