diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx
index 5258af81..7b97a78d 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx
@@ -151,10 +151,11 @@ test('clears old alert rows and inspector evidence when the event scope changes'
expect(view.container.querySelector('.v2-alert-inspector.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-workspace')).toHaveClass('is-inspector-open');
expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'true');
- expect(screen.getByText('事件信息').closest('.semi-card')).toHaveClass('v2-alert-detail-card');
- expect(screen.getByText('证据对比').closest('.semi-card')).toHaveClass('v2-alert-evidence-card');
+ expect(view.container.querySelector('.v2-alert-focus-card[aria-label="告警处置摘要"]')).toHaveClass('v2-alert-detail-card');
+ expect(view.container.querySelector('.v2-alert-technical-collapse.semi-collapse')).toBeInTheDocument();
+ expect(screen.getByText('技术详情')).toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-inspector-heading')).toHaveClass('v2-workspace-panel-header');
- expect(view.container.querySelectorAll('.v2-alert-descriptions.semi-descriptions')).toHaveLength(2);
+ expect(view.container.querySelectorAll('.v2-alert-descriptions.semi-descriptions')).toHaveLength(1);
expect(screen.getByLabelText('告警处理进度')).toHaveClass('semi-timeline');
expect(screen.getByText('规则首次命中')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '关闭告警详情' }));
@@ -179,6 +180,27 @@ test('clears old alert rows and inspector evidence when the event scope changes'
await waitFor(() => expect(screen.queryByText('正在更新事件…')).not.toBeInTheDocument());
});
+test('keeps status shortcuts comparable while the event list is status-filtered', async () => {
+ mocks.alertSummaryV2.mockResolvedValue({ active: 3, unprocessed: 2, processing: 1, recovered: 4, closed: 1, ignored: 0, unreadNotifications: 0, asOf: '' });
+ mocks.alertEventsV2.mockResolvedValue({ items: [], total: 2, limit: 20, offset: 0 });
+ mocks.alertRulesV2.mockResolvedValue([]);
+ mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ render();
+
+ await screen.findByText('当前筛选条件没有告警事件');
+ const summaryScope = mocks.alertSummaryV2.mock.calls[0][0];
+ expect(summaryScope).toEqual(expect.objectContaining({ keyword: 'TESTVIN' }));
+ expect(summaryScope).not.toHaveProperty('status');
+ expect(screen.getByRole('button', { name: '筛选全部事件,共 8 条' })).toHaveAttribute('aria-pressed', 'false');
+ expect(screen.getByRole('button', { name: '筛选未处理,共 2 条' })).toHaveAttribute('aria-pressed', 'true');
+ expect(screen.getByRole('button', { name: '已恢复 4' })).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('button', { name: '筛选全部事件,共 8 条' }));
+ await waitFor(() => expect(mocks.alertEventsV2).toHaveBeenLastCalledWith(expect.not.objectContaining({ status: expect.anything() }), expect.anything()));
+ expect(screen.getByRole('button', { name: '筛选全部事件,共 8 条' })).toHaveAttribute('aria-pressed', 'true');
+});
+
test('renders only selectable Semi alert cards on mobile', async () => {
layout.mobile = true;
const event = alertEvent('mobile-event', 'MOBILEVIN', '粤A移动01');
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx
index c84026ec..1087d949 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx
@@ -1,5 +1,5 @@
import { IconAlarm, IconBell, IconChevronRight, IconClose, IconFilter, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
-import { Button, Card, CardGroup, Descriptions, Empty, Input, Radio, Select, SideSheet, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
+import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Radio, Select, SideSheet, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FormEvent, useEffect, useMemo, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
@@ -77,12 +77,12 @@ function AlertEventTable({ rows, selectedID, onSelect }: { rows: AlertEvent[]; s
{ title: '规则', dataIndex: 'ruleName', width: 170, render: (value: string) => {value} },
{ title: '协议', dataIndex: 'protocol', width: 92, render: (value: string) => value || '—' },
{ title: '触发时间', dataIndex: 'triggeredAt', width: 132, render: (value: string) => formatAlertTime(value) },
- { title: '恢复时间', dataIndex: 'recoveredAt', width: 132, render: (value: string) => formatAlertTime(value) },
{ title: '状态', dataIndex: 'status', width: 90, render: (_: AlertEvent['status'], event: AlertEvent) => },
- { title: '触发值', dataIndex: 'triggerValue', width: 105, render: (_: unknown, event: AlertEvent) => alertValue(event) },
- { title: '阈值', dataIndex: 'threshold', width: 125, render: (_: unknown, event: AlertEvent) => thresholdText(event) },
+ {
+ title: '触发 / 阈值', dataIndex: 'evidence', width: 164,
+ render: (_: unknown, event: AlertEvent) =>
{alertValue(event)}{thresholdText(event)}
+ },
{ title: '位置', dataIndex: 'location', width: 180, render: (value: string) => {value || '—'} },
- { title: '处理人', dataIndex: 'handler', width: 110, render: (value: string) => value || '—' }
], [onSelect, selectedID]);
return } title="选择告警事件" description="查看触发证据、状态时间线和处置动作。" />;
return {sheet ? null : } aria-label="关闭告警详情" onClick={onClose} />}>} />
-
事件信息} headerLine bodyStyle={{ padding: 0 }}>
- {event.id} },
- { key: '规则 / 版本', value: `${event.ruleId} / v${event.ruleVersion}` },
- { key: '车辆 / VIN', value: `${event.plate || '—'} / ${event.vin}` },
- { key: '触发时间', value: formatAlertTime(event.triggeredAt) },
- { key: '恢复时间', value: formatAlertTime(event.recoveredAt) }
- ]} />
-
-
证据对比} headerLine bodyStyle={{ padding: 0 }}>
+
+
+ 告警车辆{event.plate || '未绑定车牌'}{event.vin}
+ {event.protocol || '未知协议'}
+
+
+ 触发时间{formatAlertTime(event.triggeredAt)}
+ 当前状态{statusLabels[event.status]}
+
触发值{alertValue(event)}VS阈值条件{thresholdText(event)}
- {event.protocol || '—'} },
- { key: '事件 / 接收', value: `${formatAlertTime(event.eventAt)} / ${formatAlertTime(event.receivedAt)}` }
- ]} />
处理进度} headerLine>
{event.actions?.length ? {event.actions.map((item, index) => {item.actor} · {formatAlertTime(item.createdAt)}}>{actionLabels[item.action] ?? item.action}{item.note ? {item.note}
: null})} : }
@@ -129,6 +123,21 @@ function EventInspector({ event, note, acting, actionError, editable, onNote, on
处置与备注} headerLine>
{editable ? <>{actionError ? {actionError}
: null}> : 当前为只读角色,可查看完整证据与处置记录。
}
+
+ 技术详情事件标识、规则版本与接收时间}
+ >
+ {event.id} },
+ { key: '来源事件 ID', value: event.sourceEventId || '—' },
+ { key: '规则 / 版本', value: `${event.ruleId} / v${event.ruleVersion}` },
+ { key: '事件 / 接收', value: `${formatAlertTime(event.eventAt)} / ${formatAlertTime(event.receivedAt)}` },
+ { key: '恢复时间', value: formatAlertTime(event.recoveredAt) },
+ { key: '处理人', value: event.handler || '—' }
+ ]} />
+
+
;
@@ -145,8 +154,13 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
const mobileLayout = useMobileLayout();
const query: AlertQuery = useMemo(() => ({ ...Object.fromEntries(Object.entries(filters).filter(([, value]) => value)), limit, offset }), [filters, limit, offset]);
const baseQuery = useMemo(() => ({ ...query, limit: undefined, offset: undefined }), [query]);
+ const summaryQuery = useMemo(() => {
+ const scope = { ...baseQuery };
+ delete scope.status;
+ return scope;
+ }, [baseQuery]);
const eventScope = useMemo(() => queryScopeKey(baseQuery), [baseQuery]);
- const summary = useQuery({ queryKey: ['alert-summary-v2', baseQuery], queryFn: ({ signal }) => api.alertSummaryV2(baseQuery, signal), staleTime: 8_000, gcTime: QUERY_MEMORY.summaryGcTime });
+ const summary = useQuery({ queryKey: ['alert-summary-v2', summaryQuery], queryFn: ({ signal }) => api.alertSummaryV2(summaryQuery, signal), staleTime: 8_000, gcTime: QUERY_MEMORY.summaryGcTime });
const events = useQuery>({ queryKey: ['alert-events-v2', eventScope, limit, offset], queryFn: ({ signal }) => api.alertEventsV2(query, signal), placeholderData: retainPreviousPageWithinScope>(eventScope), staleTime: 5_000, gcTime: QUERY_MEMORY.highVolumeGcTime });
const rows = events.data?.items ?? [];
const selectedID = selection?.scope === eventScope ? selection.id : '';
@@ -174,6 +188,7 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
const sums = summary.data;
const activeFilterCount = Object.values(filters).filter(Boolean).length;
const advancedFilterCount = [filters.ruleId, filters.protocol, filters.dateFrom, filters.dateTo].filter(Boolean).length;
+ const allEventCount = Number(sums?.active ?? 0) + Number(sums?.recovered ?? 0) + Number(sums?.closed ?? 0) + Number(sums?.ignored ?? 0);
const inspector = { const event = detail.data; if (event) action.mutate({ name, event }); }} onClose={closeDetail} />;
return <>
summary.refetch()} /> : null}
{[
- { label: '活跃告警', value: sums?.active, tone: 'active', status: '' },
+ { label: '全部事件', value: allEventCount, tone: 'active', status: '' },
{ label: '未处理', value: sums?.unprocessed, tone: 'unprocessed', status: 'unprocessed' },
{ label: '处理中', value: sums?.processing, tone: 'processing', status: 'processing' },
{ label: '未读通知', value: unread, tone: 'notice', status: 'notice' }
@@ -229,7 +244,7 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
aria-label={mobileLayout ? '告警事件列表,可上下滚动' : undefined}
>
{mobileLayout
- ? {rows.map((event) =>
)}
+ ? {rows.map((event) =>
)}
: setSelection({ scope: eventScope, id })} />}
{events.isFetching ? : null}
{!events.isFetching && !rows.length ? : null}
diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
index 3cd984e9..9f3cdaf9 100644
--- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
+++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts
@@ -177,8 +177,8 @@ describe('V2 production entry', () => {
expect(corePageSources.AlertsPage).toContain('事件信息
');
expect(corePageSources.AlertsPage).not.toContain('');
expect(corePageSources.AlertsPage).toContain(' .semi-card-body > footer {
min-height: 46px;
padding: 6px 12px;
@@ -6703,6 +6727,134 @@
background: #fff;
}
+.v2-alert-focus-card > .semi-card-body {
+ background: linear-gradient(145deg, #fff 0%, #f7faff 100%);
+}
+
+.v2-alert-focus-identity {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ border-bottom: 1px solid #e6ebf2;
+ padding: 13px 14px 12px;
+}
+
+.v2-alert-focus-identity > span {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+}
+
+.v2-alert-focus-identity small,
+.v2-alert-focus-facts small {
+ color: #7e8c9f;
+ font-size: 10px;
+}
+
+.v2-alert-focus-identity strong {
+ overflow: hidden;
+ color: #233b56;
+ font-size: 18px;
+ line-height: 1.2;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-alert-focus-identity code {
+ overflow: hidden;
+ color: #7b899c;
+ font: 10px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-alert-focus-identity > .semi-tag {
+ flex: 0 0 auto;
+ border-radius: 999px;
+ font-size: 9px;
+}
+
+.v2-alert-focus-facts {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ border-bottom: 1px solid #e6ebf2;
+}
+
+.v2-alert-focus-facts > span {
+ display: grid;
+ min-width: 0;
+ gap: 4px;
+ padding: 9px 14px;
+}
+
+.v2-alert-focus-facts > span + span {
+ border-left: 1px solid #e6ebf2;
+}
+
+.v2-alert-focus-facts strong {
+ overflow: hidden;
+ color: #3d5068;
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.v2-alert-focus-card .v2-alert-evidence {
+ padding: 11px 12px 12px;
+}
+
+.v2-alert-technical-collapse.semi-collapse {
+ flex: 0 0 auto;
+ overflow: hidden;
+ border: 1px solid #dfe6ef;
+ border-radius: 10px;
+ background: #fff;
+ box-shadow: 0 3px 12px rgba(31, 53, 80, .035);
+}
+
+.v2-alert-technical-collapse > .semi-collapse-item {
+ margin: 0;
+ border: 0;
+}
+
+.v2-alert-technical-collapse .semi-collapse-header {
+ min-height: 48px;
+ align-items: center;
+ padding: 8px 12px;
+}
+
+.v2-alert-technical-collapse .semi-collapse-header:hover {
+ background: #f7faff;
+}
+
+.v2-alert-technical-heading {
+ display: grid;
+ gap: 2px;
+}
+
+.v2-alert-technical-heading strong {
+ color: #33475f;
+ font-size: 12px;
+}
+
+.v2-alert-technical-heading small {
+ color: #8491a3;
+ font-size: 9px;
+ font-weight: 400;
+}
+
+.v2-alert-technical-collapse .semi-collapse-content {
+ border-top: 1px solid #e7ecf2;
+ background: #fff;
+}
+
+.v2-alert-technical-collapse .semi-collapse-content-wrapper {
+ padding: 0;
+}
+
.v2-alert-detail-card.v2-alert-progress-card > .semi-card-body,
.v2-alert-detail-card.v2-alert-disposition-card > .semi-card-body {
padding: 10px !important;
@@ -7032,7 +7184,7 @@
}
.v2-alert-context-card.semi-card {
- min-height: 104px;
+ min-height: 88px;
}
.v2-alert-context-card > .semi-card-body {
@@ -7040,18 +7192,18 @@
}
.v2-alert-context-card .v2-alert-kpis {
- height: 64px;
- min-height: 64px;
+ height: 54px;
+ min-height: 54px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.v2-alert-context-card .v2-alert-kpis .v2-metric-action,
.v2-alert-context-card .v2-alert-kpis .v2-metric-action-content {
- min-height: 64px;
+ min-height: 54px;
}
.v2-alert-context-card .v2-alert-kpis .v2-metric-action {
- padding: 7px 5px;
+ padding: 5px 4px;
}
.v2-alert-context-card .v2-alert-kpis .v2-metric-action.is-notice {
@@ -7070,18 +7222,18 @@
.v2-alert-context-card .v2-alert-kpis strong {
margin-top: 0;
- font-size: 18px;
+ font-size: 16px;
}
.v2-alert-context-card .v2-alert-secondary-states {
- min-height: 40px;
+ min-height: 34px;
border-top: 1px solid #e4e9f0;
border-left: 0;
padding: 3px 4px;
}
.v2-alert-context-card .v2-alert-secondary-states > .semi-button {
- min-height: 34px;
+ min-height: 28px;
padding: 2px 5px;
}
@@ -7090,7 +7242,7 @@
}
.v2-alert-context-card .v2-alert-secondary-states b {
- font-size: 13px;
+ font-size: 12px;
}
.v2-alert-page.is-events .v2-alert-workspace {
@@ -7106,9 +7258,9 @@
}
.v2-alert-table-card > .semi-card-body > .v2-workspace-panel-header {
- min-height: 50px;
+ min-height: 44px;
align-items: center;
- padding: 6px 8px;
+ padding: 5px 8px;
}
.v2-alert-table-card .v2-workspace-panel-copy > .semi-typography {
@@ -7144,17 +7296,17 @@
}
.v2-alert-mobile-list {
- gap: 8px;
- padding: 8px;
+ gap: 6px;
+ padding: 6px;
}
.v2-alert-mobile-list > .v2-alert-mobile-card.semi-card {
border-radius: 9px;
- contain-intrinsic-size: auto 110px;
+ contain-intrinsic-size: auto 96px;
}
.v2-alert-mobile-card-content {
- padding: 10px 11px;
+ padding: 8px 10px;
}
.v2-alert-mobile-card-content header > strong {
@@ -7175,7 +7327,7 @@
}
.v2-alert-mobile-card-content p {
- margin: 6px 0 7px;
+ margin: 4px 0 5px;
overflow: hidden;
color: #405168;
font-size: 12px;
@@ -7185,15 +7337,15 @@
}
.v2-alert-mobile-card-content dl {
- grid-template-columns: repeat(4, minmax(0, 1fr));
+ grid-template-columns: .92fr .7fr 1.38fr;
gap: 0;
- padding-top: 7px;
+ padding-top: 5px;
}
.v2-alert-mobile-card-content dl > div {
display: block;
min-width: 0;
- min-height: 32px;
+ min-height: 28px;
padding: 0 5px;
}
@@ -7214,7 +7366,7 @@
.v2-alert-mobile-card-content dd {
min-width: 0;
- margin: 3px 0 0;
+ margin: 2px 0 0;
overflow: hidden;
font-size: 10px;
line-height: 13px;
@@ -7228,7 +7380,7 @@
}
.v2-alert-table-card > .semi-card-body > footer:has(.v2-table-pagination-controls) {
- min-height: 50px;
+ min-height: 44px;
flex-direction: row;
align-items: center;
padding: 5px 6px;
@@ -7241,7 +7393,7 @@
.v2-alert-table-card > .semi-card-body > footer .v2-table-pagination-button.semi-button,
.v2-alert-table-card > .semi-card-body > footer .v2-table-pagination-current,
.v2-alert-table-card > .semi-card-body > footer .v2-table-pagination-size.semi-select {
- height: 34px;
+ height: 32px;
}
.v2-alert-rules {