diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 0469eaf..55c9a83 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -20,7 +20,17 @@ export { openAdminBaccaratLossCoverModal, submitBaccaratLossCoverEvent, } from "./chat-room/baccarat-loss-cover-admin.js"; -export { bindBankControls } from "./chat-room/bank-controls.js"; +export { + bankAction, + bankLoadInfo, + bankShowMsg, + bindBankControls, + closeBankModal, + fetchBankRanking, + openBankModal, + switchBankTab, + toggleBankRankSort, +} from "./chat-room/bank-modal.js"; export { bindFishingControls } from "./chat-room/fishing.js"; export { bindProfileControls } from "./chat-room/profile-controls.js"; export { bindShopControls } from "./chat-room/shop-controls.js"; @@ -69,7 +79,17 @@ import { openAdminBaccaratLossCoverModal, submitBaccaratLossCoverEvent, } from "./chat-room/baccarat-loss-cover-admin.js"; -import { bindBankControls } from "./chat-room/bank-controls.js"; +import { + bankAction, + bankLoadInfo, + bankShowMsg, + bindBankControls, + closeBankModal, + fetchBankRanking, + openBankModal, + switchBankTab, + toggleBankRankSort, +} from "./chat-room/bank-modal.js"; import { bindFishingControls } from "./chat-room/fishing.js"; import { bindProfileControls } from "./chat-room/profile-controls.js"; import { bindShopControls } from "./chat-room/shop-controls.js"; @@ -100,7 +120,7 @@ import { import { createMessageQueue } from "./chat-room/message-queue.js"; if (typeof window !== "undefined") { - // 保留聚合入口,给仍在 Blade 内的存量脚本按需读取已迁移工具。 + // 保留聚合入口,给新迁移模块、测试和仍在 Blade 内的存量脚本统一读取工具。 window.ChatRoomTools = { escapeHtml, escapeHtmlWithLineBreaks, @@ -123,7 +143,15 @@ if (typeof window !== "undefined") { loadAdminCurrentLossCoverEvent, openAdminBaccaratLossCoverModal, submitBaccaratLossCoverEvent, + bankAction, + bankLoadInfo, + bankShowMsg, bindBankControls, + closeBankModal, + fetchBankRanking, + openBankModal, + switchBankTab, + toggleBankRankSort, bindFishingControls, bindMarriageStatusControls, bindProfileControls, @@ -156,7 +184,7 @@ if (typeof window !== "undefined") { createMessageQueue, }; - // 兼容存量 Blade 调用点,避免一次性重构所有 window.* 入口。 + // 直接挂载只服务暂未迁移的 Blade 调用点;新代码优先通过模块导入或 ChatRoomTools 复用。 window.closeChatImageLightbox = closeChatImageLightbox; window.openChatImageLightbox = openChatImageLightbox; window.closeFriendPanel = closeFriendPanel; @@ -166,6 +194,14 @@ if (typeof window !== "undefined") { window.closeCurrentBaccaratLossCoverEvent = closeCurrentBaccaratLossCoverEvent; window.openAdminBaccaratLossCoverModal = openAdminBaccaratLossCoverModal; window.submitBaccaratLossCoverEvent = submitBaccaratLossCoverEvent; + window.bankAction = bankAction; + window.bankLoadInfo = bankLoadInfo; + window.bankShowMsg = bankShowMsg; + window.closeBankModal = closeBankModal; + window.fetchBankRanking = fetchBankRanking; + window.openBankModal = openBankModal; + window.switchBankTab = switchBankTab; + window.toggleBankRankSort = toggleBankRankSort; window.applyFontSize = applyFontSize; // 页面加载后立即注册事件委托,具体业务逻辑仍由各子模块负责。 diff --git a/resources/js/chat-room/bank-controls.js b/resources/js/chat-room/bank-controls.js deleted file mode 100644 index a467206..0000000 --- a/resources/js/chat-room/bank-controls.js +++ /dev/null @@ -1,84 +0,0 @@ -// 金币银行基础按钮事件绑定,替代 toolbar 银行区域内联 onclick。 - -let bankControlEventsBound = false; - -/** - * 调用银行存量全局函数。 - * - * @param {string} functionName 全局函数名 - * @param {...unknown} args 参数 - * @returns {void} - */ -function callBankGlobal(functionName, ...args) { - if (typeof window[functionName] === "function") { - window[functionName](...args); - } -} - -/** - * 从分页文本中解析当前页码。 - * - * @returns {number} - */ -function resolveCurrentRankPage() { - const pageInfo = document.getElementById("bank-rank-page-info")?.textContent || "1 / 1"; - const currentPage = Number.parseInt(pageInfo.split("/")[0]?.trim() || "1", 10); - - return Number.isInteger(currentPage) && currentPage > 0 ? currentPage : 1; -} - -/** - * 绑定银行 tab、存取款、排行榜排序与分页按钮事件。 - * - * @returns {void} - */ -export function bindBankControls() { - if (bankControlEventsBound || typeof document === "undefined") { - return; - } - - bankControlEventsBound = true; - document.addEventListener("click", (event) => { - if (!(event.target instanceof Element)) { - return; - } - - const tabButton = event.target.closest("[data-bank-tab]"); - if (tabButton) { - event.preventDefault(); - callBankGlobal("switchBankTab", tabButton.getAttribute("data-bank-tab") || ""); - return; - } - - if (event.target.closest("[data-bank-modal-close]")) { - event.preventDefault(); - callBankGlobal("closeBankModal"); - return; - } - - const actionButton = event.target.closest("[data-bank-action]"); - if (actionButton) { - event.preventDefault(); - // 存取款共用原 bankAction,由 type 决定读取哪个输入框和提交哪个接口。 - callBankGlobal("bankAction", actionButton.getAttribute("data-bank-action") || ""); - return; - } - - if (event.target.closest("[data-bank-rank-sort]")) { - event.preventDefault(); - callBankGlobal("toggleBankRankSort"); - return; - } - - const pageButton = event.target.closest("[data-bank-rank-page-delta]"); - if (!pageButton) { - return; - } - - event.preventDefault(); - - // 分页状态仍由存量脚本维护,这里从显示文本推导目标页,避免依赖其闭包变量。 - const delta = Number.parseInt(pageButton.getAttribute("data-bank-rank-page-delta") || "0", 10); - callBankGlobal("fetchBankRanking", resolveCurrentRankPage() + delta); - }); -} diff --git a/resources/js/chat-room/bank-modal.js b/resources/js/chat-room/bank-modal.js new file mode 100644 index 0000000..2792eb6 --- /dev/null +++ b/resources/js/chat-room/bank-modal.js @@ -0,0 +1,423 @@ +// 金币银行弹窗业务模块,替代 toolbar 中的银行内联脚本。 + +import { escapeHtml } from "./html.js"; + +let bankRankPage = 1; +let bankRankSort = "desc"; +let bankModalEventsBound = false; + +/** + * 获取 CSRF Token。 + * + * @returns {string} + */ +function csrf() { + return document.querySelector('meta[name="csrf-token"]')?.content ?? ""; +} + +/** + * 设置元素显示状态。 + * + * @param {string} id 元素 ID + * @param {string} display CSS display 值 + * @returns {void} + */ +function setDisplay(id, display) { + const element = document.getElementById(id); + if (element) { + element.style.display = display; + } +} + +/** + * 切换银行弹窗 Tab,并在首次进入排行榜时加载数据。 + * + * @param {string} tabName Tab 名称 + * @returns {void} + */ +export function switchBankTab(tabName) { + document.getElementById("bank-tabbtn-account")?.classList.remove("active"); + document.getElementById("bank-tabbtn-ranking")?.classList.remove("active"); + setDisplay("bank-view-account", "none"); + setDisplay("bank-view-ranking", "none"); + + document.getElementById(`bank-tabbtn-${tabName}`)?.classList.add("active"); + setDisplay(`bank-view-${tabName}`, "flex"); + + const rankingList = document.getElementById("bank-ranking-list"); + if (tabName === "ranking" && bankRankPage === 1 && rankingList?.innerHTML.includes("加载中")) { + void fetchBankRanking(1); + } +} + +/** + * 打开银行弹窗并刷新账户资金信息。 + * + * @returns {void} + */ +export function openBankModal() { + setDisplay("bank-modal", "flex"); + switchBankTab("account"); + void bankLoadInfo(); +} + +/** + * 关闭银行弹窗。 + * + * @returns {void} + */ +export function closeBankModal() { + setDisplay("bank-modal", "none"); +} + +/** + * 执行存款或取款操作。 + * + * @param {"deposit"|"withdraw"|string} type 操作类型 + * @returns {Promise} + */ +export async function bankAction(type) { + if (!["deposit", "withdraw"].includes(type)) { + return; + } + + const inputEl = document.getElementById(`bank-${type}-input`); + const btnEl = document.getElementById(`bank-${type}-btn`); + if (!(inputEl instanceof HTMLInputElement) || !(btnEl instanceof HTMLButtonElement)) { + return; + } + + const amount = Number.parseInt(inputEl.value, 10); + if (!amount || amount <= 0) { + bankShowMsg("请输入有效的金额", false); + return; + } + + btnEl.disabled = true; + try { + const response = await fetch(`/bank/${type}`, { + method: "POST", + headers: { + "X-CSRF-TOKEN": csrf(), + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ amount }), + }); + const data = await response.json(); + bankShowMsg(data.message, data.status === "success"); + + if (data.status === "success") { + inputEl.value = ""; + await bankLoadInfo(); + + if (document.getElementById("bank-view-ranking")?.style.display !== "none") { + await fetchBankRanking(bankRankPage); + } + } + } catch (error) { + bankShowMsg("网络异常,请稍后重试", false); + } + + btnEl.disabled = false; +} + +/** + * 获取当前用户资金与最近流水,并同步 chatContext 金币缓存。 + * + * @returns {Promise} + */ +export async function bankLoadInfo() { + try { + const response = await fetch("/bank", { + headers: { + Accept: "application/json", + }, + }); + const data = await response.json(); + if (data.status !== "success") { + return; + } + + const cashDisplay = document.getElementById("bank-jjb-display"); + const bankDisplay = document.getElementById("bank-bank-display"); + if (cashDisplay) { + cashDisplay.textContent = Number(data.jjb || 0).toLocaleString(); + } + if (bankDisplay) { + bankDisplay.textContent = Number(data.bank_jjb || 0).toLocaleString(); + } + + if (window.chatContext) { + window.chatContext.myGold = data.jjb; + window.chatContext.bankGold = data.bank_jjb; + } + + renderBankLogs(Array.isArray(data.logs) ? data.logs : []); + } catch (error) { + const list = document.getElementById("bank-logs-list"); + if (list) { + list.innerHTML = '
加载失败
'; + } + } +} + +/** + * 渲染最近资金流水。 + * + * @param {Array>} logs 流水记录 + * @returns {void} + */ +function renderBankLogs(logs) { + const list = document.getElementById("bank-logs-list"); + if (!list) { + return; + } + + if (logs.length === 0) { + list.innerHTML = '
暂无记录
'; + return; + } + + list.innerHTML = logs.map((log) => { + const isDeposit = log.type === "deposit"; + const className = isDeposit ? "bank-log-deposit" : "bank-log-withdraw"; + const sign = isDeposit ? "+ 存入" : "- 取出"; + const createdAt = String(log.created_at || "").replace("T", " ").substring(0, 16); + + return `
+ ${sign} ${Number(log.amount || 0).toLocaleString()} 金币 + 余额: ${Number(log.balance_after || 0).toLocaleString()} + ${escapeHtml(createdAt)} +
`; + }).join(""); +} + +/** + * 显示银行操作结果提示。 + * + * @param {string} message 提示内容 + * @param {boolean} success 是否成功 + * @returns {void} + */ +export function bankShowMsg(message, success) { + const element = document.getElementById("bank-op-msg"); + if (!element) { + return; + } + + element.textContent = message; + element.style.background = success ? "#f0fdf4" : "#fef2f2"; + element.style.border = success ? "1px solid #bbf7d0" : "1px solid #fecaca"; + element.style.color = success ? "#16a34a" : "#ef4444"; + element.style.display = "block"; + clearTimeout(element._t); + element._t = setTimeout(() => { + element.style.display = "none"; + }, 3000); +} + +/** + * 切换排行榜排序并回到第一页。 + * + * @returns {void} + */ +export function toggleBankRankSort() { + bankRankSort = bankRankSort === "desc" ? "asc" : "desc"; + + const button = document.getElementById("bank-rank-sort-btn"); + if (button) { + button.textContent = bankRankSort === "desc" ? "降序 ↓" : "升序 ↑"; + } + + void fetchBankRanking(1); +} + +/** + * 拉取并渲染存款排行榜。 + * + * @param {number} page 页码 + * @returns {Promise} + */ +export async function fetchBankRanking(page) { + if (page < 1) { + return; + } + + const listEl = document.getElementById("bank-ranking-list"); + if (!listEl) { + return; + } + + listEl.innerHTML = '
加载中...
'; + + try { + const response = await fetch(`/bank/ranking?page=${page}&sort=${bankRankSort}`, { + headers: { + Accept: "application/json", + }, + }); + const data = await response.json(); + + if (data.status !== "success") { + return; + } + + bankRankPage = data.pagination.current_page; + const pageInfo = document.getElementById("bank-rank-page-info"); + if (pageInfo) { + pageInfo.textContent = `${bankRankPage} / ${data.pagination.last_page}`; + } + syncRankingButtons(data.pagination.last_page); + + if (!Array.isArray(data.ranking) || data.ranking.length === 0) { + listEl.innerHTML = '
暂无数据
'; + return; + } + + listEl.innerHTML = data.ranking.map((user, index) => renderRankingRow(user, index)).join(""); + } catch (error) { + listEl.innerHTML = '
拉取失败
'; + } +} + +/** + * 同步排行榜分页按钮状态。 + * + * @param {number} lastPage 最后一页页码 + * @returns {void} + */ +function syncRankingButtons(lastPage) { + const prevButton = document.getElementById("bank-rank-prev"); + const nextButton = document.getElementById("bank-rank-next"); + if (!(prevButton instanceof HTMLButtonElement) || !(nextButton instanceof HTMLButtonElement)) { + return; + } + + prevButton.disabled = bankRankPage <= 1; + prevButton.style.opacity = bankRankPage <= 1 ? "0.5" : "1"; + nextButton.disabled = bankRankPage >= lastPage; + nextButton.style.opacity = bankRankPage >= lastPage ? "0.5" : "1"; +} + +/** + * 渲染排行榜单行;前三名样式只在降序第一页生效。 + * + * @param {Record} user 用户排行数据 + * @param {number} index 当前页索引 + * @returns {string} + */ +function renderRankingRow(user, index) { + const absoluteRank = (bankRankPage - 1) * 20 + index + 1; + let rankClass = ""; + if (bankRankSort === "desc" && bankRankPage === 1) { + if (absoluteRank === 1) { + rankClass = "bank-rank-top1"; + } else if (absoluteRank === 2) { + rankClass = "bank-rank-top2"; + } else if (absoluteRank === 3) { + rankClass = "bank-rank-top3"; + } + } + + const avatarUrl = escapeHtml(resolveSafeBankAvatarUrl(user.headfaceUrl)); + const username = escapeHtml(user.username || ""); + const sexSymbol = user.sex === "女" ? "♀" : "♂"; + + return `
+
${absoluteRank}
+ +
+
${username} ${sexSymbol}
+
+
+ 🏦 ${Number(user.bank_jjb || 0).toLocaleString()} +
+
`; +} + +/** + * 规整排行榜头像地址,避免任意 URL 直接进入 img src。 + * + * @param {unknown} value 头像地址 + * @returns {string} + */ +function resolveSafeBankAvatarUrl(value) { + const url = String(value || ""); + + if (url.startsWith("/images/headface/") || url.startsWith("/storage/")) { + return url; + } + + return "/images/headface/1.gif"; +} + +/** + * 从分页文本中解析当前页码。 + * + * @returns {number} + */ +function resolveCurrentRankPage() { + const pageInfo = document.getElementById("bank-rank-page-info")?.textContent || "1 / 1"; + const currentPage = Number.parseInt(pageInfo.split("/")[0]?.trim() || "1", 10); + + return Number.isInteger(currentPage) && currentPage > 0 ? currentPage : 1; +} + +/** + * 绑定银行弹窗按钮和遮罩关闭事件。 + * + * @returns {void} + */ +export function bindBankControls() { + if (bankModalEventsBound || typeof document === "undefined") { + return; + } + + bankModalEventsBound = true; + document.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) { + return; + } + + const tabButton = event.target.closest("[data-bank-tab]"); + if (tabButton) { + event.preventDefault(); + switchBankTab(tabButton.getAttribute("data-bank-tab") || ""); + return; + } + + if (event.target.closest("[data-bank-modal-close]")) { + event.preventDefault(); + closeBankModal(); + return; + } + + const actionButton = event.target.closest("[data-bank-action]"); + if (actionButton) { + event.preventDefault(); + void bankAction(actionButton.getAttribute("data-bank-action") || ""); + return; + } + + if (event.target.closest("[data-bank-rank-sort]")) { + event.preventDefault(); + toggleBankRankSort(); + return; + } + + const pageButton = event.target.closest("[data-bank-rank-page-delta]"); + if (pageButton) { + event.preventDefault(); + const delta = Number.parseInt(pageButton.getAttribute("data-bank-rank-page-delta") || "0", 10); + void fetchBankRanking(resolveCurrentRankPage() + delta); + return; + } + + const modal = event.target.closest("#bank-modal"); + // 点击遮罩关闭,只响应最外层背景,避免内容区误关。 + if (modal && event.target === modal) { + closeBankModal(); + } + }); +} diff --git a/resources/views/chat/partials/layout/toolbar.blade.php b/resources/views/chat/partials/layout/toolbar.blade.php index fbe1e59..351400f 100644 --- a/resources/views/chat/partials/layout/toolbar.blade.php +++ b/resources/views/chat/partials/layout/toolbar.blade.php @@ -2518,183 +2518,3 @@ async function generateWechatBindCode() { - -