- 字体颜色:s_color 改为 varchar,发消息时保存颜色,进入聊天室自动恢复 - 等级体系:maxlevel 15→99,superlevel 16→100,99级经验阶梯(幂次曲线) - 管理权限等级按比例调整:禁言50、踢人60、设公告60、封号80、封IP90 - 钓鱼小游戏:FishingController(抛竿扣金币+收竿随机结果+广播) - 补充6个缺失的 sysparam 参数 + 4个钓鱼参数 - 用户列表点击用户名后自动聚焦输入框 - Pint 格式化
935 lines
38 KiB
PHP
935 lines
38 KiB
PHP
{{--
|
||
文件功能:聊天室前端交互脚本(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);
|
||
|
||
// 获取排序方式
|
||
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';
|
||
|
||
item.innerHTML = `
|
||
<img class="user-head" src="/images/headface/${headface}" onerror="this.src='/images/headface/1.GIF'">
|
||
<span class="user-name">${username}</span>
|
||
`;
|
||
|
||
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 clickableUser = (uName, color) =>
|
||
`<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 (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';
|
||
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';
|
||
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("您已被管理员踢出房间!");
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// ── 滚屏开关 ─────────────────────────────────────
|
||
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;
|
||
}
|
||
</script>
|