const $ = (id) => document.getElementById(id); function setStatus(msg, danger = false) { const el = $("status"); if (!el) return; el.style.color = danger ? "#b42318" : "#0f5f3d"; el.textContent = msg; } function setLoading(button, loading, idleText, loadingText) { if (!button) return; button.disabled = loading; button.textContent = loading ? loadingText : idleText; } function importantNotice(message, title = "提示") { if (typeof window.uiAlert === "function") { void window.uiAlert(message, title); return; } setStatus(message, true); } function openPayLink(url) { const payUrl = String(url || "").trim(); if (!payUrl) return false; const tab = window.open(payUrl, "_blank", "noopener"); if (tab) return true; window.location.href = payUrl; return true; } async function postJSON(url, body) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || "请求失败"); return data; } async function authMe() { const res = await fetch("/api/auth/me"); const data = await res.json(); if (!data.logged_in) { window.location.href = "/auth?next=/billing"; return null; } return data; } function fmtTime(ts) { const n = Number(ts || 0); if (!n) return "-"; return new Date(n * 1000).toLocaleString(); } function mapOrderStatus(status) { const s = String(status || "").toLowerCase(); if (s === "paid" || s === "success") return "已支付"; if (s === "pending") return "待支付"; if (s === "failed") return "支付失败"; if (s === "cancelled") return "已取消"; if (s === "closed") return "已关闭"; return status || "-"; } function mapChannel(channel) { const c = String(channel || "").toLowerCase(); if (c === "wechat") return "微信支付"; if (c === "alipay") return "支付宝"; if (c === "stripe") return "Stripe"; return channel || "-"; } function formatDetail(detail) { if (!detail || typeof detail !== "object") return "-"; const rows = []; if (detail.source_chars) rows.push(`原文${detail.source_chars}字`); if (detail.body_chars) rows.push(`正文${detail.body_chars}字`); if (detail.title_chars) rows.push(`标题${detail.title_chars}字`); if (detail.summary_chars) rows.push(`摘要${detail.summary_chars}字`); if (detail.image_count) rows.push(`图片${detail.image_count}张`); if (detail.prompt_tokens) rows.push(`输入Token:${detail.prompt_tokens}`); if (detail.completion_tokens) rows.push(`输出Token:${detail.completion_tokens}`); if (detail.total_tokens) rows.push(`总Token:${detail.total_tokens}`); if (detail.model) rows.push(`模型:${detail.model}`); if (detail.image_model) rows.push(`生图模型:${detail.image_model}`); if (detail.image_price_package_images && detail.image_price_package_cny) { rows.push(`图片计价:${detail.image_price_package_images}张=${Number(detail.image_price_package_cny).toFixed(2)}元`); } if (detail.credits_rule) rows.push(`规则:${detail.credits_rule}`); if (detail.billed_basis === "usage_tokens") rows.push("按真实Token计费"); if (detail.billed_basis === "char_estimate") rows.push("按字符估算计费"); if (detail.paid_amount_cny) rows.push(`实付¥${Number(detail.paid_amount_cny).toFixed(2)}`); if (detail.external_txn_id) rows.push(`交易号:${detail.external_txn_id}`); if (detail.credit_source && typeof detail.credit_source === "object") { const seat = Number(detail.credit_source.seat || 0); const shared = Number(detail.credit_source.shared || 0); rows.push(`抵扣来源:席位${seat}/共享${shared}`); } return rows.length ? rows.join(" | ") : "-"; } function renderRechargeRecords(records) { const el = $("rechargeRecords"); if (!el) return; const list = Array.isArray(records) ? records : []; if (!list.length) { el.innerHTML = '

暂无充值记录

'; return; } const rows = list .map((r) => { const statusText = mapOrderStatus(r.status); const statusClass = statusText === "已支付" ? "paid" : statusText === "待支付" ? "pending" : statusText === "支付失败" ? "failed" : "closed"; return ` ${r.order_no || "-"} ${statusText} ${mapChannel(r.channel)} ${Number(r.token_amount || 0)} ¥${Number(r.amount_cny || 0).toFixed(2)} ${fmtTime(r.created_at)} ${r.paid_at ? fmtTime(r.paid_at) : "-"} ${ statusText === "待支付" ? `` : "-" } `; }) .join(""); el.innerHTML = `${rows}
订单号 状态 渠道 Credits 金额 创建时间 支付时间 操作
`; } function renderConsumeRecords(records) { const el = $("consumeRecords"); if (!el) return; const list = Array.isArray(records) ? records : []; if (!list.length) { el.innerHTML = '

暂无消费记录

'; return; } const kindTextMap = { trial_grant: "试用赠送", paid_recharge: "充值到账", manual_recharge: "手动充值", rewrite: "AI改写", cover_generate: "封面生成", poster_generate: "段落海报", usage: "模型调用", }; const rows = list .map((r) => { const kindText = kindTextMap[r.kind] || r.kind || "-"; const delta = `${r.direction === "out" ? "-" : "+"}${Number(r.token_change || 0)}`; const ref = `${r.ref_type || "-"} ${r.ref_id || ""}`.trim(); const detail = formatDetail(r.detail); return ` ${kindText} ${delta} ${Number(r.balance_after || 0)} ${ref} ${detail} ${fmtTime(r.created_at)} `; }) .join(""); el.innerHTML = `${rows}
类型 Credits变动 余额 关联 明细 时间
`; } async function refreshBilling() { const data = await fetch("/api/billing/overview").then((r) => r.json()); if (!data.ok) throw new Error(data.detail || "账单加载失败"); renderRechargeRecords(data.recharge_records || []); renderConsumeRecords(data.consume_records || []); } const createRechargeOrderBtn = $("createRechargeOrderBtn"); const refreshBillingBtn = $("refreshBillingBtn"); const logoutBtn = $("logoutBtn"); const billingRechargeTokensInput = $("billingRechargeTokens"); const billingRechargeAmountInput = $("billingRechargeAmount"); function packageRate() { const credits = Number((billingRechargeTokensInput && billingRechargeTokensInput.dataset.packageCredits) || "1500"); const amount = Number((billingRechargeAmountInput && billingRechargeAmountInput.dataset.packageAmount) || "19.9"); return { packageCredits: Number.isFinite(credits) && credits > 0 ? credits : 1500, packageAmount: Number.isFinite(amount) && amount > 0 ? amount : 19.9, }; } function syncBillingAmount() { if (!billingRechargeTokensInput || !billingRechargeAmountInput) return; const credits = Number((billingRechargeTokensInput.value || "0").trim()); if (!Number.isFinite(credits) || credits <= 0) return; const { packageCredits, packageAmount } = packageRate(); const amount = (credits / packageCredits) * packageAmount; billingRechargeAmountInput.value = amount.toFixed(2); } if (billingRechargeTokensInput) billingRechargeTokensInput.addEventListener("input", syncBillingAmount); if (billingRechargeAmountInput) billingRechargeAmountInput.readOnly = true; if (createRechargeOrderBtn) { createRechargeOrderBtn.addEventListener("click", async () => { setLoading(createRechargeOrderBtn, true, "创建充值订单", "创建中..."); try { const tokens = Number((($("billingRechargeTokens") && $("billingRechargeTokens").value) || "0").trim()); const amount = Number((($("billingRechargeAmount") && $("billingRechargeAmount").value) || "0").trim()); if (!Number.isFinite(tokens) || tokens <= 0) { setStatus("请输入正确的充值 Credits 数量", true); return; } if (!Number.isFinite(amount) || amount <= 0) { setStatus("请输入正确的支付金额", true); return; } const out = await postJSON("/api/pay/wechat/", { tokens: Math.round(tokens), amount_cny: Number(amount.toFixed(2)), channel: "wechat", }); if (!out.ok) { setStatus(out.detail || "创建订单失败", true); return; } const orderNo = out.order && out.order.order_no ? out.order.order_no : ""; if (out.pay_url) { openPayLink(out.pay_url); } else { importantNotice("订单已创建,但未获取到支付链接,请检查支付网关配置。", "支付链接缺失"); } setStatus(`订单已创建:${orderNo}`); await refreshBilling(); } catch (e) { setStatus(e.message || "创建订单失败", true); } finally { setLoading(createRechargeOrderBtn, false, "创建充值订单", "创建中..."); } }); } const rechargeRecordsWrap = $("rechargeRecords"); if (rechargeRecordsWrap) { rechargeRecordsWrap.addEventListener("click", async (evt) => { const btn = evt.target && evt.target.closest ? evt.target.closest(".pay-now-btn") : null; if (!btn) return; const orderNo = (btn.getAttribute("data-order-no") || "").trim(); if (!orderNo) return; setLoading(btn, true, "立即支付", "拉起中..."); try { const out = await postJSON("/api/billing/recharge/pay-now", { order_no: orderNo }); if (!out.ok) { setStatus(out.detail || "拉起支付失败", true); await refreshBilling(); return; } if (out.pay_url) { openPayLink(out.pay_url); } else { importantNotice("未获取到支付链接,请检查支付网关配置。", "立即支付失败"); } setStatus(`订单 ${orderNo} 已拉起支付。`); await refreshBilling(); } catch (e) { setStatus(e.message || "拉起支付失败", true); await refreshBilling(); } finally { setLoading(btn, false, "立即支付", "拉起中..."); } }); } if (refreshBillingBtn) { refreshBillingBtn.addEventListener("click", async () => { setLoading(refreshBillingBtn, true, "刷新账单记录", "刷新中..."); try { await refreshBilling(); setStatus("账单已刷新。"); } catch (e) { setStatus(e.message || "账单刷新失败", true); } finally { setLoading(refreshBillingBtn, false, "刷新账单记录", "刷新中..."); } }); } if (logoutBtn) { logoutBtn.addEventListener("click", async () => { try { await postJSON("/api/auth/logout", {}); } finally { window.location.href = "/auth?next=/"; } }); } (async () => { const me = await authMe(); if (!me) return; syncBillingAmount(); await refreshBilling(); })();