util.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /* util.js —— 纯函数:转义、日期、排序、分组,不碰 DOM 也不需要数据 */
  2. (function () {
  3. "use strict";
  4. const ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
  5. function esc(v) {
  6. if (v == null) return "";
  7. return String(v).replace(/[&<>"']/g, (c) => ESC[c]);
  8. }
  9. function fmtDate(iso, withTime) {
  10. const d = toDate(iso);
  11. if (!d) return "—";
  12. const p = (n) => String(n).padStart(2, "0");
  13. const s = `${d.getMonth() + 1}月${d.getDate()}日`;
  14. return withTime ? `${s} ${p(d.getHours())}:${p(d.getMinutes())}` : s;
  15. }
  16. /* 相对时间,用于"最后活动" */
  17. function fmtAgo(iso) {
  18. const t = timeOf(iso);
  19. if (!t) return "—";
  20. const m = Math.round((Date.now() - t) / 60000);
  21. if (m < 1) return "刚刚";
  22. if (m < 60) return `${m} 分钟前`;
  23. const h = Math.round(m / 60);
  24. if (h < 24) return `${h} 小时前`;
  25. return fmtDate(iso);
  26. }
  27. /* Parse 的 Date 字段在 REST 里是 {"__type":"Date","iso":"…"}。
  28. * 直接 new Date(对象) 得到 Invalid Date —— 之前"最后活动时间"全部丢失就是这个原因。
  29. * 统一在这里拆:Date 对象 / ISO 串 / Parse 日期对象都能读。 */
  30. function toDate(d) {
  31. if (!d) return null;
  32. if (d instanceof Date) return isNaN(d) ? null : d;
  33. if (typeof d === "object" && typeof d.iso === "string") d = d.iso;
  34. const dt = new Date(d);
  35. return isNaN(dt) ? null : dt;
  36. }
  37. function timeOf(d) {
  38. const dt = toDate(d);
  39. return dt ? dt.getTime() : 0;
  40. }
  41. /* 最近活动时间:Date 字段或 ISO 串,取最大 */
  42. function latest(dates) {
  43. let best = 0;
  44. for (const d of dates) {
  45. const t = timeOf(d);
  46. if (t > best) best = t;
  47. }
  48. return best ? new Date(best).toISOString() : null;
  49. }
  50. function countBy(arr, keyFn) {
  51. const m = new Map();
  52. for (const x of arr) {
  53. const k = keyFn(x);
  54. m.set(k, (m.get(k) || 0) + 1);
  55. }
  56. return m;
  57. }
  58. function groupBy(arr, keyFn) {
  59. const m = new Map();
  60. for (const x of arr) {
  61. const k = keyFn(x);
  62. if (!m.has(k)) m.set(k, []);
  63. m.get(k).push(x);
  64. }
  65. return m;
  66. }
  67. /* 排序工具:空值永远沉底 */
  68. function cmpNum(a, b, dir) {
  69. const ea = a == null || isNaN(a), eb = b == null || isNaN(b);
  70. if (ea && eb) return 0;
  71. if (ea) return 1;
  72. if (eb) return -1;
  73. return dir * (a - b);
  74. }
  75. function initials(name) {
  76. if (!name) return "?";
  77. return Array.from(String(name)).slice(-2).join("");
  78. }
  79. function shortId(prefix, idx) {
  80. return `${prefix}-${String(idx + 1).padStart(2, "0")}`;
  81. }
  82. /* 名称包含匹配,忽略大小写;空查询视为命中(用于 ?focus= 这种"定位某一条"的场景) */
  83. function matches(hay, q) {
  84. if (!q) return true;
  85. return String(hay || "").toLowerCase().includes(q);
  86. }
  87. window.KF_UTIL = { esc, toDate, timeOf, fmtDate, fmtAgo, latest, countBy, groupBy, cmpNum, initials, shortId, matches };
  88. })();