From 7cf856fd51a7356bae424e59490fba13a07fa6df Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 16 Jul 2026 08:03:59 +0800 Subject: [PATCH] fix(history): activate column visibility settings --- .../web/src/v2/pages/HistoryPage.test.tsx | 30 +++++++++++++++++++ .../apps/web/src/v2/pages/HistoryPage.tsx | 24 ++++++++++++++- .../apps/web/src/v2/styles/v2.css | 6 +++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx index 6437db47..8de4ad89 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx @@ -22,6 +22,7 @@ afterEach(() => { }); const metric = { key: 'speedKmh', label: '速度', unit: 'km/h', category: 'location', valueType: 'number', defaultVisible: true }; +const optionalMetric = { key: 'socPercent', label: 'SOC', unit: '%', category: 'location', valueType: 'number', defaultVisible: false }; function historyData(vin: string, plate: string, asOf: string): HistoryDataResponse { return { @@ -91,3 +92,32 @@ test('releases export job state immediately after leaving the history page', asy unmount(); await waitFor(() => expect(client.getQueryCache().find({ queryKey: ['history-exports'] })).toBeUndefined()); }); + +test('opens a working column visibility panel and preserves non-hideable identity columns', async () => { + const data = historyData('OLDVIN', '旧车牌', 'old-as-of'); + data.columns = [metric, optionalMetric]; + data.rows[0].values.socPercent = 88; + mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric, optionalMetric] }); + mocks.historyData.mockResolvedValue(data); + mocks.historySeries.mockResolvedValue(historySeries('OLDVIN', '旧车牌', 'old-as-of')); + mocks.historyExports.mockResolvedValue([]); + renderPage(); + + expect(await screen.findByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument(); + expect(screen.queryByRole('columnheader', { name: 'SOC (%)' })).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '列设置' })); + + expect(screen.getByRole('dialog', { name: '列显示设置' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('checkbox', { name: /速度/ })); + expect(screen.queryByRole('columnheader', { name: '速度 (km/h)' })).not.toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'VIN' })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: '显示全部' })); + expect(screen.getByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'SOC (%)' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '恢复默认' })); + expect(screen.getByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument(); + expect(screen.queryByRole('columnheader', { name: 'SOC (%)' })).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '关闭列显示设置' })); + expect(screen.queryByRole('dialog', { name: '列显示设置' })).not.toBeInTheDocument(); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx index 6bb8349f..5f91332e 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -59,6 +59,26 @@ function EvidencePanel({ row, metrics, onClose }: { row?: HistoryDataRow; metric ; } +function ColumnVisibilityPanel({ metrics, visibleKeys, onToggle, onShowAll, onReset, onClose }: { + metrics: HistoryMetricDefinition[]; + visibleKeys: string[]; + onToggle: (key: string) => void; + onShowAll: () => void; + onReset: () => void; + onClose: () => void; +}) { + useEffect(() => { + const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; + window.addEventListener('keydown', closeOnEscape); + return () => window.removeEventListener('keydown', closeOnEscape); + }, [onClose]); + return ; +} + export default function HistoryPage() { const [searchParams, setSearchParams] = useSearchParams(); const today = useMemo(currentHistoryWindow, []); @@ -70,6 +90,7 @@ export default function HistoryPage() { const [visibleByCategory, setVisibleByCategory] = useState>({}); const [selectedRowRef, setSelectedRowRef] = useState<{ scope: string; id: string }>(); const [density, setDensity] = useState<'compact' | 'comfortable'>('compact'); + const [columnSettingsOpen, setColumnSettingsOpen] = useState(false); const keywords = useMemo(() => parseHistoryKeywords(criteria.keywords), [criteria.keywords]); const params = useMemo(() => { const next = new URLSearchParams({ keywords: keywords.join(','), category: criteria.category, limit: String(limit), offset: String(offset) }); @@ -129,6 +150,7 @@ export default function HistoryPage() { const next = baseline.includes(key) ? baseline.filter((item) => item !== key) : [...baseline, key]; return { ...current, [criteria.category]: next }; }); + const setVisibleMetrics = (keys: string[]) => setVisibleByCategory((current) => ({ ...current, [criteria.category]: keys })); const totalPages = Math.max(1, Math.ceil((result?.total ?? 0) / limit)); const page = Math.floor(offset / limit) + 1; @@ -147,7 +169,7 @@ export default function HistoryPage() {
结果行数{result?.total.toLocaleString('zh-CN') ?? '—'}
车辆数{result?.summary.vehicleCount ?? '—'}
数据源{result?.summary.sources.join('、') || '—'}
查询耗时{result ? `${result.summary.queryDurationMs} ms` : '—'}
-
数据明细
{visibleMetrics.map((metric) => )}{result?.rows.map((row) => {visibleMetrics.map((metric) => )})}
设备时间服务时间车牌VIN协议{metric.label}{metric.unit ? ` (${metric.unit})` : ''}质量操作
setSelectedRowRef(selectedRow?.id === row.id ? undefined : { scope: dataScope, id: row.id })} aria-label={`选择 ${row.plate || row.vin} ${row.deviceTime}`} />{row.deviceTime}{row.serverTime}{row.plate || '—'}{row.vin}{row.protocol}{formatHistoryValue(row.values[metric.key], metric)}{row.quality === 'normal' ? '正常' : row.quality}
{dataQuery.isPending && keywords.length ?
正在加载当前筛选范围的历史数据…
: !result?.rows.length ?
{keywords.length ? '当前条件没有历史记录' : '输入车辆并查询历史数据'}
: null}
第 {page} / {totalPages} 页,共 {result?.total ?? 0} 条
+
数据明细
{columnSettingsOpen ? setVisibleMetrics(allMetrics.map((metric) => metric.key))} onReset={() => setVisibleMetrics(allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key))} onClose={() => setColumnSettingsOpen(false)} /> : null}
{visibleMetrics.map((metric) => )}{result?.rows.map((row) => {visibleMetrics.map((metric) => )})}
设备时间服务时间车牌VIN协议{metric.label}{metric.unit ? ` (${metric.unit})` : ''}质量操作
setSelectedRowRef(selectedRow?.id === row.id ? undefined : { scope: dataScope, id: row.id })} aria-label={`选择 ${row.plate || row.vin} ${row.deviceTime}`} />{row.deviceTime}{row.serverTime}{row.plate || '—'}{row.vin}{row.protocol}{formatHistoryValue(row.values[metric.key], metric)}{row.quality === 'normal' ? '正常' : row.quality}
{dataQuery.isPending && keywords.length ?
正在加载当前筛选范围的历史数据…
: !result?.rows.length ?
{keywords.length ? '当前条件没有历史记录' : '输入车辆并查询历史数据'}
: null}
第 {page} / {totalPages} 页,共 {result?.total ?? 0} 条
diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css index 7b84a99c..a51df8fb 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css @@ -555,11 +555,15 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-chart-axis text:first-child, .v2-chart-axis text:nth-child(2) { text-anchor: end; } .v2-history-chart-empty { display: flex; flex: 1; align-items: center; justify-content: center; color: var(--v2-muted); font-size: 9px; } .v2-history-trend-evidence { display: block; flex: 0 0 auto; overflow: hidden; border-top: 1px solid #eef2f7; padding: 4px 10px; color: var(--v2-muted); font-size: 7px; text-overflow: ellipsis; white-space: nowrap; } -.v2-history-table-card { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; border: 1px solid var(--v2-border); border-radius: var(--v2-radius); background: #fff; box-shadow: var(--v2-shadow); } +.v2-history-table-card { position: relative; display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; border: 1px solid var(--v2-border); border-radius: var(--v2-radius); background: #fff; box-shadow: var(--v2-shadow); } .v2-history-table-card > header { display: flex; min-height: 38px; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--v2-border); padding: 0 10px 0 12px; } .v2-history-table-card > header strong { font-size: 10px; } .v2-history-table-card > header div { display: flex; gap: 6px; } .v2-history-table-card > header button, .v2-history-table-card > header select { display: inline-flex; height: 27px; align-items: center; gap: 5px; border: 1px solid #dfe6ef; border-radius: 5px; background: #fff; padding: 0 8px; color: #657286; cursor: pointer; font-size: 8px; } +.v2-history-column-panel { position: absolute; z-index: 15; top: 37px; right: 9px; display: flex; width: min(340px, calc(100% - 18px)); max-height: min(390px, calc(100% - 48px)); flex-direction: column; overflow: hidden; border: 1px solid #d8e2ee; border-radius: 8px; background: #fff; box-shadow: 0 14px 34px rgba(30, 47, 70, .18); } +.v2-history-column-panel > header { display: flex; flex: 0 0 auto; align-items: center; justify-content: space-between; border-bottom: 1px solid #e8edf4; padding: 10px 11px; }.v2-history-column-panel > header div { display: grid; gap: 3px; }.v2-history-column-panel > header strong { color: #35445a; font-size: 10px; }.v2-history-column-panel > header span { color: #8591a2; font-size: 8px; }.v2-history-column-panel > header button { display: grid; width: 25px; height: 25px; place-items: center; border: 0; border-radius: 5px; background: #f2f5f9; color: #68768a; cursor: pointer; } +.v2-history-column-panel > div { display: grid; min-height: 0; overflow: auto; padding: 5px 8px; }.v2-history-column-panel label { display: grid; min-height: 36px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 8px; border-bottom: 1px solid #f0f3f7; padding: 0 4px; color: #4d5b6f; font-size: 9px; cursor: pointer; }.v2-history-column-panel label input { margin: 0; accent-color: var(--v2-blue); }.v2-history-column-panel label small { max-width: 130px; overflow: hidden; color: #929dac; font-size: 7px; text-overflow: ellipsis; white-space: nowrap; }.v2-history-column-panel > div > p { margin: 18px 8px; color: var(--v2-muted); text-align: center; font-size: 9px; } +.v2-history-column-panel > footer { display: flex; flex: 0 0 auto; justify-content: flex-end; gap: 6px; border-top: 1px solid #e8edf4; padding: 8px 10px; }.v2-history-column-panel > footer button { height: 27px; border: 1px solid #dce4ee; border-radius: 5px; background: #fff; padding: 0 9px; color: #5f6e83; cursor: pointer; font-size: 8px; }.v2-history-column-panel > footer button:disabled { opacity: .4; cursor: not-allowed; } .v2-history-table-scroll { position: relative; min-height: 0; flex: 1; overflow: auto; overscroll-behavior: contain; } .v2-history-table-scroll table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; color: #4e5b6f; font-size: 8px; } .v2-history-table-scroll th { position: sticky; z-index: 3; top: 0; height: 31px; border-bottom: 1px solid var(--v2-border); background: #f8fafc; color: #607086; text-align: left; white-space: nowrap; font-weight: 700; }