From 9f61dcc619a3756b7ea2aafedf1d28a314f7d3fc Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 14:43:33 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E9=92=93=E9=B1=BC=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/chat-room.js | 20 +- resources/js/chat-room/fishing.js | 609 +++++++++++++++++- resources/views/chat/frame.blade.php | 7 + .../partials/games/fishing-panel.blade.php | 570 +--------------- 4 files changed, 631 insertions(+), 575 deletions(-) diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 541519e..58d4516 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -31,7 +31,7 @@ * - holiday-modal.js:处理节日福利弹窗和系统消息入口。 * - initial-state.js:恢复首屏历史消息、欢迎消息、入场特效和挂起婚姻事件。 * - bank-modal.js:处理银行弹窗、转账、排行和标签切换。 - * - fishing.js:处理钓鱼入口与自动钓鱼相关交互。 + * - fishing.js:处理钓鱼抛竿、收竿、浮漂和自动钓鱼循环。 * - fortune-panel.js:提供神秘占卜 fortunePanel Alpine 组件。 * - profile-controls.js:处理用户资料和资料相关按钮。 * - shop-controls.js:处理商店弹窗的基础按钮事件。 @@ -106,7 +106,7 @@ export { switchBankTab, toggleBankRankSort, } from "./chat-room/bank-modal.js"; -export { bindFishingControls } from "./chat-room/fishing.js"; +export { bindFishingControls, checkAndAutoStartFishing, createBobber, reelFish, removeBobber, resetFishingBtn, startFishing, stopAutoFishing } from "./chat-room/fishing.js"; export { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-panel.js"; export { bindProfileControls } from "./chat-room/profile-controls.js"; export { bindShopControls } from "./chat-room/shop-controls.js"; @@ -200,7 +200,7 @@ import { switchBankTab, toggleBankRankSort, } from "./chat-room/bank-modal.js"; -import { bindFishingControls } from "./chat-room/fishing.js"; +import { bindFishingControls, checkAndAutoStartFishing, createBobber, reelFish, removeBobber, resetFishingBtn, startFishing, stopAutoFishing } from "./chat-room/fishing.js"; import { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-panel.js"; import { bindProfileControls } from "./chat-room/profile-controls.js"; import { bindShopControls } from "./chat-room/shop-controls.js"; @@ -314,6 +314,13 @@ if (typeof window !== "undefined") { switchBankTab, toggleBankRankSort, bindFishingControls, + checkAndAutoStartFishing, + createBobber, + reelFish, + removeBobber, + resetFishingBtn, + startFishing, + stopAutoFishing, bindFortunePanelControls, fortunePanel, bindMarriageStatusControls, @@ -408,6 +415,13 @@ if (typeof window !== "undefined") { window.openLotteryPanel = openLotteryPanel; window.openBankModal = openBankModal; window.showLotteryMsg = showLotteryMsg; + window.checkAndAutoStartFishing = checkAndAutoStartFishing; + window.createBobber = createBobber; + window.reelFish = reelFish; + window.removeBobber = removeBobber; + window.resetFishingBtn = resetFishingBtn; + window.startFishing = startFishing; + window.stopAutoFishing = stopAutoFishing; window.buyVip = buyVip; window.closeVipModal = closeVipModal; window.openVipModal = openVipModal; diff --git a/resources/js/chat-room/fishing.js b/resources/js/chat-room/fishing.js index 1497ca4..174e97c 100644 --- a/resources/js/chat-room/fishing.js +++ b/resources/js/chat-room/fishing.js @@ -1,13 +1,605 @@ -// 聊天室钓鱼入口事件绑定,先兼容存量全局 startFishing 实现。 +// 聊天室钓鱼小游戏模块,管理抛竿、收竿、自动钓鱼循环和浮漂交互。 + +import { escapeHtml } from "./html.js"; let fishingEventsBound = false; +let fishingTimer = null; +let fishingReelTimeout = null; +let fishToken = null; +let autoFishing = false; +let autoFishCooldownTimer = null; +let autoFishCooldownCountdown = null; /** - * 绑定钓鱼按钮点击事件。 + * 读取 CSRF Token。 + * + * @returns {string} + */ +function csrf() { + return document.querySelector('meta[name="csrf-token"]')?.content || ""; +} + +/** + * 获取私聊消息容器,钓鱼提示沿用旧版展示位置。 + * + * @returns {HTMLElement|null} + */ +function messageContainer() { + return document.getElementById("chat-messages-container2"); +} + +/** + * 判断当前是否允许自动滚动。 + * + * @returns {boolean} + */ +function shouldAutoScroll() { + return typeof window.isChatAutoScrollEnabled === "function" ? window.isChatAutoScrollEnabled() : true; +} + +/** + * 追加一条钓鱼提示消息。 + * + * @param {string} html + * @returns {void} + */ +function appendFishingMessage(html) { + const container = messageContainer(); + if (!container) { + return; + } + + const line = document.createElement("div"); + line.className = "msg-line"; + line.innerHTML = html; + container.appendChild(line); + + if (shouldAutoScroll()) { + container.scrollTop = container.scrollHeight; + } +} + +/** + * 当前本地时间文本。 + * + * @returns {string} + */ +function timeText() { + return new Date().toLocaleTimeString("zh-CN", { hour12: false }); +} + +/** + * 注入浮漂和自动钓鱼按钮需要的动画样式。 + * + * @returns {void} + */ +function ensureFishingStyles() { + if (!document.getElementById("bobber-style")) { + const style = document.createElement("style"); + style.id = "bobber-style"; + style.textContent = ` + @keyframes bobberFloat { + 0%,100% { transform: translateY(0) rotate(-8deg); } + 50% { transform: translateY(-10px) rotate(8deg); } + } + @keyframes bobberSink { + 0% { transform: translateY(0) scale(1); opacity:1; } + 30% { transform: translateY(12px) scale(1.3); opacity:1; } + 100% { transform: translateY(40px) scale(0.5); opacity:0; } + } + #fishing-bobber.sinking { + animation: bobberSink 1.5s forwards !important; + } + `; + document.head.appendChild(style); + } + + if (!document.getElementById("auto-fish-stop-style")) { + const style = document.createElement("style"); + style.id = "auto-fish-stop-style"; + style.textContent = ` + @keyframes autoFishBtnPulse { + 0%,100% { box-shadow: 0 4px 12px rgba(220,38,38,0.4); } + 50% { box-shadow: 0 4px 20px rgba(220,38,38,0.7); } + } + #auto-fish-stop-btn { + position: fixed; + z-index: 10000; + background: linear-gradient(135deg, #dc2626, #b91c1c); + color: #fff; + border: none; + border-radius: 20px; + padding: 8px 18px; + font-size: 13px; + font-weight: bold; + cursor: grab; + user-select: none; + animation: autoFishBtnPulse 1.8s ease-in-out infinite; + touch-action: none; + } + #auto-fish-stop-btn:active { cursor: grabbing; } + #auto-fish-stop-btn .drag-hint { + display: block; + font-size: 9px; + font-weight: normal; + opacity: .65; + margin-top: 1px; + text-align: center; + letter-spacing: .5px; + } + `; + document.head.appendChild(style); + } +} + +/** + * 创建浮漂 DOM 元素。 + * + * @param {number} x 水平百分比 + * @param {number} y 垂直百分比 + * @returns {HTMLElement} + */ +export function createBobber(x, y) { + ensureFishingStyles(); + + const bobber = document.createElement("div"); + bobber.id = "fishing-bobber"; + bobber.style.cssText = ` + position: fixed; + left: ${Number(x) || 50}vw; + top: ${Number(y) || 50}vh; + font-size: 28px; + cursor: pointer; + z-index: 9999; + animation: bobberFloat 1.2s ease-in-out infinite; + filter: drop-shadow(0 2px 6px rgba(0,0,0,0.4)); + user-select: none; + transition: transform 0.3s; + `; + bobber.textContent = "🪝"; + bobber.title = "鱼上钩了!快点击!"; + + return bobber; +} + +/** + * 移除当前浮漂。 + * + * @returns {void} + */ +export function removeBobber() { + document.getElementById("fishing-bobber")?.remove(); +} + +/** + * 获取钓鱼按钮。 + * + * @returns {HTMLButtonElement|null} + */ +function fishingButton() { + const button = document.getElementById("fishing-btn"); + + return button instanceof HTMLButtonElement ? button : null; +} + +/** + * 设置钓鱼按钮文案和禁用状态。 + * + * @param {string} text + * @param {boolean} disabled + * @returns {void} + */ +function setFishingButton(text, disabled) { + const button = fishingButton(); + if (!button) { + return; + } + + button.textContent = text; + button.disabled = disabled; +} + +/** + * 启动自动钓鱼冷却倒计时。 + * + * @param {number} cooldown + * @returns {void} + */ +function startAutoFishingCooldown(cooldown) { + let remaining = cooldown; + setFishingButton(`⏳ 冷却 ${remaining}s`, true); + showAutoFishStopButton(cooldown); + + autoFishCooldownCountdown = window.setInterval(() => { + remaining -= 1; + setFishingButton(`⏳ 冷却 ${remaining}s`, true); + + if (remaining <= 0) { + window.clearInterval(autoFishCooldownCountdown); + autoFishCooldownCountdown = null; + } + }, 1000); + + autoFishCooldownTimer = window.setTimeout(() => { + autoFishCooldownTimer = null; + hideAutoFishStopButton(); + + if (autoFishing) { + void startFishing(); + } + }, cooldown * 1000); +} + +/** + * 展示可拖动的停止自动钓鱼按钮。 + * + * @param {number} cooldown + * @returns {void} + */ +function showAutoFishStopButton(cooldown) { + if (document.getElementById("auto-fish-stop-btn")) { + return; + } + + ensureFishingStyles(); + + const button = document.createElement("button"); + button.id = "auto-fish-stop-btn"; + button.innerHTML = `🛑 停止自动钓鱼冷却 ${Number(cooldown) || 0}s · 可拖动`; + + try { + const saved = JSON.parse(window.localStorage.getItem("autoFishBtnPos") || "null"); + if (saved) { + button.style.left = `${Number(saved.left) || 0}px`; + button.style.top = `${Number(saved.top) || 0}px`; + } else { + button.style.bottom = "80px"; + button.style.right = "20px"; + } + } catch (error) { + button.style.bottom = "80px"; + button.style.right = "20px"; + } + + bindAutoFishStopDrag(button); + document.body.appendChild(button); +} + +/** + * 给停止自动钓鱼按钮绑定拖拽和点击停止事件。 + * + * @param {HTMLButtonElement} button + * @returns {void} + */ +function bindAutoFishStopDrag(button) { + let isDragging = false; + let startX = 0; + let startY = 0; + let startLeft = 0; + let startTop = 0; + + const dragStart = (event) => { + const rect = button.getBoundingClientRect(); + button.style.left = `${rect.left}px`; + button.style.top = `${rect.top}px`; + button.style.right = "auto"; + button.style.bottom = "auto"; + + isDragging = false; + const point = event.touches ? event.touches[0] : event; + startX = point.clientX; + startY = point.clientY; + startLeft = rect.left; + startTop = rect.top; + + document.addEventListener("mousemove", dragMove, { passive: false }); + document.addEventListener("mouseup", dragEnd); + document.addEventListener("touchmove", dragMove, { passive: false }); + document.addEventListener("touchend", dragEnd); + }; + + const dragMove = (event) => { + event.preventDefault(); + const point = event.touches ? event.touches[0] : event; + const dx = point.clientX - startX; + const dy = point.clientY - startY; + + if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { + isDragging = true; + } + + if (!isDragging) { + return; + } + + const nextLeft = Math.max(0, Math.min(window.innerWidth - button.offsetWidth, startLeft + dx)); + const nextTop = Math.max(0, Math.min(window.innerHeight - button.offsetHeight, startTop + dy)); + button.style.left = `${nextLeft}px`; + button.style.top = `${nextTop}px`; + }; + + const dragEnd = () => { + document.removeEventListener("mousemove", dragMove); + document.removeEventListener("mouseup", dragEnd); + document.removeEventListener("touchmove", dragMove); + document.removeEventListener("touchend", dragEnd); + + if (isDragging) { + window.localStorage.setItem("autoFishBtnPos", JSON.stringify({ + left: Number.parseInt(button.style.left, 10), + top: Number.parseInt(button.style.top, 10), + })); + } + }; + + button.addEventListener("mousedown", dragStart); + button.addEventListener("touchstart", dragStart, { passive: true }); + button.addEventListener("click", () => { + if (!isDragging) { + stopAutoFishing(); + } + }); +} + +/** + * 隐藏停止自动钓鱼按钮。 + * + * @returns {void} + */ +function hideAutoFishStopButton() { + document.getElementById("auto-fish-stop-btn")?.remove(); +} + +/** + * 开始钓鱼:调用抛竿接口并显示浮漂。 + * + * @returns {Promise} + */ +export async function startFishing() { + setFishingButton("🎣 抛竿中...", true); + + try { + const response = await fetch(window.chatContext.fishCastUrl, { + method: "POST", + headers: { + "X-CSRF-TOKEN": csrf(), + "Accept": "application/json", + }, + }); + const data = await response.json(); + + if (!response.ok || data.status !== "success") { + window.chatDialog?.alert?.(data.message || "钓鱼失败", "操作失败", "#cc4444"); + setFishingButton("🎣 钓鱼", false); + return; + } + + fishToken = data.token; + autoFishing = Boolean(data.auto_fishing); + appendFishingMessage(`🎣【钓鱼】${escapeHtml(data.message)}(${timeText()})`); + setFishingButton("🎣 等待中...", true); + + const bobber = createBobber(data.bobber_x, data.bobber_y); + document.body.appendChild(bobber); + + fishingTimer = window.setTimeout(() => { + bobber.classList.add("sinking"); + bobber.textContent = "🐟"; + + if (data.auto_fishing) { + appendFishingMessage(`🎣 自动钓鱼卡生效!自动收竿中... (剩余${Number(data.auto_fishing_minutes_left) || 0}分钟)`); + fishingReelTimeout = window.setTimeout(() => { + removeBobber(); + void reelFish(); + }, 1800); + return; + } + + appendFishingMessage('🐟 鱼上钩了!快点击屏幕上的浮漂!'); + setFishingButton("🎣 点击浮漂!", true); + bobber.addEventListener("click", () => { + removeBobber(); + + if (fishingReelTimeout) { + window.clearTimeout(fishingReelTimeout); + fishingReelTimeout = null; + } + + void reelFish(); + }, { once: true }); + + fishingReelTimeout = window.setTimeout(() => { + removeBobber(); + fishToken = null; + appendFishingMessage('💨 你反应太慢了,鱼跑掉了...'); + resetFishingBtn(); + }, 8000); + }, Number(data.wait_time || 0) * 1000); + } catch (error) { + window.chatDialog?.alert?.(`网络错误:${error.message}`, "网络异常", "#cc4444"); + removeBobber(); + setFishingButton("🎣 钓鱼", false); + } +} + +/** + * 收竿并提交本次钓鱼 token。 + * + * @returns {Promise} + */ +export async function reelFish() { + setFishingButton("🎣 拉竿中...", true); + + if (fishingReelTimeout) { + window.clearTimeout(fishingReelTimeout); + fishingReelTimeout = null; + } + + const token = fishToken; + fishToken = null; + + try { + const response = await fetch(window.chatContext.fishReelUrl, { + method: "POST", + headers: { + "X-CSRF-TOKEN": csrf(), + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + const data = await response.json(); + + if (response.ok && data.status === "success") { + const result = data.result || {}; + const color = Number(result.exp || 0) >= 0 ? "#16a34a" : "#dc2626"; + appendFishingMessage( + `${escapeHtml(result.emoji || "🎣")}【钓鱼结果】${escapeHtml(result.message || "")}` + + ` (经验:${Number(data.exp_num) || 0} 金币:${Number(data.jjb) || 0})` + + `(${timeText()})`, + ); + + if (autoFishing) { + startAutoFishingCooldown(Number(data.cooldown_seconds) || 300); + return; + } + } else { + appendFishingMessage(`【钓鱼】${escapeHtml(data.message || "操作失败")}(${timeText()})`); + + if (autoFishing) { + retryAutoFishing(); + return; + } + + autoFishing = false; + } + } catch (error) { + if (autoFishing) { + appendFishingMessage('⚠️ 网络异常,5秒后自动重试钓鱼...'); + retryAutoFishing(); + return; + } + + window.chatDialog?.alert?.(`网络错误:${error.message}`, "网络异常", "#cc4444"); + autoFishing = false; + } + + resetFishingBtn(); +} + +/** + * 自动钓鱼异常时短暂等待后重试。 + * + * @returns {void} + */ +function retryAutoFishing() { + setFishingButton("⏳ 重试中...", true); + autoFishCooldownTimer = window.setTimeout(() => { + autoFishCooldownTimer = null; + + if (autoFishing) { + void startFishing(); + } + }, 5000); +} + +/** + * 手动停止自动钓鱼循环。 + * + * @returns {void} + */ +export function stopAutoFishing() { + autoFishing = false; + clearAutoFishingTimers(); + hideAutoFishStopButton(); + appendFishingMessage('🛑 已停止自动钓鱼。'); + resetFishingBtn(); +} + +/** + * 清理自动钓鱼冷却计时器。 + * + * @returns {void} + */ +function clearAutoFishingTimers() { + if (autoFishCooldownTimer) { + window.clearTimeout(autoFishCooldownTimer); + autoFishCooldownTimer = null; + } + + if (autoFishCooldownCountdown) { + window.clearInterval(autoFishCooldownCountdown); + autoFishCooldownCountdown = null; + } +} + +/** + * 重置钓鱼按钮和临时状态。 + * + * @returns {void} + */ +export function resetFishingBtn() { + autoFishing = false; + clearAutoFishingTimers(); + hideAutoFishStopButton(); + + if (fishingTimer) { + window.clearTimeout(fishingTimer); + fishingTimer = null; + } + + if (fishingReelTimeout) { + window.clearTimeout(fishingReelTimeout); + fishingReelTimeout = null; + } + + setFishingButton("🎣 钓鱼", false); + removeBobber(); +} + +/** + * 检查自动钓鱼卡状态并恢复自动循环。 + * + * @returns {void} + */ +export function checkAndAutoStartFishing() { + const minutesLeft = Number(window.chatContext?.autoFishingMinutesLeft || 0); + const initialCooldown = Number(window.chatContext?.fishingCooldownSeconds || 0); + + if (minutesLeft <= 0 || autoFishing) { + return; + } + + autoFishing = true; + + if (initialCooldown > 0) { + console.log(`检测到自动钓鱼卡有效,恢复钓鱼状态,剩余冷却 ${initialCooldown}s`); + startAutoFishingCooldown(initialCooldown); + return; + } + + console.log("检测到自动钓鱼卡有效,自动抛竿"); + void startFishing(); +} + +/** + * 绑定钓鱼按钮、全局兼容入口和清屏恢复事件。 * * @returns {void} */ export function bindFishingControls() { + if (typeof window === "undefined") { + return; + } + + window.createBobber = createBobber; + window.removeBobber = removeBobber; + window.startFishing = startFishing; + window.reelFish = reelFish; + window.stopAutoFishing = stopAutoFishing; + window.resetFishingBtn = resetFishingBtn; + window.checkAndAutoStartFishing = checkAndAutoStartFishing; + if (fishingEventsBound || typeof document === "undefined") { return; } @@ -19,10 +611,15 @@ export function bindFishingControls() { } event.preventDefault(); + void startFishing(); + }); - // 钓鱼完整流程仍在 fishing-panel.blade.php,当前模块只统一按钮事件入口。 - if (typeof window.startFishing === "function") { - window.startFishing(); - } + document.addEventListener("DOMContentLoaded", () => { + checkAndAutoStartFishing(); + window.addEventListener("chat:screen-cleared", () => { + if (!autoFishing) { + checkAndAutoStartFishing(); + } + }); }); } diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 21f0e0f..56c24e2 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -30,6 +30,11 @@ $operatorActivePosition = Auth::user()->activePosition?->load('position.department')->position; $operatorDepartmentRank = (int) ($operatorActivePosition?->department?->rank ?? 0); $operatorPositionRank = (int) ($operatorActivePosition?->rank ?? 0); + // 自动钓鱼状态下发给 Vite 模块,避免钓鱼面板继续在 Blade 内写业务脚本。 + $autoFishingMinutesLeft = app(\App\Services\ShopService::class)->getActiveAutoFishingMinutesLeft(Auth::user()); + $fishingCooldownKey = 'fishing:cd:'.Auth::id(); + $fishingCooldownSeconds = \Illuminate\Support\Facades\Redis::ttl($fishingCooldownKey); + $fishingCooldownSeconds = $fishingCooldownSeconds > 0 ? $fishingCooldownSeconds : 0; @endphp +{{-- + 钓鱼小游戏业务脚本已迁移到 resources/js/chat-room/fishing.js。 + 自动钓鱼初始状态由 resources/views/chat/frame.blade.php 下发到 window.chatContext。 +--}}