迁移商店弹窗脚本
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user