diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts index 78ef0612..34b8fd20 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -117,9 +117,11 @@ describe('V2 production entry', () => { expect(corePageSources.MonitorPage).toContain('车辆'); - 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(' ({ useMobileLayout: vi.fn(() => false) })); + const evidence: VehicleSourceEvidence = { vin: 'VIN-001', plate: '粤A12345', @@ -40,6 +43,7 @@ const evidence: VehicleSourceEvidence = { afterEach(() => { cleanup(); vi.restoreAllMocks(); + vi.mocked(useMobileLayout).mockReturnValue(false); }); 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(); 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(); + + 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(); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx b/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx index 79511ec9..a1e378c8 100644 --- a/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx +++ b/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx @@ -1,9 +1,11 @@ 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 { api } from '../../api/client'; import type { VehicleLocationSourceEvidence, VehicleMileageSourceEvidence } from '../../api/types'; import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols'; +import { useMobileLayout } from '../hooks/useMobileLayout'; +import { useSideSheetA11y } from '../hooks/useSideSheetA11y'; import { QUERY_MEMORY } from '../queryPolicy'; function today() { @@ -86,6 +88,7 @@ export function VehicleSourceEvidencePanel({ open?: boolean; onOpenChange?: (open: boolean) => void; }) { + const mobileLayout = useMobileLayout(); const [internalOpen, setInternalOpen] = useState(false); const expanded = open ?? internalOpen; const setExpanded = (value: boolean) => { @@ -107,13 +110,9 @@ export function VehicleSourceEvidencePanel({ if (sourceCount <= 2) return '当前车辆来源较少,仅展示实际存在的证据'; return `已读取 ${query.data.locationSources.length} 个位置来源、${query.data.mileageSources.length} 个里程来源`; }, [query.data, sourceCount]); + useSideSheetA11y(mobileLayout && expanded, '.v2-source-evidence-sidesheet', 'v2-source-evidence-sheet', '位置与里程来源证据', '关闭来源证据'); - return -
-
位置与里程来源{description}
- -
- {expanded ?
+ const evidenceBody =
终端已脱敏;推荐结果来自后台选举,不改动原始证据。 @@ -131,6 +130,27 @@ export function VehicleSourceEvidencePanel({ {query.data.mileageSources.length ?
{query.data.mileageDate} 里程来源日里程差 {number(query.data.comparison.dailyMileageDeltaKm)} km
{query.data.mileageSources.map((source, index) => )}
: null} {!query.data.locationSources.length && !query.data.mileageSources.length ? : null} : null} -
: null} - ; +
; + + return <> + +
+
位置与里程来源{description}
+ +
+ {expanded && !mobileLayout ? evidenceBody : null} +
+ {mobileLayout ? 位置与里程来源{description}
} + footer={null} + onCancel={() => setExpanded(false)} + > +
{evidenceBody}
+ : null} + ; } diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index 280bb108..2929ad4e 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -16087,6 +16087,86 @@ 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 { display: flex; width: 100%;