feat(web): unify vehicle evidence workspace
This commit is contained in:
@@ -16,7 +16,7 @@ import { formatZhNumber, vehicleOperationStatusLabels } from '../domain/formatte
|
|||||||
import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols';
|
import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols';
|
||||||
import { isValidAMapCoordinate } from '../../integrations/amap';
|
import { isValidAMapCoordinate } from '../../integrations/amap';
|
||||||
import { FleetMap } from '../map/FleetMap';
|
import { FleetMap } from '../map/FleetMap';
|
||||||
import { InlineError, PageLoading, PanelEmpty } from '../shared/AsyncState';
|
import { InlineError, PageLoading, PanelEmpty, PanelLoading } from '../shared/AsyncState';
|
||||||
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
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';
|
||||||
@@ -280,7 +280,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
|
|||||||
{ title: '数据来源', dataIndex: 'protocol', width: 180, render: (_: unknown, item: LatestTelemetryValue) => <TelemetrySourceCell item={item} /> }
|
{ title: '数据来源', dataIndex: 'protocol', width: 180, render: (_: unknown, item: LatestTelemetryValue) => <TelemetrySourceCell item={item} /> }
|
||||||
], []);
|
], []);
|
||||||
const state = pending
|
const state = pending
|
||||||
? <div className="v2-telemetry-state" role="status"><strong>正在读取最新遥测</strong><span>按协议整理最新标量证据…</span></div>
|
? <PanelLoading className="v2-telemetry-loading" compact title="正在读取最新遥测" description="按协议整理最新标量证据。" />
|
||||||
: error
|
: error
|
||||||
? <div className="v2-telemetry-state is-error" role="alert"><strong>最新遥测不可用</strong><span>{error}</span></div>
|
? <div className="v2-telemetry-state is-error" role="alert"><strong>最新遥测不可用</strong><span>{error}</span></div>
|
||||||
: visibleMetrics.length === 0
|
: visibleMetrics.length === 0
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { canAdminister } from '../auth/session';
|
|||||||
import { formatZhNumber } from '../domain/formatters';
|
import { formatZhNumber } from '../domain/formatters';
|
||||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy';
|
import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||||
import { PanelEmpty } from '../shared/AsyncState';
|
import { PanelEmpty, PanelLoading } from '../shared/AsyncState';
|
||||||
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
||||||
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
||||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||||
@@ -328,7 +328,7 @@ export default function VehicleSearchWorkspace() {
|
|||||||
</div>}
|
</div>}
|
||||||
/>
|
/>
|
||||||
{candidates.isFetching && !candidates.data
|
{candidates.isFetching && !candidates.data
|
||||||
? <div className="v2-vehicle-recent-state" role="status"><span className="v2-spinner" /><span>正在读取授权车辆…</span></div>
|
? <PanelLoading className="v2-vehicle-directory-loading" compact title="正在读取授权车辆" description="车辆目录返回后会自动显示。" />
|
||||||
: candidates.isError
|
: candidates.isError
|
||||||
? <div className="v2-vehicle-recent-state is-error" role="alert"><span>{candidates.error instanceof Error ? candidates.error.message : '授权车辆加载失败'}</span><Button size="small" theme="light" onClick={() => candidates.refetch()}>重新加载</Button></div>
|
? <div className="v2-vehicle-recent-state is-error" role="alert"><span>{candidates.error instanceof Error ? candidates.error.message : '授权车辆加载失败'}</span><Button size="small" theme="light" onClick={() => candidates.refetch()}>重新加载</Button></div>
|
||||||
: pageVehicles.length
|
: pageVehicles.length
|
||||||
@@ -418,6 +418,6 @@ export default function VehicleSearchWorkspace() {
|
|||||||
/>}
|
/>}
|
||||||
{total > 0 ? <footer className="v2-vehicle-directory-pagination"><TablePagination page={page} totalPages={totalPages} info={`共 ${total.toLocaleString('zh-CN')} 辆 · 本页 ${pageVehicles.length.toLocaleString('zh-CN')} 辆`} disabled={candidates.isFetching} onPageChange={(next) => { setPage(next); setCandidatesOpen(false); }} /></footer> : null}
|
{total > 0 ? <footer className="v2-vehicle-directory-pagination"><TablePagination page={page} totalPages={totalPages} info={`共 ${total.toLocaleString('zh-CN')} 辆 · 本页 ${pageVehicles.length.toLocaleString('zh-CN')} 辆`} disabled={candidates.isFetching} onPageChange={(next) => { setPage(next); setCandidatesOpen(false); }} /></footer> : null}
|
||||||
</Card>
|
</Card>
|
||||||
{syncOpen ? <Suspense fallback={<Card className="v2-profile-sync-panel v2-profile-sync-loading" bodyStyle={{ padding: 0 }}><span role="status"><span className="v2-spinner" />正在加载主档同步工具…</span></Card>}><VehicleProfileSyncPanel onClose={() => setSyncOpen(false)} /></Suspense> : null}
|
{syncOpen ? <Suspense fallback={<Card className="v2-profile-sync-panel v2-profile-sync-loading" bodyStyle={{ padding: 0 }}><PanelLoading compact title="正在加载主档同步工具" description="同步配置就绪后会自动显示。" /></Card>}><VehicleProfileSyncPanel onClose={() => setSyncOpen(false)} /></Suspense> : null}
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.VehiclePage).toContain('<SegmentedTabs');
|
expect(corePageSources.VehiclePage).toContain('<SegmentedTabs');
|
||||||
expect(corePageSources.VehiclePage).toContain('<Descriptions className="v2-record-descriptions"');
|
expect(corePageSources.VehiclePage).toContain('<Descriptions className="v2-record-descriptions"');
|
||||||
expect(corePageSources.VehiclePage).toContain('<PanelEmpty');
|
expect(corePageSources.VehiclePage).toContain('<PanelEmpty');
|
||||||
|
expect(corePageSources.VehiclePage).toContain('<PanelLoading className="v2-telemetry-loading"');
|
||||||
expect(corePageSources.VehiclePage).toContain('className="v2-vehicle-not-found-state"');
|
expect(corePageSources.VehiclePage).toContain('className="v2-vehicle-not-found-state"');
|
||||||
expect(corePageSources.VehiclePage).not.toContain('<Empty image={<IconSearch');
|
expect(corePageSources.VehiclePage).not.toContain('<Empty image={<IconSearch');
|
||||||
expect(corePageSources.VehiclePage).not.toContain('<dl className="v2-record-list"');
|
expect(corePageSources.VehiclePage).not.toContain('<dl className="v2-record-list"');
|
||||||
@@ -135,7 +136,9 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.VehiclePage).not.toContain('<input');
|
expect(corePageSources.VehiclePage).not.toContain('<input');
|
||||||
expect(corePageSources.VehiclePage).not.toContain('<button');
|
expect(corePageSources.VehiclePage).not.toContain('<button');
|
||||||
expect(vehicleSearchSource).toContain("lazy(() => import('./VehicleProfileSyncPanel'))");
|
expect(vehicleSearchSource).toContain("lazy(() => import('./VehicleProfileSyncPanel'))");
|
||||||
|
expect(vehicleSearchSource).toContain('<PanelLoading className="v2-vehicle-directory-loading"');
|
||||||
expect(vehicleSearchSource).toContain('<Card className="v2-profile-sync-panel v2-profile-sync-loading"');
|
expect(vehicleSearchSource).toContain('<Card className="v2-profile-sync-panel v2-profile-sync-loading"');
|
||||||
|
expect(vehicleSearchSource).toContain('<PanelLoading compact title="正在加载主档同步工具"');
|
||||||
expect(profileSyncPanelSource).toContain('<Upload');
|
expect(profileSyncPanelSource).toContain('<Upload');
|
||||||
expect(profileSyncPanelSource).toContain('<Card className="v2-profile-sync-panel"');
|
expect(profileSyncPanelSource).toContain('<Card className="v2-profile-sync-panel"');
|
||||||
expect(profileSyncPanelSource).not.toContain('<input');
|
expect(profileSyncPanelSource).not.toContain('<input');
|
||||||
@@ -159,13 +162,15 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.MonitorPage).toContain('<PanelLoading className="v2-monitor-table-loading"');
|
expect(corePageSources.MonitorPage).toContain('<PanelLoading className="v2-monitor-table-loading"');
|
||||||
expect(corePageSources.MonitorPage).toContain('title="正在加载车辆详情"');
|
expect(corePageSources.MonitorPage).toContain('title="正在加载车辆详情"');
|
||||||
expect(corePageSources.MonitorPage).not.toContain('<table><thead><tr><th>车辆</th>');
|
expect(corePageSources.MonitorPage).not.toContain('<table><thead><tr><th>车辆</th>');
|
||||||
expect(sourceEvidenceSource).toContain("import { Button, Card, Input, Spin, Tag } from '@douyinfe/semi-ui'");
|
expect(sourceEvidenceSource).toContain("import { Button, Card, Input, Tag } from '@douyinfe/semi-ui'");
|
||||||
expect(sourceEvidenceSource).toContain('<Card className={`v2-source-evidence');
|
expect(sourceEvidenceSource).toContain('<Card className={`v2-source-evidence');
|
||||||
expect(sourceEvidenceSource).toContain('<Card className={`v2-source-evidence-card');
|
expect(sourceEvidenceSource).toContain('<Card className={`v2-source-evidence-card');
|
||||||
expect(sourceEvidenceSource).toContain('<WorkspaceDetailSideSheet');
|
expect(sourceEvidenceSource).toContain('<WorkspaceDetailSideSheet');
|
||||||
expect(sourceEvidenceSource).toContain('className="v2-source-evidence-sidesheet"');
|
expect(sourceEvidenceSource).toContain('className="v2-source-evidence-sidesheet"');
|
||||||
expect(sourceEvidenceSource).toContain('<Tag className="is-recommended"');
|
expect(sourceEvidenceSource).toContain('<Tag className="is-recommended"');
|
||||||
expect(sourceEvidenceSource).toContain('<Spin size="small"');
|
expect(sourceEvidenceSource).toContain('<PanelLoading className="v2-source-evidence-loading"');
|
||||||
|
expect(sourceEvidenceSource).not.toContain('<Spin');
|
||||||
|
expect(sourceEvidenceSource).toContain("placement={mobileLayout ? 'bottom' : 'right'}");
|
||||||
expect(sourceEvidenceSource).toContain('<PanelEmpty className="v2-source-evidence-empty"');
|
expect(sourceEvidenceSource).toContain('<PanelEmpty className="v2-source-evidence-empty"');
|
||||||
expect(sourceEvidenceSource).not.toContain('<article className={`v2-source-evidence-card');
|
expect(sourceEvidenceSource).not.toContain('<article className={`v2-source-evidence-card');
|
||||||
expect(sourceEvidenceSource).not.toContain('<section className={`v2-source-evidence');
|
expect(sourceEvidenceSource).not.toContain('<section className={`v2-source-evidence');
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ afterEach(() => {
|
|||||||
vi.mocked(useMobileLayout).mockReturnValue(false);
|
vi.mocked(useMobileLayout).mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loads all source evidence only after the user expands it', async () => {
|
test('loads all source evidence in a Semi right SideSheet without reflowing the desktop record', async () => {
|
||||||
const sourceEvidence = vi.spyOn(api, 'vehicleSourceEvidence').mockResolvedValue(evidence);
|
const sourceEvidence = vi.spyOn(api, 'vehicleSourceEvidence').mockResolvedValue(evidence);
|
||||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
|
||||||
@@ -57,10 +57,16 @@ test('loads all source evidence only after the user expands it', async () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '查看全部来源' }));
|
fireEvent.click(screen.getByRole('button', { name: '查看全部来源' }));
|
||||||
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
|
||||||
expect(await screen.findByText('北斗平台')).toBeInTheDocument();
|
expect(await screen.findByText('北斗平台')).toBeInTheDocument();
|
||||||
expect(view.container.querySelectorAll('.v2-source-evidence-card.semi-card')).toHaveLength(3);
|
expect(view.container.querySelector('.v2-source-evidence > .semi-card-body > .v2-source-evidence-body')).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.v2-source-evidence-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet');
|
||||||
|
expect(document.querySelector('.v2-source-evidence-sidesheet .semi-sidesheet-inner')).toHaveStyle({ width: 'min(680px, calc(100vw - 24px))' });
|
||||||
|
expect(document.querySelectorAll('.v2-source-evidence-sidesheet .v2-source-evidence-card.semi-card')).toHaveLength(3);
|
||||||
expect(screen.getAllByText('当前推荐').every((item) => item.closest('.semi-tag'))).toBe(true);
|
expect(screen.getAllByText('当前推荐').every((item) => item.closest('.semi-tag'))).toBe(true);
|
||||||
expect(screen.getAllByText('当前推荐').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('当前推荐').length).toBeGreaterThan(0);
|
||||||
expect(screen.queryByText('13307795425')).not.toBeInTheDocument();
|
expect(screen.queryByText('13307795425')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '关闭来源证据' }));
|
||||||
|
await waitFor(() => expect(view.container.querySelector('.v2-source-evidence')).not.toHaveClass('is-open'));
|
||||||
client.clear();
|
client.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { Button, Card, Input, Spin, Tag } from '@douyinfe/semi-ui';
|
import { Button, Card, Input, Tag } from '@douyinfe/semi-ui';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import type { VehicleLocationSourceEvidence, VehicleMileageSourceEvidence } from '../../api/types';
|
import type { VehicleLocationSourceEvidence, VehicleMileageSourceEvidence } from '../../api/types';
|
||||||
import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols';
|
import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols';
|
||||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { QUERY_MEMORY } from '../queryPolicy';
|
import { QUERY_MEMORY } from '../queryPolicy';
|
||||||
import { PanelEmpty } from './AsyncState';
|
import { PanelEmpty, PanelLoading } from './AsyncState';
|
||||||
import { WorkspaceDetailSideSheet } from './WorkspaceDetailSideSheet';
|
import { WorkspaceDetailSideSheet } from './WorkspaceDetailSideSheet';
|
||||||
|
|
||||||
function today() {
|
function today() {
|
||||||
@@ -117,7 +117,7 @@ export function VehicleSourceEvidencePanel({
|
|||||||
<label><span>里程日期</span><Input aria-label="里程日期" type="date" value={date} max={today()} onChange={setDate} /></label>
|
<label><span>里程日期</span><Input aria-label="里程日期" type="date" value={date} max={today()} onChange={setDate} /></label>
|
||||||
<small>终端已脱敏;推荐结果来自后台选举,不改动原始证据。</small>
|
<small>终端已脱敏;推荐结果来自后台选举,不改动原始证据。</small>
|
||||||
</div>
|
</div>
|
||||||
{query.isPending ? <div className="v2-source-evidence-state" role="status"><Spin size="small" />正在读取全部来源证据…</div> : null}
|
{query.isPending ? <PanelLoading className="v2-source-evidence-loading" compact title="正在读取全部来源证据" description="位置、里程与质量选举结果返回后会自动更新。" /> : null}
|
||||||
{query.isError ? <div className="v2-source-evidence-state is-error"><span>{query.error instanceof Error ? query.error.message : '来源证据读取失败'}</span><Button theme="borderless" type="danger" size="small" onClick={() => void query.refetch()}>重试</Button></div> : null}
|
{query.isError ? <div className="v2-source-evidence-state is-error"><span>{query.error instanceof Error ? query.error.message : '来源证据读取失败'}</span><Button theme="borderless" type="danger" size="small" onClick={() => void query.refetch()}>重试</Button></div> : null}
|
||||||
{query.data ? <>
|
{query.data ? <>
|
||||||
<div className="v2-source-evidence-summary">
|
<div className="v2-source-evidence-summary">
|
||||||
@@ -138,13 +138,13 @@ export function VehicleSourceEvidencePanel({
|
|||||||
<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>
|
||||||
{expanded && !mobileLayout ? evidenceBody : null}
|
|
||||||
</Card>
|
</Card>
|
||||||
{mobileLayout ? <WorkspaceDetailSideSheet
|
<WorkspaceDetailSideSheet
|
||||||
className="v2-source-evidence-sidesheet"
|
className="v2-source-evidence-sidesheet"
|
||||||
visible={expanded}
|
visible={expanded}
|
||||||
placement="bottom"
|
placement={mobileLayout ? 'bottom' : 'right'}
|
||||||
height="min(88dvh, 780px)"
|
width={mobileLayout ? undefined : 'min(680px, calc(100vw - 24px))'}
|
||||||
|
height={mobileLayout ? 'min(88dvh, 780px)' : undefined}
|
||||||
ariaLabel="位置与里程来源证据"
|
ariaLabel="位置与里程来源证据"
|
||||||
closeLabel="关闭来源证据"
|
closeLabel="关闭来源证据"
|
||||||
title="位置与里程来源"
|
title="位置与里程来源"
|
||||||
@@ -154,6 +154,6 @@ export function VehicleSourceEvidencePanel({
|
|||||||
onCancel={() => setExpanded(false)}
|
onCancel={() => setExpanded(false)}
|
||||||
>
|
>
|
||||||
<div className="v2-source-evidence-sheet-body">{evidenceBody}</div>
|
<div className="v2-source-evidence-sheet-body">{evidenceBody}</div>
|
||||||
</WorkspaceDetailSideSheet> : null}
|
</WorkspaceDetailSideSheet>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18338,6 +18338,58 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 681px) {
|
||||||
|
.v2-source-evidence-sidesheet .semi-sidesheet-inner {
|
||||||
|
border-radius: 16px 0 0 16px;
|
||||||
|
box-shadow: -18px 0 48px rgba(25, 45, 72, .18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .semi-sidesheet-header {
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 13px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-sheet-body > .v2-source-evidence-body {
|
||||||
|
padding: 14px 14px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-toolbar {
|
||||||
|
margin: -14px -14px 14px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-group > div {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-loading.v2-state-surface {
|
||||||
|
min-height: 190px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-color: #d9e3ee;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-directory-v3 .v2-vehicle-directory-loading.v2-state-surface {
|
||||||
|
min-height: 220px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-record-v3 .v2-telemetry-loading.v2-state-surface {
|
||||||
|
min-height: 190px;
|
||||||
|
margin: 10px;
|
||||||
|
border-color: #dce5ef;
|
||||||
|
background: #fbfdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-profile-sync-loading .v2-state-surface {
|
||||||
|
min-height: 180px;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-vehicle-record-v3.v2-vehicle-record-page {
|
.v2-vehicle-record-v3.v2-vehicle-record-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user