From a7e10f0983d6efcb2389a295d469e889b73ac0d9 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 12:17:20 +0800 Subject: [PATCH] feat(platform): refine mileage customer workflow --- .../apps/web/src/pages/Mileage.tsx | 122 +++++++++--------- .../apps/web/src/test/App.test.tsx | 60 ++++----- 2 files changed, 91 insertions(+), 91 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index 657580b2..2809c7b6 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -75,7 +75,7 @@ function mileageActionRecommendation(row: DailyMileageRow) { return { label: '可用于日报统计', color: 'green' as const, - detail: '该日里程未发现异常,可进入车辆服务查看来源证据。' + detail: '该日里程未发现异常,可进入车辆服务查看统计依据。' }; } @@ -153,7 +153,7 @@ function mileageSummaryText({ vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; const dateFrom = filters.dateFrom?.trim(); const dateTo = filters.dateTo?.trim(); const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; @@ -163,7 +163,7 @@ function mileageSummaryText({ return [ '【里程统计摘要】', `车辆范围:${keyword}`, - `证据来源:${protocol}`, + `数据通道:${protocol}`, `日期范围:${dateRange}`, `统计车辆:${summary.vehicleCount.toLocaleString()},记录:${summary.recordCount.toLocaleString()},来源:${summary.sourceCount.toLocaleString()}`, `累计里程:${formatKm(summary.totalMileageKm)} km`, @@ -207,7 +207,7 @@ function mileageDeliveryPackageText({ vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; const dateFrom = filters.dateFrom?.trim(); const dateTo = filters.dateTo?.trim(); const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; @@ -221,7 +221,7 @@ function mileageDeliveryPackageText({ '【客户里程复核交付包】', `交付状态:${deliveryState}`, `车辆范围:${keyword}`, - `证据来源:${protocol}`, + `数据通道:${protocol}`, `日期范围:${dateRange}`, `累计里程:${formatKm(summary.totalMileageKm)} km`, `当前页明细合计:${formatKm(pageMileageTotal)} km`, @@ -235,7 +235,7 @@ function mileageDeliveryPackageText({ '统计口径:日里程按首末总里程差值计算,区间总值应等于每日里程之和', `里程统计:${statisticsURL}`, `轨迹复核:${appURL(buildAppHash({ page: 'history', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) } }))}`, - `原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`, + `历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); } @@ -270,7 +270,7 @@ function mileageCustomerDecisionText({ vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; const dateFrom = filters.dateFrom?.trim(); const dateTo = filters.dateTo?.trim(); const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; @@ -284,7 +284,7 @@ function mileageCustomerDecisionText({ '【客户里程决策说明】', `决策结论:${deliveryState}`, `车辆范围:${keyword}`, - `证据来源:${protocol}`, + `数据通道:${protocol}`, `时间范围:${dateRange}`, `区间总里程:${formatKm(summary.totalMileageKm)} km`, `当前明细合计:${formatKm(pageMileageTotal)} km`, @@ -298,11 +298,11 @@ function mileageCustomerDecisionText({ '客户判断路径:', '1. 先看区间闭合:区间总值必须等于每日里程之和。', '2. 再看记录覆盖:发布前需要覆盖全量明细或导出全量。', - '3. 再看异常记录:异常日必须回看轨迹和原始记录。', + '3. 再看异常记录:异常日必须回看轨迹和历史明细。', '4. 最后看在线影响:离线车辆会影响当天里程解释。', `里程统计:${statisticsURL}`, `轨迹复核:${appURL(buildAppHash({ page: 'history', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) } }))}`, - `原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`, + `历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); } @@ -333,7 +333,7 @@ function mileageBIPublishText({ vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; const dateFrom = filters.dateFrom?.trim(); const dateTo = filters.dateTo?.trim(); const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; @@ -342,7 +342,7 @@ function mileageBIPublishText({ '【BI里程发布口径】', `发布结论:${publishable ? '可发布' : '需复核后发布'}`, `车辆范围:${keyword}`, - `证据来源:${protocol}`, + `数据通道:${protocol}`, `日期范围:${dateRange}`, `统计口径:日里程按首末总里程差值计算,区间总值等于每日里程之和`, `累计里程:${formatKm(summary.totalMileageKm)} km`, @@ -350,9 +350,9 @@ function mileageBIPublishText({ `闭合状态:${closureStatus}${closureDeltaKm == null ? '' : `,差值 ${formatKm(Math.abs(closureDeltaKm))} km`}`, `记录覆盖率:${formatPercent(pageCoverageRate)}`, `异常记录:${anomalyCount.toLocaleString()}`, - `来源一致性:${sourceConsistencyText}`, + `通道一致性:${sourceConsistencyText}`, `统计车辆:${summary.vehicleCount.toLocaleString()},记录:${summary.recordCount.toLocaleString()},来源:${summary.sourceCount.toLocaleString()}`, - `发布风险:${publishable ? '当前筛选范围闭合且无异常记录' : '存在分页未全量、异常记录或闭合差值,需要先复核轨迹和原始记录'}`, + `发布风险:${publishable ? '当前筛选范围闭合且无异常记录' : '存在分页未全量、异常记录或闭合差值,需要先复核轨迹和历史明细'}`, `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); @@ -382,14 +382,14 @@ function statisticsImpactText({ vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; const dateFrom = filters.dateFrom?.trim(); const dateTo = filters.dateTo?.trim(); const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; return [ '【统计影响范围】', `车辆范围:${keyword}`, - `证据来源:${protocol}`, + `数据通道:${protocol}`, `日期范围:${dateRange}`, `影响车辆:${summary.vehicleCount.toLocaleString()} 辆`, `统计记录:${summary.recordCount.toLocaleString()} 条`, @@ -400,7 +400,7 @@ function statisticsImpactText({ `可信度:${confidenceStatus}`, `异常记录:${anomalyCount.toLocaleString()}`, `当前页覆盖:${formatPercent(pageCoverageRate)}`, - `业务影响:${publishAuditConclusion === '可发布' ? '当前统计范围可进入运营和BI口径' : '当前统计范围需要先复核轨迹、原始记录和离线车辆影响'}`, + `业务影响:${publishAuditConclusion === '可发布' ? '当前统计范围可进入运营和BI口径' : '当前统计范围需要先复核轨迹、历史明细和离线车辆影响'}`, `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); @@ -444,13 +444,13 @@ function mileagePublishAuditReport({ const review = items.filter((item) => item.status === 'review').length; const pass = items.filter((item) => item.status === 'pass').length; const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; return [ '【统计发布审计】', `发布结论:${blocked > 0 ? '阻断发布' : review > 0 ? '复核后发布' : '可发布'}`, `车辆范围:${keyword}`, - `证据来源:${protocol}`, - `统计规模:${summary.vehicleCount.toLocaleString()} 车 / ${summary.recordCount.toLocaleString()} 条 / ${summary.sourceCount.toLocaleString()} 来源`, + `数据通道:${protocol}`, + `统计规模:${summary.vehicleCount.toLocaleString()} 车 / ${summary.recordCount.toLocaleString()} 条 / ${summary.sourceCount.toLocaleString()} 数据通道`, `审计结果:通过 ${pass.toLocaleString()} / 复核 ${review.toLocaleString()} / 阻断 ${blocked.toLocaleString()}`, '', ...items.map((item, index) => [ @@ -473,9 +473,9 @@ function mileageEvidencePackageText(row: DailyMileageRow) { }; const vehicle = row.plate?.trim() ? `${row.plate.trim()} / ${row.vin}` : row.vin; return [ - '【里程异常证据包】', + '【里程异常复核包】', `车辆:${vehicle}`, - `证据来源:${row.source || '未知来源'}`, + `数据通道:${row.source || '未知通道'}`, `统计日期:${row.date || '-'}`, `起始里程:${formatKm(row.startMileageKm)} km`, `结束里程:${formatKm(row.endMileageKm)} km`, @@ -484,7 +484,7 @@ function mileageEvidencePackageText(row: DailyMileageRow) { `处置建议:${mileageActionRecommendation(row).detail}`, `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: row.vin, protocol: row.source }))}`, `轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: row.vin, protocol: row.source, filters: evidenceFilters }))}`, - `原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: row.vin, protocol: row.source, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`, + `历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: row.vin, protocol: row.source, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`, `统计查询:${appURL(buildAppHash({ page: 'mileage', keyword: row.vin, protocol: row.source, filters: evidenceFilters }))}` ].join('\n'); } @@ -501,7 +501,7 @@ function onlineStatusHandoffText({ total: number; }) { const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; const handoffRows = Array.isArray(rows) ? rows : []; const offlineRows = handoffRows.filter((row) => !row.online); const protocolStats = Array.isArray(summary.protocolStats) ? summary.protocolStats : []; @@ -511,7 +511,7 @@ function onlineStatusHandoffText({ return [ '【车辆在线状态交接】', `车辆范围:${keyword}`, - `证据来源:${protocol}`, + `数据通道:${protocol}`, `在线概览:${summary.onlineVehicleCount.toLocaleString()} 在线 / ${summary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(summary.onlineRatePercent)}`, `Redis 在线 Key:${summary.redisOnlineKeys == null ? '-' : summary.redisOnlineKeys.toLocaleString()}`, `来源在线:${protocolLines}`, @@ -545,7 +545,7 @@ function onlineOperationsReportText({ vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; - const protocol = filters.protocol?.trim() || '全部来源'; + const protocol = filters.protocol?.trim() || '全部数据通道'; const protocolStats = Array.isArray(summary.protocolStats) ? summary.protocolStats : []; const offlineRows = onlineRows.filter((row) => !row.online); const longestOffline = offlineRows.reduce((current, row) => { @@ -555,7 +555,7 @@ function onlineOperationsReportText({ return [ '【在线统计运营态势】', `车辆范围:${keyword}`, - `证据来源:${protocol}`, + `数据通道:${protocol}`, `在线概览:${summary.onlineVehicleCount.toLocaleString()} 在线 / ${summary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(summary.onlineRatePercent)}`, `Redis 在线 Key:${summary.redisOnlineKeys == null ? '-' : summary.redisOnlineKeys.toLocaleString()}`, `统计证据:${summary.evidence || '-'}`, @@ -571,7 +571,7 @@ function onlineOperationsReportText({ '', '处置建议:', '1. 在线率下降时先检查实时在线状态、数据接入和最后上报时间', - '2. 离线车辆优先打开车辆服务,核对实时、轨迹、原始记录和告警证据', + '2. 离线车辆优先打开车辆服务,核对实时、轨迹、历史明细和告警记录', '3. 统计发布前确认离线车辆是否影响当日里程和区间闭合', `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) @@ -641,8 +641,8 @@ export function Mileage({ : '汇总总里程与明细合计不一致,请核对分页、来源和异常记录。'; const confidenceStatus = anomalyRows.length > 0 || pageCoverageRate < 100 ? '需复核' : '可用于 BI'; 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 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); @@ -681,15 +681,15 @@ export function Mileage({ status: anomalyRows.length > 0 ? 'review' : 'pass', value: anomalyRows.length.toLocaleString(), detail: anomalyRows.length > 0 ? '存在日里程异常标记,需核对首末总里程、补传和断链。' : '当前筛选范围未发现异常里程记录。', - action: anomalyRows.length > 0 ? '优先打开异常行的轨迹和原始记录复核。' : '继续检查来源一致性和在线证据。' + action: anomalyRows.length > 0 ? '优先打开异常行的轨迹和历史明细复核。' : '继续检查通道一致性和在线状态。' }, { key: 'source', - label: '来源一致性', + label: '通道一致性', status: summary.sourceCount > 1 ? 'pass' : summary.sourceCount === 1 ? 'review' : 'blocked', value: sourceConsistencyText, - detail: summary.sourceCount > 1 ? '可使用多来源交叉验证车辆统计。' : summary.sourceCount === 1 ? '只有单一来源,统计可用但可信度低于多来源。' : '当前统计缺少来源证据。', - action: summary.sourceCount > 1 ? '保留来源贡献作为审计附件。' : '核对 32960、808、MQTT 是否有缺失来源。' + detail: summary.sourceCount > 1 ? '可使用多通道交叉验证车辆统计。' : summary.sourceCount === 1 ? '只有单一通道,统计可用但可信度低于多通道。' : '当前统计缺少数据通道。', + action: summary.sourceCount > 1 ? '保留通道贡献作为审计附件。' : '核对 32960、808、MQTT 是否有缺失通道。' }, { key: 'online', @@ -772,7 +772,7 @@ export function Mileage({ value: `${anomalyRows.length.toLocaleString()} 条`, detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 需要回看轨迹。` : '当前明细未命中异常标记。', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, - action: '异常证据', + action: '异常明细', onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDecision() }, { @@ -788,7 +788,7 @@ export function Mileage({ const maxSourceMileage = Math.max(...sourceSeries.map((item) => item.value), 0); const filterSummary = [ currentVehicleKeyword ? `车辆:${currentVehicleKeyword}` : '', - currentProtocol ? `证据来源:${currentProtocol}` : '', + currentProtocol ? `数据通道:${currentProtocol}` : '', filters.dateFrom?.trim() ? `开始日期:${filters.dateFrom.trim()}` : '', filters.dateTo?.trim() ? `结束日期:${filters.dateTo.trim()}` : '' ].filter(Boolean); @@ -812,7 +812,7 @@ export function Mileage({ label: '异常记录', value: anomalyRows.length.toLocaleString(), color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, - detail: anomalyRows.length > 0 ? '建议先核对轨迹和原始记录' : '当前页未发现异常' + detail: anomalyRows.length > 0 ? '建议先核对轨迹和历史明细' : '当前页未发现异常' }, { label: '在线影响', @@ -833,7 +833,7 @@ export function Mileage({ action: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }) }, { - label: '查看原始记录', + label: '查看历史明细', disabled: !currentVehicleKeyword || !onOpenRaw, action: () => onOpenRaw?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '', includeFields: 'true' }) }, @@ -848,7 +848,7 @@ export function Mileage({ step: '01', title: '选择范围', value: currentVehicleKeyword || '全部车辆', - detail: `时间范围 ${mileageRangeText},来源 ${currentProtocol || '全部来源'}。`, + detail: `时间范围 ${mileageRangeText},数据通道 ${currentProtocol || '全部数据通道'}。`, action: '调整筛选', color: currentVehicleKeyword ? 'green' as const : 'blue' as const, disabled: false, @@ -881,7 +881,7 @@ export function Mileage({ step: '04', title: '复核证据', value: anomalyRows.length > 0 ? `${anomalyRows.length.toLocaleString()} 异常` : '轨迹/原始', - detail: anomalyRows.length > 0 ? '异常日优先回看轨迹和原始记录。' : '可从同一时间窗进入轨迹和原始记录。', + detail: anomalyRows.length > 0 ? '异常日优先回看轨迹和历史明细。' : '可从同一时间窗进入轨迹和历史明细。', action: '轨迹复核', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, disabled: !currentVehicleKeyword || !onOpenHistory, @@ -889,7 +889,7 @@ export function Mileage({ }, { step: '05', - title: '数据交付', + title: '明细导出', value: `${rows.length.toLocaleString()} 当前页`, detail: '导出明细并附带统计口径,支撑BI、客户复盘和运营日报。', action: '导出明细', @@ -1144,7 +1144,7 @@ export function Mileage({ detail: `范围 ${mileageRangeText},${closureActionText}`, color: closureStatusColor, primaryAction: '轨迹证据', - secondaryAction: '原始记录', + secondaryAction: '历史明细', disabled: !currentVehicleKeyword || !onOpenHistory, secondaryDisabled: !currentVehicleKeyword || !onOpenRaw, onPrimary: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }), @@ -1163,7 +1163,7 @@ export function Mileage({ onSecondary: () => currentVehicleKeyword && onOpenVehicle(currentVehicleKeyword, currentProtocol) }, { - title: '数据交付', + title: '明细导出', value: `${rows.length.toLocaleString()} 条当前页`, detail: `累计 ${formatKm(summary.totalMileageKm)} km,当前页 ${formatKm(pageMileageTotal)} km。`, color: 'blue' as const, @@ -1197,8 +1197,8 @@ export function Mileage({ />
当前车辆:{currentVehicleKeyword || '-'} - 当前来源:{currentProtocol || '全部来源'} - 里程统计按当前车辆与来源范围汇总。 + 数据通道:{currentProtocol || '全部数据通道'} + 里程统计按当前车辆与数据通道范围汇总。
@@ -1209,7 +1209,7 @@ export function Mileage({ 先定车辆和时间范围,再判断里程能否交付 - 统计查询从车辆服务出发:选择车辆或车队、设定时间窗、判断区间里程和发布状态,再进入轨迹、原始记录和导出明细。 + 统计查询从车辆服务出发:选择车辆或车队、设定时间窗、判断区间里程和发布状态,再进入轨迹、历史明细和导出明细。 @@ -1263,13 +1263,13 @@ export function Mileage({ title: '数据完整性', value: confidenceStatus, color: confidenceColor, - detail: '围绕位置、里程、SOC、速度和来源新鲜度判断统计可信度。' + detail: '围绕位置、里程、SOC、速度和数据通道新鲜度判断统计可信度。' }, { - title: '来源一致性', + title: '通道一致性', value: sourceConsistencyText, color: summary.sourceCount > 1 ? 'green' as const : 'orange' as const, - detail: 'GB32960、JT808、Yutong MQTT 只作为同一车辆服务的来源证据。' + detail: 'GB32960、JT808、Yutong MQTT 只作为同一车辆服务的数据通道。' } ].map((item) => (
@@ -1292,14 +1292,14 @@ export function Mileage({ {publishAuditConclusion} {confidenceStatus} - 先判断能不能交付,再进入轨迹、原始记录和导出复核 + 先判断能不能交付,再进入轨迹、历史明细和导出复核 - 客户关心的是区间里程是否可信、异常是否解释清楚、离线是否影响统计。数据来源只作为可追溯证据,不作为页面主入口。 + 客户关心的是区间里程是否可信、异常是否解释清楚、离线是否影响统计。数据通道只作为可追溯依据,不作为页面主入口。 - +
@@ -1330,7 +1330,7 @@ export function Mileage({
{formatKm(summary.totalMileageKm)} km
- 面向客户交付里程时,先确认区间里程、闭合状态、异常记录和在线影响,再导出明细或进入轨迹与原始记录复核。 + 面向客户交付里程时,先确认区间里程、闭合状态、异常记录和在线影响,再导出明细或进入轨迹与历史明细复核。 @@ -1367,7 +1367,7 @@ export function Mileage({ value: anomalyRows.length.toLocaleString(), detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 需要复核。` : '当前页没有异常里程记录。', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, - action: '异常证据', + action: '异常明细', onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDeliveryPackage }, { @@ -1467,7 +1467,7 @@ export function Mileage({ applyFilters(nextFilters); }}> - + GB32960 JT808 YUTONG_MQTT @@ -1677,7 +1677,7 @@ export function Mileage({
{closureStatus} - {closurePassed && anomalyRows.length === 0 ? '当前统计口径可直接用于 BI 里程口径;若存在跨来源差异,再进入车辆服务查看来源证据。' : '统计进入 BI 前需要结合异常记录、轨迹回放和原始记录字段完成复核。'} + {closurePassed && anomalyRows.length === 0 ? '当前统计口径可直接用于 BI 里程口径;若存在跨通道差异,再进入车辆服务查看统计依据。' : '统计进入 BI 前需要结合异常记录、轨迹回放和历史明细完成复核。'} @@ -1706,14 +1706,14 @@ export function Mileage({ {confidenceStatus === '可用于 BI' && closureStatus === '闭合通过' ? '可发布' : '需复核后发布'}
- 日里程按首末总里程差值计算,区间总值必须等于每日里程之和;发布前同步检查异常记录、分页覆盖和来源一致性。 + 日里程按首末总里程差值计算,区间总值必须等于每日里程之和;发布前同步检查异常记录、分页覆盖和通道一致性。
闭合状态{closureStatus}
记录覆盖{formatPercent(pageCoverageRate)}
异常记录{anomalyRows.length.toLocaleString()}
-
来源一致性{sourceConsistencyText}
+
通道一致性{sourceConsistencyText}
@@ -1729,7 +1729,7 @@ export function Mileage({ 通过 {publishAuditItems.length - publishReviewCount - publishBlockedCount} / 复核 {publishReviewCount} / 阻断 {publishBlockedCount} - 发布前统一审计闭合、覆盖、异常、来源和在线证据;不满足时先回到轨迹和原始记录复核。 + 发布前统一审计闭合、覆盖、异常、数据通道和在线状态;不满足时先回到轨迹和历史明细复核。 @@ -1785,7 +1785,7 @@ export function Mileage({
{[ - { label: '统计可信度', value: confidenceStatus, color: confidenceColor, detail: anomalyRows.length > 0 ? '存在异常里程记录,建议先核对来源证据。' : '当前分页未发现异常记录。' }, + { label: '统计可信度', value: confidenceStatus, color: confidenceColor, detail: anomalyRows.length > 0 ? '存在异常里程记录,建议先核对统计依据。' : '当前分页未发现异常记录。' }, { label: '异常率', value: formatPercent(anomalyRate), color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, detail: `${anomalyRows.length.toLocaleString()} / ${rows.length.toLocaleString()} 条明细命中异常标记。` }, { label: '最大单日占比', value: formatPercent(peakMileageShare), color: peakMileageShare > 50 ? 'orange' as const : 'blue' as const, detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} 贡献最高单日里程。` : '暂无明细。' }, { label: '当前页里程', value: `${formatKm(pageMileageTotal)} km`, color: 'blue' as const, detail: '用于快速核对当前分页明细合计。' }, @@ -1885,7 +1885,7 @@ export function Mileage({
异常复核 -

异常优先检查补传、来源切换、总里程回退和跨来源差异,必要时回到车辆服务查看原始记录。

+

异常优先检查补传、通道切换、总里程回退和跨通道差异,必要时回到车辆服务查看历史明细。

diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index dbd67fcd..e05e8bdd 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2482,7 +2482,7 @@ test('shows and clears current vehicle service filters', async () => { expect(await screen.findByText('当前车辆筛选')).toBeInTheDocument(); expect(screen.getByText('关键词:粤A')).toBeInTheDocument(); - expect(screen.getByText('证据来源:JT808')).toBeInTheDocument(); + expect(screen.getAllByText('数据通道:JT808').length).toBeGreaterThan(0); expect(screen.getByText('车辆覆盖:多源')).toBeInTheDocument(); expect(screen.getByText('缺失来源:YUTONG_MQTT')).toBeInTheDocument(); expect(screen.getByText('服务状态:来源不完整')).toBeInTheDocument(); @@ -5437,7 +5437,7 @@ test('copies trajectory playback summary from history page', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹回放摘要】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('查询范围:2026-07-03 至 2026-07-04')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹点:2,有效定位:2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('区间里程:22.6 km')); @@ -5463,7 +5463,7 @@ test('copies trajectory playback summary from history page', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【轨迹运营影响】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:VIN-HISTORY-SUMMARY')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务状态:需要复核')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹范围:位置 2 / 有效定位 2 / 原始记录 1 / 字段明细 2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('定位覆盖率:100%')); @@ -6376,7 +6376,7 @@ test('shows and clears current history filters while keeping vehicle scope', asy expect(await screen.findByText('当前历史筛选')).toBeInTheDocument(); expect(screen.getByText('车辆:VIN-HISTORY-001')).toBeInTheDocument(); - expect(screen.getByText('证据来源:JT808')).toBeInTheDocument(); + expect(screen.getAllByText('数据通道:JT808').length).toBeGreaterThan(0); expect(screen.getByText('开始时间:2026-07-01 00:00:00')).toBeInTheDocument(); expect(screen.getByText('结束时间:2026-07-01 23:59:59')).toBeInTheDocument(); expect(screen.getAllByText('返回字段明细').length).toBeGreaterThanOrEqual(2); @@ -7249,7 +7249,7 @@ test('shows vehicle and source scope on mileage hash', async () => { render(); expect(await screen.findByText('当前车辆:VIN-MILEAGE-001')).toBeInTheDocument(); - expect(screen.getByText('当前来源:GB32960')).toBeInTheDocument(); + expect(screen.getAllByText('数据通道:GB32960').length).toBeGreaterThan(0); }); test('applies mileage date range from shareable hash to API requests', async () => { @@ -7294,7 +7294,7 @@ test('applies mileage date range from shareable hash to API requests', async () expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateFrom=2026-07-01'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('dateTo=2026-07-03'), undefined); expect(screen.getByText('当前车辆:VIN-MILEAGE-002')).toBeInTheDocument(); - expect(screen.getByText('当前来源:JT808')).toBeInTheDocument(); + expect(screen.getAllByText('数据通道:JT808').length).toBeGreaterThan(0); }); test('shows and clears current mileage filters while keeping vehicle scope', async () => { @@ -7335,7 +7335,7 @@ test('shows and clears current mileage filters while keeping vehicle scope', asy expect(await screen.findByText('当前里程筛选')).toBeInTheDocument(); expect(screen.getByText('车辆:VIN-MILEAGE-002')).toBeInTheDocument(); - expect(screen.getByText('证据来源:JT808')).toBeInTheDocument(); + expect(screen.getAllByText('数据通道:JT808').length).toBeGreaterThan(0); expect(screen.getByText('开始日期:2026-07-01')).toBeInTheDocument(); expect(screen.getByText('结束日期:2026-07-03')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '清空筛选' })); @@ -7384,7 +7384,7 @@ test('updates mileage hash when mileage filters are submitted', async () => { await screen.findByRole('heading', { name: '里程统计' }); fireEvent.change(screen.getByPlaceholderText('VIN / 车牌 / 手机号 / OEM'), { target: { value: '粤AG18312' } }); - fireEvent.click(screen.getByText('全部来源')); + fireEvent.click(screen.getByText('全部数据通道')); fireEvent.click(await screen.findByText('JT808')); fireEvent.change(screen.getByPlaceholderText('2026-07-01'), { target: { value: '2026-07-01' } }); fireEvent.change(screen.getByPlaceholderText('2026-07-03'), { target: { value: '2026-07-03' } }); @@ -7454,7 +7454,7 @@ test('opens current vehicle service from mileage header with current source evid render(); - expect(await screen.findByText('当前来源:GB32960')).toBeInTheDocument(); + expect((await screen.findAllByText('数据通道:GB32960')).length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '当前车辆服务' })); await waitFor(() => { @@ -7773,15 +7773,15 @@ test('shows mileage anomaly action guidance', async () => { expect(screen.getByText('日里程异常,优先核对当天首末总里程、来源断链和补传数据。')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: '复制证据' })[0]); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程异常证据包】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程异常复核包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A异常1 / VIN-MILEAGE-ANOMALY')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:GB32960')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道:GB32960')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计日期:2026-07-03')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('日里程:-3.2 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常等级:warning')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆服务:http://localhost:3000/#/detail?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('轨迹证据:http://localhost:3000/#/history?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960&dateFrom=2026-07-03&dateTo=2026-07-04')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('原始记录:http://localhost:3000/#/history-query?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史明细:http://localhost:3000/#/history-query?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960&tab=raw&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计查询:http://localhost:3000/#/mileage?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960&dateFrom=2026-07-03&dateTo=2026-07-04')); }); @@ -8001,28 +8001,28 @@ test('shows vehicle-first statistics domains for one vehicle service', async () expect(screen.getByRole('button', { name: '客户统计查询 区间里程 复制摘要' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户统计查询 发布判断 发布审计' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户统计查询 复核证据 轨迹复核' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '客户统计查询 数据交付 导出明细' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户统计查询 明细导出 导出明细' })).toBeInTheDocument(); expect(await screen.findByText('车辆统计服务总览')).toBeInTheDocument(); expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('在线率与离线时长')).toBeInTheDocument(); expect(screen.getByText('数据完整性')).toBeInTheDocument(); - expect(screen.getAllByText('来源一致性').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('通道一致性').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('三类数据源最终汇总为一个车辆服务统计视图')).toBeInTheDocument(); expect(screen.getByText('当前统计证据')).toBeInTheDocument(); - expect(screen.getByText('3 车 / 2 来源 / 3 条明细')).toBeInTheDocument(); + expect(screen.getByText('3 车 / 2 数据通道 / 3 条明细')).toBeInTheDocument(); expect(screen.getByText('客户里程决策台')).toBeInTheDocument(); - expect(screen.getByText('先判断能不能交付,再进入轨迹、原始记录和导出复核')).toBeInTheDocument(); + expect(screen.getByText('先判断能不能交付,再进入轨迹、历史明细和导出复核')).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: '复制决策说明' }).length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '客户里程决策 交付结论 复制决策' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程决策 区间闭合 统计摘要' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程决策 覆盖率 导出明细' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '客户里程决策 异常复核 异常证据' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程决策 异常复核 异常明细' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户里程决策 在线影响 在线态势' })).toBeInTheDocument(); expect(screen.getByText('客户里程复核交付包')).toBeInTheDocument(); - expect(screen.getByText('面向客户交付里程时,先确认区间里程、闭合状态、异常记录和在线影响,再导出明细或进入轨迹与原始记录复核。')).toBeInTheDocument(); + expect(screen.getByText('面向客户交付里程时,先确认区间里程、闭合状态、异常记录和在线影响,再导出明细或进入轨迹与历史明细复核。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程交付 区间里程 复制交付' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程交付 闭合校验 闭合摘要' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '里程交付 异常记录 异常证据' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程交付 异常记录 异常明细' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程交付 在线影响 在线态势' })).toBeInTheDocument(); expect(screen.getByText('统计影响范围')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制影响范围' })).toBeInTheDocument(); @@ -8033,11 +8033,11 @@ test('shows vehicle-first statistics domains for one vehicle service', async () expect(screen.getAllByText('发布判断').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('区间复核')).toBeInTheDocument(); expect(screen.getByText('异常处理')).toBeInTheDocument(); - expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('明细导出').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '里程服务任务 发布判断 BI口径' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程服务任务 区间复核 轨迹证据' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '里程服务任务 异常处理 复制证据' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '里程服务任务 数据交付 导出明细' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '里程服务任务 明细导出 导出明细' })).toBeInTheDocument(); expect(screen.getByText('影响车辆')).toBeInTheDocument(); expect(screen.getByText('统计规模')).toBeInTheDocument(); expect(screen.getByText('闭合影响')).toBeInTheDocument(); @@ -8057,13 +8057,13 @@ test('shows vehicle-first statistics domains for one vehicle service', async () fireEvent.click(screen.getByRole('button', { name: '复制影响范围' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【统计影响范围】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响车辆:3 辆')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('统计记录:6 条')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('累计里程:210 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线状态:3 在线 / 1 离线 / 在线率 75%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布判断:复核后发布')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务影响:当前统计范围需要先复核轨迹、原始记录和离线车辆影响')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('业务影响:当前统计范围需要先复核轨迹、历史明细和离线车辆影响')); fireEvent.click(screen.getAllByRole('button', { name: '复制决策说明' })[0]); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户里程决策说明】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('决策结论:复核后交付')); @@ -8072,13 +8072,13 @@ test('shows vehicle-first statistics domains for one vehicle service', async () fireEvent.click(screen.getByRole('button', { name: '复制在线态势' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【在线统计运营态势】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线概览:3 在线 / 1 离线 / 在线率 75%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('Redis 在线 Key:92')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页车辆:2 / 当前筛选 2')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最长离线:粤A离线1 / 2 小时 / 2026-07-03 18:12:10')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. JT808:2/3,在线率 66.7%')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. 离线车辆优先打开车辆服务,核对实时、轨迹、原始记录和告警证据')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. 离线车辆优先打开车辆服务,核对实时、轨迹、历史明细和告警记录')); fireEvent.click(screen.getByRole('button', { name: '里程交付 区间里程 复制交付' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户里程复核交付包】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-STATS-SERVICE')); @@ -8165,7 +8165,7 @@ test('copies mileage statistics summary for operations reporting', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【里程统计摘要】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:VIN-MILEAGE-COPY')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据来源:JT808')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('数据通道:JT808')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('日期范围:2026-07-01 至 2026-07-03')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('累计里程:140 km')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前页里程:140 km')); @@ -8181,8 +8181,8 @@ test('copies mileage statistics summary for operations reporting', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('闭合状态:分页抽样')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('记录覆盖率:50%')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('异常记录:1')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('来源一致性:可做跨来源核对')); - expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布风险:存在分页未全量、异常记录或闭合差值,需要先复核轨迹和原始记录')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('通道一致性:可做跨通道核对')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布风险:存在分页未全量、异常记录或闭合差值,需要先复核轨迹和历史明细')); fireEvent.click(screen.getByRole('button', { name: '复制统计发布审计' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【统计发布审计】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('发布结论:复核后发布')); @@ -8764,7 +8764,7 @@ test('opens current vehicle service from history header with current source evid render(); - expect(await screen.findByText('当前来源:JT808')).toBeInTheDocument(); + expect((await screen.findAllByText('数据通道:JT808')).length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '当前车辆服务' })); await waitFor(() => { @@ -10319,7 +10319,7 @@ test('shows and clears current realtime service filters', async () => { expect(await screen.findByText('当前实时筛选')).toBeInTheDocument(); expect(screen.getByText('关键词:粤ART002')).toBeInTheDocument(); - expect(screen.getByText('数据通道:JT808')).toBeInTheDocument(); + expect(screen.getAllByText('数据通道:JT808').length).toBeGreaterThan(0); expect(screen.getByText('在线:在线')).toBeInTheDocument(); expect(screen.getByText('服务状态:来源不完整')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '清空筛选' }));