Files
chatroom/resources/views/chat/partials/scripts.blade.php

1132 lines
46 KiB
PHP
Raw Normal View History

{{--
文件功能聊天室前端交互脚本Blade 模板形式)
包含消息渲染、用户列表管理、发送消息、WebSocket 事件监听、
管理操作、存点心跳、头像选择等全部前端逻辑
通过 @include('chat.partials.scripts') 引入到 frame.blade.php
@author ChatRoom Laravel
@version 1.0.0
--}}
<script>
/**
* 聊天室前端交互逻辑
* 保留所有 WebSocket 事件监听,复刻原版 UI 交互
*/
// ── DOM 元素引用 ──────────────────────────────────────
const container = document.getElementById('chat-messages-container');
const container2 = document.getElementById('chat-messages-container2');
const userList = document.getElementById('online-users-list');
const toUserSelect = document.getElementById('to_user');
const onlineCount = document.getElementById('online-count');
const onlineCountBottom = document.getElementById('online-count-bottom');
let onlineUsers = {};
let autoScroll = true;
// ── Tab 切换 ──────────────────────────────────────
function switchTab(tab) {
['users', 'rooms', 'emoji', 'action'].forEach(t => {
document.getElementById('panel-' + t).style.display = t === tab ? 'block' : 'none';
document.getElementById('tab-' + t).classList.toggle('active', t === tab);
});
}
// ── 分屏切换 ──────────────────────────────────────
function changeSplitScreen(mode) {
const panes = document.getElementById('message-panes');
if (mode === '1') {
panes.classList.add('split-h');
} else {
panes.classList.remove('split-h');
}
}
// ── 表情插入 ──────────────────────────────────────
function insertEmoji(code) {
const input = document.getElementById('content');
input.value += code;
input.focus();
switchTab('users');
}
// ── 动作选择 ──────────────────────────────────────
function setAction(act) {
document.getElementById('action').value = act;
switchTab('users');
document.getElementById('content').focus();
}
// ── 自动滚屏 ──────────────────────────────────────
document.getElementById('auto_scroll').addEventListener('change', function() {
autoScroll = this.checked;
});
// ── 滚动到底部 ───────────────────────────────────
function scrollToBottom() {
if (autoScroll) {
container.scrollTop = container.scrollHeight;
}
}
// ── 渲染在线人员列表(支持排序) ──────────────────
function renderUserList() {
userList.innerHTML = '';
toUserSelect.innerHTML = '<option value="大家">大家</option>';
// 在列表顶部添加"大家"条目(原版风格)
let allDiv = document.createElement('div');
allDiv.className = 'user-item';
allDiv.innerHTML = '<span class="user-name" style="padding-left: 4px; color: navy;">大家</span>';
allDiv.onclick = () => {
toUserSelect.value = '大家';
};
userList.appendChild(allDiv);
// ── AI 小助手(仅当全局开关开启时显示,与普通用户风格一致)──
if (window.chatContext.chatBotEnabled) {
let botDiv = document.createElement('div');
botDiv.className = 'user-item';
botDiv.innerHTML = `
<img class="user-head" src="/images/headface/1.GIF" onerror="this.src='/images/headface/1.GIF'">
<span class="user-name">AI小助手</span><span style="font-size:12px; margin-left:2px;" title="聊天机器人">🤖</span>
`;
botDiv.onclick = () => {
toUserSelect.value = 'AI小助手';
document.getElementById('content').focus();
};
userList.appendChild(botDiv);
// 在发言对象下拉框中也添加 AI 小助手
let botOption = document.createElement('option');
botOption.value = 'AI小助手';
botOption.textContent = '🤖 AI小助手';
toUserSelect.appendChild(botOption);
}
// 获取排序方式
const sortSelect = document.getElementById('user-sort-select');
const sortBy = sortSelect ? sortSelect.value : 'default';
// 构建用户数组并排序
let userArr = [];
for (let username in onlineUsers) {
userArr.push({
username,
...onlineUsers[username]
});
}
if (sortBy === 'name') {
userArr.sort((a, b) => a.username.localeCompare(b.username, 'zh'));
} else if (sortBy === 'level') {
userArr.sort((a, b) => (b.user_level || 0) - (a.user_level || 0));
}
let count = userArr.length;
userArr.forEach(user => {
const username = user.username;
let item = document.createElement('div');
item.className = 'user-item';
item.dataset.username = username;
const headface = user.headface || '1.GIF';
// VIP 图标和管理员标识
let badges = '';
if (user.vip_icon) {
const vipColor = user.vip_color || '#f59e0b';
badges +=
`<span style="font-size:12px; margin-left:2px; color:${vipColor};" title="${user.vip_name || 'VIP'}">${user.vip_icon}</span>`;
}
if (user.is_admin) {
badges += `<span style="font-size:11px; margin-left:2px;" title="管理员">🛡️</span>`;
}
item.innerHTML = `
<img class="user-head" src="/images/headface/${headface}" onerror="this.src='/images/headface/1.GIF'">
<span class="user-name">${username}</span>${badges}
`;
item.onclick = () => {
toUserSelect.value = username;
document.getElementById('content').focus();
};
item.ondblclick = () => {
if (username !== window.chatContext.username) {
showUserInfoInSay2(username);
}
};
userList.appendChild(item);
if (username !== window.chatContext.username) {
let option = document.createElement('option');
option.value = username;
option.textContent = username;
toUserSelect.appendChild(option);
}
});
onlineCount.innerText = count;
onlineCountBottom.innerText = count;
const footer = document.getElementById('online-count-footer');
if (footer) footer.innerText = count;
// 如果有搜索关键词,重新过滤
filterUserList();
}
/**
* 搜索/过滤用户列表
*/
function filterUserList() {
const searchInput = document.getElementById('user-search-input');
const keyword = searchInput ? searchInput.value.trim().toLowerCase() : '';
const items = userList.querySelectorAll('.user-item');
items.forEach(item => {
if (!keyword) {
item.style.display = '';
return;
}
const name = (item.dataset.username || item.textContent || '').toLowerCase();
item.style.display = name.includes(keyword) ? '' : 'none';
});
}
// ── 点击消息中的用户名,切换发言对象 ──────────
function switchTarget(username) {
const options = toUserSelect.options;
let found = false;
for (let i = 0; i < options.length; i++) {
if (options[i].value === username) {
toUserSelect.value = username;
found = true;
break;
}
}
// 如果不在列表中(可能已离线),临时添加
if (!found && username !== '大家') {
const opt = document.createElement('option');
opt.value = username;
opt.textContent = username;
toUserSelect.appendChild(opt);
toUserSelect.value = username;
}
// 切换目标后自动聚焦输入框,方便直接输入
document.getElementById('content').focus();
}
/**
* 双击用户名 在包厢窗口(say2)显示用户基本信息
*/
async function showUserInfoInSay2(username) {
try {
const res = await fetch('/user/' + encodeURIComponent(username));
const info = await res.json();
const sexText = info.sex === '女' ? '女' : '男';
const level = info.user_level || 0;
const exp = info.exp_num || 0;
const jjb = info.jjb || 0;
const sign = info.qianming || info.sign || '暂无';
const lines = [
`══════ <b style="color:#336699;">${info.username || username}</b> 的资料 ══════`,
`性别:${sexText} 等级:${level} 经验:${exp} 金币:${jjb}`,
`签名:${sign}`,
`════════════════════════`
];
lines.forEach(text => {
const div = document.createElement('div');
div.className = 'msg-line';
div.innerHTML = `<span style="color:#666; font-size:12px;">${text}</span>`;
container2.appendChild(div);
});
container2.scrollTop = container2.scrollHeight;
} catch (e) {
console.error('获取用户资料失败:', e);
}
}
/**
* 追加消息到聊天窗格(原版风格:非气泡模式,逐行显示)
*/
function appendMessage(msg) {
const isMe = msg.from_user === window.chatContext.username;
const fontColor = msg.font_color || '#000000';
const div = document.createElement('div');
div.className = 'msg-line';
const timeStr = msg.sent_at || '';
// 系统用户名列表(不可被选为聊天对象)
const systemUsers = ['钓鱼播报', '星海小博士', '系统传音', '系统公告', 'AI小助手'];
// 用户名(单击切换发言对象,双击查看资料;系统用户仅显示文本)
const clickableUser = (uName, color) => {
if (systemUsers.includes(uName)) {
return `<span class="msg-user" style="color: ${color};">${uName}</span>`;
}
return `<span class="msg-user" style="color: ${color}; cursor: pointer;" onclick="switchTarget('${uName}')" ondblclick="showUserInfoInSay2('${uName}')">${uName}</span>`;
};
// 获取发言者头像
const senderInfo = onlineUsers[msg.from_user];
const senderHead = (senderInfo && senderInfo.headface) || '1.GIF';
const headImg =
`<img src="/images/headface/${senderHead}" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;" onerror="this.src='/images/headface/1.GIF'">`;
let html = '';
// 系统用户消息以醒目公告样式显示
if (systemUsers.includes(msg.from_user)) {
if (msg.from_user === '系统公告' || msg.from_user === '系统传音') {
// 管理员公告/系统传音:大字醒目样式
div.style.cssText =
'background: linear-gradient(135deg, #fef2f2, #fff1f2); border: 2px solid #ef4444; border-radius: 6px; padding: 8px 12px; margin: 4px 0; box-shadow: 0 2px 4px rgba(239,68,68,0.15);';
html = `<div style="font-size: 14px; font-weight: bold; color: #dc2626;">${msg.content}</div>`;
} else {
// 其他系统用户钓鱼播报、AI小助手等普通样式
html =
`${headImg}<span class="msg-user" style="color: ${fontColor}; font-weight: bold;">${msg.from_user}</span><span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
}
} else if (msg.is_secret) {
// 悄悄话样式(原版:紫色斜体)
html =
`<span class="msg-secret">${headImg}${clickableUser(msg.from_user, '#cc00cc')}对${clickableUser(msg.to_user, '#cc00cc')}`;
if (msg.action) html += `${msg.action}`;
html += `悄悄说:${msg.content}</span>`;
} else if (msg.to_user && msg.to_user !== '大家') {
html = `${headImg}${clickableUser(msg.from_user, '#000099')}对${clickableUser(msg.to_user, '#000099')}`;
if (msg.action) html += `${msg.action}`;
html += `说:<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
} else {
html = `${headImg}${clickableUser(msg.from_user, '#000099')}对大家`;
if (msg.action) html += `${msg.action}`;
html += `说:<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
}
html += ` <span class="msg-time">(${timeStr})</span>`;
div.innerHTML = html;
// 路由规则(复刻原版):
// 公众窗口(say1):别人的公聊消息
// 包厢窗口(say2):自己发的消息 + 悄悄话 + 对自己说的消息
const isRelatedToMe = isMe ||
msg.is_secret ||
msg.to_user === window.chatContext.username;
if (isRelatedToMe) {
container2.appendChild(div);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
} else {
container.appendChild(div);
scrollToBottom();
}
}
// ── WebSocket 初始化 ─────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
if (typeof window.initChat === 'function') {
window.initChat(window.chatContext.roomId);
}
});
// ── WebSocket 事件监听 ────────────────────────────
window.addEventListener('chat:here', (e) => {
const users = e.detail;
onlineUsers = {};
users.forEach(u => {
onlineUsers[u.username] = u;
});
renderUserList();
});
window.addEventListener('chat:joining', (e) => {
const user = e.detail;
onlineUsers[user.username] = user;
renderUserList();
// 原版风格:完整句式的随机趣味欢迎语
const gender = user.sex === '女' ? '美女' : '帅哥';
const uname = user.username;
const welcomeTemplates = [
`${gender}<b>${uname}</b>开着刚买不久的车,来到了,见到各位大虾,拱手曰:"众位大虾,小生有礼了"`,
`${gender}<b>${uname}</b>骑着小毛驴哼着小调,悠闲地走了进来,对大家嘿嘿一笑`,
`${gender}<b>${uname}</b>坐着豪华轿车缓缓驶入,推门而出,拍了拍身上的灰,霸气说道:"我来也!"`,
`${gender}<b>${uname}</b>踩着七彩祥云从天而降,众人皆惊,抱拳道:"各位久等了!"`,
`${gender}<b>${uname}</b>划着小船飘然而至,微微一笑,翩然上岸`,
`${gender}<b>${uname}</b>骑着自行车铃铛叮当响,远远就喊:"我来啦!想我没?"`,
`${gender}<b>${uname}</b>开着拖拉机突突突地开了进来,下车后拍了拍手说:"交通不便,来迟了!"`,
`${gender}<b>${uname}</b>坐着火箭嗖的一声到了,吓了大家一跳,嘿嘿笑道:"别怕别怕,是我啊"`,
`${gender}<b>${uname}</b>骑着白马翩翩而来,英姿飒爽,拱手道:"江湖路远,各位有礼了"`,
`${gender}<b>${uname}</b>开着宝马一路飞驰到此,推开车门走了下来,向大家挥了挥手`,
`${gender}<b>${uname}</b>踩着风火轮呼啸而至,在人群中潇洒亮相`,
`${gender}<b>${uname}</b>乘坐滑翔伞从天空缓缓降落,对大家喊道:"hello我从天上来"`,
`${gender}<b>${uname}</b>从地下钻了出来,拍了拍土,说:"哎呀,走错路了,不过总算到了"`,
`${gender}<b>${uname}</b>蹦蹦跳跳地跑了进来,嘻嘻哈哈地跟大家打招呼`,
`${gender}<b>${uname}</b>悄悄地溜了进来,生怕被人发现,东张西望了一番`,
`${gender}<b>${uname}</b>迈着六亲不认的步伐走进来,气场两米八`,
];
const msg = welcomeTemplates[Math.floor(Math.random() * welcomeTemplates.length)];
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
const sysDiv = document.createElement('div');
sysDiv.className = 'msg-line';
// VIP 用户使用专属颜色和图标
if (user.vip_icon && user.vip_name) {
const vipColor = user.vip_color || '#f59e0b';
sysDiv.innerHTML =
`<span style="color: ${vipColor}; font-weight: bold;">【${user.vip_icon} ${user.vip_name}】${msg}</span><span class="msg-time">(${timeStr})</span>`;
} else {
sysDiv.innerHTML =
`<span style="color: green">【欢迎】${msg}</span><span class="msg-time">(${timeStr})</span>`;
}
container.appendChild(sysDiv);
scrollToBottom();
});
window.addEventListener('chat:leaving', (e) => {
const user = e.detail;
delete onlineUsers[user.username];
renderUserList();
const sysDiv = document.createElement('div');
sysDiv.className = 'msg-line sys-msg';
// VIP 用户离开也带专属颜色
if (user.vip_icon && user.vip_name) {
const vipColor = user.vip_color || '#f59e0b';
sysDiv.innerHTML =
`<span style="color: ${vipColor}">☆ ${user.vip_icon}${user.vip_name} ${user.username} 潇洒离去 ☆</span>`;
} else {
sysDiv.innerHTML = `<span style="color: gray">☆ ${user.username} 离开了聊天室 ☆</span>`;
}
container.appendChild(sysDiv);
scrollToBottom();
});
window.addEventListener('chat:message', (e) => {
const msg = e.detail;
if (msg.is_secret && msg.from_user !== window.chatContext.username && msg.to_user !== window
.chatContext.username) {
return;
}
appendMessage(msg);
});
window.addEventListener('chat:kicked', (e) => {
if (e.detail.username === window.chatContext.username) {
alert("您已被管理员踢出房间!" + (e.detail.reason ? "\n原因:" + e.detail.reason : ""));
window.location.href = "{{ route('rooms.index') }}";
}
});
// ── 禁言状态(本地计时器) ──
let isMutedUntil = 0;
window.addEventListener('chat:muted', (e) => {
const d = e.detail;
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
// 在聊天窗口显示禁言通知
const div = document.createElement('div');
div.className = 'msg-line';
div.innerHTML =
`<span style="color: #c00; font-weight: bold;">【系统】${d.message}</span><span class="msg-time">(${timeStr})</span>`;
container.appendChild(div);
if (autoScroll) container.scrollTop = container.scrollHeight;
// 如果是自己被禁言,设置本地禁言计时
if (d.username === window.chatContext.username && d.mute_time > 0) {
isMutedUntil = Date.now() + d.mute_time * 60 * 1000;
const contentInput = document.getElementById('content');
if (contentInput) {
contentInput.placeholder = `您已被禁言 ${d.mute_time} 分钟,解禁后方可发言...`;
contentInput.disabled = true;
// 到期自动恢复
setTimeout(() => {
isMutedUntil = 0;
contentInput.placeholder = '在这里输入聊天内容,按 Enter 发送...';
contentInput.disabled = false;
const unmuteDiv = document.createElement('div');
unmuteDiv.className = 'msg-line';
unmuteDiv.innerHTML =
'<span style="color: #16a34a; font-weight: bold;">【系统】您的禁言已解除,可以继续发言了。</span>';
container.appendChild(unmuteDiv);
if (autoScroll) container.scrollTop = container.scrollHeight;
}, d.mute_time * 60 * 1000);
}
}
});
window.addEventListener('chat:title-updated', (e) => {
document.getElementById('room-title-display').innerText = e.detail.title;
});
// ── 发送消息Enter 发送) ───────────────────────
document.getElementById('content').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(e);
}
});
/**
* 发送聊天消息
*/
async function sendMessage(e) {
if (e) e.preventDefault();
// 前端禁言检查
if (isMutedUntil > Date.now()) {
const remaining = Math.ceil((isMutedUntil - Date.now()) / 1000);
alert(`您正在禁言中,还有 ${remaining} 秒后解禁。`);
return;
}
const form = document.getElementById('chat-form');
const formData = new FormData(form);
const contentInput = document.getElementById('content');
const submitBtn = document.getElementById('send-btn');
const content = formData.get('content').trim();
if (!content) {
contentInput.focus();
return;
}
// 如果发言对象是 AI 小助手,走专用机器人 API
const toUser = formData.get('to_user');
if (toUser === 'AI小助手') {
contentInput.value = '';
contentInput.focus();
await sendToChatBot(content);
return;
}
submitBtn.disabled = true;
try {
const response = await fetch(window.chatContext.sendUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content'),
'Accept': 'application/json'
},
body: formData
});
const data = await response.json();
if (response.ok && data.status === 'success') {
contentInput.value = '';
contentInput.focus();
} else {
alert('发送失败: ' + (data.message || JSON.stringify(data.errors)));
}
} catch (error) {
alert('网络连接错误,消息发送失败!');
console.error(error);
} finally {
submitBtn.disabled = false;
}
}
// ── 管理操作执行(下拉选择 + 目标用户) ─────────────
async function executeAdminAction() {
const select = document.getElementById('admin-action-select');
if (!select) return;
const action = select.value;
if (!action) {
alert('请先选择一个管理操作。');
return;
}
// 获取聊天对象下拉框中选中的用户作为目标
const toUserSelect = document.getElementById('to_user');
const targetUser = toUserSelect ? toUserSelect.value : '';
if (!targetUser || targetUser === '大家') {
alert('请先在「对...说」下拉中选择要操作的用户。');
return;
}
// 操作名称映射
const actionNames = {
kick: '踢出',
mute: '禁言',
ban: '封号',
banip: '封IP'
};
const actionName = actionNames[action] || action;
// 禁言需要输入时长
let duration = 5;
if (action === 'mute') {
const input = prompt('请输入禁言时长(分钟):', '5');
if (!input) return;
duration = parseInt(input) || 5;
}
// 二次确认
const confirmMsg = action === 'banip' ?
`⚠️ 严重操作:确定要封禁 ${targetUser} 的IP地址吗该用户将被封号+封IP` :
`确定要对 ${targetUser} 执行「${actionName}」操作吗?`;
if (!confirm(confirmMsg)) return;
try {
const body = {
room_id: window.chatContext.roomId
};
if (action === 'mute') body.duration = duration;
const res = await fetch(`/user/${encodeURIComponent(targetUser)}/${action}`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(body)
});
const data = await res.json();
alert(data.message);
} catch (e) {
alert('操作失败:' + e.message);
}
select.value = ''; // 重置下拉
}
// ── 设置房间公告 ─────────────────────────────────────
async function promptAnnouncement() {
const currentText = document.getElementById('announcement-text')?.textContent?.trim() || '';
const newText = prompt('请输入新的房间公告/祝福语:', currentText);
if (newText === null || newText.trim() === '') return;
try {
const res = await fetch(`/room/${window.chatContext.roomId}/announcement`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
announcement: newText.trim()
})
});
const data = await res.json();
if (data.status === 'success') {
const marquee = document.getElementById('announcement-text');
if (marquee) marquee.textContent = newText.trim();
alert('公告已更新!');
} else {
alert(data.message || '更新失败');
}
} catch (e) {
alert('设置公告失败:' + e.message);
}
}
// ── 站长公屏讲话 ─────────────────────────────────────
async function promptAnnounceMessage() {
const content = prompt('请输入公屏讲话内容:');
if (!content || !content.trim()) return;
try {
const res = await fetch('/command/announce', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
content: content.trim(),
room_id: window.chatContext.roomId,
})
});
const data = await res.json();
if (!res.ok || data.status !== 'success') {
alert(data.message || '发送失败');
}
} catch (e) {
alert('发送失败:' + e.message);
}
}
// ── 滚屏开关 ─────────────────────────────────────
function toggleAutoScroll() {
autoScroll = !autoScroll;
const cb = document.getElementById('auto_scroll');
if (cb) cb.checked = autoScroll;
const statusEl = document.getElementById('scroll-status');
if (statusEl) statusEl.textContent = autoScroll ? '开' : '关';
}
// ── 退出房间 ─────────────────────────────────────
async function leaveRoom() {
try {
await fetch(window.chatContext.leaveUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content'),
'Accept': 'application/json'
}
});
} catch (e) {
console.error(e);
}
// 弹出窗口直接关闭,如果不是弹出窗口则跳回首页
window.close();
setTimeout(() => {
window.location.href = '/';
}, 500);
}
// ── 掉线检测计数器 ──
let heartbeatFailCount = 0;
const MAX_HEARTBEAT_FAILS = 3;
// ── 存点功能(手动 + 自动)─────────────────────
async function saveExp(silent = false) {
if (!window.chatContext || !window.chatContext.heartbeatUrl) return;
try {
const response = await fetch(window.chatContext.heartbeatUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')
.getAttribute('content'),
'Accept': 'application/json'
}
});
// 检测登录态失效
if (response.status === 401 || response.status === 419) {
alert('⚠️ 您的登录已失效(可能超时或在其他设备登录),请重新登录。');
window.location.href = '/';
return;
}
const data = await response.json();
if (response.ok && data.status === 'success') {
heartbeatFailCount = 0;
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
const d = data.data;
const levelTitle = d.user_level >= 10 ? '管理员' : (d.user_level >= 5 ? '高级会员' : (d
.user_level >= 3 ?
'三级会员' : '普通会员'));
let levelInfo = '';
if (d.is_max_level) {
levelInfo = `级别(${d.user_level});累积经验(${d.exp_num});已满级。`;
} else {
const requiredExp = d.user_level * d.user_level * 10;
const remaining = Math.max(0, requiredExp - d.exp_num);
levelInfo = `级别(${d.user_level});累积经验(${d.exp_num});还有(${remaining})升级。`;
}
if (data.data.leveled_up) {
const upDiv = document.createElement('div');
upDiv.className = 'msg-line';
upDiv.innerHTML =
`<span style="color: #d97706; font-weight: bold;">【系统】恭喜!你的经验值已达 ${d.exp_num},等级突破至 LV.${d.user_level}!🌟</span><span class="msg-time">(${timeStr})</span>`;
container2.appendChild(upDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
}
if (!silent) {
const detailDiv = document.createElement('div');
detailDiv.className = 'msg-line';
detailDiv.innerHTML =
`<span style="color: green;">【${levelTitle}存点】您的最新情况:${levelInfo}</span><span class="msg-time">(${timeStr})</span>`;
container2.appendChild(detailDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
} else {
return;
}
}
} catch (e) {
console.error('存点失败', e);
heartbeatFailCount++;
if (heartbeatFailCount >= MAX_HEARTBEAT_FAILS) {
alert('⚠️ 与服务器的连接已断开,请检查网络后重新登录。');
window.location.href = '/';
return;
}
if (!silent) {
const sysDiv = document.createElement('div');
sysDiv.className = 'msg-line';
sysDiv.innerHTML = `<span style="color: red;">【系统】存点失败,请稍后重试</span>`;
container2.appendChild(sysDiv);
}
}
}
// ── 自动存点心跳每60秒自动存一次───────────
const HEARTBEAT_INTERVAL = 60 * 1000;
setInterval(() => saveExp(true), HEARTBEAT_INTERVAL);
setTimeout(() => saveExp(true), 10000);
// ── 头像选择器(原版 fw.asp 功能)───────────────
let avatarPickerLoaded = false;
/**
* 打开头像选择弹窗
*/
function openAvatarPicker() {
const modal = document.getElementById('avatar-picker-modal');
modal.style.display = 'flex';
if (!avatarPickerLoaded) {
loadHeadfaces();
avatarPickerLoaded = true;
}
}
/**
* 关闭头像选择弹窗
*/
function closeAvatarPicker() {
document.getElementById('avatar-picker-modal').style.display = 'none';
}
/**
* 加载头像列表
*/
async function loadHeadfaces() {
const grid = document.getElementById('avatar-grid');
grid.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
try {
const res = await fetch('/headface/list');
const data = await res.json();
grid.innerHTML = '';
data.headfaces.forEach(file => {
const img = document.createElement('img');
img.src = '/images/headface/' + file;
img.className = 'avatar-option';
img.title = file;
img.dataset.file = file;
img.onerror = () => img.style.display = 'none';
img.onclick = () => selectAvatar(file, img);
grid.appendChild(img);
});
} catch (e) {
grid.innerHTML = '<div style="text-align:center;padding:20px;color:red;">加载失败</div>';
}
}
/**
* 选中一个头像
*/
function selectAvatar(file, imgEl) {
document.querySelectorAll('.avatar-option.selected').forEach(el => el.classList.remove('selected'));
imgEl.classList.add('selected');
document.getElementById('avatar-preview').src = '/images/headface/' + file;
document.getElementById('avatar-selected-name').textContent = file;
document.getElementById('avatar-save-btn').disabled = false;
document.getElementById('avatar-save-btn').dataset.file = file;
}
/**
* 保存选中的头像
*/
async function saveAvatar() {
const btn = document.getElementById('avatar-save-btn');
const file = btn.dataset.file;
if (!file) return;
btn.disabled = true;
btn.textContent = '保存中...';
try {
const res = await fetch('/headface/change', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
},
body: JSON.stringify({
headface: file
})
});
const data = await res.json();
if (data.status === 'success') {
alert('头像修改成功!');
const myName = window.chatContext.username;
if (onlineUsers[myName]) {
onlineUsers[myName].headface = data.headface;
}
renderUserList();
closeAvatarPicker();
} else {
alert(data.message || '修改失败');
}
} catch (e) {
alert('网络错误');
}
btn.disabled = false;
btn.textContent = '确定更换';
}
// ── 钓鱼小游戏(复刻原版 diaoyu/ 功能)─────────────
let fishingTimer = null;
let fishingReelTimeout = null;
/**
* 开始钓鱼 调用抛竿 API花费金币显示等待动画
*/
async function startFishing() {
const btn = document.getElementById('fishing-btn');
btn.disabled = true;
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') {
alert(data.message || '钓鱼失败');
btn.disabled = false;
return;
}
// 在包厢窗口显示抛竿消息
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
const castDiv = document.createElement('div');
castDiv.className = 'msg-line';
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 = '🎣 等待中...';
fishingTimer = setTimeout(() => {
// 鱼上钩了!
const hookDiv = document.createElement('div');
hookDiv.className = 'msg-line';
hookDiv.innerHTML =
'<span style="color: #d97706; font-weight: bold; font-size: 14px;">🐟 鱼上钩了!快点击 [拉竿] 按钮!</span>';
container2.appendChild(hookDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
btn.textContent = '🎣 拉竿!';
btn.disabled = false;
btn.onclick = reelFish;
// 15 秒内不拉竿,鱼跑掉
fishingReelTimeout = setTimeout(() => {
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();
}, 15000);
}, data.wait_time * 1000);
} catch (e) {
alert('网络错误:' + e.message);
btn.disabled = false;
}
}
/**
* 拉竿 调用收竿 API获取随机结果
*/
async function reelFish() {
const btn = document.getElementById('fishing-btn');
btn.disabled = true;
btn.textContent = '🎣 拉竿中...';
// 取消跑鱼计时器
if (fishingReelTimeout) {
clearTimeout(fishingReelTimeout);
fishingReelTimeout = 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'
}
});
const data = await res.json();
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
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);
} 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);
}
if (autoScroll) container2.scrollTop = container2.scrollHeight;
} catch (e) {
alert('网络错误:' + e.message);
}
resetFishingBtn();
}
/**
* 重置钓鱼按钮状态
*/
function resetFishingBtn() {
const btn = document.getElementById('fishing-btn');
btn.textContent = '🎣 钓鱼';
btn.disabled = false;
btn.onclick = startFishing;
fishingTimer = null;
fishingReelTimeout = null;
}
// ── AI 聊天机器人 ──────────────────────────────────
let chatBotSending = false;
/**
* 发送消息给 AI 机器人
* 先在包厢窗口显示用户消息,再调用 API 获取回复
*/
async function sendToChatBot(content) {
if (chatBotSending) {
alert('AI 正在思考中,请稍候...');
return;
}
chatBotSending = true;
// 显示"思考中"提示
// 延迟显示"思考中",让广播消息先到达
const thinkDiv = document.createElement('div');
thinkDiv.className = 'msg-line';
thinkDiv.innerHTML = '<span style="color: #16a34a;">🤖 <b>AI小助手</b> 正在思考中...</span>';
setTimeout(() => {
container2.appendChild(thinkDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
}, 500);
try {
const res = await fetch(window.chatContext.chatBotUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
message: content,
room_id: window.chatContext.roomId
})
});
const data = await res.json();
// 移除"思考中"提示(消息已通过广播显示)
thinkDiv.remove();
if (!res.ok || data.status !== 'success') {
const errDiv = document.createElement('div');
errDiv.className = 'msg-line';
errDiv.innerHTML = `<span style="color: #dc2626;">🤖【AI小助手】${data.message || '回复失败,请稍后重试'}</span>`;
container.appendChild(errDiv);
}
} catch (e) {
thinkDiv.remove();
const errDiv = document.createElement('div');
errDiv.className = 'msg-line';
errDiv.innerHTML = '<span style="color: #dc2626;">🤖【AI小助手】网络连接错误请稍后重试</span>';
container.appendChild(errDiv);
}
chatBotSending = false;
scrollToBottom();
}
/**
* 清除与 AI 小助手的对话上下文
*/
async function clearChatBotContext() {
try {
const res = await fetch(window.chatContext.chatBotClearUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content'),
'Accept': 'application/json'
}
});
const data = await res.json();
const sysDiv = document.createElement('div');
sysDiv.className = 'msg-line';
sysDiv.innerHTML = '<span style="color: #16a34a;">🤖【系统】' + (data.message || '对话已重置') + '</span>';
container2.appendChild(sysDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
} catch (e) {
alert('清除失败:' + e.message);
}
}
/**
* HTML 转义函数,防止 XSS
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>