feat: unify detail side sheets
This commit is contained in:
@@ -12,6 +12,14 @@ const shanghaiDateTimeFormatter = new Intl.DateTimeFormat('sv-SE', {
|
|||||||
|
|
||||||
export type DateTimeSourceZone = 'preserve' | 'utc';
|
export type DateTimeSourceZone = 'preserve' | 'utc';
|
||||||
|
|
||||||
|
export const vehicleOperationStatusLabels = {
|
||||||
|
unknown: '待维护',
|
||||||
|
active: '运营中',
|
||||||
|
inactive: '停运',
|
||||||
|
maintenance: '维保中',
|
||||||
|
retired: '已退役'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export function formatZhNumber(value: number, maximumFractionDigits = 0) {
|
export function formatZhNumber(value: number, maximumFractionDigits = 0) {
|
||||||
let formatter = zhNumberFormatters.get(maximumFractionDigits);
|
let formatter = zhNumberFormatters.get(maximumFractionDigits);
|
||||||
if (!formatter) {
|
if (!formatter) {
|
||||||
|
|||||||
@@ -262,8 +262,9 @@ test('renders mobile access vehicles as selectable Semi cards', async () => {
|
|||||||
expect(within(dialog).getByRole('button', { name: '收起 JT808 完整证据' })).toHaveAttribute('aria-expanded', 'true');
|
expect(within(dialog).getByRole('button', { name: '收起 JT808 完整证据' })).toHaveAttribute('aria-expanded', 'true');
|
||||||
expect(mocks.accessVehicles).toHaveBeenCalledTimes(vehicleRequestsBeforeEvidenceToggle);
|
expect(mocks.accessVehicles).toHaveBeenCalledTimes(vehicleRequestsBeforeEvidenceToggle);
|
||||||
expect(mocks.accessSummary).toHaveBeenCalledTimes(summaryRequestsBeforeEvidenceToggle);
|
expect(mocks.accessSummary).toHaveBeenCalledTimes(summaryRequestsBeforeEvidenceToggle);
|
||||||
expect(document.querySelector('.v2-access-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-access-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet');
|
||||||
expect(document.querySelector('.v2-access-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(84dvh, 740px)' });
|
expect(document.querySelector('.v2-access-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(84dvh, 740px)' });
|
||||||
|
expect(document.querySelector('.v2-access-detail-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('部分来源异常');
|
||||||
fireEvent.click(within(dialog).getByRole('button', { name: '关闭车辆接入详情' }));
|
fireEvent.click(within(dialog).getByRole('button', { name: '关闭车辆接入详情' }));
|
||||||
expect(action).toHaveAttribute('aria-pressed', 'false');
|
expect(action).toHaveAttribute('aria-pressed', 'false');
|
||||||
expect(action).toHaveAttribute('aria-expanded', 'false');
|
expect(action).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
|||||||
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
||||||
import { TablePagination } from '../shared/TablePagination';
|
import { TablePagination } from '../shared/TablePagination';
|
||||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||||
|
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
||||||
@@ -149,8 +150,11 @@ const ProtocolState = memo(function ProtocolState({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function ConnectionTag({ row }: { row: AccessVehicleRow }) {
|
function ConnectionTag({ row }: { row: AccessVehicleRow }) {
|
||||||
const color = row.connectionState === 'healthy' ? 'green' : row.connectionState === 'not_connected' || row.connectionState === 'offline' ? 'red' : 'orange';
|
return <Tag className="v2-access-connection-tag" color={connectionTagColor(row.connectionState)} type="light" size="small">{connectionLabels[row.connectionState]}</Tag>;
|
||||||
return <Tag className="v2-access-connection-tag" color={color} type="light" size="small">{connectionLabels[row.connectionState]}</Tag>;
|
}
|
||||||
|
|
||||||
|
function connectionTagColor(state: AccessVehicleRow['connectionState']) {
|
||||||
|
return state === 'healthy' ? 'green' as const : state === 'not_connected' || state === 'offline' ? 'red' as const : 'orange' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectionState = memo(function ConnectionState({ row }: { row: AccessVehicleRow }) {
|
const ConnectionState = memo(function ConnectionState({ row }: { row: AccessVehicleRow }) {
|
||||||
@@ -352,7 +356,6 @@ export default function AccessPage() {
|
|||||||
}, [mobileLayout]);
|
}, [mobileLayout]);
|
||||||
const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN);
|
const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN);
|
||||||
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
||||||
useSideSheetA11y(Boolean(selected), '.v2-access-detail-sidesheet', 'v2-access-detail', '车辆接入详情', '关闭车辆接入详情');
|
|
||||||
useSideSheetA11y(editable && governanceOpen, '.v2-access-governance-sidesheet', 'v2-access-governance', '接入治理配置', '关闭接入治理配置');
|
useSideSheetA11y(editable && governanceOpen, '.v2-access-governance-sidesheet', 'v2-access-governance', '接入治理配置', '关闭接入治理配置');
|
||||||
useSideSheetA11y(mobileFiltersOpen, '.v2-access-mobile-filter-sidesheet', 'v2-access-mobile-filters', '接入车辆筛选', '关闭接入车辆筛选');
|
useSideSheetA11y(mobileFiltersOpen, '.v2-access-mobile-filter-sidesheet', 'v2-access-mobile-filters', '接入车辆筛选', '关闭接入车辆筛选');
|
||||||
const syncURL = (filters: Filters) => { const next = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) next.set(key, value); }); setSearchParams(next, { replace: true }); };
|
const syncURL = (filters: Filters) => { const next = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) next.set(key, value); }); setSearchParams(next, { replace: true }); };
|
||||||
@@ -487,18 +490,21 @@ export default function AccessPage() {
|
|||||||
<footer><TablePagination page={page} totalPages={totalPages} info={`共 ${(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆`} onPageChange={(next) => setOffset((next - 1) * limit)} pageSize={mobileLayout ? undefined : limit} pageSizeLabel="每页车辆数" onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={mobileLayout ? undefined : [{ value: 20, label: '20 辆/页' }, { value: 50, label: '50 辆/页' }, { value: 100, label: '100 辆/页' }]} /></footer>
|
<footer><TablePagination page={page} totalPages={totalPages} info={`共 ${(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆`} onPageChange={(next) => setOffset((next - 1) * limit)} pageSize={mobileLayout ? undefined : limit} pageSizeLabel="每页车辆数" onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={mobileLayout ? undefined : [{ value: 20, label: '20 辆/页' }, { value: 50, label: '50 辆/页' }, { value: 100, label: '100 辆/页' }]} /></footer>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<SideSheet
|
<WorkspaceDetailSideSheet
|
||||||
className="v2-access-detail-sidesheet"
|
className="v2-access-detail-sidesheet"
|
||||||
visible={Boolean(selected)}
|
visible={Boolean(selected)}
|
||||||
aria-label="车辆接入详情"
|
ariaLabel="车辆接入详情"
|
||||||
placement={mobileLayout ? 'bottom' : 'right'}
|
placement={mobileLayout ? 'bottom' : 'right'}
|
||||||
width={mobileLayout ? undefined : 520}
|
width={mobileLayout ? undefined : 520}
|
||||||
height={mobileLayout ? 'min(84dvh, 740px)' : undefined}
|
height={mobileLayout ? 'min(84dvh, 740px)' : undefined}
|
||||||
title={<div className="v2-access-sheet-title"><strong>车辆接入详情</strong><span>{selected ? `${selected.plate || '未绑定车牌'} · ${selected.vin}` : '来源、在线状态与接入证据'}</span></div>}
|
title="车辆接入详情"
|
||||||
|
description={selected ? `${selected.plate || '未绑定车牌'} · ${selected.vin}` : '来源、在线状态与接入证据'}
|
||||||
|
badge={selected ? connectionLabels[selected.connectionState] : undefined}
|
||||||
|
badgeColor={selected ? connectionTagColor(selected.connectionState) : 'grey'}
|
||||||
onCancel={() => setSelectedVIN('')}
|
onCancel={() => setSelectedVIN('')}
|
||||||
>
|
>
|
||||||
{selected ? <VehicleInspector key={selected.vin} row={selected} onClose={() => setSelectedVIN('')} sheet mobile={mobileLayout} /> : null}
|
{selected ? <VehicleInspector key={selected.vin} row={selected} onClose={() => setSelectedVIN('')} sheet mobile={mobileLayout} /> : null}
|
||||||
</SideSheet>
|
</WorkspaceDetailSideSheet>
|
||||||
{editable ? <SideSheet
|
{editable ? <SideSheet
|
||||||
className="v2-access-governance-sidesheet"
|
className="v2-access-governance-sidesheet"
|
||||||
visible={governanceOpen}
|
visible={governanceOpen}
|
||||||
|
|||||||
@@ -269,8 +269,9 @@ test('renders only selectable Semi alert cards on mobile', async () => {
|
|||||||
expect(action).toHaveAttribute('aria-pressed', 'true');
|
expect(action).toHaveAttribute('aria-pressed', 'true');
|
||||||
expect(action).toHaveAttribute('aria-expanded', 'true');
|
expect(action).toHaveAttribute('aria-expanded', 'true');
|
||||||
expect(await screen.findByRole('dialog', { name: '告警事件详情' })).toBeInTheDocument();
|
expect(await screen.findByRole('dialog', { name: '告警事件详情' })).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-alert-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-alert-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet');
|
||||||
expect(document.querySelector('.v2-alert-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(90dvh, 800px)' });
|
expect(document.querySelector('.v2-alert-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(90dvh, 800px)' });
|
||||||
|
expect(document.querySelector('.v2-alert-detail-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('未处理');
|
||||||
expect(document.querySelector('.v2-alert-detail-sidesheet .v2-alert-inspector-heading')).not.toBeInTheDocument();
|
expect(document.querySelector('.v2-alert-detail-sidesheet .v2-alert-inspector-heading')).not.toBeInTheDocument();
|
||||||
const mobileDisposition = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-disposition-card');
|
const mobileDisposition = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-disposition-card');
|
||||||
const mobileProgress = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-progress-card');
|
const mobileProgress = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-progress-card');
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { ProtocolTag } from '../shared/ProtocolTag';
|
|||||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||||
import { TablePagination } from '../shared/TablePagination';
|
import { TablePagination } from '../shared/TablePagination';
|
||||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||||
|
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||||
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
||||||
@@ -63,8 +64,11 @@ function SeverityTag({ severity }: Pick<AlertEvent, 'severity'>) {
|
|||||||
return <Tag className={`v2-alert-severity is-${severity}`} color={color} type="light" size="small"><i />{severityLabels[severity]}</Tag>;
|
return <Tag className={`v2-alert-severity is-${severity}`} color={color} type="light" size="small"><i />{severityLabels[severity]}</Tag>;
|
||||||
}
|
}
|
||||||
function StatusTag({ status }: Pick<AlertEvent, 'status'>) {
|
function StatusTag({ status }: Pick<AlertEvent, 'status'>) {
|
||||||
const color = status === 'unprocessed' ? 'red' : status === 'processing' ? 'blue' : status === 'recovered' ? 'green' : 'grey';
|
return <Tag className={`v2-alert-status is-${status}`} color={alertStatusColor(status)} type="light" size="small">{statusLabels[status]}</Tag>;
|
||||||
return <Tag className={`v2-alert-status is-${status}`} color={color} type="light" size="small">{statusLabels[status]}</Tag>;
|
}
|
||||||
|
|
||||||
|
function alertStatusColor(status: AlertStatus) {
|
||||||
|
return status === 'unprocessed' ? 'red' as const : status === 'processing' ? 'blue' as const : status === 'recovered' ? 'green' as const : 'grey' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertTime({ value, className = '' }: { value?: string; className?: string }) {
|
function AlertTime({ value, className = '' }: { value?: string; className?: string }) {
|
||||||
@@ -193,7 +197,6 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
|||||||
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
||||||
useSideSheetA11y(!mobileLayout && advancedOpen, '.v2-alert-filter-sidesheet', 'v2-alert-advanced-filters', '告警高级筛选', '关闭告警高级筛选');
|
useSideSheetA11y(!mobileLayout && advancedOpen, '.v2-alert-filter-sidesheet', 'v2-alert-advanced-filters', '告警高级筛选', '关闭告警高级筛选');
|
||||||
useSideSheetA11y(mobileFiltersOpen, '.v2-alert-mobile-filter-sidesheet', 'v2-alert-mobile-filters', '告警事件筛选', '关闭告警事件筛选');
|
useSideSheetA11y(mobileFiltersOpen, '.v2-alert-mobile-filter-sidesheet', 'v2-alert-mobile-filters', '告警事件筛选', '关闭告警事件筛选');
|
||||||
useSideSheetA11y(mobileLayout && Boolean(selectedID), '.v2-alert-detail-sidesheet', 'v2-alert-detail', '告警事件详情', '关闭告警详情');
|
|
||||||
const applyDraft = () => {
|
const applyDraft = () => {
|
||||||
setFilters(draft);
|
setFilters(draft);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
@@ -337,17 +340,21 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
|||||||
<div className="v2-alert-advanced-filter">{advancedFilterFields}</div>
|
<div className="v2-alert-advanced-filter">{advancedFilterFields}</div>
|
||||||
</SideSheet>
|
</SideSheet>
|
||||||
: null}
|
: null}
|
||||||
<SideSheet
|
<WorkspaceDetailSideSheet
|
||||||
className="v2-alert-detail-sidesheet"
|
className="v2-alert-detail-sidesheet"
|
||||||
visible={mobileLayout && Boolean(selectedID)}
|
visible={mobileLayout && Boolean(selectedID)}
|
||||||
aria-label="告警事件详情"
|
ariaLabel="告警事件详情"
|
||||||
|
closeLabel="关闭告警详情"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
height="min(90dvh, 800px)"
|
height="min(90dvh, 800px)"
|
||||||
title={<div className="v2-alert-sheet-title"><strong>告警详情</strong><span>{selectedEvent ? `${selectedEvent.plate || selectedEvent.vin} · ${selectedEvent.ruleName}` : '证据、状态与处置履历'}</span></div>}
|
title="告警详情"
|
||||||
|
description={selectedEvent ? `${selectedEvent.plate || selectedEvent.vin} · ${selectedEvent.ruleName}` : '证据、状态与处置履历'}
|
||||||
|
badge={selectedEvent ? statusLabels[selectedEvent.status] : undefined}
|
||||||
|
badgeColor={selectedEvent ? alertStatusColor(selectedEvent.status) : 'grey'}
|
||||||
onCancel={closeDetail}
|
onCancel={closeDetail}
|
||||||
>
|
>
|
||||||
{mobileLayout && selectedID ? <EventInspector event={selectedEvent} note={note} acting={action.isPending} actionError={action.error instanceof Error ? action.error.message : undefined} editable={editable} onNote={setNote} onAction={(name) => { const event = detail.data; if (event) action.mutate({ name, event }); }} onClose={closeDetail} sheet /> : null}
|
{mobileLayout && selectedID ? <EventInspector event={selectedEvent} note={note} acting={action.isPending} actionError={action.error instanceof Error ? action.error.message : undefined} editable={editable} onNote={setNote} onAction={(name) => { const event = detail.data; if (event) action.mutate({ name, event }); }} onClose={closeDetail} sheet /> : null}
|
||||||
</SideSheet>
|
</WorkspaceDetailSideSheet>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,8 +267,9 @@ test('renders only selectable Semi history cards on mobile', async () => {
|
|||||||
expect(action).toHaveAttribute('aria-pressed', 'true');
|
expect(action).toHaveAttribute('aria-pressed', 'true');
|
||||||
expect(action).toHaveAttribute('aria-expanded', 'true');
|
expect(action).toHaveAttribute('aria-expanded', 'true');
|
||||||
expect(await screen.findByRole('dialog', { name: '历史数据详情' })).toBeInTheDocument();
|
expect(await screen.findByRole('dialog', { name: '历史数据详情' })).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-history-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-history-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet');
|
||||||
expect(document.querySelector('.v2-history-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 720px)' });
|
expect(document.querySelector('.v2-history-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 720px)' });
|
||||||
|
expect(document.querySelector('.v2-history-detail-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('正常');
|
||||||
expect(await screen.findByText('MOBILEVIN-evidence')).toBeInTheDocument();
|
expect(await screen.findByText('MOBILEVIN-evidence')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '关闭历史数据详情' }));
|
fireEvent.click(screen.getByRole('button', { name: '关闭历史数据详情' }));
|
||||||
expect(action).toHaveAttribute('aria-pressed', 'false');
|
expect(action).toHaveAttribute('aria-pressed', 'false');
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { PlatformTime } from '../shared/PlatformTime';
|
|||||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||||
import { TablePagination } from '../shared/TablePagination';
|
import { TablePagination } from '../shared/TablePagination';
|
||||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||||
|
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||||
import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail';
|
import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
@@ -356,7 +357,6 @@ export default function HistoryPage() {
|
|||||||
setSelectedRowRef({ scope: dataScope, id: '' });
|
setSelectedRowRef({ scope: dataScope, id: '' });
|
||||||
}, [dataScope]);
|
}, [dataScope]);
|
||||||
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
||||||
useSideSheetA11y(mobileLayout && Boolean(selectedRow), '.v2-history-detail-sidesheet', 'v2-history-detail', '历史数据详情', '关闭历史数据详情');
|
|
||||||
useSideSheetA11y(exportAllowed && exportJobsOpen, '.v2-history-export-sidesheet', 'v2-history-export-jobs', '历史数据导出任务', '关闭历史数据导出任务');
|
useSideSheetA11y(exportAllowed && exportJobsOpen, '.v2-history-export-sidesheet', 'v2-history-export-jobs', '历史数据导出任务', '关闭历史数据导出任务');
|
||||||
useSideSheetA11y(mobileFiltersOpen, '.v2-history-filter-sidesheet', 'v2-history-filter-sheet', '历史查询范围', '关闭历史查询范围');
|
useSideSheetA11y(mobileFiltersOpen, '.v2-history-filter-sidesheet', 'v2-history-filter-sheet', '历史查询范围', '关闭历史查询范围');
|
||||||
|
|
||||||
@@ -548,17 +548,20 @@ export default function HistoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
{!mobileLayout && selectedRow ? <aside className="v2-history-side"><EvidencePanel row={selectedRow} metrics={visibleMetrics} onClose={closeSelectedRow} /></aside> : null}
|
{!mobileLayout && selectedRow ? <aside className="v2-history-side"><EvidencePanel row={selectedRow} metrics={visibleMetrics} onClose={closeSelectedRow} /></aside> : null}
|
||||||
</div>
|
</div>
|
||||||
<SideSheet
|
<WorkspaceDetailSideSheet
|
||||||
className="v2-history-detail-sidesheet"
|
className="v2-history-detail-sidesheet"
|
||||||
visible={mobileLayout && Boolean(selectedRow)}
|
visible={mobileLayout && Boolean(selectedRow)}
|
||||||
aria-label="历史数据详情"
|
ariaLabel="历史数据详情"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
height="min(82dvh, 720px)"
|
height="min(82dvh, 720px)"
|
||||||
title={<div className="v2-history-mobile-sheet-title"><strong>数据详情</strong><span>{selectedRow ? `${selectedRow.plate || '未绑定车牌'} · ${selectedRow.protocol}` : '来源、质量与原始证据'}</span></div>}
|
title="数据详情"
|
||||||
|
description={selectedRow ? `${selectedRow.plate || '未绑定车牌'} · ${selectedRow.protocol}` : '来源、质量与原始证据'}
|
||||||
|
badge={selectedRow ? historyQualityLabel(selectedRow.quality) : undefined}
|
||||||
|
badgeColor={selectedRow && isHealthyHistoryQuality(selectedRow.quality) ? 'green' : selectedRow?.quality === 'error' || selectedRow?.quality === 'critical' ? 'red' : 'orange'}
|
||||||
onCancel={closeSelectedRow}
|
onCancel={closeSelectedRow}
|
||||||
>
|
>
|
||||||
{mobileLayout && selectedRow ? <EvidencePanel row={selectedRow} metrics={visibleMetrics} onClose={closeSelectedRow} showHeader={false} /> : null}
|
{mobileLayout && selectedRow ? <EvidencePanel row={selectedRow} metrics={visibleMetrics} onClose={closeSelectedRow} showHeader={false} /> : null}
|
||||||
</SideSheet>
|
</WorkspaceDetailSideSheet>
|
||||||
<SideSheet
|
<SideSheet
|
||||||
className="v2-history-export-sidesheet"
|
className="v2-history-export-sidesheet"
|
||||||
visible={exportAllowed && exportJobsOpen}
|
visible={exportAllowed && exportJobsOpen}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { usePlatformSession } from '../auth/AuthGate';
|
|||||||
import { canAdminister, hasMenu } from '../auth/session';
|
import { canAdminister, hasMenu } from '../auth/session';
|
||||||
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
|
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
|
||||||
import { formatTelemetryTime, formatTelemetryValue, telemetryQualityColor, telemetryQualityLabel, telemetryValueTypeLabel } from '../domain/telemetry';
|
import { formatTelemetryTime, formatTelemetryValue, telemetryQualityColor, telemetryQualityLabel, telemetryValueTypeLabel } from '../domain/telemetry';
|
||||||
import { formatZhNumber } from '../domain/formatters';
|
import { formatZhNumber, vehicleOperationStatusLabels } from '../domain/formatters';
|
||||||
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';
|
||||||
@@ -34,7 +34,6 @@ function availableMetric(value: number | undefined, available?: boolean) { retur
|
|||||||
function issueTone(issue: QualityIssueRow) { return issue.severity === 'error' ? 'error' : 'warning'; }
|
function issueTone(issue: QualityIssueRow) { return issue.severity === 'error' ? 'error' : 'warning'; }
|
||||||
function durationHours(seconds?: number | null) { return seconds == null ? '—' : `${formatZhNumber(seconds / 3600, 1)} 小时`; }
|
function durationHours(seconds?: number | null) { return seconds == null ? '—' : `${formatZhNumber(seconds / 3600, 1)} 小时`; }
|
||||||
function localDateTime(value?: string) { return value ? value.slice(0, 16) : ''; }
|
function localDateTime(value?: string) { return value ? value.slice(0, 16) : ''; }
|
||||||
const operationStatusLabels = { unknown: '待维护', active: '运营中', inactive: '停运', maintenance: '维保中', retired: '已退役' } as const;
|
|
||||||
const SINGLE_VEHICLE_REFRESH_MS = 10_000;
|
const SINGLE_VEHICLE_REFRESH_MS = 10_000;
|
||||||
const VehicleSearch = lazy(() => import('./VehicleSearchWorkspace'));
|
const VehicleSearch = lazy(() => import('./VehicleSearchWorkspace'));
|
||||||
const VehicleActions = lazy(() => import('./VehicleActions'));
|
const VehicleActions = lazy(() => import('./VehicleActions'));
|
||||||
@@ -101,7 +100,7 @@ function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; edita
|
|||||||
<label><span>车型</span><Input maxLength={128} value={draft.modelName} onChange={(value) => setDraft({ ...draft, modelName: value })} /></label>
|
<label><span>车型</span><Input maxLength={128} value={draft.modelName} onChange={(value) => setDraft({ ...draft, modelName: value })} /></label>
|
||||||
<label><span>车辆类型</span><Input maxLength={64} value={draft.vehicleType} onChange={(value) => setDraft({ ...draft, vehicleType: value })} /></label>
|
<label><span>车辆类型</span><Input maxLength={64} value={draft.vehicleType} onChange={(value) => setDraft({ ...draft, vehicleType: value })} /></label>
|
||||||
<label><span>所属企业</span><Input maxLength={128} value={draft.companyName} onChange={(value) => setDraft({ ...draft, companyName: value })} /></label>
|
<label><span>所属企业</span><Input maxLength={128} value={draft.companyName} onChange={(value) => setDraft({ ...draft, companyName: value })} /></label>
|
||||||
<label><span>运营状态</span><Select value={draft.operationStatus} onChange={(value) => setDraft({ ...draft, operationStatus: String(value) })} optionList={Object.entries(operationStatusLabels).map(([value, label]) => ({ value, label }))} /></label>
|
<label><span>运营状态</span><Select value={draft.operationStatus} onChange={(value) => setDraft({ ...draft, operationStatus: String(value) })} optionList={Object.entries(vehicleOperationStatusLabels).map(([value, label]) => ({ value, label }))} /></label>
|
||||||
<label><span>接入服务商</span><Input maxLength={128} value={draft.accessProvider} onChange={(value) => setDraft({ ...draft, accessProvider: value })} /></label>
|
<label><span>接入服务商</span><Input maxLength={128} value={draft.accessProvider} onChange={(value) => setDraft({ ...draft, accessProvider: value })} /></label>
|
||||||
<label><span>首次接入</span><Input aria-label="首次接入" type="datetime-local" value={draft.firstAccessAt} onChange={(value) => setDraft({ ...draft, firstAccessAt: value })} /></label>
|
<label><span>首次接入</span><Input aria-label="首次接入" type="datetime-local" value={draft.firstAccessAt} onChange={(value) => setDraft({ ...draft, firstAccessAt: value })} /></label>
|
||||||
<label><span>累计运行(小时)</span><Input type="number" min="0" step="0.1" value={draft.runtimeHours} onChange={(value) => setDraft({ ...draft, runtimeHours: value })} /></label>
|
<label><span>累计运行(小时)</span><Input type="number" min="0" step="0.1" value={draft.runtimeHours} onChange={(value) => setDraft({ ...draft, runtimeHours: value })} /></label>
|
||||||
@@ -110,7 +109,7 @@ function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; edita
|
|||||||
{ key: '车辆品牌', value: fmt(profile?.brandName) },
|
{ key: '车辆品牌', value: fmt(profile?.brandName) },
|
||||||
{ key: '车型 / 类型', value: [profile?.modelName, profile?.vehicleType].filter(Boolean).join(' / ') || '—' },
|
{ key: '车型 / 类型', value: [profile?.modelName, profile?.vehicleType].filter(Boolean).join(' / ') || '—' },
|
||||||
{ key: '所属企业', value: fmt(profile?.companyName) },
|
{ key: '所属企业', value: fmt(profile?.companyName) },
|
||||||
{ key: '运营状态', value: <Tag color={profile?.operationStatus === 'active' ? 'green' : profile?.operationStatus === 'maintenance' ? 'orange' : 'grey'} type="light" size="small">{operationStatusLabels[profile?.operationStatus ?? 'unknown']}</Tag> },
|
{ key: '运营状态', value: <Tag color={profile?.operationStatus === 'active' ? 'green' : profile?.operationStatus === 'maintenance' ? 'orange' : 'grey'} type="light" size="small">{vehicleOperationStatusLabels[profile?.operationStatus ?? 'unknown']}</Tag> },
|
||||||
{ key: '接入服务商', value: fmt(profile?.accessProvider) },
|
{ key: '接入服务商', value: fmt(profile?.accessProvider) },
|
||||||
{ key: '首次接入', value: fmt(profile?.firstAccessAt) },
|
{ key: '首次接入', value: fmt(profile?.firstAccessAt) },
|
||||||
{ key: '累计运行', value: durationHours(profile?.runtimeSeconds) }
|
{ key: '累计运行', value: durationHours(profile?.runtimeSeconds) }
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const passwordInputSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/P
|
|||||||
const workspaceFilterPanelSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceFilterPanel.tsx'), 'utf8');
|
const workspaceFilterPanelSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceFilterPanel.tsx'), 'utf8');
|
||||||
const workspaceMetricRailSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceMetricRail.tsx'), 'utf8');
|
const workspaceMetricRailSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceMetricRail.tsx'), 'utf8');
|
||||||
const workspaceSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceSideSheet.tsx'), 'utf8');
|
const workspaceSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceSideSheet.tsx'), 'utf8');
|
||||||
|
const workspaceDetailSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceDetailSideSheet.tsx'), 'utf8');
|
||||||
const segmentedTabsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/SegmentedTabs.tsx'), 'utf8');
|
const segmentedTabsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/SegmentedTabs.tsx'), 'utf8');
|
||||||
const metricActionSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/MetricActionButton.tsx'), 'utf8');
|
const metricActionSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/MetricActionButton.tsx'), 'utf8');
|
||||||
const tablePaginationSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/TablePagination.tsx'), 'utf8');
|
const tablePaginationSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/TablePagination.tsx'), 'utf8');
|
||||||
@@ -123,16 +124,21 @@ 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, SideSheet, Spin, Tag } from '@douyinfe/semi-ui'");
|
expect(sourceEvidenceSource).toContain("import { Button, Card, Empty, Input, 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('<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('<Spin size="small"');
|
||||||
expect(sourceEvidenceSource).toContain('<Empty className="v2-source-evidence-empty"');
|
expect(sourceEvidenceSource).toContain('<Empty 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');
|
||||||
|
expect(workspaceDetailSideSheetSource).toContain('variant="detail"');
|
||||||
|
for (const name of ['HistoryPage', 'AlertsPage', 'AccessPage']) {
|
||||||
|
expect(corePageSources[name]).toContain("from '../shared/WorkspaceDetailSideSheet'");
|
||||||
|
expect(corePageSources[name]).toContain('<WorkspaceDetailSideSheet');
|
||||||
|
}
|
||||||
expect(corePageSources.HistoryPage).toContain('<Input');
|
expect(corePageSources.HistoryPage).toContain('<Input');
|
||||||
expect(corePageSources.HistoryPage).toContain('<Select');
|
expect(corePageSources.HistoryPage).toContain('<Select');
|
||||||
expect(corePageSources.HistoryPage).toContain('<Checkbox');
|
expect(corePageSources.HistoryPage).toContain('<Checkbox');
|
||||||
@@ -178,7 +184,7 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.AlertsPage).toContain('<WorkspaceCommandBar');
|
expect(corePageSources.AlertsPage).toContain('<WorkspaceCommandBar');
|
||||||
expect(corePageSources.AlertsPage).toContain('className="v2-alert-navigation v2-alert-command-bar"');
|
expect(corePageSources.AlertsPage).toContain('className="v2-alert-navigation v2-alert-command-bar"');
|
||||||
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-filter-sidesheet"');
|
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-filter-sidesheet"');
|
||||||
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-detail-sidesheet"');
|
expect(corePageSources.AlertsPage).toContain('<WorkspaceDetailSideSheet\n className="v2-alert-detail-sidesheet"');
|
||||||
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-rule-editor-sidesheet"');
|
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-rule-editor-sidesheet"');
|
||||||
expect(corePageSources.AlertsPage).toContain('className={`v2-alert-event-table');
|
expect(corePageSources.AlertsPage).toContain('className={`v2-alert-event-table');
|
||||||
expect(corePageSources.AlertsPage).toContain('detailTriggerRow({');
|
expect(corePageSources.AlertsPage).toContain('detailTriggerRow({');
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ test('opens source evidence in a Semi bottom SideSheet on mobile without expandi
|
|||||||
expect(view.container.querySelector('.v2-source-evidence.is-mobile-sheet')).toHaveClass('is-open');
|
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(view.container.querySelector('.v2-source-evidence > .semi-card-body > .v2-source-evidence-body')).not.toBeInTheDocument();
|
||||||
expect(await screen.findByText('北斗平台')).toBeInTheDocument();
|
expect(await screen.findByText('北斗平台')).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-source-evidence-sidesheet')).toBeInTheDocument();
|
expect(document.querySelector('.v2-source-evidence-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet');
|
||||||
|
expect(document.querySelector('.v2-source-evidence-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('3 个来源');
|
||||||
expect(document.querySelectorAll('.v2-source-evidence-sidesheet .v2-source-evidence-card')).toHaveLength(3);
|
expect(document.querySelectorAll('.v2-source-evidence-sidesheet .v2-source-evidence-card')).toHaveLength(3);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '关闭来源证据' }));
|
fireEvent.click(screen.getByRole('button', { name: '关闭来源证据' }));
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button, Card, Empty, Input, SideSheet, Spin, Tag } from '@douyinfe/semi-ui';
|
import { Button, Card, Empty, Input, 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 { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
|
||||||
import { QUERY_MEMORY } from '../queryPolicy';
|
import { QUERY_MEMORY } from '../queryPolicy';
|
||||||
|
import { WorkspaceDetailSideSheet } from './WorkspaceDetailSideSheet';
|
||||||
|
|
||||||
function today() {
|
function today() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -110,8 +110,6 @@ 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', '位置与里程来源证据', '关闭来源证据');
|
|
||||||
|
|
||||||
const evidenceBody = <div className="v2-source-evidence-body">
|
const evidenceBody = <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>
|
||||||
@@ -140,17 +138,20 @@ export function VehicleSourceEvidencePanel({
|
|||||||
</header>
|
</header>
|
||||||
{expanded && !mobileLayout ? evidenceBody : null}
|
{expanded && !mobileLayout ? evidenceBody : null}
|
||||||
</Card>
|
</Card>
|
||||||
{mobileLayout ? <SideSheet
|
{mobileLayout ? <WorkspaceDetailSideSheet
|
||||||
className="v2-source-evidence-sidesheet"
|
className="v2-source-evidence-sidesheet"
|
||||||
visible={expanded}
|
visible={expanded}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
height="min(88dvh, 780px)"
|
height="min(88dvh, 780px)"
|
||||||
aria-label="位置与里程来源证据"
|
ariaLabel="位置与里程来源证据"
|
||||||
title={<div className="v2-source-evidence-sheet-title"><strong>位置与里程来源</strong><span>{description}</span></div>}
|
closeLabel="关闭来源证据"
|
||||||
footer={null}
|
title="位置与里程来源"
|
||||||
|
description={description}
|
||||||
|
badge={sourceCount ? `${sourceCount} 个来源` : '按需读取'}
|
||||||
|
badgeColor={sourceCount ? 'blue' : 'grey'}
|
||||||
onCancel={() => setExpanded(false)}
|
onCancel={() => setExpanded(false)}
|
||||||
>
|
>
|
||||||
<div className="v2-source-evidence-sheet-body">{evidenceBody}</div>
|
<div className="v2-source-evidence-sheet-body">{evidenceBody}</div>
|
||||||
</SideSheet> : null}
|
</WorkspaceDetailSideSheet> : null}
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
|
import { WorkspaceDetailSideSheet } from './WorkspaceDetailSideSheet';
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
test('renders one accessible Semi detail sheet with shared identity and status', () => {
|
||||||
|
const onCancel = vi.fn();
|
||||||
|
render(<WorkspaceDetailSideSheet
|
||||||
|
className="test-detail-sheet"
|
||||||
|
visible
|
||||||
|
ariaLabel="车辆证据详情"
|
||||||
|
closeLabel="关闭证据详情"
|
||||||
|
title="车辆详情"
|
||||||
|
description="粤A00001 · VIN-001"
|
||||||
|
badge="状态正常"
|
||||||
|
badgeColor="green"
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
<p>详情内容</p>
|
||||||
|
</WorkspaceDetailSideSheet>);
|
||||||
|
|
||||||
|
const dialog = screen.getByRole('dialog', { name: '车辆证据详情' });
|
||||||
|
expect(dialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'test-detail-sheet');
|
||||||
|
expect(screen.getByText('粤A00001 · VIN-001')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('状态正常').closest('.semi-tag')).toHaveClass('semi-tag-green-light');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '关闭证据详情' }));
|
||||||
|
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
WorkspaceSideSheet,
|
||||||
|
type WorkspaceSideSheetBadgeColor
|
||||||
|
} from './WorkspaceSideSheet';
|
||||||
|
|
||||||
|
type WorkspaceDetailSideSheetProps = {
|
||||||
|
className?: string;
|
||||||
|
visible: boolean;
|
||||||
|
ariaLabel: string;
|
||||||
|
closeLabel?: string;
|
||||||
|
title: ReactNode;
|
||||||
|
description: ReactNode;
|
||||||
|
badge?: ReactNode;
|
||||||
|
badgeColor?: WorkspaceSideSheetBadgeColor;
|
||||||
|
placement?: 'left' | 'right' | 'top' | 'bottom';
|
||||||
|
width?: string | number;
|
||||||
|
height?: string | number;
|
||||||
|
onCancel: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WorkspaceDetailSideSheet({
|
||||||
|
className,
|
||||||
|
visible,
|
||||||
|
ariaLabel,
|
||||||
|
closeLabel,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
badge,
|
||||||
|
badgeColor,
|
||||||
|
placement,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onCancel,
|
||||||
|
children
|
||||||
|
}: WorkspaceDetailSideSheetProps) {
|
||||||
|
return <WorkspaceSideSheet
|
||||||
|
className={className}
|
||||||
|
variant="detail"
|
||||||
|
visible={visible}
|
||||||
|
ariaLabel={ariaLabel}
|
||||||
|
closeLabel={closeLabel}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
badge={badge}
|
||||||
|
badgeColor={badgeColor}
|
||||||
|
placement={placement}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</WorkspaceSideSheet>;
|
||||||
|
}
|
||||||
@@ -17,15 +17,18 @@ export type WorkspaceSideSheetSummaryItem = {
|
|||||||
tone?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger';
|
tone?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger';
|
||||||
};
|
};
|
||||||
|
|
||||||
type WorkspaceSideSheetProps = {
|
export type WorkspaceSideSheetBadgeColor = 'blue' | 'green' | 'orange' | 'red' | 'grey';
|
||||||
|
|
||||||
|
export type WorkspaceSideSheetProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: 'config' | 'detail';
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
closeLabel?: string;
|
closeLabel?: string;
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
description: ReactNode;
|
description: ReactNode;
|
||||||
badge?: ReactNode;
|
badge?: ReactNode;
|
||||||
badgeColor?: 'blue' | 'green' | 'orange' | 'red' | 'grey';
|
badgeColor?: WorkspaceSideSheetBadgeColor;
|
||||||
placement?: 'left' | 'right' | 'top' | 'bottom';
|
placement?: 'left' | 'right' | 'top' | 'bottom';
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
@@ -39,6 +42,7 @@ type WorkspaceSideSheetProps = {
|
|||||||
|
|
||||||
export function WorkspaceSideSheet({
|
export function WorkspaceSideSheet({
|
||||||
className = '',
|
className = '',
|
||||||
|
variant = 'config',
|
||||||
visible,
|
visible,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
closeLabel = `关闭${ariaLabel}`,
|
closeLabel = `关闭${ariaLabel}`,
|
||||||
@@ -57,7 +61,7 @@ export function WorkspaceSideSheet({
|
|||||||
children
|
children
|
||||||
}: WorkspaceSideSheetProps) {
|
}: WorkspaceSideSheetProps) {
|
||||||
const sheetId = useId();
|
const sheetId = useId();
|
||||||
const sheetClassName = ['v2-workspace-config-sidesheet', className].filter(Boolean).join(' ');
|
const sheetClassName = [`v2-workspace-${variant}-sidesheet`, className].filter(Boolean).join(' ');
|
||||||
const hasFooter = Boolean(footerNote || secondaryActions.length || primaryAction);
|
const hasFooter = Boolean(footerNote || secondaryActions.length || primaryAction);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|||||||
@@ -20806,6 +20806,72 @@
|
|||||||
box-shadow: 0 7px 18px rgba(18, 104, 243, .18);
|
box-shadow: 0 7px 18px rgba(18, 104, 243, .18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shared Semi UI detail SideSheet: preserve each page's evidence content while
|
||||||
|
* keeping object identity, status, surface depth and scrolling consistent.
|
||||||
|
*/
|
||||||
|
.v2-workspace-detail-sidesheet .semi-sidesheet-inner {
|
||||||
|
overflow: hidden;
|
||||||
|
border-left: 1px solid #d9e3ef;
|
||||||
|
background: #f4f7fb;
|
||||||
|
box-shadow: -22px 0 64px rgba(24, 43, 68, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .semi-sidesheet-header {
|
||||||
|
min-height: 78px;
|
||||||
|
border-bottom: 1px solid #dfe7f0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 0%, rgba(18, 104, 243, .075), transparent 36%),
|
||||||
|
linear-gradient(180deg, #fff 0%, #fbfcfe 100%);
|
||||||
|
padding: 13px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .semi-sidesheet-title {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-title strong {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .semi-sidesheet-close {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .semi-sidesheet-body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f4f7fb;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 9px;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-content > .semi-card {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
border-radius: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-alert-detail-sidesheet .v2-workspace-config-content,
|
||||||
|
.v2-source-evidence-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-detail-sidesheet .v2-workspace-config-content,
|
||||||
|
.v2-access-detail-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-source-evidence-sidesheet .v2-source-evidence-sheet-body > .v2-source-evidence-body {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-history-column-sidesheet .v2-workspace-config-content {
|
.v2-history-column-sidesheet .v2-workspace-config-content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -20872,6 +20938,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (max-width: 680px) {
|
||||||
|
.v2-workspace-detail-sidesheet.semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid #d8e2ed;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
box-shadow: 0 -18px 48px rgba(23, 43, 68, .17);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet.semi-sidesheet-bottom .semi-sidesheet-header {
|
||||||
|
min-height: 70px;
|
||||||
|
padding: 10px 12px 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-title {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-title strong {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-title small {
|
||||||
|
max-width: 250px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-title > .semi-tag {
|
||||||
|
min-height: 23px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-detail-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-alert-detail-sidesheet .v2-workspace-config-content,
|
||||||
|
.v2-source-evidence-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-workspace-config-sidesheet.semi-sidesheet-bottom .semi-sidesheet-inner {
|
.v2-workspace-config-sidesheet.semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||||
border: 1px solid #d8e2ed;
|
border: 1px solid #d8e2ed;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user