304 lines
10 KiB
JavaScript
304 lines
10 KiB
JavaScript
const $ = (id) => document.getElementById(id);
|
||
let pendingOrderNo = "";
|
||
let pendingPollTimer = null;
|
||
let pendingPollCount = 0;
|
||
|
||
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);
|
||
}
|
||
|
||
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 fetchMe() {
|
||
const res = await fetch("/api/auth/me");
|
||
const data = await res.json();
|
||
if (!data.logged_in) {
|
||
window.location.href = "/auth?next=/upgrade";
|
||
return null;
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function formatDateTime(tsSec) {
|
||
const n = Number(tsSec || 0);
|
||
if (!n) return "-";
|
||
return new Date(n * 1000).toLocaleString();
|
||
}
|
||
|
||
function formatDate(tsSec) {
|
||
const n = Number(tsSec || 0);
|
||
if (!n) return "-";
|
||
const d = new Date(n * 1000);
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||
const day = String(d.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${day}`;
|
||
}
|
||
|
||
function refreshPurchaseCycleText() {
|
||
const el = $("purchaseCycleText");
|
||
if (!el) return;
|
||
const start = Math.floor(Date.now() / 1000);
|
||
const end = start + 30 * 24 * 3600;
|
||
el.textContent = `时长:1个月(到期:${formatDate(end)})`;
|
||
}
|
||
|
||
function renderVip(vip) {
|
||
const shared = Number(vip.shared_credits || vip.token_balance || 0);
|
||
const seatRemaining = Number(vip.seat_remaining_credits || 0);
|
||
const totalAvailable = Number(vip.total_available_credits || seatRemaining + shared);
|
||
const enabled = Boolean(vip.vip_enabled);
|
||
const cycleStart = Number(vip.cycle_started_at || 0);
|
||
const cycleEnd = Number(vip.cycle_expires_at || 0);
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const hasActiveSubscription = cycleStart > 0 && cycleEnd > now;
|
||
if ($("upgradeTokenBalance")) $("upgradeTokenBalance").textContent = String(totalAvailable);
|
||
if ($("upgradeTokenBalanceHero")) $("upgradeTokenBalanceHero").textContent = String(totalAvailable);
|
||
if ($("vipTokenBalance")) $("vipTokenBalance").textContent = String(shared);
|
||
if ($("vipSeatRemaining")) $("vipSeatRemaining").textContent = String(seatRemaining);
|
||
if ($("vipTotalConsumed")) $("vipTotalConsumed").textContent = String(Number(vip.total_consumed_tokens || 0));
|
||
if ($("vipEnabledSelect")) $("vipEnabledSelect").value = enabled ? "1" : "0";
|
||
if ($("vipStateText")) {
|
||
$("vipStateText").textContent = totalAvailable <= 0 ? "额度已用完" : enabled ? "平台模型已开启" : "平台模型已关闭";
|
||
}
|
||
if ($("vipCycleHint")) {
|
||
if (cycleStart > 0 && cycleEnd > 0) {
|
||
const startText = formatDateTime(cycleStart);
|
||
const endText = formatDateTime(cycleEnd);
|
||
$("vipCycleHint").textContent = `当前周期:${startText} - ${endText}(到期自动清零)`;
|
||
} else {
|
||
$("vipCycleHint").textContent = "当前未开始月周期,首次支付成功后开始计时。";
|
||
}
|
||
}
|
||
if ($("vipModeHint")) {
|
||
if (enabled && hasActiveSubscription) {
|
||
$("vipModeHint").textContent = "当前模型模式:平台模型(订阅有效)";
|
||
} else if (enabled && !hasActiveSubscription) {
|
||
$("vipModeHint").textContent = "当前模型模式:自由模型(未开通订阅)";
|
||
} else {
|
||
$("vipModeHint").textContent = "当前模型模式:自由模型";
|
||
}
|
||
}
|
||
if (totalAvailable <= 0) {
|
||
setStatus("Credits 额度已用完,请充值共享加油包或等待下个计费周期。", true);
|
||
}
|
||
}
|
||
|
||
async function refresh() {
|
||
const me = await fetchMe();
|
||
if (me && me.vip) renderVip(me.vip);
|
||
}
|
||
|
||
async function fetchBillingOverview() {
|
||
const res = await fetch("/api/billing/overview");
|
||
const data = await res.json();
|
||
if (!res.ok || !data.ok) throw new Error(data.detail || "账单读取失败");
|
||
return data;
|
||
}
|
||
|
||
function stopPendingPoll() {
|
||
if (pendingPollTimer) {
|
||
window.clearInterval(pendingPollTimer);
|
||
pendingPollTimer = null;
|
||
}
|
||
pendingPollCount = 0;
|
||
}
|
||
|
||
function startPendingPoll(orderNo, showCreatedNotice = true) {
|
||
if (!orderNo) return;
|
||
pendingOrderNo = orderNo;
|
||
stopPendingPoll();
|
||
const msg = `订单 ${orderNo} 已创建,请完成支付,系统将自动刷新余额。`;
|
||
setStatus(msg);
|
||
if (showCreatedNotice) {
|
||
importantNotice(msg, "订单已创建");
|
||
}
|
||
pendingPollTimer = window.setInterval(async () => {
|
||
pendingPollCount += 1;
|
||
try {
|
||
const data = await fetchBillingOverview();
|
||
const records = Array.isArray(data.recharge_records) ? data.recharge_records : [];
|
||
const current = records.find((r) => (r.order_no || "") === pendingOrderNo);
|
||
if (current && (current.status || "") === "paid") {
|
||
stopPendingPoll();
|
||
pendingOrderNo = "";
|
||
setStatus("支付成功,余额已自动刷新。");
|
||
await refresh();
|
||
return;
|
||
}
|
||
if (current && ["cancelled", "closed"].includes(String(current.status || "").toLowerCase())) {
|
||
stopPendingPoll();
|
||
pendingOrderNo = "";
|
||
setStatus("订单超过15分钟未支付,已自动取消。", true);
|
||
await refresh();
|
||
return;
|
||
}
|
||
if (pendingPollCount >= 120) {
|
||
stopPendingPoll();
|
||
setStatus("订单仍未支付,可支付后点击刷新查看余额。", true);
|
||
}
|
||
} catch {
|
||
if (pendingPollCount >= 120) {
|
||
stopPendingPoll();
|
||
}
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
const saveVipBtn = $("saveVipBtn");
|
||
const vipRechargeBtn = $("vipRechargeBtn");
|
||
const vipRechargeTokensInput = $("vipRechargeTokens");
|
||
const vipRechargeAmountInput = $("vipRechargeAmount");
|
||
const payChannelOptions = Array.from(document.querySelectorAll(".pay-channel-option"));
|
||
let selectedPayChannel = "wechat";
|
||
|
||
function bindPayChannelOptions() {
|
||
if (!payChannelOptions.length) return;
|
||
payChannelOptions.forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
selectedPayChannel = (btn.dataset.channel || "wechat").trim() || "wechat";
|
||
payChannelOptions.forEach((item) => {
|
||
const active = item === btn;
|
||
item.classList.toggle("is-active", active);
|
||
item.setAttribute("aria-pressed", active ? "true" : "false");
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function packageRate() {
|
||
const credits = Number((vipRechargeTokensInput && vipRechargeTokensInput.dataset.packageCredits) || "1500");
|
||
const amount = Number((vipRechargeAmountInput && vipRechargeAmountInput.dataset.packageAmount) || "19.9");
|
||
return {
|
||
packageCredits: Number.isFinite(credits) && credits > 0 ? credits : 1500,
|
||
packageAmount: Number.isFinite(amount) && amount > 0 ? amount : 19.9,
|
||
};
|
||
}
|
||
|
||
function syncRechargeAmount() {
|
||
const { packageCredits, packageAmount } = packageRate();
|
||
const credits = packageCredits;
|
||
const amount = packageAmount;
|
||
if (vipRechargeTokensInput) vipRechargeTokensInput.value = String(credits);
|
||
vipRechargeAmountInput.value = amount.toFixed(2);
|
||
if ($("purchaseCredits")) $("purchaseCredits").textContent = String(credits);
|
||
if ($("purchaseAmount")) $("purchaseAmount").textContent = `¥${amount.toFixed(2)}`;
|
||
}
|
||
|
||
if (vipRechargeAmountInput) vipRechargeAmountInput.readOnly = true;
|
||
syncRechargeAmount();
|
||
refreshPurchaseCycleText();
|
||
bindPayChannelOptions();
|
||
|
||
if (saveVipBtn) {
|
||
saveVipBtn.addEventListener("click", async () => {
|
||
setLoading(saveVipBtn, true, "保存升级设置", "保存中...");
|
||
try {
|
||
const enabled = (($("vipEnabledSelect") && $("vipEnabledSelect").value) || "0") === "1";
|
||
const out = await postJSON("/api/auth/vip/toggle", { enabled });
|
||
if (!out.ok) {
|
||
setStatus(out.detail || "VIP 设置保存失败", true);
|
||
return;
|
||
}
|
||
setStatus("升级设置已保存。");
|
||
await refresh();
|
||
} catch (e) {
|
||
setStatus(e.message || "VIP 设置保存失败", true);
|
||
} finally {
|
||
setLoading(saveVipBtn, false, "保存升级设置", "保存中...");
|
||
}
|
||
});
|
||
}
|
||
|
||
if (vipRechargeBtn) {
|
||
vipRechargeBtn.addEventListener("click", async () => {
|
||
setLoading(vipRechargeBtn, true, "订阅", "创建订单中...");
|
||
try {
|
||
const tokens = Number((($("vipRechargeTokens") && $("vipRechargeTokens").value) || "0").trim());
|
||
const amount = Number((($("vipRechargeAmount") && $("vipRechargeAmount").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: selectedPayChannel || "wechat",
|
||
subscriber_name: "",
|
||
subscriber_phone: "",
|
||
shipping_address: "",
|
||
});
|
||
if (!out.ok) {
|
||
setStatus(out.detail || "充值失败", true);
|
||
return;
|
||
}
|
||
const orderNo = out.order && out.order.order_no ? out.order.order_no : "";
|
||
if (orderNo) startPendingPoll(orderNo, true);
|
||
if (out.pay_url) {
|
||
openPayLink(out.pay_url);
|
||
return;
|
||
}
|
||
const tip = "订单已创建,但未获取到支付链接,请检查支付网关配置。";
|
||
setStatus(tip, true);
|
||
importantNotice(tip, "支付链接缺失");
|
||
} catch (e) {
|
||
setStatus(e.message || "充值失败", true);
|
||
} finally {
|
||
setLoading(vipRechargeBtn, false, "订阅", "创建订单中...");
|
||
}
|
||
});
|
||
}
|
||
|
||
refresh();
|
||
(async () => {
|
||
await fetchMe();
|
||
try {
|
||
const data = await fetchBillingOverview();
|
||
const records = Array.isArray(data.recharge_records) ? data.recharge_records : [];
|
||
const pending = records.find((r) => (r.status || "") === "pending");
|
||
if (pending && pending.order_no) startPendingPoll(pending.order_no, false);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
})();
|
||
window.setInterval(refreshPurchaseCycleText, 60000);
|