feat: refine mobile vehicle detail actions
This commit is contained in:
@@ -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 <div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
||||
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => onSelect(action.to)}>{action.label}</Button>)}
|
||||
</div>;
|
||||
|
||||
return <>
|
||||
<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
||||
{primaryActions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => selectAction(action.to)}>{action.label}</Button>)}
|
||||
{overflowActions.length ? <Button className="v2-vehicle-more-trigger" theme="light" type="tertiary" icon={<IconMore />} aria-label="更多车辆操作" aria-haspopup="dialog" aria-controls="v2-vehicle-mobile-actions" aria-expanded={open} onClick={() => setOpen(true)}>更多</Button> : null}
|
||||
</div>
|
||||
<SideSheet
|
||||
className="v2-vehicle-mobile-actions-sidesheet"
|
||||
visible={open}
|
||||
placement="bottom"
|
||||
height="min(48dvh, 360px)"
|
||||
aria-label="更多车辆操作"
|
||||
title={<div className="v2-vehicle-mobile-actions-title"><strong>更多车辆操作</strong><span>继续查看这辆车的历史、里程与告警</span></div>}
|
||||
onCancel={() => setOpen(false)}
|
||||
footer={<Button block theme="solid" type="primary" onClick={() => setOpen(false)}>完成</Button>}
|
||||
>
|
||||
<div className="v2-vehicle-mobile-actions-list">
|
||||
{overflowActions.map((action) => <Button key={action.key} block theme="light" type="tertiary" icon={action.icon} iconPosition="left" aria-label={action.label} onClick={() => selectAction(action.to)}>{action.label}<IconChevronRight /></Button>)}
|
||||
</div>
|
||||
</SideSheet>
|
||||
</>;
|
||||
}
|
||||
@@ -518,7 +518,7 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => {
|
||||
});
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
|
||||
render(<QueryClientProvider client={client}><MemoryRouter initialEntries={[`/vehicles/${initialRealtime.vin}`]} future={ROUTER_FUTURE}><Routes><Route path="/vehicles/:vin" element={<VehiclePage />} /></Routes></MemoryRouter></QueryClientProvider>);
|
||||
render(<QueryClientProvider client={client}><MemoryRouter initialEntries={[`/vehicles/${initialRealtime.vin}`]} future={ROUTER_FUTURE}><Routes><Route path="/vehicles/:vin" element={<VehiclePage />} /><Route path="/history" element={<div data-testid="mobile-history-route">历史数据页面</div>} /></Routes></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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<Element | null>([
|
||||
@@ -492,9 +493,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
|
||||
<span className="v2-identity-model"><small>车辆品牌 / 车型</small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></span>
|
||||
<span className="v2-identity-source-context"><small>可用数据来源</small><span className="v2-identity-sources">{detail.sources.length ? detail.sources.map((source) => <ProtocolTag key={source} protocol={source} compact />) : <Tag color="grey" type="light" size="small">暂无来源</Tag>}</span></span>
|
||||
</span>}
|
||||
actions={<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
||||
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => navigate(action.to)}>{action.label}</Button>)}
|
||||
</div>}
|
||||
actions={<Suspense fallback={null}><VehicleActions actions={actions} mobile={mobileLayout} onSelect={navigate} /></Suspense>}
|
||||
/>
|
||||
<section className="v2-vehicle-live-section" aria-labelledby="vehicle-live-heading">
|
||||
<WorkspacePanelHeader
|
||||
|
||||
Reference in New Issue
Block a user