新增聊天室座驾系统
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
// 聊天室座驾弹窗模块,负责座驾列表、购买、当前座驾和购买记录展示。
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user