feat: publish mileage assessment freshness
This commit is contained in:
@@ -170,6 +170,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
||||
### 阶段 C:数据可信度
|
||||
|
||||
- 里程年度归因使用考核台账 `current_mileage` 快照,按车辆目标封顶后加总完成量与缺口,并自动核对归因合计和目标汇总;带日期的 OneOS 里程只用于车辆实时明细。
|
||||
- 里程考核目标接口发布车辆台账 `MAX(update_time)`,统计页使用真实日期和时间展示数据截至点,不再用页面访问日或考核结束日代替快照时间。
|
||||
- 已完成电能数据截至时间和实际趋势月展示。
|
||||
- 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。
|
||||
- 已完成氢能结构化故障状态与重试体验。
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { TargetSummary, TargetVehicle, TargetYearlyAssessment, TrendPoint }
|
||||
import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api';
|
||||
import Blur from '../../components/Blur';
|
||||
import { weightedCompletionRate } from '../../shared/analytics/metrics';
|
||||
import { formatMileageSnapshotDateTime } from './data-freshness';
|
||||
import MileageAttributionView from './MileageAttributionView';
|
||||
import {
|
||||
filterAttributionVehicles,
|
||||
@@ -30,11 +31,6 @@ function getDefaultDate(): string {
|
||||
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 {
|
||||
if (value >= 10000) return (value / 10000).toFixed(2) + '万';
|
||||
return value.toLocaleString();
|
||||
@@ -53,12 +49,6 @@ function resolveAssessmentYear(target: TargetSummary, requestedYear?: number): n
|
||||
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 {
|
||||
// Extract the number and a short description
|
||||
const match = name.match(/(\d+)[辆台](.+)/);
|
||||
@@ -74,7 +64,6 @@ function shortTargetName(name: string): string {
|
||||
}
|
||||
|
||||
export default function StatisticsView() {
|
||||
const currentDateLabel = getCurrentDateLabel();
|
||||
const [drillContext, setDrillContext] = useState<MileageDrillContext>(() => (
|
||||
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-[10px] font-black text-emerald-600">{fmtKm(assessment.completed)} km</p>
|
||||
<p className="text-[8px] font-bold text-slate-300">
|
||||
数据截至 {assessment.daysLeft === 0 ? fmtDateLabel(assessment.endDate) : currentDateLabel}
|
||||
数据截至 {formatMileageSnapshotDateTime(target.snapshotUpdatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
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', () => {
|
||||
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(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',
|
||||
}).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 {
|
||||
id: number;
|
||||
targetName: string;
|
||||
snapshotUpdatedAt: string | null;
|
||||
vehicleCount: number;
|
||||
totalMileagePerVehicle: 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(current_year_mileage_task) as current_year_target,
|
||||
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
|
||||
GROUP BY target_id
|
||||
`) as [any[], unknown];
|
||||
@@ -199,6 +200,7 @@ app.get('/', async (c) => {
|
||||
return {
|
||||
id: t.id,
|
||||
targetName: t.target_name,
|
||||
snapshotUpdatedAt: s.snapshot_updated_at || null,
|
||||
vehicleCount: Number(s.total) || t.vehicle_count,
|
||||
totalMileagePerVehicle: Number(t.total_mileage_per_vehicle),
|
||||
annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle),
|
||||
|
||||
Reference in New Issue
Block a user