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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FormEvent, useEffect, useMemo, useState } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { AlertEvent, AlertNotification, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, MetricDefinition, Page } from '../../api/types'; import { actionLabels, alertValue, canAct, formatAlertTime, operatorLabels, ruleCondition, severityLabels, statusLabels, thresholdText } from '../domain/alert'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MetricActionButton } from '../shared/MetricActionButton'; import { PageHeader } from '../shared/PageHeader'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { TablePagination } from '../shared/TablePagination'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { detailTriggerRow } from '../shared/detailTriggerRow'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister, canOperate } from '../auth/session'; import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy'; import { useMobileLayout } from '../hooks/useMobileLayout'; import { useSideSheetA11y } from '../hooks/useSideSheetA11y'; type Tab = 'events' | 'rules' | 'notifications'; type Filters = { keyword: string; severity: string; status: string; ruleId: string; protocol: string; dateFrom: string; dateTo: string }; const EMPTY_FILTERS: Filters = { keyword: '', severity: '', status: '', ruleId: '', protocol: '', dateFrom: '', dateTo: '' }; const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT']; const NUMERIC_OPERATORS = ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'between', 'outside']; const BOOLEAN_OPERATORS = ['eq', 'neq', 'changed']; type RuleTemplate = { id: string; title: string; summary: string; draft: AlertRuleInput }; const RULE_TEMPLATES: RuleTemplate[] = [ { id: 'offline-10h', title: '离线超过 10 小时', summary: '全部协议 · 自动恢复 · 每小时最多提醒一次', draft: { id: '', name: '车辆离线超过 10 小时', description: '车辆任一有效来源持续 10 小时未上报时告警。', severity: 'major', valueType: 'numeric', metric: 'freshness_sec', operator: 'gt', threshold: 36_000, thresholdHigh: 0, durationSec: 0, recoveryOperator: 'lte', recoveryThreshold: 300, repeatIntervalSec: 3_600, scopeProtocols: [...PROTOCOLS], scopeVins: [], scopeOems: [], scopeModels: [], scopeCompanies: [], notificationChannels: ['in_app'], enabled: true, version: 0 } }, { id: 'hydrogen-concentration', title: '最高氢浓度', summary: 'GB32960 · 阈值由车型或厂家标准确定', draft: { id: '', name: '最高氢浓度超限', description: '监控 GB32960 燃料电池系统最高氢浓度;保存前须按车型、厂家和安全规范确认百分比阈值。', severity: 'critical', valueType: 'numeric', metric: 'hydrogen_concentration_percent', operator: 'gt', threshold: Number.NaN, thresholdHigh: 0, durationSec: 0, recoveryOperator: '', recoveryThreshold: 0, repeatIntervalSec: 600, scopeProtocols: ['GB32960'], scopeVins: [], scopeOems: [], scopeModels: [], scopeCompanies: [], notificationChannels: ['in_app'], enabled: true, version: 0 } } ]; function SeverityTag({ severity }: Pick) { const color = severity === 'critical' ? 'red' : severity === 'major' ? 'orange' : 'amber'; return {severityLabels[severity]}; } function StatusTag({ status }: Pick) { const color = status === 'unprocessed' ? 'red' : status === 'processing' ? 'blue' : status === 'recovered' ? 'green' : 'grey'; return {statusLabels[status]}; } function AlertEventTable({ rows, selectedID, onSelect }: { rows: AlertEvent[]; selectedID: string; onSelect: (id: string) => void }) { const columns = useMemo(() => [ { title: '', dataIndex: 'selection', width: 42, render: (_: unknown, event: AlertEvent) => onSelect(event.id)} aria-label={`选择 ${event.ruleName}`} /> }, { title: '严重程度', dataIndex: 'severity', width: 90, render: (_: AlertEvent['severity'], event: AlertEvent) => }, { title: '车牌 / VIN', dataIndex: 'plate', width: 142, render: (_: string, event: AlertEvent) =>
{event.plate || '未绑定车牌'}{event.vin}
}, { 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: 'location', width: 180, render: (value: string) => {value || '—'} }, { title: '处理人', dataIndex: 'handler', width: 110, render: (value: string) => value || '—' } ], [onSelect, selectedID]); return event ? detailTriggerRow({ className: selectedID === event.id ? 'is-selected' : '', expanded: selectedID === event.id, label: `查看 ${event.plate || event.vin} ${event.ruleName} 告警详情`, testId: `alert-row-${event.id}`, onOpen: () => onSelect(event.id) }) : ({})} />; } function EventInspector({ event, note, acting, actionError, editable, onNote, onAction, onClose, sheet = false }: { event?: AlertEvent; note: string; acting: boolean; actionError?: string; editable: boolean; onNote: (value: string) => void; onAction: (action: 'acknowledge' | 'close' | 'ignore') => void; onClose: () => void; sheet?: boolean }) { if (!event) return } title="选择告警事件" description="查看触发证据、状态时间线和处置动作。" />; return {sheet ? null :