revert: roll back signature quality period trends

This commit is contained in:
hectorzhao
2026-08-09 23:24:50 +08:00
parent e0c8f82bcf
commit 232d1c22a3
11 changed files with 38 additions and 393 deletions
-13
View File
@@ -65,17 +65,6 @@ export type SignatureCarrierBusinessQualityStat = {
averageArrivalMs?: number | null;
};
export type SignatureChannelCarrierDailyQualityStat = {
signatureId: string;
channelId: string;
channelName: string;
carrier: string;
date: string;
businessMessageCount: number;
deliveredMessageCount: number;
deliveryRate: number;
};
export type SignatureChannelQualityItem = {
signatureId: string;
signatureName: string;
@@ -94,12 +83,10 @@ export type SignatureChannelQualityItem = {
carrierOverview: SignatureCarrierBusinessQualityStat[];
breakdowns: SignatureChannelCarrierQualityStat[];
drainageBreakdowns: SignatureChannelCarrierDrainageQualityStat[];
dailyBreakdowns: SignatureChannelCarrierDailyQualityStat[];
};
export type SignatureChannelQualityResponse = {
date: string;
selectedDaySignatureCount: number;
items: SignatureChannelQualityItem[];
total: number;
page: number;
+20 -150
View File
@@ -4,12 +4,11 @@ import {
adminApi,
type SendQualityResponse,
type SignatureChannelCarrierQualityStat,
type SignatureChannelCarrierDailyQualityStat,
type SignatureChannelQualityItem,
type SignatureChannelQualityResponse,
} from '@/api/adminApi';
import { Breadcrumb, Button, Chart, Input, Pagination, Table, Tag, type TableColumn } from '@/components/ui';
import { createBarOption, createPieOption, createQualityTrendOption } from '@/theme/chartOptions';
import { createBarOption, createPieOption } from '@/theme/chartOptions';
import { successRateClassName } from '@/utils/successRate';
const carrierOrder = ['mobile', 'unicom', 'telecom', 'unknown'];
@@ -26,13 +25,6 @@ const carrierLabels: Record<string, string> = {
unknown: '未知',
};
type TrendSelection = {
channelId: string;
channelName: string;
carrier: string;
days: 3 | 7 | 30;
};
export function AdminAnalyticsPage() {
const [statisticsDate, setStatisticsDate] = useState(() => shanghaiDateKey());
const [quality, setQuality] = useState<SendQualityResponse | null>(null);
@@ -112,48 +104,48 @@ export function AdminAnalyticsPage() {
},
{
key: 'businessTotal',
title: '近30日业务短信',
title: '业务短信',
width: '110px',
align: 'right',
render: (record) => record.total.toLocaleString('zh-CN'),
},
{
key: 'channelSubmitTotal',
title: '近30日通道提交',
title: '通道提交',
width: '110px',
align: 'right',
render: (record) => record.channelSubmitTotal.toLocaleString('zh-CN'),
},
{
key: 'successCount',
title: '近30日送达',
title: '送达成功',
width: '110px',
align: 'right',
render: (record) => <span className="quality-number quality-number--success">{record.successCount.toLocaleString('zh-CN')}</span>,
},
{
key: 'failureCount',
title: '近30日送达失败',
title: '送达失败',
width: '110px',
align: 'right',
render: (record) => <span className="quality-number quality-number--danger">{record.failureCount.toLocaleString('zh-CN')}</span>,
},
{
key: 'submitFailureCount',
title: '近30日提交失败',
title: '提交失败',
width: '110px',
align: 'right',
render: (record) => <span className="quality-number quality-number--warning">{record.submitFailureCount.toLocaleString('zh-CN')}</span>,
},
{
key: 'successRate',
title: '近30日成功率',
title: '成功率',
width: '170px',
render: (record) => <QualityRate value={record.successRate} />,
},
{
key: 'averageArrivalMs',
title: '近30日平均到达',
title: '平均到达时间',
width: '140px',
align: 'right',
render: (record) => formatDuration(record.averageArrivalMs),
@@ -183,11 +175,11 @@ export function AdminAnalyticsPage() {
<section className="page-stack">
<div className="page-heading">
<div>
<Breadcrumb items={['签名发送质量']} />
<Breadcrumb items={['数据统计']} />
</div>
<div className="page-actions">
<Input
aria-label="统计截止日"
aria-label="统计日"
max={shanghaiDateKey()}
onChange={(event) => setStatisticsDate(event.target.value)}
type="date"
@@ -206,11 +198,6 @@ export function AdminAnalyticsPage() {
<strong>{quality?.summary.total.toLocaleString('zh-CN') ?? 0}</strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{signatureQuality?.selectedDaySignatureCount.toLocaleString('zh-CN') ?? 0}</strong>
<small>{effectiveDate} </small>
</div>
<div className="surface metric-card">
<span>{effectiveDate} </span>
<strong>{(quality?.summary.successRate ?? 0).toFixed(1)}%</strong>
@@ -245,11 +232,11 @@ export function AdminAnalyticsPage() {
<div className="signature-quality-card__heading">
<div>
<div className="section-heading__title">
<h2></h2>
<h2></h2>
<Tag tone="info"></Tag>
</div>
<p className="muted">
{signatureQuality?.date ?? effectiveDate} 3/7/30
{signatureQuality?.date ?? effectiveDate}
</p>
</div>
<div className="signature-quality-card__query">
@@ -267,12 +254,12 @@ export function AdminAnalyticsPage() {
</div>
<div className="signature-quality-card__note">
<strong></strong>
3 7 30 2629
</div>
<Table
columns={signatureColumns}
data={signatureQuality?.items ?? []}
emptyText={loading ? '正在加载签名统计…' : '截至所选日期的近30日暂无已登记签名发送数据'}
emptyText={loading ? '正在加载签名统计…' : '所选日期暂无已登记签名发送数据'}
pagination={false}
rowKey="signatureId"
/>
@@ -311,7 +298,6 @@ function SignatureQualityDrawer({
onClose: () => void;
}) {
const [matrixMode, setMatrixMode] = useState<'overall' | 'drainage'>('overall');
const [trendSelection, setTrendSelection] = useState<TrendSelection | null>(null);
const carriers = item.carrierOverview
.map((carrier) => ({
...carrier,
@@ -332,10 +318,6 @@ function SignatureQualityDrawer({
.map((entry) => [entry.channelId, entry.channelName])).entries()]
.map(([channelId, channelName]) => ({ channelId, channelName }));
const visibleCarriers = carrierOrder.filter((carrier) => item.breakdowns.some((entry) => normalizeCarrier(entry.carrier) === carrier));
const businessQualityRows = [...new Map(item.dailyBreakdowns.map((entry) => [
`${entry.channelId}:${normalizeCarrier(entry.carrier)}`,
{ channelId: entry.channelId, channelName: entry.channelName, carrier: normalizeCarrier(entry.carrier) },
])).values()];
return (
<div className="signature-quality-drawer__backdrop" onMouseDown={(event) => {
@@ -353,56 +335,17 @@ function SignatureQualityDrawer({
<div className="signature-quality-drawer__body">
<div className="signature-quality-overview">
<QualityMetric label="近30日业务短信" value={item.total.toLocaleString('zh-CN')} />
<QualityMetric label="近30日通道提交" value={item.channelSubmitTotal.toLocaleString('zh-CN')} />
<QualityMetric label="近30日最终成功率" value={`${item.successRate.toFixed(1)}%`} valueClassName={successRateClassName(item.successRate)} />
<QualityMetric label="近30日平均到达时间" value={formatDuration(item.averageArrivalMs)} />
<QualityMetric label="业务短信" value={item.total.toLocaleString('zh-CN')} />
<QualityMetric label="通道提交" value={item.channelSubmitTotal.toLocaleString('zh-CN')} />
<QualityMetric label="最终成功率" value={`${item.successRate.toFixed(1)}%`} valueClassName={successRateClassName(item.successRate)} />
<QualityMetric label="平均到达时间" value={formatDuration(item.averageArrivalMs)} />
</div>
<section className="signature-quality-section">
<div className="signature-quality-section__heading">
<div>
<h3> × </h3>
<p></p>
</div>
</div>
<div className="signature-business-quality-table">
<table>
<thead><tr><th></th><th></th>{([3, 7, 30] as const).map((days) => <th key={days}> {days} </th>)}</tr></thead>
<tbody>
{businessQualityRows.map((row) => (
<tr key={`${row.channelId}:${row.carrier}`}>
<th>{row.channelName}</th>
<td><Tag tone={carrierTagTone(row.carrier)}>{carrierLabel(row.carrier)}</Tag></td>
{([3, 7, 30] as const).map((days) => {
const metric = aggregateBusinessQuality(item.dailyBreakdowns, row.channelId, row.carrier, date, days);
return (
<td key={days}>
<button
className="signature-business-quality-metric"
onClick={() => setTrendSelection({ ...row, days })}
type="button"
>
<strong>{metric.sent.toLocaleString('zh-CN')} / {metric.delivered.toLocaleString('zh-CN')}</strong>
<span className={successRateClassName(metric.rate)}>{metric.rate.toFixed(1)}%</span>
<small></small>
</button>
</td>
);
})}
</tr>
))}
{businessQualityRows.length === 0 ? <tr><td colSpan={5}> 30 </td></tr> : null}
</tbody>
</table>
</div>
</section>
<section className="signature-quality-section">
<div className="signature-quality-section__heading">
<div>
<h3></h3>
<p>30</p>
<p></p>
</div>
</div>
<div className="signature-carrier-grid">
@@ -427,7 +370,7 @@ function SignatureQualityDrawer({
<div>
<h3> × </h3>
<p>{matrixMode === 'overall'
? '整体口径展示近30日该组合全部真实提交;“—”表示该期间没有真实提交。'
? '整体口径展示该组合全部真实提交;“—”表示所选日期没有真实提交。'
: '固定按含引流、不含引流、未检测三行及移动、联通、电信三列展示;没有真实提交的组合显示 0。'}</p>
</div>
<div className="page-actions"><Button onClick={() => setMatrixMode('overall')} size="sm" variant={matrixMode === 'overall' ? 'primary' : 'ghost'}></Button><Button onClick={() => setMatrixMode('drainage')} size="sm" variant={matrixMode === 'drainage' ? 'primary' : 'ghost'}></Button></div>
@@ -488,84 +431,11 @@ function SignatureQualityDrawer({
</p>
</div>
{trendSelection ? (
<SignatureQualityTrend
date={date}
entries={item.dailyBreakdowns}
onClose={() => setTrendSelection(null)}
selection={trendSelection}
signatureName={item.signatureName}
/>
) : null}
</aside>
</div>
);
}
function SignatureQualityTrend({
date,
entries,
onClose,
selection,
signatureName,
}: {
date: string;
entries: SignatureChannelCarrierDailyQualityStat[];
onClose: () => void;
selection: TrendSelection;
signatureName: string;
}) {
const dates = inclusiveDateKeys(date, selection.days);
const points = dates.map((dateKey) => entries.find((entry) => (
entry.date === dateKey
&& entry.channelId === selection.channelId
&& normalizeCarrier(entry.carrier) === selection.carrier
)));
const option = createQualityTrendOption({
labels: dates.map((dateKey) => dateKey.slice(5)),
sent: points.map((point) => point?.businessMessageCount ?? 0),
delivered: points.map((point) => point?.deliveredMessageCount ?? 0),
rates: points.map((point) => point?.deliveryRate ?? 0),
});
return (
<div className="signature-quality-trend" role="dialog" aria-modal="true" aria-label="签名发送质量趋势">
<div className="signature-quality-trend__heading">
<div><h3>{signatureName} · {selection.days} </h3><p>{selection.channelName} · {carrierLabel(selection.carrier)} · {dates[0]} {date}</p></div>
<button aria-label="关闭趋势图" onClick={onClose} type="button"><X size={18} /></button>
</div>
<Chart height={360} option={option} />
<p className="muted">线 0% </p>
</div>
);
}
function aggregateBusinessQuality(
entries: SignatureChannelCarrierDailyQualityStat[],
channelId: string,
carrier: string,
date: string,
days: 3 | 7 | 30,
) {
const includedDates = new Set(inclusiveDateKeys(date, days));
const relevant = entries.filter((entry) => (
entry.channelId === channelId
&& normalizeCarrier(entry.carrier) === carrier
&& includedDates.has(entry.date)
));
const sent = relevant.reduce((sum, entry) => sum + entry.businessMessageCount, 0);
const delivered = relevant.reduce((sum, entry) => sum + entry.deliveredMessageCount, 0);
return { sent, delivered, rate: sent === 0 ? 0 : delivered * 100 / sent };
}
function inclusiveDateKeys(endDate: string, days: number) {
const [year, month, day] = endDate.split('-').map(Number);
const end = new Date(Date.UTC(year, month - 1, day));
return Array.from({ length: days }, (_, index) => {
const current = new Date(end.getTime() - (days - index - 1) * 24 * 60 * 60 * 1000);
return current.toISOString().slice(0, 10);
});
}
function QualityMetric({ label, value, valueClassName }: { label: string; value: string; valueClassName?: string }) {
return (
<div className="signature-quality-metric">
+1 -1
View File
@@ -105,7 +105,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
{ label: '运营看板', to: '/admin', icon: Gauge },
{ label: '发送监控', to: '/admin/monitor', icon: Activity },
{ label: '网关异常', to: '/admin/gateway-submit-exceptions', icon: AlertTriangle },
{ label: '签名发送质量', to: '/admin/analytics', icon: BarChart3 },
{ label: '数据统计', to: '/admin/analytics', icon: BarChart3 },
],
},
{
-95
View File
@@ -6767,101 +6767,6 @@
font-size: 13px;
}
.signature-business-quality-table {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
overflow: auto;
}
.signature-business-quality-table table {
border-collapse: collapse;
min-width: 820px;
width: 100%;
}
.signature-business-quality-table th,
.signature-business-quality-table td {
border-bottom: 1px solid var(--color-border);
padding: var(--space-3);
text-align: left;
vertical-align: middle;
}
.signature-business-quality-table thead th {
background: var(--color-surface-muted);
color: var(--color-text-muted);
font-size: 13px;
}
.signature-business-quality-table tbody tr:last-child > * {
border-bottom: 0;
}
.signature-business-quality-metric {
background: transparent;
border: 0;
cursor: pointer;
display: grid;
gap: 3px;
padding: 0;
text-align: left;
}
.signature-business-quality-metric strong {
color: var(--color-text-strong);
}
.signature-business-quality-metric span {
font-weight: 600;
}
.signature-business-quality-metric small {
color: var(--color-primary);
}
.signature-business-quality-metric:hover small {
text-decoration: underline;
}
.signature-quality-trend {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
left: 50%;
max-width: 880px;
padding: var(--space-5);
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: min(88%, 880px);
z-index: 2;
}
.signature-quality-trend__heading {
align-items: flex-start;
display: flex;
justify-content: space-between;
}
.signature-quality-trend__heading h3,
.signature-quality-trend__heading p {
margin: 0;
}
.signature-quality-trend__heading p {
color: var(--color-text-muted);
margin-top: var(--space-1);
}
.signature-quality-trend__heading button {
background: transparent;
border: 0;
color: var(--color-text-muted);
cursor: pointer;
padding: var(--space-2);
}
.signature-carrier-grid {
display: grid;
gap: var(--space-3);
-24
View File
@@ -31,30 +31,6 @@ export function createLineOption(params: {
};
}
export function createQualityTrendOption(params: {
labels: string[];
sent: number[];
delivered: number[];
rates: number[];
}): EChartsOption {
return {
color: [...chartPalette],
grid: { left: 12, right: 18, top: 42, bottom: 8, containLabel: true },
legend: { top: 0, right: 0, textStyle: { color: themeColors.textMuted } },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: params.labels, ...axisStyle },
yAxis: [
{ type: 'value', name: '业务条数', minInterval: 1, ...axisStyle },
{ type: 'value', name: '到达率', min: 0, max: 100, ...axisStyle, axisLabel: { formatter: '{value}%', color: themeColors.textMuted } },
],
series: [
{ name: '发送业务条数', data: params.sent, type: 'line', smooth: true, symbolSize: 6, lineStyle: { width: 3 } },
{ name: '到达业务条数', data: params.delivered, type: 'line', smooth: true, symbolSize: 6, lineStyle: { width: 3 } },
{ name: '到达率', data: params.rates, type: 'line', yAxisIndex: 1, smooth: true, symbolSize: 6, lineStyle: { width: 3 } },
],
};
}
export function createBarOption(params: {
labels: string[];
series: Array<{ name: string; data: number[] }>;