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 ff325313..316efda7 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, IconSetting } from '@douyinfe/semi-icons'; -import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui'; +import { Button, Card, CardGroup, Collapse, Descriptions, Input, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FormEvent, memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; @@ -129,7 +129,7 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac }); 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="查看触发证据、状态时间线和处置动作。" />; + if (!event) return } title="选择告警事件" description="查看触发证据、状态时间线和处置动作。" />; const actionCount = event.actions?.length ?? 0; return {!sheet ? } aria-label="关闭告警详情" onClick={onClose} />>} /> : null} @@ -155,7 +155,7 @@ function EventInspector({ event, note, acting, actionError, editable, onNote, on {editable ? <>{actionError ? {actionError} : null} onAction('acknowledge')}>确认告警 onAction('close')}>关闭 onAction('ignore')}>忽略> : 当前为只读角色,可查看完整证据与处置记录。} 处理进度{actionCount ? `${actionCount} 条记录` : '待处置'}} headerLine> - {event.actions?.length ? {event.actions.map((item, index) => {item.actor} · }>{actionLabels[item.action] ?? item.action}{item.note ? {item.note} : null})} : } + {event.actions?.length ? {event.actions.map((item, index) => {item.actor} · }>{actionLabels[item.action] ?? item.action}{item.note ? {item.note} : null})} : } {rule.enabled ? '已启用' : '已停用'} )} - {!rules.length ? : null} + {!rules.length ? } title="暂无告警规则" description="新建规则后可从常用模板开始,也可直接配置自定义条件。" /> : null} {!mobileLayout ? editor : null} @@ -602,7 +602,7 @@ function NotificationsWorkspace({ editable }: { editable: boolean }) { {notifications.isPending ? : items.length ? {items.map((item) => {item.title}} headerExtraContent={{item.read ? '已读' : '未读'}} headerLine bodyStyle={{ padding: 0 }}> {item.content} - )} : !notifications.isError ? } title="暂无站内通知" description="告警触发或状态变更后,通知会在这里形成可追溯记录。" /> : null} + )} : !notifications.isError ? } title="暂无站内通知" description="告警触发或状态变更后,通知会在这里形成可追溯记录。" /> : null} 外部通知通道短信、邮件与企业微信均为预留能力3 个 · 未启用}> diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index 71c4cbbf..89897bd4 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -2,14 +2,14 @@ import { IconChevronLeft, IconFilter, IconList, IconMapPin, IconQrCode, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; -import { Button, Card, Empty, Input, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; +import { Button, Card, Input, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; import { useQuery } from '@tanstack/react-query'; import { lazy, memo, Suspense, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { Page, VehicleRealtimeRow } from '../../api/types'; import { FleetMap } from '../map/FleetMap'; -import { EmptyState, InlineError } from '../shared/AsyncState'; +import { EmptyState, InlineError, PanelEmpty } from '../shared/AsyncState'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { TablePagination } from '../shared/TablePagination'; import { ProtocolTag } from '../shared/ProtocolTag'; @@ -196,8 +196,8 @@ function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, mo description={mobile ? '实时数据 · 地址按需解析' : '覆盖全部授权车辆;缺失值显示“—”,地址按需解析'} meta={`${total.toLocaleString('zh-CN')} 辆${mobile ? '' : '车辆'}`} /> - {!mobile ? {loading ? : null}{!loading && !rows.length ? : null} : null} - {mobile ? {rows.map((row) => )}{loading ? : null}{!loading && !rows.length ? : null} : null} + {!mobile ? {loading ? : null}{!loading && !rows.length ? } title="暂无符合条件的车辆" description="调整搜索、协议或在线状态筛选后重试。" /> : null} : null} + {mobile ? {rows.map((row) => )}{loading ? : null}{!loading && !rows.length ? } title="暂无符合条件的车辆" description="调整搜索、协议或在线状态筛选后重试。" /> : null} : null} ; } diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx index 6cfd65ec..37d36381 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx @@ -1,11 +1,11 @@ -import { IconPulse, IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons'; -import { Button, Card, CardGroup, Checkbox, Descriptions, Empty, Input, Progress, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui'; +import { IconAlertTriangle, IconPulse, IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons'; +import { Button, Card, CardGroup, Checkbox, Descriptions, Input, Progress, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { LinkHealth, SourceReadinessRow, VehicleCoverageRow, VehicleLocationSourceEvidence, VehicleSourceDiagnostic } from '../../api/types'; -import { InlineError } from '../shared/AsyncState'; +import { InlineError, PanelEmpty } from '../shared/AsyncState'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet'; import { PlatformTime } from '../shared/PlatformTime'; @@ -408,7 +408,7 @@ function SourceDiagnosticWorkspace() { actions={selected ? } onClick={() => diagnostic.refetch()} disabled={diagnostic.isFetching}>刷新该车 : null} /> {diagnostic.isError ? diagnostic.refetch()} /> : null} - {!selected ? : null} + {!selected ? } title="先选择一辆车" description="将展示所有协议和同协议多终端、推荐原因、上报周期与可审计策略。" /> : null} {selected && diagnostic.isPending ? 正在读取来源证据只查询当前车辆,不会扫描整车队。 : null} {data ? <> @@ -594,18 +594,24 @@ export default function OperationsPage() { 在线覆盖{source.online.toLocaleString('zh-CN')} / {source.total.toLocaleString('zh-CN')}{number(onlineRate)}% 在线{source.missingVehicles ? ` · ${source.missingVehicles.toLocaleString('zh-CN')} 辆待恢复` : ''} 当前证据{source.evidence}建议动作{source.action} ; - })} : : readinessUnavailable ? : } + })} : } title="暂无协议来源" description="当前响应没有可展示的协议来源就绪度。" /> : readinessUnavailable ? } + title="协议来源证据不可用" + description="运行证据仍可查看;恢复来源证据后再判断协议覆盖。" + /> : } 0 ? 'warning' : 'ok'}>{!data ? '读取中' : linkIssueCount > 0 ? `${linkIssueCount} 条异常` : '链路正常'}} />{sortedLinks.length ? sortedLinks.map((item) => { const label = opsLinkLabel(item.name); return {label}{label !== item.name ? {item.name} : null}{item.detail || '无补充信息'}; - }) : } + }) : } title={data ? '暂无链路探针' : '正在读取链路状态'} description={data ? '当前响应没有可展示的数据链路证据。' : '全局健康数据返回后将在这里更新。'} />} 0 ? 'warning' : 'ok'}>{!data ? '读取中' : runtimeIssueCount + capacityIssueCount > 0 ? `${runtimeIssueCount + capacityIssueCount} 项关注` : '配置正常'}} />{data?.runtime.dataMode === 'production' ? '已启用' : '未启用'} }, { key: 'MySQL 写探针', value: {data?.mysqlWritable ? '正常' : '异常'} }, { key: 'TDengine 写探针', value: {data?.tdengineWritable ? '正常' : '异常'} }, { key: '请求超时', value: `${data?.runtime.requestTimeoutMs ?? '—'} ms` }, { key: '高德安全代理', value: {data?.runtime.amapSecurityProxyEnabled ? '服务端代理' : '未启用'} } - ]} />{capacityFindings.length ? {capacityFindings.map((item) => 待处理{item})} : } title="容量检查通过" description="当前没有需要处理的容量风险。" />} + ]} />{capacityFindings.length ? {capacityFindings.map((item) => 待处理{item})} : } title="容量检查通过" description="当前没有需要处理的容量风险。" />} > : null} ; diff --git a/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx b/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx index f52f035d..ee5ddbb4 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx @@ -1,10 +1,10 @@ import { IconAlertTriangle, IconChevronDown, IconChevronRight, IconChevronUp, IconClose, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; -import { Button, Card, Empty, Input, RadioGroup, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; +import { Button, Card, Input, RadioGroup, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useDeferredValue, useEffect, useMemo, useState } from 'react'; import { api } from '../../api/client'; import type { ReconciliationIssue, ReconciliationSummary } from '../../api/types'; -import { InlineError } from '../shared/AsyncState'; +import { InlineError, PanelEmpty } from '../shared/AsyncState'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { PlatformTime } from '../shared/PlatformTime'; import { TablePagination } from '../shared/TablePagination'; @@ -268,6 +268,13 @@ export default function ReconciliationCenter() { const [selectedID, setSelectedID] = useState(''); const [filtersCollapsed, setFiltersCollapsed] = useState(true); const [trendExpanded, setTrendExpanded] = useState(false); + const resetFilters = () => { + setKeyword(''); + setStatus('active'); + setSeverity('all'); + setRuleCode('all'); + setOffset(0); + }; const closeDetail = () => setSelectedID(''); const openDetail = (id: string) => { setTrendExpanded(false); @@ -439,7 +446,14 @@ export default function ReconciliationCenter() { )}} {issues.isPending ? : null} - {!issues.isPending && !issueRows.length ? : null} + {!issues.isPending && !issueRows.length ? } + title="当前筛选条件没有差异" + description="当前范围没有待复核证据,可重置筛选返回活跃差异。" + action={} onClick={resetFilters}>重置筛选} + /> : null} diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx index 13a76373..7020d2bc 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx @@ -3,7 +3,7 @@ import { IconChevronLeft, IconChevronRight, IconClose, IconDownload, IconEyeClosed, IconEyeOpened, IconList, IconMapPin, IconPause, IconPlay, IconRefresh, IconRoute, IconSearch } from '@douyinfe/semi-icons'; -import { Button, Card, Descriptions, Empty, Input, List, Select, Slider, Tag } from '@douyinfe/semi-ui'; +import { Button, Card, Descriptions, Input, List, Select, Slider, Tag } from '@douyinfe/semi-ui'; import { FormEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; @@ -11,7 +11,7 @@ import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../. import { protocolDisplayLabel } from '../domain/protocols'; import { buildTrackRailSelection, compactTrackWindowLabel, downloadTrackCsv, formatDuration, sampledEventIndex, TRACK_ADDRESS_SETTLE_MS, trackAddressCoordinate, trackPlaybackInterval, trackPointMileageAvailable, type TrackAddressCoordinate } from '../domain/track'; import { TrackMap } from '../map/TrackMap'; -import { InlineError, WorkspaceEmptyGuide } from '../shared/AsyncState'; +import { InlineError, PanelEmpty, WorkspaceEmptyGuide } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { ProtocolTag } from '../shared/ProtocolTag'; import { SegmentedTabs } from '../shared/SegmentedTabs'; @@ -310,9 +310,9 @@ const TrackRail = memo(function TrackRail({ draft, track, summaryItems, loading, /> - {!track ? } title="选择车辆后查询轨迹" description="停留点、轨迹事件与行程证据会在这里统一呈现。" /> : null} - {track && tab === 'stops' ? {track.stops.map((stop, index) => onSelectIndex(stop.sampledIndex)}>{index + 1}{dateTime(stop.startTime)}停留 {formatDuration(stop.durationSeconds)} · {stop.pointCount} 个点{time(stop.endTime)})}{!track.stops.length ? } title="当前没有停留点" description="时间窗内没有超过 3 分钟的连续停留。" /> : null} : null} - {track && tab === 'events' ? {track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return onSelectIndex(sampled)}>{index + 1}{event.title}{dateTime(event.time)}{number(event.speedKmh, 0)} km/h; })}{!track.events.length ? } title="当前没有事件点" description="时间窗内没有识别到启停、急加速或异常间隔。" /> : null} : null} + {!track ? } title="选择车辆后查询轨迹" description="停留点、轨迹事件与行程证据会在这里统一呈现。" /> : null} + {track && tab === 'stops' ? {track.stops.map((stop, index) => onSelectIndex(stop.sampledIndex)}>{index + 1}{dateTime(stop.startTime)}停留 {formatDuration(stop.durationSeconds)} · {stop.pointCount} 个点{time(stop.endTime)})}{!track.stops.length ? } title="当前没有停留点" description="时间窗内没有超过 3 分钟的连续停留。" /> : null} : null} + {track && tab === 'events' ? {track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return onSelectIndex(sampled)}>{index + 1}{event.title}{dateTime(event.time)}{number(event.speedKmh, 0)} km/h; })}{!track.events.length ? } title="当前没有事件点" description="时间窗内没有识别到启停、急加速或异常间隔。" /> : null} : null} {track && tab === 'overview' ? : null} diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index ac9b8442..d2fb4c4e 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -16,7 +16,7 @@ import { formatZhNumber, vehicleOperationStatusLabels } from '../domain/formatte import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols'; import { isValidAMapCoordinate } from '../../integrations/amap'; import { FleetMap } from '../map/FleetMap'; -import { InlineError, PageLoading } from '../shared/AsyncState'; +import { InlineError, PageLoading, PanelEmpty } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { PlatformTime } from '../shared/PlatformTime'; import { ProtocolTag } from '../shared/ProtocolTag'; @@ -134,7 +134,7 @@ function Events({ detail }: { detail: VehicleDetail }) { className="v2-event-list" dataSource={events} split={false} - emptyContent={} + emptyContent={} title="暂无可用事件证据" description="车辆产生质量提醒或来源状态变化后会显示在这里。" />} renderItem={(event, index) => {event.tone === 'success' ? : } {event.title}{event.detail}{eventTimeLabel(event.time)} @@ -284,7 +284,7 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon : error ? 最新遥测不可用{error} : visibleMetrics.length === 0 - ? + ? } title="暂无可展示字段" description={`该协议最近 ${data?.scannedFrames ?? 0} 帧没有可展示的标量遥测。`} /> : mobileLayout ? diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx index f9b15f32..73d801ef 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { IconArrowRight, IconBox, IconChevronRight, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; -import { Button, Card, Empty, Input, Table, Tag } from '@douyinfe/semi-ui'; +import { Button, Card, Input, Table, Tag } from '@douyinfe/semi-ui'; import { type FormEvent, lazy, Suspense, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { api } from '../../api/client'; @@ -10,6 +10,7 @@ import { canAdminister } from '../auth/session'; import { formatZhNumber } from '../domain/formatters'; import { useMobileLayout } from '../hooks/useMobileLayout'; import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy'; +import { PanelEmpty } from '../shared/AsyncState'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet'; import { ProtocolTag } from '../shared/ProtocolTag'; @@ -405,7 +406,16 @@ export default function VehicleSearchWorkspace() { loading={candidates.isFetching} empty={null} /> - : } + : } + title={deferredKeyword ? '没有匹配车辆' : '暂无授权车辆'} + description={deferredKeyword ? '当前范围没有结果,可清除搜索返回完整车辆目录。' : '管理员分配车辆权限后会显示在这里。'} + action={deferredKeyword + ? } onClick={() => { setKeyword(''); setDraftKeyword(''); setPage(1); setCandidatesOpen(false); }}>清除搜索 + : } loading={candidates.isFetching} onClick={() => candidates.refetch()}>刷新车辆目录} + />} {total > 0 ? : null} {syncOpen ? 正在加载主档同步工具…}> setSyncOpen(false)} /> : 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 0aa635eb..7d6b53dc 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -153,17 +153,17 @@ describe('V2 production entry', () => { expect(corePageSources.MonitorPage).not.toContain('
{actionError}
当前为只读角色,可查看完整证据与处置记录。
{item.note}
{item.content}
{source.evidence}
{source.action}
{item.detail || '无补充信息'}
{item}
{event.detail}