refine Semi UI history evidence workspace

This commit is contained in:
lingniu
2026-07-18 20:51:50 +08:00
parent 9101d26005
commit 14e87f143d
4 changed files with 304 additions and 8 deletions

View File

@@ -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="按车辆、时间和协议整理可追溯证据。" />