initial: novel-app snapshot

This commit is contained in:
omo
2026-08-17 17:11:30 +08:00
commit 1ae06209ac
34 changed files with 2821 additions and 0 deletions

77
static/js/utils.js Normal file
View 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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/** 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>';
}