feat(web): unify data panel hierarchy
This commit is contained in:
@@ -511,6 +511,8 @@ 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] || '全部主车辆'} · 缺席协议仅表示当前未发现`}
|
||||
actionsClassName="v2-access-table-actions"
|
||||
|
||||
@@ -349,6 +349,8 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
||||
<div className={`v2-alert-workspace${selectedID && !mobileLayout ? ' is-inspector-open' : ''}`}>
|
||||
<Card className="v2-alert-table-card" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
eyebrow="实时队列"
|
||||
tone="warning"
|
||||
title="当前事件队列"
|
||||
description={selectedID && !mobileLayout ? '已展开处置详情 · 点击其他事件快速切换' : '点击事件查看触发证据、处理进度与处置动作'}
|
||||
meta={`${Number(sums?.unprocessed ?? 0).toLocaleString('zh-CN')} 条待处置 · 共 ${(events.data?.total ?? 0).toLocaleString('zh-CN')} 条`}
|
||||
|
||||
@@ -623,6 +623,8 @@ 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)} />
|
||||
<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="blue" type="light" size="small">{visibleMetrics.length} / {allMetrics.length} 字段</Tag>{resultSummary ? <span className="v2-history-query-duration">查询 {resultSummary.queryDurationMs} ms</span> : null}</span>}
|
||||
|
||||
@@ -570,6 +570,8 @@ 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="关键链路、基础设施与车辆覆盖的同一健康快照"
|
||||
meta={<span className="v2-ops-overview-meta"><HealthTag status={overallStatus}>{!data ? '正在读取' : healthIssueCount > 0 ? `${healthIssueCount} 项需关注` : '全部正常'}</HealthTag><Typography.Text type="tertiary">15 秒自动刷新</Typography.Text></span>}
|
||||
|
||||
@@ -333,6 +333,8 @@ export default function ReconciliationCenter() {
|
||||
return <><Card className="v2-reconcile-center" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
className="v2-reconcile-heading"
|
||||
eyebrow="自动对账"
|
||||
tone="health"
|
||||
title="质量问题队列"
|
||||
description="自动发现并合并重复问题;复核人员基于原始证据形成结论,不直接改写数据。"
|
||||
meta="每日自动对账"
|
||||
|
||||
@@ -762,6 +762,8 @@ export default function StatisticsPage() {
|
||||
{statistics.isError || mileage.isError || fleetVehicles.isError ? <InlineError message={(statistics.error ?? mileage.error ?? fleetVehicles.error) instanceof Error ? (statistics.error ?? mileage.error ?? fleetVehicles.error as Error).message : '里程数据加载失败'} onRetry={() => { statistics.refetch(); mileage.refetch(); if (!hasVehicles) fleetVehicles.refetch(); }} /> : null}
|
||||
<Card className="v2-mileage-results" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
eyebrow="每日统计"
|
||||
tone="primary"
|
||||
title="车辆每日里程"
|
||||
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>}
|
||||
|
||||
@@ -73,6 +73,21 @@ describe('V2 production entry', () => {
|
||||
expect(corePageSources[name]).toContain("from '../shared/WorkspaceCommandBar'");
|
||||
expect(corePageSources[name]).toContain('<WorkspaceCommandBar');
|
||||
}
|
||||
for (const [name, eyebrow, tone] of [
|
||||
['StatisticsPage', '每日统计', 'primary'],
|
||||
['AlertsPage', '实时队列', 'warning'],
|
||||
['AccessPage', '接入差异', 'primary'],
|
||||
['HistoryPage', '证据结果', 'primary'],
|
||||
['OperationsPage', '运行快照', 'health']
|
||||
]) {
|
||||
expect(corePageSources[name]).toContain(`eyebrow="${eyebrow}"`);
|
||||
expect(corePageSources[name]).toContain(`tone="${tone}"`);
|
||||
}
|
||||
expect(reconciliationSource).toContain('eyebrow="自动对账"');
|
||||
expect(reconciliationSource).toContain('tone="health"');
|
||||
expect(workspaceStyles).toContain('.v2-workspace-panel-header.has-eyebrow');
|
||||
expect(workspaceStyles).toContain('.v2-workspace-panel-eyebrow.semi-typography');
|
||||
expect(workspaceStyles).toContain('.v2-mileage-results .v2-workspace-panel-header.has-eyebrow > .v2-workspace-panel-copy');
|
||||
expect(corePageSources.VehiclePage).toContain("lazy(() => import('./VehicleSearchWorkspace'))");
|
||||
expect(vehicleSearchSource).toContain("from '../shared/WorkspaceCommandBar'");
|
||||
expect(vehicleSearchSource).toContain('<WorkspaceCommandBar');
|
||||
|
||||
@@ -23,4 +23,18 @@ describe('WorkspacePanelHeader', () => {
|
||||
rerender(<WorkspacePanelHeader variant="inverted" title="粤A12345" description="07/17 11:30:00" meta="50%" />);
|
||||
expect(screen.getByRole('heading', { name: '粤A12345', level: 5 }).closest('header')).toHaveClass('is-inverted');
|
||||
});
|
||||
|
||||
test('adds one compact semantic context marker for primary data panels', () => {
|
||||
render(<WorkspacePanelHeader
|
||||
eyebrow="实时队列"
|
||||
tone="warning"
|
||||
title="当前事件队列"
|
||||
description="点击事件查看触发证据"
|
||||
meta="12 条待处置"
|
||||
/>);
|
||||
|
||||
const header = screen.getByRole('heading', { name: '当前事件队列', level: 5 }).closest('header');
|
||||
expect(header).toHaveClass('has-eyebrow', 'tone-warning');
|
||||
expect(screen.getByText('实时队列')).toHaveClass('v2-workspace-panel-eyebrow', 'semi-typography');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,29 +3,42 @@ import type { ReactNode } from 'react';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export type WorkspacePanelTone = 'neutral' | 'primary' | 'warning' | 'authority' | 'health' | 'danger';
|
||||
|
||||
export function WorkspacePanelHeader({
|
||||
title,
|
||||
description,
|
||||
eyebrow,
|
||||
meta,
|
||||
actions,
|
||||
variant = 'default',
|
||||
tone = 'neutral',
|
||||
className = '',
|
||||
actionsClassName = ''
|
||||
}: {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
eyebrow?: ReactNode;
|
||||
meta?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
variant?: 'default' | 'compact' | 'inverted';
|
||||
tone?: WorkspacePanelTone;
|
||||
className?: string;
|
||||
actionsClassName?: string;
|
||||
}) {
|
||||
const headerClassName = ['v2-workspace-panel-header', `is-${variant}`, className].filter(Boolean).join(' ');
|
||||
const headerClassName = [
|
||||
'v2-workspace-panel-header',
|
||||
`is-${variant}`,
|
||||
`tone-${tone}`,
|
||||
eyebrow != null ? 'has-eyebrow' : '',
|
||||
className
|
||||
].filter(Boolean).join(' ');
|
||||
const trailingClassName = ['v2-workspace-panel-actions', actionsClassName].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<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}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
*/
|
||||
|
||||
.v2-workspace-panel-header {
|
||||
--v2-panel-accent: #708198;
|
||||
--v2-panel-accent-soft: rgba(112, 129, 152, .08);
|
||||
--v2-panel-chip-border: rgba(112, 129, 152, .16);
|
||||
--v2-panel-chip-background: #f5f7fa;
|
||||
display: flex;
|
||||
min-height: 52px;
|
||||
flex: 0 0 auto;
|
||||
@@ -18,6 +22,47 @@
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.tone-primary {
|
||||
--v2-panel-accent: #1268df;
|
||||
--v2-panel-accent-soft: rgba(18, 104, 223, .08);
|
||||
--v2-panel-chip-border: rgba(18, 104, 223, .16);
|
||||
--v2-panel-chip-background: #edf5ff;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.tone-warning {
|
||||
--v2-panel-accent: #d97706;
|
||||
--v2-panel-accent-soft: rgba(217, 119, 6, .09);
|
||||
--v2-panel-chip-border: rgba(217, 119, 6, .17);
|
||||
--v2-panel-chip-background: #fff6e8;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.tone-authority {
|
||||
--v2-panel-accent: #6656c8;
|
||||
--v2-panel-accent-soft: rgba(102, 86, 200, .08);
|
||||
--v2-panel-chip-border: rgba(102, 86, 200, .16);
|
||||
--v2-panel-chip-background: #f4f1ff;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.tone-health {
|
||||
--v2-panel-accent: #0f8f68;
|
||||
--v2-panel-accent-soft: rgba(15, 143, 104, .08);
|
||||
--v2-panel-chip-border: rgba(15, 143, 104, .16);
|
||||
--v2-panel-chip-background: #eaf8f3;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.tone-danger {
|
||||
--v2-panel-accent: #d9473f;
|
||||
--v2-panel-accent-soft: rgba(217, 71, 63, .08);
|
||||
--v2-panel-chip-border: rgba(217, 71, 63, .16);
|
||||
--v2-panel-chip-background: #fff2f1;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.has-eyebrow {
|
||||
background:
|
||||
linear-gradient(90deg, var(--v2-panel-accent-soft) 0%, rgba(255, 255, 255, .94) 28%, #fff 100%);
|
||||
box-shadow: inset 3px 0 var(--v2-panel-accent);
|
||||
}
|
||||
|
||||
/*
|
||||
* Shared Semi UI mobile filter sheet.
|
||||
* Keep the same title hierarchy, scroll surface, condition card and action
|
||||
@@ -2534,6 +2579,43 @@
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.has-eyebrow > .v2-workspace-panel-copy {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
align-items: center;
|
||||
column-gap: 7px;
|
||||
row-gap: 1px;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-eyebrow.semi-typography {
|
||||
display: inline-flex;
|
||||
min-height: 18px;
|
||||
align-items: center;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border: 1px solid var(--v2-panel-chip-border);
|
||||
border-radius: 999px;
|
||||
background: var(--v2-panel-chip-background);
|
||||
padding-inline: 6px;
|
||||
color: var(--v2-panel-accent);
|
||||
font-size: 8px;
|
||||
font-weight: 750;
|
||||
letter-spacing: .04em;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.has-eyebrow > .v2-workspace-panel-copy > h5.semi-typography {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header.has-eyebrow > .v2-workspace-panel-copy > .semi-typography:last-child:not(.v2-workspace-panel-eyebrow) {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.v2-workspace-panel-header > .v2-workspace-panel-copy {
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
@@ -15550,12 +15632,34 @@
|
||||
}
|
||||
|
||||
.v2-mileage-results .v2-workspace-panel-copy {
|
||||
width: 82px;
|
||||
min-width: 82px;
|
||||
max-width: 82px;
|
||||
width: 108px;
|
||||
min-width: 108px;
|
||||
max-width: 108px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.v2-mileage-results .v2-workspace-panel-header.has-eyebrow > .v2-workspace-panel-copy {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
align-content: center;
|
||||
justify-items: start;
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
.v2-mileage-results .v2-workspace-panel-header.has-eyebrow .v2-workspace-panel-eyebrow.semi-typography {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-height: 16px;
|
||||
padding-inline: 5px;
|
||||
font-size: 7px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.v2-mileage-results .v2-workspace-panel-header.has-eyebrow > .v2-workspace-panel-copy > h5.semi-typography {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.v2-mileage-results .v2-workspace-panel-copy > h5.semi-typography {
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
|
||||
@@ -151,4 +151,51 @@
|
||||
- 首次截图在 SideSheet 进入动画期间截到不完整底栏;等待动画稳定后复核,重置与应用按钮完整可见。
|
||||
- 同视口对比确认新增摘要没有压缩关键输入、日期范围和来源选择,未发现 P0、P1 或 P2 问题。
|
||||
|
||||
## 共享数据面板标题 Semi UI Design QA
|
||||
|
||||
- source visual truth: ECS 生产端里程查询、告警中心、接入管理与运维质量结果面板
|
||||
- source screenshots:
|
||||
- statistics baseline: `/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/.codex-audit/semi-panel-hierarchy-20260720/desktop-statistics-after.png`
|
||||
- mobile statistics baseline: `/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/.codex-audit/semi-panel-hierarchy-20260720/mobile-statistics-after.png`
|
||||
- implementation screenshots:
|
||||
- statistics desktop: `/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/.codex-audit/semi-panel-hierarchy-20260720/desktop-statistics-fixed.png`
|
||||
- statistics mobile: `/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/.codex-audit/semi-panel-hierarchy-20260720/mobile-statistics-fixed.png`
|
||||
- alert center desktop: `/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/.codex-audit/semi-panel-hierarchy-20260720/desktop-alerts-fixed.png`
|
||||
- access center desktop: `/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/.codex-audit/semi-panel-hierarchy-20260720/desktop-access-fixed.png`
|
||||
- operations desktop: `/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/.codex-audit/semi-panel-hierarchy-20260720/desktop-operations-fixed.png`
|
||||
- viewport and state:
|
||||
- mobile `390 × 844`
|
||||
- desktop `1440 × 900`
|
||||
- 管理员权限、生产数据、默认查询状态
|
||||
|
||||
### Fidelity Ledger
|
||||
|
||||
| Surface | Required fidelity | Implementation evidence | Result |
|
||||
| --- | --- | --- | --- |
|
||||
| Semantic hierarchy | 高频结果区必须先说明数据语境,再给出当前任务标题。 | 共享 `WorkspacePanelHeader` 新增真实眉题层;里程、告警、接入、历史、对账和运维分别使用“每日统计”“实时队列”“接入差异”等业务语义。 | Improved. |
|
||||
| Tone consistency | 不同业务状态需要可辨认,但不能形成高饱和装饰色块。 | 使用共享 primary、warning、health 等低对比度语义变量,只在顶部细线、眉题与轻微渐变中表达状态。 | Passed. |
|
||||
| Data density | 标题升级不能挤压表格、统计条和分页主体。 | `1440 × 900` 下四个生产结果页保留原表格行数、筛选条件、分页和滚动边界。 | Preserved. |
|
||||
| Mobile hierarchy | 眉题、结果标题、车辆数量和操作不能在窄屏互相遮挡。 | 里程结果头在 `390px` 下缩短描述并收敛文本轨道;车辆数量、刷新和导出仍完整可见。 | Passed. |
|
||||
| Action continuity | 刷新、导出和结果计数仍需保持原按钮语义与点击范围。 | 共享组件只调整标题结构,原 Semi Button、Tag、事件处理器和可访问名称均未替换。 | Preserved. |
|
||||
| Shared implementation | 页面不能各自复制标题视觉规则。 | 六个页面复用同一 `WorkspacePanelHeader` 与统一 tone 类型,样式集中在 `workspace.css`。 | Passed. |
|
||||
| Desktop stability | 共享视觉层不得产生横向溢出、截断或固定列错位。 | 里程、告警、接入和运维生产截图均无标题裁切,列表滚动条、固定列及分页位置正常。 | Passed. |
|
||||
|
||||
### Interaction And Accessibility Checks
|
||||
|
||||
- 结果标题继续输出正确的 Heading 层级,眉题使用 Semi Typography 文本而不是模拟标题;
|
||||
- 里程查询的“刷新里程数据”和“导出 Excel”保留完整可访问名称;
|
||||
- 告警、接入和运维结果列表保留原筛选、刷新、分页与纵向滚动交互;
|
||||
- `390 × 844` 下里程矩阵车牌列与区间总里程列继续固定,日期列可横向浏览;
|
||||
- `1440 × 900` 下四个重点页面没有出现标题折断、操作区覆盖或列表主体缩水;
|
||||
- 生产发布版本 `semi-panel-hierarchy-20260720031800` 完成资源烟测与登录态复核。
|
||||
|
||||
### Comparison History
|
||||
|
||||
#### Pass 1 — fixed
|
||||
|
||||
- 首轮移动端复核发现新增眉题、结果标题和车辆数量在同一窄轨道内显得拥挤。
|
||||
- 通过共享标题的移动端两级文本布局和里程结果区专用宽度约束,减少无效描述占位,同时保留结果计数与操作。
|
||||
- 第二轮同视口截图确认“每日统计—车辆每日里程—20 / 1024 辆”层级可辨,刷新、导出和首屏表格均可达。
|
||||
- 桌面端里程、告警、接入与运维四个结果面板语义色克制、密度稳定,未发现 P0、P1 或 P2 视觉与交互问题。
|
||||
|
||||
final result: passed
|
||||
|
||||
Reference in New Issue
Block a user