diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 79a29f2d..7ce2f20e 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -600,7 +600,7 @@ export default function App() { }; const pages: Record = { - dashboard: , + dashboard: , vehicles: , map: , realtime: , diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 98178fa7..2e09a01a 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -154,6 +154,16 @@ function dayRangeText(dateFrom?: string, dateTo?: string) { return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`; } +function naturalDayRangeText(dateFrom?: string, dateTo?: string) { + if (!dateFrom && !dateTo) return '未限定时间'; + if (!dateFrom || !dateTo) return '单边时间窗'; + const start = new Date(`${dateFrom}T00:00:00+08:00`).getTime(); + const end = new Date(`${dateTo}T00:00:00+08:00`).getTime(); + if (!Number.isFinite(start) || !Number.isFinite(end)) return '时间格式待确认'; + const days = Math.max(0, Math.round((end - start) / 86400000)) + 1; + return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`; +} + function priorityIssueVehicleLabel(row: QualityIssueRow) { const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim(); return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-'; @@ -261,6 +271,14 @@ function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Re return {label}; } +function defaultTimeMonitorFilters() { + const today = todayDateString(); + return { + dateFrom: previousDateString(today), + dateTo: today + }; +} + export function Dashboard({ onOpenVehicle, onOpenQuality, @@ -268,7 +286,8 @@ export function Dashboard({ onOpenRealtime, onOpenVehicles, onOpenHistory, - onOpenMileage + onOpenMileage, + customerTimeWindow }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenQuality: (filters?: Record) => void; @@ -277,6 +296,7 @@ export function Dashboard({ onOpenVehicles: (filters?: Record) => void; onOpenHistory: (filters?: Record) => void; onOpenMileage: (filters?: Record) => void; + customerTimeWindow?: Record; }) { const [summary, setSummary] = useState(null); const [serviceSummary, setServiceSummary] = useState(null); @@ -284,13 +304,7 @@ export function Dashboard({ const [locations, setLocations] = useState([]); const [qualityIssues, setQualityIssues] = useState([]); const [opsHealth, setOpsHealth] = useState(null); - const [timeMonitorFilters, setTimeMonitorFilters] = useState>(() => { - const today = todayDateString(); - return { - dateFrom: previousDateString(today), - dateTo: today - }; - }); + const [timeMonitorFilters, setTimeMonitorFilters] = useState>(defaultTimeMonitorFilters); const [loading, setLoading] = useState(true); const [coverageLoading, setCoverageLoading] = useState(false); const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState(''); @@ -343,6 +357,25 @@ export function Dashboard({ .finally(() => setLoading(false)); }, []); + useEffect(() => { + const dateFrom = customerTimeWindow?.dateFrom?.trim(); + const dateTo = customerTimeWindow?.dateTo?.trim(); + if (!dateFrom && !dateTo) { + return; + } + setTimeMonitorFilters((previous) => { + const next = { + ...previous, + dateFrom: dateFrom ?? '', + dateTo: dateTo ?? '' + }; + if (previous.dateFrom === next.dateFrom && previous.dateTo === next.dateTo) { + return previous; + } + return next; + }); + }, [customerTimeWindow?.dateFrom, customerTimeWindow?.dateTo]); + const serviceActionQueue = useMemo; detail: string }>>(() => { const items: Array<{ label: string; count: number; filters: Record; detail: string }> = []; if ((serviceSummary?.noDataVehicles ?? 0) > 0) { @@ -873,7 +906,14 @@ export function Dashboard({ } ]; const timeMonitorScopeValue = timeMonitorScope(); - const timeMonitorWindowLabel = dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo); + const usesCustomerTimeWindow = Boolean( + (customerTimeWindow?.dateFrom || customerTimeWindow?.dateTo) + && customerTimeWindow?.dateFrom === timeMonitorScopeValue.dateFrom + && customerTimeWindow?.dateTo === timeMonitorScopeValue.dateTo + ); + const timeMonitorWindowLabel = usesCustomerTimeWindow + ? naturalDayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo) + : dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo); const timeMonitorChecklist = [ { label: '范围可解释', @@ -1003,7 +1043,7 @@ export function Dashboard({ { step: '1', label: '锁定范围', - value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo), + value: timeMonitorWindowLabel, detail: timeMonitorScopeValue.keyword ? `单车 ${timeMonitorScopeValue.keyword}` : '先确认车辆或车辆池,再应用同一时间窗。', action: '应用时间窗', color: timeMonitorHasRange ? 'green' as const : 'orange' as const, 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 6deb5312..e92d657c 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -7324,6 +7324,7 @@ test('topbar customer time window presets support common monitoring ranges', asy fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 近7天' })); expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-06-29'); expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05'); + expect(await screen.findByRole('button', { name: /时间窗服务路径 1 锁定范围 7 天窗口 应用时间窗/ })).toBeInTheDocument(); fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A快捷窗' } }); fireEvent.click(screen.getByRole('button', { name: '查询车辆' }));