feat(platform): add online statistics summary

This commit is contained in:
lingniu
2026-07-04 16:57:11 +08:00
parent d7f1a9279f
commit 9ac2b90d12
8 changed files with 146 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ import type {
DashboardSummary,
HistoryLocationRow,
MileageSummary,
OnlineStatisticsSummary,
OpsHealth,
QualityNotificationPlan,
Page,
@@ -105,6 +106,7 @@ export const api = {
}),
mileageSummary: (params = new URLSearchParams()) => request<MileageSummary>(`/api/mileage/summary?${params.toString()}`),
dailyMileage: (params = new URLSearchParams()) => request<Page<DailyMileageRow>>(`/api/mileage/daily?${params.toString()}`),
onlineStatisticsSummary: (params = new URLSearchParams()) => request<OnlineStatisticsSummary>(`/api/statistics/online-summary?${params.toString()}`),
qualitySummary: (params = new URLSearchParams()) => request<QualitySummary>(`/api/quality/summary?${params.toString()}`),
qualityIssues: (params = new URLSearchParams()) => request<Page<QualityIssueRow>>(`/api/quality/issues?${params.toString()}`),
qualityNotificationPlan: (params = new URLSearchParams()) => request<QualityNotificationPlan>(`/api/quality/notification-plan?${params.toString()}`),

View File

@@ -255,6 +255,16 @@ export interface MileageSummary {
averageMileagePerVin: number;
}
export interface OnlineStatisticsSummary {
vehicleCount: number;
onlineVehicleCount: number;
offlineVehicleCount: number;
onlineRatePercent: number;
redisOnlineKeys?: number | null;
protocolStats: ProtocolStat[];
evidence: string;
}
export interface QualityIssueRow {
vin: string;
plate: string;

View File

@@ -1,7 +1,7 @@
import { Button, Card, Form, Select, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { DailyMileageRow, MileageSummary } from '../api/types';
import type { DailyMileageRow, MileageSummary, OnlineStatisticsSummary } from '../api/types';
import { PageHeader } from '../components/PageHeader';
import { buildAppHash } from '../domain/appRoute';
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
@@ -14,6 +14,16 @@ const emptySummary: MileageSummary = {
averageMileagePerVin: 0
};
const emptyOnlineSummary: OnlineStatisticsSummary = {
vehicleCount: 0,
onlineVehicleCount: 0,
offlineVehicleCount: 0,
onlineRatePercent: 0,
redisOnlineKeys: null,
protocolStats: [],
evidence: ''
};
function mileageParams(values: Record<string, string>, pageSize?: number, offset?: number) {
const params = new URLSearchParams();
if (pageSize != null) params.set('limit', String(pageSize));
@@ -191,6 +201,7 @@ export function Mileage({
}) {
const [rows, setRows] = useState<DailyMileageRow[]>([]);
const [summary, setSummary] = useState<MileageSummary>(emptySummary);
const [onlineSummary, setOnlineSummary] = useState<OnlineStatisticsSummary>(emptyOnlineSummary);
const [loading, setLoading] = useState(true);
const [summaryLoading, setSummaryLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, string>>(mergeInitialFilters(initialVin, initialProtocol, initialFilters));
@@ -212,6 +223,16 @@ export function Mileage({
const confidenceColor = confidenceStatus === '可用于 BI' ? 'green' as const : 'orange' as const;
const currentEvidenceText = `${summary.vehicleCount.toLocaleString()} 车 / ${summary.sourceCount.toLocaleString()} 来源 / ${rows.length.toLocaleString()} 条明细`;
const sourceConsistencyText = summary.sourceCount > 1 ? '可做跨来源核对' : summary.sourceCount === 1 ? '单来源车辆需关注' : '等待来源证据';
const onlineVehicleCount = Number(onlineSummary.onlineVehicleCount ?? 0);
const offlineVehicleCount = Number(onlineSummary.offlineVehicleCount ?? 0);
const onlineRatePercent = Number(onlineSummary.onlineRatePercent ?? 0);
const redisOnlineKeyText = onlineSummary.redisOnlineKeys == null ? '' : `Redis 在线 ${onlineSummary.redisOnlineKeys.toLocaleString()} key`;
const onlineStatsDetail = [
`在线 ${onlineVehicleCount.toLocaleString()}`,
`离线 ${offlineVehicleCount.toLocaleString()}`,
redisOnlineKeyText,
onlineSummary.evidence
].filter(Boolean).join('');
const maxDateMileage = Math.max(...dateSeries.map((item) => item.value), 0);
const maxSourceMileage = Math.max(...sourceSeries.map((item) => item.value), 0);
const filterSummary = [
@@ -227,6 +248,9 @@ export function Mileage({
.then(setSummary)
.catch((error: Error) => Toast.error(error.message))
.finally(() => setSummaryLoading(false));
api.onlineStatisticsSummary(mileageParams(values))
.then(setOnlineSummary)
.catch((error: Error) => Toast.error(error.message));
};
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
@@ -338,9 +362,9 @@ export function Mileage({
},
{
title: '在线率与离线时长',
value: currentVehicleKeyword ? '车辆维度' : '车队维度',
value: formatPercent(onlineRatePercent),
color: 'green' as const,
detail: '后续接入在线区间聚合,按 VIN 统计在线率、离线时间和无更新时长。'
detail: onlineStatsDetail || '按 VIN 统计在线率、离线时间和无更新时长。'
},
{
title: '数据完整性',

View File

@@ -4516,6 +4516,27 @@ test('opens same-day mileage statistics from quality issue row', async () => {
})
} as Response;
}
if (path.includes('/api/statistics/online-summary')) {
return {
ok: true,
json: async () => ({
data: {
vehicleCount: 4,
onlineVehicleCount: 3,
offlineVehicleCount: 1,
onlineRatePercent: 75,
redisOnlineKeys: 92,
protocolStats: [
{ protocol: 'JT808', online: 2, total: 3 },
{ protocol: 'GB32960', online: 1, total: 2 }
],
evidence: '由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算'
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
return {
ok: true,
json: async () => ({
@@ -6541,6 +6562,27 @@ test('shows vehicle-first statistics domains for one vehicle service', async ()
})
} as Response;
}
if (path.includes('/api/statistics/online-summary')) {
return {
ok: true,
json: async () => ({
data: {
vehicleCount: 4,
onlineVehicleCount: 3,
offlineVehicleCount: 1,
onlineRatePercent: 75,
redisOnlineKeys: 92,
protocolStats: [
{ protocol: 'JT808', online: 2, total: 3 },
{ protocol: 'GB32960', online: 1, total: 2 }
],
evidence: '由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算'
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
return {
ok: true,
json: async () => ({
@@ -6561,6 +6603,10 @@ test('shows vehicle-first statistics domains for one vehicle service', async ()
expect(screen.getByText('三类数据源最终汇总为一个车辆服务统计视图')).toBeInTheDocument();
expect(screen.getByText('当前统计证据')).toBeInTheDocument();
expect(screen.getByText('3 车 / 2 来源 / 3 条明细')).toBeInTheDocument();
expect(await screen.findByText('75%')).toBeInTheDocument();
expect(screen.getByText((content) => content.includes('离线 1 车'))).toBeInTheDocument();
expect(screen.getByText((content) => content.includes('Redis 在线 92 key'))).toBeInTheDocument();
expect(screen.getByText((content) => content.includes('由车辆服务覆盖汇总、实时来源状态和 Redis 在线 key 计算'))).toBeInTheDocument();
});
test('copies mileage statistics summary for operations reporting', async () => {