fix: connect remaining sms pages to real backend

This commit is contained in:
hectorzhao
2026-07-02 19:30:04 +08:00
parent 06562e42dd
commit 131f344ac4
50 changed files with 2074 additions and 6400 deletions
+74 -87
View File
@@ -1,5 +1,5 @@
import { Fragment, useMemo, useState } from 'react';
import { Clock3, Eye, FileText, Search, StopCircle, TrendingUp } from 'lucide-react';
import { Fragment, useEffect, useMemo, useState } from 'react';
import { Clock3, Eye, FileText, Search, StopCircle } from 'lucide-react';
import {
Button,
DateRangeInput,
@@ -7,21 +7,36 @@ import {
DetailProgressStats,
DetailSection,
DetailTitle,
getRateTone,
InlineTextPreview,
Input,
Modal,
Pagination,
ProgressBar,
QueryPanel,
RateCard,
RateOverview,
Select,
Tag,
type DateRangeValue,
type TableColumn,
} from '@/components/ui';
import { clientService, type BatchTask, type BatchTaskStatus } from '@/mock';
import { clientApi, type SmsBatchTask } from '@/api/adminApi';
type BatchTaskStatus = 'completed' | 'sending' | 'terminated';
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: BatchTaskStatus;
};
const statusToneMap: Record<BatchTaskStatus, 'success' | 'info' | 'danger'> = {
completed: 'success',
@@ -35,22 +50,6 @@ const statusLabelMap: Record<BatchTaskStatus, string> = {
terminated: '已终止',
};
const carrierStats = [
{ name: '中国移动', success: 738, total: 750, rate: 98.4 },
{ name: '中国联通', success: 443, total: 450, rate: 98.44 },
{ name: '中国电信', success: 294, total: 300, rate: 98 },
];
const cityStats = [
{ city: '北京', total: 300, success: 295 },
{ city: '上海', total: 280, success: 276 },
{ city: '深圳', total: 250, success: 246 },
{ city: '广州', total: 220, success: 215 },
{ city: '杭州', total: 200, success: 197 },
{ city: '成都', total: 150, success: 148 },
{ city: '武汉', total: 100, success: 98 },
];
function splitSignature(content: string) {
const match = content.match(/^【(.+?)】(.+)$/);
return {
@@ -60,7 +59,7 @@ function splitSignature(content: string) {
}
function getProgress(task: BatchTask) {
return Math.round((task.sentCount / task.totalCount) * 100);
return task.totalCount > 0 ? Math.round((task.sentCount / task.totalCount) * 100) : 0;
}
function getBillingCount(task: BatchTask) {
@@ -68,21 +67,59 @@ function getBillingCount(task: BatchTask) {
}
function getDeliveredCount(task: BatchTask) {
if (task.status === 'completed') {
return Math.round(task.totalCount * 0.9833);
}
return task.deliveredCount;
}
return task.sentCount;
function normalizeTaskStatus(status: string): BatchTaskStatus {
if (['completed', 'done'].includes(status)) return 'completed';
if (['cancelled', 'terminated', 'rejected', 'failed'].includes(status)) return 'terminated';
return 'sending';
}
function mapTask(task: SmsBatchTask): BatchTask {
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: task.progressSent,
deliveredCount: task.progressDelivered,
failedCount: task.progressFailed,
totalCount: task.progressTotal || task.phoneTotal,
templateContent: task.content,
status: normalizeTaskStatus(task.status),
};
}
export function ClientBatchTasksPage() {
const [tasks, setTasks] = useState(() => clientService.getBatchTasks());
const [tasks, setTasks] = useState<BatchTask[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [keyword, setKeyword] = useState('');
const [application, setApplication] = useState('all');
const [submittedDateRange, setSubmittedDateRange] = useState<DateRangeValue>({});
const [hoveredTaskId, setHoveredTaskId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<BatchTask | null>(null);
function loadTasks() {
setLoading(true);
clientApi.listBatchTasks()
.then((items) => {
setTasks(items.map(mapTask));
setError('');
})
.catch((reason: Error) => setError(reason.message || '批量任务加载失败'))
.finally(() => setLoading(false));
}
useEffect(() => {
loadTasks();
}, []);
const applicationOptions = useMemo(() => {
const names = Array.from(new Set(tasks.map((item) => item.applicationName)));
return [
@@ -101,7 +138,11 @@ export function ClientBatchTasksPage() {
});
function terminateTask(id: string) {
setTasks(clientService.terminateBatchTask(id));
const source = tasks.find((item) => item.id === id);
if (!source) return;
clientApi.cancelBatchTask(source.backendId)
.then(loadTasks)
.catch((reason: Error) => setError(reason.message || '任务终止失败'));
}
const columns: Array<TableColumn<BatchTask>> = [
@@ -200,6 +241,8 @@ export function ClientBatchTasksPage() {
</QueryPanel>
<div className="surface batch-table-card">
{loading ? <p className="muted">...</p> : null}
{error ? <p className="form-error">{error}</p> : null}
<div className="ui-table-wrap">
<table className="ui-table batch-table">
<thead>
@@ -287,66 +330,10 @@ export function ClientBatchTasksPage() {
{ label: '提交总数量', value: selectedTask.totalCount.toLocaleString('zh-CN') },
{ label: '提交成功数量', value: selectedTask.totalCount.toLocaleString('zh-CN') },
{ label: '发送成功数量', value: getDeliveredCount(selectedTask).toLocaleString('zh-CN') },
{ label: '发送失败数量', value: selectedTask.failedCount.toLocaleString('zh-CN') },
]}
/>
</DetailSection>
<DetailSection title={<><TrendingUp size={20} /> </>}>
{(() => {
const overallRate = (getDeliveredCount(selectedTask) / selectedTask.totalCount) * 100;
return (
<RateOverview
label="总体成功率"
metrics={[
{ label: '成功总数', value: getDeliveredCount(selectedTask).toLocaleString('zh-CN') },
{ label: '总计', value: selectedTask.totalCount.toLocaleString('zh-CN') },
]}
rate={overallRate}
tone={getRateTone(overallRate)}
/>
);
})()}
<h4></h4>
<div className="carrier-rate-grid">
{carrierStats.map((item) => (
<RateCard
key={item.name}
meta={<><span>: {item.success}</span><span>: {item.total}</span></>}
rate={item.rate}
title={item.name}
tone={getRateTone(item.rate)}
/>
))}
</div>
<h4></h4>
<div className="ui-table-wrap">
<table className="ui-table city-rate-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{cityStats.map((item) => {
const rate = (item.success / item.total) * 100;
return (
<tr key={item.city}>
<td><strong>{item.city}</strong></td>
<td>{item.total}</td>
<td><span className={`rate-text ui-rate-tone-${getRateTone(rate)}`}>{item.success}</span></td>
<td><span className={`rate-text ui-rate-tone-${getRateTone(rate)}`}>{rate.toFixed(2)}%</span></td>
<td><ProgressBar percent={rate} tone={getRateTone(rate)} /></td>
</tr>
);
})}
</tbody>
</table>
</div>
</DetailSection>
</div>
) : null}
</Modal>