import { useMutation, useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; import { IconCalendar, IconChevronRight, IconClose, IconDownload, IconMapPin, IconMore, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons'; import { Button, Card, CardGroup, Checkbox, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui'; import { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { HistoryDataResponse, HistoryDataRow, HistoryExportJob, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types'; import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history'; import { downloadBlob } from '../domain/download'; import { formatShanghaiDateTime } from '../domain/formatters'; import { InlineError, PanelEmpty, PanelLoading, WorkspaceEmptyGuide } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet'; import { PlatformTime } from '../shared/PlatformTime'; import { ProtocolTag } from '../shared/ProtocolTag'; import { TablePagination } from '../shared/TablePagination'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { normalizeWorkspaceDateTimeRange, WorkspaceDateTimeRange, workspaceDateTimePresetWindow } from '../shared/WorkspaceDateTimeRange'; import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet'; import { detailTriggerRow } from '../shared/detailTriggerRow'; import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy'; import { usePlatformSession } from '../auth/AuthGate'; import { canOperate } from '../auth/session'; import { monitorReturnFromParams, preserveMonitorReturn } from '../routing/monitorContext'; import { useMobileLayout } from '../hooks/useMobileLayout'; const DESKTOP_HISTORY_PAGE_SIZE = 50; const MOBILE_HISTORY_PAGE_SIZE = 20; const historyAxisTimeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); function historyQualityLabel(quality: string) { if (quality === 'normal' || quality === 'good') return '正常'; if (quality === 'warning') return '需关注'; if (quality === 'error' || quality === 'critical') return '异常'; return quality || '未知'; } function isHealthyHistoryQuality(quality: string) { return quality === 'normal' || quality === 'good'; } function HistoryQualityTag({ quality }: { quality: string }) { const color = isHealthyHistoryQuality(quality) ? 'green' : quality === 'error' || quality === 'critical' ? 'red' : 'orange'; return {historyQualityLabel(quality)}; } function HistoryTrend({ response, category, loading, error, hasMetrics, expanded, onToggle }: { response?: HistorySeriesResponse; category: string; loading: boolean; error?: string; hasMetrics: boolean; expanded: boolean; onToggle: () => void; }) { const panels = useMemo(() => buildHistorySeriesPanels(response), [response]); const summary = response?.summary; const coverage = summary?.expectedBucketCount ? Math.max(0, (summary.expectedBucketCount - summary.missingBucketCount) / summary.expectedBucketCount * 100) : 0; const description = category === 'raw' ? '原始报文通过明细和导出核验证据' : category === 'mileage' ? '日里程按自然日通过明细核对' : '按查询时间窗汇总连续指标'; const coverageColor = coverage >= 95 ? 'green' : coverage >= 70 ? 'orange' : 'red'; const header = 覆盖 {coverage.toFixed(1)}% {formatSeriesGrain(summary.grainSeconds)}粒度 · {summary.rawPointCount.toLocaleString('zh-CN')} 原始点 : undefined} actions={} />; if (!expanded) return null; if (category !== 'location') return {header}; if (!hasMetrics) return {header}; return {header} {error ? : loading && !response ?
: panels.length ?
{panels.map((panel) =>
{panel.label}{panel.unit || '数值'} · {panel.lines.reduce((sum, line) => sum + line.points, 0)} 个桶点
{panel.maximum.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}{panel.minimum.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}{formatAxisTime(panel.start)}{formatAxisTime(panel.end)} {panel.lines.flatMap((line) => line.paths.map((path, index) => {line.label}))}
{panel.lines.map((line) => {line.label})}
)}
: } {summary ?
{summary.missingBucketCount ? '存在空窗' : '时间窗完整'} {summary.evidence} 缺失 {summary.missingBucketCount.toLocaleString('zh-CN')} / {summary.expectedBucketCount.toLocaleString('zh-CN')} 桶 查询 {summary.queryDurationMs} ms
: null}
; } export function formatAxisTime(value: string) { const parsed = new Date(value); if (!Number.isFinite(parsed.getTime())) return value.replace('T', ' ').slice(5, 16); return historyAxisTimeFormatter.format(parsed).replace('/', '-'); } export function formatHistoryDateTime(value: string) { return formatShanghaiDateTime(value); } function currentHistoryWindow() { return historyPresetWindow('today'); } type HistoryRangePreset = 'today' | 'yesterday' | 'last6Hours' | 'last24Hours'; export function historyPresetWindow(preset: HistoryRangePreset, now = new Date()) { return workspaceDateTimePresetWindow(preset, now); } export function historyDateTimePickerRange( dateValue?: Date | Date[] | string | string[], formattedValue?: string | string[] | Date | Date[] ) { return normalizeWorkspaceDateTimeRange(dateValue, formattedValue); } function CreateExportButton({ request, disabled }: { request: HistoryExportRequest; disabled: boolean }) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: api.createHistoryExport, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['history-exports'] }) }); const label = mutation.isPending ? '任务排队中' : mutation.isError ? '导出失败,重试' : '创建导出'; return ; } function ExportDownloadButton({ id }: { id: string }) { const mutation = useMutation({ mutationFn: () => api.downloadHistoryExport(id), onSuccess: ({ blob, filename }) => downloadBlob(blob, filename) }); return ; } function useHistoryExportJobs(enabled = true) { return useQuery({ queryKey: ['history-exports'], queryFn: ({ signal }) => api.historyExports(signal), enabled, refetchInterval: (current) => historyExportPollInterval(current.state.data), ...LIVE_QUERY_POLICY, gcTime: QUERY_MEMORY.highVolumeGcTime }); } function ExportJobsPanel({ query, showHeader = true }: { query: UseQueryResult; showHeader?: boolean }) { const jobs = query.data ?? []; const statusLabel = (status: string) => status === 'queued' ? '排队中' : status === 'running' ? '执行中' : status === 'completed' ? '已完成' : '失败'; const statusColor = (status: string) => status === 'completed' ? 'green' : status === 'running' ? 'blue' : status === 'failed' ? 'red' : 'grey'; return {showHeader ? : null}
{query.isPending ?
: query.isError ? : !jobs.length ? : {jobs.slice(0, 6).map((job) => {job.name}} headerExtraContent={{statusLabel(job.status)}} headerLine bodyStyle={{ padding: 0 }}>
{job.status === 'queued' ? '等待单并发执行' : job.status === 'running' ? `${job.processedRows.toLocaleString('zh-CN')} / ${job.totalRows.toLocaleString('zh-CN')} 行` : job.status === 'completed' ? `${job.rowCount.toLocaleString('zh-CN')} 行 · ${formatExportFileSize(job.fileSizeBytes)}` : job.error || '任务失败'} {job.ownerUsername || job.ownerName || '历史任务'} · {job.vehicleVins?.length ?? job.keywords.length} 辆{job.downloadUrl ? : null} {job.dateFrom && job.dateTo ? `${formatHistoryDateTime(job.dateFrom)} 至 ${formatHistoryDateTime(job.dateTo)}` : <>创建于 }
{job.status === 'running' ? : null}
)}
}
; } function ExportJobsWorkspace({ visible, mobileLayout, onClose }: { visible: boolean; mobileLayout: boolean; onClose: () => void }) { const query = useHistoryExportJobs(visible); const jobs = query.data ?? []; const activeJobs = jobs.filter((job) => job.status === 'queued' || job.status === 'running').length; const completedJobs = jobs.filter((job) => job.status === 'completed').length; const failedJobs = jobs.filter((job) => job.status === 'failed').length; const latestUpdatedAt = jobs.reduce((latest, job) => { if (!latest) return job.updatedAt; return Date.parse(job.updatedAt) > Date.parse(latest) ? job.updatedAt : latest; }, undefined); const badge = query.isFetching ? '同步中' : `${jobs.length.toLocaleString('zh-CN')} 个任务`; const badgeColor = failedJobs ? 'red' : activeJobs ? 'blue' : completedJobs ? 'green' : 'grey'; return } badge={badge} badgeColor={badgeColor} summaryItems={[ { label: '生成中', value: activeJobs, detail: '排队与执行', tone: activeJobs ? 'primary' : 'neutral' }, { label: '可下载', value: completedJobs, detail: '已完成任务', tone: 'success' }, { label: '需处理', value: failedJobs, detail: '失败任务', tone: failedJobs ? 'danger' : 'neutral' } ]} footerNote={query.isError ? '任务状态读取失败,请手动刷新' : latestUpdatedAt ? <>最近更新 : '打开后自动同步任务状态'} secondaryActions={[{ label: '刷新任务', ariaLabel: '刷新导出任务', icon: , loading: query.isFetching, onClick: () => { void query.refetch(); } }]} primaryAction={{ label: '完成', onClick: onClose }} onCancel={onClose} > {visible ? : null} ; } function EvidencePanel({ row, metrics, onClose, showHeader = true }: { row?: HistoryDataRow; metrics: HistoryMetricDefinition[]; onClose: () => void; showHeader?: boolean }) { return {showHeader ? } onClick={onClose} aria-label="关闭数据详情" /> : null} /> : null} {row ? <>
证据判读 {historyQualityLabel(row.quality)} {row.qualityReason || '平台未返回质量说明'}
}, { key: '服务时间', value: }, { key: '数据来源', value: }, { key: '数据质量', value: }, { key: '质量原因', value: row.qualityReason || '平台未返回质量说明' } ]} />
已选字段{metrics.length}
{metrics.length ? ({ key: {metric.label}{metric.key}, value: formatHistoryValue(row.values[metric.key], metric) }))} /> : } {row.evidenceId ?
RAW 证据{row.evidenceId}
: null} : }
; } function ColumnVisibilityPanel({ metrics, visible, visibleKeys, onToggle, onShowAll, onReset, onClose }: { metrics: HistoryMetricDefinition[]; visible: boolean; visibleKeys: string[]; onToggle: (key: string) => void; onShowAll: () => void; onReset: () => void; onClose: () => void; }) { const mobileLayout = useMobileLayout(); const [search, setSearch] = useState(''); const filteredMetrics = useMemo(() => { const keyword = search.trim().toLowerCase(); if (!keyword) return metrics; return metrics.filter((metric) => `${metric.label} ${metric.key} ${metric.unit} ${metric.description ?? ''} ${metric.valueMappings?.map((item) => `${item.value} ${item.label}`).join(' ') ?? ''}`.toLowerCase().includes(keyword)); }, [metrics, search]); const displayedMetrics = filteredMetrics.slice(0, 200); const defaultMetricCount = useMemo(() => metrics.filter((metric) => metric.defaultVisible).length, [metrics]); return
{displayedMetrics.map((metric) => )}{!metrics.length ?

当前数据类型没有可配置指标

: search && !filteredMetrics.length ?

没有匹配字段

: filteredMetrics.length > displayedMetrics.length ?

当前仅展示前 200 项,请输入字段名或 key 缩小范围。

: null}
; } function HistoryDataTable({ rows, metrics, selectedRowID, onSelect }: { rows: HistoryDataRow[]; metrics: HistoryMetricDefinition[]; selectedRowID?: string; onSelect: (row: HistoryDataRow) => void }) { const columns = useMemo(() => [ { title: '', dataIndex: 'selection', width: 42, fixed: 'left' as const, className: 'v2-history-selection-column', render: (_: unknown, row: HistoryDataRow) => onSelect(row)} aria-label={`选择 ${row.plate || row.vin} ${row.deviceTime}`} /> }, { title: '设备时间', dataIndex: 'deviceTime', width: 150, fixed: 'left' as const, className: 'v2-history-device-time-column', render: (value: string) => }, { title: '车牌', dataIndex: 'plate', width: 110, fixed: 'left' as const, className: 'v2-history-plate-column', render: (value: string) => value || '—' }, { title: '服务时间', dataIndex: 'serverTime', width: 150, render: (value: string) => }, { title: 'VIN', dataIndex: 'vin', width: 160, render: (value: string) => {value} }, { title: '协议', dataIndex: 'protocol', width: 118, render: (value: string) => }, ...metrics.map((metric) => ({ title: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}`, dataIndex: metric.key, key: metric.key, width: 120, render: (_: unknown, row: HistoryDataRow) => formatHistoryValue(row.values[metric.key], metric) })), { title: '质量', dataIndex: 'quality', width: 90, render: (_: string, row: HistoryDataRow) => {historyQualityLabel(row.quality)} }, { title: '质量原因', dataIndex: 'qualityReason', width: 220, className: 'v2-history-quality-reason', render: (value: string) => {value || '—'} }, { title: '操作', dataIndex: 'action', width: 90, render: (_: unknown, row: HistoryDataRow) => } ], [metrics, onSelect, selectedRowID]); const minWidth = 1112 + metrics.length * 120; return row ? detailTriggerRow({ className: selectedRowID === row.id ? 'is-selected' : '', expanded: selectedRowID === row.id, label: `查看 ${row.plate || row.vin} ${row.deviceTime} 数据详情`, testId: `history-row-${row.id}`, onOpen: () => onSelect(row) }) : ({})} />; } export default function HistoryPage() { const { session } = usePlatformSession(); const exportAllowed = canOperate(session) || session.role === 'customer'; const [searchParams, setSearchParams] = useSearchParams(); const monitorReturn = monitorReturnFromParams(searchParams); const today = useMemo(currentHistoryWindow, []); const initial = { keywords: searchParams.get('vin') || searchParams.get('keywords') || '', dateFrom: searchParams.get('dateFrom') || today.dateFrom, dateTo: searchParams.get('dateTo') || today.dateTo, category: searchParams.get('category') || 'location', protocol: searchParams.get('protocol') || '' }; const [draft, setDraft] = useState(initial); const [criteria, setCriteria] = useState(initial); const [offset, setOffset] = useState(0); const mobileLayout = useMobileLayout(); const [limit, setLimit] = useState(() => mobileLayout ? MOBILE_HISTORY_PAGE_SIZE : DESKTOP_HISTORY_PAGE_SIZE); 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 [trendExpanded, setTrendExpanded] = useState(false); const [filtersCollapsed, setFiltersCollapsed] = useState(true); const [exportJobsOpen, setExportJobsOpen] = useState(false); const [toolsOpen, setToolsOpen] = useState(false); useEffect(() => { if (!mobileLayout) return; setLimit(MOBILE_HISTORY_PAGE_SIZE); setOffset(0); }, [mobileLayout]); 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) }); if (criteria.dateFrom) next.set('dateFrom', criteria.dateFrom); if (criteria.dateTo) next.set('dateTo', criteria.dateTo); if (criteria.protocol) next.set('protocol', criteria.protocol); return next; }, [criteria, keywords, limit, offset]); const dataScope = useMemo(() => { const scope = new URLSearchParams(params); scope.delete('limit'); scope.delete('offset'); return scope.toString(); }, [params]); const catalogQuery = useQuery({ queryKey: ['history-metric-catalog'], queryFn: ({ signal }) => api.historyMetricCatalog(signal), staleTime: 30 * 60_000 }); const dataQuery = useQuery({ queryKey: ['history-data', dataScope, limit, offset], enabled: keywords.length > 0, queryFn: ({ signal }) => api.historyData(params, signal), placeholderData: retainPreviousPageWithinScope(dataScope), gcTime: QUERY_MEMORY.highVolumeGcTime }); const result = dataQuery.data; const resultRows = result?.rows ?? []; const resultSummary = result?.summary; const allMetrics = result?.columns ?? catalogQuery.data?.metrics.filter((metric) => metric.category === criteria.category) ?? []; const visibleKeys = visibleByCategory[criteria.category] ?? allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key); const visibleMetrics = allMetrics.filter((metric) => visibleKeys.includes(metric.key)); const seriesMetricKey = visibleKeys.filter((key) => key === 'speedKmh' || key === 'totalMileageKm').join(','); const seriesParams = useMemo(() => { const next = new URLSearchParams({ keywords: keywords.join(','), category: criteria.category, metrics: seriesMetricKey, targetPoints: '240' }); if (criteria.dateFrom) next.set('dateFrom', criteria.dateFrom); if (criteria.dateTo) next.set('dateTo', criteria.dateTo); if (criteria.protocol) next.set('protocol', criteria.protocol); return next; }, [criteria, keywords, seriesMetricKey]); const seriesQuery = useQuery({ queryKey: ['history-series', seriesParams.toString()], enabled: trendExpanded && keywords.length > 0 && criteria.category === 'location' && Boolean(seriesMetricKey), queryFn: ({ signal }) => api.historySeries(seriesParams, signal), gcTime: QUERY_MEMORY.highVolumeGcTime }); const scopedSelectedRowRef = selectedRowRef?.scope === dataScope ? selectedRowRef : undefined; const selectedRow = scopedSelectedRowRef?.id === '' ? undefined : scopedSelectedRowRef ? resultRows.find((row) => row.id === scopedSelectedRowRef.id) : undefined; const selectRow = useCallback((row: HistoryDataRow) => { setSelectedRowRef({ scope: dataScope, id: row.id }); }, [dataScope]); const closeSelectedRow = useCallback(() => { setSelectedRowRef({ scope: dataScope, id: '' }); }, [dataScope]); const mobileFiltersOpen = mobileLayout && !filtersCollapsed; const draftVehicleCount = parseHistoryKeywords(draft.keywords).length; const draftCategoryLabel = catalogQuery.data?.categories.find((item) => item.key === draft.category)?.label ?? (draft.category === 'raw' ? '原始报文' : draft.category === 'mileage' ? '日里程' : '位置数据'); const draftSourceLabel = draft.protocol || '全部来源'; const draftWindowLabel = `${draft.dateFrom.slice(5, 10)}–${draft.dateTo.slice(5, 10)}`; const historyRangePresets = useMemo(() => [ { key: 'today' as const, label: '今天', range: historyPresetWindow('today') }, { key: 'yesterday' as const, label: '昨天', range: historyPresetWindow('yesterday') }, { key: 'last6Hours' as const, label: '近 6 小时', range: historyPresetWindow('last6Hours') }, { key: 'last24Hours' as const, label: '近 24 小时', range: historyPresetWindow('last24Hours') } ], []); const applyDraft = () => { const parsed = parseHistoryKeywords(draft.keywords); if (!parsed.length) return; const next = { ...draft, keywords: parsed.join(',') }; setCriteria(next); setOffset(0); setFiltersCollapsed(true); const url = new URLSearchParams({ keywords: next.keywords, category: next.category }); if (next.dateFrom) url.set('dateFrom', next.dateFrom); if (next.dateTo) url.set('dateTo', next.dateTo); if (next.protocol) url.set('protocol', next.protocol); setSearchParams(preserveMonitorReturn(url, monitorReturn), { replace: true }); }; const submit = (event: FormEvent) => { event.preventDefault(); applyDraft(); }; const resetDraft = () => setDraft({ keywords: '', ...currentHistoryWindow(), category: 'location', protocol: '' }); const reset = () => { const next = { keywords: '', ...currentHistoryWindow(), category: 'location', protocol: '' }; setDraft(next); setCriteria(next); setOffset(0); setFiltersCollapsed(true); setSearchParams(preserveMonitorReturn(new URLSearchParams(), monitorReturn), { replace: true }); }; const toggleMetric = (key: string) => setVisibleByCategory((current) => { const baseline = current[criteria.category] ?? allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key); 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 scrollHistoryTable = (event: KeyboardEvent) => { if (mobileLayout || event.currentTarget !== event.target || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return; const tableScroller = event.currentTarget.querySelector('.semi-table-body'); if (!tableScroller) return; event.preventDefault(); tableScroller.scrollBy({ left: event.key === 'ArrowRight' ? 260 : -260, behavior: 'smooth' }); }; const totalPages = Math.max(1, Math.ceil((result?.total ?? 0) / limit)); const page = Math.floor(offset / limit) + 1; const summarySources = resultSummary?.sources?.join('、') || '—'; const summaryLoading = keywords.length > 0 && dataQuery.isFetching && !result; const historyMetrics: WorkspaceQueueMetricRailItem[] = [ { label: '结果行数', value: summaryLoading ? '—' : (result?.total ?? 0).toLocaleString('zh-CN'), note: summaryLoading ? '正在读取' : '当前范围', tone: 'primary', emphasis: 'primary' }, { label: '车辆数', value: summaryLoading ? '—' : <>{resultSummary?.vehicleCount ?? 0}, note: `${keywords.length} 个车辆标识`, tone: 'success', emphasis: 'primary' }, { label: '数据来源', value: summaryLoading ? '—' : <>{resultSummary?.sources?.length ?? 0}, note: summarySources, tone: 'primary', emphasis: 'secondary' } ]; const historyFilterFields = <> setDraft((current) => ({ ...current, ...range }))} presets={historyRangePresets} /> ; const exportRequest: HistoryExportRequest = { keywords, category: criteria.category, protocol: criteria.protocol || undefined, dateFrom: criteria.dateFrom, dateTo: criteria.dateTo, metrics: visibleKeys, format: 'csv' }; return
上海时区 · 最多 5 台车辆} /> {mobileLayout ?
setFiltersCollapsed((value) => !value)} /> setFiltersCollapsed(true)} secondaryAction={{ label: '重置条件', onClick: resetDraft }} primaryAction={{ label: '应用并查询', disabled: !parseHistoryKeywords(draft.keywords).length, onClick: applyDraft }} >
{historyFilterFields}
{exportAllowed ?
导出当前结果按已应用范围与当前字段生成 CSV
: 只读}
: setFiltersCollapsed((value) => !value)} >
{historyFilterFields}
{!exportAllowed ? 只读 : null}
}
{keywords.length ? : null} {dataQuery.isError ? dataQuery.refetch()} /> : null}
setTrendExpanded((value) => !value)} /> {visibleMetrics.length} / {allMetrics.length} 字段{resultSummary ? 查询 {resultSummary.queryDurationMs} ms : null}} actionsClassName={`v2-history-result-actions${mobileLayout ? ' is-mobile-actions' : ''}`} actions={mobileLayout ? keywords.length ? <> } onClick={() => { setToolsOpen(false); setColumnSettingsOpen(true); }}>字段设置 {exportAllowed ? } onClick={() => { setToolsOpen(false); setExportJobsOpen(true); }}>导出任务 : null} } onClick={() => { setToolsOpen(false); void dataQuery.refetch(); }}>刷新数据 } > : null : <> {exportAllowed ? : null} {exportAllowed ? } onClick={() => { setToolsOpen(false); setExportJobsOpen(true); }}>导出任务 : null} } onClick={() => { setToolsOpen(false); setDensity((value) => value === 'compact' ? 'comfortable' : 'compact'); }}>{density === 'compact' ? '切换为舒适密度' : '切换为紧凑密度'} } onClick={() => { setToolsOpen(false); void dataQuery.refetch(); }}>刷新数据 } > } /> setVisibleMetrics(allMetrics.map((metric) => metric.key))} onReset={() => setVisibleMetrics(allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key))} onClose={() => setColumnSettingsOpen(false)} />
{mobileLayout ?
{resultRows.map((row) => )}
: resultRows.length ? : null} {dataQuery.isPending && keywords.length ? : !resultRows.length ? keywords.length ? } aria-label="调整查询范围" onClick={() => setFiltersCollapsed(false)}>调整查询范围} /> : } title="选择车辆,开始核验历史证据" description="一次最多查询 5 台车辆,结果会保留协议来源、质量说明与 RAW 证据。" steps={[ { icon: , title: '选择车辆', description: '支持车牌、VIN 与批量粘贴' }, { icon: , title: '确认时间', description: '按上海时区精确到分钟' }, { icon: , title: '核验数据', description: '位置、RAW 与日里程统一检索' } ]} action={} /> : null}
{keywords.length ?
setOffset((next - 1) * limit)} pageSize={mobileLayout ? undefined : limit} onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={mobileLayout ? undefined : [{ value: 20, label: '20 条/页' }, { value: 50, label: '50 条/页' }, { value: 100, label: '100 条/页' }]} />
: null}
{!mobileLayout && selectedRow ? : null}
{mobileLayout && selectedRow ? : null} {exportAllowed ? setExportJobsOpen(false)} /> : null}
; }