refine Semi UI history evidence workspace
This commit is contained in:
@@ -158,8 +158,10 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
|
||||
expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(await screen.findByText('OLDVIN-evidence')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-evidence-descriptions.semi-descriptions')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-evidence-status')).toHaveTextContent('证据判读正常');
|
||||
expect(view.container.querySelector('.v2-evidence-values.semi-descriptions')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-quality-tag.semi-tag')).toHaveTextContent('正常');
|
||||
expect(view.container.querySelector('.v2-history-protocol-tag.semi-tag')).toHaveTextContent('JT808');
|
||||
expect(view.container.querySelector('.v2-history-workspace')).toHaveClass('is-inspector-open');
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭数据详情' }));
|
||||
expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -170,6 +172,8 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
|
||||
await waitFor(() => expect(mocks.historySeries).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getByRole('heading', { level: 5, name: '聚合趋势' })).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-trend')).toHaveClass('is-expanded');
|
||||
expect((await screen.findByText('覆盖 100.0%')).closest('.semi-tag')).toBeInTheDocument();
|
||||
expect((await screen.findByText('时间窗完整')).closest('.semi-tag')).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('车牌 / VIN,多台用逗号分隔'), { target: { value: 'NEWVIN' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '查询' }));
|
||||
@@ -206,6 +210,7 @@ test('renders only selectable Semi history cards on mobile', async () => {
|
||||
expect(action).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(view.container.querySelector('.v2-history-table-scroll.is-mobile-scroll')).toHaveAttribute('aria-label', '历史明细列表,可上下滚动');
|
||||
expect(view.container.querySelector('.v2-history-table-scroll.is-mobile-scroll')).toHaveAttribute('tabindex', '0');
|
||||
expect(action.closest('.v2-history-mobile-card')?.querySelector('.v2-history-protocol-tag.semi-tag')).toHaveTextContent('JT808');
|
||||
expect(view.container.querySelector('.v2-history-metric-scroll')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-summary-rail')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-result-actions')).toHaveClass('is-mobile-actions');
|
||||
|
||||
@@ -44,6 +44,11 @@ function HistoryQualityTag({ quality }: { quality: string }) {
|
||||
return <Tag className="v2-history-quality-tag" color={color} type="light" size="small">{historyQualityLabel(quality)}</Tag>;
|
||||
}
|
||||
|
||||
function HistoryProtocolTag({ protocol }: { protocol: string }) {
|
||||
const color = protocol === 'GB32960' ? 'blue' : protocol === 'JT808' ? 'cyan' : protocol === 'YUTONG_MQTT' ? 'violet' : 'grey';
|
||||
return <Tag className="v2-history-protocol-tag" color={color} type="light" size="small">{protocol || '未知来源'}</Tag>;
|
||||
}
|
||||
|
||||
function HistoryTrend({ response, category, loading, error, hasMetrics, expanded, onToggle }: {
|
||||
response?: HistorySeriesResponse;
|
||||
category: string;
|
||||
@@ -61,10 +66,14 @@ function HistoryTrend({ response, category, loading, error, hasMetrics, expanded
|
||||
: category === 'mileage'
|
||||
? '日里程按自然日通过明细核对'
|
||||
: '按查询时间窗汇总连续指标';
|
||||
const coverageColor = coverage >= 95 ? 'green' : coverage >= 70 ? 'orange' : 'red';
|
||||
const header = <WorkspacePanelHeader
|
||||
title="聚合趋势"
|
||||
description={description}
|
||||
meta={summary ? `${formatSeriesGrain(summary.grainSeconds)}粒度 · 覆盖 ${coverage.toFixed(1)}% · ${summary.rawPointCount.toLocaleString('zh-CN')} 原始点` : undefined}
|
||||
meta={summary ? <span className="v2-history-trend-meta">
|
||||
<Tag color={coverageColor} type="light" size="small">覆盖 {coverage.toFixed(1)}%</Tag>
|
||||
<span>{formatSeriesGrain(summary.grainSeconds)}粒度 · {summary.rawPointCount.toLocaleString('zh-CN')} 原始点</span>
|
||||
</span> : undefined}
|
||||
actions={<Button
|
||||
className="v2-history-trend-toggle is-expanded"
|
||||
theme="borderless"
|
||||
@@ -84,7 +93,12 @@ function HistoryTrend({ response, category, loading, error, hasMetrics, expanded
|
||||
<g className="v2-chart-axis"><text x="49" y="14">{panel.maximum.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}</text><text x="49" y="100">{panel.minimum.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}</text><text x="54" y="112">{formatAxisTime(panel.start)}</text><text x="786" y="112" textAnchor="end">{formatAxisTime(panel.end)}</text></g>
|
||||
{panel.lines.flatMap((line) => line.paths.map((path, index) => <path key={`${line.key}-${index}`} d={path} fill="none" stroke={line.color} strokeWidth="2" vectorEffect="non-scaling-stroke"><title>{line.label}</title></path>))}
|
||||
</svg><footer>{panel.lines.map((line) => <span key={line.key}><i style={{ background: line.color }} />{line.label}</span>)}</footer></article>)}</div> : <Empty className="v2-history-chart-empty" title="当前时间窗没有趋势点" description="没有可聚合的数值点,空窗不会被人工补值。" />}
|
||||
{summary ? <small className="v2-history-trend-evidence">{summary.evidence} · 缺失 {summary.missingBucketCount.toLocaleString('zh-CN')} / {summary.expectedBucketCount.toLocaleString('zh-CN')} 桶 · 查询 {summary.queryDurationMs} ms</small> : null}
|
||||
{summary ? <footer className="v2-history-trend-evidence">
|
||||
<Tag color={summary.missingBucketCount ? 'orange' : 'green'} type="light" size="small">{summary.missingBucketCount ? '存在空窗' : '时间窗完整'}</Tag>
|
||||
<span title={summary.evidence}>{summary.evidence}</span>
|
||||
<strong>缺失 {summary.missingBucketCount.toLocaleString('zh-CN')} / {summary.expectedBucketCount.toLocaleString('zh-CN')} 桶</strong>
|
||||
<span>查询 {summary.queryDurationMs} ms</span>
|
||||
</footer> : null}
|
||||
</Card>;
|
||||
}
|
||||
|
||||
@@ -141,12 +155,22 @@ function ExportJobsPanel({ showHeader = true }: { showHeader?: boolean }) {
|
||||
|
||||
function EvidencePanel({ row, metrics, onClose, showHeader = true }: { row?: HistoryDataRow; metrics: HistoryMetricDefinition[]; onClose: () => void; showHeader?: boolean }) {
|
||||
return <Card className="v2-history-evidence" bodyStyle={{ padding: 0 }}>{showHeader ? <WorkspacePanelHeader title="数据详情" description={row ? `${row.plate || '未绑定车牌'} · ${row.protocol}` : '选择明细后核对来源与质量'} actions={row ? <Button theme="borderless" type="tertiary" icon={<IconClose />} onClick={onClose} aria-label="关闭数据详情" /> : null} /> : null}
|
||||
{row ? <><Descriptions className="v2-history-evidence-descriptions" align="left" size="small" data={[
|
||||
{row ? <><div className={`v2-history-evidence-status${isHealthyHistoryQuality(row.quality) ? ' is-healthy' : ' is-attention'}`}>
|
||||
<div>
|
||||
<span>证据判读</span>
|
||||
<strong>{historyQualityLabel(row.quality)}</strong>
|
||||
<small>{row.qualityReason || '平台未返回质量说明'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<HistoryProtocolTag protocol={row.protocol} />
|
||||
<PlatformTime className="v2-history-time" value={row.deviceTime} />
|
||||
</div>
|
||||
</div><Descriptions className="v2-history-evidence-descriptions" align="left" size="small" data={[
|
||||
{ key: '车牌', value: row.plate || '—' },
|
||||
{ key: 'VIN', value: row.vin },
|
||||
{ key: '设备时间', value: <PlatformTime className="v2-history-time" value={row.deviceTime} /> },
|
||||
{ key: '服务时间', value: <PlatformTime className="v2-history-time" value={row.serverTime} /> },
|
||||
{ key: '数据来源', value: <Tag color="blue" type="light" size="small">{row.protocol}</Tag> },
|
||||
{ key: '数据来源', value: <HistoryProtocolTag protocol={row.protocol} /> },
|
||||
{ key: '数据质量', value: <HistoryQualityTag quality={row.quality} /> },
|
||||
{ key: '质量原因', value: row.qualityReason || '平台未返回质量说明' }
|
||||
]} />
|
||||
@@ -202,7 +226,7 @@ function HistoryDataTable({ rows, metrics, selectedRowID, onSelect }: { rows: Hi
|
||||
{ title: '车牌', dataIndex: 'plate', width: 110, fixed: 'left' as const, className: 'v2-history-plate-column', render: (value: string) => value || '—' },
|
||||
{ title: '服务时间', dataIndex: 'serverTime', width: 150, render: (value: string) => <PlatformTime className="v2-history-time" value={value} /> },
|
||||
{ title: 'VIN', dataIndex: 'vin', width: 160, render: (value: string) => <span className="v2-history-vin" title={value}>{value}</span> },
|
||||
{ title: '协议', dataIndex: 'protocol', width: 100 },
|
||||
{ title: '协议', dataIndex: 'protocol', width: 110, render: (value: string) => <HistoryProtocolTag protocol={value} /> },
|
||||
...metrics.map((metric) => ({
|
||||
title: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}`,
|
||||
dataIndex: metric.key,
|
||||
@@ -461,7 +485,7 @@ export default function HistoryPage() {
|
||||
onKeyDown={scrollHistoryTable}
|
||||
>
|
||||
{mobileLayout
|
||||
? <div className="v2-history-mobile-list">{resultRows.map((row) => <Card key={row.id} className={`v2-history-mobile-card${selectedRow?.id === row.id ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}><Button theme="borderless" type="tertiary" className="v2-history-mobile-action" aria-pressed={selectedRow?.id === row.id} aria-expanded={selectedRow?.id === row.id} aria-label={`查看 ${row.plate || row.vin} ${row.deviceTime} 数据详情`} onClick={() => selectRow(row)}><span className="v2-history-mobile-card-content"><header><span><strong>{row.plate || '未绑定车牌'}</strong><small>{row.vin}</small></span><span className={`v2-quality is-${row.quality}`}><i />{historyQualityLabel(row.quality)}</span></header><p><PlatformTime className="v2-history-time" value={row.deviceTime} /><b>{row.protocol}</b></p>{isHealthyHistoryQuality(row.quality) ? null : <small className="v2-history-mobile-quality">{row.qualityReason || '平台未返回质量说明'}</small>}<dl>{visibleMetrics.slice(0, 4).map((metric) => <div key={metric.key}><dt>{metric.label}</dt><dd>{formatHistoryValue(row.values[metric.key], metric)}</dd></div>)}</dl><footer>查看完整详情<IconChevronRight /></footer></span></Button></Card>)}</div>
|
||||
? <div className="v2-history-mobile-list">{resultRows.map((row) => <Card key={row.id} className={`v2-history-mobile-card${selectedRow?.id === row.id ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}><Button theme="borderless" type="tertiary" className="v2-history-mobile-action" aria-pressed={selectedRow?.id === row.id} aria-expanded={selectedRow?.id === row.id} aria-label={`查看 ${row.plate || row.vin} ${row.deviceTime} 数据详情`} onClick={() => selectRow(row)}><span className="v2-history-mobile-card-content"><header><span><strong>{row.plate || '未绑定车牌'}</strong><small>{row.vin}</small></span><span className={`v2-quality is-${row.quality}`}><i />{historyQualityLabel(row.quality)}</span></header><span className="v2-history-mobile-meta"><PlatformTime className="v2-history-time" value={row.deviceTime} /><HistoryProtocolTag protocol={row.protocol} /></span>{isHealthyHistoryQuality(row.quality) ? null : <small className="v2-history-mobile-quality">{row.qualityReason || '平台未返回质量说明'}</small>}<dl>{visibleMetrics.slice(0, 4).map((metric) => <div key={metric.key}><dt>{metric.label}</dt><dd>{formatHistoryValue(row.values[metric.key], metric)}</dd></div>)}</dl><footer>查看完整详情<IconChevronRight /></footer></span></Button></Card>)}</div>
|
||||
: <HistoryDataTable rows={resultRows} metrics={visibleMetrics} selectedRowID={selectedRow?.id} onSelect={selectRow} />}
|
||||
{dataQuery.isPending && keywords.length
|
||||
? <PanelLoading className="v2-history-loading" title="正在加载当前筛选范围的历史数据" description="按车辆、时间和协议整理可追溯证据。" />
|
||||
|
||||
@@ -286,7 +286,7 @@ test('filters the customer directory by status and exposes the active result sco
|
||||
}
|
||||
]);
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
|
||||
const view = render(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument();
|
||||
@@ -309,6 +309,14 @@ test('filters the customer directory by status and exposes the active result sco
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument());
|
||||
expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument();
|
||||
|
||||
// Semi keeps the first Select popup mounted briefly while its close animation
|
||||
// finishes. Remount before the second scope assertion so the test never clicks
|
||||
// a stale portal when the full suite is under load.
|
||||
view.unmount();
|
||||
document.querySelectorAll('.semi-portal').forEach((portal) => portal.remove());
|
||||
render(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
|
||||
expect(await screen.findByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('combobox', { name: '访问状态' }));
|
||||
const attentionOption = await waitFor(() => {
|
||||
const options = [...document.querySelectorAll<HTMLElement>('.semi-select-option')]
|
||||
|
||||
Reference in New Issue
Block a user