fix: distinguish track mileage evidence
This commit is contained in:
@@ -1036,7 +1036,7 @@ func (m *MockStore) HistoryLocationsFromTDengine(ctx context.Context, query url.
|
|||||||
VIN: row.VIN, Plate: row.Plate, Protocol: row.Protocol,
|
VIN: row.VIN, Plate: row.Plate, Protocol: row.Protocol,
|
||||||
Longitude: row.Longitude - float64(remaining)*0.0062,
|
Longitude: row.Longitude - float64(remaining)*0.0062,
|
||||||
Latitude: row.Latitude - float64(remaining)*0.0021 + float64(index%3)*0.0008,
|
Latitude: row.Latitude - float64(remaining)*0.0021 + float64(index%3)*0.0008,
|
||||||
SpeedKmh: speed, TotalMileageKm: row.TotalMileageKm - float64(remaining)*1.35,
|
SpeedKmh: speed, TotalMileageKm: row.TotalMileageKm - float64(remaining)*1.35, MileageAvailable: true,
|
||||||
DeviceTime: observedAt, ServerTime: observedAt,
|
DeviceTime: observedAt, ServerTime: observedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ type TrackPlaybackSummary struct {
|
|||||||
StartTime string `json:"startTime"`
|
StartTime string `json:"startTime"`
|
||||||
EndTime string `json:"endTime"`
|
EndTime string `json:"endTime"`
|
||||||
DistanceKm float64 `json:"distanceKm"`
|
DistanceKm float64 `json:"distanceKm"`
|
||||||
|
DistanceMethod string `json:"distanceMethod"`
|
||||||
DurationSeconds int64 `json:"durationSeconds"`
|
DurationSeconds int64 `json:"durationSeconds"`
|
||||||
AverageSpeedKmh float64 `json:"averageSpeedKmh"`
|
AverageSpeedKmh float64 `json:"averageSpeedKmh"`
|
||||||
MaximumSpeedKmh float64 `json:"maximumSpeedKmh"`
|
MaximumSpeedKmh float64 `json:"maximumSpeedKmh"`
|
||||||
@@ -1304,19 +1305,20 @@ type VehicleRealtimeRow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HistoryLocationRow struct {
|
type HistoryLocationRow struct {
|
||||||
VIN string `json:"vin"`
|
VIN string `json:"vin"`
|
||||||
Plate string `json:"plate"`
|
Plate string `json:"plate"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
SpeedKmh float64 `json:"speedKmh"`
|
SpeedKmh float64 `json:"speedKmh"`
|
||||||
SOCPercent float64 `json:"socPercent"`
|
SOCPercent float64 `json:"socPercent"`
|
||||||
SOCAvailable bool `json:"socAvailable"`
|
SOCAvailable bool `json:"socAvailable"`
|
||||||
DirectionDeg *int64 `json:"directionDeg,omitempty"`
|
DirectionDeg *int64 `json:"directionDeg,omitempty"`
|
||||||
AlarmFlag *int64 `json:"alarmFlag,omitempty"`
|
AlarmFlag *int64 `json:"alarmFlag,omitempty"`
|
||||||
TotalMileageKm float64 `json:"totalMileageKm"`
|
TotalMileageKm float64 `json:"totalMileageKm"`
|
||||||
DeviceTime string `json:"deviceTime"`
|
MileageAvailable bool `json:"mileageAvailable"`
|
||||||
ServerTime string `json:"serverTime"`
|
DeviceTime string `json:"deviceTime"`
|
||||||
|
ServerTime string `json:"serverTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawFrameRow struct {
|
type RawFrameRow struct {
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ func (s *ProductionStore) HistoryLocations(ctx context.Context, query url.Values
|
|||||||
for _, row := range realtime.Items {
|
for _, row := range realtime.Items {
|
||||||
items = append(items, HistoryLocationRow{
|
items = append(items, HistoryLocationRow{
|
||||||
VIN: row.VIN, Plate: row.Plate, Protocol: row.Protocol, Longitude: row.Longitude, Latitude: row.Latitude,
|
VIN: row.VIN, Plate: row.Plate, Protocol: row.Protocol, Longitude: row.Longitude, Latitude: row.Latitude,
|
||||||
SpeedKmh: row.SpeedKmh, SOCPercent: row.SOCPercent, SOCAvailable: row.SOCPercent > 0, TotalMileageKm: row.TotalMileageKm, DeviceTime: row.LastSeen, ServerTime: row.LastSeen,
|
SpeedKmh: row.SpeedKmh, SOCPercent: row.SOCPercent, SOCAvailable: row.SOCPercent > 0, TotalMileageKm: row.TotalMileageKm, MileageAvailable: row.TotalMileageKm > 0, DeviceTime: row.LastSeen, ServerTime: row.LastSeen,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return Page[HistoryLocationRow]{Items: items, Total: realtime.Total, Limit: realtime.Limit, Offset: realtime.Offset}, nil
|
return Page[HistoryLocationRow]{Items: items, Total: realtime.Total, Limit: realtime.Limit, Offset: realtime.Offset}, nil
|
||||||
@@ -675,6 +675,7 @@ func (s *ProductionStore) HistoryLocationsFromTDengine(ctx context.Context, quer
|
|||||||
row.DirectionDeg = nullInt64Pointer(direction)
|
row.DirectionDeg = nullInt64Pointer(direction)
|
||||||
row.AlarmFlag = nullInt64Pointer(alarm)
|
row.AlarmFlag = nullInt64Pointer(alarm)
|
||||||
row.TotalMileageKm = nullFloat64(mileage)
|
row.TotalMileageKm = nullFloat64(mileage)
|
||||||
|
row.MileageAvailable = mileage.Valid
|
||||||
row.DeviceTime = time.UnixMilli(ts).UTC().Format(time.RFC3339Nano)
|
row.DeviceTime = time.UnixMilli(ts).UTC().Format(time.RFC3339Nano)
|
||||||
row.ServerTime = row.DeviceTime
|
row.ServerTime = row.DeviceTime
|
||||||
if receivedAt.Valid {
|
if receivedAt.Valid {
|
||||||
|
|||||||
@@ -3474,10 +3474,14 @@ func summarizeTrack(points []HistoryLocationRow) TrackPlaybackSummary {
|
|||||||
summary.AverageSpeedKmh = speedTotal / float64(speedCount)
|
summary.AverageSpeedKmh = speedTotal / float64(speedCount)
|
||||||
}
|
}
|
||||||
mileageDistance := points[len(points)-1].TotalMileageKm - points[0].TotalMileageKm
|
mileageDistance := points[len(points)-1].TotalMileageKm - points[0].TotalMileageKm
|
||||||
if points[0].Protocol == points[len(points)-1].Protocol && mileageDistance >= 0 && mileageDistance < 10000 {
|
if points[0].Protocol == points[len(points)-1].Protocol &&
|
||||||
|
points[0].MileageAvailable && points[len(points)-1].MileageAvailable &&
|
||||||
|
mileageDistance >= 0 && mileageDistance < 10000 {
|
||||||
summary.DistanceKm = mileageDistance
|
summary.DistanceKm = mileageDistance
|
||||||
|
summary.DistanceMethod = "odometer"
|
||||||
} else {
|
} else {
|
||||||
summary.DistanceKm = coordinateDistance
|
summary.DistanceKm = coordinateDistance
|
||||||
|
summary.DistanceMethod = "coordinates"
|
||||||
}
|
}
|
||||||
start, startOK := parseVehicleServiceTime(summary.StartTime)
|
start, startOK := parseVehicleServiceTime(summary.StartTime)
|
||||||
end, endOK := parseVehicleServiceTime(summary.EndTime)
|
end, endOK := parseVehicleServiceTime(summary.EndTime)
|
||||||
|
|||||||
@@ -377,15 +377,26 @@ func TestCompleteProtocolStatsIncludesCanonicalSlots(t *testing.T) {
|
|||||||
|
|
||||||
func TestSummarizeTrackUsesMileageAndChronology(t *testing.T) {
|
func TestSummarizeTrackUsesMileageAndChronology(t *testing.T) {
|
||||||
points := []HistoryLocationRow{
|
points := []HistoryLocationRow{
|
||||||
{DeviceTime: "2026-07-03 10:00:00", TotalMileageKm: 100, SpeedKmh: 0, Longitude: 113.1, Latitude: 23.1},
|
{DeviceTime: "2026-07-03 10:00:00", TotalMileageKm: 100, MileageAvailable: true, SpeedKmh: 0, Longitude: 113.1, Latitude: 23.1},
|
||||||
{DeviceTime: "2026-07-03 10:30:00", TotalMileageKm: 112.5, SpeedKmh: 50, Longitude: 113.2, Latitude: 23.2},
|
{DeviceTime: "2026-07-03 10:30:00", TotalMileageKm: 112.5, MileageAvailable: true, SpeedKmh: 50, Longitude: 113.2, Latitude: 23.2},
|
||||||
}
|
}
|
||||||
summary := summarizeTrack(points)
|
summary := summarizeTrack(points)
|
||||||
if summary.DistanceKm != 12.5 || summary.DurationSeconds != 1800 || summary.MaximumSpeedKmh != 50 {
|
if summary.DistanceKm != 12.5 || summary.DistanceMethod != "odometer" || summary.DurationSeconds != 1800 || summary.MaximumSpeedKmh != 50 {
|
||||||
t.Fatalf("unexpected track summary: %+v", summary)
|
t.Fatalf("unexpected track summary: %+v", summary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSummarizeTrackUsesCoordinatesWhenProtocolMileageIsMissing(t *testing.T) {
|
||||||
|
points := []HistoryLocationRow{
|
||||||
|
{Protocol: "JT808", DeviceTime: "2026-07-03 10:00:00", Longitude: 113.1, Latitude: 23.1},
|
||||||
|
{Protocol: "JT808", DeviceTime: "2026-07-03 10:01:00", Longitude: 113.101, Latitude: 23.101},
|
||||||
|
}
|
||||||
|
summary := summarizeTrack(points)
|
||||||
|
if summary.DistanceMethod != "coordinates" || summary.DistanceKm <= 0 {
|
||||||
|
t.Fatalf("missing protocol mileage must use an explicitly labelled coordinate distance: %+v", summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTrackEventKeepsSynchronizedTelemetryEvidence(t *testing.T) {
|
func TestTrackEventKeepsSynchronizedTelemetryEvidence(t *testing.T) {
|
||||||
direction := int64(88)
|
direction := int64(88)
|
||||||
alarm := int64(2)
|
alarm := int64(2)
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export interface TrackPlaybackSummary {
|
|||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
distanceKm: number;
|
distanceKm: number;
|
||||||
|
distanceMethod?: 'odometer' | 'coordinates' | string;
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
averageSpeedKmh: number;
|
averageSpeedKmh: number;
|
||||||
maximumSpeedKmh: number;
|
maximumSpeedKmh: number;
|
||||||
@@ -748,6 +749,7 @@ export interface VehicleRealtimeRow {
|
|||||||
|
|
||||||
export interface HistoryLocationRow extends RealtimeLocationRow {
|
export interface HistoryLocationRow extends RealtimeLocationRow {
|
||||||
socAvailable: boolean;
|
socAvailable: boolean;
|
||||||
|
mileageAvailable?: boolean;
|
||||||
directionDeg?: number;
|
directionDeg?: number;
|
||||||
alarmFlag?: number;
|
alarmFlag?: number;
|
||||||
deviceTime: string;
|
deviceTime: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { TrackPlaybackResponse } from '../../api/types';
|
import type { TrackPlaybackResponse } from '../../api/types';
|
||||||
import { buildTrackRailSelection, formatDuration, sampledEventIndex, trackAddressCoordinate, trackCsv, trackPlaybackInterval } from './track';
|
import { buildTrackRailSelection, formatDuration, sampledEventIndex, trackAddressCoordinate, trackCsv, trackPlaybackInterval, trackPointMileageAvailable } from './track';
|
||||||
|
|
||||||
describe('track domain', () => {
|
describe('track domain', () => {
|
||||||
it('formats durations and maps original event indices to sampled points', () => {
|
it('formats durations and maps original event indices to sampled points', () => {
|
||||||
@@ -39,6 +39,11 @@ describe('track domain', () => {
|
|||||||
expect(csv).toContain('"粤A,001"');
|
expect(csv).toContain('"粤A,001"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps missing protocol mileage distinct from a real zero', () => {
|
||||||
|
expect(trackPointMileageAvailable({ totalMileageKm: 123, mileageAvailable: true } as never)).toBe(true);
|
||||||
|
expect(trackPointMileageAvailable({ totalMileageKm: 0, mileageAvailable: false } as never)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('coalesces GPS jitter into street-level address cells and rejects invalid coordinates', () => {
|
it('coalesces GPS jitter into street-level address cells and rejects invalid coordinates', () => {
|
||||||
expect(trackAddressCoordinate(113.123441, 23.123441)).toEqual({ longitude: 113.1234, latitude: 23.1234, key: '113.1234,23.1234' });
|
expect(trackAddressCoordinate(113.123441, 23.123441)).toEqual({ longitude: 113.1234, latitude: 23.1234, key: '113.1234,23.1234' });
|
||||||
expect(trackAddressCoordinate(113.123449, 23.123449)?.key).toBe('113.1234,23.1234');
|
expect(trackAddressCoordinate(113.123449, 23.123449)?.key).toBe('113.1234,23.1234');
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export function trackPlaybackInterval(speed: number) {
|
|||||||
return Math.max(55, 300 / Math.max(0.5, speed));
|
return Math.max(55, 300 / Math.max(0.5, speed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function trackPointMileageAvailable(point?: HistoryLocationRow) {
|
||||||
|
return Boolean(point && (point.mileageAvailable ?? point.totalMileageKm > 0));
|
||||||
|
}
|
||||||
|
|
||||||
export function buildTrackRailSelection(track?: TrackPlaybackResponse) {
|
export function buildTrackRailSelection(track?: TrackPlaybackResponse) {
|
||||||
const pointCount = track?.points.length ?? 0;
|
const pointCount = track?.points.length ?? 0;
|
||||||
const stopIndexesByPoint: Array<number[] | undefined> = new Array(pointCount);
|
const stopIndexesByPoint: Array<number[] | undefined> = new Array(pointCount);
|
||||||
@@ -65,7 +69,7 @@ function csvCell(value: unknown) {
|
|||||||
|
|
||||||
export function trackCsv(track: TrackPlaybackResponse) {
|
export function trackCsv(track: TrackPlaybackResponse) {
|
||||||
const headers = ['VIN', '车牌', '协议', '设备时间', '服务时间', '经度', '纬度', '速度(km/h)', '总里程(km)'];
|
const headers = ['VIN', '车牌', '协议', '设备时间', '服务时间', '经度', '纬度', '速度(km/h)', '总里程(km)'];
|
||||||
const rows = track.points.map((point) => [point.vin, point.plate, point.protocol, point.deviceTime, point.serverTime, point.longitude, point.latitude, point.speedKmh, point.totalMileageKm]);
|
const rows = track.points.map((point) => [point.vin, point.plate, point.protocol, point.deviceTime, point.serverTime, point.longitude, point.latitude, point.speedKmh, trackPointMileageAvailable(point) ? point.totalMileageKm : '']);
|
||||||
return `\uFEFF${[headers, ...rows].map((row) => row.map(csvCell).join(',')).join('\n')}`;
|
return `\uFEFF${[headers, ...rows].map((row) => row.map(csvCell).join(',')).join('\n')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ vi.mock('@douyinfe/semi-ui', async (importOriginal) => {
|
|||||||
const track = {
|
const track = {
|
||||||
vin: 'LTEST000000000001', plate: '粤A12345', total: 3, truncated: false, sampled: false, asOf: '2026-07-15T08:00:00Z',
|
vin: 'LTEST000000000001', plate: '粤A12345', total: 3, truncated: false, sampled: false, asOf: '2026-07-15T08:00:00Z',
|
||||||
points: [
|
points: [
|
||||||
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:00:00Z', serverTime: '2026-07-15T00:00:01Z', longitude: 113.1, latitude: 23.1, speedKmh: 0, totalMileageKm: 100, socPercent: 80, socAvailable: true },
|
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:00:00Z', serverTime: '2026-07-15T00:00:01Z', longitude: 113.1, latitude: 23.1, speedKmh: 0, totalMileageKm: 100, mileageAvailable: true, socPercent: 80, socAvailable: true },
|
||||||
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:10:00Z', serverTime: '2026-07-15T00:10:01Z', longitude: 113.2, latitude: 23.2, speedKmh: 35, totalMileageKm: 104, socPercent: 79, socAvailable: true },
|
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:10:00Z', serverTime: '2026-07-15T00:10:01Z', longitude: 113.2, latitude: 23.2, speedKmh: 35, totalMileageKm: 104, mileageAvailable: true, socPercent: 79, socAvailable: true },
|
||||||
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:20:00Z', serverTime: '2026-07-15T00:20:01Z', longitude: 113.3, latitude: 23.3, speedKmh: 10, totalMileageKm: 108, socPercent: 78, socAvailable: true }
|
{ vin: 'LTEST000000000001', plate: '粤A12345', protocol: 'JT808', deviceTime: '2026-07-15T00:20:00Z', serverTime: '2026-07-15T00:20:01Z', longitude: 113.3, latitude: 23.3, speedKmh: 10, totalMileageKm: 108, mileageAvailable: true, socPercent: 78, socAvailable: true }
|
||||||
],
|
],
|
||||||
events: [
|
events: [
|
||||||
{ index: 0, sampledIndex: 0, type: 'start', title: '开始行驶', time: '2026-07-15T00:00:00Z', speedKmh: 0, longitude: 113.1, latitude: 23.1 },
|
{ index: 0, sampledIndex: 0, type: 'start', title: '开始行驶', time: '2026-07-15T00:00:00Z', speedKmh: 0, longitude: 113.1, latitude: 23.1 },
|
||||||
@@ -62,7 +62,7 @@ const track = {
|
|||||||
sources: [{ protocol: 'JT808', pointCount: 3, startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z' }],
|
sources: [{ protocol: 'JT808', pointCount: 3, startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z' }],
|
||||||
segments: [{ index: 0, type: 'moving', title: '行驶', startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z', durationSeconds: 1200, distanceKm: 8, pointCount: 3, startIndex: 0, endIndex: 2, sampledStartIndex: 0, sampledEndIndex: 2 }],
|
segments: [{ index: 0, type: 'moving', title: '行驶', startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z', durationSeconds: 1200, distanceKm: 8, pointCount: 3, startIndex: 0, endIndex: 2, sampledStartIndex: 0, sampledEndIndex: 2 }],
|
||||||
stops: [{ index: 0, startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:05:00Z', durationSeconds: 300, pointCount: 1, longitude: 113.1, latitude: 23.1, sampledIndex: 0, evidence: 'GPS 推断' }],
|
stops: [{ index: 0, startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:05:00Z', durationSeconds: 300, pointCount: 1, longitude: 113.1, latitude: 23.1, sampledIndex: 0, evidence: 'GPS 推断' }],
|
||||||
summary: { startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z', distanceKm: 8, durationSeconds: 1200, averageSpeedKmh: 24, maximumSpeedKmh: 35, pointCount: 3, movingSeconds: 900, stoppedSeconds: 300, stopCount: 1, segmentCount: 1 },
|
summary: { startTime: '2026-07-15T00:00:00Z', endTime: '2026-07-15T00:20:00Z', distanceKm: 8, distanceMethod: 'odometer', durationSeconds: 1200, averageSpeedKmh: 24, maximumSpeedKmh: 35, pointCount: 3, movingSeconds: 900, stoppedSeconds: 300, stopCount: 1, segmentCount: 1 },
|
||||||
coverage: { requestedStart: '', requestedEnd: '', actualStart: '', actualEnd: '', totalPoints: 3, fetchedPoints: 3, processedPoints: 3, returnedPoints: 3, complete: true, limitReasons: [], evidence: '时间窗完整' },
|
coverage: { requestedStart: '', requestedEnd: '', actualStart: '', actualEnd: '', totalPoints: 3, fetchedPoints: 3, processedPoints: 3, returnedPoints: 3, complete: true, limitReasons: [], evidence: '时间窗完整' },
|
||||||
quality: { status: 'good', selectedProtocol: 'JT808', rawPoints: 3, validPoints: 3, alternateSourcePoints: 0, invalidCoordinatePoints: 0, duplicatePoints: 0, driftPoints: 0, sourceSwitches: 0, largeGapCount: 0, maximumGapSeconds: 0, evidence: '轨迹质量正常' }
|
quality: { status: 'good', selectedProtocol: 'JT808', rawPoints: 3, validPoints: 3, alternateSourcePoints: 0, invalidCoordinatePoints: 0, duplicatePoints: 0, driftPoints: 0, sourceSwitches: 0, largeGapCount: 0, maximumGapSeconds: 0, evidence: '轨迹质量正常' }
|
||||||
} as unknown as TrackPlaybackResponse;
|
} as unknown as TrackPlaybackResponse;
|
||||||
@@ -181,8 +181,8 @@ test('renders a map-first replay workspace and connects stop, event, and panel i
|
|||||||
expect(view.container.querySelector('.v2-track-rail-result')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-track-rail-result')).toBeInTheDocument();
|
||||||
const tripSummary = screen.getByRole('list', { name: '轨迹行程摘要' });
|
const tripSummary = screen.getByRole('list', { name: '轨迹行程摘要' });
|
||||||
expect(tripSummary.closest('.v2-track-result-metric-rail')).toHaveClass('semi-card', 'v2-workspace-metric-rail', 'is-queue');
|
expect(tripSummary.closest('.v2-track-result-metric-rail')).toHaveClass('semi-card', 'v2-workspace-metric-rail', 'is-queue');
|
||||||
expect(Array.from(tripSummary.querySelectorAll(':scope > [role="listitem"] > small')).map((item) => item.textContent)).toEqual(['行程里程', '停留点', '事件点', '数据点']);
|
expect(Array.from(tripSummary.querySelectorAll(':scope > [role="listitem"] > small')).map((item) => item.textContent)).toEqual(['协议总里程差', '停留点', '事件点', '数据点']);
|
||||||
expect(tripSummary).toHaveTextContent('行程里程8 km00:20:00');
|
expect(tripSummary).toHaveTextContent('协议总里程差8 km00:20:00');
|
||||||
expect(tripSummary).toHaveTextContent('停留点1超过 3 分钟');
|
expect(tripSummary).toHaveTextContent('停留点1超过 3 分钟');
|
||||||
expect(tripSummary).toHaveTextContent('事件点3轨迹事件');
|
expect(tripSummary).toHaveTextContent('事件点3轨迹事件');
|
||||||
expect(tripSummary).toHaveTextContent('数据点3时间窗完整');
|
expect(tripSummary).toHaveTextContent('数据点3时间窗完整');
|
||||||
@@ -285,7 +285,7 @@ test('uses the shared Semi editor SideSheet for mobile track criteria and eviden
|
|||||||
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('textbox', { name: '搜索轨迹车辆' })).not.toBeInTheDocument();
|
||||||
expect(screen.getAllByText('07-15 00:00 – 07-15 23:59 · 自动来源')).toHaveLength(2);
|
expect(screen.getAllByText('07-15 00:00 – 07-15 23:59 · 自动来源')).toHaveLength(2);
|
||||||
const mobileSummary = screen.getByRole('list', { name: '轨迹查询与明细摘要' });
|
const mobileSummary = screen.getByRole('list', { name: '轨迹查询与明细摘要' });
|
||||||
expect(mobileSummary).toHaveTextContent('行程里程8 km00:20:00');
|
expect(mobileSummary).toHaveTextContent('协议总里程差8 km00:20:00');
|
||||||
expect(mobileSummary).toHaveTextContent('停留点1超过 3 分钟');
|
expect(mobileSummary).toHaveTextContent('停留点1超过 3 分钟');
|
||||||
expect(mobileSummary).toHaveTextContent('事件点3轨迹事件');
|
expect(mobileSummary).toHaveTextContent('事件点3轨迹事件');
|
||||||
expect(mobileSummary).toHaveTextContent('数据点3时间窗完整');
|
expect(mobileSummary).toHaveTextContent('数据点3时间窗完整');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../../api/types';
|
import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../../api/types';
|
||||||
import { protocolDisplayLabel } from '../domain/protocols';
|
import { protocolDisplayLabel } from '../domain/protocols';
|
||||||
import { buildTrackRailSelection, downloadTrackCsv, formatDuration, sampledEventIndex, TRACK_ADDRESS_SETTLE_MS, trackAddressCoordinate, trackPlaybackInterval, type TrackAddressCoordinate } from '../domain/track';
|
import { buildTrackRailSelection, downloadTrackCsv, formatDuration, sampledEventIndex, TRACK_ADDRESS_SETTLE_MS, trackAddressCoordinate, trackPlaybackInterval, trackPointMileageAvailable, type TrackAddressCoordinate } from '../domain/track';
|
||||||
import { TrackMap } from '../map/TrackMap';
|
import { TrackMap } from '../map/TrackMap';
|
||||||
import { InlineError } from '../shared/AsyncState';
|
import { InlineError } from '../shared/AsyncState';
|
||||||
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
||||||
@@ -129,9 +129,10 @@ function mobileTrackSheetHeight(track: TrackPlaybackResponse | undefined, queryC
|
|||||||
}
|
}
|
||||||
|
|
||||||
function trackSummaryItems(track?: TrackPlaybackResponse): WorkspaceQueueMetricRailItem[] {
|
function trackSummaryItems(track?: TrackPlaybackResponse): WorkspaceQueueMetricRailItem[] {
|
||||||
|
const distanceLabel = track?.summary.distanceMethod === 'coordinates' ? 'GPS 轨迹累计' : track?.summary.distanceMethod === 'odometer' ? '协议总里程差' : '行程里程';
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: '行程里程',
|
label: distanceLabel,
|
||||||
value: track ? `${number(track.summary.distanceKm)} km` : '—',
|
value: track ? `${number(track.summary.distanceKm)} km` : '—',
|
||||||
note: track ? formatDuration(track.summary.durationSeconds) : '等待查询',
|
note: track ? formatDuration(track.summary.durationSeconds) : '等待查询',
|
||||||
tone: 'primary',
|
tone: 'primary',
|
||||||
@@ -206,13 +207,14 @@ function VehiclePicker({ value, onChange, onSelect }: { value: string; onChange:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
|
function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
|
||||||
|
const distanceLabel = track.summary.distanceMethod === 'coordinates' ? 'GPS 轨迹累计' : track.summary.distanceMethod === 'odometer' ? '协议总里程差' : '行驶里程';
|
||||||
return <div className="v2-track-overview-panel">
|
return <div className="v2-track-overview-panel">
|
||||||
<section className="v2-track-overview-section">
|
<section className="v2-track-overview-section">
|
||||||
<WorkspacePanelHeader variant="compact" title="行程概览" meta={<Tag color={track.sampled ? 'orange' : 'green'} type="light" size="small">{track.sampled ? '地图已抽稀' : '完整点集'}</Tag>} />
|
<WorkspacePanelHeader variant="compact" title="行程概览" meta={<Tag color={track.sampled ? 'orange' : 'green'} type="light" size="small">{track.sampled ? '地图已抽稀' : '完整点集'}</Tag>} />
|
||||||
<div className="v2-track-overview-card-body"><Descriptions className="v2-track-overview-descriptions" align="left" size="small" data={[
|
<div className="v2-track-overview-card-body"><Descriptions className="v2-track-overview-descriptions" align="left" size="small" data={[
|
||||||
{ key: '开始时间', value: dateTime(track.summary.startTime) },
|
{ key: '开始时间', value: dateTime(track.summary.startTime) },
|
||||||
{ key: '结束时间', value: dateTime(track.summary.endTime) },
|
{ key: '结束时间', value: dateTime(track.summary.endTime) },
|
||||||
{ key: '行驶里程', value: `${number(track.summary.distanceKm)} km` },
|
{ key: distanceLabel, value: `${number(track.summary.distanceKm)} km` },
|
||||||
{ key: '行驶 / 停车', value: `${formatDuration(track.summary.movingSeconds)} / ${formatDuration(track.summary.stoppedSeconds)}` },
|
{ key: '行驶 / 停车', value: `${formatDuration(track.summary.movingSeconds)} / ${formatDuration(track.summary.stoppedSeconds)}` },
|
||||||
{ key: '平均 / 最高速度', value: `${number(track.summary.averageSpeedKmh)} / ${number(track.summary.maximumSpeedKmh)} km/h` },
|
{ key: '平均 / 最高速度', value: `${number(track.summary.averageSpeedKmh)} / ${number(track.summary.maximumSpeedKmh)} km/h` },
|
||||||
{ key: '停车 / 分段', value: `${track.summary.stopCount} / ${track.summary.segmentCount}` }
|
{ key: '停车 / 分段', value: `${track.summary.stopCount} / ${track.summary.segmentCount}` }
|
||||||
@@ -501,7 +503,7 @@ export default function TrackPage() {
|
|||||||
<span><small>速度</small><strong>{number(current?.speedKmh ?? 0)}<em> km/h</em></strong></span>
|
<span><small>速度</small><strong>{number(current?.speedKmh ?? 0)}<em> km/h</em></strong></span>
|
||||||
<span><small>方向</small><strong>{direction(current?.directionDeg)}</strong></span>
|
<span><small>方向</small><strong>{direction(current?.directionDeg)}</strong></span>
|
||||||
<span><small>SOC</small><strong>{current?.socAvailable ? `${number(current.socPercent)}%` : '—'}</strong></span>
|
<span><small>SOC</small><strong>{current?.socAvailable ? `${number(current.socPercent)}%` : '—'}</strong></span>
|
||||||
<span><small>总里程</small><strong>{number(current?.totalMileageKm ?? 0)}<em> km</em></strong></span>
|
<span><small>节点累计总里程</small><strong>{trackPointMileageAvailable(current) ? number(current?.totalMileageKm ?? 0) : '—'}{trackPointMileageAvailable(current) ? <em> km</em> : null}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`v2-track-current-evidence v2-track-current-coverage${track.coverage.complete ? '' : ' is-warning'}`}>
|
<div className={`v2-track-current-evidence v2-track-current-coverage${track.coverage.complete ? '' : ' is-warning'}`}>
|
||||||
<ProtocolTag className="v2-track-protocol-tag" protocol={current?.protocol} compact />
|
<ProtocolTag className="v2-track-protocol-tag" protocol={current?.protocol} compact />
|
||||||
|
|||||||
Reference in New Issue
Block a user