async function pageIndex() { const pageMode = document.body.getAttribute("data-page") || ""; const isHome = pageMode === "index"; const qInput = document.getElementById("q"); const typeSelect = document.getElementById("type"); const sortSelect = document.getElementById("sort"); const list = document.getElementById("resourceList"); const prevBtn = document.getElementById("prevPage"); const nextBtn = document.getElementById("nextPage"); const pageInfo = document.getElementById("pageInfo"); const searchBtn = document.getElementById("searchBtn"); const pager = document.getElementById("pager"); const homeMore = document.getElementById("homeMore"); let page = 1; function setLoading(loading) { if (searchBtn) searchBtn.disabled = loading; if (prevBtn) prevBtn.disabled = loading || page <= 1; if (nextBtn) nextBtn.disabled = loading; if (qInput) qInput.disabled = loading; if (typeSelect) typeSelect.disabled = loading; if (sortSelect) sortSelect.disabled = loading; } function skeletonCard() { const cover = el("div", { class: "resource-card-cover skeleton" }); const line1 = el("div", { class: "skeleton skeleton-line" }); const line2 = el("div", { class: "skeleton skeleton-line" }); line1.style.width = "70%"; line2.style.width = "90%"; const stats = el("div", { class: "toolbar" }, el("div", { class: "skeleton skeleton-pill" }), el("div", { class: "skeleton skeleton-pill" })); return el("div", { class: "card" }, cover, line1, line2, stats); } function renderSkeleton(count) { list.innerHTML = ""; for (let i = 0; i < count; i += 1) list.appendChild(skeletonCard()); } function renderEmpty(text) { list.innerHTML = ""; list.appendChild( el( "div", { class: "card", style: "grid-column: 1 / -1; text-align:center" }, el("div", {}, text || "暂无数据"), el( "div", { class: "toolbar", style: "justify-content:center" }, el( "button", { class: "btn", onclick: async () => { if (qInput) qInput.value = ""; if (typeSelect) typeSelect.value = ""; if (sortSelect) sortSelect.value = "latest"; page = 1; await load(); }, }, "清空筛选" ) ) ) ); } async function load() { if (!list) return; renderSkeleton(isHome ? 6 : 9); setLoading(true); const q = (qInput ? qInput.value : "").trim(); const type = (typeSelect ? typeSelect.value : "").trim(); const sort = (sortSelect ? sortSelect.value : "").trim() || "latest"; const params = new URLSearchParams(); if (q) params.set("q", q); if (type) params.set("type", type); if (!isHome) params.set("sort", sort); params.set("page", String(page)); params.set("pageSize", isHome ? "9" : "12"); let data = null; try { data = await apiFetch(`/resources?${params.toString()}`); } finally { setLoading(false); } list.innerHTML = ""; const items = (data && data.items) || []; if (!items.length) { renderEmpty(q || type ? "没有找到匹配的资源" : "暂无资源"); if (pageInfo) pageInfo.textContent = ""; if (pager) pager.style.display = isHome ? "none" : ""; if (homeMore) homeMore.style.display = isHome ? "" : "none"; return; } items.forEach((r) => { const badgeClass = r.type === "VIP" ? "badge badge-vip" : "badge badge-free"; const badgeIcon = r.type === "VIP" ? "ri-vip-crown-line" : "ri-price-tag-3-line"; const coverImg = r.coverUrl ? el("img", { class: "resource-card-cover-img", src: r.coverUrl, alt: r.title }) : null; if (coverImg) ensureImgFallback(coverImg); const cover = coverImg ? el("div", { class: "resource-card-cover" }, coverImg) : null; const tags = Array.isArray(r.tags) ? r.tags.slice(0, 4) : []; const tagRow = tags.length ? el("div", { class: "resource-card-tags" }, ...tags.map((t) => badge(t, "badge"))) : null; list.appendChild( el( "div", { class: "card", style: "display:flex; flex-direction:column; height:100%;" }, cover, el( "div", { style: "flex:1;" }, el("span", { class: badgeClass, style: "float:right; display:flex; align-items:center; gap:4px;" }, el("i", {class: badgeIcon}), r.type === "VIP" ? "VIP" : "FREE"), el("h3", { style: "margin-top:0;" }, r.title) ), tagRow, el("div", { class: "muted", style: "margin-bottom:16px; flex:1;" }, (r.summary || "").slice(0, 80)), el( "div", { class: "toolbar", style: "margin-top:auto;" }, 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"}), "查看详情"), el("span", { class: "muted", style: "display:flex; align-items:center; gap:8px; font-size:0.9rem;" }, el("span", {style:"display:flex; align-items:center; gap:2px;"}, el("i", {class: "ri-bar-chart-box-line"}), String(r.viewCount)), el("span", {style:"display:flex; align-items:center; gap:2px;"}, el("i", {class: "ri-download-cloud-2-line"}), String(r.downloadCount)) ) ) ) ); }); const totalPages = Math.max(Math.ceil((data.total || 0) / (data.pageSize || 12)), 1); if (pageInfo) pageInfo.textContent = isHome ? "" : `第 ${data.page} / ${totalPages} 页,共 ${data.total} 条`; if (prevBtn) prevBtn.disabled = isHome || page <= 1; if (nextBtn) nextBtn.disabled = isHome || page >= totalPages; if (pager) pager.style.display = isHome ? "none" : ""; if (homeMore) homeMore.style.display = isHome ? "" : "none"; } if (prevBtn) prevBtn.addEventListener("click", async () => { page = Math.max(page - 1, 1); await load(); }); if (nextBtn) nextBtn.addEventListener("click", async () => { page += 1; await load(); }); if (searchBtn) searchBtn.addEventListener("click", async () => { page = 1; await load(); }); if (sortSelect) sortSelect.addEventListener("change", async () => { page = 1; await load(); }); if (qInput) qInput.addEventListener("keydown", async (e) => { if (e.key !== "Enter") return; page = 1; await load(); }); await load(); } async function pageLogin() { const phone = document.getElementById("phone"); const password = document.getElementById("password"); const btn = document.getElementById("loginBtn"); const msg = document.getElementById("msg"); const toRegister = document.getElementById("toRegister"); const next = nextFromQuery(); if (toRegister) toRegister.setAttribute("href", `/ui/register?next=${next ? encodeURIComponent(next) : currentNextParam()}`); function setMsg(text) { if (!msg) return; if (!text) { msg.style.display = "none"; msg.textContent = ""; return; } msg.style.display = ""; msg.textContent = String(text); } async function submit() { setMsg(""); const phoneVal = String(phone.value || "").trim(); const passwordVal = String(password.value || ""); if (!/^\d{6,20}$/.test(phoneVal)) { setMsg("请输入正确的手机号"); phone.focus(); return; } if (!passwordVal) { setMsg("请输入密码"); password.focus(); return; } const original = btn.textContent; btn.disabled = true; btn.textContent = "登录中…"; try { await apiFetch("/auth/login", { method: "POST", body: { phone: phoneVal, password: passwordVal }, }); showToastSuccess("登录成功"); window.location.href = next || "/ui/me"; } catch (e) { setMsg(`登录失败:${e.detail?.error || e.status || "unknown"}`); } finally { btn.disabled = false; btn.textContent = original; } } try { const me = await apiFetch("/me"); if (me && me.user) { window.location.href = next || "/ui/me"; return; } } catch (e) {} btn.addEventListener("click", submit); phone.addEventListener("keydown", (e) => { if (e.key === "Enter") submit(); }); password.addEventListener("keydown", (e) => { if (e.key === "Enter") submit(); }); } async function pageRegister() { const phone = document.getElementById("phone"); const password = document.getElementById("password"); const btn = document.getElementById("registerBtn"); const msg = document.getElementById("msg"); const toLogin = document.getElementById("toLogin"); const next = nextFromQuery(); if (toLogin) toLogin.setAttribute("href", `/ui/login?next=${next ? encodeURIComponent(next) : currentNextParam()}`); function setMsg(text) { if (!msg) return; if (!text) { msg.style.display = "none"; msg.textContent = ""; return; } msg.style.display = ""; msg.textContent = String(text); } async function submit() { setMsg(""); const phoneVal = String(phone.value || "").trim(); const passwordVal = String(password.value || ""); if (!/^\d{6,20}$/.test(phoneVal)) { setMsg("请输入正确的手机号"); phone.focus(); return; } if (String(passwordVal).length < 6) { setMsg("密码至少 6 位"); password.focus(); return; } const original = btn.textContent; btn.disabled = true; btn.textContent = "注册中…"; try { await apiFetch("/auth/register", { method: "POST", body: { phone: phoneVal, password: passwordVal }, }); showToastSuccess("注册成功"); window.location.href = next || "/ui/me"; } catch (e) { setMsg(`注册失败:${e.detail?.error || e.status || "unknown"}`); } finally { btn.disabled = false; btn.textContent = original; } } btn.addEventListener("click", submit); phone.addEventListener("keydown", (e) => { if (e.key === "Enter") submit(); }); password.addEventListener("keydown", (e) => { if (e.key === "Enter") submit(); }); } async function pageMe() { const meInfo = document.getElementById("meInfo"); const orderList = document.getElementById("orderList"); const logoutBtn = document.getElementById("logoutBtn"); const orderSection = document.getElementById("orderSection"); const downloadSection = document.getElementById("downloadSection"); const downloadList = document.getElementById("downloadList"); const downloadPager = document.getElementById("downloadPager"); const meMsg = document.getElementById("meMsg"); let downloadPage = 1; const downloadPageSize = 10; async function loadDownloads(page) { if (!downloadList) return; downloadPage = Math.max(1, parseInt(page || 1, 10) || 1); downloadList.innerHTML = ""; downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" })); downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" })); if (downloadPager) downloadPager.innerHTML = ""; const q = new URLSearchParams(); q.set("page", String(downloadPage)); q.set("pageSize", String(downloadPageSize)); const data = await apiFetch(`/me/downloads?${q.toString()}`); const items = (data && data.items) || []; const total = parseInt(data.total || 0, 10) || 0; const totalPages = Math.max(1, Math.ceil(total / downloadPageSize)); downloadList.innerHTML = ""; if (!items.length) { downloadList.appendChild( el( "div", { style: "text-align: center; padding: 28px 16px; color: var(--muted);" }, el("i", { class: "ri-inbox-line", style: "font-size: 2.2rem; margin-bottom: 12px; opacity: 0.5;" }), el("div", { style: "font-size: 1.05rem; margin-bottom: 6px;" }, "暂无下载记录"), el("div", { style: "font-size: 0.9rem;" }, "下载过资源后会显示在这里。") ) ); } else { items.forEach((it) => { let stateBadge = el("span", { class: "badge badge-success" }, "可用"); if (it.resourceState === "DELETED") stateBadge = el("span", { class: "badge badge-danger" }, "资源已删除"); else if (it.resourceState === "OFFLINE") stateBadge = el("span", { class: "badge badge-warning" }, "资源已下架"); const typeBadge = el( "span", { class: `badge ${it.resourceType === "VIP" ? "badge-vip" : "badge-free"}` }, `下载时:${it.resourceType === "VIP" ? "VIP" : "免费"}` ); const currentType = it.currentResourceType || ""; const currentTypeBadge = currentType && it.resourceState !== "DELETED" ? el( "span", { class: `badge ${currentType === "VIP" ? "badge-vip" : "badge-free"}` }, `当前:${currentType === "VIP" ? "VIP" : "免费"}` ) : null; const driftBadge = currentType && it.resourceType && currentType !== it.resourceType && it.resourceState === "ONLINE" ? el("span", { class: "badge badge-warning" }, "类型已变更") : null; const canOpenDetail = !!it.resourceId && it.resourceState === "ONLINE"; const titleNode = canOpenDetail ? el( "a", { href: `/ui/resources/${it.resourceId}`, style: "color: inherit; text-decoration: none;" }, it.resourceTitle || "" ) : el("span", { class: "muted" }, it.resourceTitle || ""); downloadList.appendChild( el( "div", { style: "border: 1px solid var(--border); border-radius: 12px; padding: 16px; background: var(--bg); display: flex; flex-direction: column; gap: 10px;" }, el( "div", { style: "display: flex; justify-content: space-between; align-items: center; gap: 12px;" }, el("div", { style: "font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" }, titleNode), stateBadge ), el( "div", { class: "muted", style: "display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 0.9rem;" }, typeBadge, currentTypeBadge, driftBadge, el("span", {}, `Ref:${it.ref || ""}`), el("span", {}, `下载:${formatDateTime(it.downloadedAt)}`) ) ) ); }); } if (downloadPager) { const prevBtn = el("button", { class: "btn", disabled: downloadPage <= 1 }, "上一页"); const nextBtn = el("button", { class: "btn", disabled: downloadPage >= totalPages }, "下一页"); prevBtn.addEventListener("click", async () => loadDownloads(downloadPage - 1)); nextBtn.addEventListener("click", async () => loadDownloads(downloadPage + 1)); downloadPager.appendChild(el("div", { class: "muted" }, `第 ${downloadPage} / ${totalPages} 页 · 共 ${total} 条`)); downloadPager.appendChild(el("div", { style: "display: flex; gap: 8px;" }, prevBtn, nextBtn)); } } async function load() { if (meMsg) { meMsg.style.display = "none"; meMsg.textContent = ""; } meInfo.innerHTML = ""; meInfo.appendChild(el("div", { class: "skeleton skeleton-line", style: "width: 60%;" })); meInfo.appendChild(el("div", { class: "skeleton skeleton-line", style: "width: 45%;" })); if (orderList) { orderList.innerHTML = ""; orderList.appendChild(el("div", { class: "card skeleton", style: "height: 96px;" })); orderList.appendChild(el("div", { class: "card skeleton", style: "height: 96px;" })); } if (downloadList) { downloadList.innerHTML = ""; downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" })); downloadList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" })); } if (downloadPager) downloadPager.innerHTML = ""; const data = await apiFetch("/me"); if (!data.user) { meInfo.innerHTML = ""; meInfo.appendChild(el("div", {}, "未登录")); meInfo.appendChild(el("div", { class: "muted" }, "登录后可查看会员状态与订单记录。")); 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()}` }, "去注册"))); if (orderSection) orderSection.style.display = "none"; if (downloadSection) downloadSection.style.display = "none"; if (meMsg) { meMsg.style.display = ""; meMsg.textContent = "提示:未登录"; } if (orderList) orderList.innerHTML = ""; if (downloadList) downloadList.innerHTML = ""; if (downloadPager) downloadPager.innerHTML = ""; return; } if (orderSection) orderSection.style.display = ""; if (downloadSection) downloadSection.style.display = ""; meInfo.innerHTML = ""; const infoGrid = el( "div", { class: "me-info-grid" }, el( "div", { class: "me-info-row" }, el("div", { class: "me-info-label" }, el("i", { class: "ri-smartphone-line" }), "手机号"), el("div", { class: "me-info-value" }, String(data.user.phone || "-")) ), el( "div", { class: "me-info-row" }, el("div", { class: "me-info-label" }, el("i", { class: "ri-vip-crown-line" }), "状态"), el( "div", { class: "me-info-value" }, el("span", { class: data.user.vipActive ? "badge badge-success" : "badge badge-danger" }, data.user.vipActive ? "VIP 有效" : "无/已过期") ) ), el( "div", { class: "me-info-row" }, el("div", { class: "me-info-label" }, el("i", { class: "ri-calendar-event-line" }), "到期时间"), el("div", { class: "me-info-value muted" }, formatDateTime(data.user.vipExpireAt)) ) ); meInfo.appendChild(infoGrid); const orders = await apiFetch("/orders"); orderList.innerHTML = ""; const items = (orders && orders.items) || []; if (!items.length) { orderList.appendChild( el("div", { style: "text-align: center; padding: 40px 20px; color: var(--muted);" }, el("i", { class: "ri-inbox-line", style: "font-size: 3rem; margin-bottom: 16px; opacity: 0.5;" }), el("div", { style: "font-size: 1.1rem; margin-bottom: 8px;" }, "暂无订单"), el("div", { style: "font-size: 0.9rem;" }, "购买会员后将显示订单记录。") ) ); } if (items.length) { items.forEach((o) => { let statusBadge; if (o.status === "PAID") statusBadge = el("span", { class: "badge badge-success" }, "已支付"); else if (o.status === "CLOSED") statusBadge = el("span", { class: "badge badge-danger" }, "已关闭"); else statusBadge = el("span", { class: "badge badge-info" }, o.status); orderList.appendChild( el( "div", { 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;" }, el("div", { style: "display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 12px;" }, el("div", { style: "font-weight: 500;" }, `订单号:${o.id}`), statusBadge ), el("div", { style: "display: flex; justify-content: space-between; align-items: center;" }, 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} 天)`)), el("div", { style: "font-weight: bold; color: var(--brand); font-size: 1.1rem;" }, formatCents(o.amountCents)) ), el("div", { class: "muted", style: "font-size: 0.9rem; display: flex; justify-content: space-between; margin-top: 4px;" }, el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-time-line" }), `创建:${formatDateTime(o.createdAt)}`), el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-check-double-line" }), `支付:${formatDateTime(o.paidAt)}`) ) ) ); }); } await loadDownloads(1); } if (logoutBtn) { logoutBtn.addEventListener("click", async () => { await apiFetch("/auth/logout", { method: "POST" }); window.location.reload(); }); } await load(); } async function pageMessages() { const messageList = document.getElementById("messageList"); const messagePager = document.getElementById("messagePager"); const messageUnreadBadge = document.getElementById("messageUnreadBadge"); const messageUnreadOnlyBtn = document.getElementById("messageUnreadOnlyBtn"); const messageAllBtn = document.getElementById("messageAllBtn"); const messageMsg = document.getElementById("messageMsg"); let page = 1; const pageSize = 10; let unreadOnly = false; function setMsg(text, isError = true) { if (!messageMsg) return; messageMsg.style.display = ""; messageMsg.className = `form-msg ${isError ? "form-msg-error" : "form-msg-success"}`; messageMsg.textContent = String(text || ""); } function clearMsg() { if (!messageMsg) return; messageMsg.style.display = "none"; messageMsg.textContent = ""; } function updateUnreadBadge(cnt) { if (!messageUnreadBadge) return; const n = parseInt(cnt || 0, 10) || 0; if (n > 0) { messageUnreadBadge.style.display = ""; messageUnreadBadge.textContent = `${n} 未读`; } else { messageUnreadBadge.style.display = "none"; messageUnreadBadge.textContent = ""; } } function updateToggleButtons() { if (messageUnreadOnlyBtn) messageUnreadOnlyBtn.className = `btn btn-sm ${unreadOnly ? "btn-primary" : ""}`.trim(); if (messageAllBtn) messageAllBtn.className = `btn btn-sm ${!unreadOnly ? "btn-primary" : ""}`.trim(); } async function loadMessages(targetPage) { if (!messageList) return; page = Math.max(1, parseInt(targetPage || 1, 10) || 1); clearMsg(); messageList.innerHTML = ""; messageList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" })); messageList.appendChild(el("div", { class: "card skeleton", style: "height: 72px;" })); if (messagePager) messagePager.innerHTML = ""; const q = new URLSearchParams(); q.set("page", String(page)); q.set("pageSize", String(pageSize)); if (unreadOnly) q.set("unread", "1"); let data; try { data = await apiFetch(`/me/messages?${q.toString()}`); } catch (e) { if (e.status === 401) { setMsg("未登录,无法查看消息。"); messageList.innerHTML = ""; messageList.appendChild(el("div", { class: "toolbar" }, el("a", { class: "btn btn-primary", href: `/ui/login?next=${currentNextParam()}` }, "去登录"))); return; } setMsg(e.detail?.error || e.status || "消息加载失败"); messageList.innerHTML = ""; return; } const items = (data && data.items) || []; const total = parseInt(data.total || 0, 10) || 0; const unreadCount = parseInt(data.unreadCount || 0, 10) || 0; const totalPages = Math.max(1, Math.ceil(total / pageSize)); updateUnreadBadge(unreadCount); updateToggleButtons(); if (typeof window.__refreshMessageBadge === "function") { try { await window.__refreshMessageBadge(); } catch (e) {} } messageList.innerHTML = ""; if (!items.length) { messageList.appendChild( el( "div", { style: "text-align: center; padding: 28px 16px; color: var(--muted);" }, el("i", { class: "ri-inbox-line", style: "font-size: 2.2rem; margin-bottom: 12px; opacity: 0.5;" }), el("div", { style: "font-size: 1.05rem; margin-bottom: 6px;" }, unreadOnly ? "暂无未读消息" : "暂无消息"), el("div", { style: "font-size: 0.9rem;" }, "系统通知会显示在这里。") ) ); } else { items.forEach((m) => { const read = !!m.read; const statusBadge = read ? el("span", { class: "badge" }, "已读") : el("span", { class: "badge badge-warning" }, "未读"); const actions = !read ? el( "button", { class: "btn btn-sm", onclick: async () => { try { await apiFetch(`/me/messages/${m.id}/read`, { method: "PUT" }); await loadMessages(page); } catch (e) { showToastError(e?.detail?.error || e?.status || "标记失败"); } }, }, "标记已读" ) : null; messageList.appendChild( el( "div", { style: "border: 1px solid var(--border); border-radius: 12px; padding: 16px; background: var(--bg); display: flex; flex-direction: column; gap: 10px;" }, el( "div", { style: "display: flex; justify-content: space-between; align-items: center; gap: 12px;" }, el("div", { style: "font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" }, m.title || "通知"), el("div", { style: "display: flex; align-items: center; gap: 8px; flex: 0 0 auto;" }, statusBadge, actions) ), el("div", { class: "muted", style: "white-space: pre-wrap;" }, formatMessageText(m.content || "")), el("div", { class: "muted", style: "font-size: 0.9rem;" }, `发送时间:${formatDateTime(m.createdAt)}`) ) ); }); } if (messagePager) { const prevBtn = el("button", { class: "btn", disabled: page <= 1 }, "上一页"); const nextBtn = el("button", { class: "btn", disabled: page >= totalPages }, "下一页"); prevBtn.addEventListener("click", async () => loadMessages(page - 1)); nextBtn.addEventListener("click", async () => loadMessages(page + 1)); messagePager.appendChild(el("div", { class: "muted" }, `第 ${page} / ${totalPages} 页 · 共 ${total} 条`)); messagePager.appendChild(el("div", { style: "display: flex; gap: 8px;" }, prevBtn, nextBtn)); } } if (messageUnreadOnlyBtn) { messageUnreadOnlyBtn.addEventListener("click", async () => { unreadOnly = true; await loadMessages(1); }); } if (messageAllBtn) { messageAllBtn.addEventListener("click", async () => { unreadOnly = false; await loadMessages(1); }); } updateToggleButtons(); await loadMessages(1); } async function pageVip() { const planList = document.getElementById("planList"); const vipMsg = document.getElementById("vipMsg"); const vipStatus = document.getElementById("vipStatus"); let me = null; try { me = await apiFetch("/me"); } catch (e) { me = { user: null }; } const user = me && me.user ? me.user : null; if (vipStatus) { vipStatus.style.display = ""; vipStatus.innerHTML = ""; if (!user) { vipStatus.appendChild( el("div", { style: "display: flex; flex-direction: column; align-items: center; padding: 16px;" }, el("div", { style: "margin-bottom: 16px; font-size: 1.1rem; color: var(--brand);" }, "未登录。登录后可购买/续费会员,并查看权益状态。"), 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;" }, el("i", { class: "ri-login-box-line" }), "去登录" ) ) ); } else { vipStatus.appendChild( el("div", { style: "display: flex; justify-content: space-around; flex-wrap: wrap; gap: 16px; padding: 16px;" }, el("div", { style: "display: flex; flex-direction: column; align-items: center; gap: 8px;" }, el("div", { class: "muted", style: "font-size: 0.9rem;" }, "当前账号"), 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) ), el("div", { style: "display: flex; flex-direction: column; align-items: center; gap: 8px;" }, el("div", { class: "muted", style: "font-size: 0.9rem;" }, "会员状态"), el("div", { style: "font-weight: 500; display: flex; align-items: center; gap: 4px;" }, el("i", { class: user.vipActive ? "ri-vip-crown-fill" : "ri-vip-crown-line", style: user.vipActive ? "color: #ffd700;" : "color: var(--muted);" }), el("span", { class: user.vipActive ? "badge badge-success" : "badge badge-danger", style: "margin-left: 4px;" }, user.vipActive ? "VIP 有效" : "无/已过期") ) ), el("div", { style: "display: flex; flex-direction: column; align-items: center; gap: 8px;" }, el("div", { class: "muted", style: "font-size: 0.9rem;" }, "到期时间"), 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)) ) ) ); } } planList.innerHTML = ""; planList.appendChild(el("div", { class: "card skeleton", style: "grid-column: 1 / -1; height: 120px;" })); const plans = await apiFetch("/plans"); planList.innerHTML = ""; plans.forEach((p) => { const isRecommended = plans[0] && p.id === plans[0].id; planList.appendChild( el( "div", { 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%;` }, 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, el( "div", { style: "text-align: center; margin-bottom: 24px;" }, el("h3", { style: "margin: 0 0 8px 0; font-size: 1.5rem; color: var(--text);" }, p.name), el("div", { class: "muted" }, `有效期 ${p.durationDays} 天`) ), el( "div", { style: "text-align: center; margin-bottom: 32px;" }, el("span", { style: "font-size: 1.25rem; color: var(--brand); font-weight: bold;" }, "¥ "), el("span", { style: "font-size: 2.5rem; color: var(--brand); font-weight: bold; line-height: 1;" }, (p.priceCents / 100).toFixed(2)) ), el( "div", { style: "margin-top: auto;" }, el( "button", { class: isRecommended ? "btn btn-primary" : "btn", style: "width: 100%; height: 48px; font-size: 1.1rem; border-radius: 24px; display: flex; justify-content: center; align-items: center; gap: 8px;", "data-plan-id": String(p.id), }, el("i", { class: "ri-shopping-cart-2-line" }), "立即开通" ) ) ) ); }); planList.addEventListener("click", async (evt) => { const btn = evt.target.closest("button[data-plan-id]"); if (!btn) return; vipMsg.textContent = ""; const originalText = btn.textContent; btn.disabled = true; btn.textContent = "处理中…"; const planId = Number(btn.getAttribute("data-plan-id")); try { const order = await apiFetch("/orders", { method: "POST", body: { planId } }); const payResp = await apiFetch(`/orders/${order.id}/pay`, { method: "POST" }); if (payResp && payResp.payUrl) { vipMsg.textContent = "已发起支付宝支付,正在跳转…"; // 打开支付宝收银台,同时在后台轮询订单状态 const payWin = window.open(payResp.payUrl, "_blank"); _startPayPolling(order.id, payWin); return; } vipMsg.textContent = "支付成功(模拟),已发放会员权益。"; showToastSuccess("支付成功,会员权益已生效"); setTimeout(() => { window.location.href = "/ui/me"; }, 300); } catch (e) { if (e.status === 401) window.location.href = `/ui/login?next=${currentNextParam()}`; vipMsg.textContent = `下单/支付失败:${e.detail?.error || e.status || "unknown"}`; } finally { btn.disabled = false; btn.textContent = originalText; } }); } /** * 支付轮询:每 3 秒向后端查询一次订单状态。 * 与 callback_url 回调竞争,谁先触发谁先激活,后端幂等保护。 * @param {string} orderId 订单 ID * @param {Window|null} payWin 支付宝收银台窗口(可为 null) */ function _startPayPolling(orderId, payWin) { const INTERVAL_MS = 3000; // 轮询间隔 3 秒 const MAX_TRIES = 40; // 最多轮询 40 次(约 2 分钟) let tries = 0; let stopped = false; const vipMsg = document.getElementById("vipMsg"); if (vipMsg) { vipMsg.style.display = ""; vipMsg.textContent = "等待支付结果,请在新窗口完成支付…"; } const timer = setInterval(async () => { if (stopped) return; tries++; try { const res = await apiFetch(`/orders/${orderId}/query-and-activate`, { method: "POST" }); if (res && res.status === "PAID") { stopped = true; clearInterval(timer); // 关闭支付宝窗口(如果还开着) try { if (payWin && !payWin.closed) payWin.close(); } catch (_) {} showToastSuccess("支付成功,会员权益已生效"); setTimeout(() => { window.location.href = "/ui/me"; }, 800); return; } } catch (_) { // 网络抖动,忽略,继续轮询 } if (tries >= MAX_TRIES) { stopped = true; clearInterval(timer); if (vipMsg) vipMsg.textContent = "未检测到支付结果,如已付款请稍后刷新个人中心查看。"; } }, INTERVAL_MS); } async function pageResourceDetail() { const root = document.getElementById("resourceDetail"); const resourceId = Number(root.getAttribute("data-resource-id")); const descRoot = document.getElementById("resourceDescription"); const refSelect = document.getElementById("refSelect"); const breadcrumb = document.getElementById("breadcrumb"); const treeEl = document.getElementById("tree"); const fileContent = document.getElementById("fileContent"); const downloadBtn = document.getElementById("downloadBtn"); let detail = null; let me = null; let inlineDownloadBtn = null; function setDownloadButtonLabel(btn, label) { if (!btn) return; const text = String(label || "").trim() || "下载 ZIP"; const iconClass = text.includes("开通会员") ? "ri-vip-crown-line" : "ri-download-cloud-2-line"; btn.innerHTML = ""; btn.appendChild(el("i", { class: iconClass })); btn.appendChild(document.createTextNode(text)); } function updateDownloadButton() { const user = me && me.user ? me.user : null; if (!user) { setDownloadButtonLabel(downloadBtn, "下载 ZIP"); setDownloadButtonLabel(inlineDownloadBtn, "下载 ZIP"); return; } if (detail && detail.type === "VIP" && !user.vipActive) { setDownloadButtonLabel(downloadBtn, "开通会员下载"); setDownloadButtonLabel(inlineDownloadBtn, "开通会员下载"); return; } setDownloadButtonLabel(downloadBtn, "下载 ZIP"); setDownloadButtonLabel(inlineDownloadBtn, "下载 ZIP"); } async function loadMe() { try { me = await apiFetch("/me"); } catch (e) { me = null; } updateDownloadButton(); } async function loadDetail() { const r = await apiFetch(`/resources/${resourceId}`); detail = r; inlineDownloadBtn = null; root.innerHTML = ""; if (descRoot) descRoot.innerHTML = ""; const coverCol = r.coverUrl ? 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);" }) : null; if (coverCol) ensureImgFallback(coverCol); inlineDownloadBtn = el( "button", { class: "btn btn-primary", style: "margin-top: 14px; border-radius: 10px; display: inline-flex; align-items: center; gap: 6px; width: fit-content;" }, el("i", { class: "ri-download-cloud-2-line" }), "下载 ZIP" ); inlineDownloadBtn.addEventListener("click", downloadZip); const metaCol = el( "div", { style: "flex: 1; display: flex; flex-direction: column;" }, el("h1", { style: "margin: 0 0 16px 0; font-size: 2rem; color: var(--text);" }, r.title), el( "div", { style: "display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap;" }, 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;" }), "免费资源"), 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}`), ...((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))) ), el("div", { class: "muted", style: "display: flex; gap: 16px; margin-bottom: 8px;" }, el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-eye-line" }), `浏览 ${r.viewCount}`), el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-download-cloud-2-line" }), `下载 ${r.downloadCount}`) ), 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 ? " (私有)" : ""}`), inlineDownloadBtn ); const headerRow = el("div", { style: "display: flex; gap: 32px; flex-wrap: wrap; margin-bottom: 32px;" }, coverCol, metaCol); root.appendChild( el( "div", { class: "card", style: "padding: 32px; border-radius: 16px; box-shadow: var(--shadow-md);" }, headerRow ) ); if (descRoot) { if (typeof renderMarkdown !== "function") { await loadScriptOnce("/static/app_markdown.js"); } const summary = el("div", { class: "md", style: "line-height: 1.8; color: var(--text-light);", html: renderMarkdown(r.summary || "暂无描述") }); descRoot.appendChild( el( "div", { class: "card", style: "margin-top:24px; padding: 32px; border-radius: 16px; box-shadow: var(--shadow-md);" }, el("h3", { style: "display: flex; align-items: center; gap: 8px; margin-bottom: 16px;" }, el("i", { class: "ri-file-text-line", style: "color: var(--brand);" }), "资源描述"), summary ) ); } updateDownloadButton(); } async function downloadZip() { const user = me && me.user ? me.user : null; const currentRef = refSelect ? String(refSelect.value || "") : ""; if (!currentRef) { showToastError("仓库加载中,请稍后再试"); return; } if (!user) { Swal.fire({ title: '未登录', text: '下载资源需要先登录账户', icon: 'info', showCancelButton: true, confirmButtonText: '去登录', cancelButtonText: '取消', confirmButtonColor: 'var(--brand)' }).then((result) => { if (result.isConfirmed) { window.location.href = `/ui/login?next=${currentNextParam()}`; } }); return; } if (detail && detail.type === "VIP" && !user.vipActive) { Swal.fire({ title: 'VIP 专享资源', text: '该资源为 VIP 专属,需要开通会员后才能下载。', icon: 'warning', showCancelButton: true, confirmButtonText: '去开通会员', cancelButtonText: '取消', confirmButtonColor: '#ffd700', iconColor: '#ffd700' }).then((result) => { if (result.isConfirmed) { window.location.href = "/ui/vip"; } }); return; } if (downloadBtn) downloadBtn.disabled = true; if (inlineDownloadBtn) inlineDownloadBtn.disabled = true; Swal.fire({ title: "正在准备下载", text: "请稍候…", allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); }, }); try { const prep = await apiFetch(`/resources/${resourceId}/download`, { method: "POST", body: { ref: currentRef }, }); const downloadUrl = prep?.downloadUrl || `/resources/${resourceId}/download?ref=${encodeURIComponent(currentRef)}`; const statusUrl = prep?.statusUrl || `/resources/${resourceId}/download/status?ref=${encodeURIComponent(currentRef)}`; const poll = async () => { for (let i = 0; i < 240; i++) { const st = await apiFetch(statusUrl, { method: "GET" }); if (st?.ready) return; if (st?.state === "error") { throw new Error(st?.error || "build_failed"); } await new Promise((resolve) => setTimeout(resolve, 800)); } throw new Error("build_timeout"); }; if (!prep?.ready) { Swal.update({ text: "正在生成压缩包,请稍候…" }); await poll(); } Swal.close(); window.location.href = downloadUrl; } catch (e) { Swal.close(); if (e.status === 401) window.location.href = `/ui/login?next=${currentNextParam()}`; if (e.status === 403 && e.detail?.error === "vip_required") { Swal.fire({ title: 'VIP 专享资源', text: '该资源为 VIP 专属,需要开通会员后才能下载。', icon: 'warning', showCancelButton: true, confirmButtonText: '去开通会员', cancelButtonText: '取消', confirmButtonColor: '#ffd700', iconColor: '#ffd700' }).then((result) => { if (result.isConfirmed) { window.location.href = "/ui/vip"; } }); return; } Swal.fire({ icon: 'error', title: '下载失败', text: e.detail?.error || e.message || e.status || "未知错误" }); } finally { if (downloadBtn) downloadBtn.disabled = false; if (inlineDownloadBtn) inlineDownloadBtn.disabled = false; } } if (downloadBtn) downloadBtn.addEventListener("click", downloadZip); try { await loadDetail(); } catch (e) { root.innerHTML = ""; root.appendChild(el("div", { class: "card" }, `加载失败:${e.detail?.error || e.status || "unknown"}`)); return; } await loadMe(); const startRepo = async () => { try { await loadScriptOnce("/static/app_user_repo.js"); if (typeof window.initUserRepoBrowser === "function") { await window.initUserRepoBrowser({ resourceId }); } } catch (e) { if (breadcrumb) breadcrumb.textContent = "仓库加载失败"; if (treeEl) { treeEl.innerHTML = ""; treeEl.appendChild(el("div", { class: "card", style: "margin: 8px; padding: 16px; border-radius: 12px;" }, `加载失败:${e?.detail?.error || e?.status || e?.message || "unknown"}`)); } if (fileContent) fileContent.textContent = ""; } }; try { if (window.requestIdleCallback) { window.requestIdleCallback(() => { startRepo(); }, { timeout: 1200 }); } else { setTimeout(() => { startRepo(); }, 0); } } catch (e) { startRepo(); } } async function main() { const page = document.body.getAttribute("data-page") || ""; try { await initTopbar(); if (page === "index") await pageIndex(); if (page === "resources") await pageIndex(); if (page === "login") await pageLogin(); if (page === "register") await pageRegister(); if (page === "me") await pageMe(); if (page === "messages") await pageMessages(); if (page === "vip") await pageVip(); if (page === "resource_detail") await pageResourceDetail(); } catch (e) { showToastError(e?.detail?.error || e?.status || e?.message || "页面初始化失败"); } } main();