app_common.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. function getCookie(name) {
  2. const raw = document.cookie || "";
  3. const parts = raw.split(";");
  4. for (const p of parts) {
  5. const s = p.trim();
  6. if (!s) continue;
  7. const idx = s.indexOf("=");
  8. if (idx <= 0) continue;
  9. const k = s.slice(0, idx).trim();
  10. if (k !== name) continue;
  11. return decodeURIComponent(s.slice(idx + 1));
  12. }
  13. return "";
  14. }
  15. async function apiFetch(url, { method = "GET", body } = {}) {
  16. const init = { method, headers: {} };
  17. if (!["GET", "HEAD", "OPTIONS", "TRACE"].includes(String(method || "GET").toUpperCase())) {
  18. const csrf = getCookie("csrf_token");
  19. if (csrf) init.headers["X-CSRF-Token"] = csrf;
  20. }
  21. if (body !== undefined) {
  22. init.headers["Content-Type"] = "application/json";
  23. init.body = JSON.stringify(body);
  24. }
  25. const resp = await fetch(url, init);
  26. const contentType = resp.headers.get("content-type") || "";
  27. const isJson = contentType.includes("application/json");
  28. if (!resp.ok) {
  29. let detail = null;
  30. if (isJson) {
  31. try {
  32. detail = await resp.json();
  33. } catch (e) {
  34. detail = null;
  35. }
  36. }
  37. const err = new Error("request_failed");
  38. err.status = resp.status;
  39. err.detail = detail;
  40. throw err;
  41. }
  42. if (isJson) return await resp.json();
  43. return resp;
  44. }
  45. const Toast = Swal.mixin({
  46. toast: true,
  47. position: 'top-end',
  48. showConfirmButton: false,
  49. timer: 3000,
  50. timerProgressBar: true,
  51. didOpen: (toast) => {
  52. toast.addEventListener('mouseenter', Swal.stopTimer)
  53. toast.addEventListener('mouseleave', Swal.resumeTimer)
  54. }
  55. });
  56. function showToastError(text) {
  57. Toast.fire({
  58. icon: 'error',
  59. title: String(text || "未知错误")
  60. });
  61. }
  62. function showToastSuccess(text) {
  63. Toast.fire({
  64. icon: 'success',
  65. title: String(text || "操作成功")
  66. });
  67. }
  68. function currentNextParam() {
  69. return encodeURIComponent(window.location.pathname + window.location.search);
  70. }
  71. function nextFromQuery() {
  72. const p = new URLSearchParams(window.location.search || "");
  73. const next = (p.get("next") || "").trim();
  74. if (!next) return "";
  75. if (!next.startsWith("/")) return "";
  76. if (next.startsWith("//")) return "";
  77. return next;
  78. }
  79. async function initTopbar() {
  80. const navAuth = document.getElementById("navAuth");
  81. const navLogin = document.getElementById("navLogin");
  82. const navRegister = document.getElementById("navRegister");
  83. if (navLogin) navLogin.setAttribute("href", `/ui/login?next=${currentNextParam()}`);
  84. if (navRegister) navRegister.setAttribute("href", `/ui/register?next=${currentNextParam()}`);
  85. if (!navAuth) return;
  86. let msgBadge = null;
  87. async function refreshMessageBadge() {
  88. if (!msgBadge) return;
  89. try {
  90. const data = await apiFetch("/me/messages?page=1&pageSize=1");
  91. const cnt = parseInt(data?.unreadCount || 0, 10) || 0;
  92. if (cnt > 0) {
  93. msgBadge.style.display = "inline-flex";
  94. msgBadge.textContent = cnt > 99 ? "99+" : String(cnt);
  95. } else {
  96. msgBadge.style.display = "none";
  97. msgBadge.textContent = "";
  98. }
  99. } catch (e) {
  100. msgBadge.style.display = "none";
  101. msgBadge.textContent = "";
  102. }
  103. }
  104. let me = null;
  105. try {
  106. me = await apiFetch("/me");
  107. } catch (e) {
  108. me = { user: null };
  109. }
  110. const user = me && me.user ? me.user : null;
  111. if (!user) return;
  112. navAuth.innerHTML = "";
  113. const userEl = el(
  114. "a",
  115. { class: "nav-user", href: "/ui/me", style: "display:flex; align-items:center; gap:4px;" },
  116. el("i", { class: "ri-user-smile-line" }),
  117. el("span", {}, String(user.phone || "我的"))
  118. );
  119. if (user.vipActive) userEl.appendChild(el("span", { class: "nav-badge nav-badge-vip" }, "VIP"));
  120. msgBadge = el("span", { class: "nav-msg-badge" }, "");
  121. const msgLink = el("a", { href: "/ui/messages", class: "btn-ghost nav-msg", title: "消息通知" }, el("i", { class: "ri-notification-3-line" }), msgBadge);
  122. 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"}), "退出");
  123. logout.addEventListener("click", async (evt) => {
  124. evt.preventDefault();
  125. try {
  126. await apiFetch("/auth/logout", { method: "POST" });
  127. } catch (e) {}
  128. window.location.href = "/";
  129. });
  130. navAuth.appendChild(userEl);
  131. navAuth.appendChild(msgLink);
  132. navAuth.appendChild(logout);
  133. window.__refreshMessageBadge = refreshMessageBadge;
  134. await refreshMessageBadge();
  135. }
  136. window.addEventListener("error", (evt) => {
  137. const msg = evt?.error?.message || evt?.message || "页面脚本错误";
  138. showToastError(msg);
  139. });
  140. window.addEventListener("unhandledrejection", (evt) => {
  141. const msg = evt?.reason?.message || String(evt?.reason || "异步错误");
  142. showToastError(msg);
  143. });
  144. function el(tag, attrs = {}, ...children) {
  145. const node = document.createElement(tag);
  146. Object.entries(attrs).forEach(([k, v]) => {
  147. if (k === "class") node.className = v;
  148. else if (k === "html") node.innerHTML = v;
  149. else if (k.startsWith("on") && typeof v === "function") node.addEventListener(k.slice(2), v);
  150. else node.setAttribute(k, v);
  151. });
  152. children.forEach((c) => {
  153. if (c === null || c === undefined) return;
  154. if (typeof c === "string") node.appendChild(document.createTextNode(c));
  155. else node.appendChild(c);
  156. });
  157. return node;
  158. }
  159. const DEFAULT_COVER_URL = "/static/images/resources/default.png";
  160. function ensureImgFallback(img, fallbackSrc = DEFAULT_COVER_URL, placeholderClass = "") {
  161. if (!img) return;
  162. const fb = String(fallbackSrc || DEFAULT_COVER_URL);
  163. img.addEventListener("error", () => {
  164. if (img.getAttribute("data-fallback-applied") === "1") return;
  165. img.setAttribute("data-fallback-applied", "1");
  166. if (placeholderClass) img.classList.add(placeholderClass);
  167. img.setAttribute("src", fb);
  168. });
  169. }
  170. function btnGroup(...children) {
  171. return el("div", { class: "btn-group" }, ...children);
  172. }
  173. function renderEmptyRow(tbody, colCount, text) {
  174. tbody.appendChild(el("tr", {}, el("td", { colspan: String(colCount), class: "table-empty muted" }, text)));
  175. }
  176. function badge(text, variantClass = "") {
  177. const cls = ["badge", variantClass].filter(Boolean).join(" ");
  178. return el("span", { class: cls }, text);
  179. }
  180. function resourceTypeBadge(type) {
  181. if (type === "VIP") return badge("VIP", "badge-vip");
  182. return badge("免费", "badge-free");
  183. }
  184. function resourceStatusBadge(status) {
  185. if (status === "ONLINE") return badge("上架", "badge-success");
  186. if (status === "OFFLINE") return badge("下架", "badge-danger");
  187. return badge("草稿", "badge");
  188. }
  189. function orderStatusBadge(status) {
  190. if (status === "PAID") return badge("已支付", "badge-success");
  191. if (status === "PENDING") return badge("待支付", "badge-warning");
  192. if (status === "FAILED") return badge("失败", "badge-danger");
  193. return badge("已关闭", "badge");
  194. }
  195. function userStatusBadge(status) {
  196. if (status === "ACTIVE") return badge("启用", "badge-success");
  197. return badge("禁用", "badge-danger");
  198. }
  199. function formatCents(cents) {
  200. return `¥${(cents / 100).toFixed(2)}`;
  201. }
  202. function formatDateTime(value) {
  203. if (value === null || value === undefined) return "-";
  204. if (value === "-") return "-";
  205. if (typeof value === "number") {
  206. const ms = value < 1e12 ? value * 1000 : value;
  207. const d = new Date(ms);
  208. if (Number.isNaN(d.getTime())) return String(value);
  209. const pad2 = (n) => String(n).padStart(2, "0");
  210. return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
  211. }
  212. const s = String(value).trim();
  213. if (!s) return "-";
  214. if (/^\d+$/.test(s)) return formatDateTime(Number(s));
  215. const d = new Date(s);
  216. if (Number.isNaN(d.getTime())) return s;
  217. const pad2 = (n) => String(n).padStart(2, "0");
  218. return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
  219. }
  220. function formatMessageText(text) {
  221. const s = String(text || "");
  222. const isoRe = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/g;
  223. return s.replace(isoRe, (m) => formatDateTime(m));
  224. }
  225. function loadScriptOnce(src) {
  226. const url = String(src || "").trim();
  227. if (!url) return Promise.resolve();
  228. const store = (window.__scriptLoaders = window.__scriptLoaders || {});
  229. if (store[url]) return store[url];
  230. store[url] = new Promise((resolve, reject) => {
  231. const s = document.createElement("script");
  232. s.async = true;
  233. s.src = url;
  234. s.onload = () => resolve();
  235. s.onerror = () => reject(new Error("script_load_failed"));
  236. document.head.appendChild(s);
  237. });
  238. return store[url];
  239. }