fix: connect remaining sms pages to real backend
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user