347 lines
9.6 KiB
JavaScript
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;
|
|
}
|