fix: align daily operations statistics
This commit is contained in:
+28
-1
@@ -354,6 +354,9 @@ export type ChannelQualityStat = {
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
total: number;
|
||||
acceptedCount: number;
|
||||
submitFailureCount: number;
|
||||
submitFailureRate: number;
|
||||
successCount: number;
|
||||
unknownCount: number;
|
||||
failureCount: number;
|
||||
@@ -378,10 +381,27 @@ export type SignatureQualityStat = {
|
||||
averageArrivalMs?: number | null;
|
||||
};
|
||||
|
||||
export type DailySendSummary = {
|
||||
total: number;
|
||||
successCount: number;
|
||||
unknownCount: number;
|
||||
failureCount: number;
|
||||
successRate: number;
|
||||
};
|
||||
|
||||
export type ApplicationQualityStat = DailySendSummary & {
|
||||
applicationId: string;
|
||||
applicationName: string;
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
};
|
||||
|
||||
export type SendQualityResponse = {
|
||||
date: string;
|
||||
summary: DailySendSummary;
|
||||
channels: ChannelQualityStat[];
|
||||
signatures: SignatureQualityStat[];
|
||||
applications: ApplicationQualityStat[];
|
||||
};
|
||||
|
||||
export type RechargeOrder = {
|
||||
@@ -581,7 +601,7 @@ export type ImportPreviewResponse = {
|
||||
|
||||
export type SmsMessageRecord = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
tenantId?: string | null;
|
||||
batchTaskId?: string | null;
|
||||
applicationId?: string | null;
|
||||
channelId?: string | null;
|
||||
@@ -607,6 +627,13 @@ export type SmsMessageRecord = {
|
||||
application?: { id: string; name: string };
|
||||
submitRecords?: SmsSubmitRecord[];
|
||||
receiptRecords?: SmsReceiptRecord[];
|
||||
downstreamDeliveries?: Array<{
|
||||
id: string;
|
||||
deliveryType: string;
|
||||
status: string;
|
||||
deliveredAt?: string | null;
|
||||
lastError?: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SmsSubmitRecord = {
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
import { adminApi, type DashboardResponse } from '@/api/adminApi';
|
||||
import { adminApi, type SendQualityResponse } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Chart, Input, Tag } from '@/components/ui';
|
||||
import { createBarOption, createPieOption } from '@/theme/chartOptions';
|
||||
|
||||
export function AdminAnalyticsPage() {
|
||||
const [statisticsDate, setStatisticsDate] = useState(() => shanghaiDateKey());
|
||||
const [dashboard, setDashboard] = useState<DashboardResponse | null>(null);
|
||||
const [tenantStats, setTenantStats] = useState<Array<{ tenantId: string | null; _count: { _all: number }; _sum: { amountCents?: number | null } }>>([]);
|
||||
const [channelStats, setChannelStats] = useState<Array<{ channelId: string; channelName: string; total: number }>>([]);
|
||||
const [quality, setQuality] = useState<SendQualityResponse | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
function loadData() {
|
||||
Promise.all([
|
||||
adminApi.getDashboard(),
|
||||
adminApi.listStatistics({ groupBy: 'tenantId' }),
|
||||
adminApi.getSendQuality(statisticsDate),
|
||||
])
|
||||
.then(([dashboardData, tenantData, qualityData]) => {
|
||||
setDashboard(dashboardData);
|
||||
setTenantStats((Array.isArray(tenantData) ? tenantData : []) as Array<{ tenantId: string | null; _count: { _all: number }; _sum: { amountCents?: number | null } }>);
|
||||
setChannelStats(qualityData.channels.map((item) => ({ channelId: item.channelId, channelName: item.channelName, total: item.total })));
|
||||
adminApi.getSendQuality(statisticsDate)
|
||||
.then((qualityData) => {
|
||||
setQuality(qualityData);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '统计数据加载失败'));
|
||||
@@ -30,14 +22,15 @@ export function AdminAnalyticsPage() {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const tenantOption = useMemo(() => createBarOption({
|
||||
labels: tenantStats.map((item) => item.tenantId ?? '未绑定企业'),
|
||||
series: [{ name: '发送量', data: tenantStats.map((item) => item._count._all) }],
|
||||
}), [tenantStats]);
|
||||
const applicationOption = useMemo(() => createBarOption({
|
||||
labels: quality?.applications.map((item) => item.applicationName) ?? [],
|
||||
series: [{ name: '发送量', data: quality?.applications.map((item) => item.total) ?? [] }],
|
||||
}), [quality]);
|
||||
|
||||
const channelOption = useMemo(() => createPieOption({
|
||||
data: channelStats.map((item) => ({ name: item.channelName || item.channelId, value: item.total })),
|
||||
}), [channelStats]);
|
||||
data: quality?.channels.map((item) => ({ name: item.channelName || item.channelId, value: item.total })) ?? [],
|
||||
}), [quality]);
|
||||
const effectiveDate = quality?.date ?? statisticsDate;
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
@@ -60,19 +53,14 @@ export function AdminAnalyticsPage() {
|
||||
|
||||
<div className="dashboard-grid">
|
||||
<div className="surface metric-card">
|
||||
<span>今日发送量</span>
|
||||
<strong>{dashboard?.today.sent.toLocaleString('zh-CN') ?? 0}</strong>
|
||||
<small>真实消息记录</small>
|
||||
<span>{effectiveDate} 发送量</span>
|
||||
<strong>{quality?.summary.total.toLocaleString('zh-CN') ?? 0}</strong>
|
||||
<small>所选日期真实消息记录</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>成功率</span>
|
||||
<strong>{dashboard?.today.successRate ?? 0}%</strong>
|
||||
<small>今日已回执</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>待审核</span>
|
||||
<strong>{dashboard?.pendingAuditCount ?? 0}</strong>
|
||||
<small>企业/签名/模板/风控</small>
|
||||
<span>{effectiveDate} 成功率</span>
|
||||
<strong>{(quality?.summary.successRate ?? 0).toFixed(1)}%</strong>
|
||||
<small>{quality?.summary.successCount.toLocaleString('zh-CN') ?? 0} 条已送达 / {quality?.summary.total.toLocaleString('zh-CN') ?? 0} 条发送</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,18 +68,18 @@ export function AdminAnalyticsPage() {
|
||||
<div className="surface chart-card">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<h2>企业发送排行</h2>
|
||||
<p className="muted">按真实短信消息记录聚合。</p>
|
||||
<h2>企业应用发送排行</h2>
|
||||
<p className="muted">{effectiveDate} 当天按企业应用名称聚合真实短信消息记录。</p>
|
||||
</div>
|
||||
<Tag tone="info">企业</Tag>
|
||||
<Tag tone="info">企业应用</Tag>
|
||||
</div>
|
||||
<Chart height={320} option={tenantOption} />
|
||||
<Chart height={320} option={applicationOption} />
|
||||
</div>
|
||||
<div className="surface chart-card">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<h2>通道占比</h2>
|
||||
<p className="muted">{statisticsDate} 当天按真实通道提交及回执聚合。</p>
|
||||
<p className="muted">{effectiveDate} 当天按真实通道提交及回执聚合。</p>
|
||||
</div>
|
||||
<Tag tone="accent">通道</Tag>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,8 @@ type SmsChannel = {
|
||||
unitPrice: number;
|
||||
status: ChannelStatus;
|
||||
total: number;
|
||||
submitFailureRate: number;
|
||||
submitFailureCount: number;
|
||||
successRate: number;
|
||||
successCount: number;
|
||||
unknownRate: number;
|
||||
@@ -154,6 +156,8 @@ function mapApiChannel(
|
||||
unitPrice: channel.unitPrice,
|
||||
status: resolveChannelStatus(channel, connections),
|
||||
total: quality?.total ?? 0,
|
||||
submitFailureRate: quality?.submitFailureRate ?? 0,
|
||||
submitFailureCount: quality?.submitFailureCount ?? 0,
|
||||
successRate: quality?.successRate ?? 0,
|
||||
successCount: quality?.successCount ?? 0,
|
||||
unknownRate: quality?.unknownRate ?? 0,
|
||||
@@ -256,6 +260,8 @@ function ChannelFormModal({
|
||||
unitPrice: yuanToMoneyUnits(unitPrice),
|
||||
status: channel?.status ?? 'connecting',
|
||||
total: channel?.total ?? 0,
|
||||
submitFailureRate: channel?.submitFailureRate ?? 0,
|
||||
submitFailureCount: channel?.submitFailureCount ?? 0,
|
||||
successRate: channel?.successRate ?? 0,
|
||||
successCount: channel?.successCount ?? 0,
|
||||
unknownRate: channel?.unknownRate ?? 0,
|
||||
@@ -607,7 +613,7 @@ export function AdminChannelsPage() {
|
||||
<Select label="运营商" onChange={(event) => setCarrier(event.target.value)} options={carrierOptions} value={carrier} />
|
||||
<Select label="当前状态" onChange={(event) => setStatus(event.target.value)} options={statusOptions} value={status} />
|
||||
<div className="audit-filter-actions">
|
||||
<Button icon={<Search size={16} />}>查询</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => void loadChannels()}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setCarrier('all'); setStatus('all'); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -618,8 +624,8 @@ export function AdminChannelsPage() {
|
||||
<span>通道信息</span>
|
||||
<span>运营商 / 成本</span>
|
||||
<span>状态</span>
|
||||
<span>今日总数</span>
|
||||
<span>今日发送质量</span>
|
||||
<span>今日提交</span>
|
||||
<span>今日提交 / 送达质量</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
{visibleChannels.map((channel) => (
|
||||
@@ -640,9 +646,10 @@ export function AdminChannelsPage() {
|
||||
</div>
|
||||
<strong className="sms-channel-total">{channel.total.toLocaleString('zh-CN')}</strong>
|
||||
<div className="sms-channel-quality">
|
||||
<RateBlock count={channel.successCount} label="成功" rate={channel.successRate} tone={channel.successRate >= 80 ? 'success' : 'warning'} />
|
||||
<RateBlock count={channel.unknownCount} label="未知" rate={channel.unknownRate} />
|
||||
<RateBlock count={channel.failureCount} label="失败" rate={channel.failureRate} tone={channel.failureRate >= 50 ? 'danger' : 'neutral'} />
|
||||
<RateBlock count={channel.submitFailureCount} label="提交失败" rate={channel.submitFailureRate} tone={channel.submitFailureCount > 0 ? 'danger' : 'neutral'} />
|
||||
<RateBlock count={channel.successCount} label="送达成功" rate={channel.successRate} tone={channel.successRate >= 80 ? 'success' : 'warning'} />
|
||||
<RateBlock count={channel.unknownCount} label="回执未知" rate={channel.unknownRate} />
|
||||
<RateBlock count={channel.failureCount} label="送达失败" rate={channel.failureRate} tone={channel.failureRate >= 50 ? 'danger' : 'neutral'} />
|
||||
</div>
|
||||
<div className="sms-channel-actions">
|
||||
<button className="sms-channel-report-entry" onClick={() => navigate(`/admin/channels/${channel.id}/reports`)} type="button">
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Button,
|
||||
Chart,
|
||||
Modal,
|
||||
Pagination,
|
||||
Table,
|
||||
Tag,
|
||||
type TableColumn,
|
||||
@@ -27,6 +28,12 @@ type EnterpriseSpendRank = {
|
||||
availableBalance: number;
|
||||
};
|
||||
|
||||
type RankedSignatureQualityStat = SignatureQualityStat & {
|
||||
rank: number;
|
||||
};
|
||||
|
||||
const SIGNATURE_PAGE_SIZE = 10;
|
||||
|
||||
const balanceTone = {
|
||||
充足: 'success',
|
||||
紧张: 'warning',
|
||||
@@ -47,6 +54,8 @@ export function AdminHome() {
|
||||
const [quality, setQuality] = useState<SendQualityResponse | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [selectedEnterprise, setSelectedEnterprise] = useState<EnterpriseSpendRank | null>(null);
|
||||
const [plainSignaturePage, setPlainSignaturePage] = useState(1);
|
||||
const [drainageSignaturePage, setDrainageSignaturePage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([adminApi.getDashboard(), adminApi.getSendQuality()])
|
||||
@@ -131,8 +140,8 @@ export function AdminHome() {
|
||||
},
|
||||
];
|
||||
|
||||
const signatureColumns: Array<TableColumn<SignatureQualityStat>> = [
|
||||
{ key: 'rank', title: '排名', width: '72px', render: (_record, index) => index + 1 },
|
||||
const signatureColumns: Array<TableColumn<RankedSignatureQualityStat>> = [
|
||||
{ key: 'rank', title: '排名', width: '72px', render: (record) => record.rank },
|
||||
{ key: 'signatureName', title: '签名', render: (record) => <div><strong>{record.signatureName}</strong><p className="text-caption">{record.tenantName}</p></div> },
|
||||
{ key: 'total', title: '发送总数', align: 'right', render: (record) => formatCount(record.total) },
|
||||
{ key: 'successCount', title: '成功', align: 'right', render: (record) => formatCount(record.successCount) },
|
||||
@@ -143,6 +152,22 @@ export function AdminHome() {
|
||||
];
|
||||
const plainSignatureQuality = quality?.signatures.filter((item) => !item.hasDrainage) ?? [];
|
||||
const drainageSignatureQuality = quality?.signatures.filter((item) => item.hasDrainage) ?? [];
|
||||
const plainSignatureTotalPages = Math.max(1, Math.ceil(plainSignatureQuality.length / SIGNATURE_PAGE_SIZE));
|
||||
const drainageSignatureTotalPages = Math.max(1, Math.ceil(drainageSignatureQuality.length / SIGNATURE_PAGE_SIZE));
|
||||
const pagedPlainSignatureQuality = plainSignatureQuality
|
||||
.slice((plainSignaturePage - 1) * SIGNATURE_PAGE_SIZE, plainSignaturePage * SIGNATURE_PAGE_SIZE)
|
||||
.map((item, index) => ({ ...item, rank: (plainSignaturePage - 1) * SIGNATURE_PAGE_SIZE + index + 1 }));
|
||||
const pagedDrainageSignatureQuality = drainageSignatureQuality
|
||||
.slice((drainageSignaturePage - 1) * SIGNATURE_PAGE_SIZE, drainageSignaturePage * SIGNATURE_PAGE_SIZE)
|
||||
.map((item, index) => ({ ...item, rank: (drainageSignaturePage - 1) * SIGNATURE_PAGE_SIZE + index + 1 }));
|
||||
|
||||
useEffect(() => {
|
||||
setPlainSignaturePage((page) => Math.min(page, plainSignatureTotalPages));
|
||||
}, [plainSignatureTotalPages]);
|
||||
|
||||
useEffect(() => {
|
||||
setDrainageSignaturePage((page) => Math.min(page, drainageSignatureTotalPages));
|
||||
}, [drainageSignatureTotalPages]);
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-dashboard">
|
||||
@@ -207,7 +232,17 @@ export function AdminHome() {
|
||||
</div>
|
||||
<Tag tone="info">{plainSignatureQuality.length} 个签名</Tag>
|
||||
</div>
|
||||
<Table columns={signatureColumns} data={plainSignatureQuality} emptyText="今日暂无不含引流的签名发送记录" rowKey="id" />
|
||||
<Table columns={signatureColumns} data={pagedPlainSignatureQuality} emptyText="今日暂无不含引流的签名发送记录" rowKey="id" />
|
||||
<Pagination
|
||||
total={plainSignatureQuality.length}
|
||||
page={plainSignaturePage}
|
||||
totalPages={plainSignatureTotalPages}
|
||||
previousDisabled={plainSignaturePage <= 1}
|
||||
nextDisabled={plainSignaturePage >= plainSignatureTotalPages}
|
||||
onPrevious={() => setPlainSignaturePage((page) => Math.max(1, page - 1))}
|
||||
onNext={() => setPlainSignaturePage((page) => Math.min(plainSignatureTotalPages, page + 1))}
|
||||
onPageChange={setPlainSignaturePage}
|
||||
/>
|
||||
</div>
|
||||
<div className="surface section-stack">
|
||||
<div className="section-heading">
|
||||
@@ -217,7 +252,17 @@ export function AdminHome() {
|
||||
</div>
|
||||
<Tag tone="accent">{drainageSignatureQuality.length} 个签名</Tag>
|
||||
</div>
|
||||
<Table columns={signatureColumns} data={drainageSignatureQuality} emptyText="今日暂无含引流的签名发送记录" rowKey="id" />
|
||||
<Table columns={signatureColumns} data={pagedDrainageSignatureQuality} emptyText="今日暂无含引流的签名发送记录" rowKey="id" />
|
||||
<Pagination
|
||||
total={drainageSignatureQuality.length}
|
||||
page={drainageSignaturePage}
|
||||
totalPages={drainageSignatureTotalPages}
|
||||
previousDisabled={drainageSignaturePage <= 1}
|
||||
nextDisabled={drainageSignaturePage >= drainageSignatureTotalPages}
|
||||
onPrevious={() => setDrainageSignaturePage((page) => Math.max(1, page - 1))}
|
||||
onNext={() => setDrainageSignaturePage((page) => Math.min(drainageSignatureTotalPages, page + 1))}
|
||||
onPageChange={setDrainageSignaturePage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { AlertTriangle, Download, MessageSquare, Search, Smartphone } from 'lucide-react';
|
||||
import { AlertTriangle, Download, Info, MessageSquare, Search, Smartphone } from 'lucide-react';
|
||||
import { adminApi, type SmsMessageRecord, type SmsMessageSegmentAudit, type SmsReceiptRecord, type SmsSubmitRecord } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
|
||||
import { formatCents } from '@/utils/currency';
|
||||
@@ -8,8 +8,9 @@ const statusLabelMap: Record<string, string> = {
|
||||
delivered: '发送成功',
|
||||
queued: '排队中',
|
||||
submitted: '已提交',
|
||||
submit_failed: '提交失败',
|
||||
unknown: '未知',
|
||||
failed: '失败',
|
||||
failed: '送达失败',
|
||||
rejected: '已拒绝',
|
||||
};
|
||||
|
||||
@@ -17,6 +18,7 @@ const statusToneMap: Record<string, 'success' | 'neutral' | 'danger' | 'info'> =
|
||||
delivered: 'success',
|
||||
queued: 'info',
|
||||
submitted: 'info',
|
||||
submit_failed: 'danger',
|
||||
unknown: 'neutral',
|
||||
failed: 'danger',
|
||||
rejected: 'danger',
|
||||
@@ -26,6 +28,7 @@ const statusDotClassMap: Record<string, string> = {
|
||||
delivered: 'is-success',
|
||||
queued: 'is-unknown',
|
||||
submitted: 'is-unknown',
|
||||
submit_failed: 'is-failed',
|
||||
unknown: 'is-unknown',
|
||||
failed: 'is-failed',
|
||||
rejected: 'is-failed',
|
||||
@@ -85,6 +88,36 @@ function getStatusLabel(status?: string | null) {
|
||||
return status ? (statusLabelMap[status] ?? status) : '-';
|
||||
}
|
||||
|
||||
function isSubmitFailure(record: SmsMessageRecord) {
|
||||
return record.status === 'submit_failed' || ['rejected', 'timeout'].includes(record.submitStatus ?? '');
|
||||
}
|
||||
|
||||
function getRecordStatus(record: SmsMessageRecord) {
|
||||
return isSubmitFailure(record) ? 'submit_failed' : record.status;
|
||||
}
|
||||
|
||||
function getRecordStatusLabel(record: SmsMessageRecord) {
|
||||
return getStatusLabel(getRecordStatus(record));
|
||||
}
|
||||
|
||||
function getReceiptNotice(record: SmsMessageRecord) {
|
||||
const hasPlatformFailureReceipt = (record.receiptRecords ?? []).some((receipt) =>
|
||||
receipt.gatewayMessageId.startsWith('PLATFORM:') && receipt.rawStatus === 'REJECTD',
|
||||
);
|
||||
if (hasPlatformFailureReceipt) {
|
||||
const deliveries = (record.downstreamDeliveries ?? []).filter((item) => item.deliveryType === 'receipt');
|
||||
if (deliveries.some((item) => item.status === 'delivered')) {
|
||||
return '平台已生成失败回执并通知企业';
|
||||
}
|
||||
const deliveryStatuses = Array.from(new Set(deliveries.map((item) => item.status)));
|
||||
return `平台已生成失败回执,企业通知状态:${deliveryStatuses.join('、') || '待投递'}`;
|
||||
}
|
||||
if (!record.tenantId && !record.applicationId && record.messageId.startsWith('MSG-TEST-')) {
|
||||
return '运营端通道测试,无需生成客户回执';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCarrierLabel(carrier?: string | null) {
|
||||
return carrier ? (carrierLabelMap[carrier] ?? carrier) : '-';
|
||||
}
|
||||
@@ -121,7 +154,8 @@ function buildRouteRows(record: SmsMessageRecord): RouteRow[] {
|
||||
}];
|
||||
}
|
||||
|
||||
function StatusLine({ status }: { status: string }) {
|
||||
function StatusLine({ record }: { record: SmsMessageRecord }) {
|
||||
const status = getRecordStatus(record);
|
||||
return (
|
||||
<span className="admin-sms-record-status">
|
||||
<i className={statusDotClassMap[status] ?? 'is-unknown'} />
|
||||
@@ -149,7 +183,7 @@ function downloadCsv(records: SmsMessageRecord[]) {
|
||||
record.billingUnits,
|
||||
formatCents(record.amountCents),
|
||||
record.channel?.name ?? record.channelId ?? '',
|
||||
getStatusLabel(record.status),
|
||||
getRecordStatusLabel(record),
|
||||
getTime(record.deliveredAt),
|
||||
record.content,
|
||||
]),
|
||||
@@ -176,6 +210,8 @@ function SendDetailModal({
|
||||
}) {
|
||||
const routeRows = buildRouteRows(record);
|
||||
const sentAccessNumber = `${record.channel?.srcId ?? ''}${record.applicationExtension ?? ''}`;
|
||||
const displayStatus = getRecordStatus(record);
|
||||
const receiptNotice = getReceiptNotice(record);
|
||||
return (
|
||||
<Modal
|
||||
footer={<Button onClick={onClose} variant="ghost">关闭</Button>}
|
||||
@@ -188,7 +224,7 @@ function SendDetailModal({
|
||||
<div className="admin-sms-detail-overview">
|
||||
<div>
|
||||
<span>最终状态</span>
|
||||
<Tag tone={record.status === 'delivered' ? 'success' : ['failed', 'rejected'].includes(record.status) ? 'danger' : 'info'}>{getStatusLabel(record.status)}</Tag>
|
||||
<Tag tone={statusToneMap[displayStatus] ?? 'info'}>{getRecordStatusLabel(record)}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span>提交状态</span>
|
||||
@@ -219,6 +255,12 @@ function SendDetailModal({
|
||||
<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">{record.content}</p>
|
||||
@@ -260,7 +302,7 @@ function SendDetailModal({
|
||||
<h3>状态信息</h3>
|
||||
<div className="admin-sms-detail-status-grid">
|
||||
<div><span>消息编号</span><strong>{record.messageId}</strong></div>
|
||||
<div><span>发送状态</span><strong>{getStatusLabel(record.status)}</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>
|
||||
@@ -377,7 +419,11 @@ export function AdminSmsRecordsPage() {
|
||||
|
||||
const enterpriseOptions = useMemo(() => {
|
||||
const tenants = new Map<string, string>();
|
||||
records.forEach((record) => tenants.set(record.tenantId, record.tenant?.name ?? record.tenantId));
|
||||
records.forEach((record) => {
|
||||
if (record.tenantId) {
|
||||
tenants.set(record.tenantId, record.tenant?.name ?? record.tenantId);
|
||||
}
|
||||
});
|
||||
return [{ label: '全部企业', value: 'all' }, ...Array.from(tenants, ([value, label]) => ({ label, value }))];
|
||||
}, [records]);
|
||||
|
||||
@@ -441,7 +487,8 @@ export function AdminSmsRecordsPage() {
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '发送成功', value: 'delivered' },
|
||||
{ label: '未知', value: 'unknown' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
{ label: '提交失败', value: 'submit_failed' },
|
||||
{ label: '送达失败', value: 'failed' },
|
||||
]}
|
||||
value={status}
|
||||
/>
|
||||
@@ -460,10 +507,10 @@ export function AdminSmsRecordsPage() {
|
||||
<article className="admin-sms-record-card" key={record.id}>
|
||||
<header>
|
||||
<div className="admin-sms-record-sender">
|
||||
<strong>{record.tenant?.name ?? record.tenantId}</strong>
|
||||
<strong>{record.tenant?.name ?? record.tenantId ?? '运营端通道测试'}</strong>
|
||||
<span>{record.application?.name ?? record.applicationId ?? '-'}</span>
|
||||
</div>
|
||||
<StatusLine status={record.status} />
|
||||
<StatusLine record={record} />
|
||||
<time>{getDate(record.queuedAt)} {getClock(record.queuedAt)}</time>
|
||||
</header>
|
||||
<p className="admin-sms-record-content">{record.content}</p>
|
||||
|
||||
+19
-4
@@ -5342,7 +5342,7 @@ h3 {
|
||||
}
|
||||
|
||||
.admin-signature-rank-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.admin-workload-grid {
|
||||
@@ -6661,8 +6661,8 @@ h3 {
|
||||
.sms-channel-table__row {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: minmax(150px, 1.2fr) 88px 86px 96px minmax(210px, 1.35fr) 190px;
|
||||
min-width: 860px;
|
||||
grid-template-columns: minmax(150px, 1.2fr) 88px 86px 96px minmax(280px, 1.6fr) 190px;
|
||||
min-width: 940px;
|
||||
}
|
||||
|
||||
.sms-channel-table__head {
|
||||
@@ -6740,7 +6740,7 @@ h3 {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
@@ -9293,6 +9293,21 @@ h3 {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-sms-detail-notice {
|
||||
align-items: center;
|
||||
background: var(--color-selected-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--color-selected) 28%, transparent);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-selected);
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.admin-sms-detail-notice strong {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.admin-sms-send-detail h3 {
|
||||
color: var(--color-text-strong);
|
||||
font-size: var(--font-size-md);
|
||||
|
||||
Reference in New Issue
Block a user