// 金币银行弹窗业务模块,替代 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"; // 连续存取款时清理上一次隐藏任务,避免旧 timer 把新提示提前隐藏。 clearTimeout(element._t); element._t = setTimeout(() => { element.style.display = "none"; }, 3000); } /** * 确认是否扣费查看别人银行存款。 * * @param {number} cost 查看费用 * @returns {Promise} */ async function confirmRevealBankBalance(cost) { const message = `查看 TA 的银行存款需扣除 ${Number(cost || 0).toLocaleString()} 金币,是否继续?`; if (typeof window.chatDialog?.confirm === "function") { return Boolean(await window.chatDialog.confirm(message, "信息查看付费", "#d97706", "扣费查看", "取消")); } return window.confirm(message); } /** * 付费查看排行榜里的指定用户存款,并只更新当前行展示。 * * @param {HTMLElement} trigger 触发查看的按钮 * @returns {Promise} */ async function revealRankingBankBalance(trigger) { const userId = Number.parseInt(trigger.getAttribute("data-bank-reveal-user-id") || "0", 10); const cost = Number.parseInt(trigger.getAttribute("data-bank-reveal-cost") || "1000", 10); if (!userId || trigger.getAttribute("aria-disabled") === "true") { return; } const confirmed = await confirmRevealBankBalance(cost); if (!confirmed) { return; } trigger.setAttribute("aria-disabled", "true"); try { const response = await fetch("/user/reveal-info", { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ user_id: userId, asset: "bank_jjb" }), }); const data = await response.json(); if (data.status !== "success") { await window.chatDialog?.alert?.(data.message || "查看失败,请稍后重试。", "查看失败", "#cc4444"); return; } const valueWrap = trigger.closest(".bank-rank-val"); if (valueWrap) { valueWrap.innerHTML = `🏦 ${Number(data.value || 0).toLocaleString()}`; } if (window.chatContext && data.jjb !== undefined) { // 同步流通金币缓存,避免后续购买或游戏继续使用扣费前余额。 window.chatContext.myGold = data.jjb; } bankShowMsg(data.message || "存款金额已显示。", true); } catch (error) { await window.chatDialog?.alert?.("网络异常,请稍后重试。", "查看失败", "#cc4444"); } finally { trigger.removeAttribute("aria-disabled"); } } /** * 切换排行榜排序并回到第一页。 * * @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) { // 排行榜接口每页 20 条,这里用当前页和页内索引还原全局名次。 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 === "女" ? "♀" : "♂"; const bankBalanceHtml = user.can_reveal ? `` : `🏦 ${Number(user.bank_jjb || 0).toLocaleString()}`; return `
${absoluteRank}
${username} ${sexSymbol}
${bankBalanceHtml}
`; } /** * 规整排行榜头像地址,避免任意 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 revealButton = event.target.closest("[data-bank-reveal-user-id]"); if (revealButton instanceof HTMLElement) { event.preventDefault(); void revealRankingBankBalance(revealButton); 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(); } }); }