initial: novel-app snapshot
This commit is contained in:
77
static/js/utils.js
Normal file
77
static/js/utils.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/* 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>';
|
||||
}
|
||||
Reference in New Issue
Block a user