feat(assets): add verified asset history trends and compact overview

This commit is contained in:
shishengliang
2026-10-24 13:16:48 +08:00
parent dbe0421bfe
commit 8afe13c9a3
36 changed files with 1716 additions and 9 deletions
+76
View File
@@ -0,0 +1,76 @@
export type HistoryMode = 'monthly' | 'annual';
export type HistoryState = 'LIVE' | 'ARCHIVED' | 'MISSING' | 'FUTURE' | 'CAPTURING' | 'FAILED' | 'RECONSTRUCTED';
export type HistoryCategory = 'all' | 'operating' | 'inventory';
export interface HistoryCounts { total: number; operating: number; inventory: number }
export interface HistoryPoint {
period: string;
label: string;
month: string;
scheduledFor: string;
asOf: string | null;
state: HistoryState;
source: 'AUTO' | 'IMPORT' | 'LIVE' | 'RECONSTRUCTED' | null;
batchId: string | null;
counts: HistoryCounts | null;
difference: HistoryCounts | null;
comparisonLabel: string;
detailAvailable: boolean;
reconstruction?: {
operating: number; inventory: number; unclassified: number;
currentlyExited: number; sourceAsOf: string; sourceReference: string;
workbookSha256: string;
difference: { operating: number; inventory: number } | null;
};
}
export interface HistoryTrend {
mode: HistoryMode;
year: number;
points: HistoryPoint[];
years: number[];
subjects: string[];
storageReady: boolean;
enabledAt: string | null;
nextCaptureAt: string | null;
warning: string | null;
}
export interface HistoryDetailItem {
id: string; plateNumber: string; vin: string; type: string; model: string;
disposition: 'operating' | 'inventory'; operationStatus: string | null;
vehicleStatus: string | null; subjectOrg: string | null; departmentName: string | null;
customerManager: string | null; customerName: string | null; contractNo: string | null;
reconstructionBasis?: string;
}
export interface HistoryDetails {
point: HistoryPoint;
counts: HistoryCounts | null;
total: number;
page: number;
pageSize: number;
items: HistoryDetailItem[];
}
const OFFSET = 8 * 60 * 60 * 1000;
export const CAPTURE_WINDOW_MS = 60_000;
export const HISTORY_VERSION = 'asset-v1';
export function shanghaiMonth(now: Date): string { return new Date(now.getTime() + OFFSET).toISOString().slice(0, 7); }
export function shanghaiTimestamp(now: Date): string { return new Date(now.getTime() + OFFSET).toISOString().slice(0, -1) + '+08:00'; }
export function monthCutoff(month: string): Date {
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(month)) throw new Error('月份格式无效');
const [year, m] = month.split('-').map(Number);
return new Date(Date.UTC(year, m, 0, 9)); // 17:00 Asia/Shanghai, on the last day.
}
export function shiftMonth(month: string, offset: number): string {
const [year, m] = month.split('-').map(Number);
return new Date(Date.UTC(year, m - 1 + offset, 1)).toISOString().slice(0, 7);
}
export function nextCapture(now: Date): string {
const month = shanghaiMonth(now);
return shanghaiTimestamp(monthCutoff(monthCutoff(month).getTime() > now.getTime() ? month : shiftMonth(month, 1)));
}
export function historyTime(value: string | null): string {
if (!value) return '暂无时间';
return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(value));
}
export const HISTORY_STATE_LABELS: Record<HistoryState, string> = {
LIVE: '未结期 · 实时', ARCHIVED: '已归档', MISSING: '暂无快照', FUTURE: '尚未到达', CAPTURING: '归档中', FAILED: '归档失败', RECONSTRUCTED: '历史回溯 · 估算',
};