feat(platform): add dashboard time window monitor

This commit is contained in:
lingniu
2026-07-05 01:10:12 +08:00
parent bb18a5075c
commit 83f033244c
3 changed files with 138 additions and 5 deletions

View File

@@ -107,6 +107,24 @@ function nextDate(value: string) {
return date.toISOString().slice(0, 10);
}
function todayDateString() {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function previousDateString(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
if (!match) return value;
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]) - 1);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function priorityIssueVehicleLabel(row: QualityIssueRow) {
const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim();
return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-';
@@ -237,6 +255,13 @@ export function Dashboard({
const [locations, setLocations] = useState<VehicleRealtimeRow[]>([]);
const [qualityIssues, setQualityIssues] = useState<QualityIssueRow[]>([]);
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
const [timeMonitorFilters, setTimeMonitorFilters] = useState<Record<string, string>>(() => {
const today = todayDateString();
return {
dateFrom: previousDateString(today),
dateTo: today
};
});
const [loading, setLoading] = useState(true);
const [coverageLoading, setCoverageLoading] = useState(false);
const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState('');
@@ -655,6 +680,39 @@ export function Dashboard({
]
}
];
const timeMonitorScope = (values = timeMonitorFilters) => {
const normalized: Record<string, string> = {};
for (const key of ['keyword', 'protocol', 'dateFrom', 'dateTo'] as const) {
const value = String(values[key] ?? '').trim();
if (value) {
normalized[key] = value;
}
}
return normalized;
};
const openTimeMonitorHistory = () => {
onOpenHistory(timeMonitorScope());
};
const openTimeMonitorMileage = () => {
onOpenMileage(timeMonitorScope());
};
const openTimeMonitorRaw = () => {
onOpenHistory({ ...timeMonitorScope(), tab: 'raw', includeFields: 'true' });
};
const openTimeMonitorAlerts = () => {
const scope = timeMonitorScope();
onOpenQuality({
...(scope.keyword ? { keyword: scope.keyword } : {}),
...(scope.protocol ? { protocol: scope.protocol } : {})
});
};
const timeMonitorSummary = (() => {
const scope = timeMonitorScope();
const vehicle = scope.keyword || '全部车辆';
const source = scope.protocol || '全部数据通道';
const range = scope.dateFrom || scope.dateTo ? `${scope.dateFrom || '-'}${scope.dateTo || '-'}` : '全部时间';
return `${vehicle} / ${source} / ${range}`;
})();
const dataFlowStages = [
{
stage: '01',
@@ -1009,13 +1067,48 @@ export function Dashboard({
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}> CSV</Button>
</Space>
</Card>
<Card bordered title="自定义时间监控" style={{ marginBottom: 16 }}>
<Card
bordered
title={<Space><span></span><Tag color="blue">{timeMonitorSummary}</Tag></Space>}
style={{ marginBottom: 16 }}
>
<Form
initValues={timeMonitorFilters}
layout="horizontal"
onSubmit={(values) => setTimeMonitorFilters(timeMonitorScope(values as Record<string, string>))}
style={{ marginBottom: 12 }}
>
<Form.Input field="keyword" label="车辆" placeholder="VIN / 车牌 / 手机号" style={{ width: 240 }} />
<Form.Select field="protocol" label="数据通道" placeholder="全部通道" style={{ width: 150 }}>
<Select.Option value="GB32960">GB32960</Select.Option>
<Select.Option value="JT808">JT808</Select.Option>
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
</Form.Select>
<Form.Input field="dateFrom" label="开始日期" placeholder="2026-07-01" style={{ width: 150 }} />
<Form.Input field="dateTo" label="结束日期" placeholder="2026-07-05" style={{ width: 150 }} />
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => {
const today = todayDateString();
setTimeMonitorFilters({ dateFrom: previousDateString(today), dateTo: today });
}}></Button>
</Space>
</Form>
<div className="vp-time-monitor-actions">
<Button theme="solid" type="primary" onClick={openTimeMonitorHistory}></Button>
<Button theme="light" type="primary" onClick={openTimeMonitorMileage}></Button>
<Button theme="light" type="primary" onClick={openTimeMonitorRaw}></Button>
<Button theme="light" type="warning" onClick={openTimeMonitorAlerts}></Button>
</div>
<Typography.Text type="secondary">
</Typography.Text>
<div className="vp-time-monitor-grid">
{[
{ title: '今天车辆轨迹', detail: '查看当天活跃车辆的轨迹、速度和里程断点。', action: '查今天', onClick: () => onOpenHistory() },
{ title: '历史时间窗', detail: '按任意起止时间查询位置历史、原始记录和字段明细。', action: '自定义查询', onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) },
{ title: '区间里程', detail: '按车辆和时间范围核对区间里程、日统计和异常点。', action: '里程统计', onClick: () => onOpenMileage() },
{ title: '告警复盘', detail: '按车辆或事件回放告警发生前后的轨迹和数据证据。', action: '告警事件', onClick: () => onOpenQuality() }
{ title: '今天车辆轨迹', detail: '查看当天活跃车辆的轨迹、速度和里程断点。', action: '查今天', onClick: openTimeMonitorHistory },
{ title: '历史时间窗', detail: '按任意起止时间查询位置历史、原始记录和字段明细。', action: '自定义查询', onClick: openTimeMonitorRaw },
{ title: '区间里程', detail: '按车辆和时间范围核对区间里程、日统计和异常点。', action: '里程统计', onClick: openTimeMonitorMileage },
{ title: '告警复盘', detail: '按车辆或事件回放告警发生前后的轨迹和数据证据。', action: '告警事件', onClick: openTimeMonitorAlerts }
].map((item) => (
<button key={item.title} type="button" className="vp-time-monitor-item" onClick={item.onClick} aria-label={`时间监控 ${item.title}`}>
<div>

View File

@@ -414,6 +414,16 @@ body {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.vp-time-monitor-actions {
min-height: 36px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.vp-time-monitor-item {

View File

@@ -792,6 +792,11 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
expect(screen.getByText('历史数据导出')).toBeInTheDocument();
expect(screen.getByText('车辆服务入口')).toBeInTheDocument();
expect(screen.getByText('自定义时间监控')).toBeInTheDocument();
expect(screen.getByText('应用时间窗')).toBeInTheDocument();
expect(screen.getByText('重置最近一天')).toBeInTheDocument();
expect(screen.getByText('查看轨迹')).toBeInTheDocument();
expect(screen.getByText('统计里程')).toBeInTheDocument();
expect(screen.getByText('导出原始记录')).toBeInTheDocument();
expect(screen.getByText('今天车辆轨迹')).toBeInTheDocument();
expect(screen.getByText('历史时间窗')).toBeInTheDocument();
expect(screen.getByText('区间里程')).toBeInTheDocument();
@@ -856,10 +861,35 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
cleanup();
await renderDashboard();
fireEvent.click(screen.getByRole('button', { name: '查看轨迹' }));
expect(window.location.hash.startsWith('#/history')).toBe(true);
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy();
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy();
cleanup();
await renderDashboard();
fireEvent.click(screen.getByRole('button', { name: '统计里程' }));
expect(window.location.hash.startsWith('#/mileage')).toBe(true);
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy();
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy();
cleanup();
await renderDashboard();
fireEvent.click(screen.getByRole('button', { name: '导出原始记录' }));
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw');
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy();
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy();
cleanup();
await renderDashboard();
fireEvent.click(screen.getByRole('button', { name: '时间监控 历史时间窗' }));
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy();
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateTo')).toBeTruthy();
cleanup();
await renderDashboard();