From dac8adfc5a690b1d8b0c3465e050bd39781fea05 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 14:40:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E4=BC=9A=E5=91=98=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/chat-room.js | 16 +- resources/js/chat-room/vip-controls.js | 445 +++++++++++++++++- .../chat/partials/layout/toolbar.blade.php | 289 +----------- 3 files changed, 441 insertions(+), 309 deletions(-) diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 76a5df4..541519e 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -36,7 +36,7 @@ * - profile-controls.js:处理用户资料和资料相关按钮。 * - shop-controls.js:处理商店弹窗的基础按钮事件。 * - slot-machine.js:提供老虎机 slotPanel/slotFab Alpine 组件。 - * - vip-controls.js:处理 VIP 中心相关入口。 + * - vip-controls.js:处理 VIP 中心弹窗、会员数据渲染、支付跳转和专属进退场设置。 * - preferences-status.js:处理聊天偏好、屏蔽系统播报和静音状态。 * - right-panel.js:处理右侧在线用户列表和用户名交互。 * - rooms.js:处理房间在线状态渲染和跳转 URL。 @@ -111,7 +111,7 @@ export { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-pane export { bindProfileControls } from "./chat-room/profile-controls.js"; export { bindShopControls } from "./chat-room/shop-controls.js"; export { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js"; -export { bindVipControls } from "./chat-room/vip-controls.js"; +export { bindVipControls, buyVip, closeVipModal, openVipModal, saveVipPresenceSettings, switchVipTab } from "./chat-room/vip-controls.js"; export { BLOCKABLE_SYSTEM_SENDERS, BLOCKED_SYSTEM_SENDERS_STORAGE_KEY, @@ -205,7 +205,7 @@ import { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-pane import { bindProfileControls } from "./chat-room/profile-controls.js"; import { bindShopControls } from "./chat-room/shop-controls.js"; import { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js"; -import { bindVipControls } from "./chat-room/vip-controls.js"; +import { bindVipControls, buyVip, closeVipModal, openVipModal, saveVipPresenceSettings, switchVipTab } from "./chat-room/vip-controls.js"; import { BLOCKABLE_SYSTEM_SENDERS, BLOCKED_SYSTEM_SENDERS_STORAGE_KEY, @@ -323,6 +323,11 @@ if (typeof window !== "undefined") { slotFab, slotPanel, bindVipControls, + buyVip, + closeVipModal, + openVipModal, + saveVipPresenceSettings, + switchVipTab, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize, closeChatImageLightbox, @@ -403,6 +408,11 @@ if (typeof window !== "undefined") { window.openLotteryPanel = openLotteryPanel; window.openBankModal = openBankModal; window.showLotteryMsg = showLotteryMsg; + window.buyVip = buyVip; + window.closeVipModal = closeVipModal; + window.openVipModal = openVipModal; + window.saveVipPresenceSettings = saveVipPresenceSettings; + window.switchVipTab = switchVipTab; window.switchBankTab = switchBankTab; window.toggleBankRankSort = toggleBankRankSort; window.applyFontSize = applyFontSize; diff --git a/resources/js/chat-room/vip-controls.js b/resources/js/chat-room/vip-controls.js index 1d4acd5..61b7808 100644 --- a/resources/js/chat-room/vip-controls.js +++ b/resources/js/chat-room/vip-controls.js @@ -1,27 +1,433 @@ -// 会员中心基础按钮事件绑定,替代 toolbar VIP 区域内联 onclick。 +// 聊天室 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。 * - * @param {string} functionName 全局函数名 - * @param {...unknown} args 参数 + * @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 callVipGlobal(functionName, ...args) { - // VIP 业务函数暂留在 Blade 内,当前模块只统一按钮事件与旧函数调用边界。 - if (typeof window[functionName] === "function") { - window[functionName](...args); +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); } } /** - * 绑定会员中心 tab、关闭、购买与个性化保存事件。 + * 打开 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; } @@ -32,37 +438,40 @@ export function bindVipControls() { return; } - // VIP 内容由接口动态渲染,tab 和购买按钮通过 data-* 代理避免重复绑定。 const tabButton = event.target.closest("[data-vip-tab]"); if (tabButton) { event.preventDefault(); - callVipGlobal("switchVipTab", tabButton.getAttribute("data-vip-tab") || ""); + switchVipTab(tabButton.getAttribute("data-vip-tab") || ""); return; } if (event.target.closest("[data-vip-modal-close]")) { event.preventDefault(); - callVipGlobal("closeVipModal"); + closeVipModal(); return; } const buyButton = event.target.closest("[data-vip-buy-level]"); if (buyButton) { event.preventDefault(); - - // 购买按钮由 VIP 数据动态渲染,等级和支付渠道从 data 属性读取。 const levelId = Number.parseInt(buyButton.getAttribute("data-vip-buy-level") || "", 10); const provider = buyButton.getAttribute("data-vip-buy-provider") || "alipay"; - if (Number.isInteger(levelId)) { - callVipGlobal("buyVip", levelId, provider); - } + if (Number.isInteger(levelId)) { + buyVip(levelId, provider); + } return; } if (event.target.closest("[data-vip-save-presence]")) { event.preventDefault(); - callVipGlobal("saveVipPresenceSettings"); + void saveVipPresenceSettings(); + return; + } + + const modal = element("vip-modal"); + if (modal && event.target === modal) { + closeVipModal(); } }); } diff --git a/resources/views/chat/partials/layout/toolbar.blade.php b/resources/views/chat/partials/layout/toolbar.blade.php index 1ee6728..22937f6 100644 --- a/resources/views/chat/partials/layout/toolbar.blade.php +++ b/resources/views/chat/partials/layout/toolbar.blade.php @@ -1623,294 +1623,7 @@ async function generateWechatBindCode() { - +{{-- VIP 中心业务脚本已迁移到 resources/js/chat-room/vip-controls.js --}} {{-- ═══════════ 婚姻状态弹窗 ═══════════ --}}