@@ -8,10 +8,12 @@ import { FormEvent, memo, useCallback, useEffect, useMemo, useRef, useState } fr
import { useSearchParams } from 'react-router-dom' ;
import { api } from '../../api/client' ;
import type { TrackPlaybackEvent , TrackPlaybackResponse , VehicleRow } from '../../api/types' ;
import { protocolDisplayLabel } from '../domain/protocols' ;
import { buildTrackRailSelection , downloadTrackCsv , formatDuration , sampledEventIndex , TRACK_ADDRESS_SETTLE_MS , trackAddressCoordinate , trackPlaybackInterval , type TrackAddressCoordinate } from '../domain/track' ;
import { TrackMap } from '../map/TrackMap' ;
import { InlineError } from '../shared/AsyncState' ;
import { MonitorReturnBar } from '../shared/MonitorReturnBar' ;
import { ProtocolTag } from '../shared/ProtocolTag' ;
import { SegmentedTabs } from '../shared/SegmentedTabs' ;
import { VehicleCandidateList } from '../shared/VehicleCandidateList' ;
import { mergeVehicleCandidates } from '../shared/vehicleCandidates' ;
@@ -28,6 +30,12 @@ type PanelTab = 'stops' | 'events' | 'overview';
type Draft = { keyword : string ; dateFrom : string ; dateTo : string ; protocol : string } ;
type TrackPreset = 'today' | 'yesterday' | 'three-days' ;
const EMPTY_INDEXES : readonly number [ ] = [ ] ;
const TRACK_SOURCE_OPTIONS = [
{ value : '' , label : '自动选择最佳来源' } ,
{ value : 'GB32960' , label : ` ${ protocolDisplayLabel ( 'GB32960' ) } · 仪表盘里程 ` } ,
{ value : 'JT808' , label : ` ${ protocolDisplayLabel ( 'JT808' ) } · GPS 里程 ` } ,
{ value : 'YUTONG_MQTT' , label : ` ${ protocolDisplayLabel ( 'YUTONG_MQTT' ) } · 仪表盘里程 ` }
] ;
const numberFormatters = new Map < number , Intl.NumberFormat > ( ) ;
const timeFormatter = new Intl . DateTimeFormat ( 'zh-CN' , { timeZone : 'Asia/Shanghai' , hour12 : false , hour : '2-digit' , minute : '2-digit' , second : '2-digit' } ) ;
const dateTimeFormatter = new Intl . DateTimeFormat ( 'zh-CN' , { timeZone : 'Asia/Shanghai' , hour12 : false , month : '2-digit' , day : '2-digit' , hour : '2-digit' , minute : '2-digit' , second : '2-digit' } ) ;
@@ -167,7 +175,7 @@ function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
< / section >
< section className = "v2-track-overview-section v2-track-source-card" >
< WorkspacePanelHeader variant = "compact" title = "数据来源" meta = { < Tag color = "blue" type = "light" size = "small" > { track . coverage . totalPoints . toLocaleString ( 'zh-CN' ) } 个 源 点 < / Tag > } / >
< div className = "v2-track-overview-card-body" > < List className = "v2-track-source-list" > { track . sources . map ( ( source ) = > < List.Item className = "v2-track-source-item" key = { source . protocol } > < Tag color = "blue" type = "light" size = "small" > { source . protocol } < / Tag > < strong > { source . pointCount . toLocaleString ( 'zh-CN' ) } 点 < / strong > < small > { time ( source . startTime ) } – { time ( source . endTime ) } < / small > < / List.Item > ) } < / List > < / div >
< div className = "v2-track-overview-card-body" > < List className = "v2-track-source-list" > { track . sources . map ( ( source ) = > < List.Item className = "v2-track-source-item" key = { source . protocol } > < ProtocolTag className = "v2-track-protocol-tag" protocol = { source . protocol } compact / > < strong > { source . pointCount . toLocaleString ( 'zh-CN' ) } 点 < / strong > < small > { time ( source . startTime ) } – { time ( source . endTime ) } < / small > < / List.Item > ) } < / List > < / div >
< / section >
< section className = { ` v2-track-overview-section v2-track-quality-card is- ${ track . quality . status } ` } >
< WorkspacePanelHeader variant = "compact" title = "轨迹质量" meta = { < Tag color = { track . quality . status === 'good' ? 'green' : 'orange' } type = "light" size = "small" > { track . quality . status === 'good' ? '通过' : '需关注' } < / Tag > } / >
@@ -194,7 +202,7 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
const activePreset = trackPreset ( draft ) ;
const choosePreset = ( daysBack : number ) = > onDraft ( { . . . draft , . . . trackWindow ( daysBack , daysBack > 0 ) } ) ;
const appliedWindow = ` ${ draft . dateFrom . replace ( 'T' , ' ' ) . slice ( 5 ) } – ${ draft . dateTo . replace ( 'T' , ' ' ) . slice ( 5 ) } ` ;
const appliedSource = draft . protocol || '自动来源' ;
const appliedSource = draft . protocol ? protocolDisplayLabel ( draft . protocol ) : '自动来源' ;
const contextTitle = queryCollapsed ? track ? . plate || draft . keyword || '轨迹查询' : '轨迹查询' ;
return < aside className = "v2-track-rail" >
< Card className = "v2-track-rail-shell" bodyStyle = { { padding : 0 } } >
@@ -228,7 +236,7 @@ const TrackRail = memo(function TrackRail({ draft, track, loading, activeStopInd
< Button theme = { activePreset === 'three-days' ? 'solid' : 'light' } aria-pressed = { activePreset === 'three-days' } onClick = { ( ) = > { const end = new Date ( ) ; const start = new Date ( end ) ; start . setDate ( start . getDate ( ) - 2 ) ; start . setHours ( 0 , 0 , 0 , 0 ) ; onDraft ( { . . . draft , dateFrom : localDateTime ( start ) , dateTo : localDateTime ( end ) } ) ; } } > 近 3 天 < / Button >
< / div >
< div className = "v2-track-date-grid" > < label > < span > 开 始 时 间 < / span > < Input aria-label = "开始时间" type = "datetime-local" value = { draft . dateFrom } onChange = { ( value ) = > onDraft ( { . . . draft , dateFrom : value } ) } / > < / label > < label > < span > 结 束 时 间 < / span > < Input aria-label = "结束时间" type = "datetime-local" value = { draft . dateTo } onChange = { ( value ) = > onDraft ( { . . . draft , dateTo : value } ) } / > < / label > < / div >
< label > < span id = "track-source-label" > 数 据 来 源 < / span > < Select aria-labelledby = "track-source-label" value = { draft . protocol } onChange = { ( value ) = > onDraft ( { . . . draft , protocol : String ( value ) } ) } optionList = { [ { value : '' , label : '自动选择最佳来源' } , { value : 'GB32960' , label : 'GB32960 · 仪表盘里程' } , { value : 'JT808' , label : 'JT808 · GPS 里程' } , { value : 'YUTONG_MQTT' , label : 'YUTONG · 仪表盘里程' } ] } / > < / label >
< label > < span id = "track-source-label" > 数 据 来 源 < / span > < Select aria-labelledby = "track-source-label" value = { draft . protocol } onChange = { ( value ) = > onDraft ( { . . . draft , protocol : String ( value ) } ) } optionList = { TRACK_SOURCE_OPTIONS } / > < / label >
< Button className = "v2-track-query-button" theme = "solid" htmlType = "submit" icon = { < IconSearch / > } loading = { loading } disabled = { ! draft . keyword . trim ( ) } > 查 询 轨 迹 < / Button >
< / form > : null }
< / section >
@@ -413,7 +421,7 @@ export default function TrackPage() {
< span > < small > 总 里 程 < / small > < strong > { number ( current ? . totalMileageKm ? ? 0 ) } < em > km < / em > < / strong > < / span >
< / div >
< div className = { ` v2-track-current-evidence v2-track-current-coverage ${ track . coverage . complete ? '' : ' is-warning' } ` } >
< Tag color = "blue" type = "light" size = "small" > { current ? . protocol || '未知来源' } < / Tag >
< ProtocolTag className = "v2-track-protocol-tag" protocol = { current ? . protocol } compact / >
< span title = { track . coverage . evidence } > < i / > < strong > { track . coverage . complete ? '时间窗完整' : '最新切片' } < / strong > < / span >
< em > { track . coverage . processedPoints . toLocaleString ( 'zh-CN' ) } → { track . coverage . returnedPoints . toLocaleString ( 'zh-CN' ) } 点 < / em >
< small > { alarm ( current ? . alarmFlag ) } < / small >