From 0c3c6e7b3d503d70085a8edea951d3d54f374067 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 19:28:25 +0800 Subject: [PATCH] feat(platform): add customer time window presets --- vehicle-data-platform/apps/web/src/App.tsx | 45 +++++------ .../apps/web/src/layout/AppShell.tsx | 23 ++++++ .../apps/web/src/styles/global.css | 26 +++++++ .../apps/web/src/test/App.test.tsx | 74 +++++++++++++++++++ 4 files changed, 142 insertions(+), 26 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 74f1d546..dc1270be 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -244,6 +244,7 @@ export default function App() { const updateHistoryFilters = (filters: Record = {}, tab = historyTab) => { const hasKeywordInput = Object.prototype.hasOwnProperty.call(filters, 'keyword'); const nextFilters = normalizeHistoryFilterValues(filters); + setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); setHistoryFilters(nextFilters); if (nextFilters.keyword) { setAnalysisVin(nextFilters.keyword); @@ -271,6 +272,7 @@ export default function App() { const updateMileageFilters = (filters: Record = {}) => { const nextFilters = normalizeMileageFilterValues(filters); + setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); setMileageFilters(nextFilters); if (nextFilters.keyword) { setAnalysisVin(nextFilters.keyword); @@ -302,11 +304,13 @@ export default function App() { }; const updateQualityFilters = (filters: Record = {}) => { + setCustomerTimeWindow(timeWindowFiltersFromValues(filters)); setQualityFilters(filters); replaceQualityHash(filters); }; const openQuality = (filters: Record = {}) => { + setCustomerTimeWindow(timeWindowFiltersFromValues(filters)); setQualityFilters(filters); setActivePage('alert-events'); replaceQualityHash(filters); @@ -364,6 +368,7 @@ export default function App() { const openHistoryWithFilters = (filters: Record = {}) => { const nextFilters = normalizeHistoryFilterValues(filters); + setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); const hasKeywordInput = Object.prototype.hasOwnProperty.call(nextFilters, 'keyword'); const nextVin = nextFilters.keyword?.trim() || (hasKeywordInput ? '' : analysisVin); const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; @@ -399,6 +404,7 @@ export default function App() { const openRawWithFilters = (filters: Record = {}) => { const nextFilters = normalizeHistoryFilterValues(filters); + setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); const hasKeywordInput = Object.prototype.hasOwnProperty.call(nextFilters, 'keyword'); const nextVin = nextFilters.keyword?.trim() || (hasKeywordInput ? '' : analysisVin); const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; @@ -440,6 +446,7 @@ export default function App() { const openMileageWithFilters = (filters: Record = {}) => { const nextFilters = normalizeMileageFilterValues(filters); + setCustomerTimeWindow(timeWindowFiltersFromValues(nextFilters)); const nextVin = nextFilters.keyword?.trim() || analysisVin; const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; setAnalysisVin(nextVin); @@ -453,30 +460,10 @@ export default function App() { const currentCustomerTaskTimeWindow = () => { const globalDateFrom = customerTimeWindow.dateFrom?.trim(); const globalDateTo = customerTimeWindow.dateTo?.trim(); - if (globalDateFrom || globalDateTo) { - return { - ...(globalDateFrom ? { dateFrom: globalDateFrom } : {}), - ...(globalDateTo ? { dateTo: globalDateTo } : {}) - }; - } - const candidates = activePage === 'mileage' - ? [mileageFilters, historyFilters, qualityFilters] - : activePage === 'history' || activePage === 'history-query' - ? [historyFilters, mileageFilters, qualityFilters] - : activePage === 'alert-events' || activePage === 'quality' - ? [qualityFilters, historyFilters, mileageFilters] - : [historyFilters, mileageFilters, qualityFilters]; - for (const filters of candidates) { - const dateFrom = filters.dateFrom?.trim(); - const dateTo = filters.dateTo?.trim(); - if (dateFrom || dateTo) { - return { - ...(dateFrom ? { dateFrom } : {}), - ...(dateTo ? { dateTo } : {}) - }; - } - } - return {}; + return { + ...(globalDateFrom ? { dateFrom: globalDateFrom } : {}), + ...(globalDateTo ? { dateTo: globalDateTo } : {}) + }; }; const openCustomerTask = (page: PageKey) => { @@ -555,9 +542,15 @@ export default function App() { } function timeWindowFiltersFromRoute(route: ReturnType): Record { + return timeWindowFiltersFromValues(route.filters ?? {}); +} + +function timeWindowFiltersFromValues(filters: Record): Record { + const dateFrom = String(filters.dateFrom ?? '').trim(); + const dateTo = String(filters.dateTo ?? '').trim(); return { - ...(route.filters?.dateFrom ? { dateFrom: route.filters.dateFrom } : {}), - ...(route.filters?.dateTo ? { dateTo: route.filters.dateTo } : {}) + ...(dateFrom ? { dateFrom } : {}), + ...(dateTo ? { dateTo } : {}) }; } diff --git a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx index 2e24cf45..39c7ead0 100644 --- a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx @@ -121,6 +121,16 @@ function customerStatusTitle(title?: string) { return title === '来源不完整' ? '数据通道不完整' : title; } +function dateText(offsetDays = 0) { + const date = new Date(); + date.setHours(0, 0, 0, 0); + date.setDate(date.getDate() + offsetDays); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + export function AppShell({ activePage, linkIssueCount, @@ -161,6 +171,13 @@ export function AppShell({ const dateFrom = customerTimeWindow?.dateFrom ?? ''; const dateTo = customerTimeWindow?.dateTo ?? ''; + const applyTimeWindowPreset = (days: number) => { + onCustomerTimeWindowChange?.({ + dateFrom: dateText(-(days - 1)), + dateTo: dateText() + }); + }; + const search = () => { const value = keyword.trim(); if (!value) { @@ -283,6 +300,12 @@ export function AppShell({ onChange={(event) => onCustomerTimeWindowChange?.({ dateFrom, dateTo: event.currentTarget.value })} /> +
+ + + + +
{currentVehicleLabel ? {currentVehicleLabel} : null} {currentVehicleStatus ? ( diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 3bedb584..55507682 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -276,6 +276,32 @@ body { background: #f8fafc; } +.vp-topbar-time-window-presets { + display: inline-flex; + align-items: center; + gap: 2px; + padding-left: 2px; +} + +.vp-topbar-time-window-presets button { + height: 24px; + min-width: 44px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--vp-text-muted); + font-size: 12px; + font-weight: 600; + cursor: pointer; +} + +.vp-topbar-time-window-presets button:hover, +.vp-topbar-time-window-presets button:focus-visible { + background: #ffffff; + color: var(--vp-primary); + outline: none; +} + .vp-topbar-time-window label { display: inline-flex; align-items: center; 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 bb95033c..4b987f46 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4,6 +4,7 @@ import App from '../App'; afterEach(() => { cleanup(); + vi.useRealTimers(); window.history.replaceState(null, '', '/'); delete window.__LINGNIU_APP_CONFIG__; vi.restoreAllMocks(); @@ -6599,6 +6600,79 @@ test('topbar customer time window scopes vehicle service tasks', async () => { expect(window.location.hash).toBe('#/alert-events?keyword=VIN-TOPBAR-WINDOW&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03'); }); +test('topbar customer time window presets support common monitoring ranges', async () => { + vi.setSystemTime(new Date('2026-07-05T10:00:00+08:00')); + window.history.replaceState(null, '', '/#/dashboard'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/vehicle-service/overview')) { + return { + ok: true, + json: async () => ({ + data: { + vin: 'VIN-TOPBAR-PRESET', + plate: '粤A快捷窗', + primaryProtocol: 'GB32960', + protocols: ['GB32960'], + coverageStatus: 'online', + sourceCount: 1, + onlineSourceCount: 1, + realtimeCount: 1, + historyCount: 8, + rawCount: 20, + mileageCount: 3, + qualityIssueCount: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + 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; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 今天' })); + expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-07-05'); + expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05'); + + fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 近7天' })); + expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue('2026-06-29'); + expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue('2026-07-05'); + + fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤A快捷窗' } }); + fireEvent.click(screen.getByRole('button', { name: '查询车辆' })); + expect(await screen.findByText('粤A快捷窗 / VIN-TOPBAR-PRESET')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: '顶部数据导出' })); + expect(window.location.hash).toBe('#/history-query?keyword=VIN-TOPBAR-PRESET&protocol=GB32960&tab=raw&dateFrom=2026-06-29&dateTo=2026-07-05'); + + fireEvent.click(screen.getByRole('button', { name: '顶部时间窗 清空' })); + expect(screen.getByLabelText('顶部时间窗开始')).toHaveValue(''); + expect(screen.getByLabelText('顶部时间窗结束')).toHaveValue(''); + + fireEvent.click(screen.getByRole('button', { name: '顶部统计查询' })); + expect(window.location.hash).toBe('#/mileage?keyword=VIN-TOPBAR-PRESET&protocol=GB32960'); +}); + test('shows and clears current history filters while keeping vehicle scope', async () => { window.history.replaceState(null, '', '/#/history-query?keyword=VIN-HISTORY-001&protocol=JT808&dateFrom=2026-07-01+00%3A00%3A00&dateTo=2026-07-01+23%3A59%3A59&includeFields=true&fields=jt808.location.longitude%2Cjt808.location.latitude&tab=raw'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {