app_user.js 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142
  1. async function pageIndex() {
  2. const pageMode = document.body.getAttribute("data-page") || "";
  3. const isHome = pageMode === "index";
  4. const qInput = document.getElementById("q");
  5. const typeSelect = document.getElementById("type");
  6. const sortSelect = document.getElementById("sort");
  7. const list = document.getElementById("resourceList");
  8. const prevBtn = document.getElementById("prevPage");
  9. const nextBtn = document.getElementById("nextPage");
  10. const pageInfo = document.getElementById("pageInfo");
  11. const searchBtn = document.getElementById("searchBtn");
  12. const pager = document.getElementById("pager");
  13. const homeMore = document.getElementById("homeMore");
  14. let page = 1;
  15. function setLoading(loading) {
  16. if (searchBtn) searchBtn.disabled = loading;
  17. if (prevBtn) prevBtn.disabled = loading || page <= 1;
  18. if (nextBtn) nextBtn.disabled = loading;
  19. if (qInput) qInput.disabled = loading;
  20. if (typeSelect) typeSelect.disabled = loading;
  21. if (sortSelect) sortSelect.disabled = loading;
  22. }
  23. function skeletonCard() {
  24. const cover = el("div", { class: "resource-card-cover skeleton" });
  25. const line1 = el("div", { class: "skeleton skeleton-line" });
  26. const line2 = el("div", { class: "skeleton skeleton-line" });
  27. line1.style.width = "70%";
  28. line2.style.width = "90%";
  29. const stats = el("div", { class: "toolbar" }, el("div", { class: "skeleton skeleton-pill" }), el("div", { class: "skeleton skeleton-pill" }));
  30. return el("div", { class: "card" }, cover, line1, line2, stats);
  31. }
  32. function renderSkeleton(count) {
  33. list.innerHTML = "";
  34. for (let i = 0; i < count; i += 1) list.appendChild(skeletonCard());
  35. }
  36. function renderEmpty(text) {
  37. list.innerHTML = "";
  38. list.appendChild(
  39. el(
  40. "div",
  41. { class: "card", style: "grid-column: 1 / -1; text-align:center" },
  42. el("div", {}, text || "暂无数据"),
  43. el(
  44. "div",
  45. { class: "toolbar", style: "justify-content:center" },
  46. el(
  47. "button",
  48. {
  49. class: "btn",
  50. onclick: async () => {
  51. if (qInput) qInput.value = "";
  52. if (typeSelect) typeSelect.value = "";
  53. if (sortSelect) sortSelect.value = "latest";
  54. page = 1;
  55. await load();
  56. },
  57. },
  58. "清空筛选"
  59. )
  60. )
  61. )
  62. );
  63. }
  64. async function load() {
  65. if (!list) return;
  66. renderSkeleton(isHome ? 6 : 9);
  67. setLoading(true);
  68. const q = (qInput ? qInput.value : "").trim();
  69. const type = (typeSelect ? typeSelect.value : "").trim();
  70. const sort = (sortSelect ? sortSelect.value : "").trim() || "latest";
  71. const params = new URLSearchParams();
  72. if (q) params.set("q", q);
  73. if (type) params.set("type", type);
  74. if (!isHome) params.set("sort", sort);
  75. params.set("page", String(page));
  76. params.set("pageSize", isHome ? "9" : "12");
  77. let data = null;
  78. try {
  79. data = await apiFetch(`/resources?${params.toString()}`);
  80. } finally {
  81. setLoading(false);
  82. }
  83. list.innerHTML = "";
  84. const items = (data && data.items) || [];
  85. if (!items.length) {
  86. renderEmpty(q || type ? "没有找到匹配的资源" : "暂无资源");
  87. if (pageInfo) pageInfo.textContent = "";
  88. if (pager) pager.style.display = isHome ? "none" : "";
  89. if (homeMore) homeMore.style.display = isHome ? "" : "none";
  90. return;
  91. }
  92. items.forEach((r) => {
  93. const badgeClass = r.type === "VIP" ? "badge badge-vip" : "badge badge-free";
  94. const badgeIcon = r.type === "VIP" ? "ri-vip-crown-line" : "ri-price-tag-3-line";
  95. const coverImg = r.coverUrl ? el("img", { class: "resource-card-cover-img", src: r.coverUrl, alt: r.title }) : null;
  96. if (coverImg) ensureImgFallback(coverImg);
  97. const cover = coverImg ? el("div", { class: "resource-card-cover" }, coverImg) : null;
  98. const tags = Array.isArray(r.tags) ? r.tags.slice(0, 4) : [];
  99. const tagRow = tags.length ? el("div", { class: "resource-card-tags" }, ...tags.map((t) => badge(t, "badge"))) : null;
  100. list.appendChild(
  101. el(
  102. "div",
  103. { class: "card", style: "display:flex; flex-direction:column; height:100%;" },
  104. cover,
  105. el(
  106. "div",
  107. { style: "flex:1;" },
  108. el("span", { class: badgeClass, style: "float:right; display:flex; align-items:center; gap:4px;" }, el("i", {class: badgeIcon}), r.type === "VIP" ? "VIP" : "FREE"),
  109. el("h3", { style: "margin-top:0;" }, r.title)
  110. ),
  111. tagRow,
  112. el("div", { class: "muted", style: "margin-bottom:16px; flex:1;" }, (r.summary || "").slice(0, 80)),
  113. el(
  114. "div",
  115. { class: "toolbar", style: "margin-top:auto;" },
  116. el("a", { class: "btn btn-primary", style: "display:flex; align-items:center; gap:4px;", href: `/ui/resources/${r.id}` }, el("i", {class: "ri-eye-line"}), "查看详情"),
  117. el("span", { class: "muted", style: "display:flex; align-items:center; gap:8px; font-size:0.9rem;" },
  118. el("span", {style:"display:flex; align-items:center; gap:2px;"}, el("i", {class: "ri-bar-chart-box-line"}), String(r.viewCount)),
  119. el("span", {style:"display:flex; align-items:center; gap:2px;"}, el("i", {class: "ri-download-cloud-2-line"}), String(r.downloadCount))
  120. )
  121. )
  122. )
  123. );
  124. });
  125. const totalPages = Math.max(Math.ceil((data.total || 0) / (data.pageSize || 12)), 1);
  126. if (pageInfo) pageInfo.textContent = isHome ? "" : `第 ${data.page} / ${totalPages} 页,共 ${data.total} 条`;
  127. if (prevBtn) prevBtn.disabled = isHome || page <= 1;
  128. if (nextBtn) nextBtn.disabled = isHome || page >= totalPages;
  129. if (pager) pager.style.display = isHome ? "none" : "";
  130. if (homeMore) homeMore.style.display = isHome ? "" : "none";
  131. }
  132. if (prevBtn)
  133. prevBtn.addEventListener("click", async () => {
  134. page = Math.max(page - 1, 1);
  135. await load();
  136. });
  137. if (nextBtn)
  138. nextBtn.addEventListener("click", async () => {
  139. page += 1;
  140. await load();
  141. });
  142. if (searchBtn)
  143. searchBtn.addEventListener("click", async () => {
  144. page = 1;
  145. await load();
  146. });
  147. if (sortSelect)
  148. sortSelect.addEventListener("change", async () => {
  149. page = 1;
  150. await load();
  151. });
  152. if (qInput)
  153. qInput.addEventListener("keydown", async (e) => {
  154. if (e.key !== "Enter") return;
  155. page = 1;
  156. await load();
  157. });
  158. await load();
  159. }
  160. async function pageLogin() {
  161. const phone = document.getElementById("phone");
  162. const password = document.getElementById("password");
  163. const btn = document.getElementById("loginBtn");
  164. const msg = document.getElementById("msg");
  165. const toRegister = document.getElementById("toRegister");
  166. const next = nextFromQuery();
  167. if (toRegister) toRegister.setAttribute("href", `/ui/register?next=${next ? encodeURIComponent(next) : currentNextParam()}`);
  168. function setMsg(text) {
  169. if (!msg) return;
  170. if (!text) {
  171. msg.style.display = "none";
  172. msg.textContent = "";
  173. return;
  174. }
  175. msg.style.display = "";
  176. msg.textContent = String(text);
  177. }
  178. async function submit() {
  179. setMsg("");
  180. const phoneVal = String(phone.value || "").trim();
  181. const passwordVal = String(password.value || "");
  182. if (!/^\d{6,20}$/.test(phoneVal)) {
  183. setMsg("请输入正确的手机号");
  184. phone.focus();
  185. return;
  186. }
  187. if (!passwordVal) {
  188. setMsg("请输入密码");
  189. password.focus();
  190. return;
  191. }
  192. const original = btn.textContent;
  193. btn.disabled = true;
  194. btn.textContent = "登录中…";
  195. try {
  196. await apiFetch("/auth/login", {
  197. method: "POST",
  198. body: { phone: phoneVal, password: passwordVal },
  199. });
  200. showToastSuccess("登录成功");
  201. window.location.href = next || "/ui/me";
  202. } catch (e) {
  203. setMsg(`登录失败:${e.detail?.error || e.status || "unknown"}`);
  204. } finally {
  205. btn.disabled = false;
  206. btn.textContent = original;
  207. }
  208. }
  209. try {
  210. const me = await apiFetch("/me");
  211. if (me && me.user) {
  212. window.location.href = next || "/ui/me";
  213. return;
  214. }
  215. } catch (e) {}
  216. btn.addEventListener("click", submit);
  217. phone.addEventListener("keydown", (e) => {
  218. if (e.key === "Enter") submit();
  219. });
  220. password.addEventListener("keydown", (e) => {
  221. if (e.key === "Enter") submit();
  222. });
  223. }
  224. async function pageRegister() {
  225. const phone = document.getElementById("phone");
  226. const password = document.getElementById("password");
  227. const btn = document.getElementById("registerBtn");
  228. const msg = document.getElementById("msg");
  229. const toLogin = document.getElementById("toLogin");
  230. const next = nextFromQuery();
  231. if (toLogin) toLogin.setAttribute("href", `/ui/login?next=${next ? encodeURIComponent(next) : currentNextParam()}`);
  232. function setMsg(text) {
  233. if (!msg) return;
  234. if (!text) {
  235. msg.style.display = "none";
  236. msg.textContent = "";
  237. return;
  238. }
  239. msg.style.display = "";
  240. msg.textContent = String(text);
  241. }
  242. async function submit() {
  243. setMsg("");
  244. const phoneVal = String(phone.value || "").trim();
  245. const passwordVal = String(password.value || "");
  246. if (!/^\d{6,20}$/.test(phoneVal)) {
  247. setMsg("请输入正确的手机号");
  248. phone.focus();
  249. return;
  250. }
  251. if (String(passwordVal).length < 6) {
  252. setMsg("密码至少 6 位");
  253. password.focus();
  254. return;
  255. }
  256. const original = btn.textContent;
  257. btn.disabled = true;
  258. btn.textContent = "注册中…";
  259. try {
  260. await apiFetch("/auth/register", {
  261. method: "POST",
  262. body: { phone: phoneVal, password: passwordVal },
  263. });
  264. showToastSuccess("注册成功");
  265. window.location.href = next || "/ui/me";
  266. } catch (e) {
  267. setMsg(`注册失败:${e.detail?.error || e.status || "unknown"}`);
  268. } finally {
  269. btn.disabled = false;
  270. btn.textContent = original;
  271. }
  272. }
  273. btn.addEventListener("click", submit);
  274. phone.addEventListener("keydown", (e) => {
  275. if (e.key === "Enter") submit();
  276. });
  277. password.addEventListener("keydown", (e) => {
  278. if (e.key === "Enter") submit();
  279. });
  280. }
  281. async function pageMe() {
  282. const meInfo = document.getElementById("meInfo");
  283. const orderList = document.getElementById("orderList");
  284. const logoutBtn = document.getElementById("logoutBtn");
  285. const orderSection = document.getElementById("orderSection");
  286. const downloadSection = document.getElementById("downloadSection");
  287. const downloadList = document.getElementById("downloadList");
  288. const downloadPager = document.getElementById("downloadPager");
  289. const meMsg = document.getElementById("meMsg");
  290. let downloadPage = 1;
  291. const downloadPageSize = 10;
  292. async function loadDownloads(page) {
  293. if (!downloadList) return;
  294. downloadPage = Math.max(1, parseInt(page || 1, 10) || 1);
  295. downloadList.innerHTML = "";
  296. downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" }));
  297. downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" }));
  298. if (downloadPager) downloadPager.innerHTML = "";
  299. const q = new URLSearchParams();
  300. q.set("page", String(downloadPage));
  301. q.set("pageSize", String(downloadPageSize));
  302. const data = await apiFetch(`/me/downloads?${q.toString()}`);
  303. const items = (data && data.items) || [];
  304. const total = parseInt(data.total || 0, 10) || 0;
  305. const totalPages = Math.max(1, Math.ceil(total / downloadPageSize));
  306. downloadList.innerHTML = "";
  307. if (!items.length) {
  308. downloadList.appendChild(
  309. el(
  310. "div",
  311. { style: "text-align: center; padding: 28px 16px; color: var(--muted);" },
  312. el("i", { class: "ri-inbox-line", style: "font-size: 2.2rem; margin-bottom: 12px; opacity: 0.5;" }),
  313. el("div", { style: "font-size: 1.05rem; margin-bottom: 6px;" }, "暂无下载记录"),
  314. el("div", { style: "font-size: 0.9rem;" }, "下载过资源后会显示在这里。")
  315. )
  316. );
  317. } else {
  318. items.forEach((it) => {
  319. let stateBadge = el("span", { class: "badge badge-success" }, "可用");
  320. if (it.resourceState === "DELETED") stateBadge = el("span", { class: "badge badge-danger" }, "资源已删除");
  321. else if (it.resourceState === "OFFLINE") stateBadge = el("span", { class: "badge badge-warning" }, "资源已下架");
  322. const typeBadge = el(
  323. "span",
  324. { class: `badge ${it.resourceType === "VIP" ? "badge-vip" : "badge-free"}` },
  325. `下载时:${it.resourceType === "VIP" ? "VIP" : "免费"}`
  326. );
  327. const currentType = it.currentResourceType || "";
  328. const currentTypeBadge =
  329. currentType && it.resourceState !== "DELETED"
  330. ? el(
  331. "span",
  332. { class: `badge ${currentType === "VIP" ? "badge-vip" : "badge-free"}` },
  333. `当前:${currentType === "VIP" ? "VIP" : "免费"}`
  334. )
  335. : null;
  336. const driftBadge =
  337. currentType && it.resourceType && currentType !== it.resourceType && it.resourceState === "ONLINE"
  338. ? el("span", { class: "badge badge-warning" }, "类型已变更")
  339. : null;
  340. const canOpenDetail = !!it.resourceId && it.resourceState === "ONLINE";
  341. const titleNode = canOpenDetail
  342. ? el(
  343. "a",
  344. { href: `/ui/resources/${it.resourceId}`, style: "color: inherit; text-decoration: none;" },
  345. it.resourceTitle || ""
  346. )
  347. : el("span", { class: "muted" }, it.resourceTitle || "");
  348. downloadList.appendChild(
  349. el(
  350. "div",
  351. { style: "border: 1px solid var(--border); border-radius: 12px; padding: 16px; background: var(--bg); display: flex; flex-direction: column; gap: 10px;" },
  352. el(
  353. "div",
  354. { style: "display: flex; justify-content: space-between; align-items: center; gap: 12px;" },
  355. el("div", { style: "font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" }, titleNode),
  356. stateBadge
  357. ),
  358. el(
  359. "div",
  360. { class: "muted", style: "display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 0.9rem;" },
  361. typeBadge,
  362. currentTypeBadge,
  363. driftBadge,
  364. el("span", {}, `Ref:${it.ref || ""}`),
  365. el("span", {}, `下载:${formatDateTime(it.downloadedAt)}`)
  366. )
  367. )
  368. );
  369. });
  370. }
  371. if (downloadPager) {
  372. const prevBtn = el("button", { class: "btn", disabled: downloadPage <= 1 }, "上一页");
  373. const nextBtn = el("button", { class: "btn", disabled: downloadPage >= totalPages }, "下一页");
  374. prevBtn.addEventListener("click", async () => loadDownloads(downloadPage - 1));
  375. nextBtn.addEventListener("click", async () => loadDownloads(downloadPage + 1));
  376. downloadPager.appendChild(el("div", { class: "muted" }, `第 ${downloadPage} / ${totalPages} 页 · 共 ${total} 条`));
  377. downloadPager.appendChild(el("div", { style: "display: flex; gap: 8px;" }, prevBtn, nextBtn));
  378. }
  379. }
  380. async function load() {
  381. if (meMsg) {
  382. meMsg.style.display = "none";
  383. meMsg.textContent = "";
  384. }
  385. meInfo.innerHTML = "";
  386. meInfo.appendChild(el("div", { class: "skeleton skeleton-line", style: "width: 60%;" }));
  387. meInfo.appendChild(el("div", { class: "skeleton skeleton-line", style: "width: 45%;" }));
  388. if (orderList) {
  389. orderList.innerHTML = "";
  390. orderList.appendChild(el("div", { class: "card skeleton", style: "height: 96px;" }));
  391. orderList.appendChild(el("div", { class: "card skeleton", style: "height: 96px;" }));
  392. }
  393. if (downloadList) {
  394. downloadList.innerHTML = "";
  395. downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" }));
  396. downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" }));
  397. }
  398. if (downloadPager) downloadPager.innerHTML = "";
  399. const data = await apiFetch("/me");
  400. if (!data.user) {
  401. meInfo.innerHTML = "";
  402. meInfo.appendChild(el("div", {}, "未登录"));
  403. meInfo.appendChild(el("div", { class: "muted" }, "登录后可查看会员状态与订单记录。"));
  404. meInfo.appendChild(el("div", { class: "toolbar" }, el("a", { class: "btn btn-primary", href: `/ui/login?next=${currentNextParam()}` }, "去登录"), el("a", { class: "btn", href: `/ui/register?next=${currentNextParam()}` }, "去注册")));
  405. if (orderSection) orderSection.style.display = "none";
  406. if (downloadSection) downloadSection.style.display = "none";
  407. if (meMsg) {
  408. meMsg.style.display = "";
  409. meMsg.textContent = "提示:未登录";
  410. }
  411. if (orderList) orderList.innerHTML = "";
  412. if (downloadList) downloadList.innerHTML = "";
  413. if (downloadPager) downloadPager.innerHTML = "";
  414. return;
  415. }
  416. if (orderSection) orderSection.style.display = "";
  417. if (downloadSection) downloadSection.style.display = "";
  418. meInfo.innerHTML = "";
  419. const infoGrid = el(
  420. "div",
  421. { class: "me-info-grid" },
  422. el(
  423. "div",
  424. { class: "me-info-row" },
  425. el("div", { class: "me-info-label" }, el("i", { class: "ri-smartphone-line" }), "手机号"),
  426. el("div", { class: "me-info-value" }, String(data.user.phone || "-"))
  427. ),
  428. el(
  429. "div",
  430. { class: "me-info-row" },
  431. el("div", { class: "me-info-label" }, el("i", { class: "ri-vip-crown-line" }), "状态"),
  432. el(
  433. "div",
  434. { class: "me-info-value" },
  435. el("span", { class: data.user.vipActive ? "badge badge-success" : "badge badge-danger" }, data.user.vipActive ? "VIP 有效" : "无/已过期")
  436. )
  437. ),
  438. el(
  439. "div",
  440. { class: "me-info-row" },
  441. el("div", { class: "me-info-label" }, el("i", { class: "ri-calendar-event-line" }), "到期时间"),
  442. el("div", { class: "me-info-value muted" }, formatDateTime(data.user.vipExpireAt))
  443. )
  444. );
  445. meInfo.appendChild(infoGrid);
  446. const orders = await apiFetch("/orders");
  447. orderList.innerHTML = "";
  448. const items = (orders && orders.items) || [];
  449. if (!items.length) {
  450. orderList.appendChild(
  451. el("div", { style: "text-align: center; padding: 40px 20px; color: var(--muted);" },
  452. el("i", { class: "ri-inbox-line", style: "font-size: 3rem; margin-bottom: 16px; opacity: 0.5;" }),
  453. el("div", { style: "font-size: 1.1rem; margin-bottom: 8px;" }, "暂无订单"),
  454. el("div", { style: "font-size: 0.9rem;" }, "购买会员后将显示订单记录。")
  455. )
  456. );
  457. }
  458. if (items.length) {
  459. items.forEach((o) => {
  460. let statusBadge;
  461. if (o.status === "PAID") statusBadge = el("span", { class: "badge badge-success" }, "已支付");
  462. else if (o.status === "CLOSED") statusBadge = el("span", { class: "badge badge-danger" }, "已关闭");
  463. else statusBadge = el("span", { class: "badge badge-info" }, o.status);
  464. orderList.appendChild(
  465. el(
  466. "div",
  467. { style: "border: 1px solid var(--border); border-radius: 12px; padding: 20px; display: flex; flex-direction: column; gap: 12px; background: var(--bg); transition: transform 0.2s, box-shadow 0.2s;" },
  468. el("div", { style: "display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 12px;" },
  469. el("div", { style: "font-weight: 500;" }, `订单号:${o.id}`),
  470. statusBadge
  471. ),
  472. el("div", { style: "display: flex; justify-content: space-between; align-items: center;" },
  473. el("div", { style: "display: flex; align-items: center; gap: 8px;" }, el("i", { class: "ri-vip-crown-fill", style: "color: var(--brand);" }), el("span", { style: "font-weight: 500;" }, o.planSnapshot.name), el("span", { class: "muted", style: "font-size: 0.9rem;" }, `(${o.planSnapshot.durationDays} 天)`)),
  474. el("div", { style: "font-weight: bold; color: var(--brand); font-size: 1.1rem;" }, formatCents(o.amountCents))
  475. ),
  476. el("div", { class: "muted", style: "font-size: 0.9rem; display: flex; justify-content: space-between; margin-top: 4px;" },
  477. el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-time-line" }), `创建:${formatDateTime(o.createdAt)}`),
  478. el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-check-double-line" }), `支付:${formatDateTime(o.paidAt)}`)
  479. )
  480. )
  481. );
  482. });
  483. }
  484. await loadDownloads(1);
  485. }
  486. if (logoutBtn) {
  487. logoutBtn.addEventListener("click", async () => {
  488. await apiFetch("/auth/logout", { method: "POST" });
  489. window.location.reload();
  490. });
  491. }
  492. await load();
  493. }
  494. async function pageMessages() {
  495. const messageList = document.getElementById("messageList");
  496. const messagePager = document.getElementById("messagePager");
  497. const messageUnreadBadge = document.getElementById("messageUnreadBadge");
  498. const messageUnreadOnlyBtn = document.getElementById("messageUnreadOnlyBtn");
  499. const messageAllBtn = document.getElementById("messageAllBtn");
  500. const messageMsg = document.getElementById("messageMsg");
  501. let page = 1;
  502. const pageSize = 10;
  503. let unreadOnly = false;
  504. function setMsg(text, isError = true) {
  505. if (!messageMsg) return;
  506. messageMsg.style.display = "";
  507. messageMsg.className = `form-msg ${isError ? "form-msg-error" : "form-msg-success"}`;
  508. messageMsg.textContent = String(text || "");
  509. }
  510. function clearMsg() {
  511. if (!messageMsg) return;
  512. messageMsg.style.display = "none";
  513. messageMsg.textContent = "";
  514. }
  515. function updateUnreadBadge(cnt) {
  516. if (!messageUnreadBadge) return;
  517. const n = parseInt(cnt || 0, 10) || 0;
  518. if (n > 0) {
  519. messageUnreadBadge.style.display = "";
  520. messageUnreadBadge.textContent = `${n} 未读`;
  521. } else {
  522. messageUnreadBadge.style.display = "none";
  523. messageUnreadBadge.textContent = "";
  524. }
  525. }
  526. function updateToggleButtons() {
  527. if (messageUnreadOnlyBtn) messageUnreadOnlyBtn.className = `btn btn-sm ${unreadOnly ? "btn-primary" : ""}`.trim();
  528. if (messageAllBtn) messageAllBtn.className = `btn btn-sm ${!unreadOnly ? "btn-primary" : ""}`.trim();
  529. }
  530. async function loadMessages(targetPage) {
  531. if (!messageList) return;
  532. page = Math.max(1, parseInt(targetPage || 1, 10) || 1);
  533. clearMsg();
  534. messageList.innerHTML = "";
  535. messageList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" }));
  536. messageList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" }));
  537. if (messagePager) messagePager.innerHTML = "";
  538. const q = new URLSearchParams();
  539. q.set("page", String(page));
  540. q.set("pageSize", String(pageSize));
  541. if (unreadOnly) q.set("unread", "1");
  542. let data;
  543. try {
  544. data = await apiFetch(`/me/messages?${q.toString()}`);
  545. } catch (e) {
  546. if (e.status === 401) {
  547. setMsg("未登录,无法查看消息。");
  548. messageList.innerHTML = "";
  549. messageList.appendChild(el("div", { class: "toolbar" }, el("a", { class: "btn btn-primary", href: `/ui/login?next=${currentNextParam()}` }, "去登录")));
  550. return;
  551. }
  552. setMsg(e.detail?.error || e.status || "消息加载失败");
  553. messageList.innerHTML = "";
  554. return;
  555. }
  556. const items = (data && data.items) || [];
  557. const total = parseInt(data.total || 0, 10) || 0;
  558. const unreadCount = parseInt(data.unreadCount || 0, 10) || 0;
  559. const totalPages = Math.max(1, Math.ceil(total / pageSize));
  560. updateUnreadBadge(unreadCount);
  561. updateToggleButtons();
  562. if (typeof window.__refreshMessageBadge === "function") {
  563. try {
  564. await window.__refreshMessageBadge();
  565. } catch (e) {}
  566. }
  567. messageList.innerHTML = "";
  568. if (!items.length) {
  569. messageList.appendChild(
  570. el(
  571. "div",
  572. { style: "text-align: center; padding: 28px 16px; color: var(--muted);" },
  573. el("i", { class: "ri-inbox-line", style: "font-size: 2.2rem; margin-bottom: 12px; opacity: 0.5;" }),
  574. el("div", { style: "font-size: 1.05rem; margin-bottom: 6px;" }, unreadOnly ? "暂无未读消息" : "暂无消息"),
  575. el("div", { style: "font-size: 0.9rem;" }, "系统通知会显示在这里。")
  576. )
  577. );
  578. } else {
  579. items.forEach((m) => {
  580. const read = !!m.read;
  581. const statusBadge = read ? el("span", { class: "badge" }, "已读") : el("span", { class: "badge badge-warning" }, "未读");
  582. const actions = !read
  583. ? el(
  584. "button",
  585. {
  586. class: "btn btn-sm",
  587. onclick: async () => {
  588. try {
  589. await apiFetch(`/me/messages/${m.id}/read`, { method: "PUT" });
  590. await loadMessages(page);
  591. } catch (e) {
  592. showToastError(e?.detail?.error || e?.status || "标记失败");
  593. }
  594. },
  595. },
  596. "标记已读"
  597. )
  598. : null;
  599. messageList.appendChild(
  600. el(
  601. "div",
  602. { style: "border: 1px solid var(--border); border-radius: 12px; padding: 16px; background: var(--bg); display: flex; flex-direction: column; gap: 10px;" },
  603. el(
  604. "div",
  605. { style: "display: flex; justify-content: space-between; align-items: center; gap: 12px;" },
  606. el("div", { style: "font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" }, m.title || "通知"),
  607. el("div", { style: "display: flex; align-items: center; gap: 8px; flex: 0 0 auto;" }, statusBadge, actions)
  608. ),
  609. el("div", { class: "muted", style: "white-space: pre-wrap;" }, formatMessageText(m.content || "")),
  610. el("div", { class: "muted", style: "font-size: 0.9rem;" }, `发送时间:${formatDateTime(m.createdAt)}`)
  611. )
  612. );
  613. });
  614. }
  615. if (messagePager) {
  616. const prevBtn = el("button", { class: "btn", disabled: page <= 1 }, "上一页");
  617. const nextBtn = el("button", { class: "btn", disabled: page >= totalPages }, "下一页");
  618. prevBtn.addEventListener("click", async () => loadMessages(page - 1));
  619. nextBtn.addEventListener("click", async () => loadMessages(page + 1));
  620. messagePager.appendChild(el("div", { class: "muted" }, `第 ${page} / ${totalPages} 页 · 共 ${total} 条`));
  621. messagePager.appendChild(el("div", { style: "display: flex; gap: 8px;" }, prevBtn, nextBtn));
  622. }
  623. }
  624. if (messageUnreadOnlyBtn) {
  625. messageUnreadOnlyBtn.addEventListener("click", async () => {
  626. unreadOnly = true;
  627. await loadMessages(1);
  628. });
  629. }
  630. if (messageAllBtn) {
  631. messageAllBtn.addEventListener("click", async () => {
  632. unreadOnly = false;
  633. await loadMessages(1);
  634. });
  635. }
  636. updateToggleButtons();
  637. await loadMessages(1);
  638. }
  639. async function pageVip() {
  640. const planList = document.getElementById("planList");
  641. const vipMsg = document.getElementById("vipMsg");
  642. const vipStatus = document.getElementById("vipStatus");
  643. let me = null;
  644. try {
  645. me = await apiFetch("/me");
  646. } catch (e) {
  647. me = { user: null };
  648. }
  649. const user = me && me.user ? me.user : null;
  650. if (vipStatus) {
  651. vipStatus.style.display = "";
  652. vipStatus.innerHTML = "";
  653. if (!user) {
  654. vipStatus.appendChild(
  655. el("div", { style: "display: flex; flex-direction: column; align-items: center; padding: 16px;" },
  656. el("div", { style: "margin-bottom: 16px; font-size: 1.1rem; color: var(--brand);" }, "未登录。登录后可购买/续费会员,并查看权益状态。"),
  657. el("a", { class: "btn btn-primary", href: `/ui/login?next=${currentNextParam()}`, style: "display: inline-flex; align-items: center; gap: 8px; border-radius: 20px; padding: 8px 24px;" },
  658. el("i", { class: "ri-login-box-line" }), "去登录"
  659. )
  660. )
  661. );
  662. } else {
  663. vipStatus.appendChild(
  664. el("div", { style: "display: flex; justify-content: space-around; flex-wrap: wrap; gap: 16px; padding: 16px;" },
  665. el("div", { style: "display: flex; flex-direction: column; align-items: center; gap: 8px;" },
  666. el("div", { class: "muted", style: "font-size: 0.9rem;" }, "当前账号"),
  667. el("div", { style: "font-weight: 500; display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-smartphone-line", style: "color: var(--brand);" }), user.phone)
  668. ),
  669. el("div", { style: "display: flex; flex-direction: column; align-items: center; gap: 8px;" },
  670. el("div", { class: "muted", style: "font-size: 0.9rem;" }, "会员状态"),
  671. el("div", { style: "font-weight: 500; display: flex; align-items: center; gap: 4px;" },
  672. el("i", { class: user.vipActive ? "ri-vip-crown-fill" : "ri-vip-crown-line", style: user.vipActive ? "color: #ffd700;" : "color: var(--muted);" }),
  673. el("span", { class: user.vipActive ? "badge badge-success" : "badge badge-danger", style: "margin-left: 4px;" }, user.vipActive ? "VIP 有效" : "无/已过期")
  674. )
  675. ),
  676. el("div", { style: "display: flex; flex-direction: column; align-items: center; gap: 8px;" },
  677. el("div", { class: "muted", style: "font-size: 0.9rem;" }, "到期时间"),
  678. el("div", { style: "font-weight: 500; display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-calendar-event-line", style: "color: var(--brand);" }), formatDateTime(user.vipExpireAt))
  679. )
  680. )
  681. );
  682. }
  683. }
  684. planList.innerHTML = "";
  685. planList.appendChild(el("div", { class: "card skeleton", style: "grid-column: 1 / -1; height: 120px;" }));
  686. const plans = await apiFetch("/plans");
  687. planList.innerHTML = "";
  688. plans.forEach((p) => {
  689. const isRecommended = plans[0] && p.id === plans[0].id;
  690. planList.appendChild(
  691. el(
  692. "div",
  693. { class: "card", style: `position: relative; overflow: hidden; padding: 32px 24px; border: 2px solid ${isRecommended ? 'var(--brand)' : 'transparent'}; box-shadow: var(--shadow-md); transition: transform 0.3s, box-shadow 0.3s; display: flex; flex-direction: column; height: 100%;` },
  694. isRecommended ? el("div", { style: "position: absolute; top: 16px; right: -32px; background: var(--brand); color: white; padding: 4px 40px; transform: rotate(45deg); font-size: 0.85rem; font-weight: bold; box-shadow: var(--shadow);" }, "推荐") : null,
  695. el(
  696. "div",
  697. { style: "text-align: center; margin-bottom: 24px;" },
  698. el("h3", { style: "margin: 0 0 8px 0; font-size: 1.5rem; color: var(--text);" }, p.name),
  699. el("div", { class: "muted" }, `有效期 ${p.durationDays} 天`)
  700. ),
  701. el(
  702. "div",
  703. { style: "text-align: center; margin-bottom: 32px;" },
  704. el("span", { style: "font-size: 1.25rem; color: var(--brand); font-weight: bold;" }, "¥ "),
  705. el("span", { style: "font-size: 2.5rem; color: var(--brand); font-weight: bold; line-height: 1;" }, (p.priceCents / 100).toFixed(2))
  706. ),
  707. el(
  708. "div",
  709. { style: "margin-top: auto;" },
  710. el(
  711. "button",
  712. {
  713. class: isRecommended ? "btn btn-primary" : "btn",
  714. style: "width: 100%; height: 48px; font-size: 1.1rem; border-radius: 24px; display: flex; justify-content: center; align-items: center; gap: 8px;",
  715. "data-plan-id": String(p.id),
  716. },
  717. el("i", { class: "ri-shopping-cart-2-line" }),
  718. "立即开通"
  719. )
  720. )
  721. )
  722. );
  723. });
  724. planList.addEventListener("click", async (evt) => {
  725. const btn = evt.target.closest("button[data-plan-id]");
  726. if (!btn) return;
  727. vipMsg.textContent = "";
  728. const originalText = btn.textContent;
  729. btn.disabled = true;
  730. btn.textContent = "处理中…";
  731. const planId = Number(btn.getAttribute("data-plan-id"));
  732. try {
  733. const order = await apiFetch("/orders", { method: "POST", body: { planId } });
  734. const payResp = await apiFetch(`/orders/${order.id}/pay`, { method: "POST" });
  735. if (payResp && payResp.payUrl) {
  736. vipMsg.textContent = "已发起支付宝支付,正在跳转…";
  737. // 打开支付宝收银台,同时在后台轮询订单状态
  738. const payWin = window.open(payResp.payUrl, "_blank");
  739. _startPayPolling(order.id, payWin);
  740. return;
  741. }
  742. vipMsg.textContent = "支付成功(模拟),已发放会员权益。";
  743. showToastSuccess("支付成功,会员权益已生效");
  744. setTimeout(() => {
  745. window.location.href = "/ui/me";
  746. }, 300);
  747. } catch (e) {
  748. if (e.status === 401) window.location.href = `/ui/login?next=${currentNextParam()}`;
  749. vipMsg.textContent = `下单/支付失败:${e.detail?.error || e.status || "unknown"}`;
  750. } finally {
  751. btn.disabled = false;
  752. btn.textContent = originalText;
  753. }
  754. });
  755. }
  756. /**
  757. * 支付轮询:每 3 秒向后端查询一次订单状态。
  758. * 与 callback_url 回调竞争,谁先触发谁先激活,后端幂等保护。
  759. * @param {string} orderId 订单 ID
  760. * @param {Window|null} payWin 支付宝收银台窗口(可为 null)
  761. */
  762. function _startPayPolling(orderId, payWin) {
  763. const INTERVAL_MS = 3000; // 轮询间隔 3 秒
  764. const MAX_TRIES = 40; // 最多轮询 40 次(约 2 分钟)
  765. let tries = 0;
  766. let stopped = false;
  767. const vipMsg = document.getElementById("vipMsg");
  768. if (vipMsg) {
  769. vipMsg.style.display = "";
  770. vipMsg.textContent = "等待支付结果,请在新窗口完成支付…";
  771. }
  772. const timer = setInterval(async () => {
  773. if (stopped) return;
  774. tries++;
  775. try {
  776. const res = await apiFetch(`/orders/${orderId}/query-and-activate`, { method: "POST" });
  777. if (res && res.status === "PAID") {
  778. stopped = true;
  779. clearInterval(timer);
  780. // 关闭支付宝窗口(如果还开着)
  781. try { if (payWin && !payWin.closed) payWin.close(); } catch (_) {}
  782. showToastSuccess("支付成功,会员权益已生效");
  783. setTimeout(() => { window.location.href = "/ui/me"; }, 800);
  784. return;
  785. }
  786. } catch (_) {
  787. // 网络抖动,忽略,继续轮询
  788. }
  789. if (tries >= MAX_TRIES) {
  790. stopped = true;
  791. clearInterval(timer);
  792. if (vipMsg) vipMsg.textContent = "未检测到支付结果,如已付款请稍后刷新个人中心查看。";
  793. }
  794. }, INTERVAL_MS);
  795. }
  796. async function pageResourceDetail() {
  797. const root = document.getElementById("resourceDetail");
  798. const resourceId = Number(root.getAttribute("data-resource-id"));
  799. const descRoot = document.getElementById("resourceDescription");
  800. const refSelect = document.getElementById("refSelect");
  801. const breadcrumb = document.getElementById("breadcrumb");
  802. const treeEl = document.getElementById("tree");
  803. const fileContent = document.getElementById("fileContent");
  804. const downloadBtn = document.getElementById("downloadBtn");
  805. let detail = null;
  806. let me = null;
  807. let inlineDownloadBtn = null;
  808. function setDownloadButtonLabel(btn, label) {
  809. if (!btn) return;
  810. const text = String(label || "").trim() || "下载 ZIP";
  811. const iconClass = text.includes("开通会员") ? "ri-vip-crown-line" : "ri-download-cloud-2-line";
  812. btn.innerHTML = "";
  813. btn.appendChild(el("i", { class: iconClass }));
  814. btn.appendChild(document.createTextNode(text));
  815. }
  816. function updateDownloadButton() {
  817. const user = me && me.user ? me.user : null;
  818. if (!user) {
  819. setDownloadButtonLabel(downloadBtn, "下载 ZIP");
  820. setDownloadButtonLabel(inlineDownloadBtn, "下载 ZIP");
  821. return;
  822. }
  823. if (detail && detail.type === "VIP" && !user.vipActive) {
  824. setDownloadButtonLabel(downloadBtn, "开通会员下载");
  825. setDownloadButtonLabel(inlineDownloadBtn, "开通会员下载");
  826. return;
  827. }
  828. setDownloadButtonLabel(downloadBtn, "下载 ZIP");
  829. setDownloadButtonLabel(inlineDownloadBtn, "下载 ZIP");
  830. }
  831. async function loadMe() {
  832. try {
  833. me = await apiFetch("/me");
  834. } catch (e) {
  835. me = null;
  836. }
  837. updateDownloadButton();
  838. }
  839. async function loadDetail() {
  840. const r = await apiFetch(`/resources/${resourceId}`);
  841. detail = r;
  842. inlineDownloadBtn = null;
  843. root.innerHTML = "";
  844. if (descRoot) descRoot.innerHTML = "";
  845. const coverCol = r.coverUrl
  846. ? el("img", { src: r.coverUrl, alt: r.title, style: "width: 100%; max-width: 320px; height: 240px; object-fit: cover; border-radius: 12px; box-shadow: var(--shadow);" })
  847. : null;
  848. if (coverCol) ensureImgFallback(coverCol);
  849. inlineDownloadBtn = el(
  850. "button",
  851. { class: "btn btn-primary", style: "margin-top: 14px; border-radius: 10px; display: inline-flex; align-items: center; gap: 6px; width: fit-content;" },
  852. el("i", { class: "ri-download-cloud-2-line" }),
  853. "下载 ZIP"
  854. );
  855. inlineDownloadBtn.addEventListener("click", downloadZip);
  856. const metaCol = el(
  857. "div",
  858. { style: "flex: 1; display: flex; flex-direction: column;" },
  859. el("h1", { style: "margin: 0 0 16px 0; font-size: 2rem; color: var(--text);" }, r.title),
  860. el(
  861. "div",
  862. { style: "display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap;" },
  863. r.type === "VIP" ? el("span", { class: "badge badge-danger", style: "padding: 6px 12px; font-size: 0.9rem;" }, el("i", { class: "ri-vip-crown-fill", style: "margin-right: 4px;" }), "VIP 专享") : el("span", { class: "badge badge-success", style: "padding: 6px 12px; font-size: 0.9rem;" }, el("i", { class: "ri-check-line", style: "margin-right: 4px;" }), "免费资源"),
  864. el("span", { class: "badge badge-info", style: "padding: 6px 12px; font-size: 0.9rem;" }, el("i", { class: "ri-hashtag", style: "margin-right: 4px;" }), `资源 ID: ${r.id}`),
  865. ...((r.tags || []).slice(0, 8).map((t) => el("span", { class: "badge", style: "padding: 6px 12px; font-size: 0.9rem; background: var(--bg); border: 1px solid var(--border);" }, t)))
  866. ),
  867. el("div", { class: "muted", style: "display: flex; gap: 16px; margin-bottom: 8px;" },
  868. el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-eye-line" }), `浏览 ${r.viewCount}`),
  869. el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-download-cloud-2-line" }), `下载 ${r.downloadCount}`)
  870. ),
  871. el("div", { class: "muted", style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-github-line" }), `仓库: ${r.repo.owner}/${r.repo.name}${r.repo.private ? " (私有)" : ""}`),
  872. inlineDownloadBtn
  873. );
  874. const headerRow = el("div", { style: "display: flex; gap: 32px; flex-wrap: wrap; margin-bottom: 32px;" }, coverCol, metaCol);
  875. root.appendChild(
  876. el(
  877. "div",
  878. { class: "card", style: "padding: 32px; border-radius: 16px; box-shadow: var(--shadow-md);" },
  879. headerRow
  880. )
  881. );
  882. if (descRoot) {
  883. if (typeof renderMarkdown !== "function") {
  884. await loadScriptOnce("/static/app_markdown.js");
  885. }
  886. const summary = el("div", { class: "md", style: "line-height: 1.8; color: var(--text-light);", html: renderMarkdown(r.summary || "暂无描述") });
  887. descRoot.appendChild(
  888. el(
  889. "div",
  890. { class: "card", style: "margin-top:24px; padding: 32px; border-radius: 16px; box-shadow: var(--shadow-md);" },
  891. el("h3", { style: "display: flex; align-items: center; gap: 8px; margin-bottom: 16px;" }, el("i", { class: "ri-file-text-line", style: "color: var(--brand);" }), "资源描述"),
  892. summary
  893. )
  894. );
  895. }
  896. updateDownloadButton();
  897. }
  898. async function downloadZip() {
  899. const user = me && me.user ? me.user : null;
  900. const currentRef = refSelect ? String(refSelect.value || "") : "";
  901. if (!currentRef) {
  902. showToastError("仓库加载中,请稍后再试");
  903. return;
  904. }
  905. if (!user) {
  906. Swal.fire({
  907. title: '未登录',
  908. text: '下载资源需要先登录账户',
  909. icon: 'info',
  910. showCancelButton: true,
  911. confirmButtonText: '去登录',
  912. cancelButtonText: '取消',
  913. confirmButtonColor: 'var(--brand)'
  914. }).then((result) => {
  915. if (result.isConfirmed) {
  916. window.location.href = `/ui/login?next=${currentNextParam()}`;
  917. }
  918. });
  919. return;
  920. }
  921. if (detail && detail.type === "VIP" && !user.vipActive) {
  922. Swal.fire({
  923. title: 'VIP 专享资源',
  924. text: '该资源为 VIP 专属,需要开通会员后才能下载。',
  925. icon: 'warning',
  926. showCancelButton: true,
  927. confirmButtonText: '去开通会员',
  928. cancelButtonText: '取消',
  929. confirmButtonColor: '#ffd700',
  930. iconColor: '#ffd700'
  931. }).then((result) => {
  932. if (result.isConfirmed) {
  933. window.location.href = "/ui/vip";
  934. }
  935. });
  936. return;
  937. }
  938. if (downloadBtn) downloadBtn.disabled = true;
  939. if (inlineDownloadBtn) inlineDownloadBtn.disabled = true;
  940. Swal.fire({
  941. title: "正在准备下载",
  942. text: "请稍候…",
  943. allowOutsideClick: false,
  944. allowEscapeKey: false,
  945. showConfirmButton: false,
  946. didOpen: () => {
  947. Swal.showLoading();
  948. },
  949. });
  950. try {
  951. const prep = await apiFetch(`/resources/${resourceId}/download`, {
  952. method: "POST",
  953. body: { ref: currentRef },
  954. });
  955. const downloadUrl = prep?.downloadUrl || `/resources/${resourceId}/download?ref=${encodeURIComponent(currentRef)}`;
  956. const statusUrl = prep?.statusUrl || `/resources/${resourceId}/download/status?ref=${encodeURIComponent(currentRef)}`;
  957. const poll = async () => {
  958. for (let i = 0; i < 240; i++) {
  959. const st = await apiFetch(statusUrl, { method: "GET" });
  960. if (st?.ready) return;
  961. if (st?.state === "error") {
  962. throw new Error(st?.error || "build_failed");
  963. }
  964. await new Promise((resolve) => setTimeout(resolve, 800));
  965. }
  966. throw new Error("build_timeout");
  967. };
  968. if (!prep?.ready) {
  969. Swal.update({ text: "正在生成压缩包,请稍候…" });
  970. await poll();
  971. }
  972. Swal.close();
  973. window.location.href = downloadUrl;
  974. } catch (e) {
  975. Swal.close();
  976. if (e.status === 401) window.location.href = `/ui/login?next=${currentNextParam()}`;
  977. if (e.status === 403 && e.detail?.error === "vip_required") {
  978. Swal.fire({
  979. title: 'VIP 专享资源',
  980. text: '该资源为 VIP 专属,需要开通会员后才能下载。',
  981. icon: 'warning',
  982. showCancelButton: true,
  983. confirmButtonText: '去开通会员',
  984. cancelButtonText: '取消',
  985. confirmButtonColor: '#ffd700',
  986. iconColor: '#ffd700'
  987. }).then((result) => {
  988. if (result.isConfirmed) {
  989. window.location.href = "/ui/vip";
  990. }
  991. });
  992. return;
  993. }
  994. Swal.fire({
  995. icon: 'error',
  996. title: '下载失败',
  997. text: e.detail?.error || e.message || e.status || "未知错误"
  998. });
  999. } finally {
  1000. if (downloadBtn) downloadBtn.disabled = false;
  1001. if (inlineDownloadBtn) inlineDownloadBtn.disabled = false;
  1002. }
  1003. }
  1004. if (downloadBtn) downloadBtn.addEventListener("click", downloadZip);
  1005. try {
  1006. await loadDetail();
  1007. } catch (e) {
  1008. root.innerHTML = "";
  1009. root.appendChild(el("div", { class: "card" }, `加载失败:${e.detail?.error || e.status || "unknown"}`));
  1010. return;
  1011. }
  1012. await loadMe();
  1013. const startRepo = async () => {
  1014. try {
  1015. await loadScriptOnce("/static/app_user_repo.js");
  1016. if (typeof window.initUserRepoBrowser === "function") {
  1017. await window.initUserRepoBrowser({ resourceId });
  1018. }
  1019. } catch (e) {
  1020. if (breadcrumb) breadcrumb.textContent = "仓库加载失败";
  1021. if (treeEl) {
  1022. treeEl.innerHTML = "";
  1023. treeEl.appendChild(el("div", { class: "card", style: "margin: 8px; padding: 16px; border-radius: 12px;" }, `加载失败:${e?.detail?.error || e?.status || e?.message || "unknown"}`));
  1024. }
  1025. if (fileContent) fileContent.textContent = "";
  1026. }
  1027. };
  1028. try {
  1029. if (window.requestIdleCallback) {
  1030. window.requestIdleCallback(() => {
  1031. startRepo();
  1032. }, { timeout: 1200 });
  1033. } else {
  1034. setTimeout(() => {
  1035. startRepo();
  1036. }, 0);
  1037. }
  1038. } catch (e) {
  1039. startRepo();
  1040. }
  1041. }
  1042. async function main() {
  1043. const page = document.body.getAttribute("data-page") || "";
  1044. try {
  1045. await initTopbar();
  1046. if (page === "index") await pageIndex();
  1047. if (page === "resources") await pageIndex();
  1048. if (page === "login") await pageLogin();
  1049. if (page === "register") await pageRegister();
  1050. if (page === "me") await pageMe();
  1051. if (page === "messages") await pageMessages();
  1052. if (page === "vip") await pageVip();
  1053. if (page === "resource_detail") await pageResourceDetail();
  1054. } catch (e) {
  1055. showToastError(e?.detail?.error || e?.status || e?.message || "页面初始化失败");
  1056. }
  1057. }
  1058. main();