feat: densify mobile monitor workspace
This commit is contained in:
@@ -112,6 +112,7 @@ test('starts without a selection and supports expand, collapse, reselection, and
|
|||||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
const view = render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
|
const view = render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
|
||||||
const workspace = view.container.querySelector('.v2-monitor-workspace')!;
|
const workspace = view.container.querySelector('.v2-monitor-workspace')!;
|
||||||
|
const monitorPage = view.container.querySelector('.v2-monitor-page')!;
|
||||||
const firstVehicle = screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ });
|
const firstVehicle = screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ });
|
||||||
const secondVehicle = screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ });
|
const secondVehicle = screen.getByRole('button', { name: /粤B67890 LTEST000000000002/ });
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ test('starts without a selection and supports expand, collapse, reselection, and
|
|||||||
expect(liveStatus).toHaveTextContent('最近同步等待数据');
|
expect(liveStatus).toHaveTextContent('最近同步等待数据');
|
||||||
expect(workspace).not.toHaveClass('is-detail-open');
|
expect(workspace).not.toHaveClass('is-detail-open');
|
||||||
expect(workspace).not.toHaveClass('is-detail-collapsed');
|
expect(workspace).not.toHaveClass('is-detail-collapsed');
|
||||||
|
expect(monitorPage).toHaveClass('is-map-mode');
|
||||||
expect(firstVehicle).not.toHaveClass('is-selected');
|
expect(firstVehicle).not.toHaveClass('is-selected');
|
||||||
expect(screen.queryByRole('button', { name: '取消选择车辆' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: '取消选择车辆' })).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', '');
|
expect(screen.getByTestId('fleet-map')).toHaveAttribute('data-selected-vin', '');
|
||||||
@@ -149,6 +151,7 @@ test('starts without a selection and supports expand, collapse, reselection, and
|
|||||||
fireEvent.click(firstVehicle);
|
fireEvent.click(firstVehicle);
|
||||||
expect(await screen.findByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument();
|
expect(await screen.findByRole('region', { name: '粤A12345车辆详情' })).toBeInTheDocument();
|
||||||
expect(workspace).toHaveClass('is-detail-open');
|
expect(workspace).toHaveClass('is-detail-open');
|
||||||
|
expect(monitorPage).toHaveClass('is-map-mode');
|
||||||
expect(firstVehicle).toHaveClass('is-selected');
|
expect(firstVehicle).toHaveClass('is-selected');
|
||||||
expect(screen.getByRole('button', { name: '取消选择车辆' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: '取消选择车辆' })).toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-vehicle-detail')).toHaveClass('semi-card');
|
expect(view.container.querySelector('.v2-vehicle-detail')).toHaveClass('semi-card');
|
||||||
@@ -203,6 +206,28 @@ test('starts without a selection and supports expand, collapse, reselection, and
|
|||||||
expect(secondVehicle).toHaveClass('is-selected');
|
expect(secondVehicle).toHaveClass('is-selected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('returns the monitor scroll owner to the command bar when changing views', async () => {
|
||||||
|
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 });
|
||||||
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
const view = render(
|
||||||
|
<main className="v2-content" data-testid="monitor-scroll-owner">
|
||||||
|
<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
const scrollOwner = screen.getByTestId('monitor-scroll-owner');
|
||||||
|
const monitorPage = view.container.querySelector('.v2-monitor-page')!;
|
||||||
|
|
||||||
|
scrollOwner.scrollTop = 180;
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: /列表/ }));
|
||||||
|
expect(scrollOwner.scrollTop).toBe(0);
|
||||||
|
expect(monitorPage).toHaveClass('is-list-mode');
|
||||||
|
|
||||||
|
scrollOwner.scrollTop = 120;
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: /地图/ }));
|
||||||
|
expect(scrollOwner.scrollTop).toBe(0);
|
||||||
|
expect(monitorPage).toHaveClass('is-map-mode');
|
||||||
|
});
|
||||||
|
|
||||||
test('restores URL-backed monitor context and carries it into every vehicle workflow', async () => {
|
test('restores URL-backed monitor context and carries it into every vehicle workflow', async () => {
|
||||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
const entry = '/monitor?keyword=%E7%B2%A4A12345&protocol=JT808&status=online&selectedVin=LTEST000000000001&detail=collapsed&zoom=13&bounds=113.100000%2C23.000000%2C113.400000%2C23.300000';
|
const entry = '/monitor?keyword=%E7%B2%A4A12345&protocol=JT808&status=online&selectedVin=LTEST000000000001&detail=collapsed&zoom=13&bounds=113.100000%2C23.000000%2C113.400000%2C23.300000';
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ const MemoFleetMap = memo(FleetMap);
|
|||||||
|
|
||||||
export default function MonitorPage() {
|
export default function MonitorPage() {
|
||||||
const [routeParams, setRouteParams] = useSearchParams();
|
const [routeParams, setRouteParams] = useSearchParams();
|
||||||
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
const initialContextRef = useRef(parseMonitorRouteContext(routeParams));
|
const initialContextRef = useRef(parseMonitorRouteContext(routeParams));
|
||||||
const initialContext = initialContextRef.current;
|
const initialContext = initialContextRef.current;
|
||||||
const mobileLayout = useMobileLayout(760);
|
const mobileLayout = useMobileLayout(760);
|
||||||
@@ -280,11 +281,16 @@ export default function MonitorPage() {
|
|||||||
const selected = selectedVin
|
const selected = selectedVin
|
||||||
? rows.find((vehicle) => vehicle.vin === selectedVin) ?? selectedVehicle.data?.items[0]
|
? rows.find((vehicle) => vehicle.vin === selectedVin) ?? selectedVehicle.data?.items[0]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const resetMonitorScroll = useCallback(() => {
|
||||||
|
const scrollOwner = pageRef.current?.closest<HTMLElement>('.v2-content');
|
||||||
|
if (scrollOwner) scrollOwner.scrollTop = 0;
|
||||||
|
}, []);
|
||||||
const selectVehicle = useCallback((vin: string) => {
|
const selectVehicle = useCallback((vin: string) => {
|
||||||
|
resetMonitorScroll();
|
||||||
setSelectedVin(vin);
|
setSelectedVin(vin);
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
setMode('map');
|
setMode('map');
|
||||||
}, []);
|
}, [resetMonitorScroll]);
|
||||||
const clearSelection = useCallback(() => {
|
const clearSelection = useCallback(() => {
|
||||||
setSelectedVin('');
|
setSelectedVin('');
|
||||||
setDetailOpen(false);
|
setDetailOpen(false);
|
||||||
@@ -293,9 +299,10 @@ export default function MonitorPage() {
|
|||||||
const collapseDetail = useCallback(() => setDetailOpen(false), []);
|
const collapseDetail = useCallback(() => setDetailOpen(false), []);
|
||||||
const expandDetail = useCallback(() => setDetailOpen(true), []);
|
const expandDetail = useCallback(() => setDetailOpen(true), []);
|
||||||
const changeMonitorMode = useCallback((nextMode: 'map' | 'list') => {
|
const changeMonitorMode = useCallback((nextMode: 'map' | 'list') => {
|
||||||
|
resetMonitorScroll();
|
||||||
setMode(nextMode);
|
setMode(nextMode);
|
||||||
if (nextMode === 'list') setDetailOpen(false);
|
if (nextMode === 'list') setDetailOpen(false);
|
||||||
}, []);
|
}, [resetMonitorScroll]);
|
||||||
const clearFilters = useCallback(() => {
|
const clearFilters = useCallback(() => {
|
||||||
setKeyword('');
|
setKeyword('');
|
||||||
setProtocol('');
|
setProtocol('');
|
||||||
@@ -314,7 +321,7 @@ export default function MonitorPage() {
|
|||||||
const lastSyncAt = vehicles.dataUpdatedAt ? new Date(vehicles.dataUpdatedAt) : undefined;
|
const lastSyncAt = vehicles.dataUpdatedAt ? new Date(vehicles.dataUpdatedAt) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="v2-monitor-page">
|
<div ref={pageRef} className={`v2-monitor-page is-${mode}-mode`}>
|
||||||
<Card className="v2-filterbar" bodyStyle={{ padding: 0 }} aria-label="车辆筛选">
|
<Card className="v2-filterbar" bodyStyle={{ padding: 0 }} aria-label="车辆筛选">
|
||||||
<div id="monitor-filter-fields" className={`v2-monitor-filter-fields${filtersCollapsed ? ' is-mobile-collapsed' : ''}`} aria-label="筛选条件">
|
<div id="monitor-filter-fields" className={`v2-monitor-filter-fields${filtersCollapsed ? ' is-mobile-collapsed' : ''}`} aria-label="筛选条件">
|
||||||
<div className={`v2-search-field${searchTerms.length > 1 ? ' is-batch' : ''}`}>
|
<div className={`v2-search-field${searchTerms.length > 1 ? ' is-batch' : ''}`}>
|
||||||
|
|||||||
@@ -12797,6 +12797,223 @@
|
|||||||
.v2-kpi strong {
|
.v2-kpi strong {
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards {
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards > .v2-monitor-mobile-card.semi-card {
|
||||||
|
contain-intrinsic-size: auto 132px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body {
|
||||||
|
padding: 7px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body > header {
|
||||||
|
min-height: 34px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body > header strong {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body > header span {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-card-heading-actions > b {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-card-heading-actions > .v2-monitor-card-locate.semi-button {
|
||||||
|
height: 28px;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dl {
|
||||||
|
gap: 2px 9px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dl > div {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dl > div.is-address {
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dl > div.is-address .semi-button {
|
||||||
|
min-height: 26px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) and (max-height: 500px) and (orientation: landscape) {
|
||||||
|
.v2-monitor-page {
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-filterbar > .semi-card-body {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-filter-actions {
|
||||||
|
grid-template-columns: 64px minmax(0, 1fr) 38px;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-filter-toggle.semi-button {
|
||||||
|
width: 64px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mode.v2-segmented-tabs {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mode.v2-segmented-tabs > .semi-button {
|
||||||
|
height: 30px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-filter-actions > .v2-monitor-mobile-entry.semi-button {
|
||||||
|
width: 38px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-kpis > .semi-card-body {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-kpis .v2-kpi.is-support {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-kpis .v2-kpi {
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-kpis .v2-kpi small {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-kpis .v2-kpi strong {
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-kpis .v2-kpi strong em {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-table-panel > .semi-card-body > .v2-workspace-panel-header {
|
||||||
|
min-height: 35px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-table-panel > .semi-card-body > .v2-workspace-panel-header .v2-workspace-panel-copy {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-table-panel > .semi-card-body > .v2-workspace-panel-header .v2-workspace-panel-copy > h5 {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-table-panel > .semi-card-body > .v2-workspace-panel-header .v2-workspace-panel-copy > .semi-typography:not(h5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-table-panel > .semi-card-body > .v2-workspace-panel-header .v2-workspace-panel-meta {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
align-content: start;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards > .v2-monitor-mobile-card.semi-card {
|
||||||
|
contain-intrinsic-size: auto 92px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body {
|
||||||
|
padding: 5px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body > header {
|
||||||
|
min-height: 29px;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body > header strong {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-card .semi-card-body > header span {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-card-heading-actions {
|
||||||
|
gap: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-card-heading-actions > b {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-card-heading-actions > .v2-monitor-card-locate.semi-button {
|
||||||
|
width: 44px;
|
||||||
|
height: 26px;
|
||||||
|
min-height: 26px;
|
||||||
|
padding-inline: 5px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dl {
|
||||||
|
gap: 1px 7px;
|
||||||
|
margin-top: 3px;
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dl > div {
|
||||||
|
min-height: 20px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dl > div.is-address {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dt {
|
||||||
|
font-size: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dd {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-mobile-cards dd code {
|
||||||
|
font-size: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-table-panel > .semi-card-body > footer {
|
||||||
|
bottom: calc(58px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-page > .v2-event-strip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Semi UI workspace migration: monitor vehicle inspector and live status rail. */
|
/* Semi UI workspace migration: monitor vehicle inspector and live status rail. */
|
||||||
|
|||||||
Reference in New Issue
Block a user