export const DEFAULT_OVERVIEW_RANGE = '近10日' const DAY_MS = 24 * 60 * 60 * 1000 const RISK_DAILY_TREND_MAX_BUCKETS = 14 function parseLocalDate(value) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '').trim()) if (!match) { return null } const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])) return Number.isNaN(date.getTime()) ? null : date } function clampWindowDays(value) { const days = Number(value || 0) if (!Number.isFinite(days) || days <= 0) { return 10 } return Math.max(1, Math.min(Math.round(days), 90)) } function resolveCustomRangeDays(customRange = {}) { const start = parseLocalDate(customRange.start) const end = parseLocalDate(customRange.end) if (!start || !end) { return 10 } return clampWindowDays(Math.abs(end.getTime() - start.getTime()) / DAY_MS + 1) } export function resolveTopRangeDays(range, customRange = {}) { const key = String(range || DEFAULT_OVERVIEW_RANGE).trim() if (key === 'custom') { return resolveCustomRangeDays(customRange) } if (key === '\u4eca\u65e5') { return 1 } if (key === '\u672c\u5468') { const today = new Date() const weekday = today.getDay() || 7 return clampWindowDays(weekday) } if (key === '\u672c\u6708') { return clampWindowDays(new Date().getDate()) } const match = key.match(/\d+/) return clampWindowDays(match ? Number(match[0]) : 10) } export function resolveTopRangeKey(range, customRange = {}) { const key = String(range || DEFAULT_OVERVIEW_RANGE).trim() if (key === 'custom') { return 'custom' } if (key === '\u672c\u5468' || key === '\u4eca\u65e5') { return `recent-${resolveTopRangeDays(key, customRange)}-days` } if (/\d+/.test(key)) { return `recent-${resolveTopRangeDays(key, customRange)}-days` } return key || DEFAULT_OVERVIEW_RANGE } function formatRiskTrendDateLabel(value) { const date = parseLocalDate(value) if (!date) { return String(value || '-').trim() || '-' } const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${month}-${day}` } function buildRiskTrendBucketLabel(first, last) { const start = String(first?.date || '').trim() const end = String(last?.date || '').trim() if (!start || start === end) { return formatRiskTrendDateLabel(start) } return `${formatRiskTrendDateLabel(start)}~${formatRiskTrendDateLabel(end)}` } function normalizeRiskTrendRow(item) { return { date: String(item.date || '').trim() || '-', total: Number(item.total || 0), highOrAbove: Number(item.high_or_above ?? item.highOrAbove ?? 0) } } export function aggregateRiskDailyTrendRows(rows, maxBuckets = RISK_DAILY_TREND_MAX_BUCKETS) { const normalizedRows = rows .map(normalizeRiskTrendRow) .filter((item) => item.date !== '-' || item.total > 0 || item.highOrAbove > 0) if (normalizedRows.length <= maxBuckets) { return normalizedRows.map((item) => ({ ...item, date: formatRiskTrendDateLabel(item.date), sourceStartDate: item.date, sourceEndDate: item.date })) } const bucketSize = Math.ceil(normalizedRows.length / maxBuckets) const buckets = [] for (let index = 0; index < normalizedRows.length; index += bucketSize) { const bucketRows = normalizedRows.slice(index, index + bucketSize) const first = bucketRows[0] const last = bucketRows[bucketRows.length - 1] buckets.push({ date: buildRiskTrendBucketLabel(first, last), sourceStartDate: first?.date || '', sourceEndDate: last?.date || '', total: bucketRows.reduce((sum, item) => sum + item.total, 0), highOrAbove: bucketRows.reduce((sum, item) => sum + item.highOrAbove, 0) }) } return buckets }