feat(assets): add verified asset history trends and compact overview
This commit is contained in:
@@ -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: '历史回溯 · 估算',
|
||||
};
|
||||
Reference in New Issue
Block a user