feat(platform): add parsed fields history view
This commit is contained in:
@@ -333,8 +333,8 @@ export default function App() {
|
|||||||
const nextFilters = normalizeHistoryFilterValues(filters);
|
const nextFilters = normalizeHistoryFilterValues(filters);
|
||||||
const nextVin = nextFilters.keyword?.trim() || analysisVin;
|
const nextVin = nextFilters.keyword?.trim() || analysisVin;
|
||||||
const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol;
|
const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol;
|
||||||
const nextTab = nextFilters.tab === 'raw' ? 'raw' : 'location';
|
const nextTab = nextFilters.tab === 'raw' || nextFilters.tab === 'fields' ? nextFilters.tab : 'location';
|
||||||
const nextPage = nextTab === 'raw' ? 'history-query' : 'history';
|
const nextPage = nextTab === 'raw' || nextTab === 'fields' ? 'history-query' : 'history';
|
||||||
setAnalysisVin(nextVin);
|
setAnalysisVin(nextVin);
|
||||||
setActiveProtocol(nextProtocol);
|
setActiveProtocol(nextProtocol);
|
||||||
setHistoryTab(nextTab);
|
setHistoryTab(nextTab);
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ describe('buildAppHash', () => {
|
|||||||
expect(buildAppHash({ page: 'history-query', keyword: 'VIN001', protocol: 'GB32960', filters: { tab: 'raw' } })).toBe('#/history-query?keyword=VIN001&protocol=GB32960&tab=raw');
|
expect(buildAppHash({ page: 'history-query', keyword: 'VIN001', protocol: 'GB32960', filters: { tab: 'raw' } })).toBe('#/history-query?keyword=VIN001&protocol=GB32960&tab=raw');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('builds shareable parsed fields history query hash', () => {
|
||||||
|
expect(buildAppHash({ page: 'history-query', keyword: 'VIN001', protocol: 'GB32960', filters: { tab: 'fields', fields: 'gb32960.vehicle.speed_kmh' } })).toBe('#/history-query?keyword=VIN001&protocol=GB32960&tab=fields&fields=gb32960.vehicle.speed_kmh');
|
||||||
|
});
|
||||||
|
|
||||||
test('builds shareable vehicle list hash with filters', () => {
|
test('builds shareable vehicle list hash with filters', () => {
|
||||||
expect(buildAppHash({ page: 'vehicles', filters: { coverage: 'multi', serviceStatus: 'degraded', archiveStatus: 'incomplete', archiveMissing: 'phone', missingProtocol: 'YUTONG_MQTT' } })).toBe('#/vehicles?coverage=multi&serviceStatus=degraded&archiveStatus=incomplete&archiveMissing=phone&missingProtocol=YUTONG_MQTT');
|
expect(buildAppHash({ page: 'vehicles', filters: { coverage: 'multi', serviceStatus: 'degraded', archiveStatus: 'incomplete', archiveMissing: 'phone', missingProtocol: 'YUTONG_MQTT' } })).toBe('#/vehicles?coverage=multi&serviceStatus=degraded&archiveStatus=incomplete&archiveMissing=phone&missingProtocol=YUTONG_MQTT');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,19 @@ type HistoryFilters = {
|
|||||||
includeFields?: boolean | string;
|
includeFields?: boolean | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RawFieldRow = {
|
||||||
|
id: string;
|
||||||
|
rawId: string;
|
||||||
|
vin: string;
|
||||||
|
plate: string;
|
||||||
|
protocol: string;
|
||||||
|
frameType: string;
|
||||||
|
deviceTime: string;
|
||||||
|
serverTime: string;
|
||||||
|
fieldPath: string;
|
||||||
|
fieldValue: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
const defaultFilters: HistoryFilters = {
|
const defaultFilters: HistoryFilters = {
|
||||||
keyword: 'LB9A32A24R0LS1426',
|
keyword: 'LB9A32A24R0LS1426',
|
||||||
includeFields: false
|
includeFields: false
|
||||||
@@ -48,9 +61,10 @@ function splitFields(value?: string) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
type HistoryTabKey = 'location' | 'raw';
|
type HistoryTabKey = 'location' | 'raw' | 'fields';
|
||||||
|
|
||||||
function normalizeHistoryTab(value?: string): HistoryTabKey {
|
function normalizeHistoryTab(value?: string): HistoryTabKey {
|
||||||
|
if (value === 'fields') return 'fields';
|
||||||
return value === 'raw' ? 'raw' : 'location';
|
return value === 'raw' ? 'raw' : 'location';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +150,42 @@ const rawExportColumns: CsvColumn<RawFrameRow>[] = [
|
|||||||
{ title: '解析字段', value: (row) => row.parsedFields ?? {} }
|
{ title: '解析字段', value: (row) => row.parsedFields ?? {} }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const rawFieldExportColumns: CsvColumn<RawFieldRow>[] = [
|
||||||
|
{ title: 'RAW ID', value: (row) => row.rawId },
|
||||||
|
{ title: 'VIN', value: (row) => row.vin },
|
||||||
|
{ title: '车牌', value: (row) => row.plate },
|
||||||
|
{ title: '数据来源', value: (row) => row.protocol },
|
||||||
|
{ title: '帧类型', value: (row) => row.frameType },
|
||||||
|
{ title: '字段', value: (row) => row.fieldPath },
|
||||||
|
{ title: '值', value: (row) => formatFieldValue(row.fieldValue) },
|
||||||
|
{ title: '设备时间', value: (row) => row.deviceTime },
|
||||||
|
{ title: '入库时间', value: (row) => row.serverTime }
|
||||||
|
];
|
||||||
|
|
||||||
|
function formatFieldValue(value: unknown) {
|
||||||
|
if (value == null) return '';
|
||||||
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenParsedFields(value: unknown, prefix = ''): Array<{ path: string; value: unknown }> {
|
||||||
|
if (value == null || typeof value !== 'object') {
|
||||||
|
return prefix ? [{ path: prefix, value }] : [];
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item, index) => flattenParsedFields(item, prefix ? `${prefix}.${index}` : String(index)));
|
||||||
|
}
|
||||||
|
return Object.entries(value as Record<string, unknown>).flatMap(([key, item]) => {
|
||||||
|
const nextPath = prefix ? `${prefix}.${key}` : key;
|
||||||
|
if (item != null && typeof item === 'object') {
|
||||||
|
return flattenParsedFields(item, nextPath);
|
||||||
|
}
|
||||||
|
return [{ path: nextPath, value: item }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function exportFileName(prefix: string, filters: HistoryFilters) {
|
function exportFileName(prefix: string, filters: HistoryFilters) {
|
||||||
const keyword = filters.keyword?.trim() || 'all';
|
const keyword = filters.keyword?.trim() || 'all';
|
||||||
const protocol = filters.protocol?.trim() || 'all-source';
|
const protocol = filters.protocol?.trim() || 'all-source';
|
||||||
@@ -263,13 +313,13 @@ export function History({
|
|||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildRawQuery = (nextFilters: HistoryFilters, limit: number, offset: number): RawFrameQuery => {
|
const buildRawQuery = (nextFilters: HistoryFilters, limit: number, offset: number, forceIncludeFields = false): RawFrameQuery => {
|
||||||
const query: RawFrameQuery = { limit, offset };
|
const query: RawFrameQuery = { limit, offset };
|
||||||
if (nextFilters.keyword?.trim()) query.keyword = nextFilters.keyword.trim();
|
if (nextFilters.keyword?.trim()) query.keyword = nextFilters.keyword.trim();
|
||||||
if (nextFilters.protocol?.trim()) query.protocol = nextFilters.protocol.trim();
|
if (nextFilters.protocol?.trim()) query.protocol = nextFilters.protocol.trim();
|
||||||
if (nextFilters.dateFrom?.trim()) query.dateFrom = nextFilters.dateFrom.trim();
|
if (nextFilters.dateFrom?.trim()) query.dateFrom = nextFilters.dateFrom.trim();
|
||||||
if (nextFilters.dateTo?.trim()) query.dateTo = nextFilters.dateTo.trim();
|
if (nextFilters.dateTo?.trim()) query.dateTo = nextFilters.dateTo.trim();
|
||||||
if (isIncludeFieldsEnabled(nextFilters.includeFields)) query.includeFields = true;
|
if (forceIncludeFields || isIncludeFieldsEnabled(nextFilters.includeFields)) query.includeFields = true;
|
||||||
const fields = splitFields(nextFilters.fields);
|
const fields = splitFields(nextFilters.fields);
|
||||||
if (fields.length > 0) query.fields = fields;
|
if (fields.length > 0) query.fields = fields;
|
||||||
return query;
|
return query;
|
||||||
@@ -288,15 +338,15 @@ export function History({
|
|||||||
.finally(() => setLoadingLocations(false));
|
.finally(() => setLoadingLocations(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadRawFrames = (nextFilters = filters, page = rawPagination.currentPage, pageSize = rawPagination.pageSize) => {
|
const loadRawFrames = (nextFilters = filters, page = rawPagination.currentPage, pageSize = rawPagination.pageSize, forceIncludeFields = activeTab === 'fields') => {
|
||||||
if (shouldBlockRawFieldQuery(nextFilters)) {
|
if ((forceIncludeFields || isIncludeFieldsEnabled(nextFilters.includeFields)) && shouldBlockRawFieldQuery(nextFilters)) {
|
||||||
setRawFrames({ items: [], total: 0, limit: pageSize, offset: (page - 1) * pageSize });
|
setRawFrames({ items: [], total: 0, limit: pageSize, offset: (page - 1) * pageSize });
|
||||||
setRawPagination({ currentPage: page, pageSize });
|
setRawPagination({ currentPage: page, pageSize });
|
||||||
Toast.warning('RAW 解析字段查询需要车辆、时间范围或字段裁剪');
|
Toast.warning('RAW 解析字段查询需要车辆、时间范围或字段裁剪');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoadingRaw(true);
|
setLoadingRaw(true);
|
||||||
api.rawFramesQuery(buildRawQuery(nextFilters, pageSize, (page - 1) * pageSize))
|
api.rawFramesQuery(buildRawQuery(nextFilters, pageSize, (page - 1) * pageSize, forceIncludeFields))
|
||||||
.then((nextPage) => {
|
.then((nextPage) => {
|
||||||
setRawFrames(nextPage);
|
setRawFrames(nextPage);
|
||||||
setRawPagination({ currentPage: page, pageSize });
|
setRawPagination({ currentPage: page, pageSize });
|
||||||
@@ -317,14 +367,14 @@ export function History({
|
|||||||
setFilters(nextFilters);
|
setFilters(nextFilters);
|
||||||
onFiltersChange?.(nextFilters, activeTab);
|
onFiltersChange?.(nextFilters, activeTab);
|
||||||
loadLocations(nextFilters, 1, locationPagination.pageSize);
|
loadLocations(nextFilters, 1, locationPagination.pageSize);
|
||||||
loadRawFrames(nextFilters, 1, rawPagination.pageSize);
|
loadRawFrames(nextFilters, 1, rawPagination.pageSize, activeTab === 'fields');
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyFilters = (nextFilters: HistoryFilters) => {
|
const applyFilters = (nextFilters: HistoryFilters) => {
|
||||||
setFilters(nextFilters);
|
setFilters(nextFilters);
|
||||||
onFiltersChange?.(nextFilters, activeTab);
|
onFiltersChange?.(nextFilters, activeTab);
|
||||||
loadLocations(nextFilters, 1, locationPagination.pageSize);
|
loadLocations(nextFilters, 1, locationPagination.pageSize);
|
||||||
loadRawFrames(nextFilters, 1, rawPagination.pageSize);
|
loadRawFrames(nextFilters, 1, rawPagination.pageSize, activeTab === 'fields');
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
@@ -334,19 +384,40 @@ export function History({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters);
|
const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters);
|
||||||
|
const nextTab = normalizeHistoryTab(initialTab);
|
||||||
setFilters(nextFilters);
|
setFilters(nextFilters);
|
||||||
setActiveTab(normalizeHistoryTab(initialTab));
|
setActiveTab(nextTab);
|
||||||
loadLocations(nextFilters, 1, locationPagination.pageSize);
|
loadLocations(nextFilters, 1, locationPagination.pageSize);
|
||||||
loadRawFrames(nextFilters, 1, rawPagination.pageSize);
|
loadRawFrames(nextFilters, 1, rawPagination.pageSize, nextTab === 'fields');
|
||||||
}, [initialVin, initialProtocol, initialTab, JSON.stringify(initialFilters)]);
|
}, [initialVin, initialProtocol, initialTab, JSON.stringify(initialFilters)]);
|
||||||
|
|
||||||
const changeTab = (key: string) => {
|
const changeTab = (key: string) => {
|
||||||
const nextTab = normalizeHistoryTab(String(key));
|
const nextTab = normalizeHistoryTab(String(key));
|
||||||
|
const nextFilters = nextTab === 'fields' ? { ...filters, includeFields: true } : filters;
|
||||||
setActiveTab(nextTab);
|
setActiveTab(nextTab);
|
||||||
onFiltersChange?.(filters, nextTab);
|
setFilters(nextFilters);
|
||||||
|
onFiltersChange?.(nextFilters, nextTab);
|
||||||
|
if (nextTab === 'fields') {
|
||||||
|
loadRawFrames(nextFilters, 1, rawPagination.pageSize, true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]);
|
const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]);
|
||||||
|
const rawFieldRows = useMemo<RawFieldRow[]>(() => rawFrames.items.flatMap((row) => {
|
||||||
|
const fields = flattenParsedFields(row.parsedFields ?? {});
|
||||||
|
return fields.map((field, index) => ({
|
||||||
|
id: `${row.id}-${field.path}-${index}`,
|
||||||
|
rawId: row.id,
|
||||||
|
vin: row.vin,
|
||||||
|
plate: row.plate,
|
||||||
|
protocol: row.protocol,
|
||||||
|
frameType: row.frameType,
|
||||||
|
deviceTime: row.deviceTime,
|
||||||
|
serverTime: row.serverTime,
|
||||||
|
fieldPath: field.path,
|
||||||
|
fieldValue: field.value
|
||||||
|
}));
|
||||||
|
}), [rawFrames.items]);
|
||||||
const locationItems = locations.items;
|
const locationItems = locations.items;
|
||||||
const validLocations = locationItems.filter(hasValidCoordinate);
|
const validLocations = locationItems.filter(hasValidCoordinate);
|
||||||
const mileageValues = locationItems.map((item) => item.totalMileageKm).filter(isFiniteNumber);
|
const mileageValues = locationItems.map((item) => item.totalMileageKm).filter(isFiniteNumber);
|
||||||
@@ -409,6 +480,14 @@ export function History({
|
|||||||
downloadCsv(exportFileName('raw-frames', filters), buildCsv(rawExportColumns, rawFrames.items));
|
downloadCsv(exportFileName('raw-frames', filters), buildCsv(rawExportColumns, rawFrames.items));
|
||||||
Toast.success(`已导出 ${rawFrames.items.length} 条 RAW 帧`);
|
Toast.success(`已导出 ${rawFrames.items.length} 条 RAW 帧`);
|
||||||
};
|
};
|
||||||
|
const exportRawFields = () => {
|
||||||
|
if (rawFieldRows.length === 0) {
|
||||||
|
Toast.warning('当前没有可导出的解析字段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadCsv(exportFileName('raw-fields', filters), buildCsv(rawFieldExportColumns, rawFieldRows));
|
||||||
|
Toast.success(`已导出 ${rawFieldRows.length} 条解析字段`);
|
||||||
|
};
|
||||||
const copyTrajectorySummary = () => {
|
const copyTrajectorySummary = () => {
|
||||||
copyText(
|
copyText(
|
||||||
trajectorySummaryText({
|
trajectorySummaryText({
|
||||||
@@ -772,6 +851,51 @@ export function History({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="解析字段" itemKey="fields">
|
||||||
|
<div className="vp-table-toolbar">
|
||||||
|
<Space wrap>
|
||||||
|
<Tag color="green">当前页 {rawFieldRows.length.toLocaleString()} 个字段</Tag>
|
||||||
|
<Tag color="blue">{rawFrames.items.length.toLocaleString()} 条 RAW 来源</Tag>
|
||||||
|
{selectedFieldCount > 0 ? <Tag color="blue">字段裁剪 {selectedFieldCount.toLocaleString()} 个</Tag> : <Tag color="grey">全量解析字段</Tag>}
|
||||||
|
<Button size="small" onClick={exportRawFields}>导出解析字段当前页 CSV</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={rawFieldRows}
|
||||||
|
loading={loadingRaw}
|
||||||
|
pagination={false}
|
||||||
|
columns={[
|
||||||
|
{ title: '字段', dataIndex: 'fieldPath', width: 320 },
|
||||||
|
{
|
||||||
|
title: '值',
|
||||||
|
width: 260,
|
||||||
|
render: (_: unknown, row: RawFieldRow) => (
|
||||||
|
<Typography.Text ellipsis={{ showTooltip: true }}>{formatFieldValue(row.fieldValue) || '-'}</Typography.Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ title: 'VIN', dataIndex: 'vin', width: 190 },
|
||||||
|
{ title: '车牌', dataIndex: 'plate', width: 120 },
|
||||||
|
{ title: '数据来源', dataIndex: 'protocol', width: 120 },
|
||||||
|
{ title: '帧类型', dataIndex: 'frameType', width: 190 },
|
||||||
|
{ title: '设备时间', dataIndex: 'deviceTime', width: 190 },
|
||||||
|
{ title: 'RAW ID', dataIndex: 'rawId', width: 260 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 170,
|
||||||
|
render: (_: unknown, row: RawFieldRow) => (
|
||||||
|
<Space wrap>
|
||||||
|
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, row.protocol)}>车辆服务</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Space wrap style={{ marginTop: 12 }}>
|
||||||
|
<Tag color="grey">分页沿用 RAW 帧分页</Tag>
|
||||||
|
<Tag color="grey">字段来自 parsedFields,保留协议字段映射后的路径</Tag>
|
||||||
|
</Space>
|
||||||
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
<SideSheet title="RAW 解析字段" visible={Boolean(selectedRaw)} onCancel={() => setSelectedRaw(null)} width={720}>
|
<SideSheet title="RAW 解析字段" visible={Boolean(selectedRaw)} onCancel={() => setSelectedRaw(null)} width={720}>
|
||||||
|
|||||||
@@ -5335,6 +5335,83 @@ test('shows and clears current history filters while keeping vehicle scope', asy
|
|||||||
expect(window.location.hash).toBe('#/history-query?keyword=VIN-HISTORY-001&tab=raw');
|
expect(window.location.hash).toBe('#/history-query?keyword=VIN-HISTORY-001&tab=raw');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows parsed field history as a flattened evidence table', async () => {
|
||||||
|
window.history.replaceState(null, '', '/#/history-query?keyword=VIN-FIELDS-001&protocol=GB32960&tab=fields&fields=gb32960.vehicle.speed_kmh');
|
||||||
|
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||||
|
const path = String(input);
|
||||||
|
if (path.includes('/api/ops/health')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } },
|
||||||
|
traceId: 'trace-test',
|
||||||
|
timestamp: 1783094400000
|
||||||
|
})
|
||||||
|
} as Response;
|
||||||
|
}
|
||||||
|
if (path.includes('/api/history/locations')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
data: { items: [], total: 0, limit: 10, offset: 0 },
|
||||||
|
traceId: 'trace-test',
|
||||||
|
timestamp: 1783094400000
|
||||||
|
})
|
||||||
|
} as Response;
|
||||||
|
}
|
||||||
|
if (path.includes('/api/history/raw-frames/query')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
items: [{
|
||||||
|
id: 'raw-field-001',
|
||||||
|
vin: 'VIN-FIELDS-001',
|
||||||
|
plate: '粤A字段1',
|
||||||
|
protocol: 'GB32960',
|
||||||
|
frameType: 'realtime',
|
||||||
|
rawSizeBytes: 256,
|
||||||
|
deviceTime: '2026-07-03 11:56:06',
|
||||||
|
serverTime: '2026-07-03 11:56:07',
|
||||||
|
parsedFields: {
|
||||||
|
'gb32960.vehicle.speed_kmh': 42,
|
||||||
|
'gb32960.vehicle.soc_percent': 78.4
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
total: 1,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0
|
||||||
|
},
|
||||||
|
traceId: 'trace-test',
|
||||||
|
timestamp: 1783094400000
|
||||||
|
})
|
||||||
|
} as Response;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
data: { items: [], total: 0, limit: 10, offset: 0 },
|
||||||
|
traceId: 'trace-test',
|
||||||
|
timestamp: 1783094400000
|
||||||
|
})
|
||||||
|
} as Response;
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('heading', { name: '历史查询' })).toBeInTheDocument();
|
||||||
|
expect(await screen.findByRole('tab', { name: '解析字段' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
body: expect.stringContaining('"includeFields":true')
|
||||||
|
}));
|
||||||
|
expect(screen.getAllByText('gb32960.vehicle.speed_kmh').length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(screen.getByText('gb32960.vehicle.soc_percent')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('42')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('78.4')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('当前页 2 个字段')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test('updates history hash when vehicle history filters are submitted', async () => {
|
test('updates history hash when vehicle history filters are submitted', async () => {
|
||||||
window.history.replaceState(null, '', '/#/history');
|
window.history.replaceState(null, '', '/#/history');
|
||||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||||
@@ -5898,7 +5975,7 @@ test('exports current raw frame page as csv with parsed fields', async () => {
|
|||||||
|
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(await screen.findByText('raw-export-001')).toBeInTheDocument();
|
expect((await screen.findAllByText('raw-export-001')).length).toBeGreaterThanOrEqual(1);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '导出 RAW 当前页 CSV' }));
|
fireEvent.click(screen.getByRole('button', { name: '导出 RAW 当前页 CSV' }));
|
||||||
|
|
||||||
expect(createObjectURL).toHaveBeenCalled();
|
expect(createObjectURL).toHaveBeenCalled();
|
||||||
@@ -7027,7 +7104,7 @@ test('opens same-day raw evidence from history location row', async () => {
|
|||||||
expect(params.get('includeFields')).toBe('true');
|
expect(params.get('includeFields')).toBe('true');
|
||||||
expect(params.get('tab')).toBe('raw');
|
expect(params.get('tab')).toBe('raw');
|
||||||
expect(await screen.findByRole('tab', { name: 'RAW 帧' })).toHaveAttribute('aria-selected', 'true');
|
expect(await screen.findByRole('tab', { name: 'RAW 帧' })).toHaveAttribute('aria-selected', 'true');
|
||||||
expect(await screen.findByText('raw-from-location-001')).toBeInTheDocument();
|
expect((await screen.findAllByText('raw-from-location-001')).length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('opens vehicle service from raw history rows with row source evidence', async () => {
|
test('opens vehicle service from raw history rows with row source evidence', async () => {
|
||||||
@@ -7215,7 +7292,7 @@ test('opens same-day mileage statistics from raw history row', async () => {
|
|||||||
|
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(await screen.findByText('raw-jt808-mileage-001')).toBeInTheDocument();
|
expect((await screen.findAllByText('raw-jt808-mileage-001')).length).toBeGreaterThanOrEqual(1);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '核对里程' }));
|
fireEvent.click(screen.getByRole('button', { name: '核对里程' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user