迁移商店弹窗脚本
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
* - fishing.js:处理钓鱼抛竿、收竿、浮漂和自动钓鱼循环。
|
||||
* - fortune-panel.js:提供神秘占卜 fortunePanel Alpine 组件。
|
||||
* - profile-controls.js:处理头像选择、个人资料、密码、邮箱验证码和微信绑定入口。
|
||||
* - shop-controls.js:处理商店弹窗的基础按钮事件。
|
||||
* - shop-controls.js:处理商店弹窗、商品加载、购买、赠礼和改名卡入口。
|
||||
* - slot-machine.js:提供老虎机 slotPanel/slotFab Alpine 组件。
|
||||
* - vip-controls.js:处理 VIP 中心弹窗、会员数据渲染、支付跳转和专属进退场设置。
|
||||
* - preferences-status.js:处理聊天偏好、屏蔽系统播报和静音状态。
|
||||
@@ -132,7 +132,22 @@ export {
|
||||
showInlineMsg,
|
||||
unbindWechat,
|
||||
} from "./chat-room/profile-controls.js";
|
||||
export { bindShopControls } from "./chat-room/shop-controls.js";
|
||||
export {
|
||||
bindShopControls,
|
||||
buyItem,
|
||||
closeGiftDialog,
|
||||
closeRenameModal,
|
||||
closeShopModal,
|
||||
confirmGift,
|
||||
fetchShopData,
|
||||
loadShop,
|
||||
openGiftDialog,
|
||||
openRenameModal,
|
||||
openShopModal,
|
||||
renderShop,
|
||||
showShopToast,
|
||||
submitRename,
|
||||
} from "./chat-room/shop-controls.js";
|
||||
export { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js";
|
||||
export { bindVipControls, buyVip, closeVipModal, openVipModal, saveVipPresenceSettings, switchVipTab } from "./chat-room/vip-controls.js";
|
||||
export {
|
||||
@@ -249,7 +264,22 @@ import {
|
||||
showInlineMsg,
|
||||
unbindWechat,
|
||||
} from "./chat-room/profile-controls.js";
|
||||
import { bindShopControls } from "./chat-room/shop-controls.js";
|
||||
import {
|
||||
bindShopControls,
|
||||
buyItem,
|
||||
closeGiftDialog,
|
||||
closeRenameModal,
|
||||
closeShopModal,
|
||||
confirmGift,
|
||||
fetchShopData,
|
||||
loadShop,
|
||||
openGiftDialog,
|
||||
openRenameModal,
|
||||
openShopModal,
|
||||
renderShop,
|
||||
showShopToast,
|
||||
submitRename,
|
||||
} from "./chat-room/shop-controls.js";
|
||||
import { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js";
|
||||
import { bindVipControls, buyVip, closeVipModal, openVipModal, saveVipPresenceSettings, switchVipTab } from "./chat-room/vip-controls.js";
|
||||
import {
|
||||
@@ -390,6 +420,19 @@ if (typeof window !== "undefined") {
|
||||
showInlineMsg,
|
||||
unbindWechat,
|
||||
bindShopControls,
|
||||
buyItem,
|
||||
closeGiftDialog,
|
||||
closeRenameModal,
|
||||
closeShopModal,
|
||||
confirmGift,
|
||||
fetchShopData,
|
||||
loadShop,
|
||||
openGiftDialog,
|
||||
openRenameModal,
|
||||
openShopModal,
|
||||
renderShop,
|
||||
showShopToast,
|
||||
submitRename,
|
||||
bindSlotMachineControls,
|
||||
slotFab,
|
||||
slotPanel,
|
||||
@@ -512,6 +555,19 @@ if (typeof window !== "undefined") {
|
||||
window.sendEmailCode = sendEmailCode;
|
||||
window.showInlineMsg = showInlineMsg;
|
||||
window.unbindWechat = unbindWechat;
|
||||
window.buyItem = buyItem;
|
||||
window.closeGiftDialog = closeGiftDialog;
|
||||
window.closeRenameModal = closeRenameModal;
|
||||
window.closeShopModal = closeShopModal;
|
||||
window.confirmGift = confirmGift;
|
||||
window.fetchShopData = fetchShopData;
|
||||
window.loadShop = loadShop;
|
||||
window.openGiftDialog = openGiftDialog;
|
||||
window.openRenameModal = openRenameModal;
|
||||
window.openShopModal = openShopModal;
|
||||
window.renderShop = renderShop;
|
||||
window.showShopToast = showShopToast;
|
||||
window.submitRename = submitRename;
|
||||
|
||||
// 页面加载后立即注册事件委托,具体业务逻辑仍由各子模块负责。
|
||||
bindChatBanner();
|
||||
|
||||
@@ -1,27 +1,653 @@
|
||||
// 商店弹窗基础按钮事件绑定,替代 toolbar 商店区域内联 onclick。
|
||||
// 商店弹窗业务模块,负责商品加载、购买、赠礼、改名卡和商店按钮事件。
|
||||
|
||||
import { escapeHtml } from "./html.js";
|
||||
|
||||
const DEFAULT_SHOP_ITEMS_URL = "/shop/items";
|
||||
const DEFAULT_SHOP_BUY_URL = "/shop/buy";
|
||||
const DEFAULT_SHOP_RENAME_URL = "/shop/rename";
|
||||
|
||||
const WEEK_EFFECT_ICONS = {
|
||||
fireworks: "🎆",
|
||||
rain: "🌧",
|
||||
lightning: "⚡",
|
||||
snow: "❄️",
|
||||
sakura: "🌸",
|
||||
meteors: "🌠",
|
||||
"gold-rain": "🪙",
|
||||
hearts: "💖",
|
||||
confetti: "🎊",
|
||||
fireflies: "✨",
|
||||
};
|
||||
|
||||
const SHOP_GROUPS = [
|
||||
{
|
||||
label: "⚡ 单次特效卡",
|
||||
desc: "立即播放一次",
|
||||
type: "instant",
|
||||
},
|
||||
{
|
||||
label: "📅 周卡 7天登录自动播放",
|
||||
desc: "同时只能激活一种,购新旧失效不退款",
|
||||
type: "duration",
|
||||
},
|
||||
{
|
||||
label: "💍 求婚戒指",
|
||||
desc: "存入背包,求婚时消耗(被拒则遗失)",
|
||||
type: "ring",
|
||||
},
|
||||
{
|
||||
label: "🎣 自动钓鱼卡",
|
||||
desc: "激活后自动收篼,无需手动点击浮漂",
|
||||
type: "auto_fishing",
|
||||
},
|
||||
{
|
||||
label: "🎭 道具",
|
||||
desc: "",
|
||||
type: "tools",
|
||||
},
|
||||
];
|
||||
|
||||
let shopControlEventsBound = false;
|
||||
let shopLoaded = false;
|
||||
let giftItem = null;
|
||||
|
||||
/**
|
||||
* 调用商店存量全局函数。
|
||||
* 读取商店根节点上由 Blade 注入的接口地址。
|
||||
*
|
||||
* @returns {{items: string, buy: string, rename: string}}
|
||||
*/
|
||||
function getShopUrls() {
|
||||
const modal = document.getElementById("shop-modal");
|
||||
|
||||
return {
|
||||
items: modal?.dataset.shopItemsUrl || DEFAULT_SHOP_ITEMS_URL,
|
||||
buy: modal?.dataset.shopBuyUrl || DEFAULT_SHOP_BUY_URL,
|
||||
rename: modal?.dataset.shopRenameUrl || DEFAULT_SHOP_RENAME_URL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 CSRF Token,给 fetch 请求统一使用。
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function csrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 fetch 请求头。
|
||||
*
|
||||
* @param {boolean} withJson 是否声明 JSON 请求体
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
function jsonHeaders(withJson = false) {
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
"X-CSRF-TOKEN": csrfToken(),
|
||||
};
|
||||
|
||||
if (withJson) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开商店弹窗,首次打开时拉取商品列表。
|
||||
*
|
||||
* @param {string} functionName 全局函数名
|
||||
* @returns {void}
|
||||
*/
|
||||
function callShopGlobal(functionName) {
|
||||
// 商店主体逻辑仍在 Blade 全局函数内,这里只把 data 事件桥接到旧函数。
|
||||
if (typeof window[functionName] === "function") {
|
||||
window[functionName]();
|
||||
export function openShopModal() {
|
||||
const modal = document.getElementById("shop-modal");
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
modal.style.display = "flex";
|
||||
if (!shopLoaded) {
|
||||
shopLoaded = true;
|
||||
fetchShopData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定商店关闭、改名和赠礼对话框按钮事件。
|
||||
* 关闭商店弹窗。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function closeShopModal() {
|
||||
const modal = document.getElementById("shop-modal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取商品数据并渲染列表。
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function fetchShopData() {
|
||||
try {
|
||||
const response = await fetch(getShopUrls().items, {
|
||||
headers: jsonHeaders(),
|
||||
});
|
||||
const data = await response.json();
|
||||
renderShop(data);
|
||||
} catch (error) {
|
||||
showShopToast("⚠ 加载失败,请重试", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧入口,给未迁移脚本按原函数名刷新商店。
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function loadShop() {
|
||||
return fetchShopData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建分组标题旁边的状态徽章。
|
||||
*
|
||||
* @param {Record<string, any>} group 商品分组
|
||||
* @param {Record<string, any>} data 商店接口数据
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildGroupSuffix(group, data) {
|
||||
if (group.type === "auto_fishing" && Number(data.auto_fishing_minutes_left || 0) > 0) {
|
||||
const left = Number(data.auto_fishing_minutes_left || 0);
|
||||
const leftStr = left >= 60 ? `${Math.floor(left / 60)} 小时` : `${left} 分钟`;
|
||||
|
||||
return ` <span style="display:inline-block;margin-left:8px;padding:1px 8px;background:#7c3aed;color:#fff;border-radius:10px;font-size:10px;font-weight:bold;vertical-align:middle;">⏳ 剩余 ${escapeHtml(leftStr)}</span>`;
|
||||
}
|
||||
|
||||
if (group.type === "duration" && data.active_week_effect) {
|
||||
const effectKey = String(data.active_week_effect);
|
||||
const effectItem = (data.items || []).find((item) => item.type === "duration" && String(item.slug || "").includes(effectKey));
|
||||
const effectName = effectItem ? effectItem.name : effectKey;
|
||||
|
||||
return ` <span style="display:inline-block;margin-left:8px;padding:1px 8px;background:#16a34a;color:#fff;border-radius:10px;font-size:10px;font-weight:bold;vertical-align:middle;">✅ 已激活:${escapeHtml(effectName)}</span>`;
|
||||
}
|
||||
|
||||
if (group.type === "tools" && Number(data.sign_repair_card_count || 0) > 0) {
|
||||
return ` <span style="display:inline-block;margin-left:8px;padding:1px 8px;background:#0f766e;color:#fff;border-radius:10px;font-size:10px;font-weight:bold;vertical-align:middle;">🎫 可用 ${Number(data.sign_repair_card_count || 0)} 张</span>`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 按商品类型筛选当前分组要展示的商品。
|
||||
*
|
||||
* @param {Record<string, any>} group 商品分组
|
||||
* @param {Array<Record<string, any>>} items 商品列表
|
||||
* @returns {Array<Record<string, any>>}
|
||||
*/
|
||||
function filterGroupItems(group, items) {
|
||||
if (group.type === "tools") {
|
||||
return items.filter((item) => ["one_time", "sign_repair"].includes(item.type));
|
||||
}
|
||||
|
||||
return items.filter((item) => item.type === group.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染商店商品列表和余额状态。
|
||||
*
|
||||
* @param {Record<string, any>} data 商店接口数据
|
||||
* @returns {void}
|
||||
*/
|
||||
export function renderShop(data) {
|
||||
const balance = document.getElementById("shop-jjb");
|
||||
const badge = document.getElementById("shop-week-badge");
|
||||
const list = document.getElementById("shop-items-list");
|
||||
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (balance) {
|
||||
balance.textContent = Number(data.user_jjb || 0).toLocaleString();
|
||||
}
|
||||
|
||||
if (badge) {
|
||||
if (data.active_week_effect) {
|
||||
badge.textContent = `${WEEK_EFFECT_ICONS[data.active_week_effect] ?? ""} 周卡生效中`;
|
||||
badge.style.display = "inline-block";
|
||||
} else {
|
||||
badge.textContent = "";
|
||||
badge.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
const items = Array.isArray(data.items) ? data.items : [];
|
||||
const ringCounts = data.ring_counts || {};
|
||||
list.innerHTML = "";
|
||||
|
||||
SHOP_GROUPS.forEach((group) => {
|
||||
const groupItems = filterGroupItems(group, items);
|
||||
if (!groupItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "shop-group-header";
|
||||
header.innerHTML = `${escapeHtml(group.label)}${buildGroupSuffix(group, { ...data, items })}${group.desc ? ` <span>${escapeHtml(group.desc)}</span>` : ""}`;
|
||||
list.appendChild(header);
|
||||
|
||||
groupItems.forEach((item) => {
|
||||
list.appendChild(createShopCard(item, data, ringCounts));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个商品卡片,并挂载购买或使用按钮事件。
|
||||
*
|
||||
* @param {Record<string, any>} item 商品数据
|
||||
* @param {Record<string, any>} data 商店接口数据
|
||||
* @param {Record<string, number>} ringCounts 戒指持有数量
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
function createShopCard(item, data, ringCounts) {
|
||||
const isRename = item.slug === "rename_card";
|
||||
const canUseRename = isRename && data.has_rename_card;
|
||||
const isRing = item.type === "ring";
|
||||
const ownedQty = isRing ? Number(ringCounts[item.id] || 0) : 0;
|
||||
const isAutoFishing = item.type === "auto_fishing";
|
||||
const card = document.createElement("div");
|
||||
const button = document.createElement("button");
|
||||
|
||||
card.className = "shop-card";
|
||||
card.innerHTML = buildShopCardHtml(item, {
|
||||
isRing,
|
||||
ownedQty,
|
||||
isAutoFishing,
|
||||
});
|
||||
|
||||
if (canUseRename) {
|
||||
button.className = "shop-btn shop-btn-use";
|
||||
button.textContent = "✦ 使用改名卡";
|
||||
button.addEventListener("click", openRenameModal);
|
||||
} else if (item.type === "instant") {
|
||||
button.className = "shop-btn";
|
||||
button.textContent = `💰 ${Number(item.price || 0).toLocaleString()}`;
|
||||
button.addEventListener("click", () => openGiftDialog(item));
|
||||
} else {
|
||||
button.className = "shop-btn";
|
||||
button.textContent = `💰 ${Number(item.price || 0).toLocaleString()}`;
|
||||
button.addEventListener("click", () => confirmAndBuyItem(item));
|
||||
}
|
||||
|
||||
card.appendChild(button);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接商品卡片 HTML,所有接口字段都先转义再进入 innerHTML。
|
||||
*
|
||||
* @param {Record<string, any>} item 商品数据
|
||||
* @param {{isRing: boolean, ownedQty: number, isAutoFishing: boolean}} options 渲染选项
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildShopCardHtml(item, options) {
|
||||
const iconHtml = options.isRing && options.ownedQty > 0
|
||||
? `<span style="position:relative;display:inline-block;">
|
||||
<span class="shop-card-icon">${escapeHtml(item.icon)}</span>
|
||||
<span style="position:absolute;top:-4px;right:-6px;background:#f43f5e;color:#fff;font-size:9px;font-weight:800;min-width:15px;height:15px;border-radius:8px;text-align:center;line-height:15px;padding:0 2px;">${options.ownedQty}</span>
|
||||
</span>`
|
||||
: `<span class="shop-card-icon">${escapeHtml(item.icon)}</span>`;
|
||||
const durationLabel = options.isAutoFishing && Number(item.duration_minutes || 0) > 0
|
||||
? `<div style="font-size:9px;margin-top:3px;color:#7c3aed;">⏱ 有效期 ${escapeHtml(formatMinutes(item.duration_minutes))}</div>`
|
||||
: "";
|
||||
const ringBonus = options.isRing && (Number(item.intimacy_bonus || 0) > 0 || Number(item.charm_bonus || 0) > 0)
|
||||
? `<div style="font-size:9px;margin-top:3px;display:flex;gap:8px;">
|
||||
${Number(item.intimacy_bonus || 0) > 0 ? `<span style="color:#f43f5e;">💞 亲密 +${Number(item.intimacy_bonus || 0)}</span>` : ""}
|
||||
${Number(item.charm_bonus || 0) > 0 ? `<span style="color:#a855f7;">✨ 魅力 +${Number(item.charm_bonus || 0)}</span>` : ""}
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="shop-card-top">
|
||||
${iconHtml}
|
||||
<span class="shop-card-name">${escapeHtml(item.name)}</span>
|
||||
</div>
|
||||
<div class="shop-card-desc">${escapeHtml(item.description ?? "")}</div>
|
||||
${ringBonus}
|
||||
${durationLabel}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化分钟数,供自动钓鱼卡有效期展示。
|
||||
*
|
||||
* @param {number|string} minutes 有效分钟数
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatMinutes(minutes) {
|
||||
const value = Number(minutes || 0);
|
||||
|
||||
return value >= 60 ? `${Math.floor(value / 60)} 小时` : `${value} 分钟`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买前弹出确认框,补签卡需要先询问数量。
|
||||
*
|
||||
* @param {Record<string, any>} item 商品数据
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function confirmAndBuyItem(item) {
|
||||
let quantity = 1;
|
||||
|
||||
if (item.type === "sign_repair") {
|
||||
quantity = await window.promptSignRepairQuantity?.(item);
|
||||
if (quantity === null || quantity === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmMessage = `确认花费 💰 ${Number(Number(item.price || 0) * quantity).toLocaleString()} 金币购买\n【${item.name}】${quantity > 1 ? ` × ${quantity}` : ""} 吗?`;
|
||||
const confirmed = await confirmShopPurchase(confirmMessage);
|
||||
|
||||
if (confirmed) {
|
||||
buyItem(item.id, item.name, item.price, "all", "", quantity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容全局弹窗组件缺失时的原生确认。
|
||||
*
|
||||
* @param {string} message 确认文案
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function confirmShopPurchase(message) {
|
||||
if (window.chatDialog?.confirm) {
|
||||
return Boolean(await window.chatDialog.confirm(message, "确认购买"));
|
||||
}
|
||||
|
||||
return window.confirm(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开赠礼对话框,缓存当前要赠出的单次特效卡。
|
||||
*
|
||||
* @param {Record<string, any>} item 商品数据
|
||||
* @returns {void}
|
||||
*/
|
||||
export function openGiftDialog(item) {
|
||||
giftItem = item;
|
||||
|
||||
const recipient = document.getElementById("gift-recipient");
|
||||
const message = document.getElementById("gift-message");
|
||||
const error = document.getElementById("gift-err");
|
||||
const itemName = document.getElementById("gift-item-name");
|
||||
const dialog = document.getElementById("gift-dialog");
|
||||
|
||||
if (recipient) {
|
||||
recipient.value = "";
|
||||
}
|
||||
|
||||
if (message) {
|
||||
message.value = "";
|
||||
}
|
||||
|
||||
if (error) {
|
||||
error.textContent = "";
|
||||
}
|
||||
|
||||
if (itemName) {
|
||||
itemName.textContent = `${item.icon} ${item.name}(💰 ${Number(item.price || 0).toLocaleString()})`;
|
||||
}
|
||||
|
||||
if (dialog) {
|
||||
dialog.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭赠礼对话框并清理当前商品缓存。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function closeGiftDialog() {
|
||||
const dialog = document.getElementById("gift-dialog");
|
||||
if (dialog) {
|
||||
dialog.style.display = "none";
|
||||
}
|
||||
|
||||
giftItem = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认赠礼,按旧签名调用购买接口。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function confirmGift() {
|
||||
if (!giftItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipient = document.getElementById("gift-recipient")?.value.trim() ?? "";
|
||||
const message = document.getElementById("gift-message")?.value.trim() ?? "";
|
||||
const item = giftItem;
|
||||
const error = document.getElementById("gift-err");
|
||||
|
||||
if (error) {
|
||||
error.textContent = "";
|
||||
}
|
||||
|
||||
closeGiftDialog();
|
||||
buyItem(item.id, item.name, item.price, recipient, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买商品,保留旧全局签名供补签卡等存量脚本复用。
|
||||
*
|
||||
* @param {number|string} itemId 商品 ID
|
||||
* @param {string} name 商品名称
|
||||
* @param {number|string} price 商品价格
|
||||
* @param {string} recipient 接收人,空值表示全场
|
||||
* @param {string} message 赠言
|
||||
* @param {number} quantity 购买数量
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function buyItem(itemId, name, price, recipient, message, quantity = 1) {
|
||||
try {
|
||||
const response = await fetch(getShopUrls().buy, {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(true),
|
||||
body: JSON.stringify({
|
||||
item_id: itemId,
|
||||
recipient: recipient || "all",
|
||||
message: message || "",
|
||||
quantity: quantity || 1,
|
||||
room_id: window.chatContext?.roomId ?? 0,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
handleBuySuccess(data, name);
|
||||
return;
|
||||
}
|
||||
|
||||
showShopToast(data.message, false);
|
||||
} catch (error) {
|
||||
showShopToast("⚠ 网络异常,请重试", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买成功后同步金币、播放本地特效并刷新商店状态。
|
||||
*
|
||||
* @param {Record<string, any>} data 购买接口返回
|
||||
* @param {string} itemName 商品名称
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleBuySuccess(data, itemName) {
|
||||
const balance = document.getElementById("shop-jjb");
|
||||
|
||||
if (data.jjb !== undefined && balance) {
|
||||
balance.textContent = Number(data.jjb).toLocaleString();
|
||||
}
|
||||
|
||||
showShopToast(`✅ ${itemName} 购买成功!`, true);
|
||||
|
||||
// 购买者本地也要立即看到特效,广播只负责其他在线用户。
|
||||
if (data.play_effect && window.EffectManager) {
|
||||
window.EffectManager.play(data.play_effect);
|
||||
}
|
||||
|
||||
shopLoaded = false;
|
||||
setTimeout(() => {
|
||||
fetchShopData();
|
||||
shopLoaded = true;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示商店内部 Toast。
|
||||
*
|
||||
* @param {string} message 提示内容
|
||||
* @param {boolean} ok 是否成功
|
||||
* @returns {void}
|
||||
*/
|
||||
export function showShopToast(message, ok) {
|
||||
const element = document.getElementById("shop-toast");
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.textContent = message;
|
||||
element.style.display = "block";
|
||||
element.style.background = ok ? "#064e3b" : "#7f1d1d";
|
||||
element.style.color = ok ? "#6ee7b7" : "#fca5a5";
|
||||
clearTimeout(element._shopToastTimer);
|
||||
element._shopToastTimer = setTimeout(() => {
|
||||
element.style.display = "none";
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开改名卡输入框。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function openRenameModal() {
|
||||
const modal = document.getElementById("shop-rename-overlay");
|
||||
const input = document.getElementById("rename-input");
|
||||
const error = document.getElementById("rename-err");
|
||||
|
||||
if (modal) {
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
if (error) {
|
||||
error.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭改名卡输入框。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function closeRenameModal() {
|
||||
const modal = document.getElementById("shop-rename-overlay");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交改名卡请求,成功后刷新页面同步昵称。
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function submitRename() {
|
||||
const input = document.getElementById("rename-input");
|
||||
const error = document.getElementById("rename-err");
|
||||
const newName = input?.value.trim() ?? "";
|
||||
|
||||
if (!newName) {
|
||||
if (error) {
|
||||
error.textContent = "请输入新昵称";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(getShopUrls().rename, {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(true),
|
||||
body: JSON.stringify({
|
||||
new_name: newName,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
closeRenameModal();
|
||||
showShopToast(data.message, true);
|
||||
shopLoaded = false;
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
error.textContent = data.message;
|
||||
}
|
||||
} catch (requestError) {
|
||||
if (error) {
|
||||
error.textContent = "网络异常,请重试";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把模块函数挂到 window,兼容仍在 Blade 内的调用点。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function exposeShopGlobals() {
|
||||
window.openShopModal = openShopModal;
|
||||
window.closeShopModal = closeShopModal;
|
||||
window.loadShop = loadShop;
|
||||
window.fetchShopData = fetchShopData;
|
||||
window.renderShop = renderShop;
|
||||
window.openGiftDialog = openGiftDialog;
|
||||
window.closeGiftDialog = closeGiftDialog;
|
||||
window.confirmGift = confirmGift;
|
||||
window.buyItem = buyItem;
|
||||
window.showShopToast = showShopToast;
|
||||
window.openRenameModal = openRenameModal;
|
||||
window.closeRenameModal = closeRenameModal;
|
||||
window.submitRename = submitRename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定商店按钮、对话框按钮与遮罩关闭事件。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function bindShopControls() {
|
||||
if (shopControlEventsBound || typeof document === "undefined") {
|
||||
if (typeof window === "undefined" || typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
exposeShopGlobals();
|
||||
|
||||
if (shopControlEventsBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,36 +657,36 @@ export function bindShopControls() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 商店弹窗关闭入口与遮罩关闭逻辑保持同一个旧函数。
|
||||
if (event.target.closest("[data-shop-modal-close]")) {
|
||||
const modal = document.getElementById("shop-modal");
|
||||
|
||||
// 点击商店遮罩或关闭按钮都走同一个关闭入口。
|
||||
if (event.target.closest("[data-shop-modal-close]") || (modal && event.target === modal)) {
|
||||
event.preventDefault();
|
||||
callShopGlobal("closeShopModal");
|
||||
closeShopModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// 改名确认/取消只转发按钮意图,接口请求仍由 Blade 内的 submitRename 处理。
|
||||
if (event.target.closest("[data-shop-rename-confirm]")) {
|
||||
event.preventDefault();
|
||||
callShopGlobal("submitRename");
|
||||
submitRename();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-shop-rename-cancel]")) {
|
||||
event.preventDefault();
|
||||
callShopGlobal("closeRenameModal");
|
||||
closeRenameModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// 赠礼确认/取消对应单次特效卡流程,先保留旧的弹窗状态管理函数。
|
||||
if (event.target.closest("[data-shop-gift-confirm]")) {
|
||||
event.preventDefault();
|
||||
callShopGlobal("confirmGift");
|
||||
confirmGift();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-shop-gift-cancel]")) {
|
||||
event.preventDefault();
|
||||
callShopGlobal("closeGiftDialog");
|
||||
closeGiftDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user