feat: improve audit exports and admin analytics

This commit is contained in:
2026-08-25 18:49:56 +08:00
parent 910e67bd89
commit 5740bd26e7
55 changed files with 1654 additions and 269 deletions

View File

@@ -0,0 +1,76 @@
export const BUSINESS_TIME_ZONE = "Asia/Shanghai";
const BUSINESS_OFFSET = "+08:00";
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const EXPLICIT_OFFSET_PATTERN = /(?:Z|[+-]\d{2}:?\d{2})$/i;
export function parseApiDateTime(value?: string | Date | null): Date | null {
if (!value) return null;
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
const text = value.trim();
if (!text) return null;
const isoText = text.includes(" ") ? text.replace(" ", "T") : text;
const normalized = DATE_ONLY_PATTERN.test(isoText)
? `${isoText}T00:00:00${BUSINESS_OFFSET}`
: EXPLICIT_OFFSET_PATTERN.test(isoText)
? isoText
: `${isoText}Z`;
const parsed = new Date(normalized);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function businessParts(value?: string | Date | null) {
const parsed = parseApiDateTime(value);
if (!parsed) return null;
const parts = new Intl.DateTimeFormat("zh-CN", {
timeZone: BUSINESS_TIME_ZONE,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
hourCycle: "h23",
}).formatToParts(parsed);
return Object.fromEntries(parts.map((part) => [part.type, part.value]));
}
export function formatDate(value?: string | Date | null, fallback = "-") {
const parts = businessParts(value);
return parts ? `${parts.year}-${parts.month}-${parts.day}` : fallback;
}
export function formatTime(value?: string | Date | null, fallback = "-") {
const parts = businessParts(value);
return parts ? `${parts.hour}:${parts.minute}` : fallback;
}
export function formatDateTime(value?: string | Date | null, fallback = "-") {
const parts = businessParts(value);
return parts
? `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`
: fallback;
}
export function businessDateKey(value?: string | Date | null): string | null {
const parts = businessParts(value);
return parts ? `${parts.year}-${parts.month}-${parts.day}` : null;
}
export function formatSessionTime(
value?: string | Date | null,
now: Date = new Date(),
): string {
const targetKey = businessDateKey(value);
const todayKey = businessDateKey(now);
const time = formatTime(value, "");
if (!targetKey || !todayKey || !time) return "";
const dayNumber = (key: string) => {
const [year, month, day] = key.split("-").map(Number);
return Date.UTC(year, month - 1, day) / 86_400_000;
};
const days = dayNumber(todayKey) - dayNumber(targetKey);
if (days === 0) return time;
if (days === 1) return `昨天 ${time}`;
return `${targetKey.slice(5).replace("-", "/")} ${time}`;
}