From 862671c0832671394762348ec054ad5b697ff201 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 19 Jul 2026 00:43:37 +0800 Subject: [PATCH] feat: refine mobile vehicle detail actions --- .../apps/web/src/v2/pages/VehicleActions.tsx | 57 +++++++++ .../web/src/v2/pages/VehiclePage.test.tsx | 21 +++- .../apps/web/src/v2/pages/VehiclePage.tsx | 5 +- .../apps/web/src/v2/styles/workspace.css | 112 +++++++++++++++--- 4 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 vehicle-data-platform/apps/web/src/v2/pages/VehicleActions.tsx diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehicleActions.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehicleActions.tsx new file mode 100644 index 00000000..bfd7d1a2 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehicleActions.tsx @@ -0,0 +1,57 @@ +import { IconChevronRight, IconMore } from '@douyinfe/semi-icons'; +import { Button, SideSheet } from '@douyinfe/semi-ui'; +import type { ReactNode } from 'react'; +import { useState } from 'react'; +import { useSideSheetA11y } from '../hooks/useSideSheetA11y'; + +export type VehicleAction = { + key: string; + label: string; + icon: ReactNode; + to: string; + type: 'primary' | 'tertiary'; +}; + +export default function VehicleActions({ + actions, + mobile, + onSelect +}: { + actions: VehicleAction[]; + mobile: boolean; + onSelect: (to: string) => void; +}) { + const [open, setOpen] = useState(false); + const primaryActions = actions.filter((action) => action.key === 'switch' || action.key === 'tracks'); + const overflowActions = actions.filter((action) => action.key !== 'switch' && action.key !== 'tracks'); + const selectAction = (to: string) => { + setOpen(false); + onSelect(to); + }; + useSideSheetA11y(open, '.v2-vehicle-mobile-actions-sidesheet', 'v2-vehicle-mobile-actions', '更多车辆操作', '关闭更多车辆操作'); + + if (!mobile) return
+ {actions.map((action) => )} +
; + + return <> +
+ {primaryActions.map((action) => )} + {overflowActions.length ? : null} +
+ 更多车辆操作继续查看这辆车的历史、里程与告警} + onCancel={() => setOpen(false)} + footer={} + > +
+ {overflowActions.map((action) => )} +
+
+ ; +} diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx index 588322cc..17eb680d 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx @@ -518,7 +518,7 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => { }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - render(} />); + render(} />历史数据页面} />); expect(await screen.findByText('GPS 速度')).toBeInTheDocument(); expect(document.querySelector('.v2-telemetry-table')).not.toBeInTheDocument(); @@ -532,8 +532,21 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => { expect(document.querySelector('.v2-telemetry-detail-protocol')).toHaveClass('semi-tag-cyan-light'); expect(screen.getByRole('button', { name: '关闭遥测字段详情' })).toBeInTheDocument(); expect(document.querySelector('.v2-vehicle-record-page')).toHaveClass('v2-vehicle-record-v3', 'is-mobile-layout'); - expect(screen.getByRole('group', { name: '车辆快捷操作' }).querySelectorAll('.semi-button')).toHaveLength(5); - expect(screen.getByRole('group', { name: '车辆快捷操作' })).toHaveTextContent('切换车辆轨迹回放历史数据里程查询告警事件'); + const mobileActions = screen.getByRole('group', { name: '车辆快捷操作' }); + expect(mobileActions.querySelectorAll('.semi-button')).toHaveLength(3); + expect(mobileActions).toHaveTextContent('切换车辆轨迹回放更多'); + expect(mobileActions).not.toHaveTextContent('历史数据里程查询告警事件'); + const moreActions = screen.getByRole('button', { name: '更多车辆操作' }); + expect(moreActions).toHaveAttribute('aria-haspopup', 'dialog'); + expect(moreActions).toHaveAttribute('aria-controls', 'v2-vehicle-mobile-actions'); + expect(moreActions).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(moreActions); + expect(await screen.findByRole('dialog', { name: '更多车辆操作' })).toBeVisible(); + expect(document.querySelector('.v2-vehicle-mobile-actions-sidesheet')).toHaveClass('semi-sidesheet-bottom'); + expect(screen.getByRole('button', { name: '历史数据' })).toBeVisible(); + expect(screen.getByRole('button', { name: '里程查询' })).toBeVisible(); + expect(screen.getByRole('button', { name: '告警事件' })).toBeVisible(); + expect(screen.getByRole('button', { name: '关闭更多车辆操作' })).toBeInTheDocument(); expect(document.querySelectorAll('.v2-identity-sources .semi-tag')).toHaveLength(3); expect(document.querySelector('.v2-identity-sources')).toHaveTextContent('GB/T 32960JT/T 808宇通 MQTT'); expect(document.querySelector('.v2-identity-sources .semi-tag-blue-light')).toHaveTextContent('GB/T 32960'); @@ -544,4 +557,6 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => { expect(document.querySelector('.v2-live-metric.is-speed')).toHaveTextContent('速度'); expect(document.querySelector('.v2-live-metric.is-today-mileage')).toHaveTextContent('当日里程'); expect(document.querySelector('.v2-live-metric.is-source-status')).toHaveTextContent('来源状态'); + fireEvent.click(screen.getByRole('button', { name: '历史数据' })); + expect(await screen.findByTestId('mobile-history-route')).toBeInTheDocument(); }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index cdc11d06..2d10ded0 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -37,6 +37,7 @@ function localDateTime(value?: string) { return value ? value.slice(0, 16) : ''; const operationStatusLabels = { unknown: '待维护', active: '运营中', inactive: '停运', maintenance: '维保中', retired: '已退役' } as const; const SINGLE_VEHICLE_REFRESH_MS = 10_000; const VehicleSearch = lazy(() => import('./VehicleSearchWorkspace')); +const VehicleActions = lazy(() => import('./VehicleActions')); function resetWorkspaceScroll(routeRoot?: HTMLElement | null) { const targets = new Set([ @@ -492,9 +493,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele 车辆品牌 / 车型{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)} 可用数据来源{detail.sources.length ? detail.sources.map((source) => ) : 暂无来源} } - actions={
- {actions.map((action) => )} -
} + actions={} />
.semi-button { - min-height: 48px; - padding: 2px 1px; + min-height: 40px; + border-radius: 8px; + padding: 4px 7px; } .v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button .semi-button-content { min-width: 0; - flex-direction: column; - gap: 2px; + flex-direction: row; + justify-content: center; + gap: 5px; } .v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button .semi-button-content-left { margin-right: 0; - font-size: 14px; + font-size: 15px; } .v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button .semi-button-content-right { max-width: 100%; margin-left: 0; - font-size: 8px; + font-size: 11px; + font-weight: 600; + } + + .v2-vehicle-mobile-actions-sidesheet .semi-sidesheet-inner { + width: 100vw !important; + max-width: 100%; + overflow: hidden; + border-radius: 18px 18px 0 0; + background: #f4f7fb; + box-shadow: 0 -20px 56px rgba(25, 45, 72, .2); + padding-bottom: env(safe-area-inset-bottom); + } + + .v2-vehicle-mobile-actions-sidesheet.semi-sidesheet-bottom .semi-sidesheet-header { + min-height: 68px; + border-bottom: 1px solid #dfe7f0; + background: rgba(255, 255, 255, .98); + padding: 11px 14px; + } + + .v2-vehicle-mobile-actions-sidesheet.semi-sidesheet-bottom .semi-sidesheet-body { + min-height: 0; + background: #f4f7fb; + padding: 10px 12px; + } + + .v2-vehicle-mobile-actions-sidesheet.semi-sidesheet-bottom .semi-sidesheet-footer { + border-top: 1px solid #dfe7f0; + background: rgba(255, 255, 255, .98); + padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); + } + + .v2-vehicle-mobile-actions-title { + display: grid; + gap: 3px; + } + + .v2-vehicle-mobile-actions-title > strong { + color: #253a52; + font-size: 15px; + line-height: 1.3; + } + + .v2-vehicle-mobile-actions-title > span { + color: #8090a3; + font-size: 10px; + line-height: 1.4; + } + + .v2-vehicle-mobile-actions-list { + display: grid; + gap: 7px; + } + + .v2-vehicle-mobile-actions-list > .semi-button { + min-height: 48px; + justify-content: flex-start; + border: 1px solid #e1e8f1; + border-radius: 10px; + background: #fff; + color: #2b425d; + padding-inline: 14px; + font-size: 13px; + font-weight: 600; + } + + .v2-vehicle-mobile-actions-list > .semi-button .semi-button-content { + width: 100%; + justify-content: flex-start; + } + + .v2-vehicle-mobile-actions-list > .semi-button .semi-button-content-right { + display: flex; + min-width: 0; + flex: 1 1 auto; + align-items: center; + justify-content: space-between; } .v2-vehicle-record-v3 .v2-vehicle-live-section > .v2-workspace-panel-header {