迁移会员中心脚本
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
? `<div style="font-size:10px; color:#4f46e5; font-weight:bold; margin-bottom:8px;">已省 ¥${formatMoney(Number(level.price || 0) - Number(level.upgrade_price || 0))}</div>`
|
||||
: "";
|
||||
const actionHtml = !data.vipPaymentEnabled || isDisabled
|
||||
? `<button disabled style="width:100%; border:none; border-radius:8px; padding:10px; font-size:13px; font-weight:bold; cursor:not-allowed; transition:all .2s; background:${buttonColor}; color:${buttonTextColor};">${!data.vipPaymentEnabled && !isLower ? "支付暂未开启" : buttonText}</button>`
|
||||
: `<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
|
||||
<button data-vip-buy-level="${Number(level.id)}" data-vip-buy-provider="alipay"
|
||||
style="border:none; border-radius:8px; padding:10px; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:${buttonColor}; color:${buttonTextColor};">
|
||||
支付宝
|
||||
</button>
|
||||
<button data-vip-buy-level="${Number(level.id)}" data-vip-buy-provider="wechat"
|
||||
style="border:none; border-radius:8px; padding:10px; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:#16a34a; color:#fff;">
|
||||
微信
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size:10px; color:#64748b; margin-top:8px; text-align:center;">${buttonText}后将跳转到对应支付页面</div>`;
|
||||
|
||||
return `
|
||||
<div style="font-size:18px; font-weight:900; color:#e11d48; margin-bottom:5px;">
|
||||
¥${formatMoney(priceToDisplay)}
|
||||
<span style="font-size:11px; font-weight:normal; color:#94a3b8;">/ ${Number(level.duration_days || 0)}天</span>
|
||||
</div>
|
||||
${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 `
|
||||
<div class="vip-level-card ${isCurrent ? "current" : ""}">
|
||||
${isCurrent ? '<span class="vip-level-badge">当前档位</span>' : ""}
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<span style="font-size:24px;">${escapeHtml(level.icon || "✨")}</span>
|
||||
<span style="font-size:16px; font-weight:bold; color:${color}">${escapeHtml(level.name || "")}</span>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#64748b; line-height:1.5; min-height:33px;">${escapeHtml(level.description || "")}</div>
|
||||
<div style="margin-top:5px; space-y:6px;">
|
||||
<div class="vip-feature-item"><span class="vip-feature-icon">✓</span> 经验获取 <b>${Number(level.exp_multiplier || 1)}x</b></div>
|
||||
<div class="vip-feature-item"><span class="vip-feature-icon">✓</span> 金币获取 <b>${Number(level.jjb_multiplier || 1)}x</b></div>
|
||||
<div class="vip-feature-item"><span class="vip-feature-icon">✓</span> 专属入场特效 & 横幅</div>
|
||||
</div>
|
||||
<div style="margin-top:auto; padding-top:10px;">
|
||||
${renderVipAction(level, data)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 VIP 购买记录。
|
||||
*
|
||||
* @param {Array<object>} logs
|
||||
* @returns {void}
|
||||
*/
|
||||
function renderVipLogs(logs) {
|
||||
const logsBody = element("vip-logs-body");
|
||||
if (!logsBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!logs.length) {
|
||||
logsBody.innerHTML = '<tr><td colspan="5" style="padding:40px; text-align:center; color:#94a3b8;">暂无记录</td></tr>';
|
||||
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 `
|
||||
<tr style="border-bottom:1px solid #f1f5f9;">
|
||||
<td style="padding:10px; font-family:monospace; color:#64748b;">${escapeHtml(log.order_no || "")}</td>
|
||||
<td style="padding:10px; font-weight:bold;">${escapeHtml(log.vip_name || "")}</td>
|
||||
<td style="padding:10px; font-weight:bold; color:#e11d48;">¥${formatMoney(log.amount)}</td>
|
||||
<td style="padding:10px;"><span style="display:inline-block; padding:2px 8px; border-radius:10px; font-size:10px; font-weight:bold; ${status.className}">${status.text}</span></td>
|
||||
<td style="padding:10px; color:#64748b;">${escapeHtml(openedAt)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).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]) => `<option value="${escapeHtml(key)}">${escapeHtml(label)}</option>`)
|
||||
.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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1623,294 +1623,7 @@ async function generateWechatBindCode() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const CSRF = () => document.querySelector('meta[name="csrf-token"]')?.content ?? '';
|
||||
let vipData = null;
|
||||
|
||||
window.openVipModal = function() {
|
||||
document.getElementById('vip-modal').style.display = 'flex';
|
||||
fetchVipData();
|
||||
};
|
||||
|
||||
window.closeVipModal = function() {
|
||||
document.getElementById('vip-modal').style.display = 'none';
|
||||
};
|
||||
|
||||
window.switchVipTab = function(tabName) {
|
||||
document.querySelectorAll('.vip-tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.vip-view-pane').forEach(pane => pane.classList.remove('active'));
|
||||
|
||||
document.getElementById('vip-tabbtn-' + tabName).classList.add('active');
|
||||
document.getElementById('vip-view-' + tabName).classList.add('active');
|
||||
};
|
||||
|
||||
async function fetchVipData() {
|
||||
try {
|
||||
const res = await fetch('{{ route('vip.center') }}', {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.status === 'success') {
|
||||
vipData = json.data;
|
||||
renderVipModal();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch VIP data', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderVipModal() {
|
||||
const d = vipData;
|
||||
|
||||
// 状态栏
|
||||
document.getElementById('vip-current-name').textContent = d.user.is_vip ? (d.user.vip_name || '尊贵会员') : '普通用户';
|
||||
document.getElementById('vip-current-name').style.color = d.user.vip_level?.color || '#1e293b';
|
||||
document.getElementById('vip-expire-time').textContent = d.user.is_vip ? `到期时间:${d.user.hy_time}` : '开通会员享特权';
|
||||
document.getElementById('vip-total-amount').textContent = `¥${Number(d.totalAmount).toFixed(2)}`;
|
||||
|
||||
// 会员等级
|
||||
const grid = document.getElementById('vip-levels-grid');
|
||||
grid.innerHTML = d.vipLevels.map(v => {
|
||||
const isCurrent = v.is_current;
|
||||
const isHigher = v.is_higher;
|
||||
const isLower = v.is_lower;
|
||||
|
||||
let btnText = '立即购买';
|
||||
let btnColor = '#1e293b';
|
||||
let btnTextColor = '#fff';
|
||||
let priceToDisplay = v.price;
|
||||
let isDisabled = !d.vipPaymentEnabled;
|
||||
let showUpgradeInfo = false;
|
||||
|
||||
if (isCurrent) {
|
||||
btnText = '立即续费';
|
||||
btnColor = '#f59e0b';
|
||||
btnTextColor = '#fff';
|
||||
} else if (isHigher && d.user.is_vip) {
|
||||
btnText = '补差价升级';
|
||||
btnColor = '#4f46e5';
|
||||
btnTextColor = '#fff';
|
||||
priceToDisplay = v.upgrade_price;
|
||||
showUpgradeInfo = true;
|
||||
} else if (isLower) {
|
||||
btnText = '无法降级';
|
||||
btnColor = '#f1f5f9';
|
||||
btnTextColor = '#94a3b8';
|
||||
isDisabled = true;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="vip-level-card ${isCurrent ? 'current' : ''}">
|
||||
${isCurrent ? '<span class="vip-level-badge">当前档位</span>' : ''}
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<span style="font-size:24px;">${v.icon}</span>
|
||||
<span style="font-size:16px; font-weight:bold; color:${v.color}">${v.name}</span>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#64748b; line-height:1.5; min-height:33px;">${v.description || ''}</div>
|
||||
<div style="margin-top:5px; space-y:6px;">
|
||||
<div class="vip-feature-item"><span class="vip-feature-icon">✓</span> 经验获取 <b>${v.exp_multiplier}x</b></div>
|
||||
<div class="vip-feature-item"><span class="vip-feature-icon">✓</span> 金币获取 <b>${v.jjb_multiplier}x</b></div>
|
||||
<div class="vip-feature-item"><span class="vip-feature-icon">✓</span> 专属入场特效 & 横幅</div>
|
||||
</div>
|
||||
<div style="margin-top:auto; padding-top:10px;">
|
||||
<div style="font-size:18px; font-weight:900; color:#e11d48; margin-bottom:5px;">
|
||||
¥${Number(priceToDisplay).toFixed(2)}
|
||||
<span style="font-size:11px; font-weight:normal; color:#94a3b8;">/ ${v.duration_days}天</span>
|
||||
</div>
|
||||
${showUpgradeInfo ? `<div style="font-size:10px; color:#4f46e5; font-weight:bold; margin-bottom:8px;">已省 ¥${(v.price - v.upgrade_price).toFixed(2)}</div>` : ''}
|
||||
${!d.vipPaymentEnabled || isDisabled
|
||||
? `<button ${isDisabled ? 'disabled' : 'disabled'}
|
||||
style="width:100%; border:none; border-radius:8px; padding:10px; font-size:13px; font-weight:bold; cursor:not-allowed; transition:all .2s;
|
||||
background:${btnColor}; color:${btnTextColor};">
|
||||
${!d.vipPaymentEnabled && !isLower ? '支付暂未开启' : btnText}
|
||||
</button>`
|
||||
: `<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
|
||||
<button data-vip-buy-level="${v.id}" data-vip-buy-provider="alipay"
|
||||
style="border:none; border-radius:8px; padding:10px; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:${btnColor}; color:${btnTextColor};">
|
||||
支付宝
|
||||
</button>
|
||||
<button data-vip-buy-level="${v.id}" data-vip-buy-provider="wechat"
|
||||
style="border:none; border-radius:8px; padding:10px; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:#16a34a; color:#fff;">
|
||||
微信
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size:10px; color:#64748b; margin-top:8px; text-align:center;">${btnText}后将跳转到对应支付页面</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 会员设置 Tab 显隐
|
||||
const settingsBtn = document.getElementById('vip-tabbtn-settings');
|
||||
if (d.user.is_vip) {
|
||||
settingsBtn.style.display = 'block';
|
||||
renderVipSettings();
|
||||
} else {
|
||||
settingsBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// 购买记录
|
||||
const logsBody = document.getElementById('vip-logs-body');
|
||||
if (d.paymentLogs.length === 0) {
|
||||
logsBody.innerHTML = '<tr><td colspan="5" style="padding:40px; text-align:center; color:#94a3b8;">暂无记录</td></tr>';
|
||||
} else {
|
||||
logsBody.innerHTML = d.paymentLogs.map(log => {
|
||||
const statusMap = {
|
||||
'created': { text: '待创建', class: 'background:#f1f5f9;color:#475569' },
|
||||
'pending': { text: '待支付', class: 'background:#fef3c7;color:#b45309' },
|
||||
'paid': { text: '已支付', class: 'background:#dcfce7;color:#166534' },
|
||||
'closed': { text: '已关闭', class: 'background:#f1f5f9;color:#64748b' },
|
||||
'failed': { text: '失败', class: 'background:#fee2e2;color:#991b1b' },
|
||||
};
|
||||
const s = statusMap[log.status] || { text: log.status, class: 'background:#f1f5f9' };
|
||||
return `
|
||||
<tr style="border-bottom:1px solid #f1f5f9;">
|
||||
<td style="padding:10px; font-family:monospace; color:#64748b;">${log.order_no}</td>
|
||||
<td style="padding:10px; font-weight:bold;">${log.vip_name}</td>
|
||||
<td style="padding:10px; font-weight:bold; color:#e11d48;">¥${Number(log.amount).toFixed(2)}</td>
|
||||
<td style="padding:10px;"><span style="display:inline-block; padding:2px 8px; border-radius:10px; font-size:10px; font-weight:bold; ${s.class}">${s.text}</span></td>
|
||||
<td style="padding:10px; color:#64748b;">${log.opened_vip_at ? log.opened_vip_at.substring(0, 16).replace('T', ' ') : '—'}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
function renderVipSettings() {
|
||||
const d = vipData;
|
||||
const u = d.user;
|
||||
const vl = u.vip_level;
|
||||
|
||||
if (!vl) return;
|
||||
|
||||
document.getElementById('vip-theme-icon').textContent = vl.icon || '✨';
|
||||
document.getElementById('vip-theme-name').textContent = vl.name;
|
||||
document.getElementById('vip-join-effect').textContent = d.effectOptions[u.custom_join_effect || vl.join_effect] || '无';
|
||||
document.getElementById('vip-join-banner').textContent = `风格:${d.bannerStyleOptions[vl.join_banner] || '默认'}`;
|
||||
document.getElementById('vip-leave-effect').textContent = d.effectOptions[u.custom_leave_effect || vl.leave_effect] || '无';
|
||||
document.getElementById('vip-leave-banner').textContent = `风格:${d.bannerStyleOptions[vl.leave_banner] || '默认'}`;
|
||||
|
||||
document.getElementById('vip-default-join').textContent = vl.join_templates?.[0] || '当前档位尚未配置默认欢迎语。';
|
||||
document.getElementById('vip-default-leave').textContent = vl.leave_templates?.[0] || '当前档位尚未配置默认离开语。';
|
||||
|
||||
const badge = document.getElementById('vip-customize-badge');
|
||||
const form = document.getElementById('vip-customize-form');
|
||||
const notice = document.getElementById('vip-customize-notice');
|
||||
|
||||
if (u.can_customize) {
|
||||
badge.textContent = '已开启';
|
||||
badge.style.background = '#dcfce7';
|
||||
badge.style.color = '#166534';
|
||||
form.style.display = 'block';
|
||||
notice.style.display = 'none';
|
||||
|
||||
// 填充特效选项
|
||||
const joinSelect = document.getElementById('vip-custom-join-effect');
|
||||
const leaveSelect = document.getElementById('vip-custom-leave-effect');
|
||||
|
||||
let optionsHtml = '';
|
||||
for (const [key, label] of Object.entries(d.effectOptions)) {
|
||||
optionsHtml += `<option value="${key}">${label}</option>`;
|
||||
}
|
||||
joinSelect.innerHTML = optionsHtml;
|
||||
leaveSelect.innerHTML = optionsHtml;
|
||||
|
||||
// 设置当前值
|
||||
joinSelect.value = u.custom_join_effect || vl.join_effect || 'none';
|
||||
leaveSelect.value = u.custom_leave_effect || vl.leave_effect || 'none';
|
||||
|
||||
document.getElementById('vip-custom-join').value = u.custom_join_message || '';
|
||||
document.getElementById('vip-custom-leave').value = u.custom_leave_message || '';
|
||||
} else {
|
||||
badge.textContent = '未开放';
|
||||
badge.style.background = '#f1f5f9';
|
||||
badge.style.color = '#64748b';
|
||||
form.style.display = 'none';
|
||||
notice.style.display = 'block';
|
||||
notice.textContent = u.is_vip
|
||||
? '当前会员档位暂未开放个人自定义功能,不过你仍会自动使用本等级配置的专属欢迎语、离开语和华丽特效。'
|
||||
: '开通会员后,这里会解锁对应等级的专属进退场主题;若等级允许,还能设置你自己的欢迎语和离开语。';
|
||||
}
|
||||
}
|
||||
|
||||
window.buyVip = function(levelId, provider = 'alipay') {
|
||||
// 这里我们模拟提交表单,因为支付逻辑通常需要页面跳转
|
||||
// 修改为在新窗口打开支付,避免聊天室页面丢失
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ route('vip.payment.store') }}';
|
||||
form.target = '_blank'; // 新窗口打开支付
|
||||
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = '_token';
|
||||
csrfInput.value = CSRF();
|
||||
form.appendChild(csrfInput);
|
||||
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'hidden';
|
||||
idInput.name = 'vip_level_id';
|
||||
idInput.value = levelId;
|
||||
form.appendChild(idInput);
|
||||
|
||||
const providerInput = document.createElement('input');
|
||||
providerInput.type = 'hidden';
|
||||
providerInput.name = 'provider';
|
||||
providerInput.value = provider;
|
||||
form.appendChild(providerInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
|
||||
// 提交后关闭弹窗并提示用户
|
||||
closeVipModal();
|
||||
if (window.chatDialog) {
|
||||
const providerText = provider === 'wechat' ? '微信支付二维码页' : '支付宝支付页';
|
||||
window.chatDialog.alert(`正在为您打开${providerText},请在新页面完成支付。`, '支付提示', '#3b82f6');
|
||||
}
|
||||
};
|
||||
|
||||
window.saveVipPresenceSettings = async function() {
|
||||
const joinMsg = document.getElementById('vip-custom-join').value;
|
||||
const leaveMsg = document.getElementById('vip-custom-leave').value;
|
||||
const joinEffect = document.getElementById('vip-custom-join-effect').value;
|
||||
const leaveEffect = document.getElementById('vip-custom-leave-effect').value;
|
||||
|
||||
try {
|
||||
const res = await fetch('{{ route('vip.center.presence.update') }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': CSRF()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
custom_join_message: joinMsg,
|
||||
custom_leave_message: leaveMsg,
|
||||
custom_join_effect: joinEffect,
|
||||
custom_leave_effect: leaveEffect
|
||||
})
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.status === 'success') {
|
||||
window.chatDialog?.alert(json.message, '提示', '#10b981');
|
||||
fetchVipData();
|
||||
} else {
|
||||
window.chatDialog?.alert(json.message || '保存失败', '提示', '#f59e0b');
|
||||
}
|
||||
} catch (e) {
|
||||
window.chatDialog?.alert('网络异常,请重试', '错误', '#ef4444');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('vip-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeVipModal();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{{-- VIP 中心业务脚本已迁移到 resources/js/chat-room/vip-controls.js --}}
|
||||
|
||||
{{-- ═══════════ 婚姻状态弹窗 ═══════════ --}}
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user