322 lines
12 KiB
TypeScript
322 lines
12 KiB
TypeScript
import { AlertTriangle, Info, MessageSquare } from 'lucide-react';
|
|
import type { SmsMessageRecord, SmsMessageSegmentAudit } from '@/api/adminApi';
|
|
import { Button, CarrierTag, Modal, Tag } from '@/components/ui';
|
|
import { DrainageContent } from './SmsRecordList';
|
|
import {
|
|
buildRouteRows,
|
|
getReceiptNotice,
|
|
getRecordStatus,
|
|
getRecordStatusLabel,
|
|
getTime,
|
|
statusToneMap,
|
|
} from './smsRecordModel';
|
|
|
|
type SendDetailModalProps = {
|
|
record: SmsMessageRecord;
|
|
segmentAudits: SmsMessageSegmentAudit[];
|
|
segmentLoading: boolean;
|
|
onClose: () => void;
|
|
};
|
|
|
|
export function SendDetailModal({ record, segmentAudits, segmentLoading, onClose }: SendDetailModalProps) {
|
|
const visibleWordDecisions = record.channelWordDecisions?.filter(
|
|
(decision) => decision.snapshot.hits.length > 0 || Boolean(decision.snapshot.reason),
|
|
);
|
|
const routeRows = buildRouteRows(record, segmentAudits);
|
|
const channelGroupNames = Array.from(new Set(routeRows.map((route) => route.channelGroup).filter(Boolean)));
|
|
const orderedSegmentAudits = [...segmentAudits].sort((left, right) => {
|
|
const timeDiff = new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime();
|
|
return timeDiff || left.segmentIndex - right.segmentIndex || left.id.localeCompare(right.id);
|
|
});
|
|
const sentAccessNumber = `${record.channel?.srcId ?? ''}${record.applicationExtension ?? ''}`;
|
|
const displayStatus = getRecordStatus(record);
|
|
const receiptNotice = getReceiptNotice(record);
|
|
|
|
return (
|
|
<Modal
|
|
footer={
|
|
<Button onClick={onClose} variant="ghost">
|
|
关闭
|
|
</Button>
|
|
}
|
|
onClose={onClose}
|
|
open
|
|
size="xl"
|
|
title={
|
|
<div className="template-modal-title">
|
|
<h2>发送详情</h2>
|
|
<p>{record.messageId}</p>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="admin-sms-send-detail">
|
|
{visibleWordDecisions?.length ? (
|
|
<section aria-label="通道筛选原因">
|
|
<h3>通道筛选原因</h3>
|
|
{visibleWordDecisions.map((decision) => (
|
|
<div key={decision.id}>
|
|
<p>
|
|
{getTime(decision.decidedAt)} · {decision.snapshot.reason || '已排除命中通道,按剩余候选选路'}
|
|
</p>
|
|
{decision.snapshot.hits.map((hit) => (
|
|
<p key={hit.channelId}>
|
|
通道 {hit.channelName || hit.channelId}:命中 {hit.count} 个词,
|
|
{hit.samples.map((sample) => `“${sample.word}”`).join('、')}
|
|
{hit.count > hit.samples.length ? '(仅展示部分)' : ''}
|
|
</p>
|
|
))}
|
|
</div>
|
|
))}
|
|
</section>
|
|
) : null}
|
|
<section aria-label="引流发送资格">
|
|
<h3>引流发送资格</h3>
|
|
{record.drainageGate ? (
|
|
<>
|
|
<p>
|
|
{record.drainageGate.reason ||
|
|
(record.drainageGate.targets.length
|
|
? '引流信息资格检查通过;发送仍受通道及其他规则限制'
|
|
: '未检测到引流信息')}
|
|
</p>
|
|
{record.drainageGate.targets.map((target, index) => (
|
|
<p key={index}>
|
|
{target.text} · {target.materialIds.length ? '已匹配审核通过的资料' : '未匹配有效资料'}
|
|
</p>
|
|
))}
|
|
</>
|
|
) : (
|
|
<p className="muted">{segmentLoading ? '正在加载引流资格校验结果…' : '未执行引流资格校验'}</p>
|
|
)}
|
|
</section>
|
|
<div className="admin-sms-detail-overview">
|
|
<div>
|
|
<span>最终状态</span>
|
|
<Tag tone={statusToneMap[displayStatus] ?? 'info'}>{getRecordStatusLabel(record)}</Tag>
|
|
</div>
|
|
<div>
|
|
<span>提交状态</span>
|
|
<strong>{record.submitStatus ?? '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>回执状态</span>
|
|
<strong>{record.receiptStatus ?? '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>最终回执时间</span>
|
|
<strong>{getTime(record.deliveredAt)}</strong>
|
|
</div>
|
|
<div>
|
|
<span>引流信息</span>
|
|
<Tag tone={record.hasDrainageContent === true ? 'warning' : 'neutral'}>
|
|
{record.hasDrainageContent === true
|
|
? '含引流'
|
|
: record.hasDrainageContent === false
|
|
? '不含引流'
|
|
: '未检测'}
|
|
</Tag>
|
|
</div>
|
|
<div>
|
|
<span>提交时间</span>
|
|
<strong>{getTime(record.queuedAt)}</strong>
|
|
</div>
|
|
<div>
|
|
<span>发送号码</span>
|
|
<strong>{record.phoneNumber || '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>号码归属</span>
|
|
<strong>
|
|
{record.province ?? '-'} / {record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}
|
|
</strong>
|
|
</div>
|
|
<div>
|
|
<span>通道组</span>
|
|
<strong>{channelGroupNames.join(' / ') || '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>收到的接入号</span>
|
|
<strong>{record.clientSrcId || '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>发送的接入号</span>
|
|
<strong>{sentAccessNumber || '-'}</strong>
|
|
</div>
|
|
</div>
|
|
{receiptNotice ? (
|
|
<div className="admin-sms-detail-notice" role="status">
|
|
<Info size={20} />
|
|
<strong>{receiptNotice}</strong>
|
|
</div>
|
|
) : null}
|
|
<section>
|
|
<h3>
|
|
<MessageSquare size={18} /> 短信内容
|
|
</h3>
|
|
<p className="admin-sms-detail-content">
|
|
<DrainageContent record={record} />
|
|
</p>
|
|
{record.originalContent != null ? (
|
|
<>
|
|
<h3>原始短信内容</h3>
|
|
<p className="admin-sms-detail-content">{record.originalContent}</p>
|
|
</>
|
|
) : null}
|
|
{record.originalContent != null
|
|
? record.submitRecords
|
|
?.filter((submit) => submit.sentContent != null)
|
|
.map((submit, index) => (
|
|
<div key={submit.id}>
|
|
<h3>
|
|
第 {index + 1} 次提交通道内容 · {submit.channel?.name ?? submit.channelId}
|
|
</h3>
|
|
<p className="admin-sms-detail-content">{submit.sentContent}</p>
|
|
</div>
|
|
))
|
|
: null}
|
|
</section>
|
|
|
|
<section>
|
|
<h3>通道发送与回执</h3>
|
|
<div className="admin-sms-route-list">
|
|
{routeRows.map((route, index) => (
|
|
<article key={route.id}>
|
|
<span>{index + 1}</span>
|
|
<div>
|
|
<strong>{route.channel}</strong>
|
|
<dl>
|
|
<div>
|
|
<dt>发送时间</dt>
|
|
<dd>{getTime(route.sentAt)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>回执时间</dt>
|
|
<dd>{getTime(route.receiptAt)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>回执码</dt>
|
|
<dd>{route.receiptCode ?? '-'}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>提交状态</dt>
|
|
<dd>{route.submitStatus ?? '-'}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h3>状态信息</h3>
|
|
<div className="admin-sms-detail-status-grid">
|
|
<div>
|
|
<span>消息编号</span>
|
|
<strong>{record.messageId}</strong>
|
|
</div>
|
|
<div>
|
|
<span>发送状态</span>
|
|
<strong>{getRecordStatusLabel(record)}</strong>
|
|
</div>
|
|
<div>
|
|
<span>提交状态</span>
|
|
<strong>{record.submitStatus ?? '-'}</strong>
|
|
</div>
|
|
<div>
|
|
<span>回执状态</span>
|
|
<strong>{record.receiptStatus ?? '-'}</strong>
|
|
</div>
|
|
</div>
|
|
{['submit_failed', 'failed', 'rejected'].includes(displayStatus) ? (
|
|
<div className="admin-sms-detail-failure" role="alert">
|
|
<AlertTriangle size={20} />
|
|
<div>
|
|
<span>失败原因</span>
|
|
<strong>{record.errorMessage ?? record.errorCode ?? '未返回明确失败原因'}</strong>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</section>
|
|
|
|
<section>
|
|
<h3>分片补偿审计</h3>
|
|
{segmentLoading ? (
|
|
<div className="ui-table__empty">加载中...</div>
|
|
) : segmentAudits.length === 0 ? (
|
|
<div className="ui-table__empty">暂无分片审计</div>
|
|
) : (
|
|
<div className="admin-sms-segment-list">
|
|
{orderedSegmentAudits.map((segment) => (
|
|
<article className="admin-sms-segment-card" key={segment.id}>
|
|
<header>
|
|
<strong>
|
|
分片 {segment.segmentIndex}/{segment.segmentTotal}
|
|
</strong>
|
|
<div>
|
|
<Tag
|
|
tone={
|
|
segment.submitStatus === 'accepted'
|
|
? 'success'
|
|
: segment.submitStatus === 'queued'
|
|
? 'info'
|
|
: 'danger'
|
|
}
|
|
>
|
|
{segment.submitStatus}
|
|
</Tag>
|
|
{segment.receiptStatus ? (
|
|
<Tag
|
|
tone={
|
|
segment.receiptStatus === 'delivered'
|
|
? 'success'
|
|
: segment.receiptStatus === 'unknown'
|
|
? 'neutral'
|
|
: 'danger'
|
|
}
|
|
>
|
|
{segment.receiptStatus}
|
|
</Tag>
|
|
) : null}
|
|
</div>
|
|
</header>
|
|
<dl>
|
|
<div>
|
|
<dt>通道</dt>
|
|
<dd>{segment.channel?.name ?? segment.channelId ?? '-'}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Sequence</dt>
|
|
<dd>{segment.sequenceId ?? '-'}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>提交 ID</dt>
|
|
<dd>{segment.submitId}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>网关 MsgId</dt>
|
|
<dd>{segment.gatewayMessageId ?? '-'}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>补偿方式</dt>
|
|
<dd>{segment.compensationType ?? '-'}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>审计时间</dt>
|
|
<dd>{getTime(segment.createdAt)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>错误信息</dt>
|
|
<dd>{segment.errorMessage ?? segment.errorCode ?? '-'}</dd>
|
|
</div>
|
|
</dl>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|