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 ? } aria-label="更多车辆操作" aria-haspopup="dialog" aria-controls="v2-vehicle-mobile-actions" aria-expanded={open} onClick={() => setOpen(true)}>更多 : 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 {