@@ -18,7 +18,7 @@ const EMPTY_FILTERS = { keyword: '', protocol: '', oem: '', connectionState: '',
type Filters = typeof EMPTY_FILTERS ;
const connectionLabels : Record < AccessVehicleRow [ 'connectionState' ] , string > = {
healthy : '三协议 正常' , degraded : '协议离线 ' , incomplete : '接入不完整 ' , offline : '全部 离线' , not_connected : '尚未接入 '
healthy : '已接来源 正常' , degraded : '部分来源异常 ' , incomplete : '资料待维护 ' , offline : '已接来源 离线' , not_connected : '尚无来源 '
} ;
function statusByProtocol ( row : AccessVehicleRow , protocol : string ) {
@@ -34,7 +34,7 @@ function compactTime(value: string) {
function ProtocolState ( { status , detailed = false } : { status? : AccessProtocolStatus ; detailed? : boolean } ) {
const state = ! status ? . connected ? 'missing' : status . onlineState ;
const label = state === 'missing' ? '未接入 ' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报' ;
const label = state === 'missing' ? '无来源 ' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报' ;
if ( detailed ) {
return < article className = { ` v2-access-protocol-detail is- ${ state } ` } >
< header > < strong > { status ? . protocol } < / strong > < span > < i / > { label } < / span > < / header >
@@ -46,26 +46,25 @@ function ProtocolState({ status, detailed = false }: { status?: AccessProtocolSt
< div > < dt > 上 报 间 隔 < / dt > < dd > { formatSeconds ( status ? . reportIntervalSec ) } < / dd > < / div >
< div > < dt > 数 据 延 迟 < / dt > < dd className = { status ? . delayAbnormal ? 'is-danger' : '' } > { formatSeconds ( status ? . dataDelaySec ) } < / dd > < / div >
< / dl >
< p > { status ? . firstSeenEvidence || '应接协议尚未形成实时快照 ' } < / p >
< p > { status ? . firstSeenEvidence || '当前未发现该协议来源 ' } < / p >
< / article > ;
}
return < div className = { ` v2-access-protocol-cell is- ${ state } ` } title = { status ? . latestReceivedAt ? ` 最新上报: ${ formatAccessTime ( status . latestReceivedAt ) } ` : '应接协议尚未形成实时快照 ' } >
return < div className = { ` v2-access-protocol-cell is- ${ state } ` } title = { status ? . latestReceivedAt ? ` 最新上报: ${ formatAccessTime ( status . latestReceivedAt ) } ` : '当前未发现该协议来源 ' } >
< span > < i / > { label } < / span >
< strong > { status ? . connected ? compactTime ( status . latestReceivedAt ) : '等待接入 ' } < / strong >
< small > { status ? . provider || ( status ? . connected ? '接入方未维护' : '无实时快照 ' ) } < / small >
< strong > { status ? . connected ? compactTime ( status . latestReceivedAt ) : '— ' } < / strong >
< small > { status ? . provider || ( status ? . connected ? '接入方未维护' : '未发现来源 ' ) } < / small >
< / div > ;
}
function ConnectionState ( { row } : { row : AccessVehicleRow } ) {
return < div className = { ` v2-access-connection is- ${ row . connectionState } ` } > < strong > { connectionLabels [ row . connectionState ] } < / strong > < span > { row . actualProtocols . length } / { row . expectedProtocols . length } 已 接 入 < / span > < / div > ;
return < div className = { ` v2-access-connection is- ${ row . connectionState } ` } > < strong > { connectionLabels [ row . connectionState ] } < / strong > < span > { row . actualProtocols . length } 个 真 实 来 源 < / span > < / div > ;
}
function ProtocolCoverage ( { summary } : { summary? : AccessSummary } ) {
return < div className = "v2-access-protocol-coverage" aria-label = "三协议接入 概览" >
return < div className = "v2-access-protocol-coverage" aria-label = "真实协议来源 概览" >
{ PROTOCOLS . map ( ( protocol ) = > {
const actual = summary ? . protocols . find ( ( item ) = > item . name === protocol ) ;
const total = summary ? . totalVehicles ? ? 0 ;
return < span key = { protocol } > < b > { protocol } < / b > < em > { ( actual ? . total ? ? 0 ) . toLocaleString ( 'zh-CN' ) } / { total . toLocaleString ( 'zh-CN' ) } < / em > < / span > ;
return < span key = { protocol } > < b > { protocol } < / b > < em > { ( actual ? . total ? ? 0 ) . toLocaleString ( 'zh-CN' ) } 辆 < / em > < / span > ;
} ) }
< / div > ;
}
@@ -73,7 +72,7 @@ function ProtocolCoverage({ summary }: { summary?: AccessSummary }) {
function VehicleInspector ( { row , onClose } : { row : AccessVehicleRow ; onClose : ( ) = > void } ) {
return < aside className = "v2-access-inspector-v3" >
< header > < div > < strong > { row . plate || '未绑定车牌' } < / strong > < span > { row . vin } < / span > < / div > < button type = "button" onClick = { onClose } aria-label = "关闭车辆接入详情" > < IconClose / > < / button > < / header >
< section className = "v2-access-inspector-summary" > < div > < span > 品 牌 / 车 型 < / span > < strong > { [ row . oem , row . model ] . filter ( Boolean ) . join ( ' / ' ) || '未维护' } < / strong > < / div > < div > < span > 应 接 协 议 < / span > < strong > { row . expected Protocols. join ( ' / ' ) } < / strong > < / div > < div > < span > 实 际 接 入 < / span > < strong > { row . actualProtocol s. length ? row . actualProtocol s. join ( ' / ' ) : '尚未接入 ' } < / strong > < / div > < div > < span > 综 合 状 态 < / span > < ConnectionState row = { row } / > < / div > < / section >
< section className = "v2-access-inspector-summary" > < div > < span > 品 牌 / 车 型 < / span > < strong > { [ row . oem , row . model ] . filter ( Boolean ) . join ( ' / ' ) || '未维护' } < / strong > < / div > < div > < span > 真 实 接 入 来 源 < / span > < strong > { row . actualProtocols . length ? row . actual Protocols. join ( ' / ' ) : '尚无来源' } < / strong > < / div > < div > < span > 资 料 状 态 < / span > < strong > { row . masterDataIssue s. length ? row . masterDataIssue s. join ( '; ' ) : '已维护 ' } < / strong > < / div > < div > < span > 综 合 状 态 < / span > < ConnectionState row = { row } / > < / div > < / section >
< div className = "v2-access-protocol-details" > { PROTOCOLS . map ( ( protocol ) = > < ProtocolState key = { protocol } status = { statusByProtocol ( row , protocol ) } detailed / > ) } < / div >
< footer > < span > { row . expectationEvidence } < / span > < Link to = { ` /vehicles/ ${ encodeURIComponent ( row . vin ) } ` } > 查 看 车 辆 详 情 < / Link > < / footer >
< / aside > ;
@@ -119,15 +118,15 @@ export default function AccessPage() {
const refresh = ( ) = > Promise . all ( [ summaryQuery . refetch ( ) , vehiclesQuery . refetch ( ) , . . . ( editable ? [ unresolvedQuery . refetch ( ) , thresholdQuery . refetch ( ) ] : [ ] ) ] ) ;
return < div className = "v2-access-page v2-access-page-v3" >
< header className = "v2-access-heading" > < div > < h2 > 车 辆 接 入 管 理 < / h2 > < p > 以 主 车 辆 为 对 象 , 对 照 应 接 协 议 、 实 际 接 入 和 各 协 议 最 新 上 报 时 间 < / p > < / div > < div > < span > 数 据 时 间 { summary ? . asOf ? formatAccessTime ( summary . asOf ) : '—' } < / span > < button type = "button" onClick = { ( ) = > void refresh ( ) } > < IconRefresh / > 刷 新 < / button > < / div > < / header >
< header className = "v2-access-heading" > < div > < h2 > 车 辆 接 入 管 理 < / h2 > < p > 只 展 示 车 辆 真 实 存 在 的 来 源 、 在 线 健 康 和 待 维 护 资 料 ; 未 接 业 务 系 统 前 不 推 断 应 接 协 议 < / p > < / div > < div > < span > 数 据 时 间 { summary ? . asOf ? formatAccessTime ( summary . asOf ) : '—' } < / span > < button type = "button" onClick = { ( ) = > void refresh ( ) } > < IconRefresh / > 刷 新 < / button > < / div > < / header >
< button type = "button" className = "v2-mobile-filter-toggle" aria-expanded = { ! filtersCollapsed } onClick = { ( ) = > setFiltersCollapsed ( ( value ) = > ! value ) } > < span > < b > 筛 选 条 件 < / b > < small > { Object . values ( criteria ) . filter ( Boolean ) . length ? ` 已启用 ${ Object . values ( criteria ) . filter ( Boolean ) . length } 项 ` : '全部主车辆' } < / small > < / span > < em > { filtersCollapsed ? '展开' : '收起' } < / em > < / button >
< form className = { ` v2-access-filter-v3 ${ filtersCollapsed ? ' is-mobile-collapsed' : '' } ` } onSubmit = { submit } > < label className = "is-search" > < span > 车 辆 < / span > < div > < IconSearch / > < input aria-label = "车辆" value = { draft . keyword } onChange = { ( event ) = > setDraft ( { . . . draft , keyword : event.target.value } ) } placeholder = "车牌 / VIN" / > < / div > < / label > < label > < span > 接 入 状 态 < / span > < select aria-label = "接入状态" value = { draft . connectionState } onChange = { ( event ) = > setDraft ( { . . . draft , connectionState : event.target.value } ) } > < option value = "" > 全 部 状 态 < / option > < option value = "attention" > 有 接 入 差 异 < / option > < option value = "healthy" > 三 协 议 正 常 < / option > < option value = "incomplete" > 接 入 不 完 整 < / option > < option value = "degraded" > 协 议 离 线 < / option > < option value = "offline" > 全 部 离 线 < / option > < option value = "not_connected" > 尚 未 接 入 < / option > < / select > < / label > < label > < span > 关 注 协 议 < / span > < select aria-label = "关注协议" value = { draft . protocol } onChange = { ( event ) = > setDraft ( { . . . draft , protocol : event.target.value } ) } > < option value = "" > 全 部 协 议 < / option > { PROTOCOLS . map ( ( item ) = > < option key = { item } > { item } < / option > ) } < / select > < / label > < label > < span > 车 辆 品 牌 < / span > < select aria-label = "车辆品牌" value = { draft . oem } onChange = { ( event ) = > setDraft ( { . . . draft , oem : event.target.value } ) } > < option value = "" > 全 部 品 牌 < / option > { summary ? . oems . filter ( ( item ) = > item . name !== '未维护' ) . map( ( item ) = > < option key = { item . name } > { item . name } < / option > ) } < / select > < / label > < button className = "v2-primary-button" type = "submit" > 查 询 < / button > < button className = "v2-secondary-button" type = "button" onClick = { ( ) = > apply ( EMPTY_FILTERS ) } > 重 置 < / button > < / form >
< form className = { ` v2-access-filter-v3 ${ filtersCollapsed ? ' is-mobile-collapsed' : '' } ` } onSubmit = { submit } > < label className = "is-search" > < span > 车 辆 < / span > < div > < IconSearch / > < input aria-label = "车辆" value = { draft . keyword } onChange = { ( event ) = > setDraft ( { . . . draft , keyword : event.target.value } ) } placeholder = "车牌 / VIN" / > < / div > < / label > < label > < span > 接 入 状 态 < / span > < select aria-label = "接入状态" value = { draft . connectionState } onChange = { ( event ) = > setDraft ( { . . . draft , connectionState : event.target.value } ) } > < option value = "" > 全 部 状 态 < / option > < option value = "attention" > 需 关 注 < / option > < option value = "healthy" > 已 接 来 源 正 常 < / option > < option value = "incomplete" > 资 料 待 维 护 < / option > < option value = "degraded" > 部 分 来 源 异 常 < / option > < option value = "offline" > 已 接 来 源 离 线 < / option > < option value = "not_connected" > 尚 无 来 源 < / option > < / select > < / label > < label > < span > 真 实 协 议 < / span > < select aria-label = "关注协议" value = { draft . protocol } onChange = { ( event ) = > setDraft ( { . . . draft , protocol : event.target.value } ) } > < option value = "" > 全 部 协 议 < / option > { PROTOCOLS . map ( ( item ) = > < option key = { item } > { item } < / option > ) } < / select > < / label > < label > < span > 车 辆 品 牌 < / span > < select aria-label = "车辆品牌" value = { draft . oem } onChange = { ( event ) = > setDraft ( { . . . draft , oem : event.target.value } ) } > < option value = "" > 全 部 品 牌 < / option > { summary ? . oems . map ( ( item ) = > < option key = { item . name } > { item . name } < / option > ) } < / select > < / label > < button className = "v2-primary-button" type = "submit" > 查 询 < / button > < button className = "v2-secondary-button" type = "button" onClick = { ( ) = > apply ( EMPTY_FILTERS ) } > 重 置 < / button > < / form >
{ summaryQuery . isError ? < InlineError message = { summaryQuery . error instanceof Error ? summaryQuery . error . message : '接入汇总读取失败' } onRetry = { ( ) = > summaryQuery . refetch ( ) } / > : null }
< section className = "v2-access-kpis-v3" > { [
[ '主车辆' , summary ? . totalVehicles ? ? 0 , 'all' , '' ] , [ '有接入差异 ' , Math . max ( 0 , ( summary ? . totalVehicles ? ? 0 ) - ( summary ? . healthyVehicles ? ? 0 ) ) , 'attention' , 'attention' ] , [ '全部离线 ' , summary ? . offlin eVehicles ? ? 0 , 'offline' , 'offlin e' ] , [ '尚未接入 ' , summary ? . neverReported ? ? 0 , 'never' , 'not_connected' ]
[ '主车辆' , summary ? . totalVehicles ? ? 0 , 'all' , '' ] , [ '需关注 ' , Math . max ( 0 , ( summary ? . totalVehicles ? ? 0 ) - ( summary ? . healthyVehicles ? ? 0 ) ) , 'attention' , 'attention' ] , [ '资料待维护 ' , summary ? . incomplet eVehicles ? ? 0 , 'incomplete' , 'incomplet e' ] , [ '尚无来源 ' , summary ? . neverReported ? ? 0 , 'never' , 'not_connected' ]
] . map ( ( [ label , value , tone , connectionState ] ) = > < button key = { String ( label ) } className = { ` is- ${ tone } ` } type = "button" onClick = { ( ) = > apply ( { . . . criteria , connectionState : String ( connectionState ) } ) } > < small > { label } < / small > < strong > { Number ( value ) . toLocaleString ( 'zh-CN' ) } < / strong > { label === '主车辆' ? < em > 车 辆 主 档 < / em > : null } < / button > ) } < / section >
{ vehiclesQuery . isError ? < InlineError message = { vehiclesQuery . error instanceof Error ? vehiclesQuery . error . message : '接入车辆读取失败' } onRetry = { ( ) = > vehiclesQuery . refetch ( ) } / > : null }
< div className = { ` v2-access-workspace-v3 ${ selected ? 'is-inspector-open' : '' } ` } > < section className = "v2-access-table-v3" > < header > < div className = "v2-access-table-title" > < strong > 车 辆 协 议 接 入 差 异 < / strong > < span > 优 先 展 示 应 接 与 实 接 差 异 ; 时 间 为 各 协 议 最 后 接 收 时 间 < / span > < / div > < div className = "v2-access-table-actions" > < ProtocolCoverage summary = { summary } / > < button type = "button" onClick = { ( ) = > downloadRows ( rows ) } disabled = { ! rows . length } > < IconDownload / > 导 出 当 前 页 < / button > < / div > < / header > < div className = "v2-access-table-scroll-v3" > { mobileLayout ? < div className = "v2-access-mobile-list" > { rows . map ( ( row ) = > < button type = "button" key = { row . vin } className = { selected ? . vin === row . vin ? 'is-selected' : '' } onClick = { ( ) = > setSelectedVIN ( row . vin ) } > < header > < div > < strong > { row . plate || '未绑定车牌' } < / strong > < span > { row . vin } < / span > < / div > < ConnectionState row = { row } / > < / header > < p > { row . oem || '品牌未维护' } · { row . model || row . company || '车型未维护' } < / p > < div > { PROTOCOLS . map ( ( protocol ) = > < ProtocolState key = { protocol } status = { statusByProtocol ( row , protocol ) } / > ) } < / div > < / button > ) } < / div > : < table > < thead > < tr > < th > 车 辆 < / th > < th > 品 牌 / 车 型 < / th > < th > 应 接 协 议 < / th > { PROTOCOLS . map ( ( item ) = > < th key = { item } > { item } < / th > ) } < th > 综 合 状 态 < / th > < / tr > < / thead > < tbody > { rows . map ( ( row ) = > < tr key = { row . vin } data-testid = { ` access-row- ${ row . vin } ` } tabIndex = { 0 } className = { selected ? . vin === row . vin ? 'is-selected' : '' } onClick = { ( ) = > setSelectedVIN ( row . vin ) } onKeyDown = { ( event ) = > { if ( event . key === 'Enter' || event . key === ' ' ) setSelectedVIN ( row . vin ) ; } } > < td > < strong > { row . plate || '未绑定车牌' } < / strong > < span > { row . vin } < / span > < / td > < td > < strong > { row . oem || '品牌未维护' } < / strong > < span > { row . model || row . company || '车型未维护' } < / span > < / td > < td > < b > { row . expected Protocols. length } 项 < / b > < span > { row . expected Protocols. join ( ' / ' ) } < / span > < / td > { PROTOCOLS . map ( ( protocol ) = > < td key = { protocol } > < ProtocolState status = { statusByProtocol ( row , protocol ) } / > < / td > ) } < td > < ConnectionState row = { row } / > < / td > < / tr > ) } < / tbody > < / table > } { vehiclesQuery . isFetching ? < div className = "v2-access-loading" > < i / > 正 在 更 新 车 辆 接 入 状 态 … < / div > : null } { ! vehiclesQuery . isFetching && ! rows . length ? < div className = "v2-access-empty" > 当 前 筛 选 条 件 没 有 车 辆 < / div > : null } < / div > < footer > < span > 第 { page } / { totalPages } 页 , 共 { ( vehiclesQuery . data ? . total ? ? 0 ) . toLocaleString ( 'zh-CN' ) } 辆 主 车 辆 < / span > < div > < button type = "button" disabled = { page <= 1 } onClick = { ( ) = > setOffset ( Math . max ( 0 , offset - limit ) ) } > 上 一 页 < / button > < button type = "button" disabled = { page >= totalPages } onClick = { ( ) = > setOffset ( offset + limit ) } > 下 一 页 < / button > < select aria-label = "每页数量" value = { limit } onChange = { ( event ) = > { setLimit ( Number ( event . target . value ) ) ; setOffset ( 0 ) ; } } > < option value = "20" > 20 辆 / 页 < / option > < option value = "50" > 50 辆 / 页 < / option > < option value = "100" > 100 辆 / 页 < / option > < / select > < / div > < / footer > < / section > { selected ? < VehicleInspector row = { selected } onClose = { ( ) = > setSelectedVIN ( '' ) } / > : null } < / div >
< div className = { ` v2-access-workspace-v3 ${ selected ? 'is-inspector-open' : '' } ` } > < section className = "v2-access-table-v3" > < header > < div className = "v2-access-table-title" > < strong > 车 辆 真 实 接 入 来 源 < / strong > < span > 缺 席 协 议 只 表 示 “ 当 前 未 发 现 ” , 不 会 被 推 断 成 应 接 缺 失 ; 时 间 为 各 来 源 最 后 接 收 时 间 < / span > < / div > < div className = "v2-access-table-actions" > < ProtocolCoverage summary = { summary } / > < button type = "button" onClick = { ( ) = > downloadRows ( rows ) } disabled = { ! rows . length } > < IconDownload / > 导 出 当 前 页 < / button > < / div > < / header > < div className = "v2-access-table-scroll-v3" > { mobileLayout ? < div className = "v2-access-mobile-list" > { rows . map ( ( row ) = > < button type = "button" key = { row . vin } className = { selected ? . vin === row . vin ? 'is-selected' : '' } onClick = { ( ) = > setSelectedVIN ( row . vin ) } > < header > < div > < strong > { row . plate || '未绑定车牌' } < / strong > < span > { row . vin } < / span > < / div > < ConnectionState row = { row } / > < / header > < p > { row . oem || '品牌未维护' } · { row . model || row . company || '车型未维护' } < / p > < div > { PROTOCOLS . map ( ( protocol ) = > < ProtocolState key = { protocol } status = { statusByProtocol ( row , protocol ) } / > ) } < / div > < / button > ) } < / div > : < table > < thead > < tr > < th > 车 辆 < / th > < th > 品 牌 / 车 型 < / th > < th > 真 实 来 源 < / th > { PROTOCOLS . map ( ( item ) = > < th key = { item } > { item } < / th > ) } < th > 综 合 状 态 < / th > < / tr > < / thead > < tbody > { rows . map ( ( row ) = > < tr key = { row . vin } data-testid = { ` access-row- ${ row . vin } ` } tabIndex = { 0 } className = { selected ? . vin === row . vin ? 'is-selected' : '' } onClick = { ( ) = > setSelectedVIN ( row . vin ) } onKeyDown = { ( event ) = > { if ( event . key === 'Enter' || event . key === ' ' ) setSelectedVIN ( row . vin ) ; } } > < td > < strong > { row . plate || '未绑定车牌' } < / strong > < span > { row . vin } < / span > < / td > < td > < strong > { row . oem || '品牌未维护' } < / strong > < span > { row . model || row . company || '车型未维护' } < / span > < / td > < td > < b > { row . actual Protocols. length } 个 < / b > < span > { row . actual Protocols. join ( ' / ' ) || '尚无来源' } < / span > < / td > { PROTOCOLS . map ( ( protocol ) = > < td key = { protocol } > < ProtocolState status = { statusByProtocol ( row , protocol ) } / > < / td > ) } < td > < ConnectionState row = { row } / > < / td > < / tr > ) } < / tbody > < / table > } { vehiclesQuery . isFetching ? < div className = "v2-access-loading" > < i / > 正 在 更 新 车 辆 接 入 状 态 … < / div > : null } { ! vehiclesQuery . isFetching && ! rows . length ? < div className = "v2-access-empty" > 当 前 筛 选 条 件 没 有 车 辆 < / div > : null } < / div > < footer > < span > 第 { page } / { totalPages } 页 , 共 { ( vehiclesQuery . data ? . total ? ? 0 ) . toLocaleString ( 'zh-CN' ) } 辆 主 车 辆 < / span > < div > < button type = "button" disabled = { page <= 1 } onClick = { ( ) = > setOffset ( Math . max ( 0 , offset - limit ) ) } > 上 一 页 < / button > < button type = "button" disabled = { page >= totalPages } onClick = { ( ) = > setOffset ( offset + limit ) } > 下 一 页 < / button > < select aria-label = "每页数量" value = { limit } onChange = { ( event ) = > { setLimit ( Number ( event . target . value ) ) ; setOffset ( 0 ) ; } } > < option value = "20" > 20 辆 / 页 < / option > < option value = "50" > 50 辆 / 页 < / option > < option value = "100" > 100 辆 / 页 < / option > < / select > < / div > < / footer > < / section > { selected ? < VehicleInspector row = { selected } onClose = { ( ) = > setSelectedVIN ( '' ) } / > : null } < / div >
{ editable && unresolvedQuery . isError ? < InlineError message = { unresolvedQuery . error instanceof Error ? unresolvedQuery . error . message : '待绑定身份读取失败' } onRetry = { ( ) = > unresolvedQuery . refetch ( ) } / > : null }
{ editable ? < IdentityQueue items = { unresolvedQuery . data ? . items ? ? [ ] } total = { unresolvedQuery . data ? . total ? ? 0 } / > : null }
{ editable && thresholdQuery . isError ? < InlineError message = { thresholdQuery . error instanceof Error ? thresholdQuery . error . message : '接入阈值读取失败' } onRetry = { ( ) = > thresholdQuery . refetch ( ) } / > : null }