167 lines
7.2 KiB
TypeScript
167 lines
7.2 KiB
TypeScript
import { BarChart3, MapPin, Send, Smartphone, TrendingUp } from 'lucide-react';
|
|
import { Button, Modal, Table, Tag } from '@/components/ui';
|
|
import {
|
|
formatNumber,
|
|
formatTime,
|
|
getProgress,
|
|
getRegionRate,
|
|
getSuccessRate,
|
|
sendTypeLabels,
|
|
statusLabels,
|
|
statusTones,
|
|
} from './taskModel';
|
|
import type { RegionStat, SmsTask } from './taskTypes';
|
|
|
|
function TaskDetailTitle({ task }: { task: SmsTask }) {
|
|
return (
|
|
<div className="admin-task-detail-title">
|
|
<h2>发送批次详情</h2>
|
|
<p>
|
|
<span>{task.id}</span>
|
|
<Tag tone={statusTones[task.status]}>{statusLabels[task.status]}</Tag>
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricCard({ label, value, tone }: { label: string; value: string; tone?: 'success' | 'primary' }) {
|
|
return (
|
|
<div className={['admin-task-metric', tone ? `admin-task-metric--${tone}` : ''].filter(Boolean).join(' ')}>
|
|
<span>{label}</span>
|
|
<strong>{value}</strong>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export 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
|
|
footer={<Button onClick={onClose}>关闭</Button>}
|
|
onClose={onClose}
|
|
open
|
|
size="xl"
|
|
title={<TaskDetailTitle task={task} />}
|
|
>
|
|
<div className="admin-task-detail">
|
|
<div className="admin-task-metrics">
|
|
<MetricCard label="提交总数" value={formatNumber(task.submittedCount)} />
|
|
<MetricCard label="提交成功" tone="success" value={formatNumber(task.submittedSuccess)} />
|
|
<MetricCard label="发送成功" tone="primary" value={formatNumber(task.successCount)} />
|
|
<MetricCard label="计费条数" tone="primary" value={formatNumber(task.billingCount)} />
|
|
<MetricCard label="成功率" tone="primary" value={`${successRate.toFixed(2)}%`} />
|
|
</div>
|
|
|
|
<div className="admin-task-detail-grid">
|
|
<section className="admin-task-card">
|
|
<h3><Send size={18} />发送批次信息</h3>
|
|
<dl className="admin-task-info-list">
|
|
<div>
|
|
<dt>企业/应用</dt>
|
|
<dd><strong>{task.enterprise}</strong><span>{task.application}</span></dd>
|
|
</div>
|
|
<div>
|
|
<dt>提交时间</dt>
|
|
<dd>{formatTime(task.submittedAt)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>发送方式</dt>
|
|
<dd><Tag tone={task.sendType === 'immediate' ? 'info' : 'warning'}>{sendTypeLabels[task.sendType]}</Tag></dd>
|
|
</div>
|
|
</dl>
|
|
</section>
|
|
|
|
<section className="admin-task-card">
|
|
<h3><TrendingUp size={18} />发送进度</h3>
|
|
<div className="admin-task-progress-card">
|
|
<div>
|
|
<span>已处理 {formatNumber(task.sentCount)} / 总计 {formatNumber(task.phoneCount)}</span>
|
|
<strong>{progress}%</strong>
|
|
</div>
|
|
<div className="batch-progress__track">
|
|
<span className={`batch-progress__bar batch-progress__bar--${task.status === 'failed' ? 'terminated' : task.status}`} style={{ width: `${progress}%` }} />
|
|
</div>
|
|
<div className="admin-task-progress-split">
|
|
<span>已提交<strong>{formatNumber(task.submittedSuccess)}</strong></span>
|
|
<span>已成功<strong>{formatNumber(task.successCount)}</strong></span>
|
|
</div>
|
|
{task.status === 'pending_review' ? <p className="muted">任务正在等待运营人工审核,尚未进入发送流程。</p> : null}
|
|
</div>
|
|
</section>
|
|
|
|
{(task.reviewReason || task.rejectReason || task.riskTaskId) ? <section className="admin-task-card">
|
|
<h3>审核信息</h3>
|
|
<dl className="admin-task-info-list">
|
|
{task.riskTaskId ? <div><dt>关联审核任务</dt><dd>{task.riskTaskId}</dd></div> : null}
|
|
{task.reviewReason ? <div><dt>审核原因</dt><dd>{task.reviewReason}</dd></div> : null}
|
|
{task.rejectReason ? <div><dt>驳回原因</dt><dd>{task.rejectReason}</dd></div> : null}
|
|
</dl>
|
|
</section> : null}
|
|
|
|
<section className="admin-task-card">
|
|
<h3><BarChart3 size={18} />模板信息</h3>
|
|
<div className="admin-task-template-block">
|
|
<span>短信模板内容</span>
|
|
<p className="admin-task-template">{task.templateContent}</p>
|
|
</div>
|
|
<dl className="admin-task-template-meta">
|
|
<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 条短信。本次任务单号码 {perPhoneBillingUnits} 条,预计总计费 {formatNumber(task.billingCount)} 条</span>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<section className="admin-task-card admin-task-card--full">
|
|
<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>
|
|
<Table
|
|
columns={[
|
|
{ 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: RegionStat) => <Tag tone={getRegionRate(record) >= 95 ? 'success' : 'info'}>{getRegionRate(record).toFixed(1)}%</Tag>,
|
|
},
|
|
]}
|
|
data={task.regions}
|
|
emptyText="暂无已识别省份记录"
|
|
rowKey={(record) => record.region}
|
|
/>
|
|
</section>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|