feat: densify sms records and improve uplink matching
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Search, Smartphone } from 'lucide-react';
|
||||
import { Search, Smartphone, UserX } from 'lucide-react';
|
||||
import { adminApi, type SmsMessageRecord, type SmsUplinkMatchCandidate, type SmsUplinkMessage } from '@/api/adminApi';
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -40,28 +40,41 @@ function candidateStatusText(status?: string | null) {
|
||||
}
|
||||
|
||||
function UplinkDetailModal({
|
||||
blacklistFeedback,
|
||||
blacklisting,
|
||||
claimError,
|
||||
claimingId,
|
||||
detailError,
|
||||
matchedRecords,
|
||||
matching,
|
||||
message,
|
||||
onAddBlacklist,
|
||||
onClaim,
|
||||
onClose,
|
||||
}: {
|
||||
blacklistFeedback: string;
|
||||
blacklisting: boolean;
|
||||
claimError: string;
|
||||
claimingId: string;
|
||||
detailError: string;
|
||||
matchedRecords: SmsMessageRecord[];
|
||||
matching: boolean;
|
||||
message: SmsUplinkMessage;
|
||||
onAddBlacklist: () => void;
|
||||
onClaim: (candidate: SmsUplinkMatchCandidate) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const candidates = message.matchCandidates ?? [];
|
||||
return (
|
||||
<Modal
|
||||
footer={<Button onClick={onClose} variant="ghost">关闭</Button>}
|
||||
footer={<>
|
||||
{message.tenantId && message.applicationId ? (
|
||||
<Button disabled={blacklisting} icon={<UserX size={15} />} onClick={onAddBlacklist}>
|
||||
{blacklisting ? '加入中...' : '加入应用黑名单'}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={onClose} variant="ghost">关闭</Button>
|
||||
</>}
|
||||
onClose={onClose}
|
||||
open
|
||||
size="xl"
|
||||
@@ -92,13 +105,21 @@ function UplinkDetailModal({
|
||||
<strong>{message.destId || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>网关消息ID</span>
|
||||
<strong>{message.messageId || '-'}</strong>
|
||||
<span>上行网关消息ID</span>
|
||||
<strong>{message.gatewayMessageId || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>关联平台消息ID</span>
|
||||
<strong>{message.messageRecord?.messageId ?? message.messageId ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>匹配状态</span>
|
||||
<strong>{matchStatusText(message.matchStatus)}</strong>
|
||||
</div>
|
||||
<div className="admin-uplink-info-grid__full">
|
||||
<span>匹配说明</span>
|
||||
<strong>{message.matchReason || '-'}</strong>
|
||||
</div>
|
||||
<div className="admin-uplink-info-grid__full">
|
||||
<span>上行内容</span>
|
||||
<strong>{message.content || '-'}</strong>
|
||||
@@ -160,11 +181,13 @@ function UplinkDetailModal({
|
||||
|
||||
<section className="admin-uplink-match-section">
|
||||
<h3>匹配发送记录</h3>
|
||||
{blacklistFeedback ? <p className={blacklistFeedback.startsWith('已') ? 'form-success' : 'form-error'}>{blacklistFeedback}</p> : null}
|
||||
{matching ? <p>正在查询真实下发记录...</p> : null}
|
||||
{detailError ? <p className="form-error">{detailError}</p> : null}
|
||||
{!matching && !message.messageId ? <div className="admin-uplink-empty-match">该上行记录没有网关消息ID,无法匹配下发记录</div> : null}
|
||||
{!matching && message.messageId && matchedRecords.length === 0 && !detailError ? (
|
||||
<div className="admin-uplink-empty-match">暂无匹配发送记录</div>
|
||||
{!matching && matchedRecords.length === 0 && !detailError ? (
|
||||
<div className="admin-uplink-empty-match">
|
||||
{message.matchStatus === 'ambiguous' ? '存在多个候选,请先认领正确应用' : '暂无匹配发送记录'}
|
||||
</div>
|
||||
) : null}
|
||||
{matchedRecords.map((record) => (
|
||||
<article className="admin-uplink-match-card" key={record.id}>
|
||||
@@ -211,6 +234,8 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
const [error, setError] = useState('');
|
||||
const [detailError, setDetailError] = useState('');
|
||||
const [claimError, setClaimError] = useState('');
|
||||
const [blacklisting, setBlacklisting] = useState(false);
|
||||
const [blacklistFeedback, setBlacklistFeedback] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
@@ -236,11 +261,12 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
|
||||
function openDetail(message: SmsUplinkMessage) {
|
||||
setSelectedMessage(message);
|
||||
setMatchedRecords([]);
|
||||
setMatchedRecords(message.messageRecord ? [message.messageRecord] : []);
|
||||
setDetailError('');
|
||||
setClaimError('');
|
||||
setBlacklistFeedback('');
|
||||
|
||||
if (!message.messageId) {
|
||||
if (message.messageRecord || !message.messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -251,6 +277,28 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
.finally(() => setMatching(false));
|
||||
}
|
||||
|
||||
function handleAddBlacklist() {
|
||||
if (!selectedMessage?.tenantId || !selectedMessage.applicationId) {
|
||||
setBlacklistFeedback('请先匹配或认领企业应用');
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`确认将 ${selectedMessage.phoneNumber} 加入当前应用黑名单?`)) {
|
||||
return;
|
||||
}
|
||||
setBlacklisting(true);
|
||||
setBlacklistFeedback('');
|
||||
adminApi.createEnterpriseBlacklist({
|
||||
tenantId: selectedMessage.tenantId,
|
||||
applicationId: selectedMessage.applicationId,
|
||||
phoneNumber: selectedMessage.phoneNumber,
|
||||
reason: '上行短信人工加入',
|
||||
status: 'active',
|
||||
})
|
||||
.then(() => setBlacklistFeedback('已加入当前应用黑名单'))
|
||||
.catch((reason: Error) => setBlacklistFeedback(reason.message || '加入应用黑名单失败'))
|
||||
.finally(() => setBlacklisting(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData(page);
|
||||
}, [page]);
|
||||
@@ -273,6 +321,7 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
.then((updated) => {
|
||||
setMessages((items) => items.map((item) => (item.id === updated.id ? { ...item, ...updated } : item)));
|
||||
setSelectedMessage((current) => (current && current.id === updated.id ? { ...current, ...updated } : current));
|
||||
setMatchedRecords(updated.messageRecord ? [updated.messageRecord] : []);
|
||||
loadData(page);
|
||||
})
|
||||
.catch((reason: Error) => setClaimError(reason.message || '上行认领失败'))
|
||||
@@ -332,12 +381,15 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
|
||||
{selectedMessage ? (
|
||||
<UplinkDetailModal
|
||||
blacklistFeedback={blacklistFeedback}
|
||||
blacklisting={blacklisting}
|
||||
claimError={claimError}
|
||||
claimingId={claimingId}
|
||||
detailError={detailError}
|
||||
matchedRecords={matchedRecords}
|
||||
matching={matching}
|
||||
message={selectedMessage}
|
||||
onAddBlacklist={handleAddBlacklist}
|
||||
onClaim={handleClaim}
|
||||
onClose={() => setSelectedMessage(null)}
|
||||
/>
|
||||
|
||||
@@ -5,14 +5,29 @@
|
||||
.admin-sms-record-filter {
|
||||
align-items: end;
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
grid-template-columns: repeat(4, minmax(180px, 1fr));
|
||||
gap: var(--space-4);
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.admin-sms-record-filter__field {
|
||||
grid-column: span 2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-sms-record-filter__field.is-date,
|
||||
.admin-sms-record-filter__field.is-content {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.admin-sms-record-filter__field.is-phone {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.admin-sms-record-filter__actions {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: repeat(2, minmax(120px, 1fr));
|
||||
grid-column: span 3;
|
||||
grid-template-columns: repeat(2, minmax(88px, 1fr));
|
||||
}
|
||||
|
||||
.admin-sms-record-table-card {
|
||||
@@ -25,8 +40,8 @@
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-height: 76px;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
min-height: 56px;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
}
|
||||
|
||||
.admin-sms-record-table-card .ui-table-wrap {
|
||||
@@ -35,52 +50,64 @@
|
||||
}
|
||||
|
||||
.admin-sms-record-list {
|
||||
overflow-x: auto;
|
||||
padding: 0 var(--space-4) var(--space-3);
|
||||
}
|
||||
|
||||
.admin-sms-record-list__header,
|
||||
.admin-sms-record-card {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: 96px 96px minmax(360px, 1fr) 96px 112px 36px;
|
||||
min-width: 960px;
|
||||
}
|
||||
|
||||
.admin-sms-record-list__header {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
height: 38px;
|
||||
padding: 0 var(--space-3);
|
||||
}
|
||||
|
||||
.admin-sms-record-list__header span:last-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-sms-record-group-title {
|
||||
background: var(--color-bg-subtle);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin: 0 calc(var(--space-4) * -1);
|
||||
padding: var(--space-2) var(--space-7);
|
||||
}
|
||||
|
||||
.admin-sms-record-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
min-height: 72px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
}
|
||||
|
||||
.admin-sms-record-card:hover {
|
||||
border-color: color-mix(in srgb, var(--color-selected) 35%, var(--color-border));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.admin-sms-record-card > header {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
grid-template-columns: minmax(180px, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.admin-sms-record-card > header time {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
background: color-mix(in srgb, var(--color-selected) 4%, var(--color-surface));
|
||||
}
|
||||
|
||||
.admin-sms-record-card .admin-sms-record-content {
|
||||
background: var(--color-bg-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
display: -webkit-box;
|
||||
line-height: 1.55;
|
||||
line-height: 1.45;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-sms-record-card .admin-sms-record-content.is-drainage {
|
||||
background: color-mix(in srgb, #f59e0b 13%, var(--color-surface));
|
||||
border: 1px solid color-mix(in srgb, #f59e0b 34%, var(--color-border));
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.admin-sms-record-content mark {
|
||||
@@ -95,7 +122,7 @@
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-left: var(--space-3);
|
||||
margin-left: var(--space-2);
|
||||
padding: 1px var(--space-2);
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -111,32 +138,43 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.admin-sms-record-card__meta {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.admin-sms-record-card__meta > div {
|
||||
.admin-sms-record-main {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-sms-record-card__meta span,
|
||||
.admin-sms-record-card__meta small {
|
||||
.admin-sms-record-context {
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
font-size: var(--font-size-xs);
|
||||
gap: var(--space-2) var(--space-4);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-sms-record-context span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.admin-sms-record-time,
|
||||
.admin-sms-record-billing {
|
||||
display: grid;
|
||||
font-size: var(--font-size-xs);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.admin-sms-record-time span,
|
||||
.admin-sms-record-billing span {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.admin-sms-record-card__meta strong {
|
||||
.admin-sms-record-time strong,
|
||||
.admin-sms-record-billing strong {
|
||||
color: var(--color-text-strong);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-sms-record-card > footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
.admin-sms-record-table {
|
||||
@@ -221,11 +259,21 @@
|
||||
}
|
||||
|
||||
.admin-sms-record-detail-link {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
align-items: center;
|
||||
background: var(--color-bg-subtle);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-selected);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
display: inline-flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.admin-sms-record-detail-link:hover {
|
||||
background: var(--color-selected-soft);
|
||||
border-color: color-mix(in srgb, var(--color-selected) 35%, var(--color-border));
|
||||
}
|
||||
|
||||
.admin-sms-send-detail {
|
||||
@@ -392,8 +440,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.admin-sms-record-card > header,
|
||||
.admin-sms-record-card__meta,
|
||||
.admin-sms-detail-overview,
|
||||
.admin-sms-detail-status-grid,
|
||||
.admin-sms-route-list dl,
|
||||
@@ -402,8 +448,11 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-sms-record-card > header time {
|
||||
justify-self: start;
|
||||
.admin-sms-record-filter__field,
|
||||
.admin-sms-record-filter__field.is-date,
|
||||
.admin-sms-record-filter__field.is-content,
|
||||
.admin-sms-record-filter__actions {
|
||||
grid-column: span 6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,4 +512,11 @@
|
||||
.admin-sms-route-list dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-sms-record-filter__field,
|
||||
.admin-sms-record-filter__field.is-date,
|
||||
.admin-sms-record-filter__field.is-content,
|
||||
.admin-sms-record-filter__actions {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,15 +85,15 @@ export function SmsRecordFilter({
|
||||
}: SmsRecordFilterProps) {
|
||||
return (
|
||||
<div className="surface admin-sms-record-filter">
|
||||
<Select label="企业" onChange={(event) => onEnterpriseChange(event.target.value)} options={enterpriseOptions} value={enterprise} />
|
||||
<Select label="应用" onChange={(event) => onApplicationChange(event.target.value)} options={applicationOptions} value={application} />
|
||||
<DateRangeInput label="提交日期" onChange={onDateRangeChange} value={dateRange} />
|
||||
<Input label="手机号码" onChange={(event) => onPhoneKeywordChange(event.target.value)} prefix={<Smartphone size={16} />} value={phoneKeyword} />
|
||||
<Select label="运营商" onChange={(event) => onCarrierChange(event.target.value)} options={carrierOptions} value={carrier} />
|
||||
<Input label="短信内容" onChange={(event) => onContentKeywordChange(event.target.value)} value={contentKeyword} />
|
||||
<Select label="通道" onChange={(event) => onChannelChange(event.target.value)} options={channelOptions} searchable searchPlaceholder="输入通道名称搜索" value={channel} />
|
||||
<Select label="发送状态" onChange={(event) => onStatusChange(event.target.value)} options={statusOptions} value={status} />
|
||||
<Select label="是否含引流信息" onChange={(event) => onHasDrainageChange(event.target.value)} options={drainageOptions} value={hasDrainage} />
|
||||
<div className="admin-sms-record-filter__field is-enterprise"><Select label="企业" onChange={(event) => onEnterpriseChange(event.target.value)} options={enterpriseOptions} value={enterprise} /></div>
|
||||
<div className="admin-sms-record-filter__field is-application"><Select label="应用" onChange={(event) => onApplicationChange(event.target.value)} options={applicationOptions} value={application} /></div>
|
||||
<div className="admin-sms-record-filter__field is-date"><DateRangeInput label="提交日期" onChange={onDateRangeChange} value={dateRange} /></div>
|
||||
<div className="admin-sms-record-filter__field is-phone"><Input label="手机号码" onChange={(event) => onPhoneKeywordChange(event.target.value)} prefix={<Smartphone size={16} />} value={phoneKeyword} /></div>
|
||||
<div className="admin-sms-record-filter__field is-carrier"><Select label="运营商" onChange={(event) => onCarrierChange(event.target.value)} options={carrierOptions} value={carrier} /></div>
|
||||
<div className="admin-sms-record-filter__field is-content"><Input label="短信内容" onChange={(event) => onContentKeywordChange(event.target.value)} value={contentKeyword} /></div>
|
||||
<div className="admin-sms-record-filter__field is-channel"><Select label="通道" onChange={(event) => onChannelChange(event.target.value)} options={channelOptions} searchable searchPlaceholder="输入通道名称搜索" value={channel} /></div>
|
||||
<div className="admin-sms-record-filter__field is-status"><Select label="发送状态" onChange={(event) => onStatusChange(event.target.value)} options={statusOptions} value={status} /></div>
|
||||
<div className="admin-sms-record-filter__field is-drainage"><Select label="是否含引流信息" onChange={(event) => onHasDrainageChange(event.target.value)} options={drainageOptions} value={hasDrainage} /></div>
|
||||
<div className="admin-sms-record-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={onQuery}>查询</Button>
|
||||
<Button onClick={onReset} variant="ghost">重置</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Download } from 'lucide-react';
|
||||
import { ChevronRight, Download } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { SmsMessageRecord } from '@/api/adminApi';
|
||||
import { Button, CarrierTag, MoneyText, Pagination } from '@/components/ui';
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
getDate,
|
||||
getRecordStatus,
|
||||
getStatusLabel,
|
||||
getTime,
|
||||
statusDotClassMap,
|
||||
} from './smsRecordModel';
|
||||
|
||||
@@ -60,36 +59,58 @@ export function SmsRecordList({
|
||||
onOpenDetail,
|
||||
onPageChange,
|
||||
}: SmsRecordListProps) {
|
||||
let previousDate = '';
|
||||
|
||||
return (
|
||||
<div className="surface admin-sms-record-table-card">
|
||||
<div className="admin-sms-record-toolbar">
|
||||
<Button icon={<Download size={16} />} onClick={onExport} variant="ghost">导出CSV</Button>
|
||||
</div>
|
||||
<div className="admin-sms-record-list">
|
||||
{loading ? <div className="ui-table__empty">正在加载真实短信记录...</div> : records.length === 0 ? <div className="ui-table__empty">暂无短信记录</div> : records.map((record) => (
|
||||
<article className="admin-sms-record-card" key={record.id}>
|
||||
<header>
|
||||
<div className="admin-sms-record-sender">
|
||||
<strong>{record.tenant?.name ?? record.tenantId ?? '运营端通道测试'}</strong>
|
||||
<span>{record.application?.name ?? record.applicationId ?? '-'}</span>
|
||||
</div>
|
||||
<StatusLine record={record} />
|
||||
<time>{getDate(record.queuedAt)} {getClock(record.queuedAt)}</time>
|
||||
</header>
|
||||
<p className={`admin-sms-record-content${record.hasDrainageContent ? ' is-drainage' : ''}`}>
|
||||
<DrainageContent record={record} />
|
||||
<span className={`admin-sms-record-drainage-badge is-${record.hasDrainageContent === true ? 'yes' : record.hasDrainageContent === false ? 'no' : 'unknown'}`}>
|
||||
{record.hasDrainageContent === true ? '含引流' : record.hasDrainageContent === false ? '不含引流' : '未检测'}
|
||||
</span>
|
||||
</p>
|
||||
<div className="admin-sms-record-card__meta">
|
||||
<div><span>接收号码</span><strong>{record.phoneNumber}</strong><small>{record.province ?? '-'} {record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}</small></div>
|
||||
<div><span>计费</span><strong>{record.billingUnits} 条 / <MoneyText>¥{formatCents(record.amountCents)}</MoneyText></strong><small>{record.content.length} 字</small></div>
|
||||
<div><span>发送通道</span><strong>{record.channel?.name ?? record.channelId ?? '-'}</strong><small>回执 {getTime(record.deliveredAt)}</small></div>
|
||||
{loading ? <div className="ui-table__empty">正在加载真实短信记录...</div> : records.length === 0 ? <div className="ui-table__empty">暂无短信记录</div> : (
|
||||
<>
|
||||
<div aria-hidden="true" className="admin-sms-record-list__header">
|
||||
<span>状态</span><span>提交时间</span><span>短信内容及业务信息</span><span>回执时间</span><span>计费</span><span>详情</span>
|
||||
</div>
|
||||
<footer><button className="admin-sms-record-detail-link" onClick={() => onOpenDetail(record)} type="button">查看发送详情</button></footer>
|
||||
</article>
|
||||
))}
|
||||
{records.map((record) => {
|
||||
const submitDate = getDate(record.queuedAt);
|
||||
const showDateGroup = submitDate !== previousDate;
|
||||
previousDate = submitDate;
|
||||
return (
|
||||
<div className="admin-sms-record-group" key={record.id}>
|
||||
{showDateGroup ? <div className="admin-sms-record-group-title">{submitDate}</div> : null}
|
||||
<article className="admin-sms-record-card">
|
||||
<StatusLine record={record} />
|
||||
<time className="admin-sms-record-time" dateTime={record.queuedAt}>
|
||||
<span>{submitDate}</span><strong>{getClock(record.queuedAt)}</strong>
|
||||
</time>
|
||||
<div className="admin-sms-record-main">
|
||||
<p className={`admin-sms-record-content${record.hasDrainageContent ? ' is-drainage' : ''}`}>
|
||||
<DrainageContent record={record} />
|
||||
<span className={`admin-sms-record-drainage-badge is-${record.hasDrainageContent === true ? 'yes' : record.hasDrainageContent === false ? 'no' : 'unknown'}`}>
|
||||
{record.hasDrainageContent === true ? '含引流' : record.hasDrainageContent === false ? '不含引流' : '未检测'}
|
||||
</span>
|
||||
</p>
|
||||
<div className="admin-sms-record-context">
|
||||
<span>{record.tenant?.name ?? record.tenantId ?? '运营端通道测试'} / {record.application?.name ?? record.applicationId ?? '-'}</span>
|
||||
<span>{record.phoneNumber} · {record.province ?? '-'} {record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}</span>
|
||||
<span>{record.channel?.name ?? record.channelId ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<time className="admin-sms-record-time" dateTime={record.deliveredAt ?? undefined}>
|
||||
{record.deliveredAt ? <><span>{getDate(record.deliveredAt)}</span><strong>{getClock(record.deliveredAt)}</strong></> : <span>暂无回执</span>}
|
||||
</time>
|
||||
<div className="admin-sms-record-billing">
|
||||
<MoneyText>¥{formatCents(record.amountCents)}</MoneyText>
|
||||
<span>{record.billingUnits} 分片 · {record.content.length} 字</span>
|
||||
</div>
|
||||
<button aria-label="查看发送详情" className="admin-sms-record-detail-link" onClick={() => onOpenDetail(record)} title="查看发送详情" type="button"><ChevronRight size={18} /></button>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Pagination
|
||||
nextDisabled={currentPage >= totalPages}
|
||||
|
||||
@@ -52,10 +52,10 @@ export function ClientUplinkMessagesPage() {
|
||||
|
||||
function openDetail(message: SmsUplinkMessage) {
|
||||
setSelectedMessage(message);
|
||||
setMatchedRecords([]);
|
||||
setMatchedRecords(message.messageRecord ? [message.messageRecord] : []);
|
||||
setDetailError('');
|
||||
|
||||
if (!message.messageId) {
|
||||
if (message.messageRecord || !message.messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +142,10 @@ export function ClientUplinkMessagesPage() {
|
||||
{ label: '手机号码', value: selectedMessage.phoneNumber },
|
||||
{ label: '上行时间', value: getTime(selectedMessage.receivedAt) },
|
||||
{ label: '接入号码', value: selectedMessage.destId },
|
||||
{ label: '网关消息ID', value: selectedMessage.messageId || '-' },
|
||||
{ label: '上行网关消息ID', value: selectedMessage.gatewayMessageId || '-' },
|
||||
{ label: '关联平台消息ID', value: selectedMessage.messageRecord?.messageId ?? selectedMessage.messageId ?? '-' },
|
||||
{ label: '匹配状态', value: selectedMessage.matchStatus || '-' },
|
||||
{ label: '匹配说明', value: selectedMessage.matchReason || '-', full: true },
|
||||
{ label: '上行内容', value: selectedMessage.content, full: true },
|
||||
]}
|
||||
/>
|
||||
@@ -151,9 +154,10 @@ export function ClientUplinkMessagesPage() {
|
||||
<DetailSection title="匹配发送记录">
|
||||
{matching ? <p className="uplink-detail-hint">正在查询真实下发记录...</p> : null}
|
||||
{detailError ? <p className="form-error">{detailError}</p> : null}
|
||||
{!matching && !selectedMessage.messageId ? <p className="uplink-detail-hint">该上行记录没有网关消息ID,无法匹配下发记录。</p> : null}
|
||||
{!matching && selectedMessage.messageId && matchedRecords.length === 0 && !detailError ? (
|
||||
<p className="uplink-detail-hint">未匹配到真实下发记录。</p>
|
||||
{!matching && matchedRecords.length === 0 && !detailError ? (
|
||||
<p className="uplink-detail-hint">
|
||||
{selectedMessage.matchStatus === 'ambiguous' ? '该上行存在多个候选,请联系运营人员认领。' : '未匹配到真实下发记录。'}
|
||||
</p>
|
||||
) : null}
|
||||
{matchedRecords.length > 0 ? (
|
||||
<div className="uplink-match-list">
|
||||
|
||||
Reference in New Issue
Block a user