/* util.js —— 纯函数:转义、日期、排序、分组,不碰 DOM 也不需要数据 */ (function () { "use strict"; const ESC = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; function esc(v) { if (v == null) return ""; return String(v).replace(/[&<>"']/g, (c) => ESC[c]); } function fmtDate(iso, withTime) { const d = toDate(iso); if (!d) return "—"; const p = (n) => String(n).padStart(2, "0"); const s = `${d.getMonth() + 1}月${d.getDate()}日`; return withTime ? `${s} ${p(d.getHours())}:${p(d.getMinutes())}` : s; } /* 相对时间,用于"最后活动" */ function fmtAgo(iso) { const t = timeOf(iso); if (!t) return "—"; const m = Math.round((Date.now() - t) / 60000); if (m < 1) return "刚刚"; if (m < 60) return `${m} 分钟前`; const h = Math.round(m / 60); if (h < 24) return `${h} 小时前`; return fmtDate(iso); } /* Parse 的 Date 字段在 REST 里是 {"__type":"Date","iso":"…"}。 * 直接 new Date(对象) 得到 Invalid Date —— 之前"最后活动时间"全部丢失就是这个原因。 * 统一在这里拆:Date 对象 / ISO 串 / Parse 日期对象都能读。 */ function toDate(d) { if (!d) return null; if (d instanceof Date) return isNaN(d) ? null : d; if (typeof d === "object" && typeof d.iso === "string") d = d.iso; const dt = new Date(d); return isNaN(dt) ? null : dt; } function timeOf(d) { const dt = toDate(d); return dt ? dt.getTime() : 0; } /* 最近活动时间:Date 字段或 ISO 串,取最大 */ function latest(dates) { let best = 0; for (const d of dates) { const t = timeOf(d); if (t > best) best = t; } return best ? new Date(best).toISOString() : null; } function countBy(arr, keyFn) { const m = new Map(); for (const x of arr) { const k = keyFn(x); m.set(k, (m.get(k) || 0) + 1); } return m; } function groupBy(arr, keyFn) { const m = new Map(); for (const x of arr) { const k = keyFn(x); if (!m.has(k)) m.set(k, []); m.get(k).push(x); } return m; } /* 排序工具:空值永远沉底 */ function cmpNum(a, b, dir) { const ea = a == null || isNaN(a), eb = b == null || isNaN(b); if (ea && eb) return 0; if (ea) return 1; if (eb) return -1; return dir * (a - b); } function initials(name) { if (!name) return "?"; return Array.from(String(name)).slice(-2).join(""); } function shortId(prefix, idx) { return `${prefix}-${String(idx + 1).padStart(2, "0")}`; } /* 名称包含匹配,忽略大小写;空查询视为命中(用于 ?focus= 这种"定位某一条"的场景) */ function matches(hay, q) { if (!q) return true; return String(hay || "").toLowerCase().includes(q); } window.KF_UTIL = { esc, toDate, timeOf, fmtDate, fmtAgo, latest, countBy, groupBy, cmpNum, initials, shortId, matches }; })();