feat: unify detail side sheets

This commit is contained in:
lingniu
2026-07-19 05:38:17 +08:00
parent 7eca5ae352
commit 63b50732cb
15 changed files with 273 additions and 42 deletions

View File

@@ -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) {

View File

@@ -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');

View File

@@ -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}

View File

@@ -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');

View File

@@ -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>
</>; </>;
} }

View File

@@ -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');

View File

@@ -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}

View File

@@ -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) }

View File

@@ -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({');

View File

@@ -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: '关闭来源证据' }));

View File

@@ -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}
</>; </>;
} }

View File

@@ -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);
});

View File

@@ -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>;
}

View File

@@ -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(() => {

View File

@@ -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;