feat: improve operations diagnostics and channel management
This commit is contained in:
@@ -4,14 +4,20 @@ import {
|
||||
adminApi,
|
||||
type SendQualityResponse,
|
||||
type SignatureChannelCarrierQualityStat,
|
||||
type SignatureChannelCarrierDrainageQualityStat,
|
||||
type SignatureChannelQualityItem,
|
||||
type SignatureChannelQualityResponse,
|
||||
} from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Chart, Input, Pagination, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { createBarOption, createPieOption } from '@/theme/chartOptions';
|
||||
import { successRateClassName } from '@/utils/successRate';
|
||||
|
||||
const carrierOrder = ['mobile', 'unicom', 'telecom', 'unknown'];
|
||||
const majorCarrierOrder = ['mobile', 'unicom', 'telecom'] as const;
|
||||
const drainageStates = [
|
||||
{ value: 'with', label: '含引流' },
|
||||
{ value: 'without', label: '不含引流' },
|
||||
{ value: 'unknown', label: '未检测' },
|
||||
] as const;
|
||||
const carrierLabels: Record<string, string> = {
|
||||
mobile: '移动',
|
||||
unicom: '联通',
|
||||
@@ -308,7 +314,8 @@ function SignatureQualityDrawer({
|
||||
return (leftRank < 0 ? carrierOrder.length : leftRank)
|
||||
- (rightRank < 0 ? carrierOrder.length : rightRank);
|
||||
});
|
||||
const channels = [...new Map(item.breakdowns.map((entry) => [entry.channelId, entry.channelName])).entries()]
|
||||
const channels = [...new Map([...item.breakdowns, ...item.drainageBreakdowns]
|
||||
.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));
|
||||
|
||||
@@ -330,7 +337,7 @@ function SignatureQualityDrawer({
|
||||
<div className="signature-quality-overview">
|
||||
<QualityMetric label="业务短信" value={item.total.toLocaleString('zh-CN')} />
|
||||
<QualityMetric label="通道提交" value={item.channelSubmitTotal.toLocaleString('zh-CN')} />
|
||||
<QualityMetric label="最终成功率" tone={rateTone(item.successRate)} value={`${item.successRate.toFixed(1)}%`} />
|
||||
<QualityMetric label="最终成功率" value={`${item.successRate.toFixed(1)}%`} valueClassName={successRateClassName(item.successRate)} />
|
||||
<QualityMetric label="平均到达时间" value={formatDuration(item.averageArrivalMs)} />
|
||||
</div>
|
||||
|
||||
@@ -349,7 +356,7 @@ function SignatureQualityDrawer({
|
||||
<strong>{carrier.businessMessageCount.toLocaleString('zh-CN')} 条业务短信</strong>
|
||||
</div>
|
||||
<dl>
|
||||
<div><dt>最终成功率</dt><dd>{carrier.finalSuccessRate.toFixed(1)}%</dd></div>
|
||||
<div><dt>最终成功率</dt><dd className={successRateClassName(carrier.finalSuccessRate)}>{carrier.finalSuccessRate.toFixed(1)}%</dd></div>
|
||||
<div><dt>平均到达</dt><dd>{formatDuration(carrier.averageArrivalMs)}</dd></div>
|
||||
<div><dt>涉及通道</dt><dd>{carrier.channelCount} 个</dd></div>
|
||||
</dl>
|
||||
@@ -362,39 +369,59 @@ function SignatureQualityDrawer({
|
||||
<div className="signature-quality-section__heading">
|
||||
<div>
|
||||
<h3>通道 × 运营商矩阵</h3>
|
||||
<p>{matrixMode === 'overall' ? '整体口径展示该组合全部真实提交。' : '引流切分口径分别展示含引流、不含引流和历史未检测数据。'}“—”表示所选日期没有真实提交。</p>
|
||||
<p>{matrixMode === 'overall'
|
||||
? '整体口径展示该组合全部真实提交;“—”表示所选日期没有真实提交。'
|
||||
: '固定按含引流、不含引流、未检测三行及移动、联通、电信三列展示;没有真实提交的组合显示 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>
|
||||
</div>
|
||||
<div className="signature-quality-matrix">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>通道名称</th>
|
||||
{visibleCarriers.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
|
||||
</tr>
|
||||
{matrixMode === 'overall' ? (
|
||||
<tr>
|
||||
<th>通道名称</th>
|
||||
{visibleCarriers.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th>通道名称</th>
|
||||
<th>引流类型</th>
|
||||
{majorCarrierOrder.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{channels.map((channel) => (
|
||||
<tr key={channel.channelId}>
|
||||
<th>{channel.channelName}</th>
|
||||
{visibleCarriers.map((carrier) => {
|
||||
const metric = item.breakdowns.find((entry) => (
|
||||
entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier
|
||||
));
|
||||
const drainageMetrics = item.drainageBreakdowns.filter((entry) => (
|
||||
entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier
|
||||
));
|
||||
return (
|
||||
<td key={carrier}>
|
||||
{matrixMode === 'overall'
|
||||
? metric ? <MatrixMetric metric={metric} /> : <span className="signature-quality-matrix__empty">—</span>
|
||||
: drainageMetrics.length ? <DrainageMatrixMetrics metrics={drainageMetrics} /> : <span className="signature-quality-matrix__empty">—</span>}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
{matrixMode === 'overall'
|
||||
? channels.map((channel) => (
|
||||
<tr key={channel.channelId}>
|
||||
<th>{channel.channelName}</th>
|
||||
{visibleCarriers.map((carrier) => {
|
||||
const metric = item.breakdowns.find((entry) => (
|
||||
entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier
|
||||
));
|
||||
return (
|
||||
<td key={carrier}>
|
||||
{metric ? <MatrixMetric metric={metric} /> : <span className="signature-quality-matrix__empty">—</span>}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))
|
||||
: channels.flatMap((channel) => drainageStates.map((state, stateIndex) => (
|
||||
<tr key={`${channel.channelId}-${state.value}`}>
|
||||
{stateIndex === 0 ? <th rowSpan={drainageStates.length}>{channel.channelName}</th> : null}
|
||||
<th className="signature-quality-matrix__drainage-label">{state.label}</th>
|
||||
{majorCarrierOrder.map((carrier) => {
|
||||
const metric = item.drainageBreakdowns.find((entry) => (
|
||||
entry.channelId === channel.channelId
|
||||
&& normalizeCarrier(entry.carrier) === carrier
|
||||
&& entry.drainageState === state.value
|
||||
));
|
||||
return <td key={carrier}><MatrixMetric metric={metric} zeroWhenEmpty /></td>;
|
||||
})}
|
||||
</tr>
|
||||
)))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -409,41 +436,37 @@ function SignatureQualityDrawer({
|
||||
);
|
||||
}
|
||||
|
||||
function QualityMetric({ label, value, tone = 'default' }: { label: string; value: string; tone?: string }) {
|
||||
function QualityMetric({ label, value, valueClassName }: { label: string; value: string; valueClassName?: string }) {
|
||||
return (
|
||||
<div className={`signature-quality-metric signature-quality-metric--${tone}`}>
|
||||
<div className="signature-quality-metric">
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
<strong className={valueClassName}>{value}</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MatrixMetric({ metric }: { metric: SignatureChannelCarrierQualityStat }) {
|
||||
function MatrixMetric({ metric, zeroWhenEmpty = false }: { metric?: SignatureChannelCarrierQualityStat; zeroWhenEmpty?: boolean }) {
|
||||
const total = metric?.total ?? 0;
|
||||
const successRate = metric?.successRate ?? 0;
|
||||
if (zeroWhenEmpty && total === 0) return <span className="signature-quality-matrix__zero">0</span>;
|
||||
|
||||
return (
|
||||
<div className="signature-quality-matrix__metric">
|
||||
<strong>{metric.total.toLocaleString('zh-CN')} 次</strong>
|
||||
<span className={`signature-quality-matrix__rate signature-quality-matrix__rate--${rateTone(metric.successRate)}`}>
|
||||
{metric.successRate.toFixed(1)}%
|
||||
<strong>{total.toLocaleString('zh-CN')} 次</strong>
|
||||
<span className={`signature-quality-matrix__rate ${successRateClassName(successRate)}`}>
|
||||
{successRate.toFixed(1)}%
|
||||
</span>
|
||||
<small>{formatDuration(metric.averageArrivalMs)}</small>
|
||||
{metric.submitFailureCount > 0 ? <em>提交失败 {metric.submitFailureCount}</em> : null}
|
||||
<small>{formatDuration(metric?.averageArrivalMs)}</small>
|
||||
{(metric?.submitFailureCount ?? 0) > 0 ? <em>提交失败 {metric?.submitFailureCount}</em> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DrainageMatrixMetrics({ metrics }: { metrics: SignatureChannelCarrierDrainageQualityStat[] }) {
|
||||
const labels = { with: '含引流', without: '不含引流', unknown: '未检测' };
|
||||
return <div className="signature-quality-matrix__drainage">{(['with', 'without', 'unknown'] as const).map((state) => {
|
||||
const metric = metrics.find((item) => item.drainageState === state);
|
||||
return metric ? <div key={state}><b>{labels[state]}</b><MatrixMetric metric={metric} /></div> : null;
|
||||
})}</div>;
|
||||
}
|
||||
|
||||
function QualityRate({ value }: { value: number }) {
|
||||
return (
|
||||
<div className="signature-quality-rate">
|
||||
<div><span style={{ width: `${Math.min(100, Math.max(0, value))}%` }} /></div>
|
||||
<strong className={`signature-quality-rate--${rateTone(value)}`}>{value.toFixed(1)}%</strong>
|
||||
<strong className={successRateClassName(value)}>{value.toFixed(1)}%</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -468,12 +491,6 @@ function carrierTagTone(value: string): 'info' | 'accent' | 'warning' | 'neutral
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function rateTone(value: number) {
|
||||
if (value >= 98) return 'success';
|
||||
if (value >= 95) return 'warning';
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
function formatDuration(value?: number | null) {
|
||||
if (value == null) return '—';
|
||||
if (value < 1000) return `${Math.round(value)} 毫秒`;
|
||||
|
||||
Reference in New Issue
Block a user