迁移老虎机游戏脚本
This commit is contained in:
@@ -1,5 +1,46 @@
|
|||||||
// 聊天室 Vite 入口,集中导出从 Blade 内联脚本迁移出的纯前端工具。
|
// 聊天室 Vite 入口,集中导出从 Blade 内联脚本迁移出的纯前端工具。
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块引用说明:
|
||||||
|
* - html.js:提供聊天内容通用 HTML 转义工具。
|
||||||
|
* - appointment-announcement.js:处理任命/撤销公告的大卡片和系统消息。
|
||||||
|
* - banner.js:提供 window.chatBanner 居中大卡片通知。
|
||||||
|
* - chat-bot.js:处理 AI 小班长发送消息和清空上下文。
|
||||||
|
* - dialog.js:提供 window.chatDialog 全局弹窗。
|
||||||
|
* - daily-sign-in.js:处理每日签到弹窗与补签入口。
|
||||||
|
* - font-size.js:处理聊天输入/消息字号设置。
|
||||||
|
* - image-upload.js:处理聊天图片上传入口。
|
||||||
|
* - composer.js:处理聊天输入框、发送按钮和快捷操作。
|
||||||
|
* - toast.js:提供 window.chatToast 右下角通知。
|
||||||
|
* - friend-panel.js:处理好友面板、搜索和好友快捷操作。
|
||||||
|
* - friend-notifications.js:监听好友通知和通用 BannerNotification。
|
||||||
|
* - lightbox.js:处理聊天图片预览灯箱。
|
||||||
|
* - mobile-drawer.js:处理移动端抽屉、房间列表和在线名单。
|
||||||
|
* - marriage-status.js:处理婚姻状态展示与用户名片联动。
|
||||||
|
* - toolbar.js:处理工具栏按钮和功能快捷入口。
|
||||||
|
* - user-target-actions.js:处理点击用户名切换私聊目标和打开名片。
|
||||||
|
* - welcome-menu.js:处理欢迎菜单交互。
|
||||||
|
* - admin-menu.js:处理聊天室管理菜单交互。
|
||||||
|
* - baccarat-loss-cover-admin.js:处理百家乐买单活动管理弹层。
|
||||||
|
* - baccarat-loss-cover.js:处理百家乐买单活动前台弹窗。
|
||||||
|
* - game-hall.js:处理娱乐大厅弹窗和游戏入口卡片。
|
||||||
|
* - game-bootstrap.js:提供非关键游戏延迟初始化工具。
|
||||||
|
* - game-panels.js:处理通用游戏面板关闭事件。
|
||||||
|
* - holiday-modal.js:处理节日福利弹窗和系统消息入口。
|
||||||
|
* - initial-state.js:恢复首屏历史消息、欢迎消息、入场特效和挂起婚姻事件。
|
||||||
|
* - bank-modal.js:处理银行弹窗、转账、排行和标签切换。
|
||||||
|
* - fishing.js:处理钓鱼入口与自动钓鱼相关交互。
|
||||||
|
* - profile-controls.js:处理用户资料和资料相关按钮。
|
||||||
|
* - shop-controls.js:处理商店弹窗的基础按钮事件。
|
||||||
|
* - slot-machine.js:提供老虎机 slotPanel/slotFab Alpine 组件。
|
||||||
|
* - vip-controls.js:处理 VIP 中心相关入口。
|
||||||
|
* - preferences-status.js:处理聊天偏好、屏蔽系统播报和静音状态。
|
||||||
|
* - right-panel.js:处理右侧在线用户列表和用户名交互。
|
||||||
|
* - rooms.js:处理房间在线状态渲染和跳转 URL。
|
||||||
|
* - reward-modal.js:处理职务奖励金币弹窗入口。
|
||||||
|
* - message-queue.js:提供聊天消息分批渲染队列。
|
||||||
|
*/
|
||||||
|
|
||||||
// 统一转发各子模块导出,方便测试或后续模块继续复用同一组工具。
|
// 统一转发各子模块导出,方便测试或后续模块继续复用同一组工具。
|
||||||
export { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
export { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
||||||
export { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
|
export { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
|
||||||
@@ -63,6 +104,7 @@ export {
|
|||||||
export { bindFishingControls } from "./chat-room/fishing.js";
|
export { bindFishingControls } from "./chat-room/fishing.js";
|
||||||
export { bindProfileControls } from "./chat-room/profile-controls.js";
|
export { bindProfileControls } from "./chat-room/profile-controls.js";
|
||||||
export { bindShopControls } from "./chat-room/shop-controls.js";
|
export { bindShopControls } from "./chat-room/shop-controls.js";
|
||||||
|
export { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js";
|
||||||
export { bindVipControls } from "./chat-room/vip-controls.js";
|
export { bindVipControls } from "./chat-room/vip-controls.js";
|
||||||
export {
|
export {
|
||||||
BLOCKABLE_SYSTEM_SENDERS,
|
BLOCKABLE_SYSTEM_SENDERS,
|
||||||
@@ -153,6 +195,7 @@ import {
|
|||||||
import { bindFishingControls } from "./chat-room/fishing.js";
|
import { bindFishingControls } from "./chat-room/fishing.js";
|
||||||
import { bindProfileControls } from "./chat-room/profile-controls.js";
|
import { bindProfileControls } from "./chat-room/profile-controls.js";
|
||||||
import { bindShopControls } from "./chat-room/shop-controls.js";
|
import { bindShopControls } from "./chat-room/shop-controls.js";
|
||||||
|
import { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js";
|
||||||
import { bindVipControls } from "./chat-room/vip-controls.js";
|
import { bindVipControls } from "./chat-room/vip-controls.js";
|
||||||
import {
|
import {
|
||||||
BLOCKABLE_SYSTEM_SENDERS,
|
BLOCKABLE_SYSTEM_SENDERS,
|
||||||
@@ -258,6 +301,9 @@ if (typeof window !== "undefined") {
|
|||||||
bindMarriageStatusControls,
|
bindMarriageStatusControls,
|
||||||
bindProfileControls,
|
bindProfileControls,
|
||||||
bindShopControls,
|
bindShopControls,
|
||||||
|
bindSlotMachineControls,
|
||||||
|
slotFab,
|
||||||
|
slotPanel,
|
||||||
bindVipControls,
|
bindVipControls,
|
||||||
CHAT_FONT_SIZE_STORAGE_KEY,
|
CHAT_FONT_SIZE_STORAGE_KEY,
|
||||||
restoreChatFontSize,
|
restoreChatFontSize,
|
||||||
@@ -311,6 +357,8 @@ if (typeof window !== "undefined") {
|
|||||||
window.switchTarget = switchTarget;
|
window.switchTarget = switchTarget;
|
||||||
window.clearChatBotContext = clearChatBotContext;
|
window.clearChatBotContext = clearChatBotContext;
|
||||||
window.sendToChatBot = sendToChatBot;
|
window.sendToChatBot = sendToChatBot;
|
||||||
|
window.slotFab = slotFab;
|
||||||
|
window.slotPanel = slotPanel;
|
||||||
window.runFeatureShortcut = runFeatureShortcut;
|
window.runFeatureShortcut = runFeatureShortcut;
|
||||||
window.runToolbarAction = runToolbarAction;
|
window.runToolbarAction = runToolbarAction;
|
||||||
window.openHolidayRunFromSystemMessage = openHolidayRunFromSystemMessage;
|
window.openHolidayRunFromSystemMessage = openHolidayRunFromSystemMessage;
|
||||||
@@ -362,6 +410,7 @@ if (typeof window !== "undefined") {
|
|||||||
bindMarriageStatusControls();
|
bindMarriageStatusControls();
|
||||||
bindProfileControls();
|
bindProfileControls();
|
||||||
bindShopControls();
|
bindShopControls();
|
||||||
|
bindSlotMachineControls();
|
||||||
bindVipControls();
|
bindVipControls();
|
||||||
bindChatRightPanelControls();
|
bindChatRightPanelControls();
|
||||||
bindRoomStatusControls();
|
bindRoomStatusControls();
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
// 聊天室老虎机游戏前端组件,提供 slotPanel/slotFab Alpine 入口。
|
||||||
|
|
||||||
|
const SLOT_FAB_STORAGE_KEY = "slot_fab_pos";
|
||||||
|
const SLOT_EMOJIS = ["🍒", "🍋", "🍊", "🍇", "🔔", "💎", "💀", "7️⃣"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 CSRF Token。
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function csrf() {
|
||||||
|
return document.querySelector("meta[name=csrf-token]")?.content || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机取一个老虎机图案。
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function randomSlotEmoji() {
|
||||||
|
return SLOT_EMOJIS[Math.floor(Math.random() * SLOT_EMOJIS.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取悬浮按钮保存位置。
|
||||||
|
*
|
||||||
|
* @returns {{x:number,y:number}|null}
|
||||||
|
*/
|
||||||
|
function readSavedFabPosition() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(SLOT_FAB_STORAGE_KEY) || "null");
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建老虎机悬浮入口 Alpine 组件。
|
||||||
|
*
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function slotFab() {
|
||||||
|
const saved = readSavedFabPosition();
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
posX: saved?.x ?? 18,
|
||||||
|
posY: saved?.y ?? 150,
|
||||||
|
dragging: false,
|
||||||
|
_startX: 0,
|
||||||
|
_startY: 0,
|
||||||
|
_origX: 0,
|
||||||
|
_origY: 0,
|
||||||
|
_moved: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化时检测老虎机是否开启。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/slot/info");
|
||||||
|
const data = await response.json();
|
||||||
|
this.visible = data.enabled === true;
|
||||||
|
} catch (error) {
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始拖动悬浮入口。
|
||||||
|
*
|
||||||
|
* @param {PointerEvent} event
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
startDrag(event) {
|
||||||
|
this.dragging = true;
|
||||||
|
this._moved = false;
|
||||||
|
this._startX = event.clientX;
|
||||||
|
this._startY = event.clientY;
|
||||||
|
this._origX = this.posX;
|
||||||
|
this._origY = this.posY;
|
||||||
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖动悬浮入口。
|
||||||
|
*
|
||||||
|
* @param {PointerEvent} event
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
onDrag(event) {
|
||||||
|
if (!this.dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = event.clientX - this._startX;
|
||||||
|
const dy = event.clientY - this._startY;
|
||||||
|
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
|
||||||
|
this._moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.posX = Math.max(4, Math.min(window.innerWidth - 60, this._origX - dx));
|
||||||
|
this.posY = Math.max(4, Math.min(window.innerHeight - 60, this._origY + dy));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束拖动;未移动时直接打开老虎机面板。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
endDrag() {
|
||||||
|
if (!this.dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragging = false;
|
||||||
|
localStorage.setItem(SLOT_FAB_STORAGE_KEY, JSON.stringify({
|
||||||
|
x: this.posX,
|
||||||
|
y: this.posY,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!this._moved) {
|
||||||
|
this.openPanel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开老虎机面板。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
openPanel() {
|
||||||
|
const panel = document.getElementById("slot-panel");
|
||||||
|
if (panel && window.Alpine) {
|
||||||
|
window.Alpine.$data(panel)?.open?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建老虎机主面板 Alpine 组件。
|
||||||
|
*
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function slotPanel() {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
costPerSpin: 100,
|
||||||
|
dailyLimit: 0,
|
||||||
|
remaining: null,
|
||||||
|
balance: 0,
|
||||||
|
spinning: false,
|
||||||
|
reel1Stopped: false,
|
||||||
|
reel2Stopped: false,
|
||||||
|
reel3Stopped: false,
|
||||||
|
spinEmojis: ["🎰", "🎰", "🎰"],
|
||||||
|
resultEmojis: ["❓", "❓", "❓"],
|
||||||
|
resultType: "",
|
||||||
|
resultLabel: "",
|
||||||
|
netChange: 0,
|
||||||
|
history: [],
|
||||||
|
_spinInterval: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alpine 初始化,监听 show 变化自动加载配置和历史。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.$watch("show", async (value) => {
|
||||||
|
if (value) {
|
||||||
|
await this.loadInfo();
|
||||||
|
await this.loadHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开面板并加载数据。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async open() {
|
||||||
|
this.show = true;
|
||||||
|
await this.loadInfo();
|
||||||
|
await this.loadHistory();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载游戏配置和用户余额。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async loadInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/slot/info");
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.enabled) {
|
||||||
|
this.show = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.costPerSpin = data.cost_per_spin;
|
||||||
|
this.dailyLimit = data.daily_limit;
|
||||||
|
this.remaining = data.remaining;
|
||||||
|
this.balance = window.__chatUser?.jjb ?? 0;
|
||||||
|
} catch (error) {
|
||||||
|
// 配置加载失败时保持面板当前状态,避免影响其他游戏入口。
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载最近转动记录。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async loadHistory() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/slot/history");
|
||||||
|
const data = await response.json();
|
||||||
|
this.history = data.history || [];
|
||||||
|
} catch (error) {
|
||||||
|
this.history = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行一次老虎机转动。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async doSpin() {
|
||||||
|
if (this.spinning || (this.dailyLimit > 0 && this.remaining <= 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spinning = true;
|
||||||
|
this.resultType = "";
|
||||||
|
this.resultLabel = "";
|
||||||
|
this.netChange = 0;
|
||||||
|
this.reel1Stopped = false;
|
||||||
|
this.reel2Stopped = false;
|
||||||
|
this.reel3Stopped = false;
|
||||||
|
|
||||||
|
// 本地先随机滚动,服务端返回结果后再逐列停下。
|
||||||
|
this._spinInterval = window.setInterval(() => {
|
||||||
|
this.spinEmojis = [randomSlotEmoji(), randomSlotEmoji(), randomSlotEmoji()];
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/slot/spin", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"X-CSRF-TOKEN": csrf(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
data = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
window.clearInterval(this._spinInterval);
|
||||||
|
this.spinning = false;
|
||||||
|
window.chatDialog?.alert?.("网络异常,请稍后重试", "错误", "#ef4444");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.ok) {
|
||||||
|
window.clearInterval(this._spinInterval);
|
||||||
|
this.spinning = false;
|
||||||
|
window.chatDialog?.alert?.(data.message || "转动失败", "提示", "#ef4444");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopReel = (reelIndex, emoji, delay) => new Promise((resolve) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
window.clearInterval(this._spinInterval);
|
||||||
|
this[`reel${reelIndex + 1}Stopped`] = true;
|
||||||
|
this.spinEmojis = [...this.spinEmojis];
|
||||||
|
this.spinEmojis[reelIndex] = emoji;
|
||||||
|
resolve();
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
|
await stopReel(0, data.emojis[0], 600);
|
||||||
|
this._spinInterval = window.setInterval(() => {
|
||||||
|
this.spinEmojis = [data.emojis[0], randomSlotEmoji(), randomSlotEmoji()];
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
await stopReel(1, data.emojis[1], 500);
|
||||||
|
this._spinInterval = window.setInterval(() => {
|
||||||
|
this.spinEmojis = [data.emojis[0], data.emojis[1], randomSlotEmoji()];
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
await stopReel(2, data.emojis[2], 400);
|
||||||
|
window.clearInterval(this._spinInterval);
|
||||||
|
|
||||||
|
this.resultEmojis = data.emojis;
|
||||||
|
this.resultType = data.result_type;
|
||||||
|
this.resultLabel = data.result_label;
|
||||||
|
this.netChange = data.payout > 0
|
||||||
|
? data.payout - this.costPerSpin
|
||||||
|
: -this.costPerSpin + (data.payout < 0 ? data.payout : 0);
|
||||||
|
this.balance = data.balance;
|
||||||
|
this.spinning = false;
|
||||||
|
|
||||||
|
// 转动成功后同步聊天室全局余额,避免其他面板继续显示旧金币数。
|
||||||
|
if (window.__chatUser) {
|
||||||
|
window.__chatUser.jjb = data.balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dailyLimit > 0 && this.remaining !== null) {
|
||||||
|
this.remaining = Math.max(0, this.remaining - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadHistory();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭面板并清理滚动动画。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
window.clearInterval(this._spinInterval);
|
||||||
|
this.spinning = false;
|
||||||
|
this.show = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定老虎机全局 Alpine 工厂函数。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bindSlotMachineControls() {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.slotFab = slotFab;
|
||||||
|
window.slotPanel = slotPanel;
|
||||||
|
}
|
||||||
@@ -289,260 +289,4 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
{{-- 老虎机 Alpine 组件已迁移到 resources/js/chat-room/slot-machine.js --}}
|
||||||
/**
|
|
||||||
* 老虎机悬浮按钮 Alpine 组件(检查游戏是否开启)
|
|
||||||
*/
|
|
||||||
function slotFab() {
|
|
||||||
const STORAGE_KEY = 'slot_fab_pos';
|
|
||||||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
|
|
||||||
return {
|
|
||||||
visible: false,
|
|
||||||
posX: saved?.x ?? 18,
|
|
||||||
posY: saved?.y ?? 150,
|
|
||||||
dragging: false,
|
|
||||||
_startX: 0,
|
|
||||||
_startY: 0,
|
|
||||||
_origX: 0,
|
|
||||||
_origY: 0,
|
|
||||||
_moved: false,
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/slot/info');
|
|
||||||
const data = await res.json();
|
|
||||||
this.visible = data.enabled === true;
|
|
||||||
} catch {}
|
|
||||||
},
|
|
||||||
|
|
||||||
startDrag(e) {
|
|
||||||
this.dragging = true;
|
|
||||||
this._moved = false;
|
|
||||||
this._startX = e.clientX;
|
|
||||||
this._startY = e.clientY;
|
|
||||||
this._origX = this.posX;
|
|
||||||
this._origY = this.posY;
|
|
||||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
||||||
},
|
|
||||||
|
|
||||||
onDrag(e) {
|
|
||||||
if (!this.dragging) return;
|
|
||||||
const dx = e.clientX - this._startX;
|
|
||||||
const dy = e.clientY - this._startY;
|
|
||||||
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) this._moved = true;
|
|
||||||
this.posX = Math.max(4, Math.min(window.innerWidth - 60, this._origX - dx));
|
|
||||||
this.posY = Math.max(4, Math.min(window.innerHeight - 60, this._origY + dy));
|
|
||||||
},
|
|
||||||
|
|
||||||
endDrag(e) {
|
|
||||||
if (!this.dragging) return;
|
|
||||||
this.dragging = false;
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
||||||
x: this.posX,
|
|
||||||
y: this.posY
|
|
||||||
}));
|
|
||||||
if (!this._moved) this.openPanel();
|
|
||||||
},
|
|
||||||
|
|
||||||
openPanel() {
|
|
||||||
const panel = document.getElementById('slot-panel');
|
|
||||||
if (panel) Alpine.$data(panel).open();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 老虎机主面板 Alpine 组件
|
|
||||||
*/
|
|
||||||
function slotPanel() {
|
|
||||||
// 所有图案的 emoji 数组,与服务端权重一致(用于转动时随机展示)
|
|
||||||
const ALL_EMOJIS = ['🍒', '🍋', '🍊', '🍇', '🔔', '💎', '💀', '7️⃣'];
|
|
||||||
|
|
||||||
return {
|
|
||||||
show: false,
|
|
||||||
|
|
||||||
// 配置
|
|
||||||
costPerSpin: 100,
|
|
||||||
dailyLimit: 0,
|
|
||||||
remaining: null,
|
|
||||||
balance: 0,
|
|
||||||
|
|
||||||
// 转轮状态
|
|
||||||
spinning: false,
|
|
||||||
reel1Stopped: false,
|
|
||||||
reel2Stopped: false,
|
|
||||||
reel3Stopped: false,
|
|
||||||
spinEmojis: ['🎰', '🎰', '🎰'],
|
|
||||||
resultEmojis: ['❓', '❓', '❓'],
|
|
||||||
|
|
||||||
// 结果
|
|
||||||
resultType: '',
|
|
||||||
resultLabel: '',
|
|
||||||
netChange: 0,
|
|
||||||
|
|
||||||
// 历史
|
|
||||||
history: [],
|
|
||||||
|
|
||||||
// 动画定时器
|
|
||||||
_spinInterval: null,
|
|
||||||
_stopTimers: [],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alpine 初始化: 监听 show 变化自动加载数据(解决从游戏大厅入口不调用 open() 时历史不刷新的问题)
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
this.$watch('show', async (val) => {
|
|
||||||
if (val) {
|
|
||||||
await this.loadInfo();
|
|
||||||
await this.loadHistory();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开面板并加载数据
|
|
||||||
*/
|
|
||||||
async open() {
|
|
||||||
this.show = true;
|
|
||||||
await this.loadInfo();
|
|
||||||
await this.loadHistory();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载游戏配置和余额
|
|
||||||
*/
|
|
||||||
async loadInfo() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/slot/info');
|
|
||||||
const data = await res.json();
|
|
||||||
if (!data.enabled) {
|
|
||||||
this.show = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.costPerSpin = data.cost_per_spin;
|
|
||||||
this.dailyLimit = data.daily_limit;
|
|
||||||
this.remaining = data.remaining;
|
|
||||||
// 从聊天室全局变量读取余额
|
|
||||||
this.balance = window.__chatUser?.jjb ?? 0;
|
|
||||||
} catch {}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载历史记录
|
|
||||||
*/
|
|
||||||
async loadHistory() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/slot/history');
|
|
||||||
const data = await res.json();
|
|
||||||
this.history = data.history || [];
|
|
||||||
} catch {}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行转动
|
|
||||||
*/
|
|
||||||
async doSpin() {
|
|
||||||
if (this.spinning) return;
|
|
||||||
if (this.dailyLimit > 0 && this.remaining <= 0) return;
|
|
||||||
|
|
||||||
this.spinning = true;
|
|
||||||
this.resultType = '';
|
|
||||||
this.resultLabel = '';
|
|
||||||
this.netChange = 0;
|
|
||||||
this.reel1Stopped = false;
|
|
||||||
this.reel2Stopped = false;
|
|
||||||
this.reel3Stopped = false;
|
|
||||||
|
|
||||||
// 开始随机滚动动画
|
|
||||||
this._spinInterval = setInterval(() => {
|
|
||||||
this.spinEmojis = [
|
|
||||||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
|
||||||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
|
||||||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
|
||||||
];
|
|
||||||
}, 80);
|
|
||||||
|
|
||||||
// 请求后端
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/slot/spin', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
data = await res.json();
|
|
||||||
} catch {
|
|
||||||
clearInterval(this._spinInterval);
|
|
||||||
this.spinning = false;
|
|
||||||
window.chatDialog?.alert('网络异常,请稍后重试', '错误', '#ef4444');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.ok) {
|
|
||||||
clearInterval(this._spinInterval);
|
|
||||||
this.spinning = false;
|
|
||||||
window.chatDialog?.alert(data.message || '转动失败', '提示', '#ef4444');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 逐列停止(延迟效果)
|
|
||||||
const stopReel = (reelIndex, emoji, delay) => {
|
|
||||||
return new Promise(resolve => setTimeout(() => {
|
|
||||||
clearInterval(this._spinInterval);
|
|
||||||
this['reel' + (reelIndex + 1) + 'Stopped'] = true;
|
|
||||||
this.spinEmojis = [...this.spinEmojis];
|
|
||||||
this.spinEmojis[reelIndex] = emoji;
|
|
||||||
resolve();
|
|
||||||
}, delay));
|
|
||||||
};
|
|
||||||
|
|
||||||
await stopReel(0, data.emojis[0], 600);
|
|
||||||
this._spinInterval = setInterval(() => {
|
|
||||||
this.spinEmojis = [data.emojis[0],
|
|
||||||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
|
||||||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)]
|
|
||||||
];
|
|
||||||
}, 80);
|
|
||||||
await stopReel(1, data.emojis[1], 500);
|
|
||||||
this._spinInterval = setInterval(() => {
|
|
||||||
this.spinEmojis = [data.emojis[0], data.emojis[1],
|
|
||||||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)]
|
|
||||||
];
|
|
||||||
}, 80);
|
|
||||||
await stopReel(2, data.emojis[2], 400);
|
|
||||||
|
|
||||||
clearInterval(this._spinInterval);
|
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
this.resultEmojis = data.emojis;
|
|
||||||
this.resultType = data.result_type;
|
|
||||||
this.resultLabel = data.result_label;
|
|
||||||
this.netChange = data.payout > 0 ? data.payout - this.costPerSpin : -this.costPerSpin + (data
|
|
||||||
.payout < 0 ? data.payout : 0);
|
|
||||||
this.balance = data.balance;
|
|
||||||
this.spinning = false;
|
|
||||||
|
|
||||||
// 更新全局余额(让聊天界面同步)
|
|
||||||
if (window.__chatUser) window.__chatUser.jjb = data.balance;
|
|
||||||
|
|
||||||
if (this.dailyLimit > 0 && this.remaining !== null) {
|
|
||||||
this.remaining = Math.max(0, this.remaining - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.loadHistory();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭面板
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
clearInterval(this._spinInterval);
|
|
||||||
this.spinning = false;
|
|
||||||
this.show = false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user