78 lines
2.5 KiB
JavaScript
78 lines
2.5 KiB
JavaScript
/* Common helpers shared across pages (ES module). */
|
|
|
|
/** Debounce a function call. */
|
|
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. */
|
|
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, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/** Read the session CSRF token rendered into <meta name="csrf-token">. */
|
|
export function getCsrf() {
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
return meta ? meta.content : "";
|
|
}
|
|
|
|
/** 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();
|
|
}
|
|
|
|
/** POST JSON, injecting csrf_token into the body. Returns parsed JSON. */
|
|
export async function postJson(url, data) {
|
|
const resp = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ ...data, csrf_token: getCsrf() }),
|
|
});
|
|
const body = await resp.json().catch(() => ({}));
|
|
if (!resp.ok) {
|
|
throw new Error(body.error || `请求失败: ${resp.status}`);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/** Render a dismissible error alert into a container element. */
|
|
export function showError(container, msg) {
|
|
if (!container) return;
|
|
container.innerHTML =
|
|
'<div class="alert alert-danger alert-dismissible fade show" role="alert">'
|
|
+ escapeHtml(msg)
|
|
+ '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
|
}
|