feat: publish mileage assessment freshness
This commit is contained in:
@@ -170,6 +170,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
|||||||
### 阶段 C:数据可信度
|
### 阶段 C:数据可信度
|
||||||
|
|
||||||
- 里程年度归因使用考核台账 `current_mileage` 快照,按车辆目标封顶后加总完成量与缺口,并自动核对归因合计和目标汇总;带日期的 OneOS 里程只用于车辆实时明细。
|
- 里程年度归因使用考核台账 `current_mileage` 快照,按车辆目标封顶后加总完成量与缺口,并自动核对归因合计和目标汇总;带日期的 OneOS 里程只用于车辆实时明细。
|
||||||
|
- 里程考核目标接口发布车辆台账 `MAX(update_time)`,统计页使用真实日期和时间展示数据截至点,不再用页面访问日或考核结束日代替快照时间。
|
||||||
- 已完成电能数据截至时间和实际趋势月展示。
|
- 已完成电能数据截至时间和实际趋势月展示。
|
||||||
- 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。
|
- 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。
|
||||||
- 已完成氢能结构化故障状态与重试体验。
|
- 已完成氢能结构化故障状态与重试体验。
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { TargetSummary, TargetVehicle, TargetYearlyAssessment, TrendPoint }
|
|||||||
import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api';
|
import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api';
|
||||||
import Blur from '../../components/Blur';
|
import Blur from '../../components/Blur';
|
||||||
import { weightedCompletionRate } from '../../shared/analytics/metrics';
|
import { weightedCompletionRate } from '../../shared/analytics/metrics';
|
||||||
|
import { formatMileageSnapshotDateTime } from './data-freshness';
|
||||||
import MileageAttributionView from './MileageAttributionView';
|
import MileageAttributionView from './MileageAttributionView';
|
||||||
import {
|
import {
|
||||||
filterAttributionVehicles,
|
filterAttributionVehicles,
|
||||||
@@ -30,11 +31,6 @@ function getDefaultDate(): string {
|
|||||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentDateLabel(): string {
|
|
||||||
const now = new Date();
|
|
||||||
return `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtKm(value: number): string {
|
function fmtKm(value: number): string {
|
||||||
if (value >= 10000) return (value / 10000).toFixed(2) + '万';
|
if (value >= 10000) return (value / 10000).toFixed(2) + '万';
|
||||||
return value.toLocaleString();
|
return value.toLocaleString();
|
||||||
@@ -53,12 +49,6 @@ function resolveAssessmentYear(target: TargetSummary, requestedYear?: number): n
|
|||||||
return getTargetAssessment(target, requestedYear)?.yearNumber;
|
return getTargetAssessment(target, requestedYear)?.yearNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtDateLabel(date: string | null): string {
|
|
||||||
if (!date) return '';
|
|
||||||
const [year, month, day] = date.split('-');
|
|
||||||
return `${year}.${Number(month)}.${Number(day)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shortTargetName(name: string): string {
|
function shortTargetName(name: string): string {
|
||||||
// Extract the number and a short description
|
// Extract the number and a short description
|
||||||
const match = name.match(/(\d+)[辆台](.+)/);
|
const match = name.match(/(\d+)[辆台](.+)/);
|
||||||
@@ -74,7 +64,6 @@ function shortTargetName(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StatisticsView() {
|
export default function StatisticsView() {
|
||||||
const currentDateLabel = getCurrentDateLabel();
|
|
||||||
const [drillContext, setDrillContext] = useState<MileageDrillContext>(() => (
|
const [drillContext, setDrillContext] = useState<MileageDrillContext>(() => (
|
||||||
parseMileageDrillContext(window.location.search)
|
parseMileageDrillContext(window.location.search)
|
||||||
));
|
));
|
||||||
@@ -642,7 +631,7 @@ export default function StatisticsView() {
|
|||||||
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}已完成</p>
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-wider">{assessment.label}已完成</p>
|
||||||
<p className="text-[10px] font-black text-emerald-600">{fmtKm(assessment.completed)} km</p>
|
<p className="text-[10px] font-black text-emerald-600">{fmtKm(assessment.completed)} km</p>
|
||||||
<p className="text-[8px] font-bold text-slate-300">
|
<p className="text-[8px] font-bold text-slate-300">
|
||||||
数据截至 {assessment.daysLeft === 0 ? fmtDateLabel(assessment.endDate) : currentDateLabel}
|
数据截至 {formatMileageSnapshotDateTime(target.snapshotUpdatedAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { formatMileageSnapshotTime } from './data-freshness.js';
|
import { formatMileageSnapshotDateTime, formatMileageSnapshotTime } from './data-freshness.js';
|
||||||
|
|
||||||
test('formats snapshot instants in Asia/Shanghai', () => {
|
test('formats snapshot instants in Asia/Shanghai', () => {
|
||||||
assert.equal(formatMileageSnapshotTime('2026-08-07T07:45:06.000Z'), '15:45:06');
|
assert.equal(formatMileageSnapshotTime('2026-08-07T07:45:06.000Z'), '15:45:06');
|
||||||
@@ -10,3 +10,9 @@ test('keeps historical date snapshots explicit', () => {
|
|||||||
assert.equal(formatMileageSnapshotTime('2026-08-06'), '2026-08-06');
|
assert.equal(formatMileageSnapshotTime('2026-08-06'), '2026-08-06');
|
||||||
assert.equal(formatMileageSnapshotTime(null), '--:--:--');
|
assert.equal(formatMileageSnapshotTime(null), '--:--:--');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('formats assessment snapshot instants with the business date and minute', () => {
|
||||||
|
assert.equal(formatMileageSnapshotDateTime('2026-08-07T09:45:01.000Z'), '2026.8.7 17:45');
|
||||||
|
assert.equal(formatMileageSnapshotDateTime(null), '--');
|
||||||
|
assert.equal(formatMileageSnapshotDateTime('not-a-date'), 'not-a-date');
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,3 +12,21 @@ export function formatMileageSnapshotTime(value: string | null): string {
|
|||||||
hourCycle: 'h23',
|
hourCycle: 'h23',
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatMileageSnapshotDateTime(value: string | null): string {
|
||||||
|
if (!value) return '--';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
const parts = new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
hourCycle: 'h23',
|
||||||
|
}).formatToParts(date);
|
||||||
|
const part = (type: Intl.DateTimeFormatPartTypes) => parts.find(item => item.type === type)?.value || '';
|
||||||
|
return `${part('year')}.${part('month')}.${part('day')} ${part('hour')}:${part('minute')}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export interface MonitoringData {
|
|||||||
export interface TargetSummary {
|
export interface TargetSummary {
|
||||||
id: number;
|
id: number;
|
||||||
targetName: string;
|
targetName: string;
|
||||||
|
snapshotUpdatedAt: string | null;
|
||||||
vehicleCount: number;
|
vehicleCount: number;
|
||||||
totalMileagePerVehicle: number;
|
totalMileagePerVehicle: number;
|
||||||
annualMileagePerVehicle: number;
|
annualMileagePerVehicle: number;
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ app.get('/', async (c) => {
|
|||||||
SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count,
|
SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count,
|
||||||
SUM(current_year_mileage_task) as current_year_target,
|
SUM(current_year_mileage_task) as current_year_target,
|
||||||
SUM(current_year_mileage) as current_year_completed,
|
SUM(current_year_mileage) as current_year_completed,
|
||||||
MAX(current_year_assessment_end_date) as year_end_date
|
MAX(current_year_assessment_end_date) as year_end_date,
|
||||||
|
DATE_FORMAT(MAX(update_time), '%Y-%m-%dT%H:%i:%s+08:00') as snapshot_updated_at
|
||||||
FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0
|
FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0
|
||||||
GROUP BY target_id
|
GROUP BY target_id
|
||||||
`) as [any[], unknown];
|
`) as [any[], unknown];
|
||||||
@@ -199,6 +200,7 @@ app.get('/', async (c) => {
|
|||||||
return {
|
return {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
targetName: t.target_name,
|
targetName: t.target_name,
|
||||||
|
snapshotUpdatedAt: s.snapshot_updated_at || null,
|
||||||
vehicleCount: Number(s.total) || t.vehicle_count,
|
vehicleCount: Number(s.total) || t.vehicle_count,
|
||||||
totalMileagePerVehicle: Number(t.total_mileage_per_vehicle),
|
totalMileagePerVehicle: Number(t.total_mileage_per_vehicle),
|
||||||
annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle),
|
annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle),
|
||||||
|
|||||||
Reference in New Issue
Block a user