Files
lislgosms/src/apps/admin/sms-task-progress/TaskDetailModal.tsx
T

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>
);
}