refine mobile vehicle source evidence
This commit is contained in:
@@ -117,9 +117,11 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.MonitorPage).toContain('<Empty className="v2-monitor-table-empty"');
|
expect(corePageSources.MonitorPage).toContain('<Empty className="v2-monitor-table-empty"');
|
||||||
expect(corePageSources.MonitorPage).toContain('<Spin size="small" tip="正在更新车辆实时数据…"');
|
expect(corePageSources.MonitorPage).toContain('<Spin size="small" tip="正在更新车辆实时数据…"');
|
||||||
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, Empty, Input, Spin, Tag } from '@douyinfe/semi-ui'");
|
expect(sourceEvidenceSource).toContain("import { Button, Card, Empty, Input, SideSheet, Spin, 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('<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('<Spin size="small"');
|
||||||
expect(sourceEvidenceSource).toContain('<Empty className="v2-source-evidence-empty"');
|
expect(sourceEvidenceSource).toContain('<Empty className="v2-source-evidence-empty"');
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
|
|||||||
import { afterEach, expect, test, vi } from 'vitest';
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import type { VehicleSourceEvidence } from '../../api/types';
|
import type { VehicleSourceEvidence } from '../../api/types';
|
||||||
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { VehicleSourceEvidencePanel } from './VehicleSourceEvidencePanel';
|
import { VehicleSourceEvidencePanel } from './VehicleSourceEvidencePanel';
|
||||||
|
|
||||||
|
vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: vi.fn(() => false) }));
|
||||||
|
|
||||||
const evidence: VehicleSourceEvidence = {
|
const evidence: VehicleSourceEvidence = {
|
||||||
vin: 'VIN-001',
|
vin: 'VIN-001',
|
||||||
plate: '粤A12345',
|
plate: '粤A12345',
|
||||||
@@ -40,6 +43,7 @@ const evidence: VehicleSourceEvidence = {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
vi.mocked(useMobileLayout).mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loads all source evidence only after the user expands it', async () => {
|
test('loads all source evidence only after the user expands it', async () => {
|
||||||
@@ -59,3 +63,24 @@ test('loads all source evidence only after the user expands it', async () => {
|
|||||||
expect(screen.queryByText('13307795425')).not.toBeInTheDocument();
|
expect(screen.queryByText('13307795425')).not.toBeInTheDocument();
|
||||||
client.clear();
|
client.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('opens source evidence in a Semi bottom SideSheet on mobile without expanding the page card', async () => {
|
||||||
|
vi.mocked(useMobileLayout).mockReturnValue(true);
|
||||||
|
const sourceEvidence = vi.spyOn(api, 'vehicleSourceEvidence').mockResolvedValue(evidence);
|
||||||
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
|
||||||
|
const view = render(<QueryClientProvider client={client}><VehicleSourceEvidencePanel vin="VIN-001" /></QueryClientProvider>);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '查看全部来源' }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
|
||||||
|
expect(view.container.querySelector('.v2-source-evidence.is-mobile-sheet')).toHaveClass('is-open');
|
||||||
|
expect(view.container.querySelector('.v2-source-evidence > .semi-card-body > .v2-source-evidence-body')).not.toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('北斗平台')).toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.v2-source-evidence-sidesheet')).toBeInTheDocument();
|
||||||
|
expect(document.querySelectorAll('.v2-source-evidence-sidesheet .v2-source-evidence-card')).toHaveLength(3);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '关闭来源证据' }));
|
||||||
|
await waitFor(() => expect(view.container.querySelector('.v2-source-evidence')).not.toHaveClass('is-open'));
|
||||||
|
client.clear();
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button, Card, Empty, Input, Spin, Tag } from '@douyinfe/semi-ui';
|
import { Button, Card, Empty, Input, SideSheet, Spin, 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 { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
||||||
import { QUERY_MEMORY } from '../queryPolicy';
|
import { QUERY_MEMORY } from '../queryPolicy';
|
||||||
|
|
||||||
function today() {
|
function today() {
|
||||||
@@ -86,6 +88,7 @@ export function VehicleSourceEvidencePanel({
|
|||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const mobileLayout = useMobileLayout();
|
||||||
const [internalOpen, setInternalOpen] = useState(false);
|
const [internalOpen, setInternalOpen] = useState(false);
|
||||||
const expanded = open ?? internalOpen;
|
const expanded = open ?? internalOpen;
|
||||||
const setExpanded = (value: boolean) => {
|
const setExpanded = (value: boolean) => {
|
||||||
@@ -107,13 +110,9 @@ export function VehicleSourceEvidencePanel({
|
|||||||
if (sourceCount <= 2) return '当前车辆来源较少,仅展示实际存在的证据';
|
if (sourceCount <= 2) return '当前车辆来源较少,仅展示实际存在的证据';
|
||||||
return `已读取 ${query.data.locationSources.length} 个位置来源、${query.data.mileageSources.length} 个里程来源`;
|
return `已读取 ${query.data.locationSources.length} 个位置来源、${query.data.mileageSources.length} 个里程来源`;
|
||||||
}, [query.data, sourceCount]);
|
}, [query.data, sourceCount]);
|
||||||
|
useSideSheetA11y(mobileLayout && expanded, '.v2-source-evidence-sidesheet', 'v2-source-evidence-sheet', '位置与里程来源证据', '关闭来源证据');
|
||||||
|
|
||||||
return <Card className={`v2-source-evidence${compact ? ' is-compact' : ''}${expanded ? ' is-open' : ''}`} bodyStyle={{ padding: 0 }}>
|
const evidenceBody = <div className="v2-source-evidence-body">
|
||||||
<header className="v2-source-evidence-trigger">
|
|
||||||
<div><strong>位置与里程来源</strong><span>{description}</span></div>
|
|
||||||
<Button theme="light" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>{expanded ? '收起来源' : '查看全部来源'}</Button>
|
|
||||||
</header>
|
|
||||||
{expanded ? <div className="v2-source-evidence-body">
|
|
||||||
<div className="v2-source-evidence-toolbar">
|
<div className="v2-source-evidence-toolbar">
|
||||||
<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>
|
||||||
@@ -131,6 +130,27 @@ export function VehicleSourceEvidencePanel({
|
|||||||
{query.data.mileageSources.length ? <section className="v2-source-evidence-group"><header><strong>{query.data.mileageDate} 里程来源</strong><span>日里程差 {number(query.data.comparison.dailyMileageDeltaKm)} km</span></header><div>{query.data.mileageSources.map((source, index) => <MileageSourceCard key={`${source.protocol}-${source.sourceLabel}-${source.terminalLabel}-${index}`} source={source} />)}</div></section> : null}
|
{query.data.mileageSources.length ? <section className="v2-source-evidence-group"><header><strong>{query.data.mileageDate} 里程来源</strong><span>日里程差 {number(query.data.comparison.dailyMileageDeltaKm)} km</span></header><div>{query.data.mileageSources.map((source, index) => <MileageSourceCard key={`${source.protocol}-${source.sourceLabel}-${source.terminalLabel}-${index}`} source={source} />)}</div></section> : null}
|
||||||
{!query.data.locationSources.length && !query.data.mileageSources.length ? <Empty className="v2-source-evidence-empty" title="暂无来源证据" description="该车辆当前没有可展示的位置或里程来源。" /> : null}
|
{!query.data.locationSources.length && !query.data.mileageSources.length ? <Empty className="v2-source-evidence-empty" title="暂无来源证据" description="该车辆当前没有可展示的位置或里程来源。" /> : null}
|
||||||
</> : null}
|
</> : null}
|
||||||
</div> : null}
|
</div>;
|
||||||
</Card>;
|
|
||||||
|
return <>
|
||||||
|
<Card className={`v2-source-evidence${compact ? ' is-compact' : ''}${expanded ? ' is-open' : ''}${mobileLayout ? ' is-mobile-sheet' : ''}`} bodyStyle={{ padding: 0 }}>
|
||||||
|
<header className="v2-source-evidence-trigger">
|
||||||
|
<div><strong>位置与里程来源</strong><span>{description}</span></div>
|
||||||
|
<Button theme="light" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>{expanded ? '收起来源' : '查看全部来源'}</Button>
|
||||||
|
</header>
|
||||||
|
{expanded && !mobileLayout ? evidenceBody : null}
|
||||||
|
</Card>
|
||||||
|
{mobileLayout ? <SideSheet
|
||||||
|
className="v2-source-evidence-sidesheet"
|
||||||
|
visible={expanded}
|
||||||
|
placement="bottom"
|
||||||
|
height="min(88dvh, 780px)"
|
||||||
|
aria-label="位置与里程来源证据"
|
||||||
|
title={<div className="v2-source-evidence-sheet-title"><strong>位置与里程来源</strong><span>{description}</span></div>}
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setExpanded(false)}
|
||||||
|
>
|
||||||
|
<div className="v2-source-evidence-sheet-body">{evidenceBody}</div>
|
||||||
|
</SideSheet> : null}
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16087,6 +16087,86 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .semi-sidesheet-inner {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
background: #f4f7fb;
|
||||||
|
box-shadow: 0 -18px 48px rgba(25, 45, 72, .18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .semi-sidesheet-header {
|
||||||
|
min-height: 68px;
|
||||||
|
border-bottom: 1px solid #dfe7f0;
|
||||||
|
background: rgba(255, 255, 255, .98);
|
||||||
|
padding: 11px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sheet-title {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sheet-title > strong {
|
||||||
|
color: #1f3046;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: -.015em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sheet-title > span {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #7c899b;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .semi-sidesheet-body {
|
||||||
|
overflow: auto;
|
||||||
|
background: #f4f7fb;
|
||||||
|
padding: 0;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sheet-body > .v2-source-evidence-body {
|
||||||
|
border-top: 0;
|
||||||
|
padding: 10px 9px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
margin: -10px -9px 10px;
|
||||||
|
border-bottom: 1px solid #e2e9f1;
|
||||||
|
background: rgba(255, 255, 255, .97);
|
||||||
|
padding: 10px 12px;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-summary {
|
||||||
|
border-color: #d9e3ee;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-group > header {
|
||||||
|
padding-inline: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-group > div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-card {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 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