迁移商店弹窗脚本

This commit is contained in:
2026-04-25 14:56:04 +08:00
parent 66a9e8ad23
commit b622053bc2
3 changed files with 706 additions and 364 deletions
+59 -3
View File
@@ -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();
+644 -18
View File
@@ -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();
}
});
}
@@ -583,7 +583,8 @@
}
</style>
<div id="shop-modal">
<div id="shop-modal" data-shop-items-url="{{ route('shop.items') }}" data-shop-buy-url="{{ route('shop.buy') }}"
data-shop-rename-url="{{ route('shop.rename') }}">
<div id="shop-modal-inner" style="position:relative;">
{{-- 标题栏 --}}
@@ -638,348 +639,7 @@
</div>
</div>
<script>
/**
* 商店弹窗逻辑
* 工具栏点击「商店」按钮触发,全屏模态展示
*/
(function() {
let shopLoaded = false;
/** 打开商店弹窗 */
window.openShopModal = function() {
document.getElementById('shop-modal').style.display = 'flex';
if (!shopLoaded) {
shopLoaded = true;
fetchShopData();
}
};
/** 关闭商店弹窗 */
window.closeShopModal = function() {
document.getElementById('shop-modal').style.display = 'none';
};
// 点击遮罩层关闭弹窗
document.getElementById('shop-modal').addEventListener('click', function(e) {
if (e.target === this) closeShopModal();
});
/** 拉取商品数据 */
function fetchShopData() {
fetch('{{ route('shop.items') }}', {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': _csrf()
}
})
.then(r => r.json())
.then(data => renderShop(data))
.catch(() => showShopToast('⚠ 加载失败,请重试', false));
}
/** 渲染商品列表(2列网格) */
function renderShop(data) {
// 更新余额
document.getElementById('shop-jjb').textContent = Number(data.user_jjb).toLocaleString();
// 周卡状态
const badge = document.getElementById('shop-week-badge');
if (data.active_week_effect) {
const icons = {
fireworks: '🎆',
rain: '🌧',
lightning: '⚡',
snow: '❄️',
sakura: '🌸',
meteors: '🌠',
'gold-rain': '🪙',
hearts: '💖',
confetti: '🎊',
fireflies: '✨'
};
badge.textContent = (icons[data.active_week_effect] ?? '') + ' 周卡生效中';
badge.style.display = 'inline-block';
}
const ringCounts = data.ring_counts || {};
const groups = [{
label: '⚡ 单次特效卡',
desc: '立即播放一次',
type: 'instant'
},
{
label: '📅 周卡 7天登录自动播放',
desc: '同时只能激活一种,购新旧失效不退款',
type: 'duration'
},
{
label: '💍 求婚戒指',
desc: '存入背包,求婚时消耗(被拒则遗失)',
type: 'ring'
},
{
label: '🎣 自动钓鱼卡',
desc: '激活后自动收篼,无需手动点击浮漂',
type: 'auto_fishing'
},
{
label: '🎭 道具',
desc: '',
type: 'tools'
},
];
const list = document.getElementById('shop-items-list');
list.innerHTML = '';
groups.forEach(g => {
const items = data.items.filter(i => g.type === 'tools' ? ['one_time', 'sign_repair'].includes(i.type) : i.type === g.type);
if (!items.length) return;
// 分组标题(独占一整行)
const header = document.createElement('div');
header.className = 'shop-group-header';
// 分组标题徽章:
// - auto_fishing:显示剩余时间(紫色)
// - duration:显示当前已激活的周卡名称(绿色)
let groupSuffix = '';
if (g.type === 'auto_fishing' && (data.auto_fishing_minutes_left || 0) > 0) {
const left = data.auto_fishing_minutes_left;
const leftStr = left >= 60 ? Math.floor(left / 60) + ' 小时' : left + ' 分钟';
groupSuffix =
` <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;">⏳ 剩余 ${leftStr}</span>`;
} else if (g.type === 'duration' && data.active_week_effect) {
// active_week_effect 是 effectKey 字符串,从 items 列表反查对应商品名称
const effKey = data.active_week_effect;
const effItem = data.items.find(i => i.type === 'duration' && i.slug.includes(effKey));
const effName = effItem ? effItem.name : effKey;
groupSuffix =
` <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;">✅ 已激活:${effName}</span>`;
} else if (g.type === 'tools' && (data.sign_repair_card_count || 0) > 0) {
groupSuffix =
` <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;">🎫 可用 ${data.sign_repair_card_count} 张</span>`;
}
header.innerHTML = `${g.label}${groupSuffix}${g.desc ? ` <span>${g.desc}</span>` : ''}`;
list.appendChild(header);
items.forEach(item => {
const isRename = item.slug === 'rename_card';
const canUse = isRename && data.has_rename_card;
const isRing = item.type === 'ring';
const ownedQty = isRing ? (ringCounts[item.id] || 0) : 0;
const card = document.createElement('div');
card.className = 'shop-card';
// 顶部:图标 + 名称(戒指加持有数徽章,其余正常显示)
const isAutoFishing = item.type === 'auto_fishing';
let iconHtml;
if (isRing && ownedQty > 0) {
iconHtml = `<span style="position:relative;display:inline-block;">
<span class="shop-card-icon">${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;">${ownedQty}</span>
</span>`;
} else {
iconHtml = `<span class="shop-card-icon">${item.icon}</span>`;
}
const durationLabel = isAutoFishing && item.duration_minutes > 0 ?
`<div style="font-size:9px;margin-top:3px;color:#7c3aed;">⏱ 有效期 ${item.duration_minutes >= 60 ? Math.floor(item.duration_minutes / 60) + ' 小时' : item.duration_minutes + ' 分钟'}</div>` :
'';
card.innerHTML = `
<div class="shop-card-top">
${iconHtml}
<span class="shop-card-name">${item.name}</span>
</div>
<div class="shop-card-desc">${item.description ?? ''}</div>
${isRing && (item.intimacy_bonus > 0 || item.charm_bonus > 0) ? `
<div style="font-size:9px;margin-top:3px;display:flex;gap:8px;">
${item.intimacy_bonus > 0 ? `<span style="color:#f43f5e;">💞 亲密 +${item.intimacy_bonus}</span>` : ''}
${item.charm_bonus > 0 ? `<span style="color:#a855f7;">✨ 魅力 +${item.charm_bonus}</span>` : ''}
</div>` : ''}
${durationLabel}
`;
// 按钮
const btn = document.createElement('button');
if (canUse) {
btn.className = 'shop-btn shop-btn-use';
btn.textContent = '✦ 使用改名卡';
btn.onclick = openRenameModal;
} else if (item.type === 'instant') {
// 单次卡:打开送礼弹框
btn.className = 'shop-btn';
btn.innerHTML = `💰 ${Number(item.price).toLocaleString()}`;
btn.onclick = () => openGiftDialog(item);
} else {
// 周卡、道具、戒指、自动钓鱼卡:弹确认窗口再购买
btn.className = 'shop-btn';
btn.innerHTML = `💰 ${Number(item.price).toLocaleString()}`;
btn.onclick = async () => {
let quantity = 1;
if (item.type === 'sign_repair') {
quantity = await window.promptSignRepairQuantity?.(item);
if (quantity === null || quantity === undefined) return;
}
const confirmMsg =
`确认花费 💰 ${Number(item.price * quantity).toLocaleString()} 金币购买\n【${item.name}】${quantity > 1 ? ' × ' + quantity : ''} 吗?`;
const ok = await window.chatDialog.confirm(confirmMsg, '确认购买');
if (ok) buyItem(item.id, item.name, item.price, 'all', '', quantity);
};
}
card.appendChild(btn);
list.appendChild(card);
});
});
}
/**
* 打开送礼弹框(仅单次特效卡)
* 让用户填写:送给谁 + 说一句话
*/
let _giftItem = null;
window.openGiftDialog = function(item) {
_giftItem = item;
// 重置内容
document.getElementById('gift-recipient').value = '';
document.getElementById('gift-message').value = '';
document.getElementById('gift-err').textContent = '';
document.getElementById('gift-item-name').textContent =
`${item.icon} ${item.name}(💰 ${Number(item.price).toLocaleString()}`;
document.getElementById('gift-dialog').style.display = 'flex';
};
window.closeGiftDialog = function() {
document.getElementById('gift-dialog').style.display = 'none';
_giftItem = null;
};
/** 送礼确认,提交购买 */
window.confirmGift = function() {
if (!_giftItem) return;
const recipient = document.getElementById('gift-recipient').value.trim();
const message = document.getElementById('gift-message').value.trim();
document.getElementById('gift-err').textContent = '';
// 先将商品数据保存到局部变量,再关闭弹框(closeGiftDialog 会把 _giftItem 设为 null
const item = _giftItem;
closeGiftDialog();
buyItem(item.id, item.name, item.price, recipient, message);
};
/** 购买商品(最终执行) */
window.buyItem = function(itemId, name, price, recipient, message, quantity = 1) {
const roomId = window.chatContext?.roomId ?? 0;
fetch('{{ route('shop.buy') }}', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _csrf()
},
body: JSON.stringify({
item_id: itemId,
recipient: recipient || 'all',
message: message || '',
quantity: quantity || 1,
room_id: roomId,
}),
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
// 更新金币
if (data.jjb !== undefined)
document.getElementById('shop-jjb').textContent = Number(data.jjb)
.toLocaleString();
// 购买成功提示
showShopToast(`✅ ${name} 购买成功!`, true);
// 播放本地特效(购买者自己必须也能看到)
if (data.play_effect && window.EffectManager) {
window.EffectManager.play(data.play_effect);
}
// 延迟刷新商品数据(保持商店开着让用户看到状态更新)
shopLoaded = false;
setTimeout(() => {
fetchShopData();
shopLoaded = true;
}, 1000);
} else {
showShopToast(data.message, false);
}
})
.catch(() => showShopToast('⚠ 网络异常,请重试', false));
};
/** Toast 通知 */
window.showShopToast = function(msg, ok) {
const el = document.getElementById('shop-toast');
el.textContent = msg;
el.style.display = 'block';
el.style.background = ok ? '#064e3b' : '#7f1d1d';
el.style.color = ok ? '#6ee7b7' : '#fca5a5';
clearTimeout(el._t);
el._t = setTimeout(() => {
el.style.display = 'none';
}, 3500);
};
/** 打开改名框 */
window.openRenameModal = function() {
const m = document.getElementById('shop-rename-overlay');
m.style.display = 'flex';
document.getElementById('rename-input').focus();
document.getElementById('rename-err').textContent = '';
};
/** 关闭改名框 */
window.closeRenameModal = function() {
document.getElementById('shop-rename-overlay').style.display = 'none';
};
/** 提交改名 */
window.submitRename = function() {
const newName = document.getElementById('rename-input').value.trim();
if (!newName) {
document.getElementById('rename-err').textContent = '请输入新昵称';
return;
}
fetch('{{ route('shop.rename') }}', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _csrf()
},
body: JSON.stringify({
new_name: newName
}),
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
closeRenameModal();
showShopToast(data.message, true);
shopLoaded = false;
setTimeout(() => window.location.reload(), 2000);
} else {
document.getElementById('rename-err').textContent = data.message;
}
})
.catch(() => {
document.getElementById('rename-err').textContent = '网络异常,请重试';
});
};
function _csrf() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? '';
}
})();
</script>
{{-- 商店弹窗业务脚本已迁移到 resources/js/chat-room/shop-controls.js --}}
{{-- ═══════════ 会员中心弹窗 ═══════════ --}}
<style>