| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- 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];
- }
|