fix: polish client templates docs dashboard and receipt display
CSS quality / css-quality (push) Has been cancelled
CSS quality / css-quality (push) Has been cancelled
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { Fragment, startTransition, useEffect, useMemo, useState } from 'react';
|
||||
import { FileText, Search, Smartphone } from 'lucide-react';
|
||||
import { clientApi, type SmsMessageRecord } from '@/api/adminApi';
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Tag,
|
||||
type DateRangeValue,
|
||||
} from '@/components/ui';
|
||||
import { recentBeijingDateRange } from '@/utils/dateTime';
|
||||
import { formatDateTime, recentBeijingDateRange } from '@/utils/dateTime';
|
||||
|
||||
const statusLabelMap: Record<string, string> = {
|
||||
delivered: '成功',
|
||||
@@ -44,25 +44,34 @@ const statusToneMap: Record<string, 'success' | 'info' | 'danger' | 'neutral'> =
|
||||
timeout: 'danger',
|
||||
};
|
||||
|
||||
function getDate(value?: string | null) {
|
||||
return value ? value.slice(0, 10) : '';
|
||||
}
|
||||
|
||||
function getReceipt(record: SmsMessageRecord) {
|
||||
const latest = record.receiptRecords?.[0] as { rawStatus?: string; receiptStatus?: string; deliveredAt?: string } | undefined;
|
||||
const latest = record.receiptRecords?.[0] as
|
||||
{ rawStatus?: string; receiptStatus?: string; deliveredAt?: string } | undefined;
|
||||
return {
|
||||
status: receiptStatusLabel(latest?.rawStatus ?? latest?.receiptStatus ?? record.receiptStatus),
|
||||
time: latest?.deliveredAt ?? record.deliveredAt,
|
||||
status: receiptStatusLabel(record.receiptStatus ?? latest?.rawStatus ?? latest?.receiptStatus),
|
||||
time: record.deliveredAt ?? latest?.deliveredAt,
|
||||
};
|
||||
}
|
||||
|
||||
function receiptStatusLabel(status?: string | null) {
|
||||
if (!status) return '-';
|
||||
if (!status) return '暂无回执';
|
||||
const normalized = status.trim().toUpperCase();
|
||||
return {
|
||||
DELIVRD: '送达成功', ACCEPTD: '已受理', UNDELIV: '未送达', REJECTD: '已拒绝',
|
||||
EXPIRED: '已过期', DELETED: '已删除', UNKNOWN: '状态未知',
|
||||
}[normalized] ?? statusLabelMap[status.toLowerCase()] ?? '状态未知';
|
||||
return (
|
||||
{
|
||||
DELIVERED: '送达成功',
|
||||
FAILED: '送达失败',
|
||||
TIMEOUT: '回执超时',
|
||||
DELIVRD: '送达成功',
|
||||
ACCEPTD: '已受理',
|
||||
UNDELIV: '未送达',
|
||||
REJECTD: '已拒绝',
|
||||
EXPIRED: '已过期',
|
||||
DELETED: '已删除',
|
||||
UNKNOWN: '状态未知',
|
||||
}[normalized] ??
|
||||
statusLabelMap[status.toLowerCase()] ??
|
||||
'状态未知'
|
||||
);
|
||||
}
|
||||
|
||||
export function ClientSendDetailPage() {
|
||||
@@ -81,16 +90,17 @@ export function ClientSendDetailPage() {
|
||||
|
||||
function loadData(targetPage = page) {
|
||||
setLoading(true);
|
||||
clientApi.listMessages({
|
||||
applicationId: applied.applicationId === 'all' ? undefined : applied.applicationId,
|
||||
phoneNumber: applied.phoneKeyword.trim() || undefined,
|
||||
status: applied.status === 'all' ? undefined : applied.status,
|
||||
contentKeyword: applied.contentKeyword.trim() || undefined,
|
||||
queuedAtFrom: applied.dateRange.start || undefined,
|
||||
queuedAtTo: applied.dateRange.end || undefined,
|
||||
page: targetPage,
|
||||
pageSize: 10,
|
||||
})
|
||||
clientApi
|
||||
.listMessages({
|
||||
applicationId: applied.applicationId === 'all' ? undefined : applied.applicationId,
|
||||
phoneNumber: applied.phoneKeyword.trim() || undefined,
|
||||
status: applied.status === 'all' ? undefined : applied.status,
|
||||
contentKeyword: applied.contentKeyword.trim() || undefined,
|
||||
queuedAtFrom: applied.dateRange.start || undefined,
|
||||
queuedAtTo: applied.dateRange.end || undefined,
|
||||
page: targetPage,
|
||||
pageSize: 10,
|
||||
})
|
||||
.then((result) => {
|
||||
setRecords(result.items);
|
||||
setTotal(result.total);
|
||||
@@ -101,20 +111,18 @@ export function ClientSendDetailPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData(page);
|
||||
startTransition(() => loadData(page));
|
||||
}, [applied, page]);
|
||||
|
||||
useEffect(() => {
|
||||
clientApi.listApplicationOptions()
|
||||
clientApi
|
||||
.listApplicationOptions()
|
||||
.then((items) => setApplications(items.map((item) => ({ id: item.id, name: item.name }))))
|
||||
.catch((reason: Error) => setError(reason.message || '应用列表加载失败'));
|
||||
}, []);
|
||||
|
||||
const applicationOptions = useMemo(() => {
|
||||
return [
|
||||
{ label: '全部应用', value: 'all' },
|
||||
...applications.map((item) => ({ label: item.name, value: item.id })),
|
||||
];
|
||||
return [{ label: '全部应用', value: 'all' }, ...applications.map((item) => ({ label: item.name, value: item.id }))];
|
||||
}, [applications]);
|
||||
|
||||
const filteredRows = records;
|
||||
@@ -129,7 +137,13 @@ export function ClientSendDetailPage() {
|
||||
}
|
||||
|
||||
function reset() {
|
||||
const defaults = { applicationId: 'all', status: 'all', dateRange: recentBeijingDateRange(7), contentKeyword: '', phoneKeyword: '' };
|
||||
const defaults = {
|
||||
applicationId: 'all',
|
||||
status: 'all',
|
||||
dateRange: recentBeijingDateRange(7),
|
||||
contentKeyword: '',
|
||||
phoneKeyword: '',
|
||||
};
|
||||
setApplicationId(defaults.applicationId);
|
||||
setStatus(defaults.status);
|
||||
setDateRange(defaults.dateRange);
|
||||
@@ -150,9 +164,18 @@ export function ClientSendDetailPage() {
|
||||
|
||||
<QueryPanel
|
||||
title="查询条件"
|
||||
summary={<>共找到 <strong>{total}</strong> 条发送记录</>}
|
||||
summary={
|
||||
<>
|
||||
共找到 <strong>{total}</strong> 条发送记录
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select label="应用名称" onChange={(event) => setApplicationId(event.target.value)} options={applicationOptions} value={applicationId} />
|
||||
<Select
|
||||
label="应用名称"
|
||||
onChange={(event) => setApplicationId(event.target.value)}
|
||||
options={applicationOptions}
|
||||
value={applicationId}
|
||||
/>
|
||||
<DateRangeInput label="发送时间" onChange={setDateRange} value={dateRange} />
|
||||
<Select
|
||||
label="发送状态"
|
||||
@@ -202,53 +225,79 @@ export function ClientSendDetailPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td className="ui-table__empty" colSpan={9}>正在加载真实发送记录...</td></tr>
|
||||
<tr>
|
||||
<td className="ui-table__empty" colSpan={9}>
|
||||
正在加载真实发送记录...
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredRows.length === 0 ? (
|
||||
<tr><td className="ui-table__empty" colSpan={9}>暂无发送记录</td></tr>
|
||||
) : visibleRows.map((record) => {
|
||||
const receipt = getReceipt(record);
|
||||
const region = record.province ?? '-';
|
||||
return (
|
||||
<Fragment key={record.id}>
|
||||
<tr className="send-detail-main-row">
|
||||
<td><strong className="send-detail-app-name">{record.application?.name ?? record.applicationId ?? '-'}</strong></td>
|
||||
<td>
|
||||
<span className="send-detail-time">
|
||||
{record.queuedAt.slice(0, 10)}
|
||||
<small>{record.queuedAt.slice(11, 19)}</small>
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<span className="send-detail-count">
|
||||
<strong>{[...record.content].length}字</strong>
|
||||
<small>{record.billingUnits}条</small>
|
||||
</span>
|
||||
</td>
|
||||
<td><strong>{record.phoneNumber}</strong></td>
|
||||
<td>{record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}</td>
|
||||
<td><span className="send-detail-region">{region}</span></td>
|
||||
<td style={{ textAlign: 'center' }}><Tag tone={statusToneMap[record.status] ?? 'info'}>{statusLabelMap[record.status] ?? '状态未知'}</Tag></td>
|
||||
<td style={{ textAlign: 'center' }}><strong className="send-detail-receipt-code">{receipt.status}</strong></td>
|
||||
<td>
|
||||
{receipt.time ? (
|
||||
<tr>
|
||||
<td className="ui-table__empty" colSpan={9}>
|
||||
暂无发送记录
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
visibleRows.map((record) => {
|
||||
const receipt = getReceipt(record);
|
||||
const region = record.province ?? '-';
|
||||
return (
|
||||
<Fragment key={record.id}>
|
||||
<tr className="send-detail-main-row">
|
||||
<td>
|
||||
<strong className="send-detail-app-name">
|
||||
{record.application?.name ?? record.applicationId ?? '-'}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span className="send-detail-time">
|
||||
{receipt.time.slice(0, 10)}
|
||||
<small>{receipt.time.slice(11, 19)}</small>
|
||||
{formatDateTime(record.queuedAt).slice(0, 10)}
|
||||
<small>{formatDateTime(record.queuedAt).slice(11, 19)}</small>
|
||||
</span>
|
||||
) : <span className="muted">-</span>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="send-detail-content-row">
|
||||
<td colSpan={9}>
|
||||
<div className="send-detail-content-block">
|
||||
<span>短信内容</span>
|
||||
<p>{record.content}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<span className="send-detail-count">
|
||||
<strong>{[...record.content].length}字</strong>
|
||||
<small>{record.billingUnits}条</small>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{record.phoneNumber}</strong>
|
||||
</td>
|
||||
<td>{record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}</td>
|
||||
<td>
|
||||
<span className="send-detail-region">{region}</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<Tag tone={statusToneMap[record.status] ?? 'info'}>
|
||||
{statusLabelMap[record.status] ?? '状态未知'}
|
||||
</Tag>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<strong className="send-detail-receipt-code">{receipt.status}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{receipt.time ? (
|
||||
<span className="send-detail-time">
|
||||
{formatDateTime(receipt.time).slice(0, 10)}
|
||||
<small>{formatDateTime(receipt.time).slice(11, 19)}</small>
|
||||
</span>
|
||||
) : (
|
||||
<span className="muted">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="send-detail-content-row">
|
||||
<td colSpan={9}>
|
||||
<div className="send-detail-content-block">
|
||||
<span>短信内容</span>
|
||||
<p>{record.content}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user