| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142 |
- 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();
|