fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Search, Smartphone } from 'lucide-react';
|
||||
import { adminApi, type SmsMessageRecord, type SmsUplinkMessage } from '@/api/adminApi';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
@@ -12,50 +13,27 @@ import {
|
||||
type TableColumn,
|
||||
} from '@/components/ui';
|
||||
|
||||
type UplinkMessage = {
|
||||
id: string;
|
||||
phone: string;
|
||||
receivedAt: string;
|
||||
content: string;
|
||||
channel: string;
|
||||
accessNo: string;
|
||||
matchedRecord?: MatchedSendRecord;
|
||||
};
|
||||
|
||||
type MatchedSendRecord = {
|
||||
id: string;
|
||||
sentAt: string;
|
||||
enterprise: string;
|
||||
application: string;
|
||||
accessNo: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const matchedRecord: MatchedSendRecord = {
|
||||
id: 'MT20260119001',
|
||||
sentAt: '2026-01-19 12:25:28',
|
||||
enterprise: '上海XXX有限公司',
|
||||
application: 'XXX催收',
|
||||
accessNo: '1069558812',
|
||||
content: '【XXX科技】如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 如果内容很长,换行 拒收请回复R',
|
||||
};
|
||||
|
||||
const uplinkMessages: UplinkMessage[] = [
|
||||
{ id: 'MO20260112001', phone: '13500000888', receivedAt: '2026-01-12 19:27:10', content: 'R', channel: '通道名称通道名称通道名称', accessNo: '106912246726', matchedRecord },
|
||||
{ id: 'MO20260112002', phone: '13755558888', receivedAt: '2026-01-12 19:27:19', content: 'R', channel: '通道名称通道名称通道名称', accessNo: '106912246712', matchedRecord },
|
||||
{ id: 'MO20260112003', phone: '18800000555', receivedAt: '2026-01-12 19:27:19', content: '到家了', channel: '通道名称通道名称通道名称', accessNo: '106912246732' },
|
||||
{ id: 'MO20260112004', phone: '', receivedAt: '2026-01-12 19:27:19', content: 'XX', channel: '通道名称通道名称通道名称', accessNo: '106912346745' },
|
||||
{ id: 'MO20260112005', phone: '', receivedAt: '2026-01-12 19:27:19', content: 'XX', channel: '', accessNo: '' },
|
||||
{ id: 'MO20260112006', phone: '', receivedAt: '2026-01-12 19:27:19', content: 'XX', channel: '', accessNo: '' },
|
||||
{ id: 'MO20260112007', phone: '', receivedAt: '2026-01-12 19:27:19', content: '', channel: '', accessNo: '' },
|
||||
{ id: 'MO20260112008', phone: '', receivedAt: '2026-01-12 19:27:19', content: '', channel: '', accessNo: '' },
|
||||
];
|
||||
|
||||
function getDate(value: string) {
|
||||
return value.slice(0, 10);
|
||||
function getDate(value?: string | null) {
|
||||
return value ? value.slice(0, 10) : '';
|
||||
}
|
||||
|
||||
function UplinkDetailModal({ message, onClose }: { message: UplinkMessage; onClose: () => void }) {
|
||||
function getTime(value?: string | null) {
|
||||
return value ? `${value.slice(0, 10)} ${value.slice(11, 19)}` : '-';
|
||||
}
|
||||
|
||||
function UplinkDetailModal({
|
||||
detailError,
|
||||
matchedRecords,
|
||||
matching,
|
||||
message,
|
||||
onClose,
|
||||
}: {
|
||||
detailError: string;
|
||||
matchedRecords: SmsMessageRecord[];
|
||||
matching: boolean;
|
||||
message: SmsUplinkMessage;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
footer={<Button onClick={onClose} variant="ghost">关闭</Button>}
|
||||
@@ -70,19 +48,27 @@ function UplinkDetailModal({ message, onClose }: { message: UplinkMessage; onClo
|
||||
<div className="admin-uplink-info-grid">
|
||||
<div>
|
||||
<span>手机号码</span>
|
||||
<strong>{message.phone || '-'}</strong>
|
||||
<strong>{message.phoneNumber || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>上行时间</span>
|
||||
<strong>{message.receivedAt}</strong>
|
||||
<strong>{getTime(message.receivedAt)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>上行企业</span>
|
||||
<strong>{message.tenant?.name ?? message.tenantId ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>上行通道</span>
|
||||
<strong>{message.channel || '-'}</strong>
|
||||
<strong>{message.channel?.name ?? message.channelId ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>上行接入号</span>
|
||||
<strong>{message.accessNo || '-'}</strong>
|
||||
<strong>{message.destId || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>网关消息ID</span>
|
||||
<strong>{message.messageId || '-'}</strong>
|
||||
</div>
|
||||
<div className="admin-uplink-info-grid__full">
|
||||
<span>上行内容</span>
|
||||
@@ -93,36 +79,38 @@ function UplinkDetailModal({ message, onClose }: { message: UplinkMessage; onClo
|
||||
|
||||
<section className="admin-uplink-match-section">
|
||||
<h3>匹配发送记录</h3>
|
||||
<p>搜索到上行短信前7天内的下发成功记录</p>
|
||||
{message.matchedRecord ? (
|
||||
<article className="admin-uplink-match-card">
|
||||
{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>
|
||||
) : null}
|
||||
{matchedRecords.map((record) => (
|
||||
<article className="admin-uplink-match-card" key={record.id}>
|
||||
<div className="admin-uplink-match-grid">
|
||||
<div>
|
||||
<span>发送时间</span>
|
||||
<strong>{message.matchedRecord.sentAt}</strong>
|
||||
<strong>{getTime(record.queuedAt)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>发送企业</span>
|
||||
<strong>{message.matchedRecord.enterprise}</strong>
|
||||
<strong>{record.tenant?.name ?? record.tenantId ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>发送应用</span>
|
||||
<strong>{message.matchedRecord.application}</strong>
|
||||
<strong>{record.application?.name ?? record.applicationId ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>客户提交接入号</span>
|
||||
<strong>{message.matchedRecord.accessNo}</strong>
|
||||
<strong>{record.channel?.srcId ?? '-'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-uplink-match-content">
|
||||
<span>下发内容</span>
|
||||
<p>{message.matchedRecord.content}</p>
|
||||
<p>{record.content}</p>
|
||||
</div>
|
||||
<button type="button">添加到应用黑名单</button>
|
||||
</article>
|
||||
) : (
|
||||
<div className="admin-uplink-empty-match">暂无匹配发送记录</div>
|
||||
)}
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -130,21 +118,58 @@ function UplinkDetailModal({ message, onClose }: { message: UplinkMessage; onClo
|
||||
}
|
||||
|
||||
export function AdminSmsUplinkRecordsPage() {
|
||||
const [messages, setMessages] = useState<SmsUplinkMessage[]>([]);
|
||||
const [matchedRecords, setMatchedRecords] = useState<SmsMessageRecord[]>([]);
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||||
const [phoneKeyword, setPhoneKeyword] = useState('');
|
||||
const [contentKeyword, setContentKeyword] = useState('');
|
||||
const [selectedMessage, setSelectedMessage] = useState<UplinkMessage | null>(null);
|
||||
const [selectedMessage, setSelectedMessage] = useState<SmsUplinkMessage | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [matching, setMatching] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [detailError, setDetailError] = useState('');
|
||||
|
||||
function loadData() {
|
||||
setLoading(true);
|
||||
adminApi.listAdminUplinkMessages()
|
||||
.then((items) => {
|
||||
setMessages(items);
|
||||
setError('');
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '短信上行记录加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
function openDetail(message: SmsUplinkMessage) {
|
||||
setSelectedMessage(message);
|
||||
setMatchedRecords([]);
|
||||
setDetailError('');
|
||||
|
||||
if (!message.messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMatching(true);
|
||||
adminApi.listOperationMessages({ tenantId: message.tenantId ?? undefined, messageId: message.messageId })
|
||||
.then((items) => setMatchedRecords(items))
|
||||
.catch((reason: Error) => setDetailError(reason.message || '匹配发送记录加载失败'))
|
||||
.finally(() => setMatching(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const filteredMessages = useMemo(
|
||||
() => uplinkMessages.filter((item) => {
|
||||
() => messages.filter((item) => {
|
||||
const receivedDate = getDate(item.receivedAt);
|
||||
const matchesStartDate = !dateRange.start || receivedDate >= dateRange.start;
|
||||
const matchesEndDate = !dateRange.end || receivedDate <= dateRange.end;
|
||||
const matchesPhone = !phoneKeyword || item.phone.includes(phoneKeyword);
|
||||
const matchesPhone = !phoneKeyword || item.phoneNumber.includes(phoneKeyword);
|
||||
const matchesContent = !contentKeyword || item.content.includes(contentKeyword);
|
||||
return matchesStartDate && matchesEndDate && matchesPhone && matchesContent;
|
||||
}),
|
||||
[contentKeyword, dateRange.end, dateRange.start, phoneKeyword],
|
||||
[contentKeyword, dateRange.end, dateRange.start, messages, phoneKeyword],
|
||||
);
|
||||
|
||||
function resetFilters() {
|
||||
@@ -153,7 +178,7 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
setContentKeyword('');
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<UplinkMessage>> = [
|
||||
const columns: Array<TableColumn<SmsUplinkMessage>> = [
|
||||
{
|
||||
key: 'select',
|
||||
title: '',
|
||||
@@ -161,18 +186,18 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
align: 'center',
|
||||
render: () => <input aria-label="选择上行记录" className="admin-uplink-checkbox" type="checkbox" />,
|
||||
},
|
||||
{ key: 'phone', title: '手机号码', width: '170px', render: (record) => <strong>{record.phone}</strong> },
|
||||
{ key: 'receivedAt', title: '上行时间', width: '220px', render: (record) => <strong>{record.receivedAt}</strong> },
|
||||
{ key: 'phoneNumber', title: '手机号码', width: '170px', render: (record) => <strong>{record.phoneNumber}</strong> },
|
||||
{ key: 'receivedAt', title: '上行时间', width: '220px', render: (record) => <strong>{getTime(record.receivedAt)}</strong> },
|
||||
{ key: 'content', title: '上行内容', render: (record) => <span className="uplink-content">{record.content}</span> },
|
||||
{ key: 'channel', title: '上行通道', width: '260px', render: (record) => <strong>{record.channel}</strong> },
|
||||
{ key: 'accessNo', title: '上行接入号', width: '180px', render: (record) => <strong>{record.accessNo}</strong> },
|
||||
{ key: 'channel', title: '上行通道', width: '260px', render: (record) => <strong>{record.channel?.name ?? record.channelId}</strong> },
|
||||
{ key: 'accessNo', title: '上行接入号', width: '180px', render: (record) => <strong>{record.destId}</strong> },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
width: '140px',
|
||||
align: 'center',
|
||||
render: (record) => (
|
||||
<button className="admin-uplink-detail-link" onClick={() => setSelectedMessage(record)} type="button">查看详情</button>
|
||||
<button className="admin-uplink-detail-link" onClick={() => openDetail(record)} type="button">查看详情</button>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -191,17 +216,27 @@ export function AdminSmsUplinkRecordsPage() {
|
||||
<Input label="手机号码" onChange={(event) => setPhoneKeyword(event.target.value)} prefix={<Smartphone size={16} />} value={phoneKeyword} />
|
||||
<Input label="上行内容" onChange={(event) => setContentKeyword(event.target.value)} value={contentKeyword} />
|
||||
<div className="admin-uplink-filter__actions">
|
||||
<Button icon={<Search size={16} />}>查询</Button>
|
||||
<Button icon={<Search size={16} />} onClick={loadData}>查询</Button>
|
||||
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<div className="surface admin-uplink-table-card">
|
||||
<Table columns={columns} data={filteredMessages} emptyText="暂无上行短信记录" rowKey="id" />
|
||||
<Table columns={columns} data={loading ? [] : filteredMessages} emptyText={loading ? '正在加载真实上行短信记录...' : '暂无上行短信记录'} rowKey="id" />
|
||||
<Pagination total={filteredMessages.length} />
|
||||
</div>
|
||||
|
||||
{selectedMessage ? <UplinkDetailModal message={selectedMessage} onClose={() => setSelectedMessage(null)} /> : null}
|
||||
{selectedMessage ? (
|
||||
<UplinkDetailModal
|
||||
detailError={detailError}
|
||||
matchedRecords={matchedRecords}
|
||||
matching={matching}
|
||||
message={selectedMessage}
|
||||
onClose={() => setSelectedMessage(null)}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user