function getCookie(name) { const raw = document.cookie || ""; const parts = raw.split(";"); for (const p of parts) { const s = p.trim(); if (!s) continue; const idx = s.indexOf("="); if (idx <= 0) continue; const k = s.slice(0, idx).trim(); if (k !== name) continue; return decodeURIComponent(s.slice(idx + 1)); } return ""; } async function apiFetch(url, { method = "GET", body } = {}) { const init = { method, headers: {} }; if (!["GET", "HEAD", "OPTIONS", "TRACE"].includes(String(method || "GET").toUpperCase())) { const csrf = getCookie("csrf_token"); if (csrf) init.headers["X-CSRF-Token"] = csrf; } if (body !== undefined) { init.headers["Content-Type"] = "application/json"; init.body = JSON.stringify(body); } const resp = await fetch(url, init); const contentType = resp.headers.get("content-type") || ""; const isJson = contentType.includes("application/json"); if (!resp.ok) { let detail = null; if (isJson) { try { detail = await resp.json(); } catch (e) { detail = null; } } const err = new Error("request_failed"); err.status = resp.status; err.detail = detail; throw err; } if (isJson) return await resp.json(); return resp; } const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, timerProgressBar: true, didOpen: (toast) => { toast.addEventListener('mouseenter', Swal.stopTimer) toast.addEventListener('mouseleave', Swal.resumeTimer) } }); function showToastError(text) { Toast.fire({ icon: 'error', title: String(text || "未知错误") }); } function showToastSuccess(text) { Toast.fire({ icon: 'success', title: String(text || "操作成功") }); } function currentNextParam() { return encodeURIComponent(window.location.pathname + window.location.search); } function nextFromQuery() { const p = new URLSearchParams(window.location.search || ""); const next = (p.get("next") || "").trim(); if (!next) return ""; if (!next.startsWith("/")) return ""; if (next.startsWith("//")) return ""; return next; } async function initTopbar() { const navAuth = document.getElementById("navAuth"); const navLogin = document.getElementById("navLogin"); const navRegister = document.getElementById("navRegister"); if (navLogin) navLogin.setAttribute("href", `/ui/login?next=${currentNextParam()}`); if (navRegister) navRegister.setAttribute("href", `/ui/register?next=${currentNextParam()}`); if (!navAuth) return; let msgBadge = null; async function refreshMessageBadge() { if (!msgBadge) return; try { const data = await apiFetch("/me/messages?page=1&pageSize=1"); const cnt = parseInt(data?.unreadCount || 0, 10) || 0; if (cnt > 0) { msgBadge.style.display = "inline-flex"; msgBadge.textContent = cnt > 99 ? "99+" : String(cnt); } else { msgBadge.style.display = "none"; msgBadge.textContent = ""; } } catch (e) { msgBadge.style.display = "none"; msgBadge.textContent = ""; } } let me = null; try { me = await apiFetch("/me"); } catch (e) { me = { user: null }; } const user = me && me.user ? me.user : null; if (!user) return; navAuth.innerHTML = ""; const userEl = el( "a", { class: "nav-user", href: "/ui/me", style: "display:flex; align-items:center; gap:4px;" }, el("i", { class: "ri-user-smile-line" }), el("span", {}, String(user.phone || "我的")) ); if (user.vipActive) userEl.appendChild(el("span", { class: "nav-badge nav-badge-vip" }, "VIP")); msgBadge = el("span", { class: "nav-msg-badge" }, ""); const msgLink = el("a", { href: "/ui/messages", class: "btn-ghost nav-msg", title: "消息通知" }, el("i", { class: "ri-notification-3-line" }), msgBadge); const logout = el("a", { href: "#", class: "btn-ghost", style: "padding:6px 10px; border-radius: 8px; display:flex; align-items:center; gap:4px;" }, el("i", {class: "ri-logout-box-r-line"}), "退出"); logout.addEventListener("click", async (evt) => { evt.preventDefault(); try { await apiFetch("/auth/logout", { method: "POST" }); } catch (e) {} window.location.href = "/"; }); navAuth.appendChild(userEl); navAuth.appendChild(msgLink); navAuth.appendChild(logout); window.__refreshMessageBadge = refreshMessageBadge; await refreshMessageBadge(); } window.addEventListener("error", (evt) => { const msg = evt?.error?.message || evt?.message || "页面脚本错误"; showToastError(msg); }); window.addEventListener("unhandledrejection", (evt) => { const msg = evt?.reason?.message || String(evt?.reason || "异步错误"); showToastError(msg); }); function el(tag, attrs = {}, ...children) { const node = document.createElement(tag); Object.entries(attrs).forEach(([k, v]) => { if (k === "class") node.className = v; else if (k === "html") node.innerHTML = v; else if (k.startsWith("on") && typeof v === "function") node.addEventListener(k.slice(2), v); else node.setAttribute(k, v); }); children.forEach((c) => { if (c === null || c === undefined) return; if (typeof c === "string") node.appendChild(document.createTextNode(c)); else node.appendChild(c); }); return node; } const DEFAULT_COVER_URL = "/static/images/resources/default.png"; function ensureImgFallback(img, fallbackSrc = DEFAULT_COVER_URL, placeholderClass = "") { if (!img) return; const fb = String(fallbackSrc || DEFAULT_COVER_URL); img.addEventListener("error", () => { if (img.getAttribute("data-fallback-applied") === "1") return; img.setAttribute("data-fallback-applied", "1"); if (placeholderClass) img.classList.add(placeholderClass); img.setAttribute("src", fb); }); } function btnGroup(...children) { return el("div", { class: "btn-group" }, ...children); } function renderEmptyRow(tbody, colCount, text) { tbody.appendChild(el("tr", {}, el("td", { colspan: String(colCount), class: "table-empty muted" }, text))); } function badge(text, variantClass = "") { const cls = ["badge", variantClass].filter(Boolean).join(" "); return el("span", { class: cls }, text); } function resourceTypeBadge(type) { if (type === "VIP") return badge("VIP", "badge-vip"); return badge("免费", "badge-free"); } function resourceStatusBadge(status) { if (status === "ONLINE") return badge("上架", "badge-success"); if (status === "OFFLINE") return badge("下架", "badge-danger"); return badge("草稿", "badge"); } function orderStatusBadge(status) { if (status === "PAID") return badge("已支付", "badge-success"); if (status === "PENDING") return badge("待支付", "badge-warning"); if (status === "FAILED") return badge("失败", "badge-danger"); return badge("已关闭", "badge"); } function userStatusBadge(status) { if (status === "ACTIVE") return badge("启用", "badge-success"); return badge("禁用", "badge-danger"); } function formatCents(cents) { return `¥${(cents / 100).toFixed(2)}`; } function formatDateTime(value) { if (value === null || value === undefined) return "-"; if (value === "-") return "-"; if (typeof value === "number") { const ms = value < 1e12 ? value * 1000 : value; const d = new Date(ms); if (Number.isNaN(d.getTime())) return String(value); const pad2 = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`; } const s = String(value).trim(); if (!s) return "-"; if (/^\d+$/.test(s)) return formatDateTime(Number(s)); const d = new Date(s); if (Number.isNaN(d.getTime())) return s; const pad2 = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`; } function formatMessageText(text) { const s = String(text || ""); const isoRe = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/g; return s.replace(isoRe, (m) => formatDateTime(m)); } function loadScriptOnce(src) { const url = String(src || "").trim(); if (!url) return Promise.resolve(); const store = (window.__scriptLoaders = window.__scriptLoaders || {}); if (store[url]) return store[url]; store[url] = new Promise((resolve, reject) => { const s = document.createElement("script"); s.async = true; s.src = url; s.onload = () => resolve(); s.onerror = () => reject(new Error("script_load_failed")); document.head.appendChild(s); }); return store[url]; }