fix: 修复上行归属并实现签名质量日报优化

This commit is contained in:
hectorzhao
2026-09-17 11:12:58 +08:00
parent 4eb7b16d12
commit 572290308c
40 changed files with 2854 additions and 778 deletions
+4 -2
View File
@@ -43,8 +43,10 @@ export const adminOperationsApi = {
request<PendingAuditCounts>(withQuery('/admin/operations/pending-audits', { tenantId }), { signal }),
getSendQuality: (date?: string) =>
request<SendQualityResponse>(withQuery('/admin/operations/send-quality', { date })),
getSignatureQuality: (query: { date?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
request<SignatureChannelQualityResponse>(withQuery('/admin/operations/signature-quality', query)),
getSignatureQuality: (
query: { date?: string; keyword?: string; page?: number; pageSize?: number } = {},
signal?: AbortSignal,
) => request<SignatureChannelQualityResponse>(withQuery('/admin/operations/signature-quality', query), { signal }),
listSystemLogs: (query: {
tenantId?: string;
keyword?: string;
+21 -2
View File
@@ -1,6 +1,8 @@
import { request, withQuery } from '../core/httpClient';
import type {
PagedResult,
SignatureActivityResponse,
SignatureAnalyticsMetadata,
SignatureRetirementHeatmapItem,
SignatureRetirementHeatmapDimension,
SignatureRetirementMessage,
@@ -12,6 +14,19 @@ import type {
} from '../types';
export const adminSignatureRetirementApi = {
getSignatureActivity: (
query: {
date: string;
dimensionType: 'enterprise' | 'channel';
page: number;
pageSize: number;
tenantName?: string;
applicationName?: string;
signatureName?: string;
channelName?: string;
},
signal?: AbortSignal,
) => request<SignatureActivityResponse>(withQuery('/admin/signature-retirement/activity', query), { signal }),
getSignatureRetirementConfiguration: () =>
request<{ rules: SignatureRetirementRule[]; webhooks: SignatureRetirementWebhook[] }>(
'/admin/signature-retirement/configuration',
@@ -79,8 +94,12 @@ export const adminSignatureRetirementApi = {
dimensions: SignatureRetirementHeatmapDimension[];
items: SignatureRetirementHeatmapItem[];
}>(withQuery('/admin/signature-retirement/heatmap', { date })),
getUnreportedSignatures: (query: { date?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResult<UnreportedSignatureItem> & { date: string }>(
getUnreportedSignatures: (
query: { date?: string; keyword?: string; page?: number; pageSize?: number } = {},
signal?: AbortSignal,
) =>
request<PagedResult<UnreportedSignatureItem> & { date: string } & SignatureAnalyticsMetadata>(
withQuery('/admin/signature-retirement/unreported-signatures', query),
{ signal },
),
};
+2 -1
View File
@@ -1,3 +1,4 @@
import type { SignatureAnalyticsMetadata } from './signature-retirement';
// R1 compatibility types. Keep public names re-exported from src/api/adminApi.ts.
import type { AdminChannel } from './channels-reports';
@@ -85,7 +86,7 @@ export type SignatureChannelQualityItem = {
drainageBreakdowns: SignatureChannelCarrierDrainageQualityStat[];
};
export type SignatureChannelQualityResponse = {
export type SignatureChannelQualityResponse = SignatureAnalyticsMetadata & {
date: string;
items: SignatureChannelQualityItem[];
total: number;
+39
View File
@@ -105,3 +105,42 @@ export type UnreportedSignatureItem = {
applicationName?: string | null;
messageCount: number;
};
export type SignatureAnalyticsMetadata = {
dataSource?: 'live' | 'report';
reportState?: string;
frozen?: boolean;
provenance?: string;
sourceAsOf?: string | null;
generatedAt?: string | null;
generationId?: string | null;
};
export type SignatureActivityItem = Pick<
SignatureRetirementHeatmapItem,
| 'id'
| 'dimensionType'
| 'signatureId'
| 'channelId'
| 'carrier'
| 'activityDate'
| 'submittedAttempts'
| 'acceptedBusinessCount'
| 'deliveredBusinessCount'
| 'status'
>;
export type SignatureActivityResponse = {
date: string;
items: SignatureActivityItem[];
dimensions: (SignatureRetirementHeatmapDimension & { total: number })[];
page: number;
pageSize: number;
total: number;
complete: boolean;
coverage: {
date: string;
generationId: string | null;
reportState: string;
frozen: boolean;
sourceAsOf: string | null;
}[];
};
+48 -42
View File
@@ -3,49 +3,33 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AdminAnalyticsPage } from './AdminAnalyticsPage';
const { api } = vi.hoisted(() => ({
api: { getSignatureQuality: vi.fn(), getSignatureRetirementHeatmap: vi.fn(), getUnreportedSignatures: vi.fn() },
api: { getSignatureQuality: vi.fn(), getSignatureActivity: vi.fn(), getUnreportedSignatures: vi.fn() },
}));
vi.mock('@/api/adminApi', () => ({ adminApi: api }));
describe('independent analytics tabs', () => {
it('combines separate activity search fields with AND and preserves them across tabs', async () => {
api.getSignatureRetirementHeatmap.mockResolvedValue({
items: [],
dimensions: [
{
dimensionType: 'channel',
signatureId: 'a',
signatureName: '签名甲',
tenantName: '企业甲',
applicationName: '应用甲',
channelName: '通道甲',
channelId: 'c',
carrier: 'mobile',
approvedAt: '2026-08-01',
},
{
dimensionType: 'channel',
signatureId: 'b',
signatureName: '签名乙',
tenantName: '企业甲',
applicationName: '应用乙',
channelName: '通道乙',
channelId: 'd',
carrier: 'unicom',
approvedAt: '2026-08-01',
},
],
});
it('submits independent activity fields to server and preserves drafts across tabs', async () => {
render(<AdminAnalyticsPage />);
fireEvent.click(screen.getByRole('tab', { name: '通道签名活跃度' }));
const panel = screen.getByRole('region', { name: '通道签名活跃度' });
await within(panel).findByText('签名甲');
await waitFor(() => expect(api.getSignatureActivity).toHaveBeenCalledTimes(1));
fireEvent.change(within(panel).getByLabelText('企业'), { target: { value: '企业甲' } });
fireEvent.change(within(panel).getByLabelText('企业应用'), { target: { value: '应用甲' } });
await waitFor(() => expect(within(panel).queryByText('签名乙')).not.toBeInTheDocument());
fireEvent.change(within(panel).getByLabelText('通道'), { target: { value: '通道乙' } });
await waitFor(() => expect(within(panel).queryByText('签名甲')).not.toBeInTheDocument());
expect(api.getSignatureActivity).toHaveBeenCalledTimes(1);
fireEvent.click(within(panel).getByRole('button', { name: '查询统计' }));
await waitFor(() =>
expect(api.getSignatureActivity).toHaveBeenLastCalledWith(
expect.objectContaining({ dimensionType: 'channel', tenantName: '企业甲', applicationName: '应用甲', page: 1 }),
expect.any(AbortSignal),
),
);
fireEvent.click(screen.getByRole('tab', { name: '企业签名活跃度' }));
await waitFor(() =>
expect(api.getSignatureActivity).toHaveBeenLastCalledWith(
expect.objectContaining({ dimensionType: 'enterprise', tenantName: '', applicationName: '' }),
expect.any(AbortSignal),
),
);
fireEvent.click(screen.getByRole('tab', { name: '通道签名活跃度' }));
expect(within(panel).getByLabelText('企业应用')).toHaveValue('应用甲');
});
@@ -53,19 +37,33 @@ describe('independent analytics tabs', () => {
vi.resetAllMocks();
api.getSignatureQuality.mockImplementation(async (query) => ({ ...query, total: 0, items: [] }));
api.getUnreportedSignatures.mockImplementation(async (query) => ({ ...query, total: 0, items: [] }));
api.getSignatureRetirementHeatmap.mockResolvedValue({ items: [], dimensions: [] });
api.getSignatureActivity.mockResolvedValue({
items: [],
dimensions: [],
total: 0,
page: 1,
pageSize: 25,
coverage: [],
complete: false,
});
});
it('loads only the visited tab and preserves independent dates when switching back', async () => {
render(<AdminAnalyticsPage />);
await waitFor(() =>
expect(api.getSignatureQuality).toHaveBeenCalledWith(expect.objectContaining({ pageSize: 25 })),
expect(api.getSignatureQuality).toHaveBeenCalledWith(
expect.objectContaining({ pageSize: 25 }),
expect.any(AbortSignal),
),
);
expect(api.getSignatureRetirementHeatmap).not.toHaveBeenCalled();
expect(api.getSignatureActivity).not.toHaveBeenCalled();
const quality = screen.getByRole('region', { name: '签名通道发送质量' });
fireEvent.change(within(quality).getByLabelText('统计日期'), { target: { value: '2026-08-20' } });
fireEvent.click(within(quality).getByRole('button', { name: '查询统计' }));
await waitFor(() =>
expect(api.getSignatureQuality).toHaveBeenLastCalledWith(expect.objectContaining({ date: '2026-08-20' })),
expect(api.getSignatureQuality).toHaveBeenLastCalledWith(
expect.objectContaining({ date: '2026-08-20' }),
expect.any(AbortSignal),
),
);
fireEvent.click(screen.getByRole('tab', { name: '未报备签名' }));
await waitFor(() => expect(api.getUnreportedSignatures).toHaveBeenCalledTimes(1));
@@ -75,7 +73,7 @@ describe('independent analytics tabs', () => {
expect(within(quality).getByLabelText('统计日期')).toHaveValue('2026-08-20');
fireEvent.click(screen.getByRole('tab', { name: '未报备签名' }));
expect(within(unreported).getByLabelText('统计日期')).toHaveValue('2026-08-25');
expect(api.getSignatureRetirementHeatmap).not.toHaveBeenCalled();
expect(api.getSignatureActivity).not.toHaveBeenCalled();
});
it.each([
@@ -89,9 +87,16 @@ describe('independent analytics tabs', () => {
await waitFor(() => expect(within(panel).getByRole('button', { name: '下一页' })).toBeEnabled());
fireEvent.change(within(panel).getByLabelText('统计日期'), { target: { value: '2026-08-20' } });
fireEvent.click(within(panel).getByRole('button', { name: '查询统计' }));
await waitFor(() => expect(api[method]).toHaveBeenLastCalledWith(expect.objectContaining({ date: '2026-08-20' })));
await waitFor(() =>
expect(api[method]).toHaveBeenLastCalledWith(
expect.objectContaining({ date: '2026-08-20' }),
expect.any(AbortSignal),
),
);
fireEvent.click(within(panel).getByRole('button', { name: '下一页' }));
await waitFor(() => expect(api[method]).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2 })));
await waitFor(() =>
expect(api[method]).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2 }), expect.any(AbortSignal)),
);
// An unsubmitted date must not alter a pagination request.
fireEvent.change(within(panel).getByLabelText('统计日期'), { target: { value: '2026-08-21' } });
const size = within(panel).getByLabelText(/^每页数量/);
@@ -108,6 +113,7 @@ describe('independent analytics tabs', () => {
await waitFor(() =>
expect(api[method]).toHaveBeenLastCalledWith(
expect.objectContaining({ page: 1, pageSize: 100, date: '2026-08-20' }),
expect.any(AbortSignal),
),
);
await waitFor(() => expect(within(panel).getByLabelText('跳转页码')).toHaveValue(1));
@@ -118,7 +124,7 @@ describe('independent analytics tabs', () => {
async (tab) => {
render(<AdminAnalyticsPage />);
fireEvent.click(screen.getByRole('tab', { name: tab }));
await waitFor(() => expect(api.getSignatureRetirementHeatmap).toHaveBeenCalledTimes(1));
await waitFor(() => expect(api.getSignatureActivity).toHaveBeenCalledTimes(1));
const panel = screen.getByRole('region', { name: tab });
const size = within(panel).getByLabelText(/^每页数量/);
expect(size.closest('.ui-pagination')).not.toBeNull();
@@ -131,7 +137,7 @@ describe('independent analytics tabs', () => {
).toHaveTextContent('25 条/页');
fireEvent.click(screen.getByRole('tab', { name: tab }));
expect(size).toHaveTextContent('50 条/页');
expect(api.getSignatureRetirementHeatmap).toHaveBeenCalledTimes(1);
await waitFor(() => expect(api.getSignatureActivity).toHaveBeenCalledTimes(2));
},
);
});
+114 -63
View File
@@ -1,11 +1,13 @@
import { useDeferredValue, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { BarChart3, Eye, Search, X } from 'lucide-react';
import {
adminApi,
type SignatureChannelCarrierQualityStat,
type SignatureChannelQualityItem,
type SignatureActivityResponse,
type SignatureAnalyticsMetadata,
type SignatureChannelQualityResponse,
type SignatureRetirementHeatmapItem,
type SignatureActivityItem,
type SignatureRetirementHeatmapDimension,
type UnreportedSignatureItem,
type PagedResult,
@@ -59,9 +61,19 @@ function AnalyticsPanel({ kind }: { kind: string }) {
const [pageSize, setPageSize] = useState(25);
const [appliedDate, setAppliedDate] = useState(() => shanghaiDateKey());
const requestId = useRef(0);
const abort = useRef<AbortController | null>(null);
const [activity, setActivity] = useState<SignatureActivityResponse | null>(null);
const [activityFilters, setActivityFilters] = useState({
tenantName: '',
applicationName: '',
signatureName: '',
channelName: '',
});
const [appliedActivityFilters, setAppliedActivityFilters] = useState(activityFilters);
const [metadata, setMetadata] = useState<SignatureAnalyticsMetadata | null>(null);
const [statisticsDate, setStatisticsDate] = useState(() => shanghaiDateKey());
const [signatureQuality, setSignatureQuality] = useState<SignatureChannelQualityResponse | null>(null);
const [retirementHeatmap, setRetirementHeatmap] = useState<SignatureRetirementHeatmapItem[]>([]);
const [retirementHeatmap, setRetirementHeatmap] = useState<SignatureActivityItem[]>([]);
const [retirementDimensions, setRetirementDimensions] = useState<SignatureRetirementHeatmapDimension[]>([]);
const [unreportedSignatures, setUnreportedSignatures] = useState<
(PagedResult<UnreportedSignatureItem> & { date: string }) | null
@@ -80,30 +92,47 @@ function AnalyticsPanel({ kind }: { kind: string }) {
unreported = appliedUnreportedKeyword,
date = statisticsDate,
size = pageSize,
filters = appliedActivityFilters,
) {
abort.current?.abort();
const controller = new AbortController();
abort.current = controller;
const id = ++requestId.current;
setLoading(true);
setError('');
try {
if (kind === 'quality') {
const data = await adminApi.getSignatureQuality({ date, keyword: keyword || undefined, page, pageSize: size });
const data = await adminApi.getSignatureQuality(
{ date, keyword: keyword || undefined, page, pageSize: size },
controller.signal,
);
if (id !== requestId.current) return;
setSignatureQuality(data);
setMetadata(data);
setAppliedKeyword(keyword);
setSelectedSignature(null);
} else if (kind === 'unreported') {
const data = await adminApi.getUnreportedSignatures({
date,
keyword: unreported || undefined,
page,
pageSize: size,
});
const data = await adminApi.getUnreportedSignatures(
{
date,
keyword: unreported || undefined,
page,
pageSize: size,
},
controller.signal,
);
if (id !== requestId.current) return;
setUnreportedSignatures(data);
setMetadata(data);
setAppliedUnreportedKeyword(unreported);
} else {
const data = await adminApi.getSignatureRetirementHeatmap(date);
const data = await adminApi.getSignatureActivity(
{ date, dimensionType: kind as 'enterprise' | 'channel', page, pageSize: size, ...filters },
controller.signal,
);
if (id !== requestId.current) return;
setActivity(data);
setAppliedActivityFilters(filters);
setRetirementHeatmap(data.items);
setRetirementDimensions(data.dimensions);
}
@@ -115,9 +144,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
}
}
const initialLoad = useRef(loadData);
useEffect(() => {
void loadData(1, '');
void initialLoad.current(1, '');
return () => {
abort.current?.abort();
requestId.current += 1;
};
}, []);
@@ -221,7 +252,7 @@ function AnalyticsPanel({ kind }: { kind: string }) {
];
function queryStatistics() {
void loadData(1, signatureKeyword.trim());
void loadData(1, signatureKeyword.trim(), unreportedKeyword.trim(), statisticsDate, pageSize, activityFilters);
}
function changeSignaturePage(page: number) {
@@ -233,8 +264,7 @@ function AnalyticsPanel({ kind }: { kind: string }) {
function changePageSize(size: number) {
setPageSize(size);
if (kind === 'quality' || kind === 'unreported')
void loadData(1, appliedKeyword, appliedUnreportedKeyword, appliedDate, size);
void loadData(1, appliedKeyword, appliedUnreportedKeyword, appliedDate, size);
}
return (
@@ -256,7 +286,28 @@ function AnalyticsPanel({ kind }: { kind: string }) {
</Button>
</div>
</div>
{error ? <p className="form-error">{error}</p> : null}
{error ? (
<p className="form-error">
{error}
{signatureQuality || activity || unreportedSignatures
? `;仍显示上次成功查询的数据,日期为 ${appliedDate}`
: ';尚无成功查询的数据。'}
</p>
) : null}
{metadata ? (
<p className="muted" role="status">
{metadata.dataSource === 'live' ? '当天实时查询' : metadata.frozen ? '已冻结日报' : '近三日可刷新日报'} ·{' '}
{metadata.reportState === 'missing'
? '报表尚未生成'
: metadata.reportState === 'failed'
? '生成失败,保留上次完整日报'
: metadata.reportState === 'refreshing'
? '正在刷新,显示上次完整日报'
: '查询完成'}
{metadata.provenance === 'backfill-current-source' ? ' · 事后补建' : ''}
{metadata.sourceAsOf ? ` · 数据截止 ${new Date(metadata.sourceAsOf).toLocaleString('zh-CN')}` : ''}
</p>
) : null}
{kind === 'quality' ? (
<div className="surface signature-quality-card">
@@ -323,6 +374,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
onPageSizeChange={changePageSize}
date={appliedDate}
dimensionType="enterprise"
activity={activity}
filters={activityFilters}
onFilterChange={setActivityFilters}
onSearch={queryStatistics}
onPageChange={(page) => void loadData(page, appliedKeyword, appliedUnreportedKeyword, appliedDate)}
dimensions={retirementDimensions}
items={retirementHeatmap}
title="企业签名活跃度热力图"
@@ -334,6 +390,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
onPageSizeChange={changePageSize}
date={appliedDate}
dimensionType="channel"
activity={activity}
filters={activityFilters}
onFilterChange={setActivityFilters}
onSearch={queryStatistics}
onPageChange={(page) => void loadData(page, appliedKeyword, appliedUnreportedKeyword, appliedDate)}
dimensions={retirementDimensions}
items={retirementHeatmap}
title="通道签名活跃度热力图"
@@ -349,7 +410,7 @@ function AnalyticsPanel({ kind }: { kind: string }) {
loading={loading}
onKeywordChange={setUnreportedKeyword}
onPageChange={(page) => loadUnreportedSignatures(page)}
onSearch={() => loadUnreportedSignatures(1, unreportedKeyword.trim())}
onSearch={queryStatistics}
/>
) : null}
@@ -365,6 +426,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
}
function RetirementHeatmap({
activity,
filters,
onFilterChange,
onSearch,
onPageChange,
pageSize,
onPageSizeChange,
date,
@@ -378,12 +444,14 @@ function RetirementHeatmap({
date: string;
dimensionType: 'enterprise' | 'channel';
dimensions: SignatureRetirementHeatmapDimension[];
items: SignatureRetirementHeatmapItem[];
items: SignatureActivityItem[];
title: string;
activity: SignatureActivityResponse | null;
filters: { tenantName: string; applicationName: string; signatureName: string; channelName: string };
onFilterChange: (value: typeof filters) => void;
onSearch: () => void;
onPageChange: (page: number) => void;
}) {
const [pageState, setPageState] = useState({ key: '', page: 1 });
const [filters, setFilters] = useState({ tenantName: '', applicationName: '', signatureName: '', channelName: '' });
const deferredFilters = useDeferredValue(filters);
const visible = items.filter((item) => item.dimensionType === dimensionType);
const dates = previousDateKeys(date, 30);
const cellMap = new Map(
@@ -392,40 +460,17 @@ function RetirementHeatmap({
item,
]),
);
const rows = dimensions
.filter((item) => item.dimensionType === dimensionType)
.filter((item) =>
Object.entries(deferredFilters).every(
([key, value]) =>
!value.trim() ||
(item[key as keyof typeof deferredFilters] ?? '')
.toLocaleLowerCase('zh-CN')
.includes(value.trim().toLocaleLowerCase('zh-CN')),
),
)
.map((item) => ({
key: `${item.signatureId}:${item.channelId ?? ''}:${item.carrier}`,
signatureName: item.signatureName,
channelName: item.channelName,
tenantName: item.tenantName,
applicationName: item.applicationName,
carrier: item.carrier,
approvedAt: item.approvedAt.slice(0, 10),
total: dates.reduce(
(sum, dateKey) =>
sum +
(cellMap.get(`${item.signatureId}:${item.channelId ?? ''}:${item.carrier}:${dateKey}`)
?.acceptedBusinessCount ?? 0),
0,
),
}))
.sort((left, right) => right.total - left.total || left.signatureName.localeCompare(right.signatureName, 'zh-CN'));
const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
const paginationKey = JSON.stringify([date, deferredFilters, dimensionType, dimensions.length, pageSize]);
const page = pageState.key === paginationKey ? pageState.page : 1;
const setPage = (value: number) => setPageState({ key: paginationKey, page: value });
const currentPage = Math.min(page, totalPages);
const pagedRows = rows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const rows = dimensions.map((item) => ({
...item,
key: `${item.signatureId}:${item.channelId ?? ''}:${item.carrier}`,
approvedAt: item.approvedAt?.slice(0, 10) ?? '',
total: (item as SignatureRetirementHeatmapDimension & { total: number }).total ?? 0,
}));
const totalPages = Math.max(1, Math.ceil((activity?.total ?? 0) / pageSize));
const currentPage = activity?.page ?? 1;
const pagedRows = rows;
const setPage = onPageChange;
const coverage = new Map(activity?.coverage.map((day) => [day.date, day]));
return (
<div className="surface signature-retirement-heatmap">
@@ -448,12 +493,18 @@ function RetirementHeatmap({
label={label}
placeholder={`搜索${label}`}
value={filters[key]}
onChange={(event) => setFilters((current) => ({ ...current, [key]: event.target.value }))}
onChange={(event) => onFilterChange({ ...filters, [key]: event.target.value })}
/>
))}
<Tag tone="info">T-1 T-30</Tag>
<Button onClick={onSearch} icon={<Search size={16} />}>
</Button>
<Tag tone="info">30</Tag>
</div>
</div>
{activity && !activity.complete ? (
<p className="form-error">30</p>
) : null}
{rows.length ? (
<>
<div className="signature-retirement-heatmap__scroll">
@@ -482,7 +533,7 @@ function RetirementHeatmap({
<td className="signature-retirement-heatmap__total">{row.total.toLocaleString('zh-CN')}</td>
{dates.map((dateKey) => {
const item = cellMap.get(`${row.key}:${dateKey}`);
const beforeApproval = dateKey < row.approvedAt;
const beforeApproval = Boolean(coverage.get(dateKey)?.generationId && !item);
const successRate = item?.acceptedBusinessCount
? (item.deliveredBusinessCount / item.acceptedBusinessCount) * 100
: 0;
@@ -494,10 +545,10 @@ function RetirementHeatmap({
? 'is-zero'
: `is-rate-${successRateTone(successRate)}`;
const titleText = beforeApproval
? '报备前,不适用'
? '当日报备维度不适用'
: item
? `提交条数:${item.submittedAttempts}\n上游接受条数:${item.acceptedBusinessCount}\n发送成功条数:${item.deliveredBusinessCount}\n发送成功率:${successRate.toFixed(1)}%\n检测状态${item.status === 'observing' ? '观察中(不预警)' : item.status === 'alert' ? '预警' : '正常'}\n预警阈值:${item.threshold}`
: '当日无检测快照';
? `提交条数:${item.submittedAttempts}\n上游接受条数:${item.acceptedBusinessCount}\n发送成功条数:${item.deliveredBusinessCount}\n发送成功率:${successRate.toFixed(1)}%\n数据来源${coverage.get(dateKey)?.frozen ? '冻结日报' : '可刷新日报'}`
: '当日报表尚未生成';
return (
<td className={className} key={dateKey} title={titleText}>
{beforeApproval ? 'N/A' : item ? item.acceptedBusinessCount : '—'}
@@ -512,7 +563,7 @@ function RetirementHeatmap({
</>
) : (
<p className="empty-state">
{Object.values(deferredFilters).some((value) => value.trim())
{Object.values(filters).some((value) => value.trim())
? '没有匹配企业、企业应用或签名的热力图维度。'
: '暂无已确认到运营商的报备事实,尚未形成检测热力图。'}
</p>
@@ -526,7 +577,7 @@ function RetirementHeatmap({
onPrevious={() => setPage(currentPage - 1)}
page={currentPage}
previousDisabled={currentPage <= 1}
total={rows.length}
total={activity?.total ?? 0}
totalPages={totalPages}
/>
</div>