Files
chatroom/resources/js/chat-room/bank-modal.js
T
2026-04-26 11:31:46 +08:00

517 lines
16 KiB
JavaScript

// 金币银行弹窗业务模块,替代 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<void>}
*/
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<void>}
*/
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 = '<div style="text-align:center;color:#ef4444;font-size:12px;padding:30px 0;">加载失败</div>';
}
}
}
/**
* 渲染最近资金流水。
*
* @param {Array<Record<string, unknown>>} logs 流水记录
* @returns {void}
*/
function renderBankLogs(logs) {
const list = document.getElementById("bank-logs-list");
if (!list) {
return;
}
if (logs.length === 0) {
list.innerHTML = '<div style="text-align:center;color:#9db3d4;font-size:12px;padding:30px 0;">暂无记录</div>';
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 `<div class="bank-log-item">
<span class="${className}">${sign} ${Number(log.amount || 0).toLocaleString()} 金币</span>
<span style="color:#64748b;">余额: <strong style="color:#334155">${Number(log.balance_after || 0).toLocaleString()}</strong></span>
<span style="color:#94a3b8;">${escapeHtml(createdAt)}</span>
</div>`;
}).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<boolean>}
*/
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<void>}
*/
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<void>}
*/
export async function fetchBankRanking(page) {
if (page < 1) {
return;
}
const listEl = document.getElementById("bank-ranking-list");
if (!listEl) {
return;
}
listEl.innerHTML = '<div style="text-align:center; color:#9db3d4; font-size:12px; padding:30px 0;">加载中...</div>';
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 = '<div style="text-align:center; color:#9db3d4; font-size:12px; padding:30px 0;">暂无数据</div>';
return;
}
listEl.innerHTML = data.ranking.map((user, index) => renderRankingRow(user, index)).join("");
} catch (error) {
listEl.innerHTML = '<div style="text-align:center; color:#ef4444; font-size:12px; padding:30px 0;">拉取失败</div>';
}
}
/**
* 同步排行榜分页按钮状态。
*
* @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<string, unknown>} 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
? `<button type="button"
data-bank-reveal-user-id="${escapeHtml(user.id)}"
data-bank-reveal-cost="${escapeHtml(user.reveal_cost || 1000)}"
title="点击查看存款,需扣除 ${Number(user.reveal_cost || 1000).toLocaleString()} 金币"
style="border:none;background:transparent;color:#059669;font-weight:bold;cursor:pointer;padding:0;">
****** 👁️
</button>`
: `🏦 ${Number(user.bank_jjb || 0).toLocaleString()}`;
return `<div class="bank-rank-item">
<div class="bank-rank-num ${rankClass}">${absoluteRank}</div>
<img src="${avatarUrl}" onerror="this.src='/images/headface/1.gif'" style="width:32px; height:32px; border-radius:50%; object-fit:cover; border:1px solid #d0e4f5;">
<div>
<div style="font-weight:bold; color:#1e3a8a;">${username} <span style="font-size:12px;">${sexSymbol}</span></div>
</div>
<div class="bank-rank-val">
${bankBalanceHtml}
</div>
</div>`;
}
/**
* 规整排行榜头像地址,避免任意 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();
}
});
}