gaoluyang
2 小时以前 e449a5408265e4bd1f6c66f5be28a42efac444ee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
 * 到期预警计算工具
 *
 * 各业务模块的预警阈值不同:
 *   车辆年检/保险、房屋租赁合同、设备校准、报表有效期 → 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;
}