/* Common helpers shared across pages (ES module). */ /** * Debounce a function call. * @param {Function} fn - function to debounce * @param {number} delay - delay in ms */ export function debounce(fn, delay) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } /** * Format an ISO date string to YYYY-MM-DD. * Returns empty string for falsy input. */ export function formatDate(isoString) { if (!isoString) return ""; const d = new Date(isoString); if (isNaN(d.getTime())) return String(isoString).slice(0, 10); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } /** Truncate text to maxLen chars, appending ellipsis. */ export function truncate(text, maxLen) { if (!text) return ""; const s = String(text); return s.length > maxLen ? s.slice(0, maxLen) + "…" : s; } /** Escape HTML special chars to prevent XSS when injecting via innerHTML. */ export function escapeHtml(text) { if (text === null || text === undefined) return ""; return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** Fetch JSON with basic error handling. */ export async function fetchJson(url) { const resp = await fetch(url); if (!resp.ok) { throw new Error(`请求失败: ${resp.status} ${resp.statusText}`); } return resp.json(); } /* Delegated retry: covers .js-retry-btn injected into error hints after load. */ document.addEventListener("click", (e) => { if (e.target.closest(".js-retry-btn")) location.reload(); });