Files
lkddi bfb1a3bca4 重构(chat): 聊天室 Partials 第二阶段分类拆分及修复红包弹窗隐藏 Bug
- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离
- 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题
- 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见)
- 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
2026-03-09 11:30:11 +08:00

462 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
// ── 钓鱼小游戏(随机浮漂版)─────────────────────────
let fishingTimer = null;
let fishingReelTimeout = null;
let _fishToken = null; // 当次钓鱼的 token
let _autoFishing = false; // 是否处于自动钓鱼循环中
let _autoFishCdTimer = null; // 自动钓鱼冷却计时器
let _autoFishCdCountdown = null; // 冷却倒计时 interval
/**
* 创建浮漂 DOM 元素(绝对定位在聊天框上层)
* @param {number} x 水平百分比 0-100
* @param {number} y 垂直百分比 0-100
* @returns {HTMLElement}
*/
function createBobber(x, y) {
const el = document.createElement('div');
el.id = 'fishing-bobber';
el.style.cssText = `
position: fixed;
left: ${x}vw;
top: ${y}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;
`;
el.textContent = '🪝';
el.title = '鱼上钩了!快点击!';
// 注入动画
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; }
}
@keyframes bobberPulse {
0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,0.6); }
50% { box-shadow: 0 0 0 14px rgba(220,38,38,0); }
}
#fishing-bobber.sinking {
animation: bobberSink 1.5s forwards !important;
}
`;
document.head.appendChild(style);
}
return el;
}
/** 移除浮漂 */
function removeBobber() {
const el = document.getElementById('fishing-bobber');
if (el) el.remove();
}
/**
* 开始钓鱼:调用抛竿 API,随机显示浮漂位置
*/
async function startFishing() {
const btn = document.getElementById('fishing-btn');
btn.disabled = true;
btn.textContent = '🎣 抛竿中...';
try {
const res = await fetch(window.chatContext.fishCastUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
});
const data = await res.json();
if (!res.ok || data.status !== 'success') {
window.chatDialog.alert(data.message || '钓鱼失败', '操作失败', '#cc4444');
btn.disabled = false;
btn.textContent = '🎣 钓鱼';
return;
}
// 保存本次 token(收竿时提交)
_fishToken = data.token;
_autoFishing = !!data.auto_fishing; // 持有自动钓鱼卡则开启循环模式
// 聊天框提示
const castDiv = document.createElement('div');
castDiv.className = 'msg-line';
const timeStr = new Date().toLocaleTimeString('zh-CN', {
hour12: false
});
castDiv.innerHTML =
`<span style="color:#2563eb;font-weight:bold;">🎣【钓鱼】</span>${data.message}<span class="msg-time">(${timeStr})</span>`;
container2.appendChild(castDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
btn.textContent = '🎣 等待中...';
// 创建浮漂(浮漂在随机位置)
const bobber = createBobber(data.bobber_x, data.bobber_y);
document.body.appendChild(bobber);
// 等待 wait_time 秒后浮漂「下沉」
fishingTimer = setTimeout(() => {
// 播放下沉动画
bobber.classList.add('sinking');
bobber.textContent = '🐟';
const hookDiv = document.createElement('div');
hookDiv.className = 'msg-line';
if (data.auto_fishing) {
// 自动钓鱼卡:在动画结束后自动收竿
hookDiv.innerHTML =
`<span style="color:#7c3aed;font-weight:bold;">🎣 自动钓鱼卡生效!自动收竿中... <span style="font-size:10px;opacity:0.7">(剩余${data.auto_fishing_minutes_left}分钟)</span></span>`;
container2.appendChild(hookDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
// 500ms 后自动收竿(等动画)
fishingReelTimeout = setTimeout(() => {
removeBobber();
reelFish();
}, 1800);
} else {
// 手动模式:玩家需在 8 秒内点击浮漂
hookDiv.innerHTML =
`<span style="color:#d97706;font-weight:bold;font-size:14px;">🐟 鱼上钩了!快点击屏幕上的浮漂!</span>`;
container2.appendChild(hookDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
btn.textContent = '🎣 点击浮漂!';
// 浮漂点击事件
bobber.onclick = () => {
removeBobber();
if (fishingReelTimeout) {
clearTimeout(fishingReelTimeout);
fishingReelTimeout = null;
}
reelFish();
};
// 8 秒内不点击 → 鱼跑了(token 过期服务端也会拒绝)
fishingReelTimeout = setTimeout(() => {
removeBobber();
_fishToken = null;
const missDiv = document.createElement('div');
missDiv.className = 'msg-line';
missDiv.innerHTML = '<span style="color:#999;">💨 你反应太慢了,鱼跑掉了...</span>';
container2.appendChild(missDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
resetFishingBtn();
}, 8000);
}
}, data.wait_time * 1000);
} catch (e) {
window.chatDialog.alert('网络错误:' + e.message, '网络异常', '#cc4444');
removeBobber();
btn.disabled = false;
btn.textContent = '🎣 钓鱼';
}
}
/**
* 收竿 — 提交 token 到后端,获取随机结果
*/
async function reelFish() {
const btn = document.getElementById('fishing-btn');
btn.disabled = true;
btn.textContent = '🎣 拉竿中...';
if (fishingReelTimeout) {
clearTimeout(fishingReelTimeout);
fishingReelTimeout = null;
}
const token = _fishToken;
_fishToken = null;
try {
const res = await fetch(window.chatContext.fishReelUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
token
})
});
const data = await res.json();
const timeStr = new Date().toLocaleTimeString('zh-CN', {
hour12: false
});
if (res.ok && data.status === 'success') {
const r = data.result;
const color = r.exp >= 0 ? '#16a34a' : '#dc2626';
const resultDiv = document.createElement('div');
resultDiv.className = 'msg-line';
resultDiv.innerHTML =
`<span style="color:${color};font-weight:bold;">${r.emoji}【钓鱼结果】</span>${r.message}` +
` <span style="color:#666;font-size:11px;">(经验:${data.exp_num} 金币:${data.jjb}</span>` +
`<span class="msg-time">(${timeStr})</span>`;
container2.appendChild(resultDiv);
// 自动钓鱼卡循环:等冷却时间后自动再次抛竿
if (_autoFishing) {
const cooldown = data.cooldown_seconds || 300;
const btn = document.getElementById('fishing-btn');
btn.disabled = true;
btn.textContent = `⏳ 冷却 ${cooldown}s`;
btn.onclick = null;
// 显示停止按钮
_showAutoFishStopBtn(cooldown);
// 倒计时更新文字
let remaining = cooldown;
_autoFishCdCountdown = setInterval(() => {
remaining--;
const b = document.getElementById('fishing-btn');
if (b) b.textContent = `⏳ 冷却 ${remaining}s`;
if (remaining <= 0) {
clearInterval(_autoFishCdCountdown);
_autoFishCdCountdown = null;
}
}, 1000);
// 冷却结束后自动抛竿
_autoFishCdTimer = setTimeout(() => {
_autoFishCdTimer = null;
_hideAutoFishStopBtn();
if (_autoFishing) startFishing(); // 仍未停止 → 继续
}, cooldown * 1000);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
return; // 不走 resetFishingBtn
}
} else {
const errDiv = document.createElement('div');
errDiv.className = 'msg-line';
errDiv.innerHTML =
`<span style="color:red;">【钓鱼】${data.message || '操作失败'}</span><span class="msg-time">(${timeStr})</span>`;
container2.appendChild(errDiv);
_autoFishing = false; // 出错时停止循环
}
if (autoScroll) container2.scrollTop = container2.scrollHeight;
} catch (e) {
window.chatDialog.alert('网络错误:' + e.message, '网络异常', '#cc4444');
_autoFishing = false;
}
resetFishingBtn();
}
/**
* 显示「停止自动钓鱼」悬浮按钮(支持拖拽移动)
* @param {number} cooldown 冷却秒数(用于倒计时提示)
*/
function _showAutoFishStopBtn(cooldown) {
if (document.getElementById('auto-fish-stop-btn')) return;
// 注入动画样式
if (!document.getElementById('auto-fish-stop-style')) {
const s = document.createElement('style');
s.id = 'auto-fish-stop-style';
s.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(s);
}
const btn = document.createElement('button');
btn.id = 'auto-fish-stop-btn';
btn.innerHTML = '🛑 停止自动钓鱼<span class="drag-hint">⠿ 可拖动</span>';
// 从 localStorage 恢复上次位置,默认右下角
const saved = (() => {
try {
return JSON.parse(localStorage.getItem('autoFishBtnPos'));
} catch {
return null;
}
})();
if (saved) {
btn.style.left = saved.left + 'px';
btn.style.top = saved.top + 'px';
} else {
btn.style.bottom = '80px';
btn.style.right = '20px';
}
// ── 拖拽逻辑(鼠标 + 触摸) ──────────────────────────────────
let isDragging = false;
let startX, startY, startLeft, startTop;
function onDragStart(e) {
// 将 right/bottom 转为 left/top 绝对坐标,便于拖拽计算
const rect = btn.getBoundingClientRect();
btn.style.left = rect.left + 'px';
btn.style.top = rect.top + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
isDragging = false;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
startX = clientX;
startY = clientY;
startLeft = rect.left;
startTop = rect.top;
document.addEventListener('mousemove', onDragMove, {
passive: false
});
document.addEventListener('mouseup', onDragEnd);
document.addEventListener('touchmove', onDragMove, {
passive: false
});
document.addEventListener('touchend', onDragEnd);
}
function onDragMove(e) {
e.preventDefault();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const dx = clientX - startX;
const dy = clientY - startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
isDragging = true;
}
if (!isDragging) return;
const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, startLeft + dx));
const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, startTop + dy));
btn.style.left = newLeft + 'px';
btn.style.top = newTop + 'px';
}
function onDragEnd() {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
document.removeEventListener('touchmove', onDragMove);
document.removeEventListener('touchend', onDragEnd);
// 持久化位置
if (isDragging) {
localStorage.setItem('autoFishBtnPos', JSON.stringify({
left: parseInt(btn.style.left),
top: parseInt(btn.style.top),
}));
}
}
btn.addEventListener('mousedown', onDragStart);
btn.addEventListener('touchstart', onDragStart, {
passive: true
});
// 拖拽时不触发 click;非拖拽时才停止钓鱼
btn.addEventListener('click', () => {
if (!isDragging) stopAutoFishing();
});
document.body.appendChild(btn);
}
/** 隐藏停止按钮 */
function _hideAutoFishStopBtn() {
const el = document.getElementById('auto-fish-stop-btn');
if (el) el.remove();
}
/**
* 手动停止自动钓鱼循环
*/
function stopAutoFishing() {
_autoFishing = false;
if (_autoFishCdTimer) {
clearTimeout(_autoFishCdTimer);
_autoFishCdTimer = null;
}
if (_autoFishCdCountdown) {
clearInterval(_autoFishCdCountdown);
_autoFishCdCountdown = null;
}
_hideAutoFishStopBtn();
const noticeDiv = document.createElement('div');
noticeDiv.className = 'msg-line';
noticeDiv.innerHTML = '<span style="color:#6b7280;">🛑 已停止自动钓鱼。</span>';
container2.appendChild(noticeDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
resetFishingBtn();
}
/**
* 重置钓鱼按钮状态(停止自动循环后调用)
*/
function resetFishingBtn() {
_autoFishing = false;
_hideAutoFishStopBtn();
if (_autoFishCdTimer) {
clearTimeout(_autoFishCdTimer);
_autoFishCdTimer = null;
}
if (_autoFishCdCountdown) {
clearInterval(_autoFishCdCountdown);
_autoFishCdCountdown = null;
}
const btn = document.getElementById('fishing-btn');
btn.textContent = '🎣 钓鱼';
btn.disabled = false;
btn.onclick = startFishing;
fishingTimer = null;
fishingReelTimeout = null;
removeBobber();
}
</script>