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
|
||||
|
||||
@@ -16539,48 +16539,128 @@
|
||||
|
||||
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta strong {
|
||||
margin-top: 3px;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-sources {
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
margin-top: 3px;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-sources .semi-tag {
|
||||
min-height: 18px;
|
||||
padding-inline: 4px;
|
||||
font-size: 7px;
|
||||
min-height: 20px;
|
||||
padding-inline: 6px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 3px;
|
||||
overflow: hidden;
|
||||
padding: 5px 6px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
overflow: visible;
|
||||
padding: 7px 8px 8px;
|
||||
}
|
||||
|
||||
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-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 {
|
||||
|
||||
Reference in New Issue
Block a user