diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx index c7f47883..97ca439c 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx @@ -24,6 +24,16 @@ const vehicleCardArgsSpy = vi.hoisted(() => vi.fn()); const monitorDataArgsSpy = vi.hoisted(() => vi.fn()); const qrToDataURLSpy = vi.hoisted(() => vi.fn()); const monitorQueryFlags = vi.hoisted(() => ({ isLoading: false, isFetching: false, isPlaceholderData: false })); +const monitorSummaryFixture = vi.hoisted(() => ({ + totalVehicles: 2, + locationVehicles: 2, + noLocationVehicles: 0, + onlineVehicles: 2, + offlineVehicles: 0, + drivingVehicles: 1, + idleVehicles: 1, + frameToday: 10 +})); const vehicleCardFixture = vi.hoisted(() => ({ detail: undefined as unknown })); const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard'); @@ -61,7 +71,7 @@ vi.mock('../hooks/useMonitorData', () => ({ useMonitorData: (...args: unknown[]) => { monitorDataArgsSpy(...args); return { - summary: { data: { totalVehicles: 2, locationVehicles: 2, noLocationVehicles: 0, onlineVehicles: 2, offlineVehicles: 0, drivingVehicles: 1, idleVehicles: 1, frameToday: 10 } }, + summary: { data: monitorSummaryFixture }, vehicles: { data: { items: vehicles, total: 2 }, isError: false, ...monitorQueryFlags }, map: { data: monitorMap, isPlaceholderData: monitorQueryFlags.isPlaceholderData }, selectedVehicle: { data: { items: [] } } @@ -78,6 +88,7 @@ afterEach(() => { monitorQueryFlags.isLoading = false; monitorQueryFlags.isFetching = false; monitorQueryFlags.isPlaceholderData = false; + monitorSummaryFixture.frameToday = 10; vehicleCardFixture.detail = undefined; vehicleCardArgsSpy.mockClear(); monitorDataArgsSpy.mockClear(); @@ -435,6 +446,7 @@ test('mounts only the mobile list representation and removes its viewport listen dispatchEvent: vi.fn(() => false) } as unknown as MediaQueryList); vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [vehicles[0]], total: 1, limit: 50, offset: 0 }); + monitorSummaryFixture.frameToday = 2_861_323; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); @@ -452,8 +464,14 @@ test('mounts only the mobile list representation and removes its viewport listen fireEvent.click(screen.getByRole('tab', { name: /列表/ })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-cards .v2-monitor-mobile-card.semi-card')).toHaveLength(1)); + const mobileCard = view.container.querySelector('.v2-monitor-mobile-card'); + expect(mobileCard).toHaveAttribute('aria-label', '粤A12345 实时数据'); + expect(mobileCard?.querySelector('.v2-monitor-mobile-primary')).toHaveTextContent('当日里程18.6 km总里程1,234 km协议来源JT/T 808'); + expect(mobileCard?.querySelectorAll('.v2-monitor-mobile-primary > div')).toHaveLength(3); + expect(mobileCard?.querySelector('.v2-monitor-mobile-location')).toHaveTextContent('113.260000, 23.130000解析位置'); expect(view.container.querySelector('.v2-monitor-table-scroll')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: '在地图中定位粤A12345' })).toHaveTextContent('地图'); + expect(screen.getByTitle('今日上报 2,861,323 条')).toHaveTextContent('今日上报286.1万条'); expect(view.container.querySelector('.v2-monitor-mobile-card footer')).not.toBeInTheDocument(); expect(addEventListener).toHaveBeenCalledWith('change', expect.any(Function)); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index dc05c5d5..71c4cbbf 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -89,6 +89,14 @@ function hasTodayMileage(vehicle: VehicleRealtimeRow): vehicle is VehicleRealtim return vehicle.todayMileageAvailable === true && vehicle.todayMileageKm != null; } +function formatSupportCount(value: number, compact: boolean) { + if (!compact) return formatNumber(value); + const absolute = Math.abs(value); + if (absolute >= 100_000_000) return `${Number((value / 100_000_000).toFixed(1))}亿`; + if (absolute >= 10_000) return `${Number((value / 10_000).toFixed(1))}万`; + return formatNumber(value); +} + function hasRealtimeSOC(vehicle: VehicleRealtimeRow) { return vehicle.socAvailable ?? (vehicle.primaryProtocol === 'GB32960' || vehicle.primaryProtocol === 'YUTONG_MQTT'); } @@ -126,6 +134,51 @@ const MonitorAddressCell = memo(function MonitorAddressCell({ vehicle }: { vehic return
{addressQuery.data?.formattedAddress || '暂无地址'}{addressQuery.dataUpdatedAt ? `解析于 ${new Date(addressQuery.dataUpdatedAt).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })}` : ''}{moved ? : null}
; }); +type MonitorMobileVehicleCardProps = { + row: VehicleRealtimeRow; + onSelect: (vin: string) => void; +}; + +const MonitorMobileVehicleCard = memo(function MonitorMobileVehicleCard({ row, onSelect }: MonitorMobileVehicleCardProps) { + const identity = row.plate || '未绑定车牌'; + const location = hasRealtimeLocation(row); + return +
+
{identity}{row.vin}
+
+ {hasRealtimeSpeed(row) ? formatNumber(row.speedKmh, 1) : '—'}{hasRealtimeSpeed(row) ? km/h : null} + +
+
+
+
当日里程
{hasTodayMileage(row) ? `${formatNumber(row.todayMileageKm, 1)} km` : '—'}
+
总里程
{hasRealtimeMileage(row) ? `${formatNumber(row.totalMileageKm, 1)} km` : '—'}
+
协议来源
{row.protocols.length > 1 ? {row.protocols.length} 路 : null}
+
+
+ + +
+
; +}, (previous, next) => { + const before = previous.row; + const after = next.row; + return previous.onSelect === next.onSelect + && before.vin === after.vin + && before.plate === after.plate + && before.speedAvailable === after.speedAvailable + && before.speedKmh === after.speedKmh + && before.todayMileageAvailable === after.todayMileageAvailable + && before.todayMileageKm === after.todayMileageKm + && before.mileageAvailable === after.mileageAvailable + && before.totalMileageKm === after.totalMileageKm + && before.primaryProtocol === after.primaryProtocol + && before.protocols.join('|') === after.protocols.join('|') + && before.locationAvailable === after.locationAvailable + && before.longitude === after.longitude + && before.latitude === after.latitude; +}); + function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, mobile, onSelect, onPage, onLimit }: { rows: VehicleRealtimeRow[]; total: number; page: number; totalPages: number; limit: number; loading: boolean; mobile: boolean; onSelect: (vin: string) => void; onPage: (page: number) => void; onLimit: (limit: number) => void }) { const columns = useMemo(() => [ { title: '车辆', dataIndex: 'plate', width: 210, className: 'v2-monitor-table-vehicle', render: (_value: string, row: VehicleRealtimeRow) => }, @@ -138,15 +191,13 @@ function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, mo ], [onSelect]); return {!mobile ?
{loading ?
: null}{!loading && !rows.length ? : null} : null} - {mobile ?
{rows.map((row) => -
{row.plate || '未绑定车牌'}{row.vin}
{hasRealtimeSpeed(row) ? formatNumber(row.speedKmh, 1) : '—'}{hasRealtimeSpeed(row) ? km/h : null}
-
当日里程
{hasTodayMileage(row) ? `${formatNumber(row.todayMileageKm, 1)} km` : '—'}
总里程
{hasRealtimeMileage(row) ? `${formatNumber(row.totalMileageKm, 1)} km` : '—'}
协议来源
{row.protocols.length > 1 ? {row.protocols.length} 路 : null}
经纬度
{hasRealtimeLocation(row) ? {row.longitude.toFixed(6)}, {row.latitude.toFixed(6)} : '—'}
地理位置
-
)}{loading ?
: null}{!loading && !rows.length ? : null}
: null} + {mobile ?
{rows.map((row) => )}{loading ?
: null}{!loading && !rows.length ? : null}
: null}
; } @@ -466,7 +517,7 @@ export default function MonitorPage() { className="v2-monitor-summary-rail" items={monitorMetrics} context={
- 今日上报{formatNumber(summary.data?.frameToday ?? 0)}数据活跃度 + 今日上报{formatSupportCount(summary.data?.frameToday ?? 0, mobileLayout)}数据活跃度 无实时位置{formatNumber(summary.data?.noLocationVehicles ?? 0)}辅助排查
} /> diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index f40f5065..7c4a578e 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -25489,3 +25489,400 @@ text-overflow: ellipsis; } } + +/* + * Semi UI monitor list convergence. + * Realtime evidence stays dense and stable during polling: desktop rows read + * as one continuous data surface, while phone cards keep four vehicles within + * a typical viewport without clipping the supporting fleet metrics. + */ +.v2-monitor-table-panel.semi-card { + border-color: #dce5ef; + border-radius: 12px; + box-shadow: 0 6px 22px rgba(31, 53, 80, .045); +} + +.v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header { + min-height: 58px; + border-bottom: 1px solid #e3e9f1; + padding: 9px 13px; +} + +.v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header .v2-workspace-panel-copy { + gap: 2px; +} + +.v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header .v2-workspace-panel-copy > h5 { + color: #263b52; + font-size: 15px; + font-weight: 760; +} + +.v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header .v2-workspace-panel-meta { + border-radius: 999px; + background: #eef4fb; + padding: 4px 8px; + color: #56718f; + font-size: 10px; + font-weight: 700; +} + +.v2-monitor-table .semi-table-tbody > .semi-table-row > .semi-table-row-cell { + height: 58px; + border-bottom-color: #e8edf3; +} + +.v2-monitor-table .semi-table-tbody > .semi-table-row:hover { + background: #f6f9fd; +} + +.v2-monitor-table-vehicle strong { + color: #253a51; + font-size: 13px; + font-weight: 740; +} + +.v2-monitor-table-vehicle span { + color: #8290a2; + font-size: 10px; +} + +@media (max-width: 760px) { + .v2-monitor-summary-rail.is-queue.has-context > .semi-card-body { + min-height: 130px; + grid-template-columns: minmax(0, 1fr); + } + + .v2-monitor-summary-rail .v2-workspace-metric-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span { + box-sizing: border-box; + min-height: 49px; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: 11px 20px 10px; + align-content: center; + gap: 0; + padding: 3px 10px; + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span.is-secondary { + display: none; + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span:nth-child(3), + .v2-monitor-summary-rail .v2-workspace-metric-list > span:nth-child(6) { + border-top: 1px solid #e5ebf2; + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span:nth-child(3) { + border-left: 0; + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span > :is(small, strong, em) { + min-width: 0; + grid-column: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span > small { + grid-row: 1; + font-size: 9px; + line-height: 12px; + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span > strong, + .v2-monitor-summary-rail .v2-workspace-metric-list > span.is-secondary > strong { + grid-row: 2; + font-size: 18px; + line-height: 20px; + } + + .v2-monitor-summary-rail .v2-workspace-metric-list > span > em { + display: block; + max-width: none; + grid-row: 3; + font-size: 8px; + line-height: 11px; + } + + .v2-monitor-summary-rail .v2-workspace-metric-context { + display: flex; + min-height: 38px; + border-top: 1px solid #e5ebf2; + border-left: 0; + padding: 0; + } + + .v2-monitor-summary-support { + width: 100%; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-monitor-summary-support > span { + min-height: 38px; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: 16px; + align-content: center; + align-items: center; + gap: 5px; + padding: 4px 9px; + } + + .v2-monitor-summary-support small { + min-width: 0; + overflow: hidden; + font-size: 9px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-monitor-summary-support strong { + min-width: max-content; + overflow: visible; + font-size: 13px; + line-height: 16px; + } + + .v2-monitor-summary-support strong > i { + font-size: 8px; + } + + .v2-monitor-summary-support em { + display: none; + } + + .v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header { + min-height: 50px; + align-items: center; + padding: 6px 10px; + } + + .v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header .v2-workspace-panel-copy { + gap: 1px; + } + + .v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header .v2-workspace-panel-copy > h5 { + font-size: 13px; + } + + .v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header .v2-workspace-panel-copy > .semi-typography:not(h5) { + max-width: 210px; + font-size: 9px; + } + + .v2-monitor-table-panel > .semi-card-body > .v2-monitor-list-header .v2-workspace-panel-meta { + background: transparent; + padding: 0; + font-size: 9px; + } + + .v2-monitor-mobile-cards { + gap: 5px; + padding: 5px; + scrollbar-color: #b5c2d0 transparent; + scrollbar-width: thin; + } + + .v2-monitor-mobile-cards > .v2-monitor-mobile-card.semi-card { + min-height: 109px; + flex: 0 0 auto; + overflow: hidden; + border-color: #dfe6ef; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(31, 53, 80, .025); + contain-intrinsic-size: auto 109px; + } + + .v2-monitor-mobile-card > .semi-card-body { + padding: 0; + } + + .v2-monitor-mobile-card .v2-monitor-mobile-card-header { + display: flex; + box-sizing: border-box; + height: 38px; + min-height: 38px; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 4px 8px 3px 9px; + } + + .v2-monitor-mobile-identity { + display: flex; + min-width: 0; + flex-direction: column; + gap: 1px; + } + + .v2-monitor-mobile-card .v2-monitor-mobile-identity > strong { + overflow: hidden; + color: #283d54; + font-size: 14px; + font-weight: 750; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-monitor-mobile-card .v2-monitor-mobile-identity > span { + overflow: hidden; + color: #8795a6; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 9px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-monitor-card-heading-actions { + gap: 6px !important; + } + + .v2-monitor-card-heading-actions > b { + font-size: 15px; + font-weight: 740; + } + + .v2-monitor-card-heading-actions > b small { + font-size: 9px; + } + + .v2-monitor-card-heading-actions > .v2-monitor-card-locate.semi-button { + width: 46px; + height: 28px; + min-height: 28px; + border-radius: 7px; + padding: 0 5px; + font-size: 9px; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary { + display: grid; + grid-template-columns: .9fr 1.08fr 1.12fr; + gap: 0; + margin: 0; + border-top: 1px solid #edf1f5; + padding: 3px 8px; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary > div { + display: grid; + min-width: 0; + min-height: 27px; + align-content: center; + gap: 1px; + border-right: 1px solid #edf1f5; + padding: 0 7px; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary > div:first-child { + padding-left: 1px; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary > div:last-child { + border-right: 0; + padding-right: 1px; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary dt { + color: #8996a7; + font-size: 9px; + line-height: 12px; + white-space: nowrap; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary dd { + min-width: 0; + margin: 0; + overflow: hidden; + color: #34485f; + font-size: 10px; + font-weight: 680; + line-height: 14px; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary dd.is-today { + color: #1268df; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary .v2-monitor-mobile-protocol { + justify-content: flex-start; + gap: 4px; + } + + .v2-monitor-mobile-cards .v2-monitor-mobile-primary .v2-protocol-tag.semi-tag { + max-width: 82px; + } + + .v2-monitor-mobile-location { + display: grid; + box-sizing: border-box; + height: 32px; + min-height: 32px; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 6px; + border-top: 1px solid #edf1f5; + background: #f8fafc; + padding: 3px 8px; + } + + .v2-monitor-mobile-coordinate { + display: flex; + min-width: 0; + align-items: center; + gap: 5px; + color: #77889b; + } + + .v2-monitor-mobile-coordinate > svg { + width: 12px; + height: 12px; + flex: 0 0 auto; + color: #6f87a3; + } + + .v2-monitor-mobile-coordinate > :is(code, small) { + min-width: 0; + overflow: hidden; + color: #6c7d91; + font-size: 9px; + line-height: 14px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-monitor-mobile-location .v2-monitor-address-action.semi-button, + .v2-monitor-mobile-location .v2-monitor-address-update.semi-button { + min-width: 68px; + height: 25px; + min-height: 25px; + border-radius: 7px; + padding: 0 7px; + font-size: 9px; + } + + .v2-monitor-mobile-location .v2-monitor-address-loading { + font-size: 9px; + } + + .v2-monitor-mobile-location .v2-monitor-address-result { + display: grid; + max-width: 150px; + grid-template-columns: minmax(0, 1fr); + } + + .v2-monitor-mobile-location .v2-monitor-address-result > :is(span, small) { + overflow: hidden; + font-size: 9px; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/vehicle-data-platform/design-qa.md b/vehicle-data-platform/design-qa.md index c522b7a0..7028c987 100644 --- a/vehicle-data-platform/design-qa.md +++ b/vehicle-data-platform/design-qa.md @@ -1,57 +1,48 @@ -# 账号管理 Semi UI Design QA +# 全局监控列表 Semi UI Design QA -- source visual truth: 当前生产版本 `/users` +- source visual truth: 生产版本 `e944b797` 的 `/monitor?mode=list` - source screenshots: - - `/tmp/users-desktop-before.png` - - `/tmp/users-mobile-before.png` + - desktop: `/tmp/semi-continuous-audit-20260719/04-monitor-list-desktop.png` + - mobile: `/tmp/semi-continuous-audit-20260719/05-monitor-list-mobile.png` - implementation screenshots: - - `/tmp/users-desktop-after.png` - - `/tmp/users-mobile-after.png` - - `/tmp/users-mobile-detail-after.png` -- implementation paths: - - `apps/web/src/v2/pages/UsersPage.tsx` - - `apps/web/src/v2/pages/UsersPage.test.tsx` - - `apps/web/src/v2/styles/workspace.css` + - desktop: `/tmp/semi-continuous-audit-20260719/07-monitor-list-desktop-after.png` + - mobile: `/tmp/semi-continuous-audit-20260719/06-monitor-list-mobile-after.png` +- comparison evidence: + - mobile before/after: `/tmp/semi-continuous-audit-20260719/monitor-mobile-comparison.png` - viewport and state: - - desktop `1440 × 900`,生产基线截图原始高度为 `810px`,在对比图中补齐到 `900px` + - desktop `1440 × 900` - mobile `390 × 844` - - 使用生产管理员真实数据,共 5 个客户账号 - - 验证账号目录、账号选中状态、车辆授权详情和移动端侧滑面板 - -## Comparison Evidence - -- desktop before/after: `/tmp/users-desktop-comparison.png` -- mobile before/after: `/tmp/users-mobile-comparison.png` -- directory focus before/after: `/tmp/users-directory-focus-comparison.png` -- 三组对比图均已通过 `view_image` 并排检查。 + - 使用管理员生产数据,共 `1,024` 辆授权车辆 + - 列表视图、第一页、每页 `50` 辆 ## Fidelity Ledger | Surface | Required fidelity | Implementation evidence | Result | | --- | --- | --- | --- | -| Directory hierarchy | 账号列表必须占据主工作区,不能由相互割裂的大卡片主导。 | 桌面端改为连续的证据表,行间使用轻分隔线、克制悬浮反馈与选中左侧强调线。 | Improved. | -| Account identity | 客户名称、登录账号和身份来源需形成稳定扫描顺序。 | 名称与账号仍居首,并新增 Semi `Tag` 标注“本地”或外部身份源。 | Improved. | -| Permission evidence | 菜单数、车辆数、状态和最近登录需保持对齐。 | 权限数字采用紧凑的浅色单元格表面;状态和时间列仍在原有网格位置。 | Preserved. | -| Selected state | 当前编辑账号必须可快速定位。 | 选中行同时保留背景色和蓝色前导强调,详情侧滑面板与选中行一致。 | Improved. | -| Mobile density | 移动端不能为了桌面连续表牺牲触控与信息密度。 | `390 × 844` 下保留独立卡片间距、紧凑身份标签和可点击整行。 | Preserved. | -| Detail workflow | 打开账号、切换车辆权限、关闭面板必须完整可用。 | 桌面与移动端均验证侧滑面板;移动端车辆授权列表、状态、页签和底部动作无重叠。 | Passed. | -| Asset fidelity | 品牌 Logo 与图标体系不得被临时图形替换。 | 保留现有官方 Logo、Semi 图标及组件;没有新增手绘 SVG、CSS 图标或装饰性位图。 | Preserved. | +| Monitoring workflow | 搜索、筛选、地图/列表切换、分页和实时刷新周期不能被视觉重构改变。 | 保留原查询、路由和轮询实现,只重组列表渲染与响应式样式。 | Preserved. | +| Fleet hierarchy | 车辆总数、当前在线、行驶和告警必须优先;今日上报与无实时位置属于辅助证据。 | 手机端主指标采用清晰的 `2 × 2` 网格;辅助指标独立放在底部并压缩大数。 | Improved. | +| Vehicle evidence | 车牌、VIN、速度、当日里程、总里程、协议和位置必须在一眼可扫范围内。 | 手机卡片固定为身份、三项核心证据、位置三层;桌面继续使用连续 Semi Table。 | Improved. | +| Mobile density | 单屏车辆数需要显著增加,但不牺牲字段可辨认性。 | 卡片高度从约 `147px` 降至 `109px`;列表视口可容纳约 `4.05` 张卡片,截图中从约 `2.5` 辆提升到 `4` 辆。 | Improved. | +| Realtime performance | 高频刷新不能让全部车辆卡片无差别重绘。 | 移动卡片使用字段级 `memo` 比较;不可见卡片继续使用 `content-visibility` 和固有尺寸占位。 | Improved. | +| Address cost | 地理位置不得随列表刷新自动消耗逆地理编码 API。 | 经纬度始终可见;地址仍由用户点击“解析位置”后按需请求。 | Preserved. | +| Asset fidelity | 品牌和图标不得用临时图形替代。 | 沿用生产 Logo、Semi Icon、Card、Button、Table、Tag 与 Pagination。 | Preserved. | ## Interaction And Accessibility Checks -- 点击账号行可打开详情,关闭后回到原目录状态; -- 车辆权限页签可点击,授权车辆卡片与状态信息完整; -- 搜索与状态筛选入口保持原有行为; -- 移动端侧滑面板没有遮挡页签、内容或底部动作; -- 控制台无本次改动产生的运行时异常;仅存在 Semi UI 在 React StrictMode 下既有的 `findDOMNode` 弃用警告。 +- 手机端“筛选”可展开并可通过“收起”恢复列表; +- 地图/列表使用原有可访问 tab 语义,车辆卡片具有“车牌 + 实时数据”标签; +- “地图”和“解析位置”按钮均保留车辆级可访问名称; +- `390px` 视口文档横向溢出为 `0px`,主指标和辅助指标无裁切; +- 移动卡片实测高度 `109px`,第一页仍渲染 `50` 辆并由内部列表滚动; +- 控制台未发现本次改动产生的运行时异常;仅保留 Semi UI 在 React StrictMode 下既有的 `findDOMNode` 弃用警告。 ## Comparison History ### Pass 1 — fixed -- 原实现的桌面账号行被拆成多个带间距卡片,视觉体量偏大,证据列扫描节奏不连续。 -- 已收敛为连续目录表,并补充身份来源标签,让账号、授权和状态形成一条稳定阅读路径。 -- 修复后对比图中没有发现 P0、P1 或 P2 视觉与交互问题。 -- 当前唯一低风险残余是生产数据均为本地身份源,外部身份源标签由渲染逻辑覆盖,尚无生产样本可做视觉实证。 +- 生产手机端的“今日上报”和“无实时位置”数值被截断,信息层级与主要车队指标混在一起。 +- 生产车辆卡片纵向留白过多,`390 × 844` 首屏只能看到约 `2.5` 辆。 +- 已将辅助指标改成完整的紧凑数值,将每辆车的协议、里程与坐标收拢到固定证据网格。 +- 并排对比后未发现 P0、P1 或 P2 视觉与交互问题。 final result: passed