478 lines
16 KiB
JavaScript
478 lines
16 KiB
JavaScript
// 聊天室 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
|
|
? `<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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 打开 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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|