Files
chatroom/resources/js/chat-room/ride-controls.js
T
2026-04-30 09:40:50 +08:00

347 lines
9.6 KiB
JavaScript

// 聊天室座驾弹窗模块,负责座驾列表、购买、当前座驾和购买记录展示。
import { escapeHtml } from "./html.js";
const DEFAULT_RIDE_ITEMS_URL = "/rides/items";
const DEFAULT_RIDE_BUY_URL = "/rides/buy";
let rideEventsBound = false;
let rideLoaded = false;
let rideState = {
items: [],
currentRide: null,
purchases: [],
};
/**
* 读取座驾弹窗接口地址配置。
*
* @returns {{items:string,buy:string}}
*/
function rideUrls() {
const modal = document.getElementById("ride-modal");
return {
items: modal?.dataset.rideItemsUrl || DEFAULT_RIDE_ITEMS_URL,
buy: modal?.dataset.rideBuyUrl || DEFAULT_RIDE_BUY_URL,
};
}
/**
* 读取 CSRF Token。
*
* @returns {string}
*/
function csrf() {
return document.querySelector('meta[name="csrf-token"]')?.content || "";
}
/**
* 获取座驾接口请求头。
*
* @param {boolean} withJson 是否携带 JSON Content-Type
* @returns {Record<string, string>}
*/
function rideHeaders(withJson = false) {
const headers = {
"X-CSRF-TOKEN": csrf(),
"Accept": "application/json",
};
if (withJson) {
headers["Content-Type"] = "application/json";
}
return headers;
}
/**
* 打开座驾弹窗并首次加载数据。
*
* @returns {void}
*/
export function openRideModal() {
const modal = document.getElementById("ride-modal");
if (!modal) {
return;
}
modal.style.display = "flex";
if (!rideLoaded) {
rideLoaded = true;
void loadRides();
}
}
/**
* 关闭座驾弹窗。
*
* @returns {void}
*/
export function closeRideModal() {
const modal = document.getElementById("ride-modal");
if (modal) {
modal.style.display = "none";
}
}
/**
* 拉取座驾页面数据。
*
* @returns {Promise<Record<string, any>>}
*/
export async function fetchRideData() {
const response = await fetch(rideUrls().items, {
headers: rideHeaders(),
credentials: "same-origin",
});
if (!response.ok) {
throw new Error("座驾数据加载失败");
}
return response.json();
}
/**
* 加载并渲染座驾页面。
*
* @returns {Promise<void>}
*/
export async function loadRides() {
const list = document.getElementById("ride-items-list");
if (list) {
list.innerHTML = '<div class="ride-empty">加载中...</div>';
}
try {
const data = await fetchRideData();
rideState = {
items: Array.isArray(data.items) ? data.items : [],
currentRide: data.current_ride || null,
purchases: Array.isArray(data.purchases) ? data.purchases : [],
};
renderRides(data);
} catch (error) {
if (list) {
list.innerHTML = '<div class="ride-empty ride-error">加载失败,请稍后重试</div>';
}
}
}
/**
* 渲染座驾弹窗全部内容。
*
* @param {Record<string, any>} data 接口返回数据
* @returns {void}
*/
export function renderRides(data) {
const balance = document.getElementById("ride-jjb");
if (balance) {
balance.textContent = Number(data.user_jjb || data.jjb || 0).toLocaleString();
}
renderCurrentRide(data.current_ride || null);
renderRideItems(Array.isArray(data.items) ? data.items : rideState.items);
renderRidePurchases(Array.isArray(data.purchases) ? data.purchases : rideState.purchases);
}
/**
* 渲染当前激活座驾。
*
* @param {Record<string, any>|null} currentRide 当前座驾记录
* @returns {void}
*/
function renderCurrentRide(currentRide) {
const box = document.getElementById("ride-current");
if (!box) {
return;
}
const item = currentRide?.item;
if (!item) {
box.innerHTML = '<span class="ride-current-empty">当前未启用座驾</span>';
return;
}
box.innerHTML = `
<span class="ride-current-icon">${escapeHtml(item.icon || "🚘")}</span>
<span><b>${escapeHtml(item.name)}</b> 生效中</span>
<span class="ride-current-expire">到期:${escapeHtml(currentRide.expires_at || "-")}</span>
`;
}
/**
* 渲染座驾商品卡片。
*
* @param {Array<Record<string, any>>} items 座驾商品列表
* @returns {void}
*/
function renderRideItems(items) {
const list = document.getElementById("ride-items-list");
if (!list) {
return;
}
if (!items.length) {
list.innerHTML = '<div class="ride-empty">暂无上架座驾</div>';
return;
}
const activeItemId = Number(rideState.currentRide?.item?.id || 0);
list.innerHTML = items.map((item) => {
const isActive = Number(item.id) === activeItemId;
const duration = Number(item.duration_days || 0);
return `
<div class="ride-card${isActive ? " active" : ""}">
<div class="ride-card-head">
<span class="ride-card-icon">${escapeHtml(item.icon || "🚘")}</span>
<span class="ride-card-title">${escapeHtml(item.name || "")}</span>
${isActive ? '<span class="ride-active-badge">当前</span>' : ""}
</div>
<div class="ride-card-desc">${escapeHtml(item.description || "")}</div>
<div class="ride-card-meta">
<span>💰 ${Number(item.price || 0).toLocaleString()} 金币</span>
<span>⏱ ${duration > 0 ? `${duration}` : "未配置"}</span>
</div>
<button type="button" class="ride-buy-btn" data-ride-buy="${Number(item.id)}">
${isActive ? "续费座驾" : "购买座驾"}
</button>
</div>
`;
}).join("");
}
/**
* 渲染座驾购买记录。
*
* @param {Array<Record<string, any>>} purchases 购买记录
* @returns {void}
*/
function renderRidePurchases(purchases) {
const list = document.getElementById("ride-purchase-list");
if (!list) {
return;
}
if (!purchases.length) {
list.innerHTML = '<div class="ride-empty">暂无座驾购买记录</div>';
return;
}
list.innerHTML = purchases.map((purchase) => {
const item = purchase.item || {};
const statusMap = {
active: "使用中",
expired: "已过期",
cancelled: "已替换",
used: "已使用",
};
return `
<div class="ride-record">
<span>${escapeHtml(item.icon || "🚘")} ${escapeHtml(item.name || "未知座驾")}</span>
<span>${escapeHtml(statusMap[purchase.status] || purchase.status || "-")}</span>
<span>${Number(purchase.price_paid || 0).toLocaleString()} 金币</span>
<span>${escapeHtml(purchase.expires_at || "-")}</span>
</div>
`;
}).join("");
}
/**
* 购买或续费座驾。
*
* @param {number|string} itemId 商品 ID
* @returns {Promise<void>}
*/
export async function buyRide(itemId) {
const item = rideState.items.find((entry) => Number(entry.id) === Number(itemId));
if (!item) {
return;
}
const duration = Number(item.duration_days || 0);
const ok = await window.chatDialog?.confirm?.(
`确认花费 ${Number(item.price || 0).toLocaleString()} 金币购买【${item.name}】吗?\n有效期:${duration}\n同款续购会自动叠加有效期。`,
"确认购买座驾",
);
if (!ok) {
return;
}
try {
const response = await fetch(rideUrls().buy, {
method: "POST",
credentials: "same-origin",
headers: rideHeaders(true),
body: JSON.stringify({
item_id: Number(itemId),
room_id: window.chatContext?.roomId || 0,
}),
});
const data = await response.json();
if (!response.ok || data.status !== "success") {
window.chatDialog?.alert?.(data.message || "购买失败", "座驾购买", "#cc4444");
return;
}
rideState.currentRide = data.current_ride || null;
rideState.purchases = Array.isArray(data.purchases) ? data.purchases : [];
renderRides({
items: rideState.items,
current_ride: rideState.currentRide,
purchases: rideState.purchases,
jjb: data.jjb,
});
const shopBalance = document.getElementById("shop-jjb");
if (shopBalance) {
shopBalance.textContent = Number(data.jjb || 0).toLocaleString();
}
window.chatDialog?.alert?.(data.message || "座驾购买成功", "座驾购买", "#16a34a");
} catch (error) {
window.chatDialog?.alert?.("网络异常,请稍后重试。", "座驾购买", "#cc4444");
}
}
/**
* 绑定座驾弹窗事件。
*
* @returns {void}
*/
export function bindRideControls() {
if (rideEventsBound || typeof document === "undefined") {
return;
}
rideEventsBound = true;
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const closeButton = event.target.closest("[data-ride-modal-close]");
const modal = document.getElementById("ride-modal");
if (closeButton || (modal && event.target === modal)) {
event.preventDefault();
closeRideModal();
return;
}
const buyButton = event.target.closest("[data-ride-buy]");
if (buyButton) {
event.preventDefault();
void buyRide(buyButton.getAttribute("data-ride-buy") || "");
}
});
window.openRideModal = openRideModal;
window.closeRideModal = closeRideModal;
window.loadRides = loadRides;
window.buyRide = buyRide;
}