feat: unify access decision rail

This commit is contained in:
lingniu
2026-07-19 09:25:06 +08:00
parent 8372ca5349
commit 39c828ed32
4 changed files with 156 additions and 127 deletions

View File

@@ -7,13 +7,13 @@ import { api } from '../../api/client';
import type { AccessProtocolStatus, AccessQuery, AccessSummary, AccessThresholdConfig, AccessThresholdUpdate, AccessUnresolvedIdentity, AccessVehicleRow, Page } from '../../api/types';
import { accessIssueSummary, accessRowsToCSV, formatAccessTime, formatSeconds, updateProtocolThreshold } from '../domain/access';
import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState';
import { MetricActionButton } from '../shared/MetricActionButton';
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
import { TablePagination } from '../shared/TablePagination';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
import { detailTriggerRow } from '../shared/detailTriggerRow';
@@ -365,6 +365,34 @@ export default function AccessPage() {
const page = Math.floor(offset / limit) + 1; const totalPages = Math.max(1, Math.ceil((vehiclesQuery.data?.total ?? 0) / limit)); const summary = summaryQuery.data;
const activeFilterCount = Object.values(criteria).filter(Boolean).length;
const refresh = () => Promise.all([summaryQuery.refetch(), vehiclesQuery.refetch(), ...(editable ? [unresolvedQuery.refetch(), thresholdQuery.refetch()] : [])]);
const accessMetric = (
label: string,
value: number,
connectionState: string,
options: Pick<WorkspaceQueueMetricRailItem, 'note' | 'tone' | 'emphasis'>
): WorkspaceQueueMetricRailItem => {
const formattedValue = value.toLocaleString('zh-CN');
return {
label,
value: formattedValue,
...options,
active: criteria.connectionState === connectionState,
ariaLabel: `筛选${label},共 ${formattedValue}`,
onClick: () => apply({ ...criteria, connectionState })
};
};
const attentionVehicles = Math.max(0, (summary?.totalVehicles ?? 0) - (summary?.healthyVehicles ?? 0));
const accessMetrics: WorkspaceQueueMetricRailItem[] = mobileLayout
? [
accessMetric('需关注', attentionVehicles, 'attention', { note: '异常与缺失', tone: 'warning', emphasis: 'primary' }),
accessMetric('全部车辆', summary?.totalVehicles ?? 0, '', { note: '权威主车辆', tone: 'neutral', emphasis: 'primary' })
]
: [
accessMetric('需关注', attentionVehicles, 'attention', { note: '异常、离线或缺失', tone: 'warning', emphasis: 'primary' }),
accessMetric('全部车辆', summary?.totalVehicles ?? 0, '', { note: '权威主车辆口径', tone: 'neutral', emphasis: 'primary' }),
accessMetric('尚无来源', summary?.neverReported ?? 0, 'not_connected', { note: '待核对设备映射', tone: 'warning', emphasis: 'secondary' }),
accessMetric('资料待维护', summary?.masterDataIncompleteVehicles ?? 0, 'master_data', { note: '品牌、车型或来源', tone: (summary?.masterDataIncompleteVehicles ?? 0) > 0 ? 'warning' : 'success', emphasis: 'secondary' })
];
const scrollAccessTable = (event: KeyboardEvent<HTMLDivElement>) => {
if (mobileLayout || event.currentTarget !== event.target || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;
const tableScroller = event.currentTarget.scrollWidth > event.currentTarget.clientWidth
@@ -465,17 +493,17 @@ export default function AccessPage() {
title={scopeLabels[criteria.connectionState] || '车辆接入清单'}
description="缺席协议仅表示当前未发现;优先核对异常、离线与资料差异"
actionsClassName="v2-access-table-actions"
actions={mobileLayout ? null : <><ProtocolCoverage summary={summary} /><Button theme="light" icon={<IconDownload />} onClick={() => downloadRows(rows)} disabled={!rows.length}></Button></>}
actions={mobileLayout ? null : <Button theme="light" icon={<IconDownload />} onClick={() => downloadRows(rows)} disabled={!rows.length}></Button>}
/>
<nav className="v2-access-status-tabs" aria-label="接入差异筛选">{[
{ label: '主车辆', value: summary?.totalVehicles ?? 0, tone: 'all', connectionState: '', hint: '全部' },
{ label: '需关注', value: Math.max(0, (summary?.totalVehicles ?? 0) - (summary?.healthyVehicles ?? 0)), tone: 'attention', connectionState: 'attention' },
{ label: '资料待维护', value: summary?.masterDataIncompleteVehicles ?? 0, tone: 'incomplete', connectionState: 'master_data' },
{ label: '尚无来源', value: summary?.neverReported ?? 0, tone: 'never', connectionState: 'not_connected' }
].map((item) => {
const value = Number(item.value).toLocaleString('zh-CN');
return <MetricActionButton key={item.label} label={item.label} value={value} hint={item.hint} tone={item.tone} active={criteria.connectionState === item.connectionState} ariaLabel={`筛选${item.label},共 ${value}`} onClick={() => apply({ ...criteria, connectionState: item.connectionState })} />;
})}</nav>
<nav className="v2-access-status-tabs" aria-label="接入差异筛选">
<WorkspaceMetricRail
variant="queue"
className="v2-access-metric-rail"
ariaLabel="接入差异概览"
items={accessMetrics}
context={!mobileLayout ? <ProtocolCoverage summary={summary} /> : undefined}
/>
</nav>
<div
className={`v2-access-table-scroll-v3${mobileLayout ? ' is-mobile-scroll' : ''}`}
tabIndex={0}