30 changed files with 1649 additions and 169 deletions
@@ -7,6 +7,10 @@ import './v2/styles/workspace.css';
import './v2/styles/foundation.css';
import './v2/styles/experience.css';
import './v2/styles/event-center.css';
import './v2/styles/apple-workspace.css';
import './v2/styles/apple-data-pages.css';
import './v2/styles/apple-governance.css';
import './v2/styles/apple-map-pages.css';
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual';
@@ -115,10 +115,8 @@ test('opens contextual help for the active module and closes it without navigati
expect(screen.getByRole('dialog', { name: '全局监控' })).toHaveTextContent('筛选会自动生效');
expect(document.querySelector('.v2-help-sheet')).toHaveClass('v2-workspace-help-sidesheet');
const helpSummary = screen.getByRole('list', { name: '全局监控摘要' });
expect(helpSummary).toHaveTextContent('页面目标快速完成');
expect(helpSummary).toHaveTextContent('建议步骤3 项');
expect(helpSummary).toHaveTextContent('快捷关闭Esc');
expect(screen.getByRole('dialog', { name: '全局监控' })).toHaveTextContent('查看全车队实时位置、状态和最新上报。');
expect(document.querySelectorAll('.v2-help-panel ol > li')).toHaveLength(3);
expect(document.querySelector('.v2-help-overview.semi-card')).toBeInTheDocument();
expect(screen.getByText('建议操作')).toBeInTheDocument();
expect(help).toHaveAttribute('aria-expanded', 'true');
@@ -86,18 +86,12 @@ function ContextHelp({ section, visible, mobile, onClose }: { section: string; v
title={title}
description="当前页面帮助与推荐操作"
badge={`${content.tips.length} 项建议`}
summaryItems={[
{ label: '页面目标', value: '快速完成', detail: '保留当前任务状态', tone: 'primary' },
{ label: '建议步骤', value: `${content.tips.length}`, detail: '按顺序执行' },
{ label: '快捷关闭', value: 'Esc', detail: '随时返回页面' }
]}
footerNote="关闭帮助不会改变当前筛选、地图或任务状态"
primaryAction={{ label: '返回当前页面', onClick: onClose }}
onCancel={onClose}
>
<div className="v2-help-panel">
<Card className="v2-help-overview" bodyStyle={{ padding: 0 }}>
<Tag color="blue"></Tag>
<Text type="secondary">{content.summary}</Text>
</Card>
<div className="v2-help-section-label"><Text strong></Text><Text type="tertiary" size="small">{content.tips.length} </Text></div>
@@ -127,7 +127,6 @@ test('removes old access rows immediately when the vehicle filter scope changes'
expect(await screen.findByText('旧接入车牌')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '接入健康工作台', level: 5 })).toBeInTheDocument();
expect(screen.getByLabelText('接入管理操作')).toHaveClass('v2-workspace-command-bar', 'v2-access-command-bar', 'has-identity');
expect(screen.getByLabelText('接入管理操作')).toHaveTextContent('来源治理');
expect(screen.getByLabelText('接入管理操作').querySelector('.v2-workspace-command-icon.semi-avatar')).toBeInTheDocument();
expect(view.container.querySelector('.v2-access-discovery-shell')).toBeInTheDocument();
for (const className of ['v2-access-filter-card-v3', 'v2-access-table-v3']) {
@@ -601,7 +601,6 @@ export default function AccessPage() {
<WorkspaceCommandBar
className="v2-access-command-bar"
ariaLabel="接入管理操作"
eyebrow="来源治理"
icon={<IconConnectionPoint1 />}
title="接入健康工作台"
description={mobileLayout
@@ -679,7 +678,6 @@ export default function AccessPage() {
<div className="v2-access-workspace-v3">
<Card className="v2-access-table-v3" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
eyebrow="接入差异"
tone="primary"
title="接入差异清单"
description={`当前范围:${scopeLabels[criteria.connectionState] || '全部主车辆'} · 缺席协议仅表示当前未发现`}
@@ -1395,6 +1395,7 @@ test.each([false, true])('displays native GB32960 alarms without automation (mob
expect(screen.getByLabelText('具体告警信息')).toHaveTextContent('绝缘报警');
expect(screen.getByLabelText('具体告警信息')).toHaveTextContent('驱动电机温度报警');
expect(screen.getByLabelText('具体告警信息')).toHaveTextContent('bit20、bit21');
if (!mobile) expect(screen.getByText('车辆原生告警 · GB/T 32960')).toBeInTheDocument();
expect(screen.getByLabelText('事件执行轨迹')).toHaveTextContent('车辆原生告警入库');
expect(screen.getByLabelText('事件执行轨迹')).not.toHaveTextContent('匹配自动化');
});
@@ -342,7 +342,7 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac
{ title: '协议', dataIndex: 'protocol', className: 'is-protocol', width: 98, render: (value: string) => <ProtocolTag className="v2-alert-protocol-tag" protocol={value} compact /> },
{ title: '发生时间', dataIndex: 'triggeredAt', className: 'is-triggered-at', width: 124, render: (value: string) => <AlertTime value={value} /> },
{ title: '事件来源 / 自动化', dataIndex: 'ruleName', className: 'is-rule', width: 154, render: (value: string, event: AlertEvent) => <span className="v2-alert-event-rule" title={value}>{isNativeAlarm(event) ? '车辆原生告警' : value}</span> },
{ title: '执行状态', dataIndex: 'status', className: 'is-status', width: 92, render: (_: AlertEvent['status'], event: AlertEvent) => <EventExecutionTag event={event} /> },
{ title: '执行状态', dataIndex: 'status', className: 'is-status', width: 92, fixed: 'right' as const, render: (_: AlertEvent['status'], event: AlertEvent) => <EventExecutionTag event={event} /> },
];
return compact
? allColumns.map((column) => ({ ...column, width: COMPACT_ALERT_COLUMN_WIDTHS[column.dataIndex] ?? column.width }))
@@ -352,6 +352,7 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac
return <Table
className={`v2-alert-event-table${compact ? ' is-compact' : ''}`}
columns={columns}
scroll={{ x: columns.reduce((width, column) => width + column.width, 0) }}
dataSource={rows}
rowKey="id"
pagination={false}
@@ -378,7 +379,7 @@ function EventInspector({ event, loading = false, detailError, note, acting, act
</Card>;
const normalized = toVehicleEvent(event);
return <Card className={`v2-alert-inspector${sheet ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }}>
{!sheet ? <WorkspacePanelHeader className="v2-alert-inspector-heading" title="事件详情" description={normalized.type} actions={<><span className="v2-alert-inspector-status"><EventExecutionTag event={event} /></span><Button className="v2-alert-inspector-close" theme="borderless" icon={<IconClose />} aria-label="关闭事件详情" onClick={onClose} /></>} /> : null}
{!sheet ? <WorkspacePanelHeader className="v2-alert-inspector-heading" title="事件详情" description={isNativeAlarm(event) ? "车辆原生告警 · GB/T 32960" : `${normalized.categoryLabel} · 自动化事件`} actions={<><span className="v2-alert-inspector-status"><EventExecutionTag event={event} /></span><Button className="v2-alert-inspector-close" theme="borderless" icon={<IconClose />} aria-label="关闭事件详情" onClick={onClose} /></>} /> : null}
<div className="v2-alert-inspector-content">
{detailError ? <InlineError message={`最新事件状态同步失败:${detailError}`} onRetry={onRetry} /> : null}
<Card className="v2-alert-detail-card v2-alert-focus-card" bodyStyle={{ padding: 0 }} aria-label="事件摘要">
@@ -392,7 +393,7 @@ function EventInspector({ event, loading = false, detailError, note, acting, act
<span><small></small><strong><AlertTime value={normalized.occurredAt} /></strong></span>
<span><small>{isNativeAlarm(event) ? '事件来源' : '匹配自动化'}</small><strong>{isNativeAlarm(event) ? '车辆原生告警' : `v${event.ruleVersion}`}</strong></span>
</div>
{isNativeAlarm(event) ? <section aria-label="32960 原生告警字段">
{isNativeAlarm(event) ? <section className="v2-native-alarm-fields" aria-label="32960 原生告警字段">
<div className="v2-native-alarm-meaning" aria-label="具体告警信息">
<strong></strong>
{event.nativeAlarmNames?.length ? <ul>{event.nativeAlarmNames.map((name) => <li key={name}>{name}</li>)}</ul> : <p>{event.ruleName}</p>}
@@ -1245,7 +1246,7 @@ function RulesWorkspace({ page, rulesReady, rulesLoading, rulesError, onRetryRul
<Button className={`v2-alert-rule-item${selectedRule?.id === rule.id ? ' is-selected' : ''}`} theme="borderless" type="tertiary" aria-pressed={selectedRule?.id === rule.id} aria-haspopup={mobileLayout ? 'dialog' : undefined} aria-expanded={mobileLayout ? (mobileDetailOpen || editorOpen) && selectedID === rule.id : selectedRule?.id === rule.id} onClick={() => mobileLayout ? openMobileRuleDetail(rule) : selectRule(rule)}>
<span className="v2-alert-rule-copy"><span><strong>{rule.name}</strong><Tag color={rule.archivedAt ? 'amber' : rule.enabled ? 'green' : 'grey'} type="light" size="small">{rule.archivedAt ? '已归档' : rule.enabled ? '运行中' : '已停用'}</Tag></span><small>{automationSource(rule)} · {ruleCondition(rule, catalogLabels, catalogUnits)}</small></span>
</Button>
{rule.archivedAt ? <Tag className="v2-alert-automation-readonly" color="grey" type="light" size="small"></Tag> : <Switch className="v2-alert-automation-switch" checked={rule.enabled} loading={toggle.isPending} aria-label={`${rule.enabled ? '停用' : '启用'}自动化:${rule.name}`} onChange={() => toggle.mutate(rule)} />}
{rule.archivedAt ? <Tag className="v2-alert-automation-readonly" color="grey" type="light" size="small"></Tag> : <label className="v2-alert-automation-switch-target"><Switch className="v2-alert-automation-switch" checked={rule.enabled} loading={toggle.isPending} aria-label={`${rule.enabled ? '停用' : '启用'}自动化:${rule.name}`} onChange={() => toggle.mutate(rule)} /></label>}
<small className="v2-alert-automation-version">v{rule.version}</small>
</article>)}
{rulesLoading ? <PanelLoading compact title="正在检索自动化" description="从完整规则库读取当前页。" /> : rulesError ? <PanelError compact title="自动化检索失败" description={rulesError} action={<Button theme="solid" type="primary" onClick={onRetryRules}></Button>} /> : !filteredRules.length ? <PanelEmpty className="v2-alert-rule-empty" tone="primary" icon={lifecycle === 'archived' ? <IconBox /> : <IconSearch />} title={search || protocolFilter || statusFilter !== 'all' ? '没有匹配的自动化' : lifecycle === 'archived' ? '暂无审计归档' : '暂无自动化'} description={search || protocolFilter || statusFilter !== 'all' ? '换一个名称、协议或状态筛选试试。' : lifecycle === 'archived' ? '停用规则可在详情中带原因归档,并在这里恢复。' : '创建第一条事件自动化。'} /> : null}
@@ -577,10 +577,7 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
}
expect(view.container.querySelector('.v2-history-discovery-shell')).toContainElement(screen.getByLabelText('历史数据查询操作'));
expect(screen.getByLabelText('历史数据查询操作')).toHaveClass('has-identity');
expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('历史证据');
expect(screen.getByLabelText('历史数据查询操作').querySelector('.v2-workspace-command-icon.semi-avatar')).toBeInTheDocument();
expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('遥测证据检索');
expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('查询条件');
expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('07-16 00:00 07-16 05:00');
expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('位置数据');
expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('全部来源');
@@ -1894,10 +1894,8 @@ export default function HistoryPage() {
<WorkspaceCommandBar
className="v2-history-command-bar"
ariaLabel="历史数据查询操作"
eyebrow="历史证据"
icon={<IconHistory />}
title="遥测证据检索"
description="按车辆、时间和协议核验可追溯的历史数据"
title="查询条件"
description="选择车辆和时间,查询历史上报"
status={keywords.length ? `${keywords.length} 辆 · ${(result?.total ?? 0).toLocaleString('zh-CN')}` : '等待查询'}
statusColor={keywords.length ? 'blue' : 'grey'}
meta={keywords.length ? <span className="v2-history-applied-scope" aria-label="已应用查询范围">
@@ -2029,8 +2027,6 @@ export default function HistoryPage() {
<HistoryTrend response={seriesQuery.data} category={criteria.category} loading={seriesQuery.isFetching} error={seriesQuery.isError ? (seriesQuery.error instanceof Error ? seriesQuery.error.message : '未知错误') : undefined} hasMetrics={Boolean(seriesMetricKey)} expanded={trendExpanded} onToggle={() => setTrendExpanded((value) => !value)} onRetry={() => { void seriesQuery.refetch(); }} />
<Card className={`v2-history-table-card is-${density}`} bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
eyebrow="证据结果"
tone="primary"
title="历史明细"
description={keywords.length ? `${(result?.total ?? 0).toLocaleString('zh-CN')} 条记录 · ${resultSummary?.vehicleCount ?? 0} 辆车辆 · ${summarySources}` : '选择车辆和时间范围后查询'}
meta={<span className="v2-history-result-meta"><Tag color={activeRawSegment ? 'cyan' : activeFieldViewName.includes('未保存') ? 'orange' : 'blue'} type="light" size="small">{activeRawSegment ? `${rawSegments.find((segment) => segment.key === activeRawSegment)?.label} · ${displayMetrics.length - 2} 字段` : `${activeFieldViewName} · ${visibleMetrics.length} / ${allMetrics.length} 字段`}</Tag>{resultSummary ? <span className="v2-history-query-duration"> {resultSummary.queryDurationMs} ms</span> : null}</span>}
@@ -2096,15 +2092,9 @@ export default function HistoryPage() {
/>
: <WorkspaceEmptyGuide
className="v2-history-empty"
eyebrow={mobileLayout ? undefined : '从一个明确的车辆范围开始'}
icon={<IconSearch />}
title="选择车辆,开始核验历史证据"
description="一次最多查询 5 台车辆,结果会保留协议来源、质量说明与 RAW 证据。"
steps={mobileLayout ? undefined : [
{ icon: <IconSearch />, title: '选择车辆', description: '支持车牌、VIN 与批量粘贴' },
{ icon: <IconCalendar />, title: '确认时间', description: '按上海时区精确到分钟' },
{ icon: <IconMapPin />, title: '核验数据', description: '位置、RAW 与日里程统一检索' }
]}
action={<Button className="v2-history-empty-action" theme="solid" icon={<IconSearch />} aria-label="选择车辆并查询" onClick={() => setFiltersCollapsed(false)}></Button>}
/>
: null}
@@ -620,7 +620,7 @@ export default function MonitorPage() {
{mode === 'map' ? <section className={`v2-monitor-workspace${selected && detailOpen ? ' is-detail-open' : ''}${selected && !detailOpen ? ' is-detail-collapsed' : ''}`}>
<div className="v2-vehicle-rail">
<header><strong></strong><span>{batchSearchPending ? '查询中' : `${formatNumber(vehicles.data?.total ?? visibleRows.length)}`}</span></header>
<div className={`v2-rail-search${batchMatch.missing.length && !batchSearchPending ? ' has-missing' : ''}`} title={searchTerms.length > 1 ? batchStatusTitle : undefined}><IconSearch /><span>{searchTerms.length > 1 ? batchStatus : deferredKeyword ? `正在筛选“${deferredKeyword}` : '最新上报排序'}</span></div>
<div role="status" className={`v2-rail-search${batchMatch.missing.length && !batchSearchPending ? ' has-missing' : ''}`} title={searchTerms.length > 1 ? batchStatusTitle : undefined}><span>{searchTerms.length > 1 ? batchStatus : deferredKeyword ? `正在筛选“${deferredKeyword}` : '最新上报优先'}</span></div>
<div className="v2-vehicle-scroll">
{vehicles.isLoading || batchSearchPending ? <div className="v2-list-loading"><span className="v2-spinner" />{batchSearchPending ? `正在查找 ${searchTerms.length} 辆车` : '加载车辆'}</div> : null}
{!vehicles.isLoading && !vehicles.isError && !batchSearchPending && visibleRows.length === 0 ? <EmptyState /> : null}
@@ -79,7 +79,6 @@ test('renders reconciliation queue, loads evidence on demand and records review
expect(await screen.findByText('质量问题队列')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '运行与数据质量', level: 5 })).toBeInTheDocument();
expect(screen.getByLabelText('运维质量操作')).toHaveClass('v2-workspace-command-bar', 'v2-ops-command-bar', 'has-identity');
expect(screen.getByLabelText('运维质量操作')).toHaveTextContent('运行治理');
expect(screen.getByLabelText('运维质量操作').querySelector('.v2-workspace-command-icon.semi-avatar')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '刷新全局证据' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted');
const navigation = screen.getByRole('navigation', { name: '运维质量视图' });
@@ -294,7 +293,11 @@ test('renders only the compact mobile reconciliation surface and defers secondar
expect(toolbar).not.toHaveClass('is-mobile-collapsed');
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '30 天趋势' }));
fireEvent.click(screen.getByRole('button', { name: '质量问题更多操作' }));
expect(await screen.findByRole('menuitem', { name: '批量处置' })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /导出当前筛选/ })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /刷新结果/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole('menuitem', { name: /30 天趋势/ }));
expect(await screen.findByRole('dialog', { name: '30 天差异趋势' })).toBeInTheDocument();
expect(document.querySelector('.v2-reconcile-trend-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet');
expect(document.querySelector('.v2-reconcile-trend-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(72dvh, 620px)' });
@@ -598,7 +598,6 @@ export default function OperationsPage() {
<WorkspaceCommandBar
className="v2-ops-command-bar"
ariaLabel="运维质量操作"
eyebrow="运行治理"
icon={<IconPulse />}
tone="health"
title="运行与数据质量"
@@ -634,7 +633,6 @@ export default function OperationsPage() {
{readiness.isError ? <InlineError message={readiness.error instanceof Error ? readiness.error.message : '协议来源就绪度读取失败'} onRetry={() => readiness.refetch()} /> : null}
<Card className="v2-ops-overview" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
eyebrow="运行快照"
tone="health"
title="运行概览"
description="关键链路、基础设施与车辆覆盖的同一健康快照"
@@ -1,5 +1,5 @@
import { IconAlertTriangle, IconBox, IconChevronDown, IconChevronRight, IconChevronUp, IconClose, IconDownload, IconHistory, IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons';
import { Button, Card, Checkbox, Input, RadioGroup, Select, Table, Tag, TextArea, Toast } from '@douyinfe/semi-ui';
import { IconAlertTriangle, IconBox, IconChevronDown, IconChevronRight, IconChevronUp, IconClose, IconDownload, IconHistory, IconMore, IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons';
import { Button, Card, Checkbox, Dropdown, Input, RadioGroup, Select, Table, Tag, TextArea, Toast } from '@douyinfe/semi-ui';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useDeferredValue, useEffect, useId, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
@@ -615,6 +615,7 @@ function ReconciliationBatchReview({ issues, visible, mobileLayout, onCancel, on
export default function ReconciliationCenter() {
const mobileLayout = useMobileLayout();
const [mobileActionsOpen, setMobileActionsOpen] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const initialScope = searchParams.get('reconcileScope') === 'archived' ? 'archived' : 'current';
const defaultPageSize = mobileLayout ? MOBILE_PAGE_SIZE : DESKTOP_PAGE_SIZE;
@@ -843,12 +844,23 @@ export default function ReconciliationCenter() {
return <><Card className="v2-reconcile-center" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
className="v2-reconcile-heading"
eyebrow="自动对账"
tone="health"
title="质量问题队列"
description="自动发现并合并重复问题;复核人员基于原始证据形成结论,不直接改写数据。"
meta="每日自动对账"
actions={<>
actions={mobileLayout ? <Dropdown
trigger="click"
position="bottomRight"
visible={mobileActionsOpen}
onVisibleChange={setMobileActionsOpen}
render={<Dropdown.Menu>
{!batchMode && scope === 'current' ? <Dropdown.Item onClick={() => { setMobileActionsOpen(false); enterBatchMode('operate'); }}></Dropdown.Item> : null}
{!batchMode && administrator ? <Dropdown.Item icon={scope === 'archived' ? <IconRefresh /> : <IconBox />} onClick={() => { setMobileActionsOpen(false); enterBatchMode('lifecycle'); }}>{scope === 'archived' ? '批量恢复' : '批量归档'}</Dropdown.Item> : null}
{!batchMode ? <Dropdown.Item icon={<IconDownload />} disabled={!page?.total || exportIssues.isPending} onClick={() => { setMobileActionsOpen(false); exportIssues.mutate(); }}>{exportIssues.isPending ? '正在导出…' : '导出当前筛选'}</Dropdown.Item> : null}
{scope === 'current' ? <Dropdown.Item icon={<IconHistory />} onClick={() => { setMobileActionsOpen(false); toggleTrend(); }}>30 </Dropdown.Item> : null}
<Dropdown.Item icon={<IconRefresh />} disabled={summary.isFetching || issues.isFetching} onClick={() => { setMobileActionsOpen(false); refresh(); }}></Dropdown.Item>
</Dropdown.Menu>}
><Button className="v2-reconcile-mobile-more" theme="borderless" icon={<IconMore />} aria-haspopup="menu" aria-expanded={mobileActionsOpen} aria-label="质量问题更多操作"></Button></Dropdown> : <>
{!batchMode && scope === 'current' ? <Button className="v2-reconcile-batch-entry v2-workspace-mobile-tool-button is-muted" theme="light" aria-label="进入批量处置" onClick={() => enterBatchMode('operate')}>{mobileLayout ? '批量' : '批量处置'}</Button> : null}
{!batchMode && administrator ? <Button className="v2-workspace-mobile-tool-button is-muted" theme="light" type="tertiary" icon={scope === 'archived' ? <IconRefresh /> : <IconBox />} aria-label={scope === 'archived' ? '进入批量恢复' : '进入批量归档'} onClick={() => enterBatchMode('lifecycle')}>{mobileLayout ? scope === 'archived' ? '恢复' : '归档' : scope === 'archived' ? '批量恢复' : '批量归档'}</Button> : null}
{!batchMode ? <Button className="v2-reconcile-export v2-workspace-mobile-tool-button is-muted" theme="light" icon={<IconDownload />} aria-label="导出当前筛选" loading={exportIssues.isPending} disabled={!page?.total} onClick={() => exportIssues.mutate()}>{mobileLayout ? '导出' : '导出当前筛选'}</Button> : null}
@@ -1349,8 +1349,6 @@ export default function StatisticsPage() {
</div>
<Card className="v2-mileage-results" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
eyebrow={metricView === 'mileage' ? '每日里程' : '每日氢耗'}
tone={metricView === 'mileage' ? 'primary' : 'health'}
title={metricView === 'mileage' ? '车辆每日里程' : '车辆每日氢耗'}
description={`${criteria.dateFrom}${criteria.dateTo}`}
meta={<span className="v2-workspace-result-meta"><Tag color="blue" type="light" size="small">{hasVehicles ? `${totalVehicles}` : `${displayVehicles.length} / ${totalVehicles}`}</Tag><span>{dates.length} </span></span>}
@@ -1370,6 +1368,6 @@ export default function StatisticsPage() {
<footer>{totalVehicles > PAGE_SIZE ? <TablePagination page={page} totalPages={totalPages} info={`${hasVehicles ? '已选择' : '共'} ${totalVehicles.toLocaleString('zh-CN')} 辆 · 每页 ${PAGE_SIZE}${exportFeedback ? ` · ${exportFeedback}` : ''}`} disabled={mileage.isFetching || (!hasVehicles && fleetVehicles.isFetching)} onPageChange={setPage} /> : <span className="v2-table-pagination-info">{hasVehicles ? '已选择' : '共'} {totalVehicles.toLocaleString('zh-CN')} {exportFeedback ? ` · ${exportFeedback}` : ''}</span>}</footer>
</Card>
<HydrogenEvidenceSheet target={hydrogenEvidenceTarget} onClose={() => setHydrogenEvidenceTarget(undefined)} />
<footer className="v2-mileage-evidence"><span>{statistics.data?.asOf || '—'}</span><span>{criteria.sources.filter((source) => source.enabled).map((source) => source.protocol).join(' ')}</span><span> 1 · </span></footer>
<footer className="v2-mileage-evidence"><span>{statistics.data?.asOf || '—'}</span><span>{criteria.sources.filter((source) => source.enabled).map((source) => source.protocol).join(' ')}</span><span></span></footer>
</div>;
}
@@ -160,6 +160,9 @@ test('explains an empty selected-vehicle time window without asking to select th
expect(screen.getByText('车辆已选定,可调整时间范围或数据来源后重新查询。')).toBeInTheDocument();
expect(screen.queryByText('先选择车辆,再开始轨迹回放')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '修改查询' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '修改查询' }));
expect(screen.getByRole('textbox', { name: '搜索轨迹车辆' })).toHaveValue('LTEST000000000001');
expect(screen.getByRole('button', { name: /查询轨迹/ })).toBeEnabled();
});
test('distinguishes a failed vehicle lookup from an empty result and retries in place', async () => {
@@ -456,8 +459,8 @@ test('keeps one clear mobile query entry before a vehicle is selected', async ()
expect(screen.queryByRole('button', { name: '打开轨迹详情' })).not.toBeInTheDocument();
expect(screen.queryByRole('toolbar', { name: '地图轨迹工具' })).not.toBeInTheDocument();
expect(screen.queryByRole('slider', { name: '轨迹播放进度' })).not.toBeInTheDocument();
expect(screen.getByRole('slider', { name: '轨迹播放进度,等待查询结果' })).toBeDisabled();
expect(view.container.querySelector('.v2-track-playback-dock.is-empty')).toBeInTheDocument();
expect(screen.queryByRole('slider', { name: '轨迹播放进度,等待查询结果' })).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-track-playback-dock')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-workspace-empty-guide-eyebrow')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /选择车辆/ }));
expect(await screen.findByRole('dialog', { name: '轨迹查询与明细' })).toBeInTheDocument();
@@ -135,7 +135,7 @@ function trackSummaryItems(track?: TrackPlaybackResponse): WorkspaceQueueMetricR
return [
{
label: distanceLabel,
value: track ? `${number(track.summary.distanceKm)} km` : '—',
value: track ? <>{number(track.summary.distanceKm)}<small className="v2-track-distance-unit"> km</small></> : '—',
note: track ? formatDuration(track.summary.durationSeconds) : '等待查询',
tone: 'primary',
emphasis: 'primary'
@@ -291,18 +291,18 @@ const TrackRail = memo(function TrackRail({ draft, applied, track, summaryItems,
], []).map((preset) => ({ ...preset, active: activePreset === preset.key }));
const compactAppliedWindow = compactTrackWindowLabel(applied.dateFrom, applied.dateTo);
const appliedSource = applied.protocol ? protocolDisplayLabel(applied.protocol) : '自动来源';
const contextTitle = queryCollapsed ? track?.plate || applied.keyword || '轨迹查询' : '轨迹查询';
const contextTitle = queryCollapsed ? track?.plate || applied.keyword || '查询条件' : '查询条件';
return <aside className="v2-track-rail">
<Card className="v2-track-rail-shell" bodyStyle={{ padding: 0 }}>
<section className={`v2-track-query-panel${queryCollapsed ? ' is-collapsed' : ''}${showCommandBar ? '' : ' is-command-hidden'}`}>
{showCommandBar ? <WorkspaceCommandBar
className="v2-track-query-header"
ariaLabel="轨迹查询任务栏"
eyebrow="轨迹回放"
icon={<IconRoute />}
eyebrow={track ? '轨迹回放' : undefined}
icon={track ? <IconRoute /> : undefined}
title={contextTitle}
description={queryCollapsed ? `${compactAppliedWindow} · ${appliedSource}` : '最长支持连续 7 天'}
status={loading ? '读取中' : track ? undefined : '等待查询'}
description={queryCollapsed ? `${compactAppliedWindow} · ${appliedSource}` : '最长 7 天'}
status={loading ? '读取中' : undefined}
statusColor={loading ? 'blue' : track ? 'green' : 'grey'}
actions={<>
<Button
@@ -335,7 +335,7 @@ const TrackRail = memo(function TrackRail({ draft, applied, track, summaryItems,
</form> : null}
</section>
<section className="v2-track-rail-result">
{track ? <section className="v2-track-rail-result">
<WorkspaceMetricRail
variant="queue"
ariaLabel="轨迹行程摘要"
@@ -345,12 +345,11 @@ const TrackRail = memo(function TrackRail({ draft, applied, track, summaryItems,
{track ? <TrackCoverageNotice track={track} onModify={onToggleQuery} onOverview={() => onTab('overview')} /> : null}
<SegmentedTabs className="v2-track-rail-tabs" ariaLabel="轨迹明细分类" value={tab} onChange={onTab} items={[{ key: 'stops', label: '停留点', count: track?.stops.length ?? 0 }, { key: 'events', label: '事件点', count: track?.events.length ?? 0 }, { key: 'overview', label: '概览' }]} />
<div className="v2-track-rail-scroll">
{!track ? <PanelEmpty className="v2-track-rail-empty" compact tone="primary" icon={<IconMapPin />} title="选择车辆后查询轨迹" description="停留点、轨迹事件与行程证据会在这里统一呈现。" /> : null}
{track && tab === 'stops' ? <List className="v2-track-evidence-list v2-track-stop-list">{track.stops.map((stop, index) => <List.Item className="v2-track-evidence-item" key={`${stop.startTime}-${index}`}><Button theme="borderless" type="tertiary" className={`v2-track-list-action${activeStopIndexes.includes(index) ? ' is-active' : ''}`} onClick={() => onSelectIndex(stop.sampledIndex)}><i>{index + 1}</i><span><strong>{dateTime(stop.startTime)}</strong><small> {formatDuration(stop.durationSeconds)} · {stop.pointCount} </small></span><em>{time(stop.endTime)}</em></Button></List.Item>)}{!track.stops.length ? <PanelEmpty className="v2-track-list-empty" compact icon={<IconMapPin />} title="当前没有停留点" description="时间窗内没有超过 3 分钟的连续停留。" /> : null}</List> : null}
{track && tab === 'events' ? <List className="v2-track-evidence-list v2-track-event-list">{track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return <List.Item className="v2-track-evidence-item" key={`${event.type}-${event.time}-${index}`}><Button theme="borderless" type="tertiary" className={`v2-track-list-action${activeEventIndexes.includes(index) ? ' is-active' : ''}`} onClick={() => onSelectIndex(sampled)}><i className={`is-${eventTone(event.type)}`}>{index + 1}</i><span><strong>{event.title}</strong><small>{dateTime(event.time)}</small></span><em>{number(event.speedKmh, 0)} km/h</em></Button></List.Item>; })}{!track.events.length ? <PanelEmpty className="v2-track-list-empty" compact icon={<IconMapPin />} title="当前没有事件点" description="时间窗内没有识别到启停、急加速或异常间隔。" /> : null}</List> : null}
{track && tab === 'overview' ? <OverviewPanel track={track} /> : null}
</div>
</section>
</section> : <p className="v2-track-query-hint"></p>}
</Card>
</aside>;
});
@@ -479,6 +478,11 @@ export default function TrackPage() {
}, [mobileLayout, selectIndex]);
const collapseRail = useCallback(() => setRailCollapsed(true), []);
const toggleQuery = useCallback(() => setQueryCollapsed((value) => !value), []);
const openEmptyQuery = useCallback(() => {
setQueryCollapsed(false);
setRailCollapsed(false);
window.requestAnimationFrame(() => document.querySelector<HTMLInputElement>('.v2-track-query input')?.focus());
}, []);
const togglePlayback = () => {
if (points.length < 2) return;
if (!playing && boundedIndex >= points.length - 1) setActiveIndex(0);
@@ -601,12 +605,7 @@ export default function TrackPage() {
icon={<IconMapPin />}
title={emptyTrackTitle}
description={emptyTrackDescription}
steps={emptyTrackHasVehicle ? undefined : [
{ icon: <IconSearch />, title: '选择车辆', description: '车牌或 VIN 精确定位' },
{ icon: <IconMapPin />, title: '确认来源', description: '自动选择最佳定位来源' },
{ icon: <IconPlay />, title: '播放核验', description: '逐点查看速度、里程与事件' }
]}
action={<Button theme="solid" icon={<IconSearch />} aria-label={emptyTrackHasVehicle ? '修改查询' : '选择车辆'} onClick={() => setRailCollapsed(false)}>{emptyTrackHasVehicle ? '修改查询' : '选择车辆'}</Button>}
action={<Button theme="solid" icon={<IconSearch />} aria-label={emptyTrackHasVehicle ? '修改查询' : '选择车辆'} onClick={openEmptyQuery}>{emptyTrackHasVehicle ? '修改查询' : '选择车辆'}</Button>}
/></Card>}
{query.isFetching ? <PanelLoading className="v2-track-loading" compact title="正在生成轨迹" description="正在校验来源覆盖并整理停留与事件证据。" /> : null}
@@ -628,17 +627,6 @@ export default function TrackPage() {
<label><span id="track-playback-speed-label"></span><Select aria-labelledby="track-playback-speed-label" value={playbackSpeed} onChange={(value) => setPlaybackSpeed(Number(value) as PlaybackSpeed)} optionList={speedOptions.map((speed) => ({ value: speed, label: `${speed}×` }))} /></label>
<Button theme="borderless" aria-label="回到起点" icon={<IconRefresh />} onClick={() => selectIndex(0)} disabled={!boundedIndex} />
</div>
</Card> : mobileLayout ? <Card className="v2-track-playback-dock is-empty" bodyStyle={{ padding: 0 }} aria-label="轨迹播放控制,等待查询结果">
<div className="v2-track-dock-progress">
<Slider className="v2-track-progress-slider" aria-label="轨迹播放进度,等待查询结果" min={0} max={1} value={0} disabled showBoundary={false} />
<div className="v2-track-progress-meta"><time>00:00:00</time><span className="v2-track-progress-status"></span><time>00:00:00</time></div>
</div>
<div className="v2-track-dock-controls">
<Button theme="borderless" aria-label="上一个轨迹点" icon={<IconChevronLeft />} disabled />
<Button theme="solid" className="is-primary" aria-label="开始轨迹播放" icon={<IconPlay />} disabled><span className="v2-track-playback-primary-label"></span></Button>
<Button theme="borderless" aria-label="下一个轨迹点" icon={<IconChevronRight />} disabled />
<label><span id="track-empty-playback-speed-label"></span><Select aria-labelledby="track-empty-playback-speed-label" value={1} disabled optionList={[{ value: 1, label: '1×' }]} /></label>
</div>
</Card> : null}
</section>
</div>;
@@ -158,7 +158,6 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate
const customer = await screen.findByRole('button', { name: /选择客户 华东客户/ });
expect(screen.getByLabelText('账号管理操作')).toHaveClass('has-identity');
expect(screen.getByLabelText('账号管理操作')).toHaveTextContent('访问治理');
expect(screen.getByLabelText('账号管理操作').querySelector('.v2-workspace-command-icon.semi-avatar')).toBeInTheDocument();
expect(screen.getByLabelText('账号管理操作')).toHaveTextContent('客户访问治理');
expect(screen.getByLabelText('账号管理操作')).toHaveTextContent('变更最多 30 秒生效');
@@ -170,7 +169,6 @@ test('deduplicates vehicle candidates by VIN and renders granted vehicles plate
expect(view.container.querySelector('.v2-user-list.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-user-filter-panel.v2-workspace-filter-panel.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-customer-list.semi-list')).toBeInTheDocument();
expect(screen.getByRole('row', { name: '客户账号目录列' })).toHaveTextContent('客户账号菜单权限车辆权限最近登录访问状态');
const directoryMetrics = screen.getByRole('list', { name: '客户权限目录统计' });
expect(directoryMetrics.closest('.v2-user-directory-metric-rail')).toHaveClass('v2-workspace-metric-rail', 'is-queue');
expect(directoryMetrics.querySelectorAll(':scope > [role="listitem"]')).toHaveLength(4);
@@ -754,7 +754,6 @@ export default function UsersPage() {
<WorkspaceCommandBar
className="v2-user-command-bar"
ariaLabel="账号管理操作"
eyebrow="访问治理"
icon={<IconUserGroup />}
tone="authority"
title="客户访问治理"
@@ -901,7 +900,7 @@ export default function UsersPage() {
description="创建客户账号后,可在这里配置菜单和车辆范围。"
action={<Button theme="solid" icon={<IconPlus />} aria-label="创建第一个客户账号" onClick={() => startCreate()}></Button>}
/> : <>
{visibleCustomers.length ? <div className="v2-user-directory-columns" role="row" aria-label="客户账号目录列">
{visibleCustomers.length ? <div className="v2-user-directory-columns" aria-hidden="true">
<span></span>
<span></span>
<span></span>
@@ -945,7 +944,7 @@ export default function UsersPage() {
>
<Avatar className="v2-user-avatar" color={user.status === 'enabled' ? 'light-blue' : 'grey'} shape="square" size="small">{user.displayName.slice(0, 1)}</Avatar>
<span className="v2-user-list-identity">
<span className="v2-user-list-name"><b>{user.displayName}</b><span className="v2-user-provider-hint" title={authProvider.title}><Tag className="v2-user-provider-tag" color={authProvider.color} type="light" size="small">{authProvider.label}</Tag></span></span>
<span className="v2-user-list-name"><b title={user.displayName}>{user.displayName}</b><span className="v2-user-provider-hint" title={authProvider.title}><Tag className="v2-user-provider-tag" color={authProvider.color} type="light" size="small">{authProvider.label}</Tag></span></span>
<span className="v2-user-list-secondary">
<small>@{user.username}{user.customerRef ? ` · ${user.customerRef}` : ''}</small>
<span className="v2-user-list-access-summary" aria-hidden="true"><b>{user.menuKeys.length}</b> · <b>{user.vehicles.length}</b> </span>
@@ -357,15 +357,10 @@ function VehicleRecordNavigation({ onOpenSources }: { onOpenSources: () => void
['archive', '车辆主档', <IconBox />]
] as const;
return <Card className="v2-record-card v2-vehicle-record-nav" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
variant="compact"
title="车辆视图"
description="位置、事件、遥测"
actions={<nav className="v2-vehicle-section-nav" aria-label="单车详情导航">
<nav className="v2-vehicle-section-nav" aria-label="单车详情导航">
<VehicleSourceEvidenceButton onClick={onOpenSources} />
{navigationItems.map(([key, label, icon]) => <Button key={key} size="small" theme="borderless" type="tertiary" icon={icon} aria-label={`跳转到${label}`} onClick={() => jumpToSection(key)}>{label}</Button>)}
</nav>}
/>
</nav>
</Card>;
}
@@ -466,8 +461,6 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
<WorkspaceCommandBar
className="v2-vehicle-command-header v2-vehicle-record-command"
ariaLabel="单车任务栏"
eyebrow="车辆数字档案"
icon={<IconBox />}
title={<span className="v2-identity-primary">
<span className="v2-plate">{fmt(identity?.plate || realtime?.plate)}</span>
<Tag className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`} color={realtime?.online ? 'green' : 'grey'} size="small">{realtime?.online ? '在线' : '离线'}</Tag>
@@ -508,7 +501,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
<Card className="v2-single-map-card" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
title="实时位置"
description="平滑移动"
description="车辆最新有效位置"
meta={realtime?.online ? '实时跟随' : '保留最后位置'}
/>
<FleetMap vehicles={mapVehicles} selectedVin={hasLocation ? detail.vin : undefined} onSelect={() => undefined} />
@@ -316,9 +316,8 @@ export default function VehicleSearchWorkspace() {
{!mobileLayout ? <WorkspaceCommandBar
className="v2-vehicle-command-bar"
ariaLabel="查车操作"
icon={<IconSearch />}
title="查找车辆"
description="车牌、VIN 或手机号快速查车"
description="车牌、VIN 或终端手机号搜索"
status={candidates.isPending ? '正在读取授权范围' : vehicleScope}
statusColor={candidates.isError ? 'red' : 'blue'}
actions={<div className="v2-vehicle-command-actions">
@@ -90,19 +90,19 @@ describe('V2 production entry', () => {
expect(corePageSources[name]).toContain("from '../shared/WorkspaceCommandBar'");
expect(corePageSources[name]).toContain('<WorkspaceCommandBar');
}
for (const [name, eyebrow, tone] of [
['AccessPage', '接入差异', 'primary'],
['HistoryPage', '证据结果', 'primary'],
['OperationsPage', '运行快照', 'health']
// Preserve the shared section hierarchy without requiring decorative eyebrows.
for (const [name, title] of [
['AccessPage', '接入差异清单'],
['HistoryPage', '历史明细'],
['OperationsPage', '运行概览']
]) {
expect(corePageSources[name]).toContain(`eyebrow="${eyebrow}"`);
expect(corePageSources[name]).toContain(`tone="${tone}"`);
expect(corePageSources[name]).toContain('<WorkspacePanelHeader');
expect(corePageSources[name]).toContain(`title="${title}"`);
}
expect(corePageSources.StatisticsPage).toContain("eyebrow={metricView === 'mileage' ? '每日里程' : '每日氢耗'}");
expect(corePageSources.StatisticsPage).toContain("tone={metricView === 'mileage' ? 'primary' : 'health'}");
expect(corePageSources.StatisticsPage).toContain("title={metricView === 'mileage' ? '车辆每日里程' : '车辆每日氢耗'}");
expect(corePageSources.AlertsPage).toContain('aria-label="事件执行状态"');
expect(corePageSources.AlertsPage).toContain('aria-label="事件中心工作区"');
expect(reconciliationSource).toContain('eyebrow="自动对账"');
expect(reconciliationSource).toContain('<WorkspacePanelHeader');
expect(reconciliationSource).toContain('tone="health"');
expect(workspaceStyles).toContain('.v2-workspace-panel-header.has-eyebrow');
expect(workspaceStyles).toContain('.v2-workspace-panel-eyebrow.semi-typography');
@@ -482,7 +482,7 @@ describe('V2 production entry', () => {
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-playback-dock"');
expect(corePageSources.TrackPage).toContain('<Card className="v2-track-overview-section v2-track-overview-card"');
expect(corePageSources.TrackPage).toContain('<Descriptions className="v2-track-overview-descriptions"');
expect(corePageSources.TrackPage).toContain('<PanelEmpty className="v2-track-rail-empty"');
expect(corePageSources.TrackPage).toContain('title={emptyTrackTitle}');
expect(corePageSources.TrackPage).toContain('<List className="v2-track-evidence-list v2-track-stop-list"');
expect(corePageSources.TrackPage).toContain('<List className="v2-track-evidence-list v2-track-event-list"');
expect(corePageSources.TrackPage).toContain('<List.Item className="v2-track-evidence-item"');
@@ -39,8 +39,8 @@ export function WorkspacePanelHeader({
<header className={headerClassName}>
<div className="v2-workspace-panel-copy">
{eyebrow != null ? <Text className="v2-workspace-panel-eyebrow">{eyebrow}</Text> : null}
<Title heading={5}>{title}</Title>
{description ? <Text type="tertiary">{description}</Text> : null}
<Title className="v2-workspace-panel-title" heading={5}>{title}</Title>
{description ? <Text className="v2-workspace-panel-description" type="tertiary">{description}</Text> : null}
</div>
{meta || actions ? <div className={trailingClassName}>
{meta ? <span className="v2-workspace-panel-meta">{meta}</span> : null}
@@ -0,0 +1,442 @@
/* Data workspaces: quiet grouping, readable records, explicit primary actions.
* Loaded after legacy page styles; rules stay within the three data workspaces.
*/
.v2-vehicle-directory-v3.v2-vehicle-search-page,
.v2-history-page,
.v2-mileage-page,
.v2-vehicle-record-page.v2-vehicle-record-v3 {
gap: 20px;
}
.v2-vehicle-directory-v3 > .v2-vehicle-discovery-shell,
.v2-history-page > .v2-history-discovery-shell,
.v2-mileage-page .v2-mileage-discovery-shell {
border: 1px solid var(--v2-color-border);
border-radius: 16px;
background: var(--v2-color-surface);
box-shadow: none;
}
.v2-vehicle-directory-v3 .v2-vehicle-command-bar.v2-workspace-command-bar,
.v2-history-page .v2-history-command-bar.v2-workspace-command-bar {
min-height: 66px;
margin: 0;
padding: 16px 20px;
border: 0;
border-bottom: 1px solid var(--v2-color-border);
border-radius: 16px 16px 0 0;
background: transparent;
box-shadow: none;
}
.v2-vehicle-directory-v3 .v2-workspace-command-copy,
.v2-history-page .v2-workspace-command-copy {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.v2-vehicle-directory-v3 .v2-workspace-command-title.semi-typography,
.v2-history-page .v2-workspace-command-title.semi-typography {
font-size: 17px;
font-weight: 600;
letter-spacing: -.02em;
}
.v2-vehicle-directory-v3 .v2-workspace-command-description.semi-typography,
.v2-history-page .v2-workspace-command-description.semi-typography {
color: var(--v2-color-text-secondary);
font-size: 12px;
line-height: 1.5;
}
.v2-vehicle-directory-v3 .v2-vehicle-search-form,
.v2-history-page .v2-history-toolbar,
.v2-mileage-page .v2-mileage-filter {
gap: 12px;
padding: 16px 20px;
background: transparent;
}
.v2-vehicle-directory-v3 .v2-vehicle-business-filter-grid {
gap: 16px;
margin-top: 4px;
}
.v2-vehicle-directory-v3 .v2-vehicle-business-filter-grid label > span,
.v2-history-page .v2-history-toolbar label > span,
.v2-mileage-page .v2-mileage-filter label > span {
color: var(--v2-color-text-secondary);
font-size: 12px;
line-height: 1.5;
}
.v2-vehicle-directory-v3 > .v2-vehicle-recent-card.semi-card,
.v2-history-page .v2-history-table-card.semi-card,
.v2-mileage-page .v2-mileage-results.semi-card {
border: 1px solid var(--v2-color-border);
border-radius: 16px;
background: var(--v2-color-surface);
box-shadow: none;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-context h5 {
font-size: 15px;
font-weight: 600;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-context small,
.v2-vehicle-directory-v3 .v2-vehicle-directory-status small {
color: var(--v2-color-text-secondary);
font-size: 12px;
line-height: 1.5;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-table .semi-table-thead > tr > th,
.v2-history-page .v2-history-data-table .semi-table-thead > tr > th,
.v2-mileage-page .v2-mileage-table .semi-table-thead > tr > th {
padding: 12px 16px;
border-color: var(--v2-color-border);
background: var(--v2-color-surface-subtle);
color: var(--v2-color-text-secondary);
font-size: 12px;
font-weight: 500;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-table .semi-table-tbody > tr > td {
height: 68px;
padding: 13px 16px;
border-bottom-color: var(--v2-color-border);
box-shadow: none;
font-size: 14px;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-table .semi-table-row::before {
display: none;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-identity strong,
.v2-vehicle-directory-v3 .v2-vehicle-directory-profile strong {
color: var(--v2-color-text);
font-size: 14px;
font-weight: 600;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-identity small,
.v2-vehicle-directory-v3 .v2-vehicle-directory-profile small {
margin-top: 5px;
color: var(--v2-color-text-secondary);
font-size: 12px;
letter-spacing: .01em;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-open {
font-size: 13px;
font-weight: 500;
}
.v2-history-page .v2-history-table-card > .semi-card-body > .v2-workspace-panel-header,
.v2-mileage-page .v2-mileage-results > .semi-card-body > .v2-workspace-panel-header {
min-height: 72px;
padding: 16px 20px;
background: transparent;
box-shadow: none;
}
.v2-history-page .v2-history-empty.v2-workspace-empty-guide {
min-height: 320px;
padding: 56px 24px;
border: 0;
background: transparent;
box-shadow: none;
}
.v2-history-page .v2-history-empty .semi-empty-title {
font-size: 20px;
font-weight: 600;
}
.v2-history-page .v2-history-empty .semi-empty-description {
margin-top: 12px;
color: var(--v2-color-text-secondary);
font-size: 14px;
line-height: 1.7;
}
.v2-history-page .v2-history-empty .v2-workspace-empty-guide-icon {
border: 0;
border-radius: 16px;
background: var(--v2-color-surface-subtle);
box-shadow: none;
}
.v2-history-page .v2-history-data-table .semi-table-tbody > tr > td,
.v2-mileage-page .v2-mileage-table .semi-table-tbody > tr > td {
min-height: 52px;
padding: 15px 16px;
font-size: 13px;
font-variant-numeric: tabular-nums;
}
.v2-mileage-page .v2-mileage-table .semi-table-tbody > tr > td strong,
.v2-mileage-page .v2-mileage-table .semi-table-tbody > tr > td span {
font-size: 13px;
font-weight: 500;
}
.v2-mileage-page .v2-mileage-table .semi-table-tbody > tr > td.is-total strong,
.v2-mileage-page .v2-mileage-table .semi-table-tbody > tr > td.is-plate strong {
font-weight: 600;
}
.v2-mileage-page .v2-mileage-evidence {
gap: 16px;
color: var(--v2-color-text-secondary);
font-size: 12px;
line-height: 1.6;
}
.v2-vehicle-record-v3 .v2-identity-primary .v2-plate {
color: var(--v2-color-text);
font-size: 26px;
font-weight: 600;
letter-spacing: -.03em;
}
.v2-vehicle-record-v3 .v2-identity-vin b {
font-size: 13px;
font-weight: 400;
letter-spacing: .02em;
}
@media (max-width: 1100px) and (min-width: 681px) {
.v2-vehicle-directory-v3 .v2-vehicle-command-bar.v2-workspace-command-bar,
.v2-history-page .v2-history-command-bar.v2-workspace-command-bar {
flex-wrap: wrap;
}
.v2-vehicle-directory-v3 .v2-workspace-command-actions,
.v2-history-page .v2-workspace-command-actions {
flex-wrap: wrap;
justify-content: flex-start;
}
}
@media (max-width: 680px) {
.v2-vehicle-directory-v3.v2-vehicle-search-page,
.v2-history-page,
.v2-mileage-page,
.v2-vehicle-record-page.v2-vehicle-record-v3 {
gap: 12px;
}
.v2-history-page .v2-history-command-bar.v2-workspace-command-bar,
.v2-history-page .v2-history-table-card > .semi-card-body > .v2-workspace-panel-header,
.v2-mileage-page .v2-mileage-results > .semi-card-body > .v2-workspace-panel-header {
min-height: 60px;
padding: 12px 14px;
}
.v2-history-page .v2-history-empty.v2-workspace-empty-guide {
min-height: 280px;
padding: 36px 18px;
}
.v2-history-page .v2-history-empty .semi-empty-title {
font-size: 18px;
}
.v2-mileage-page .v2-mileage-evidence {
gap: 6px;
font-size: 11px;
}
}
/* Keep search suggestions outside the rounded surface without clipping them. */
.v2-vehicle-directory-v3 .v2-vehicle-query-panel.v2-workspace-filter-panel.semi-card,
.v2-vehicle-directory-v3 .v2-vehicle-query-panel > .semi-card-body,
.v2-vehicle-directory-v3 .v2-vehicle-search-form {
border: 0;
border-radius: 0 0 16px 16px;
background: transparent;
box-shadow: none;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-table .semi-table-tbody > .semi-table-row.is-online > .semi-table-row-cell:first-child,
.v2-vehicle-directory-v3 .v2-vehicle-directory-table .semi-table-tbody > .semi-table-row.is-offline > .semi-table-row-cell:first-child {
box-shadow: none;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-open {
flex-wrap: nowrap;
flex-shrink: 0;
gap: 5px;
padding-inline: 4px;
white-space: nowrap;
word-break: keep-all;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-open > svg {
flex: 0 0 auto;
}
@media (min-width: 681px) {
.v2-vehicle-directory-v3 .v2-vehicle-directory-metric-rail.is-queue.has-context > .semi-card-body {
display: grid;
grid-template-columns: minmax(300px, 1.15fr) minmax(0, 1.5fr);
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-metric-rail .v2-workspace-metric-context {
order: -1;
padding: 14px 16px;
border: 0;
border-right: 1px solid var(--v2-color-border);
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-context {
flex-wrap: wrap;
gap: 8px 14px;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-context > span:first-child {
flex: 1 1 180px;
}
.v2-vehicle-directory-v3 .v2-vehicle-directory-metric-rail .v2-workspace-metric-list {
min-width: 0;
border: 0;
}
}
.v2-vehicle-record-v3 .v2-vehicle-record-nav > .semi-card-body > .v2-vehicle-section-nav {
display: flex;
justify-content: flex-start;
gap: 6px;
overflow-x: auto;
padding: 10px 14px;
border: 0;
background: transparent;
}
.v2-vehicle-record-v3 .v2-vehicle-section-nav .semi-button {
min-height: 38px;
flex: 0 0 auto;
font-size: 13px;
}
.v2-vehicle-record-v3 .v2-event-list .v2-event-row > div > strong {
font-size: 14px;
font-weight: 600;
}
.v2-vehicle-record-v3 .v2-event-list .v2-event-row > div > p,
.v2-vehicle-record-v3 .v2-event-list .v2-event-row > time,
.v2-vehicle-record-v3 .v2-live-report-meta,
.v2-vehicle-record-v3 .v2-map-update-time {
font-size: 12px;
line-height: 1.5;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta small,
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-vin small,
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta strong,
.v2-vehicle-record-v3.is-mobile-layout .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta small,
.v2-vehicle-record-v3.is-mobile-layout .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-meta strong {
font-size: 12px;
line-height: 1.5;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button,
.v2-vehicle-record-v3.is-mobile-layout .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button {
min-height: 38px;
font-size: 13px;
}
.v2-content.is-section-vehicles .v2-monitor-return > a,
.v2-content.is-section-vehicles .v2-monitor-return > span,
.v2-content.is-section-vehicles .v2-monitor-return > em {
font-size: 13px;
line-height: 1.5;
}
@media (min-width: 681px) {
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command {
min-height: 100px;
padding: 0;
grid-template-columns: minmax(235px, .7fr) minmax(0, 2fr);
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-copy {
display: flex;
min-height: 100px;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 9px;
padding: 16px 20px;
border-right: 0;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-workspace-command-actions {
grid-template-columns: minmax(220px, .85fr) minmax(270px, 1.15fr);
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-primary {
justify-content: flex-start;
padding: 0;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-plate {
border: 0;
background: transparent;
box-shadow: none;
padding: 0;
font-size: 24px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-content: center;
gap: 6px;
padding: 12px 16px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.v2-vehicle-record-command .v2-identity-actions > .semi-button {
width: auto;
flex: 0 0 auto;
padding-inline: 10px;
}
}
@media (max-width: 680px) {
.v2-vehicle-directory-v3 .v2-vehicle-mobile-scope-row {
grid-template-columns: minmax(0, 1fr);
gap: 6px;
min-width: 0;
}
.v2-vehicle-directory-v3 .v2-vehicle-mobile-scope-row > .v2-mobile-filter-toggle.semi-button {
width: 100%;
min-width: 0;
min-height: 56px;
}
.v2-vehicle-directory-v3 .v2-vehicle-mobile-scope-row .v2-mobile-filter-toggle-content {
width: 100%;
min-width: 0;
grid-template-columns: 38px minmax(0, 1fr) auto;
}
.v2-vehicle-directory-v3 .v2-vehicle-mobile-scope-row > .v2-vehicle-mobile-sync.semi-button {
width: auto;
min-width: 118px;
min-height: 44px;
justify-self: end;
padding: 10px 14px;
font-size: 13px;
white-space: nowrap;
}
}
@@ -0,0 +1,419 @@
/* Governance workspaces: readable evidence, quiet grouping, one row per entity.
* Keep this after the legacy workspace layers. Prefixing the user selectors also
* protects the directory from its asynchronously loaded users.css stylesheet.
*/
.v2-user-admin,
.v2-access-page-v3,
.v2-ops-page,
.v2-event-center {
font-variant-numeric: tabular-nums;
}
.v2-user-admin .v2-user-discovery-shell,
.v2-access-page-v3 .v2-access-discovery-shell,
.v2-ops-page .v2-ops-command-bar {
background: var(--v2-color-surface);
box-shadow: none;
}
.v2-user-admin .v2-user-command-bar,
.v2-access-page-v3 .v2-access-command-bar,
.v2-ops-page .v2-ops-command-bar {
background: transparent;
border-radius: 0;
}
/* Account identity and every fact share one column definition with the header. */
.v2-user-admin .v2-user-list {
--v2-user-row-columns: 36px minmax(190px, 2fr) minmax(80px, .55fr) minmax(90px, .6fr) minmax(164px, 1fr) minmax(96px, .6fr);
}
.v2-user-admin .v2-customer-list .semi-list-items {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0;
border: 0;
border-radius: 0;
}
.v2-user-admin .v2-user-directory-columns,
.v2-user-admin .v2-customer-list .v2-user-list-item .semi-button-content {
display: grid;
grid-template-columns: var(--v2-user-row-columns);
gap: 12px;
align-items: center;
}
.v2-user-admin .v2-user-directory-columns {
min-height: 42px;
padding: 0 20px;
color: var(--v2-color-text-secondary);
font-size: 12px;
background: var(--v2-color-surface-subtle);
}
.v2-user-admin .v2-user-directory-columns > span:first-child { grid-column: 1 / 3; }
.v2-user-admin .v2-user-directory-columns > span:last-child { text-align: right; }
.v2-user-admin .v2-customer-list .v2-user-list-row.semi-list-item {
width: 100%;
min-width: 0;
min-height: 72px;
background: var(--v2-color-surface);
box-shadow: none;
border-bottom: 1px solid var(--v2-color-border);
}
.v2-user-admin .v2-customer-list .v2-user-list-row.semi-list-item:hover {
background: var(--v2-color-surface-subtle);
box-shadow: none;
}
.v2-user-admin .v2-customer-list .v2-user-list-row.is-active.semi-list-item {
background: var(--v2-color-primary-soft, #edf4ff);
box-shadow: inset 3px 0 var(--v2-color-primary);
}
.v2-user-admin .v2-customer-list .v2-user-list-item.semi-button {
width: 100%;
min-height: 72px;
padding: 12px 20px;
white-space: normal;
}
.v2-user-admin .v2-user-list-name > b { font-size: 14px; font-weight: 600; }
.v2-user-admin .v2-user-list-secondary > small { font-size: 12px; color: var(--v2-color-text-secondary); }
.v2-user-admin .v2-user-list-facts b,
.v2-user-admin .v2-user-list-facts.is-login b { font-size: 13px; font-weight: 500; color: var(--v2-color-text); }
.v2-user-admin .v2-user-list-facts.is-menu,
.v2-user-admin .v2-user-list-facts.is-vehicle { padding: 0; border: 0; background: transparent; }
.v2-user-admin .v2-user-provider-tag.semi-tag,
.v2-user-admin .v2-user-status-tag.semi-tag { font-size: 12px; height: auto; min-height: 24px; line-height: 20px; }
.v2-user-admin .v2-user-avatar.semi-avatar { border: 0; background: #f0f1f3; color: var(--v2-color-text-secondary); }
.v2-user-admin .v2-customer-list-scroll { scrollbar-gutter: stable; }
/* Protocol evidence reads as aligned text; colour remains on the actual state. */
.v2-access-page-v3 .v2-access-protocol-cell {
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
padding: 4px 0;
min-height: 62px;
}
.v2-access-page-v3 .v2-access-primary-cell > strong,
.v2-access-page-v3 .v2-access-protocol-cell strong,
.v2-access-page-v3 .v2-access-protocol-cell time { font-size: 14px; font-weight: 500; }
.v2-access-page-v3 .v2-access-primary-cell > span,
.v2-access-page-v3 .v2-access-protocol-cell small,
.v2-access-page-v3 .v2-access-connection small { font-size: 12px; color: var(--v2-color-text-secondary); line-height: 1.5; }
.v2-access-page-v3 .v2-access-connection p { font-size: 13px; }
.v2-access-page-v3 .v2-access-connection-tag.semi-tag { font-size: 12px; min-height: 24px; }
.v2-access-page-v3 .v2-access-semi-table .semi-table-row > .semi-table-row-cell:first-child { box-shadow: none; }
.v2-access-page-v3 .v2-access-semi-table .semi-table-row > .semi-table-row-cell:first-child::before { display: none; }
.v2-access-page-v3 .v2-access-semi-table .semi-table-row { box-shadow: none; }
.v2-access-page-v3 .v2-access-semi-table .semi-table-tbody > .semi-table-row:is(.is-state-healthy, .is-state-degraded, .is-state-incomplete, .is-state-offline, .is-state-not_connected, .is-selected) > .semi-table-row-cell:first-child { box-shadow: none; }
.v2-access-page-v3 .v2-access-status-tabs { height: auto; overflow: visible; }
.v2-access-page-v3 .v2-access-metric-rail.is-queue,
.v2-access-page-v3 .v2-access-metric-rail.is-queue > .semi-card-body,
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-list { height: auto; min-height: 80px; }
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-list > span,
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-action.semi-button {
height: auto;
min-height: 80px;
overflow: visible;
}
/* Action metrics contain a button, unlike plain metrics' three text nodes.
* Do not place that button into the legacy 14px/24px text grid: its 80px
* content otherwise starts in the first short track and spills below the rail.
*/
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-list > span.is-action {
display: block;
grid-template-columns: none;
grid-template-rows: none;
padding: 0;
}
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-action.semi-button {
display: block;
padding: 0;
}
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-list > span:not(.is-action),
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-action .semi-button-content {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-rows: auto auto;
align-content: center;
align-items: center;
height: auto;
min-height: 80px;
gap: 4px 12px;
padding: 12px 16px;
overflow: visible;
}
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-list small { grid-column: 1 / -1; grid-row: 1; font-size: 12px; line-height: 1.5; }
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-list strong { grid-column: 1; grid-row: 2; font-size: 24px; line-height: 1.2; }
.v2-access-page-v3 .v2-access-metric-rail .v2-workspace-metric-list em { grid-column: 2; grid-row: 2; font-size: 12px; line-height: 1.5; white-space: normal; }
/* The event itself is primary. The existing native alarm title stays intact. */
.v2-event-center .v2-event-identity > span {
width: 28px;
height: 28px;
flex-basis: 28px;
border: 0;
border-radius: 7px;
background: var(--v2-color-surface-subtle);
color: var(--v2-color-text-secondary);
box-shadow: none;
}
.v2-event-center .v2-event-identity strong { font-size: 14px; line-height: 1.5; font-weight: 600; }
.v2-event-center .v2-event-identity small,
.v2-event-center .v2-alert-event-vehicle small { font-size: 12px; line-height: 1.5; color: var(--v2-color-text-secondary); }
.v2-event-center .v2-alert-event-vehicle strong,
.v2-event-center .v2-alert-event-rule,
.v2-event-center .v2-alert-time { font-size: 13px; line-height: 1.5; font-weight: 500; }
.v2-event-center .v2-event-execution.semi-tag { min-height: 24px; height: auto; font-size: 12px; line-height: 20px; }
.v2-event-center .v2-alert-event-table .semi-table-row > .semi-table-row-cell { border-right: 0; }
.v2-event-center .v2-alert-event-table .semi-table-row > .semi-table-row-cell:first-child { box-shadow: none; }
.v2-event-center .v2-alert-event-table .semi-table-row > .semi-table-row-cell:first-child::before { display: none; }
/* Let Semi's table body own horizontal scrolling. The legacy 970px minimum on
* its outer wrapper put the sticky right edge beyond the visible viewport. */
.v2-event-center .v2-alert-workspace .v2-alert-table-scroll .v2-alert-event-table.semi-table-wrapper {
min-width: 0;
width: 100%;
max-width: 100%;
}
.v2-event-center .v2-alert-event-table .is-status { position: sticky; right: 0; z-index: 2; background: var(--v2-color-surface); box-shadow: -1px 0 var(--v2-color-border); }
.v2-event-center .v2-alert-event-table .semi-table-thead .is-status { z-index: 3; background: var(--v2-color-surface-subtle); }
.v2-event-center .v2-alert-event-table .is-selected .is-status { background: var(--v2-color-primary-soft); }
.v2-event-center .v2-event-status-tabs { gap: 4px; padding: 8px 12px; }
.v2-event-center .v2-event-status-tabs .semi-button { min-height: 38px; padding-inline: 12px; font-size: 13px; }
.v2-event-center .v2-event-status-tabs strong { font-size: 13px; font-weight: 600; }
.v2-event-center .v2-alert-detail-card { border-color: var(--v2-color-border); background: var(--v2-color-surface); box-shadow: none; }
.v2-event-center .v2-alert-automation-row .v2-alert-rule-item.semi-button,
.v2-event-center .v2-alert-automation-row .v2-alert-rule-item.semi-button:hover,
.v2-event-center .v2-alert-automation-row .v2-alert-rule-item.is-selected.semi-button {
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
transform: none;
}
.v2-event-center .v2-alert-automation-row.is-selected { background: var(--v2-color-primary-soft); box-shadow: none; }
.v2-event-center .v2-alert-automation-row .v2-alert-rule-copy strong { font-size: 14px; line-height: 1.5; }
.v2-event-center .v2-alert-automation-row .v2-alert-rule-copy small { font-size: 12px; line-height: 1.5; }
.v2-event-center .v2-alert-automation-row .v2-alert-automation-icon { background: var(--v2-color-surface-subtle); color: var(--v2-color-text-secondary); box-shadow: none; }
.v2-event-center .v2-alert-automation-switch-target { grid-area: switch; display: grid; place-items: center; min-width: 44px; min-height: 44px; cursor: pointer; }
.v2-event-center .v2-alert-automation-switch-target .v2-alert-automation-switch { grid-area: auto; }
.v2-event-center .v2-alert-automation-version { font-size: 12px; }
.v2-alert-inspector .v2-native-alarm-fields { padding: 16px; }
.v2-alert-inspector .v2-native-alarm-meaning > strong { font-size: 14px; font-weight: 600; }
.v2-alert-inspector .v2-native-alarm-meaning ul { margin: 10px 0; padding-left: 20px; font-size: 14px; line-height: 1.7; }
.v2-alert-inspector .v2-native-alarm-meaning p,
.v2-alert-inspector .v2-native-alarm-fields > p { font-size: 12px; color: var(--v2-color-text-secondary); line-height: 1.6; }
.v2-alert-inspector .v2-alert-descriptions .semi-descriptions-item-key { font-size: 12px; line-height: 1.6; }
.v2-alert-inspector .v2-alert-descriptions .semi-descriptions-item-value { font-size: 13px; line-height: 1.6; }
:is(.v2-event-center, .v2-alert-detail-sidesheet) .v2-alert-inspector .v2-alert-focus-card .v2-alert-evidence {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0;
min-height: 0;
height: auto;
border: 0;
background: transparent;
padding: 12px 16px;
}
:is(.v2-event-center, .v2-alert-detail-sidesheet) .v2-alert-inspector .v2-alert-focus-card .v2-alert-evidence > .v2-alert-evidence-value.semi-card {
min-height: 0;
height: auto;
border: 0;
border-bottom: 1px solid var(--v2-color-border);
border-radius: 0;
background: transparent;
box-shadow: none;
}
:is(.v2-event-center, .v2-alert-detail-sidesheet) .v2-alert-inspector .v2-alert-focus-card .v2-alert-evidence > .v2-alert-evidence-value > .semi-card-body {
display: grid;
grid-template-columns: 76px minmax(0, 1fr);
align-items: center;
gap: 12px;
height: auto;
min-height: 44px;
padding: 10px 0;
text-align: left;
}
:is(.v2-event-center, .v2-alert-detail-sidesheet) .v2-alert-inspector .v2-alert-focus-card .v2-alert-evidence small { font-size: 12px; line-height: 1.5; }
:is(.v2-event-center, .v2-alert-detail-sidesheet) .v2-alert-inspector .v2-alert-focus-card .v2-alert-evidence strong { font-size: 14px; line-height: 1.5; white-space: normal; overflow-wrap: anywhere; }
.v2-event-center .v2-alert-inspector .v2-alert-inspector-close.semi-button { width: 36px; height: 36px; min-width: 36px; min-height: 36px; padding: 8px; }
.v2-event-center .v2-alert-inspector .v2-alert-inspector-close :is(.semi-icon, svg) { width: 16px; height: 16px; font-size: 16px; }
/* Quality queues and diagnostic evidence share the same reading size. */
.v2-ops-page .v2-reconcile-cell > strong,
.v2-ops-page .v2-reconcile-cell > b,
.v2-ops-page .v2-source-cell > strong { font-size: 14px; line-height: 1.5; font-weight: 600; }
.v2-ops-page .v2-reconcile-cell small,
.v2-ops-page .v2-source-cell small { font-size: 12px; line-height: 1.5; color: var(--v2-color-text-secondary); }
.v2-ops-page .v2-reconcile-table .semi-table-row-cell,
.v2-ops-page .v2-source-table .semi-table-row-cell { padding-block: 14px; }
.v2-ops-page .v2-ops-link-card,
.v2-ops-page .v2-ops-source-card,
.v2-ops-page .v2-source-recommendation { background: var(--v2-color-surface); box-shadow: none; border-color: var(--v2-color-border); }
.v2-ops-page .v2-ops-source-evidence p,
.v2-ops-page .v2-ops-source-action p,
.v2-ops-page .v2-ops-link-card p { font-size: 13px; line-height: 1.6; color: var(--v2-color-text-secondary); }
.v2-ops-page .v2-ops-source-evidence > strong,
.v2-ops-page .v2-ops-source-action > strong,
.v2-ops-page .v2-ops-link-status strong { font-size: 14px; font-weight: 600; }
/* Portal-mounted inspectors need their own scope. */
:is(.v2-user-editor-sidesheet, .v2-user-grant-sidesheet, .v2-user-import-sidesheet, .v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .v2-workspace-config-summary > span::before {
display: none;
}
:is(.v2-user-editor-sidesheet, .v2-user-grant-sidesheet, .v2-user-import-sidesheet, .v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .v2-workspace-config-summary > span {
background: transparent;
box-shadow: none;
border-left: 0;
}
:is(.v2-user-editor-sidesheet, .v2-user-grant-sidesheet, .v2-user-import-sidesheet, .v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .v2-workspace-config-summary > span:is(.is-primary, .is-success, .is-warning, .is-danger) {
background: transparent;
box-shadow: none;
border-left: 0;
}
:is(.v2-user-editor-sidesheet, .v2-user-grant-sidesheet, .v2-user-import-sidesheet, .v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .v2-workspace-config-summary > span :is(small, em) {
font-size: 12px;
line-height: 1.5;
color: var(--v2-color-text-secondary);
}
:is(.v2-user-editor-sidesheet, .v2-user-grant-sidesheet, .v2-user-import-sidesheet, .v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .v2-workspace-config-summary > span > strong {
font-size: 14px;
line-height: 1.5;
color: var(--v2-color-text);
}
.v2-user-editor-sidesheet .v2-workspace-config-content .v2-user-editor-form :is(small, em),
.v2-user-editor-sidesheet .v2-user-editor-switcher small {
font-size: 12px;
line-height: 1.5;
color: var(--v2-color-text-secondary);
}
.v2-user-editor-sidesheet .v2-workspace-config-content .v2-user-editor-form :is(strong, b) {
font-size: 14px;
line-height: 1.5;
font-weight: 600;
}
.v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-content {
background: var(--v2-color-surface);
padding: 0;
}
.v2-user-editor-sidesheet .v2-user-editor-form .v2-user-editor-section.semi-card {
border: 0;
border-radius: 0;
box-shadow: none;
}
.v2-user-editor-sidesheet .v2-user-editor-form :is(.v2-user-vehicle-composer, .v2-user-identity-overview, .v2-user-menu-overview, .v2-user-identity-source, .v2-assigned-vins) {
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.v2-user-editor-sidesheet .v2-user-vehicle-composer-heading {
background: transparent;
border: 0;
padding: 0 0 12px;
}
.v2-user-editor-sidesheet .v2-user-vehicle-composer .v2-vehicle-permission-tools { padding: 0 0 16px; }
.v2-user-editor-sidesheet .v2-assigned-toolbar { border-top: 1px solid var(--v2-color-border); padding-top: 16px; }
.v2-user-editor-sidesheet .v2-assigned-vins > .v2-assigned-vehicle-card.semi-card { padding: 16px 0; }
.v2-user-editor-sidesheet .v2-assigned-vehicle-card .v2-assigned-vehicle-actions .semi-button { font-size: 12px; min-height: 36px; }
.v2-user-editor-sidesheet .v2-assigned-vehicle-card .v2-assigned-vehicle-interval :is(small, strong) { font-size: 12px; line-height: 1.5; }
.v2-user-editor-sidesheet .v2-user-editor-form .v2-user-field-group { border: 0; border-radius: 0; box-shadow: none; }
.v2-user-editor-sidesheet .v2-user-editor-form .v2-user-field-group + .v2-user-field-group { border-top: 1px solid var(--v2-color-border); padding-top: 20px; }
:is(.v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .v2-workspace-config-content :is(small, em) {
font-size: 12px;
line-height: 1.5;
}
:is(.v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .v2-workspace-config-content :is(strong, b) {
font-size: 14px;
line-height: 1.5;
}
.v2-access-detail-sidesheet .v2-access-inspector-v3.semi-card,
.v2-reconcile-detail-sidesheet .v2-reconcile-detail.semi-card {
border: 0;
background: transparent;
box-shadow: none;
}
.v2-access-detail-sidesheet .v2-access-inspector-v3 .v2-access-protocol-detail.semi-card,
.v2-reconcile-detail-sidesheet .v2-reconcile-detail .v2-reconcile-evidence.semi-card {
border: 0;
border-top: 1px solid var(--v2-color-border);
border-radius: 0;
box-shadow: none;
background: transparent;
}
:is(.v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-alert-detail-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .semi-descriptions-item-key {
font-size: 12px;
color: var(--v2-color-text-secondary);
line-height: 1.6;
}
:is(.v2-access-detail-sidesheet, .v2-access-governance-sidesheet, .v2-alert-detail-sidesheet, .v2-reconcile-detail-sidesheet, .v2-source-policy-sidesheet) .semi-descriptions-item-value {
font-size: 14px;
line-height: 1.6;
}
@media (min-width: 681px) and (max-width: 1180px) {
.v2-access-page-v3 .v2-access-filter-actions { display: flex; justify-content: flex-end; grid-column: 1 / -1; }
.v2-access-page-v3 .v2-access-filter-actions > .semi-button { width: auto; min-width: 80px; flex: 0 0 auto; }
.v2-ops-page .v2-reconcile-heading.v2-workspace-panel-header { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px; }
.v2-ops-page .v2-reconcile-heading .v2-workspace-panel-copy { width: 100%; flex: 1 0 100%; min-width: 0; }
.v2-ops-page .v2-reconcile-heading .v2-workspace-panel-description { max-width: 100%; white-space: normal; }
.v2-ops-page .v2-reconcile-heading .v2-workspace-panel-actions { width: 100%; flex: 1 0 100%; justify-content: flex-end; flex-wrap: wrap; }
.v2-user-admin .v2-user-list { --v2-user-row-columns: 32px minmax(150px, 1fr) 76px 84px 100px; }
.v2-user-admin .v2-user-directory-columns > span:nth-child(4),
.v2-user-admin .v2-user-list-facts.is-login { display: none; }
.v2-user-admin .v2-customer-list .v2-user-list-item.semi-button,
.v2-user-admin .v2-user-directory-columns { padding-inline: 14px; }
}
@media (max-width: 680px) {
.v2-ops-page .v2-reconcile-heading.v2-workspace-panel-header { display: flex; flex-direction: row; align-items: center; padding: 12px 16px; min-height: 64px; }
.v2-ops-page .v2-reconcile-heading .v2-workspace-panel-actions { width: auto; flex: 0 0 auto; }
.v2-ops-page .v2-reconcile-mobile-more.semi-button { min-height: 44px; font-size: 12px; }
.v2-user-admin .v2-user-list { --v2-user-row-columns: 34px minmax(0, 1fr) auto; }
.v2-user-admin .v2-user-directory-columns { display: none; }
.v2-user-admin .v2-customer-list .v2-user-list-item .semi-button-content { gap: 10px; }
.v2-user-admin .v2-customer-list .v2-user-list-item.semi-button { padding: 12px; min-height: 76px; }
.v2-user-admin .v2-user-list-facts { display: none; }
.v2-user-admin .v2-user-list-name > b { white-space: normal; overflow-wrap: anywhere; font-size: 14px; }
.v2-user-admin .v2-user-list-secondary { display: grid; gap: 3px; }
.v2-user-admin .v2-user-list-access-summary { font-size: 12px; }
.v2-user-admin .v2-user-list-trailing { min-width: 0; }
.v2-user-admin .v2-user-status-tag.semi-tag { font-size: 11px; }
.v2-event-center .v2-alert-mobile-card-content > header > strong { font-size: 14px; white-space: normal; }
.v2-event-center .v2-alert-mobile-facts,
.v2-access-page-v3 .v2-access-mobile-card-content > p { font-size: 12px; line-height: 1.5; }
.v2-event-center .v2-event-status-tabs { padding: 6px 8px; gap: 2px; }
.v2-event-center .v2-event-status-tabs .semi-button { padding-inline: 10px; }
.v2-access-page-v3 .v2-access-protocol-cell { min-height: 58px; }
.v2-access-page-v3 .v2-access-mobile-card { box-shadow: none; border-left: 0; }
.v2-access-page-v3 .v2-access-mobile-card::before,
.v2-access-page-v3 .v2-access-mobile-card .v2-access-mobile-action::before { display: none; }
.v2-access-page-v3 .v2-access-mobile-list .v2-access-protocol-cell {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
align-content: start;
gap: 6px;
padding: 10px 7px;
min-height: 94px;
}
.v2-access-page-v3 .v2-access-mobile-list .v2-access-protocol-cell > :is(b, span, strong) {
grid-column: 1;
justify-self: start;
font-size: 12px;
line-height: 1.4;
white-space: nowrap;
overflow: visible;
}
.v2-access-page-v3 .v2-access-mobile-list .v2-access-protocol-cell time { font-size: 12px; line-height: 1.4; }
.v2-access-page-v3 .v2-access-mobile-list .v2-access-protocol-cell > span i { width: 5px; height: 5px; }
}
@@ -0,0 +1,241 @@
/* Quiet map chrome; playback and data controls keep their existing behavior. */
.v2-monitor-page .v2-monitor-workspace,
.v2-track-page .v2-track-stage,
.v2-track-page .v2-track-rail-shell.semi-card {
border-color: rgba(60, 60, 67, .13);
border-radius: 16px;
box-shadow: none;
}
.v2-monitor-page .v2-vehicle-rail { background: #fff; border-right-color: rgba(60, 60, 67, .12); }
.v2-monitor-page .v2-vehicle-rail > header { padding: 17px 18px 9px; }
.v2-monitor-page .v2-vehicle-rail > header strong { color: #1d1d1f; font-size: 15px; font-weight: 600; }
.v2-monitor-page .v2-rail-search {
min-height: 28px;
height: auto;
margin: 0 18px 8px;
padding: 0;
border: 0;
border-radius: 0;
color: #6e6e73;
background: transparent;
box-shadow: none;
font-size: 12px;
line-height: 20px;
}
.v2-monitor-page .v2-vehicle-row.semi-button {
width: calc(100% - 16px);
margin: 2px 8px;
padding: 14px 10px;
border: 0;
border-radius: 10px;
background: transparent;
box-shadow: none;
}
.v2-monitor-page .v2-vehicle-row.semi-button:hover { background: #f5f5f7; }
.v2-monitor-page .v2-vehicle-row.semi-button.is-selected { background: #eaf3ff; box-shadow: none; }
.v2-monitor-page .v2-vehicle-identity strong { color: #1d1d1f; font-size: 14px; font-weight: 600; }
.v2-monitor-page .v2-vehicle-identity small,
.v2-monitor-page .v2-vehicle-motion > small { color: #6e6e73; font-size: 11px; }
.v2-monitor-page .v2-map-control-button.semi-button {
min-height: 48px;
border: 1px solid rgba(60, 60, 67, .12);
border-radius: 12px;
background: rgba(255, 255, 255, .96);
box-shadow: 0 3px 16px rgba(0, 0, 0, .08);
}
.v2-monitor-page .v2-map-control-copy strong { color: #1d1d1f; font-size: 12px; font-weight: 600; }
.v2-monitor-page .v2-map-control-copy small { color: #6e6e73; font-size: 10px; }
.v2-monitor-page .v2-map-legend {
min-height: 36px;
border-color: rgba(60, 60, 67, .12);
border-radius: 10px;
background: rgba(255, 255, 255, .96);
box-shadow: 0 3px 16px rgba(0, 0, 0, .06);
}
.v2-monitor-page .v2-event-strip.semi-card { border-color: rgba(60, 60, 67, .1); border-radius: 12px; box-shadow: none; }
.v2-track-page { background: #f5f5f7; }
.v2-track-page .v2-track-stage,
.v2-track-page .v2-track-map { background: #eef0f2; }
.v2-track-query-hint { margin: 0; padding: 18px; color: #6e6e73; font-size: 12px; line-height: 1.7; }
.v2-track-query-panel .v2-track-query-header.v2-workspace-command-bar { background: #fff; box-shadow: none; }
.v2-track-query { gap: 18px; padding: 18px; }
.v2-track-query > label > span { color: #6e6e73; font-size: 12px; font-weight: 500; }
.v2-track-page .v2-track-empty-state.semi-card {
width: min(520px, calc(100% - 48px));
border: 0;
border-radius: 20px;
background: transparent;
box-shadow: none;
}
.v2-track-page .v2-track-empty-state .v2-workspace-empty-guide,
.v2-track-page .v2-track-mobile-empty .v2-workspace-empty-guide { border: 0; background: transparent; box-shadow: none; }
.v2-track-page .v2-track-empty-state .v2-workspace-empty-guide::before,
.v2-track-page .v2-track-mobile-empty .v2-workspace-empty-guide::before { display: none; }
.v2-track-page .v2-track-empty-state .semi-empty-title { color: #1d1d1f; font-size: 22px; font-weight: 600; letter-spacing: -.025em; line-height: 1.4; }
.v2-track-page .v2-track-empty-state .semi-empty-description { color: #6e6e73; font-size: 13px; line-height: 1.7; }
.v2-track-page .v2-track-current-card.semi-card {
border-color: rgba(60, 60, 67, .12);
border-radius: 14px;
background: rgba(255, 255, 255, .97);
box-shadow: 0 6px 24px rgba(0, 0, 0, .09);
}
.v2-track-page .v2-track-stage-tools {
padding: 4px;
border: 1px solid rgba(60, 60, 67, .12);
border-radius: 12px;
background: rgba(255, 255, 255, .97);
box-shadow: 0 3px 16px rgba(0, 0, 0, .08);
}
.v2-track-page .v2-track-stage-tools .semi-button { min-height: 36px; border-radius: 8px; font-size: 12px; font-weight: 500; }
.v2-track-page .v2-track-stage-tools .semi-button.is-active { color: #007aff; background: #eaf3ff; }
.v2-track-page .v2-track-playback-dock.semi-card { border-color: rgba(60, 60, 67, .13); background: rgba(255, 255, 255, .98); box-shadow: 0 -3px 20px rgba(0, 0, 0, .06); }
.v2-track-page .v2-track-progress-meta { color: #6e6e73; font-size: 12px; font-variant-numeric: tabular-nums; }
.v2-track-page .v2-track-progress-meta time { color: #1d1d1f; font-weight: 500; }
.v2-track-page .v2-track-dock-controls .semi-button { min-width: 40px; height: 40px; border-radius: 10px; }
.v2-track-page .v2-track-dock-controls .semi-button.is-primary { min-width: 64px; background: #007aff; box-shadow: none; }
@media (max-width: 960px) {
.v2-monitor-page .v2-map-control-button.semi-button,
.v2-monitor-page .v2-monitor-mobile-card .semi-button,
.v2-track-page .v2-track-stage-tools .semi-button,
.v2-track-page .v2-track-dock-controls .semi-button,
.v2-track-page .v2-track-dock-controls .semi-select,
.v2-track-detail-sidesheet .v2-track-query .semi-button,
.v2-track-detail-sidesheet .v2-track-query .semi-input-wrapper,
.v2-track-detail-sidesheet .v2-track-query .semi-select,
.v2-track-detail-sidesheet .v2-track-list-action.semi-button { min-height: 44px; }
.v2-track-page .v2-track-dock-controls .semi-button { min-width: 44px; }
.v2-track-page .v2-track-progress-slider.semi-slider-wrapper { min-height: 44px; display: flex; align-items: center; }
.v2-track-page .v2-track-mobile-query-launcher.semi-button { min-height: 60px; border-radius: 14px; background: #fff; box-shadow: 0 3px 18px rgba(0, 0, 0, .08); }
.v2-track-page .v2-track-mobile-empty { bottom: 24px; }
.v2-track-page .v2-track-mobile-empty .semi-empty-title { color: #1d1d1f; font-size: 20px; font-weight: 600; }
}
/* A narrow evidence rail must show complete dates and complete metric values. */
.v2-track-date-time-picker .semi-datepicker-range-input {
display: grid;
grid-template-columns: minmax(0, 1fr) 28px;
gap: 0;
height: auto;
min-height: 88px;
padding: 0 6px 0 10px;
border: 1px solid rgba(60, 60, 67, .15);
border-radius: 10px;
background: #fff;
}
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
height: 44px;
grid-column: 1;
padding: 0;
}
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper-start { grid-row: 1; }
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper-end { grid-row: 2; border-top: 1px solid rgba(60, 60, 67, .09); }
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper::before { flex: 0 0 36px; color: #6e6e73; font-size: 12px; }
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper-start::before { content: '开始'; }
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper-end::before { content: '结束'; }
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper .semi-input-wrapper {
flex: 1;
min-width: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.v2-track-date-time-picker .semi-datepicker-range-input-wrapper .semi-input {
padding-left: 0;
padding-right: 0;
font-size: 14px;
font-variant-numeric: tabular-nums;
}
.v2-track-date-time-picker .semi-datepicker-range-input-separator { display: none; }
.v2-track-date-time-picker .semi-datepicker-range-input-suffix { grid-column: 2; grid-row: 1 / 3; align-self: center; width: 28px; margin: 0; }
.v2-track-result-metric-rail .v2-workspace-metric-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.v2-track-result-metric-rail .v2-workspace-metric-list > span { padding: 12px 16px; border-bottom: 1px solid rgba(60, 60, 67, .09); }
.v2-track-result-metric-rail .v2-workspace-metric-list > span > strong,
.v2-track-result-metric-rail .v2-workspace-metric-list > span.is-secondary > strong {
overflow: visible;
white-space: normal;
overflow-wrap: anywhere;
text-overflow: clip;
font-variant-numeric: tabular-nums;
}
.v2-track-distance-unit { font-size: 12px; font-weight: 500; color: #6e6e73; }
.v2-track-page .v2-track-current-card .v2-track-current-metrics small,
.v2-track-page .v2-track-current-card .v2-track-current-metrics em,
.v2-track-page .v2-track-current-card .v2-track-current-evidence em,
.v2-track-page .v2-track-current-card .v2-track-current-evidence small,
.v2-track-page .v2-track-current-card > .semi-card-body > p { color: #6e6e73; font-size: 12px; }
.v2-track-page .v2-track-current-card .v2-track-current-metrics strong {
overflow: visible;
white-space: normal;
overflow-wrap: anywhere;
text-overflow: clip;
}
.v2-track-page .v2-track-current-card .v2-track-current-evidence { flex-wrap: wrap; gap: 6px 10px; }
.v2-track-result-metric-rail .v2-workspace-metric-list > span > small,
.v2-track-result-metric-rail .v2-workspace-metric-list > span > em { color: #6e6e73; font-size: 12px; line-height: 1.5; white-space: normal; }
.v2-track-page .v2-track-current-card > .semi-card-body > p.is-address-error,
.v2-track-page .v2-track-current-card > .semi-card-body > p.is-address-error > span { color: #6e6e73; font-size: 12px; }
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics small > b { color: #6e6e73; font-size: 11px; }
@media (max-width: 960px) {
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span { padding: 8px 12px; }
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span:nth-child(3) { border-left: 0; }
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span:nth-child(n+3) { border-top: 1px solid rgba(60, 60, 67, .09); }
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics strong,
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics > span:last-child strong { font-size: 16px; overflow: visible; text-overflow: clip; white-space: normal; }
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics small,
.v2-track-page .v2-track-current-card > .semi-card-body > .v2-track-current-metrics em { font-size: 12px; }
}
/* Let the map consume the space left by controls inside the mobile shell. */
@media (max-width: 760px) {
.v2-monitor-page.is-map-mode {
height: 100%;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
.v2-monitor-page.is-map-mode > .v2-filterbar,
.v2-monitor-page.is-map-mode > .v2-monitor-summary-rail,
.v2-monitor-page.is-map-mode > .v2-event-strip { flex: 0 0 auto; }
.v2-monitor-page.is-map-mode > .v2-monitor-workspace,
.v2-monitor-page.is-map-mode > .v2-monitor-workspace.is-detail-open,
.v2-monitor-page.is-map-mode > .v2-monitor-workspace.is-detail-collapsed {
flex: 1 1 0;
min-height: 0;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr);
}
.v2-monitor-page.is-map-mode .v2-fleet-map { min-height: 0; height: 100%; }
}
.v2-track-query-panel .v2-track-query-header.v2-workspace-command-bar,
.v2-track-query-panel .v2-track-query-header.has-identity.v2-workspace-command-bar {
display: flex;
flex-wrap: wrap;
gap: 8px 10px;
}
.v2-track-query-header .v2-workspace-command-copy,
.v2-track-query-header.has-identity .v2-workspace-command-copy {
flex: 1 1 130px;
min-width: 90px;
}
.v2-track-query-header .v2-workspace-command-actions,
.v2-track-query-header.has-identity .v2-workspace-command-actions {
flex: 0 0 auto;
margin-left: auto;
}
.v2-track-query-header .v2-workspace-command-stack { min-width: 90px; }
.v2-track-query-header .v2-workspace-command-title,
.v2-track-query-header.has-identity .v2-workspace-command-title,
.v2-track-query-header .v2-workspace-command-description,
.v2-track-query-header.has-identity .v2-workspace-command-description {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
@@ -0,0 +1,317 @@
/* Shared desktop workspace: restrained chrome, readable content, native controls.
* Keep page-specific layouts in apple-*-pages.css. Overlays also live outside
* the shell, so their selectors deliberately use the workspace component class.
*/
body {
--semi-color-primary: var(--v2-color-primary);
--semi-color-primary-hover: var(--v2-color-primary-hover);
--semi-color-primary-active: #005bb5;
--semi-color-primary-light-default: var(--v2-color-primary-soft);
--semi-color-primary-light-hover: #deebfa;
--semi-color-primary-light-active: #d3e4f8;
--semi-color-border: var(--v2-color-border);
--semi-color-bg-0: var(--v2-color-canvas);
--semi-color-text-0: var(--v2-color-text);
--semi-color-text-1: #3a3a3c;
--semi-color-text-2: var(--v2-color-text-secondary);
--semi-color-text-3: var(--v2-color-text-tertiary);
--v2-control-border: var(--v2-color-border-strong);
--v2-control-bg-muted: #f5f5f7;
--v2-control-text-muted: var(--v2-color-text-secondary);
--v2-control-height-mobile: 44px;
--v2-control-height-touch: 44px;
}
.v2-shell .v2-sidebar {
width: var(--v2-sidebar-width);
background: #f0f0f2;
border-right: 1px solid rgba(0,0,0,.08);
}
.v2-shell .v2-sidebar.is-collapsed { width: var(--v2-sidebar-width-compact); }
.v2-shell .v2-main { margin-left: var(--v2-sidebar-width); }
.v2-shell .v2-sidebar.is-collapsed + .v2-main { margin-left: var(--v2-sidebar-width-compact); }
.v2-shell .v2-brand { height: 72px; padding-inline: 24px; border-bottom: 0; }
.v2-shell .v2-brand-logo { width: 146px; }
.v2-shell .v2-navigation.semi-navigation { background: transparent; padding: 12px; }
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item {
min-height: 42px;
border-radius: 8px;
color: #49494d;
box-shadow: none;
background: transparent;
}
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item-text {
font-size: 14px;
font-weight: 500;
}
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item-icon {
background: transparent;
color: #636366;
box-shadow: none;
border-radius: 0;
}
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item:hover { background: #e7e7ea; }
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item-selected {
background: #dfe5ed;
color: #1d1d1f;
box-shadow: none;
}
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item-selected .semi-navigation-item-icon {
background: transparent;
color: var(--v2-color-primary);
box-shadow: none;
}
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item.v2-nav-group-start::before {
color: #76767b;
font-size: 12px;
font-weight: 500;
letter-spacing: 0;
}
.v2-shell .v2-collapse.semi-button { border-top-color: rgba(0,0,0,.06); color: #636366; }
.v2-shell .v2-topbar {
height: 72px;
background: rgba(250,250,252,.94);
border-bottom: 1px solid rgba(0,0,0,.08);
padding-inline: 24px;
backdrop-filter: blur(20px);
}
.v2-shell .v2-topbar-title h4.semi-typography {
color: var(--v2-color-text);
font-size: 22px;
font-weight: 650;
letter-spacing: -.025em;
}
.v2-shell .v2-topbar-title .v2-topbar-description.semi-typography { color: #76767b; font-size: 13px; }
.v2-shell .v2-topbar-actions { gap: 8px; }
.v2-shell .v2-topbar-actions > .v2-help-trigger.semi-button {
min-width: 40px;
border: 0;
background: transparent;
color: #636366;
box-shadow: none;
}
.v2-shell .v2-current-user .semi-tag { display: none; }
.v2-shell .v2-current-user b { font-weight: 500; }
.v2-shell .v2-current-user .semi-avatar { background: #e3e3e8; color: #48484a; }
.v2-shell .v2-topbar-actions .semi-button:hover { background: #ededf0; }
/* Common toolbars contain a single title and an optional supporting sentence. */
.v2-shell .v2-content .v2-workspace-command-bar {
min-height: 66px;
margin-inline: 0;
border: 0;
border-bottom: 1px solid var(--v2-color-border);
border-radius: 0;
background: var(--v2-color-surface);
padding: 14px 20px;
box-shadow: none;
gap: 16px;
}
.v2-shell .v2-content .v2-workspace-command-bar::before,
.v2-shell .v2-content .v2-workspace-panel-header::before { display: none; }
.v2-shell .v2-content .v2-workspace-command-copy { gap: 12px; }
.v2-shell .v2-content .v2-workspace-command-icon {
width: 30px;
height: 30px;
flex-basis: 30px;
border: 0;
background: transparent;
color: #636366;
box-shadow: none;
}
.v2-shell .v2-content .v2-workspace-command-eyebrow { display: none; }
.v2-shell .v2-content .v2-workspace-command-title.semi-typography {
color: var(--v2-color-text);
font-size: 17px;
font-weight: 600;
line-height: 1.4;
letter-spacing: -.015em;
}
.v2-shell .v2-content .v2-workspace-command-description.semi-typography {
margin-top: 3px;
color: var(--v2-color-text-secondary);
font-size: 12px;
line-height: 1.5;
}
.v2-shell .v2-content .v2-workspace-command-meta { color: #76767b; font-size: 12px; }
.v2-shell .v2-content .v2-workspace-command-actions { gap: 10px; }
.v2-shell .v2-content .v2-workspace-panel-header {
min-height: 60px;
padding: 14px 20px;
border-left: 0;
border-bottom: 1px solid var(--v2-color-border);
background: #fff;
box-shadow: none;
gap: 16px;
}
.v2-shell .v2-content .v2-workspace-panel-header .v2-workspace-panel-title.semi-typography {
font-size: 16px;
font-weight: 600;
color: var(--v2-color-text);
}
.v2-shell .v2-content .v2-workspace-panel-header .v2-workspace-panel-eyebrow { display: none; }
.v2-shell .v2-content .v2-workspace-panel-header .v2-workspace-panel-description.semi-typography {
color: var(--v2-color-text-secondary);
font-size: 12px;
line-height: 1.5;
}
/* Controls share geometry without changing their native disabled/pressed state. */
.v2-shell .semi-button { font-size: 13px; font-weight: 500; border-radius: 8px; }
.v2-shell .semi-input-wrapper,
.v2-shell .semi-select { border-radius: 8px; background: #fff; }
.v2-shell .semi-input,
.v2-shell .semi-select-selection { font-size: 14px; color: var(--v2-color-text); }
.v2-shell .semi-input::placeholder { color: #76767b; }
.v2-shell .semi-tag { font-size: 12px; font-weight: 500; letter-spacing: 0; }
.v2-shell .v2-content .semi-table-thead > tr > th {
background: #f8f8fa;
color: #636366;
font-size: 12px;
font-weight: 500;
border-bottom-color: var(--v2-color-border);
}
.v2-shell .v2-content .semi-table-tbody > tr > td {
font-size: 14px;
border-bottom-color: #efeff1;
}
.v2-shell .v2-content .semi-table-tbody > tr:hover > td { background: #f5f7fa; }
.v2-shell .v2-content .v2-segmented-tabs.is-filled {
gap: 2px;
border: 0;
border-radius: 9px;
background: #eeeef0;
padding: 3px;
box-shadow: none;
}
.v2-shell .v2-content .v2-segmented-tabs > .semi-button {
min-height: 34px;
font-size: 13px;
font-weight: 500;
color: #636366;
}
.v2-shell .v2-content .v2-segmented-tabs.is-filled > .semi-button.is-active {
background: #fff;
color: #1d1d1f;
box-shadow: 0 1px 3px rgba(0,0,0,.13);
border-radius: 7px;
}
.v2-shell .v2-content .v2-segmented-tab-count {
background: rgba(0,0,0,.05);
color: inherit;
font-size: 12px;
font-weight: 500;
}
.v2-shell .v2-content .v2-workspace-metric-list > span {
border-color: var(--v2-color-border);
background: #fff;
box-shadow: none;
}
.v2-shell .v2-content .v2-workspace-metric-list > span > small,
.v2-shell .v2-content .v2-workspace-metric-action > small { font-size: 12px; color: #636366; font-weight: 500; }
.v2-shell .v2-content .v2-workspace-metric-list > span > strong,
.v2-shell .v2-content .v2-workspace-metric-action > strong { font-size: 24px; font-weight: 600; letter-spacing: -.025em; font-variant-numeric: tabular-nums; }
.v2-shell .v2-content .v2-workspace-metric-list > span > em,
.v2-shell .v2-content .v2-workspace-metric-action > em { color: #76767b; font-size: 12px; line-height: 1.5; }
.v2-shell .v2-content .v2-workspace-metric-list > span.is-active { background: #f0f5fc; }
/* Sheets and dialogs are the only elevated content surfaces. */
body [data-workspace-sheet-id] .semi-sidesheet-inner {
background: #fff;
box-shadow: -12px 0 48px rgba(0,0,0,.12);
}
body [data-workspace-sheet-id] .semi-sidesheet-header {
border-bottom: 1px solid var(--v2-color-border);
background: #fafafa;
padding: 20px 24px;
}
body [data-workspace-sheet-id] .v2-workspace-config-title { gap: 12px; }
body [data-workspace-sheet-id] .v2-workspace-config-title-icon {
border: 0;
background: #eeeef0;
color: #636366;
box-shadow: none;
}
body [data-workspace-sheet-id] .v2-workspace-config-title strong { color: #1d1d1f; font-size: 20px; font-weight: 600; }
body [data-workspace-sheet-id] .v2-workspace-config-title small { color: #636366; font-size: 13px; line-height: 1.5; }
body [data-workspace-sheet-id] .v2-workspace-config-body { background: #fff; }
body [data-workspace-sheet-id] .v2-workspace-config-content { font-size: 14px; }
body [data-workspace-sheet-id] .v2-workspace-config-footer { background: #fafafa; gap: 16px; padding: 16px 24px; }
body [data-workspace-sheet-id] .v2-workspace-config-footer > span { color: #636366; font-size: 12px; }
body [data-workspace-sheet-id] .semi-button { min-height: 36px; font-size: 13px; border-radius: 8px; }
body [data-workspace-sheet-id] .semi-sidesheet-close { min-width: 36px; min-height: 36px; border-radius: 50%; }
body .v2-help-panel li { background: #f5f5f7; color: #3a3a3c; font-size: 14px; padding: 16px; }
body .v2-help-panel > footer { font-size: 12px; }
body .v2-help-overview.semi-card { border: 0; background: transparent; box-shadow: none; }
body .v2-help-overview .semi-typography { color: #636366; font-size: 14px; line-height: 1.6; }
body .v2-account-dropdown { border-color: #e5e5e7; border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,.16); }
body .v2-account-dropdown-profile { background: #f5f5f7; }
body .v2-account-dropdown-profile strong { font-size: 14px; }
body .v2-account-dropdown-profile small,
body .v2-account-dropdown-profile em { font-size: 12px; color: #636366; }
:where(.v2-shell, [data-workspace-sheet-id]) :is(button, a, input, [role="button"], [role="tab"]):focus-visible {
outline: 2px solid var(--v2-color-primary);
outline-offset: 2px;
}
@media (max-width: 1100px) and (min-width: 681px) {
.v2-shell .v2-main { margin-left: var(--v2-sidebar-width-compact); }
.v2-shell .v2-topbar-description { display: none; }
}
@media (max-width: 680px) {
.v2-shell .v2-main { margin-left: 0; }
.v2-shell .v2-topbar { height: 60px; padding-inline: 16px; }
.v2-shell .v2-topbar-title h4.semi-typography { font-size: 20px; }
.v2-shell .v2-topbar-description { display: none; }
.v2-shell .v2-topbar-actions { gap: 0; }
.v2-shell .v2-help-trigger-label { display: none; }
.v2-shell .v2-topbar-actions > .semi-button { min-height: 44px; }
.v2-shell .v2-content .v2-workspace-command-bar,
.v2-shell .v2-content .v2-workspace-panel-header { padding: 12px 16px; gap: 10px; }
.v2-shell .v2-content .v2-workspace-command-title.semi-typography { font-size: 16px; }
.v2-shell .v2-content .v2-workspace-command-actions { flex-wrap: wrap; }
.v2-shell .v2-content .v2-workspace-command-description.semi-typography { font-size: 12px; }
.v2-shell .v2-content .v2-workspace-metric-list > span > strong,
.v2-shell .v2-content .v2-workspace-metric-action > strong { font-size: 21px; }
.v2-shell .v2-content .semi-button:not(.semi-button-size-small),
.v2-shell .v2-content .semi-input-wrapper,
.v2-shell .v2-content .semi-select,
body [data-workspace-sheet-id] .semi-button { min-height: 44px; }
.v2-shell .v2-content .v2-segmented-tabs > .semi-button { min-height: 40px; }
.v2-shell .v2-mobile-navigation { background: rgba(250,250,252,.96); backdrop-filter: blur(20px); border-color: #e5e5e7; }
body [data-workspace-sheet-id] .semi-sidesheet-header { padding: 18px 16px; }
body [data-workspace-sheet-id] .v2-workspace-config-footer { padding: 12px 16px; }
}
@media (prefers-reduced-motion: reduce) {
.v2-shell *, [data-workspace-sheet-id] * { transition-duration: 0s !important; animation-duration: 0s !important; }
}
.v2-shell .v2-navigation.semi-navigation .semi-navigation-item-selected::after { display: none; }
.v2-shell .v2-topbar-title > .v2-topbar-heading > .v2-topbar-page-title.semi-typography { font-size: 22px; font-weight: 650; }
body .v2-workspace-help-sidesheet .v2-help-overview.semi-card { min-height: 0; }
body .v2-workspace-help-sidesheet .v2-help-overview > .semi-card-body { padding: 8px 2px 20px; }
/* Long titles must leave room for the close control at narrow widths. */
body [data-workspace-sheet-id] .semi-sidesheet-header { display: flex; align-items: flex-start; gap: 12px; }
body [data-workspace-sheet-id] .semi-sidesheet-title { flex: 1; min-width: 0; }
body [data-workspace-sheet-id] .v2-workspace-config-title { min-width: 0; }
body [data-workspace-sheet-id] .v2-workspace-config-title > span { min-width: 0; flex: 1; }
body [data-workspace-sheet-id] .v2-workspace-config-title small { white-space: normal; overflow-wrap: anywhere; }
body [data-workspace-sheet-id] .semi-sidesheet-close { flex: 0 0 36px; margin: 0; }
body [data-workspace-sheet-id] .v2-workspace-config-summary > span { background: #fafafa; box-shadow: none; border-color: #e5e5e7; }
body [data-workspace-sheet-id] .v2-workspace-config-summary small,
body [data-workspace-sheet-id] .v2-workspace-config-summary em { font-size: 12px; color: #636366; line-height: 1.5; }
body [data-workspace-sheet-id] .v2-workspace-config-summary strong { font-size: 16px; font-weight: 600; }
@media (max-width: 680px) {
body [data-workspace-sheet-id] .v2-workspace-config-title-icon { display: none; }
body [data-workspace-sheet-id] .semi-sidesheet-close { flex-basis: 44px; min-width: 44px; }
.v2-shell .v2-topbar-title > .v2-topbar-heading > .v2-topbar-page-title.semi-typography { font-size: 20px; }
}
body .v2-mobile-more-section > header strong { font-size: 14px; }
body .v2-mobile-more-section > header span,
body .v2-mobile-more-section nav a > span { font-size: 12px; color: #636366; }
body .v2-mobile-more-section nav a strong { font-size: 14px; }
body .v2-mobile-more-section nav a :is(small, em) { font-size: 12px; color: #636366; }
.v2-shell .semi-input:focus-visible,
body [data-workspace-sheet-id] .semi-input:focus-visible { outline: none; box-shadow: none; }
+17 -17
View File
@@ -10,24 +10,24 @@
color-scheme: light;
/* Color */
--v2-color-canvas: #f6f8fb;
--v2-color-canvas: #f5f5f7;
--v2-color-surface: #ffffff;
--v2-color-surface-subtle: #fafbfd;
--v2-color-text: #172033;
--v2-color-text-secondary: #66758a;
--v2-color-text-tertiary: #8a96a7;
--v2-color-border: #dfe5ed;
--v2-color-border-strong: #cfd7e2;
--v2-color-primary: #1467e8;
--v2-color-primary-hover: #0f5dcc;
--v2-color-primary-soft: #edf4ff;
--v2-color-success: #15965f;
--v2-color-danger: #dc3f3f;
--v2-color-warning: #d98900;
--v2-color-surface-subtle: #fafafa;
--v2-color-text: #1d1d1f;
--v2-color-text-secondary: #636366;
--v2-color-text-tertiary: #76767b;
--v2-color-border: #e5e5e7;
--v2-color-border-strong: #c7c7cc;
--v2-color-primary: #0071e3;
--v2-color-primary-hover: #0064cc;
--v2-color-primary-soft: #eaf3ff;
--v2-color-success: #248a3d;
--v2-color-danger: #d70015;
--v2-color-warning: #a15c00;
/* Typography */
--v2-font-sans: Inter, "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
--v2-type-caption: 11px;
--v2-font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
--v2-type-caption: 12px;
--v2-type-meta: 12px;
--v2-type-control: 14px;
--v2-type-body: 14px;
@@ -46,14 +46,14 @@
--v2-space-7: 28px;
--v2-space-8: 32px;
--v2-radius-control: 8px;
--v2-radius-surface: 10px;
--v2-radius-surface: 12px;
--v2-control-height: 40px;
--v2-control-height-large: 44px;
--v2-touch-target: 44px;
--v2-sidebar-width: 228px;
--v2-sidebar-width-compact: 72px;
--v2-page-gutter: clamp(18px, 1.7vw, 28px);
--v2-page-gap: 12px;
--v2-page-gap: 16px;
/* Motion */
--v2-motion-fast: 140ms;
@@ -5,29 +5,10 @@
* keeps one visual hierarchy: identity, menu access, then vehicle scope.
*/
.v2-user-admin,
.v2-user-editor-sidesheet {
--v2-color-canvas: #f6f8fb;
--v2-color-surface-subtle: #fafbfd;
--v2-color-text: #172033;
--v2-color-text-secondary: #66758a;
--v2-color-text-tertiary: #8a96a7;
--v2-color-border: #dfe5ed;
--v2-color-primary: #1467e8;
--v2-color-warning: #d98900;
--v2-radius-surface: 10px;
--v2-motion-fast: 140ms;
}
/* Batch account import keeps upload, server preflight, repair and retry in one wide task surface. */
.v2-user-import-sidesheet {
--v2-color-text: #172033;
--v2-color-text-secondary: #66758a;
--v2-color-border: #dfe5ed;
}
/* Colours, typography and motion inherit the shared foundation. */
.v2-user-import-sidesheet .v2-workspace-config-content {
background: #f6f8fb;
background: var(--v2-color-canvas);
}
.v2-user-import-workspace {
@@ -70,7 +51,7 @@
.v2-user-import-row-result small,
.v2-user-import-upload small {
color: var(--v2-color-text-secondary);
font-size: 11px;
font-size: 12px;
}
.v2-user-import-upload {
@@ -106,14 +87,14 @@
.v2-user-import-format span {
color: var(--v2-color-text-secondary);
font-size: 10px;
font-size: 12px;
font-weight: 650;
}
.v2-user-import-format code {
overflow-x: auto;
color: #36506f;
font-size: 10px;
font-size: 12px;
white-space: nowrap;
}
@@ -123,7 +104,7 @@
background: #fff4f3;
padding: 9px 14px;
color: #b42318;
font-size: 11px;
font-size: 12px;
}
.v2-user-import-list {
@@ -161,7 +142,7 @@
border-radius: 6px;
background: #eef3f9;
color: #5b6b80;
font-size: 10px;
font-size: 12px;
font-weight: 700;
}
@@ -211,7 +192,7 @@
.v2-user-import-repair-fields label > span {
color: var(--v2-color-text-secondary);
font-size: 11px;
font-size: 12px;
font-weight: 650;
}
@@ -370,7 +351,7 @@
.v2-user-directory-metric-rail .v2-workspace-metric-list > span > small {
color: var(--v2-color-text-secondary);
font-size: 11px;
font-size: 12px;
}
.v2-user-directory-metric-rail .v2-workspace-metric-list > span > strong {
@@ -381,7 +362,7 @@
.v2-user-directory-metric-rail .v2-workspace-metric-list > span > em {
color: var(--v2-color-text-tertiary);
font-size: 10px;
font-size: 12px;
}
.v2-user-directory-body {
@@ -418,7 +399,7 @@
.v2-user-route-notice small {
color: var(--v2-color-text-secondary);
font-size: 11px;
font-size: 12px;
}
.v2-user-directory-columns {
@@ -429,7 +410,7 @@
background: var(--v2-color-surface-subtle);
padding: 0 16px;
color: var(--v2-color-text-tertiary);
font-size: 11px;
font-size: 12px;
font-weight: 600;
}
@@ -520,7 +501,7 @@
.v2-user-provider-tag.semi-tag {
min-height: 20px;
border-radius: 5px;
font-size: 10px;
font-size: 12px;
}
.v2-user-list-secondary {
@@ -533,7 +514,7 @@
.v2-user-list-secondary > small {
overflow: hidden;
color: var(--v2-color-text-tertiary);
font-size: 11px;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -555,7 +536,7 @@
.v2-user-list-facts > b > i {
color: var(--v2-color-text-tertiary);
font-size: 11px;
font-size: 12px;
font-style: normal;
font-weight: 500;
}
@@ -580,7 +561,7 @@
min-width: 64px;
min-height: 24px;
border-radius: 999px;
font-size: 10px;
font-size: 12px;
font-weight: 650;
}
@@ -652,7 +633,7 @@
.v2-user-editor-switcher small {
overflow: hidden;
color: var(--v2-color-text-tertiary);
font-size: 10px;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -687,7 +668,7 @@
.v2-user-change-preview small {
color: #9b7249;
font-size: 10px;
font-size: 12px;
}
.v2-user-change-preview > div {
@@ -730,7 +711,7 @@
border: 1px solid #d9e2ee;
border-radius: 50%;
color: var(--v2-color-text-tertiary);
font-size: 10px;
font-size: 12px;
font-style: normal;
font-weight: 700;
}
@@ -815,7 +796,7 @@
.v2-user-identity-source > p,
.v2-user-identity-source > div small {
color: var(--v2-color-text-secondary);
font-size: 10px;
font-size: 12px;
line-height: 1.55;
}
@@ -843,7 +824,7 @@
.v2-user-identity-source > div strong {
overflow: hidden;
color: #30445d;
font-size: 11px;
font-size: 12px;
font-weight: 680;
text-overflow: ellipsis;
white-space: nowrap;
@@ -929,7 +910,7 @@
.v2-user-vehicle-composer-heading small {
color: var(--v2-color-text-tertiary);
font-size: 10px;
font-size: 12px;
}
.v2-user-vehicle-composer .v2-vehicle-permission-tools {
@@ -1057,7 +1038,7 @@
}
.v2-user-directory-glance {
font-size: 11px;
font-size: 12px;
}
.v2-user-directory-glance b {
@@ -1086,7 +1067,7 @@
}
.v2-user-directory-metric-rail .v2-workspace-metric-list > span > em {
font-size: 9px;
font-size: 12px;
}
.v2-user-directory-body,
@@ -1170,7 +1151,7 @@
overflow: hidden;
max-width: 47%;
flex: 0 1 auto;
font-size: 10px;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -1186,7 +1167,7 @@
flex: 0 0 auto;
align-items: center;
color: var(--v2-color-text-secondary);
font-size: 10px;
font-size: 12px;
font-weight: 520;
white-space: nowrap;
}
@@ -1204,7 +1185,7 @@
.v2-user-status-tag.semi-tag {
min-width: 40px;
min-height: 22px;
font-size: 9px;
font-size: 12px;
}
.v2-user-config-label {
@@ -1246,11 +1227,11 @@
.v2-user-editor-sidesheet .v2-workspace-config-summary > span > small,
.v2-user-editor-sidesheet .v2-workspace-config-summary > span > em {
font-size: 9px;
font-size: 12px;
}
.v2-user-editor-sidesheet .v2-workspace-config-summary > span > strong {
font-size: 11px;
font-size: 12px;
}
.v2-user-change-preview {
@@ -1269,7 +1250,7 @@
}
.v2-user-change-preview .semi-tag {
font-size: 9px;
font-size: 12px;
}
.v2-user-editor-switcher {
@@ -1294,7 +1275,7 @@
.v2-user-editor-sidesheet .v2-user-editor-tabs > .semi-tabs-bar .semi-tabs-tab {
min-height: 49px;
padding-inline: 8px;
font-size: 11px;
font-size: 12px;
}
.v2-user-tab-label {
@@ -0,0 +1,37 @@
# Apple Design 发布验收
发布版本:`apple-design-202609171849`,时间:2026-09-17 18:4918:54Asia/Shanghai)。
## 范围
现有九个业务入口、共享导航和帮助、桌面/手机查询与详情界面。采用系统字体、中性表面、清晰层级、克制蓝色动作与状态色;精简重复说明,优化密集表格和手机操作区。三个子代理分别负责数据页、治理页、地图/轨迹页,主代理统一设计和逐页验收。
UI源码提交:`39d9cb54`,分支 `codex/apple-design-20260917`,已推送origin。最终发布Tag`apple-design-20260917-1849`
## 前置回退保障
改造前完整代码快照提交 `f7e7176f88b4fd49338dda8e72d701a24617fb2f` 已推送;回退Tag `pre-apple-design-20260917-1805` 已核对远端存在。线上应用与配置归档位于 `/opt/lingniu-vehicle-platform/backups/pre-apple-design-202609171805/`,发布前 SHA256SUMS 两项再次校验成功。详情见 [回退基线](rollback-baseline.md)。
## 自动化与构建
- 完整Web测试:79个文件542项通过。
- 后续改动:AppShell、Alerts、Track、Operations共88项通过;最后事件表格滚动修复后Alerts49项通过。
- 最终 `npm run build`TypeScript、Vite、prepare-dist、verify-build)通过,84个发布资源验证通过。
- `git diff --check`通过。
- Web归档SHA-256`af53ef1f1364a756c2300d1bafa3ac726aea8cd43ff1f7c1f915704532be91c9`
## 生产发布与核验
- 使用现有原子发布脚本,仅传Web归档;当前目录为 `/opt/lingniu-vehicle-platform/releases/apple-design-202609171849`。脚本具备失败自动切回旧版本机制。
- 安装门禁通过:84个当前资源、132个兼容资源;公开HTTPS入口复验通过;保留3代兼容资源,未裁剪历史release。
- API、流式告警、规则告警三个服务均active。API、alert-stream-evaluator、alert-evaluator文件哈希与前版相同。前版没有独立alert-notification-dispatcher文件,因此发布后附加校验曾在检查此可选文件时报FileNotFoundError;安装已经成功,此错误不代表安装回滚或服务失败。
- 正式HTTPS健康接口返回新版本;逐项检查当时51条原生告警的列表标题和详情一致,均有具体名称(48条绝缘,1条SOC跳变,1条SOC低,1条仅等级且明确标注未上报具体项)。
- 线上浏览器逐页加载九个入口并检查实际画面:事件具体告警详情、车辆目录、账号目录、接入来源、历史查询、里程矩阵、轨迹查询、运维队列、监控地图。证据为 `outputs/apple-design-20260917/after/44-production-native-detail.png``52-production-monitor.png`
- 生产车辆分页从1/103切换至2/103;历史导出任务中心打开并显示已有66个任务;里程矩阵可区分无数据与数值;监控显示实际14个省级区域880辆有定位;生产浏览器本次采集的error日志为空。
- 未改动业务数据、告警定义、来源优先级或客户权限。未触发通知、归档或生产批量处置。地图逐帧播放、查询、抽屉和手机菜单交互在本地mock另行验证,详细边界见 [UI审查](ui-review.md)。
## 回滚
前版 `native-alarm-details-202609171739` 及独立备份保留。将current原子指回该release并恢复对应PLATFORM_RELEASE后重启平台API即可恢复此前前端;本轮没有数据库迁移。需要源代码时从 `pre-apple-design-20260917-1805` 创建修复分支。
构建、测试、部署、原生告警核验日志存于本地 `outputs/apple-design-20260917/`。包含车辆信息的截图与配置备份不推送到Git。
@@ -0,0 +1,70 @@
# 车辆数据中台 Apple Design UI/UX 审查
日期:2026-09-17。范围:现有 9 个业务入口、共享导航、详情/查询抽屉及手机布局。
## 审查方法与证据边界
本轮从线上 before 截图和当前代码确认问题,再在本地改造后使用 UI mock 场景检查渲染及交互。方向是系统字体、清晰文字层级、中性连续内容表面、少量蓝色主操作、轻分隔、足够触控面积;不是给所有区域添加玻璃效果。
下表引用的 before/after 截图均已由本报告作者实际打开检查。其他截图由主代理进行操作及视觉检查,另列为补充证据,不将仅存在的文件视为本报告作者已验证。截图是拍摄时刻的中间状态,不能自动证明之后的修复正确。
本地 mock 仅验证 UI 渲染和操作路径,不能证明生产车辆数量、在线状态、原生告警等级/故障码、统计结果或服务健康准确。生产数据语义与业务接口应保留;此次设计调整不重新定义“离线”“无来源”“无数据”和数值 0,不删查询/导出、权限、事件证据与数据源策略功能。原生 GB/T 32960 告警应继续显示具体名称,并保留来源与原始字段详情。
## 九页审查矩阵
证据链接相对于仓库根目录中的 `outputs/apple-design-20260917/`
| 页面 | 实际观察到的问题 | 本轮改变 | 已检查证据与限制 |
|---|---|---|---|
| 全局监控 `/monitor` | 桌面指标区、列表、地图叠加多种蓝底及细小文字;地图覆盖率和真实位置状态需要保持可辨认。 | 系统感地图工作区与中性控件;保留地图/列表切换、筛选、跟随、聚合标记和覆盖说明。 | [before](../../../../outputs/apple-design-20260917/before/08-monitor.png)、[手机 after](../../../../outputs/apple-design-20260917/after/22-monitor-390.png)。手机地图已可见;底部图例紧邻固定导航,仍需最终安全区复核。 |
| 车辆查询 `/vehicles` | 搜索卡及列表指标存在重复装饰;旧表格文字偏小。首轮 after 中“查看档案”断行、在线行绿条、搜索卡底角仍方;手机“修改”与批量同步相撞。 | 14px 车辆行、12px VIN;搜索与目录中性分组;目录标题置左、指标置右;单行查看操作;去行侧色条;范围独占手机一行、批量同步单独下一行且至少 44px 高。单车页减少首栏高度,导航改纯工具栏,操作 13px、辅助至少 12px、事件标题 14px。 | [before](../../../../outputs/apple-design-20260917/before/02-vehicles.png)、[目录 after](../../../../outputs/apple-design-20260917/after/01-vehicles-1440.png)、[单车 after](../../../../outputs/apple-design-20260917/after/02-vehicle-detail-1440.png)、[手机 after](../../../../outputs/apple-design-20260917/after/10-vehicles-390.png)。上述 after 揭示的问题已修代码,需最新重截图确认。 |
| 轨迹回放 `/tracks` | 未查询时左右各一套空状态,并提前显示无数据指标/标签;地图中央步骤卡重复说明查询表单。 | 单一主要空状态与查询按钮;查询后才展开结果指标;浅中性地图画布与明确播放操作,保留时间、来源及回放。 | [before](../../../../outputs/apple-design-20260917/before/09-tracks.png)、[桌面 after](../../../../outputs/apple-design-20260917/after/06-tracks-1440.png)。首轮 after 日期输入和描述有截断,需检查后续修复及播放结果。 |
| 历史数据 `/history` | “历史证据/遥测证据检索/证据结果/历史明细”层级冗余,空状态又放三步卡;表格和工具文字密集。 | 收敛为查询条件→历史明细;空状态保留描述与 CTA,去重复步骤;13px 表格和更清晰表头,保留多车、协议、原始报文、字段设置、趋势、导出任务。 | [before](../../../../outputs/apple-design-20260917/before/04-history.png)、[有结果 after](../../../../outputs/apple-design-20260917/after/23-history-result-1440.png)。结果表 VIN、时间、协议和数值可读;查询回执与工具辅助文字仍需最终密度复核。 |
| 里程查询 `/statistics` | 重复“每日里程”眉题;表格小字且核心数据与辅助说明层级弱;底部暴露缓存实现文案。 | 保留动态日期列/热度与区间合计;13px 数值、tabular-nums、清晰主标题;底部改为上海时区统计说明;不改变缺失与 0 的差别、来源优先级和导出。 | [before](../../../../outputs/apple-design-20260917/before/03-statistics.png)、[桌面 after](../../../../outputs/apple-design-20260917/after/24-statistics-1440.png)。当前 3 日矩阵清晰;长日期及氢耗证据要继续核验。 |
| 事件中心 `/alerts` | 表格图标底色与侧条重复强调;多标签和筛选并列占据注意力。 | 中性列表与克制状态色;保留事件流/自动化/通知导航、具体事件标题和处理状态。 | [before](../../../../outputs/apple-design-20260917/before/01-events.png)、[1024 after](../../../../outputs/apple-design-20260917/after/27-alerts-1024.png)。右侧执行状态超出当前截图宽度,需验证可横向访问且不丢列。mock 事件名称不构成原生告警线上验证。 |
| 接入管理 `/access` | “来源治理/接入健康工作台/接入差异”重复眉题;每个协议状态单独卡片造成边框密集。 | 精简眉题和协议容器装饰,保留在线/离线/无来源文字;适配窄桌面筛选换行及抽屉。 | [before](../../../../outputs/apple-design-20260917/before/05-access.png)、[1024 after](../../../../outputs/apple-design-20260917/after/25-access-1024.png)。协议状态可区分;需复核更多车辆、治理与认领操作。 |
| 账号管理 `/users` | 旧桌面列表同时并排两账号,顶部列名与两组值难对应;授权信息阅读路径不连续。 | 桌面统一一行一账号,账户/菜单/车辆/最近登录/状态对齐;中性头像,创建账号作为主操作;保留配置和权限确认。 | [before](../../../../outputs/apple-design-20260917/before/06-users.png)、[桌面 after](../../../../outputs/apple-design-20260917/after/04-users-1440.png)。可辨认正常、未关联、停用三类;数据为 mock,不能据此验证真实授权。 |
| 运维质量 `/operations` | 眉题、彩色头栏、对账标签重复;队列操作多,窄桌面容易挤压说明和表格。 | 精简运行快照/自动对账眉题;中性分组,保留差异处置/单车诊断/全局健康;批量、导出、趋势及筛选继续可用。 | [before](../../../../outputs/apple-design-20260917/before/07-operations.png)、[1024 after](../../../../outputs/apple-design-20260917/after/26-operations-1024.png)。截图中队列说明与按钮仍拥挤、右列需要横向查看,后续修改需重验。 |
## 共享实现与复杂层叠
- `foundation.css` 收敛颜色、系统字体和基础几何 token;共享顶栏、导航、输入和按钮由 `apple-workspace.css` 管理。
- 页面细节分别置于 `apple-data-pages.css``apple-governance.css``apple-map-pages.css`。数据页规则有局部作用域,不覆盖共享 topbar token。
- 帮助抽屉移除“页面目标/建议步骤/Esc”装饰指标,保留当前模块说明和操作建议。
- 既有 CSS 约 55,150 行,混有高 specificity、inline Card padding、媒体查询和懒加载用户样式;仅调整 token 不能证明视觉生效。首轮截图中的车辆左色条就是高 specificity 旧规则覆盖的实例,已用局部精确选择器处理。
- 搜索卡圆角通过内部透明背景处理,避免用 overflow:hidden 裁掉候选搜索浮层。
主代理补充检查证据:帮助 `after/03-help-1440.png`、账号详情 `05-user-detail-1440.png`、轨迹结果 `07-track-result-1440.png`、手机查询/地图 `08-tracks-390.png``09-track-map-390.png`、车辆搜索抽屉 `11-vehicle-search-390.png`、更多导航 `13-more-390.png`、事件/自动化手机 `15-alerts-390.png``16-automation-390.png`、接入详情 `18-access-detail-390.png`、健康页 `21-health-390.png`。本报告作者未独立逐一打开这些补充图,不将其归为个人完成验证。
## 已完成验证
- 数据页业务测试:VehiclePage 15、HistoryPage 52、StatisticsPage 29,共 96 项通过;单车导航改变后 VehiclePage 15 项重跑通过。
- `productionEntry.test.ts` 10 项独立通过。仅将删除的眉题/装饰色/重复空状态断言改为共享组件、主标题及统一空态标题,保留启动、构建、ARIA、路由与业务契约断言。
- TypeScript 编译检查在本报告提交前重新执行并通过;`git diff --check` 通过。当前 tsconfig 未启用 `noUnusedLocals`,因此编译通过不等于所有历史未使用 import 已清理;本次未擅自修改其他代理文件。
- 当前 Node 测试环境使用 `NODE_OPTIONS=--no-experimental-webstorage`,避免运行时 experimental Web Storage 覆盖 jsdom 存储实现;业务存储逻辑未因此更改。
- 上述局部测试结果不是最终全套测试或生产验收的替代。
## 首轮审查提出的验证清单(结果见下)
1. 最新 CSS/JSX 改动后重拍车辆目录、单车首屏、390px 范围栏;核对 nowrap、圆角、100px 首栏及44px手机动作实际生效。
2. 重验轨迹日期完整显示、1024px 运维头部和右侧表列、手机地图图例安全区;长文本和权限不同的账户需检查。
3. 九页最终桌面/手机冒烟:键盘焦点、返回上下文、分页、筛选提交/重置、抽屉开关、导出任务入口、地图播放与跟随。不能仅凭静态图宣称交互通过。
4. 最终完整测试、TypeScript、生产 build 与产物检查;只以实际完成日志作为最终证据。
5. 生产更新后验证资源加载和九页关键路径,并抽查真实原生 32960 告警名称/详情及既有数据来源语义。发布前后数值不得拿 mock 样例比对。
6. 备份、提交、push、回退 Tag 和部署版本由主代理维护;本报告未独立检查远端备份、Git remote 或部署状态,不将其列为已完成证据。
## 主代理最终本地验收(18:48
本节由主代理按实际浏览器结果补充,取代首轮中间截图的待修状态:
- 车辆目录与单车详情:`after/34-vehicles-final-1440.png``35-vehicle-detail-final-1440.png``41-vehicles-final-390.png` 已检查;单行操作、圆角、手机范围与批量同步不重叠。
- 轨迹:`40-tracks-final-1024.png` 查询标题、开始/结束日期完整;`31-track-final-390.png` 里程值完整;查询和键盘逐帧播放已实测。
- 全局监控:`32-monitor-final-390.png` 内容高度716px,底部图例不被导航遮挡;`33-monitor-1440.png` 桌面地图与列表可用。
- 接入:`38-access-fixed-1024.png` 指标恢复正常两行显示,修复旧网格轨道导致的裁切;手机协议保留在线/离线/无来源文案。
- 事件:`43-alerts-fixed-1024.png` 执行状态完整显示。修复外层970px最小宽度造成的滚动容器错位,由Semi表格承载横向滚动。
- 运维:`39-operations-final-1024.png` 头部说明和操作分行;`42-operations-menu-390.png` 手机更多操作包括批量处置、归档、导出、趋势、刷新,点击趋势成功打开详情。
- 历史查询、里程日期范围查询、车辆档案返回、帮助关闭、账号权限标签、事件/自动化切换均已实测。没有执行生产权限修改或发送通知。
- 完整Web测试79文件542项通过;后续修改针对AppShell/Alerts/Track/Operations重跑88项通过,最终表格修复后Alerts49项通过。最终 `npm run build`(包含TypeScript、Vite与产物门禁)通过,84个资源检查通过;`git diff --check`通过。
- 开发控制台存在Semi组件findDOMNode弃用告警,以及高德UI脚本初始化提示;地图实际可见。未将其报告为无错误控制台;生产构建继续单独检查。
- 本地截图用于UI验收;实际生产版本、原生告警与发布结果记录在 `release-verification.md`