/** * 到期预警计算工具 * * 各业务模块的预警阈值不同: * 车辆年检/保险、房屋租赁合同、设备校准、报表有效期 → 30 天 * 宽带与通信下次缴费 → 15 天 * 销售台账合同原件约定回传日期 → 提前 N 天(由项目管理配置决定) * 统一用 warnDays 传入,避免各页面各写一套日期差逻辑。 */ const MS_PER_DAY = 24 * 60 * 60 * 1000; /** * 解析后端返回的日期,兼容时间戳 / Date / 'YYYY-MM-DD' / 'YYYY/MM/DD' / 'YYYY-MM-DD HH:mm:ss' */ export function parseDate(value) { if (value === undefined || value === null || value === "") return null; if (value instanceof Date) { return Number.isNaN(value.getTime()) ? null : new Date(value.getTime()); } if (typeof value === "number") { const d = new Date(value); return Number.isNaN(d.getTime()) ? null : d; } const matched = String(value) .trim() .replace(/\//g, "-") .replace("T", " ") .match(/^(\d{4})-(\d{1,2})-(\d{1,2})/); if (!matched) return null; const date = new Date( Number(matched[1]), Number(matched[2]) - 1, Number(matched[3]) ); return Number.isNaN(date.getTime()) ? null : date; } /** * 距离到期日的剩余天数 * 正数=还有 N 天,0=今天到期,负数=已逾期 N 天,无法解析返回 null */ export function remainDays(dateStr) { const target = parseDate(dateStr); if (!target) return null; const today = new Date(); today.setHours(0, 0, 0, 0); target.setHours(0, 0, 0, 0); return Math.round((target.getTime() - today.getTime()) / MS_PER_DAY); } /** * 预警级别:overdue 已逾期 / warning 即将到期 / normal 正常 / none 无日期 */ export function warnLevel(dateStr, warnDays = 30) { const days = remainDays(dateStr); if (days === null) return "none"; if (days < 0) return "overdue"; if (days <= warnDays) return "warning"; return "normal"; } export function warnColor(level) { const colors = { overdue: "#f56c6c", warning: "#ff9900", normal: "#19be6b", none: "#909399", }; return colors[level] || colors.none; } export function warnText(level, days) { if (level === "overdue") return `已逾期${Math.abs(days)}天`; if (level === "warning") return days === 0 ? "今天到期" : `即将到期(${days}天)`; if (level === "normal") return "正常"; return ""; } /** * 一次拿到渲染预警标记所需的全部信息 * @returns {{days: number|null, level: string, color: string, text: string}} */ export function getWarnInfo(dateStr, warnDays = 30) { const days = remainDays(dateStr); const level = warnLevel(dateStr, warnDays); return { days, level, color: warnColor(level), text: warnText(level, days), }; } /** * 是否已逾期或即将到期(用于列表筛选、高亮) */ export function isExpiringSoon(dateStr, warnDays = 30) { const level = warnLevel(dateStr, warnDays); return level === "overdue" || level === "warning"; } /** * 按天/月推算日期,用于"下次缴费日期 = 安装日期 + 缴费周期月"这类自动计算 * @returns {string} 'YYYY-MM-DD',无法计算返回 '' */ export function addMonths(dateStr, months) { const base = parseDate(dateStr); if (!base || !Number.isFinite(Number(months))) return ""; const day = base.getDate(); base.setDate(1); base.setMonth(base.getMonth() + Number(months)); // 处理 1/31 + 1 月这类溢出,回退到当月最后一天 const lastDay = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate(); base.setDate(Math.min(day, lastDay)); const pad = n => String(n).padStart(2, "0"); return `${base.getFullYear()}-${pad(base.getMonth() + 1)}-${pad(base.getDate())}`; } /** * 生成 [start, end] 之间的月份列表(含首尾),用于房屋租赁的月度费用子表 * @returns {string[]} ['2026-01', '2026-02', ...] */ export function monthRange(startStr, endStr) { const start = parseDate(startStr); const end = parseDate(endStr); if (!start || !end || end < start) return []; const months = []; const cursor = new Date(start.getFullYear(), start.getMonth(), 1); const last = new Date(end.getFullYear(), end.getMonth(), 1); while (cursor <= last && months.length < 600) { const pad = n => String(n).padStart(2, "0"); months.push(`${cursor.getFullYear()}-${pad(cursor.getMonth() + 1)}`); cursor.setMonth(cursor.getMonth() + 1); } return months; }