feat(web): unify vehicle source navigation
This commit is contained in:
@@ -100,6 +100,8 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
|
|||||||
expect(screen.getByRole('heading', { name: '实时遥测', level: 5 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '实时遥测', level: 5 })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('heading', { name: '车辆主档', level: 5 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '车辆主档', level: 5 })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('navigation', { name: '单车详情导航' })).toBeInTheDocument();
|
expect(screen.getByRole('navigation', { name: '单车详情导航' })).toBeInTheDocument();
|
||||||
|
expect(view.container.querySelector('.v2-vehicle-record-page > .v2-source-evidence')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '来源证据' })).toBeInTheDocument();
|
||||||
const telemetryPanel = view.container.querySelector<HTMLElement>('#vehicle-telemetry-panel')!;
|
const telemetryPanel = view.container.querySelector<HTMLElement>('#vehicle-telemetry-panel')!;
|
||||||
const scrollIntoView = vi.fn();
|
const scrollIntoView = vi.fn();
|
||||||
Object.defineProperty(telemetryPanel, 'scrollIntoView', { configurable: true, value: scrollIntoView });
|
Object.defineProperty(telemetryPanel, 'scrollIntoView', { configurable: true, value: scrollIntoView });
|
||||||
@@ -116,7 +118,7 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
|
|||||||
expect(totalMileageSource).toHaveClass('semi-button', 'v2-workspace-metric-action');
|
expect(totalMileageSource).toHaveClass('semi-button', 'v2-workspace-metric-action');
|
||||||
expect(totalMileageSource.closest('.semi-card')).toHaveClass('v2-vehicle-live-metric-rail');
|
expect(totalMileageSource.closest('.semi-card')).toHaveClass('v2-vehicle-live-metric-rail');
|
||||||
expect(totalMileageSource.closest('.v2-live-overview')).toBe(commandCard);
|
expect(totalMileageSource.closest('.v2-live-overview')).toBe(commandCard);
|
||||||
fireEvent.click(locationSource);
|
fireEvent.click(screen.getByRole('button', { name: '来源证据' }));
|
||||||
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,7 +280,7 @@ test('keeps the mobile vehicle directory primary and applies search from a Semi
|
|||||||
expect(view.container.querySelector('.v2-vehicle-directory-table')).not.toBeInTheDocument();
|
expect(view.container.querySelector('.v2-vehicle-directory-table')).not.toBeInTheDocument();
|
||||||
expect(view.container.querySelector('.v2-vehicle-query-panel')).not.toBeInTheDocument();
|
expect(view.container.querySelector('.v2-vehicle-query-panel')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByLabelText('查车操作')).not.toBeInTheDocument();
|
expect(screen.queryByLabelText('查车操作')).not.toBeInTheDocument();
|
||||||
const toggle = screen.getByRole('button', { name: '修改车辆范围:10 辆授权车辆' });
|
const toggle = screen.getByRole('button', { name: '打开车辆搜索:10 辆授权车辆' });
|
||||||
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||||
fireEvent.click(toggle);
|
fireEvent.click(toggle);
|
||||||
const input = screen.getByRole('textbox', { name: '搜索车辆' });
|
const input = screen.getByRole('textbox', { name: '搜索车辆' });
|
||||||
@@ -291,20 +293,20 @@ test('keeps the mobile vehicle directory primary and applies search from a Semi
|
|||||||
expect(screen.getByRole('list', { name: '车辆搜索摘要' })).toHaveTextContent(`当前条件${items[0].plate}车牌 / VIN / 终端`);
|
expect(screen.getByRole('list', { name: '车辆搜索摘要' })).toHaveTextContent(`当前条件${items[0].plate}车牌 / VIN / 终端`);
|
||||||
expect(screen.getAllByRole('button', { name: /打开 .* 车辆档案/ })).toHaveLength(8);
|
expect(screen.getAllByRole('button', { name: /打开 .* 车辆档案/ })).toHaveLength(8);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '应用搜索' }));
|
fireEvent.click(screen.getByRole('button', { name: '应用搜索' }));
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: `修改车辆范围:${items[0].plate} · 10 辆匹配` })).toHaveAttribute('aria-expanded', 'false'));
|
await waitFor(() => expect(screen.getByRole('button', { name: `打开车辆搜索:${items[0].plate} · 10 辆匹配` })).toHaveAttribute('aria-expanded', 'false'));
|
||||||
expect(screen.getByRole('heading', { name: '匹配车辆', level: 5 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '匹配车辆', level: 5 })).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: `修改车辆范围:${items[0].plate} · 10 辆匹配` }));
|
fireEvent.click(screen.getByRole('button', { name: `打开车辆搜索:${items[0].plate} · 10 辆匹配` }));
|
||||||
fireEvent.change(screen.getByRole('textbox', { name: '搜索车辆' }), { target: { value: items[1].plate } });
|
fireEvent.change(screen.getByRole('textbox', { name: '搜索车辆' }), { target: { value: items[1].plate } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: '关闭车辆搜索' }));
|
fireEvent.click(screen.getByRole('button', { name: '关闭车辆搜索' }));
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: `修改车辆范围:${items[0].plate} · 10 辆匹配` })).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByRole('button', { name: `打开车辆搜索:${items[0].plate} · 10 辆匹配` })).toBeInTheDocument());
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: `修改车辆范围:${items[0].plate} · 10 辆匹配` }));
|
fireEvent.click(screen.getByRole('button', { name: `打开车辆搜索:${items[0].plate} · 10 辆匹配` }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: '重置范围' }));
|
fireEvent.click(screen.getByRole('button', { name: '重置范围' }));
|
||||||
expect(screen.getByRole('dialog', { name: '车辆搜索' })).toBeInTheDocument();
|
expect(screen.getByRole('dialog', { name: '车辆搜索' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: `关闭车辆范围:${items[0].plate} · 10 辆匹配` })).toHaveAttribute('aria-expanded', 'true');
|
expect(screen.getByRole('button', { name: `关闭车辆搜索:${items[0].plate} · 10 辆匹配` })).toHaveAttribute('aria-expanded', 'true');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '应用搜索' }));
|
fireEvent.click(screen.getByRole('button', { name: '应用搜索' }));
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '修改车辆范围:10 辆授权车辆' })).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByRole('button', { name: '打开车辆搜索:10 辆授权车辆' })).toBeInTheDocument());
|
||||||
expect(screen.getByRole('heading', { name: '授权车辆', level: 5 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '授权车辆', level: 5 })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
|||||||
import { PlatformTime } from '../shared/PlatformTime';
|
import { PlatformTime } from '../shared/PlatformTime';
|
||||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||||
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
|
import { VehicleSourceEvidenceButton, VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
|
||||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||||
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
|
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
@@ -425,7 +425,7 @@ const vehicleSectionIDs = {
|
|||||||
|
|
||||||
type VehicleSection = keyof typeof vehicleSectionIDs;
|
type VehicleSection = keyof typeof vehicleSectionIDs;
|
||||||
|
|
||||||
function VehicleRecordNavigation() {
|
function VehicleRecordNavigation({ onOpenSources }: { onOpenSources: () => void }) {
|
||||||
const jumpToSection = (section: VehicleSection) => {
|
const jumpToSection = (section: VehicleSection) => {
|
||||||
const target = document.getElementById(vehicleSectionIDs[section]);
|
const target = document.getElementById(vehicleSectionIDs[section]);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
@@ -442,8 +442,9 @@ function VehicleRecordNavigation() {
|
|||||||
<WorkspacePanelHeader
|
<WorkspacePanelHeader
|
||||||
variant="compact"
|
variant="compact"
|
||||||
title="车辆视图"
|
title="车辆视图"
|
||||||
description="位置、事件、遥测与主档"
|
description="位置、事件、遥测"
|
||||||
actions={<nav className="v2-vehicle-section-nav" aria-label="单车详情导航">
|
actions={<nav className="v2-vehicle-section-nav" aria-label="单车详情导航">
|
||||||
|
<VehicleSourceEvidenceButton onClick={onOpenSources} />
|
||||||
{navigationItems.map(([key, label, icon]) => <Button key={key} size="small" theme="borderless" type="tertiary" icon={icon} aria-label={`跳转到${label}`} onClick={() => jumpToSection(key)}>{label}</Button>)}
|
{navigationItems.map(([key, label, icon]) => <Button key={key} size="small" theme="borderless" type="tertiary" icon={icon} aria-label={`跳转到${label}`} onClick={() => jumpToSection(key)}>{label}</Button>)}
|
||||||
</nav>}
|
</nav>}
|
||||||
/>
|
/>
|
||||||
@@ -587,8 +588,8 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
|
|||||||
</section>
|
</section>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<VehicleSourceEvidencePanel vin={detail.vin} open={sourceEvidenceOpen} onOpenChange={setSourceEvidenceOpen} />
|
<VehicleSourceEvidencePanel vin={detail.vin} hideTrigger open={sourceEvidenceOpen} onOpenChange={setSourceEvidenceOpen} />
|
||||||
<VehicleRecordNavigation />
|
<VehicleRecordNavigation onOpenSources={() => setSourceEvidenceOpen(true)} />
|
||||||
|
|
||||||
<div className="v2-record-grid">
|
<div className="v2-record-grid">
|
||||||
<div id={vehicleSectionIDs.location} tabIndex={-1} className="v2-record-section-anchor v2-record-location-anchor">
|
<div id={vehicleSectionIDs.location} tabIndex={-1} className="v2-record-section-anchor v2-record-location-anchor">
|
||||||
|
|||||||
@@ -193,12 +193,13 @@ export default function VehicleSearchWorkspace() {
|
|||||||
/> : null}
|
/> : null}
|
||||||
{mobileLayout ? <div className="v2-vehicle-mobile-discovery">
|
{mobileLayout ? <div className="v2-vehicle-mobile-discovery">
|
||||||
<MobileFilterToggle
|
<MobileFilterToggle
|
||||||
title="车辆范围"
|
title="查找车辆"
|
||||||
summary={mobileFilterSummary}
|
summary={mobileFilterSummary}
|
||||||
expanded={mobileFiltersOpen}
|
expanded={mobileFiltersOpen}
|
||||||
controls="v2-vehicle-mobile-filter-dialog"
|
controls="v2-vehicle-mobile-filter-dialog"
|
||||||
expandedLabel="关闭"
|
expandedLabel="关闭"
|
||||||
collapsedLabel="修改"
|
collapsedLabel="查找"
|
||||||
|
ariaLabel={`${mobileFiltersOpen ? '关闭' : '打开'}车辆搜索:${mobileFilterSummary}`}
|
||||||
onToggle={mobileFiltersOpen ? closeMobileFilters : openMobileFilters}
|
onToggle={mobileFiltersOpen ? closeMobileFilters : openMobileFilters}
|
||||||
/>
|
/>
|
||||||
<MobileFilterSheet
|
<MobileFilterSheet
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type MobileFilterToggleProps = {
|
|||||||
controls?: string;
|
controls?: string;
|
||||||
expandedLabel?: string;
|
expandedLabel?: string;
|
||||||
collapsedLabel?: string;
|
collapsedLabel?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobileFilterToggle({
|
export function MobileFilterToggle({
|
||||||
@@ -18,7 +19,8 @@ export function MobileFilterToggle({
|
|||||||
onToggle,
|
onToggle,
|
||||||
controls,
|
controls,
|
||||||
expandedLabel = '收起',
|
expandedLabel = '收起',
|
||||||
collapsedLabel = '展开'
|
collapsedLabel = '展开',
|
||||||
|
ariaLabel
|
||||||
}: MobileFilterToggleProps) {
|
}: MobileFilterToggleProps) {
|
||||||
const actionLabel = expanded ? expandedLabel : collapsedLabel;
|
const actionLabel = expanded ? expandedLabel : collapsedLabel;
|
||||||
return <Button
|
return <Button
|
||||||
@@ -29,7 +31,7 @@ export function MobileFilterToggle({
|
|||||||
htmlType="button"
|
htmlType="button"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
aria-controls={controls}
|
aria-controls={controls}
|
||||||
aria-label={`${actionLabel}${title}:${summary}`}
|
aria-label={ariaLabel || `${actionLabel}${title}:${summary}`}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<span className="v2-mobile-filter-toggle-content">
|
<span className="v2-mobile-filter-toggle-content">
|
||||||
|
|||||||
@@ -79,14 +79,20 @@ function MileageSourceCard({ source }: { source: VehicleMileageSourceEvidence })
|
|||||||
</Card>;
|
</Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function VehicleSourceEvidenceButton({ onClick }: { onClick: () => void }) {
|
||||||
|
return <Button size="small" theme="light" type="primary" icon={<IconSearch />} aria-label="来源证据" onClick={onClick}>来源证据</Button>;
|
||||||
|
}
|
||||||
|
|
||||||
export function VehicleSourceEvidencePanel({
|
export function VehicleSourceEvidencePanel({
|
||||||
vin,
|
vin,
|
||||||
compact = false,
|
compact = false,
|
||||||
|
hideTrigger = false,
|
||||||
open,
|
open,
|
||||||
onOpenChange
|
onOpenChange
|
||||||
}: {
|
}: {
|
||||||
vin: string;
|
vin: string;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
hideTrigger?: boolean;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -133,12 +139,12 @@ export function VehicleSourceEvidencePanel({
|
|||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Card className={`v2-source-evidence${compact ? ' is-compact' : ''}${expanded ? ' is-open' : ''}${mobileLayout ? ' is-mobile-sheet' : ''}`} bodyStyle={{ padding: 0 }}>
|
{!hideTrigger ? <Card className={`v2-source-evidence${compact ? ' is-compact' : ''}${expanded ? ' is-open' : ''}${mobileLayout ? ' is-mobile-sheet' : ''}`} bodyStyle={{ padding: 0 }}>
|
||||||
<header className="v2-source-evidence-trigger">
|
<header className="v2-source-evidence-trigger">
|
||||||
<div><strong>位置与里程来源</strong><span>{description}</span></div>
|
<div><strong>位置与里程来源</strong><span>{description}</span></div>
|
||||||
<Button theme="light" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>{expanded ? '收起来源' : '查看全部来源'}</Button>
|
<Button theme="light" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>{expanded ? '收起来源' : '查看全部来源'}</Button>
|
||||||
</header>
|
</header>
|
||||||
</Card>
|
</Card> : null}
|
||||||
<WorkspaceDetailSideSheet
|
<WorkspaceDetailSideSheet
|
||||||
className="v2-source-evidence-sidesheet"
|
className="v2-source-evidence-sidesheet"
|
||||||
visible={expanded}
|
visible={expanded}
|
||||||
|
|||||||
@@ -25655,6 +25655,15 @@
|
|||||||
transition: background-color .16s ease;
|
transition: background-color .16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-section-nav > .semi-button:first-child {
|
||||||
|
min-width: 82px;
|
||||||
|
min-height: 34px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding-inline: 9px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-vehicle-recent-card > .semi-card-body > .v2-vehicle-directory-pagination {
|
.v2-vehicle-recent-card > .semi-card-body > .v2-vehicle-directory-pagination {
|
||||||
background: #fbfcfe;
|
background: #fbfcfe;
|
||||||
box-shadow: 0 -8px 22px rgba(43, 62, 86, .025);
|
box-shadow: 0 -8px 22px rgba(43, 62, 86, .025);
|
||||||
@@ -25701,6 +25710,18 @@
|
|||||||
.v2-vehicle-directory-v3 .v2-vehicle-recent-item-shell.is-offline {
|
.v2-vehicle-directory-v3 .v2-vehicle-recent-item-shell.is-offline {
|
||||||
box-shadow: inset 2px 0 #c9d2de;
|
box-shadow: inset 2px 0 #c9d2de;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-record-v3.is-mobile-layout .v2-vehicle-section-nav {
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-record-v3.is-mobile-layout .v2-vehicle-section-nav > .semi-button:first-child {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 36px;
|
||||||
|
padding-inline: 2px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user