// 聊天室 VIP 中心模块,负责弹窗开关、会员数据渲染、支付跳转和专属进退场设置。 import { escapeHtml } from "./html.js"; const VIP_CENTER_URL = "/vip-center"; const VIP_PAYMENT_URL = "/vip/payment"; const VIP_PRESENCE_UPDATE_URL = "/vip-center/presence-settings"; let vipControlEventsBound = false; let vipData = null; /** * 读取 CSRF Token。 * * @returns {string} */ function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content || ""; } /** * 按 ID 获取 DOM 节点。 * * @param {string} id * @returns {HTMLElement|null} */ function element(id) { return document.getElementById(id); } /** * 显示全局弹窗提示。 * * @param {string} message * @param {string} title * @param {string} color * @returns {void} */ function showDialog(message, title = "提示", color = "#3b82f6") { window.chatDialog?.alert?.(message, title, color); } /** * 格式化金额。 * * @param {unknown} value * @returns {string} */ function formatMoney(value) { return Number(value || 0).toFixed(2); } /** * 过滤可写入 style 的颜色值。 * * @param {unknown} value * @param {string} fallback * @returns {string} */ function sanitizeColor(value, fallback = "#1e293b") { const color = String(value || "").trim(); if (/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(color) || /^rgba?\([\d\s,.%]+\)$/.test(color)) { return color; } return fallback; } /** * 生成会员等级按钮 HTML。 * * @param {object} level * @param {object} data * @returns {string} */ function renderVipAction(level, data) { const isCurrent = Boolean(level.is_current); const isHigher = Boolean(level.is_higher); const isLower = Boolean(level.is_lower); let buttonText = "立即购买"; let buttonColor = "#1e293b"; let buttonTextColor = "#fff"; let priceToDisplay = level.price; let isDisabled = !data.vipPaymentEnabled; let showUpgradeInfo = false; if (isCurrent) { buttonText = "立即续费"; buttonColor = "#f59e0b"; } else if (isHigher && data.user.is_vip) { buttonText = "补差价升级"; buttonColor = "#4f46e5"; priceToDisplay = level.upgrade_price; showUpgradeInfo = true; } else if (isLower) { buttonText = "无法降级"; buttonColor = "#f1f5f9"; buttonTextColor = "#94a3b8"; isDisabled = true; } const upgradeInfo = showUpgradeInfo ? `
已省 ¥${formatMoney(Number(level.price || 0) - Number(level.upgrade_price || 0))}
` : ""; const actionHtml = !data.vipPaymentEnabled || isDisabled ? `` : `
${buttonText}后将跳转到对应支付页面
`; return `
¥${formatMoney(priceToDisplay)} / ${Number(level.duration_days || 0)}天
${upgradeInfo} ${actionHtml} `; } /** * 渲染会员等级卡片。 * * @param {object} level * @param {object} data * @returns {string} */ function renderVipLevelCard(level, data) { const isCurrent = Boolean(level.is_current); const color = sanitizeColor(level.color); return `
${isCurrent ? '当前档位' : ""}
${escapeHtml(level.icon || "✨")} ${escapeHtml(level.name || "")}
${escapeHtml(level.description || "")}
经验获取 ${Number(level.exp_multiplier || 1)}x
金币获取 ${Number(level.jjb_multiplier || 1)}x
专属入场特效 & 横幅
${renderVipAction(level, data)}
`; } /** * 渲染 VIP 购买记录。 * * @param {Array} logs * @returns {void} */ function renderVipLogs(logs) { const logsBody = element("vip-logs-body"); if (!logsBody) { return; } if (!logs.length) { logsBody.innerHTML = '暂无记录'; return; } const statusMap = { created: { text: "待创建", className: "background:#f1f5f9;color:#475569" }, pending: { text: "待支付", className: "background:#fef3c7;color:#b45309" }, paid: { text: "已支付", className: "background:#dcfce7;color:#166534" }, closed: { text: "已关闭", className: "background:#f1f5f9;color:#64748b" }, failed: { text: "失败", className: "background:#fee2e2;color:#991b1b" }, }; logsBody.innerHTML = logs.map((log) => { const status = statusMap[log.status] || { text: escapeHtml(log.status || "未知"), className: "background:#f1f5f9" }; const openedAt = log.opened_vip_at ? String(log.opened_vip_at).substring(0, 16).replace("T", " ") : "—"; return ` ${escapeHtml(log.order_no || "")} ${escapeHtml(log.vip_name || "")} ¥${formatMoney(log.amount)} ${status.text} ${escapeHtml(openedAt)} `; }).join(""); } /** * 渲染 VIP 个性化设置。 * * @returns {void} */ function renderVipSettings() { const data = vipData; const user = data?.user; const level = user?.vip_level; if (!data || !user || !level) { return; } element("vip-theme-icon").textContent = level.icon || "✨"; element("vip-theme-name").textContent = level.name || ""; element("vip-join-effect").textContent = data.effectOptions[user.custom_join_effect || level.join_effect] || "无"; element("vip-join-banner").textContent = `风格:${data.bannerStyleOptions[level.join_banner] || "默认"}`; element("vip-leave-effect").textContent = data.effectOptions[user.custom_leave_effect || level.leave_effect] || "无"; element("vip-leave-banner").textContent = `风格:${data.bannerStyleOptions[level.leave_banner] || "默认"}`; element("vip-default-join").textContent = level.join_templates?.[0] || "当前档位尚未配置默认欢迎语。"; element("vip-default-leave").textContent = level.leave_templates?.[0] || "当前档位尚未配置默认离开语。"; const badge = element("vip-customize-badge"); const form = element("vip-customize-form"); const notice = element("vip-customize-notice"); if (user.can_customize) { badge.textContent = "已开启"; badge.style.background = "#dcfce7"; badge.style.color = "#166534"; form.style.display = "block"; notice.style.display = "none"; const joinSelect = element("vip-custom-join-effect"); const leaveSelect = element("vip-custom-leave-effect"); const optionsHtml = Object.entries(data.effectOptions || {}) .map(([key, label]) => ``) .join(""); joinSelect.innerHTML = optionsHtml; leaveSelect.innerHTML = optionsHtml; joinSelect.value = user.custom_join_effect || level.join_effect || "none"; leaveSelect.value = user.custom_leave_effect || level.leave_effect || "none"; element("vip-custom-join").value = user.custom_join_message || ""; element("vip-custom-leave").value = user.custom_leave_message || ""; return; } badge.textContent = "未开放"; badge.style.background = "#f1f5f9"; badge.style.color = "#64748b"; form.style.display = "none"; notice.style.display = "block"; notice.textContent = user.is_vip ? "当前会员档位暂未开放个人自定义功能,不过你仍会自动使用本等级配置的专属欢迎语、离开语和华丽特效。" : "开通会员后,这里会解锁对应等级的专属进退场主题;若等级允许,还能设置你自己的欢迎语和离开语。"; } /** * 渲染 VIP 弹窗整体内容。 * * @returns {void} */ function renderVipModal() { const data = vipData; if (!data) { return; } element("vip-current-name").textContent = data.user.is_vip ? (data.user.vip_name || "尊贵会员") : "普通用户"; element("vip-current-name").style.color = data.user.vip_level?.color || "#1e293b"; element("vip-expire-time").textContent = data.user.is_vip ? `到期时间:${data.user.hy_time}` : "开通会员享特权"; element("vip-total-amount").textContent = `¥${formatMoney(data.totalAmount)}`; const grid = element("vip-levels-grid"); grid.innerHTML = (data.vipLevels || []).map((level) => renderVipLevelCard(level, data)).join(""); const settingsButton = element("vip-tabbtn-settings"); if (data.user.is_vip) { settingsButton.style.display = "block"; renderVipSettings(); } else { settingsButton.style.display = "none"; } renderVipLogs(data.paymentLogs || []); } /** * 拉取 VIP 中心数据。 * * @returns {Promise} */ async function fetchVipData() { try { const response = await fetch(VIP_CENTER_URL, { headers: { Accept: "application/json", }, }); const json = await response.json(); if (json.status === "success") { vipData = json.data; renderVipModal(); } } catch (error) { console.error("Failed to fetch VIP data", error); } } /** * 打开 VIP 中心弹窗。 * * @returns {void} */ export function openVipModal() { element("vip-modal").style.display = "flex"; void fetchVipData(); } /** * 关闭 VIP 中心弹窗。 * * @returns {void} */ export function closeVipModal() { element("vip-modal").style.display = "none"; } /** * 切换 VIP 中心页签。 * * @param {string} tabName * @returns {void} */ export function switchVipTab(tabName) { document.querySelectorAll(".vip-tab-btn").forEach((button) => button.classList.remove("active")); document.querySelectorAll(".vip-view-pane").forEach((pane) => pane.classList.remove("active")); element(`vip-tabbtn-${tabName}`)?.classList.add("active"); element(`vip-view-${tabName}`)?.classList.add("active"); } /** * 创建 VIP 支付表单并打开支付页。 * * @param {number} levelId * @param {string} provider * @returns {void} */ export function buyVip(levelId, provider = "alipay") { const form = document.createElement("form"); form.method = "POST"; form.action = VIP_PAYMENT_URL; form.target = "_blank"; for (const [name, value] of Object.entries({ _token: csrf(), vip_level_id: levelId, provider, })) { const input = document.createElement("input"); input.type = "hidden"; input.name = name; input.value = String(value); form.appendChild(input); } document.body.appendChild(form); form.submit(); document.body.removeChild(form); closeVipModal(); const providerText = provider === "wechat" ? "微信支付二维码页" : "支付宝支付页"; showDialog(`正在为您打开${providerText},请在新页面完成支付。`, "支付提示", "#3b82f6"); } /** * 保存 VIP 专属进退场设置。 * * @returns {Promise} */ export async function saveVipPresenceSettings() { try { const response = await fetch(VIP_PRESENCE_UPDATE_URL, { method: "PUT", headers: { "Content-Type": "application/json", "Accept": "application/json", "X-CSRF-TOKEN": csrf(), }, body: JSON.stringify({ custom_join_message: element("vip-custom-join").value, custom_leave_message: element("vip-custom-leave").value, custom_join_effect: element("vip-custom-join-effect").value, custom_leave_effect: element("vip-custom-leave-effect").value, }), }); const json = await response.json(); if (json.status === "success") { showDialog(json.message, "提示", "#10b981"); await fetchVipData(); return; } showDialog(json.message || "保存失败", "提示", "#f59e0b"); } catch (error) { showDialog("网络异常,请重试", "错误", "#ef4444"); } } /** * 绑定 VIP 中心所有按钮事件,并挂载兼容全局函数。 * * @returns {void} */ export function bindVipControls() { if (typeof window === "undefined") { return; } window.openVipModal = openVipModal; window.closeVipModal = closeVipModal; window.switchVipTab = switchVipTab; window.buyVip = buyVip; window.saveVipPresenceSettings = saveVipPresenceSettings; if (vipControlEventsBound || typeof document === "undefined") { return; } vipControlEventsBound = true; document.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } const tabButton = event.target.closest("[data-vip-tab]"); if (tabButton) { event.preventDefault(); switchVipTab(tabButton.getAttribute("data-vip-tab") || ""); return; } if (event.target.closest("[data-vip-modal-close]")) { event.preventDefault(); closeVipModal(); return; } const buyButton = event.target.closest("[data-vip-buy-level]"); if (buyButton) { event.preventDefault(); const levelId = Number.parseInt(buyButton.getAttribute("data-vip-buy-level") || "", 10); const provider = buyButton.getAttribute("data-vip-buy-provider") || "alipay"; if (Number.isInteger(levelId)) { buyVip(levelId, provider); } return; } if (event.target.closest("[data-vip-save-presence]")) { event.preventDefault(); void saveVipPresenceSettings(); return; } const modal = element("vip-modal"); if (modal && event.target === modal) { closeVipModal(); } }); }