1353 lines
58 KiB
PHP
1353 lines
58 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) {
|
||
// 所有内容面板(名单/房间/贴图/酷库 用 block,商店用 flex)
|
||
['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);
|
||
});
|
||
// 商店面板单独处理(flex 布局以支持内部滚动)
|
||
const shopPanel = document.getElementById('shop-panel');
|
||
const shopTab = document.getElementById('tab-shop');
|
||
if (shopPanel && shopTab) {
|
||
shopPanel.style.display = tab === 'shop' ? 'flex' : 'none';
|
||
shopPanel.style.flexDirection = 'column';
|
||
shopPanel.style.height = '100%';
|
||
shopTab.classList.toggle('active', tab === 'shop');
|
||
}
|
||
// 贴图 Tab 懒加载:首次切换时将 data-src 赋值到 src
|
||
if (tab === 'emoji') {
|
||
document.querySelectorAll('#panel-emoji img[data-src]').forEach(img => {
|
||
img.src = img.dataset.src;
|
||
img.removeAttribute('data-src');
|
||
});
|
||
}
|
||
}
|
||
|
||
// ── 分屏切换 ──────────────────────────────────────
|
||
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/ai_bot.png" 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').toLowerCase();
|
||
// VIP 图标和管理员标识 (互斥显示:管理员优先)
|
||
let badges = '';
|
||
if (user.is_admin) {
|
||
// 军人专属风格:由于宽度受限,采用代表最高荣誉与权威的“将官军功章”单字符
|
||
badges += `<span style="font-size:12px; margin-left:2px;" title="最高统帅">🎖️</span>`;
|
||
} else 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>`;
|
||
}
|
||
|
||
// 女生名字使用玫粉色
|
||
const nameColor = (user.sex == 2) ? 'color:#e91e8c;' : '';
|
||
item.innerHTML = `
|
||
<img class="user-head" src="/images/headface/${headface}" onerror="this.src='/images/headface/1.gif'">
|
||
<span class="user-name" style="${nameColor}">${username}</span>${badges}
|
||
`;
|
||
|
||
item.onclick = () => {
|
||
toUserSelect.value = username;
|
||
document.getElementById('content').focus();
|
||
};
|
||
// 双击打开用户名片弹窗(全局统一入口)
|
||
item.ondblclick = () => openUserCard(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 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 || '';
|
||
let timeStrOverride = false;
|
||
|
||
// 系统用户名列表(不可被选为聊天对象)
|
||
const systemUsers = ['钓鱼播报', '星海小博士', '系统传音', '系统公告', 'AI小班长', '送花播报', '系统'];
|
||
|
||
// 动作文字映射表:情绪型(着/地,放"对"之前)和动作型(了,替换"对X说")
|
||
const actionTextMap = {
|
||
'微笑': {
|
||
type: 'emotion',
|
||
word: '微笑着'
|
||
},
|
||
'大笑': {
|
||
type: 'emotion',
|
||
word: '大笑着'
|
||
},
|
||
'愤怒': {
|
||
type: 'emotion',
|
||
word: '愤怒地'
|
||
},
|
||
'哭泣': {
|
||
type: 'emotion',
|
||
word: '哭泣着'
|
||
},
|
||
'害羞': {
|
||
type: 'emotion',
|
||
word: '害羞地'
|
||
},
|
||
'鄙视': {
|
||
type: 'emotion',
|
||
word: '鄙视地'
|
||
},
|
||
'得意': {
|
||
type: 'emotion',
|
||
word: '得意地'
|
||
},
|
||
'疑惑': {
|
||
type: 'emotion',
|
||
word: '疑惑地'
|
||
},
|
||
'同情': {
|
||
type: 'emotion',
|
||
word: '同情地'
|
||
},
|
||
'无奈': {
|
||
type: 'emotion',
|
||
word: '无奈地'
|
||
},
|
||
'拳打': {
|
||
type: 'verb',
|
||
word: '拳打了'
|
||
},
|
||
'飞吻': {
|
||
type: 'verb',
|
||
word: '飞吻了'
|
||
},
|
||
'偷看': {
|
||
type: 'verb',
|
||
word: '偷看了'
|
||
},
|
||
};
|
||
// 生成自然语序的动作串:情绪型=[人][着/地]对[目标][verb]:;动作型=[人][了][目标],[verb]:
|
||
const buildActionStr = (action, fromHtml, toHtml, verb = '说') => {
|
||
const info = actionTextMap[action];
|
||
if (!info) return `${fromHtml}对${toHtml}${action}${verb}:`;
|
||
if (info.type === 'emotion') return `${fromHtml}${info.word}对${toHtml}${verb}:`;
|
||
return `${fromHtml}${info.word}${toHtml},${verb}:`;
|
||
};
|
||
// 用户名(单击切换发言对象,双击查看资料;系统用户仅显示文本)
|
||
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="openUserCard('${uName}')">${uName}</span>`;
|
||
};
|
||
|
||
// 系统播报用户(不包含 AI小班长)使用军号图标,AI小班长用专属图,普通用户用头像
|
||
const buggleUsers = ['钓鱼播报', '星海小博士', '送花播报', '系统传音', '系统公告'];
|
||
const senderInfo = onlineUsers[msg.from_user];
|
||
const senderHead = ((senderInfo && senderInfo.headface) || '1.gif').toLowerCase();
|
||
let headImgSrc = `/images/headface/${senderHead}`;
|
||
if (msg.from_user === 'AI小班长') {
|
||
headImgSrc = '/images/ai_bot.png';
|
||
} else if (buggleUsers.includes(msg.from_user)) {
|
||
headImgSrc = '/images/bugle.png';
|
||
}
|
||
const headImg =
|
||
`<img src="${headImgSrc}" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;mix-blend-mode: multiply;" onerror="this.src='/images/headface/1.gif'">`;
|
||
|
||
let html = '';
|
||
|
||
// 系统用户消息以醒目公告样式显示
|
||
if (systemUsers.includes(msg.from_user)) {
|
||
if (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} <span style="font-size: 10px; color: #999; font-weight: normal;">(${timeStr})</span></div>`;
|
||
timeStrOverride = true;
|
||
} else if (msg.from_user === '系统传音') {
|
||
// 自动升级播报:金色左边框,轻量提示样式,不喧宾夺主
|
||
div.style.cssText =
|
||
'background: #fffbeb; border-left: 3px solid #d97706; border-radius: 4px; padding: 4px 10px; margin: 2px 0;';
|
||
html =
|
||
`<span style="color: #b45309; font-size: 12px;">🌟 ${msg.content}</span>`;
|
||
} else if (msg.from_user === '系统' && msg.to_user && msg.to_user !== '大家') {
|
||
// 系统私人通知(自动存点等):无头像,绿色左边框简洁条形样式
|
||
div.style.cssText =
|
||
'background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px;padding:3px 8px;margin:2px 0;font-size:12px;';
|
||
html =
|
||
`<span style="color:#16a34a;font-weight:bold;">📢 系统:</span><span style="color:#15803d;">${msg.content}</span>`;
|
||
} else {
|
||
// 其他系统用户(钓鱼播报、送花播报、AI小班长等):普通样式
|
||
let giftHtml = '';
|
||
if (msg.gift_image) {
|
||
giftHtml =
|
||
`<img src="${msg.gift_image}" alt="${msg.gift_name || ''}" style="display:inline-block;width:40px;height:40px;vertical-align:middle;margin-left:6px;animation:giftBounce 0.6s ease-in-out;">`;
|
||
}
|
||
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>${giftHtml}`;
|
||
}
|
||
} else if (msg.is_secret) {
|
||
if (msg.from_user === '系统') {
|
||
// 系统悄悄通知(自动存点等):绿色左边框条形,无头像,不显示"悄悄说"
|
||
div.style.cssText =
|
||
'background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px;padding:3px 8px;margin:2px 0;font-size:12px;';
|
||
html =
|
||
`<span style="color:#16a34a;font-weight:bold;">📢 系统:</span><span style="color:#15803d;">${msg.content}</span>`;
|
||
} else {
|
||
// 普通悄悄话样式(原版:紫色斜体,使用自然语序动作)
|
||
const fromHtml = clickableUser(msg.from_user, '#cc00cc');
|
||
const toHtml = clickableUser(msg.to_user, '#cc00cc');
|
||
const verbStr = msg.action ?
|
||
buildActionStr(msg.action, fromHtml, toHtml, '悄悄说') :
|
||
`${fromHtml}对${toHtml}悄悄说:`;
|
||
html = `<span class="msg-secret">${headImg}${verbStr}${msg.content}</span>`;
|
||
}
|
||
} else if (msg.to_user && msg.to_user !== '大家') {
|
||
// 对特定对象说话
|
||
const fromHtml = clickableUser(msg.from_user, '#000099');
|
||
const toHtml = clickableUser(msg.to_user, '#000099');
|
||
const verbStr = msg.action ?
|
||
buildActionStr(msg.action, fromHtml, toHtml) :
|
||
`${fromHtml}对${toHtml}说:`;
|
||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
|
||
} else {
|
||
// 对大家说话
|
||
const fromHtml = clickableUser(msg.from_user, '#000099');
|
||
const verbStr = msg.action ?
|
||
buildActionStr(msg.action, fromHtml, '大家') :
|
||
`${fromHtml}对大家说:`;
|
||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
|
||
}
|
||
|
||
if (!timeStrOverride) {
|
||
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();
|
||
|
||
// 管理员自己进房时,在本地播放烟花(服务端广播可能在 WS 连上前已发出)
|
||
const ctx = window.chatContext;
|
||
if (ctx && ctx.userLevel >= ctx.superLevel && typeof EffectManager !== 'undefined') {
|
||
// 延迟 800ms 确保页面渲染完成再播特效
|
||
setTimeout(() => EffectManager.play('fireworks'), 800);
|
||
}
|
||
});
|
||
|
||
window.addEventListener('chat:joining', (e) => {
|
||
const user = e.detail;
|
||
onlineUsers[user.username] = user;
|
||
renderUserList();
|
||
|
||
// 原版风格:完整句式的随机趣味欢迎语
|
||
const gender = user.sex == 2 ? '美女' : '帅哥';
|
||
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 gender = user.sex == 2 ? '美女' : '帅哥';
|
||
const uname = user.username;
|
||
const leaveTemplates = [
|
||
`${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>化作一缕青烟消散在空气中……`,
|
||
];
|
||
const msg = leaveTemplates[Math.floor(Math.random() * leaveTemplates.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: #cc6600">【离开】${msg}</span><span class="msg-time">(${timeStr})</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 isMe = d.username === window.chatContext.username;
|
||
|
||
// 禁言通知:自己被禁言显示在包厢(say2),其他人显示在公聊(say1)
|
||
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>`;
|
||
const targetContainer = isMe ? document.getElementById('say2') : container;
|
||
if (targetContainer) {
|
||
targetContainer.appendChild(div);
|
||
targetContainer.scrollTop = targetContainer.scrollHeight;
|
||
}
|
||
|
||
// 如果是自己被禁言,设置本地禁言计时
|
||
if (isMe && d.mute_time > 0) {
|
||
isMutedUntil = Date.now() + d.mute_time * 60 * 1000;
|
||
const contentInput = document.getElementById('content');
|
||
const operatorName = d.operator || '管理员';
|
||
if (contentInput) {
|
||
contentInput.placeholder = `${operatorName} 已将您禁言 ${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>';
|
||
// 解禁提示也显示在包厢窗口
|
||
const say2 = document.getElementById('say2');
|
||
if (say2) {
|
||
say2.appendChild(unmuteDiv);
|
||
say2.scrollTop = say2.scrollHeight;
|
||
}
|
||
}, d.mute_time * 60 * 1000);
|
||
}
|
||
}
|
||
});
|
||
|
||
window.addEventListener('chat:title-updated', (e) => {
|
||
document.getElementById('room-title-display').innerText = e.detail.title;
|
||
});
|
||
|
||
// ── 管理员全员清屏事件(等待 Echo 就绪后监听) ───────
|
||
function setupScreenClearedListener() {
|
||
if (!window.Echo || !window.chatContext) {
|
||
// Echo 或 chatContext 还没就绪,延迟重试
|
||
setTimeout(setupScreenClearedListener, 500);
|
||
return;
|
||
}
|
||
window.Echo.join(`room.${window.chatContext.roomId}`)
|
||
.listen('ScreenCleared', (e) => {
|
||
console.log('收到全员清屏事件:', e);
|
||
const operator = e.operator;
|
||
|
||
// 清除公聊窗口所有消息
|
||
const say1 = document.getElementById('chat-messages-container');
|
||
if (say1) say1.innerHTML = '';
|
||
|
||
// 清除包厢窗口中非悄悄话的消息
|
||
const say2 = document.getElementById('chat-messages-container2');
|
||
if (say2) {
|
||
const items = say2.querySelectorAll('.msg-line');
|
||
items.forEach(item => {
|
||
// 保留悄悄话消息(含 msg-secret 类)
|
||
if (!item.querySelector('.msg-secret')) {
|
||
item.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 显示清屏提示
|
||
const sysDiv = document.createElement('div');
|
||
sysDiv.className = 'msg-line';
|
||
const now = new Date();
|
||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||
now.getSeconds().toString().padStart(2, '0');
|
||
sysDiv.innerHTML =
|
||
`<span style="color: #dc2626; font-weight: bold;">🧹 管理员 <b>${operator}</b> 已执行全员清屏</span><span class="msg-time">(${timeStr})</span>`;
|
||
if (say1) {
|
||
say1.appendChild(sysDiv);
|
||
say1.scrollTop = say1.scrollHeight;
|
||
}
|
||
});
|
||
console.log('ScreenCleared 监听器已注册');
|
||
}
|
||
// DOMContentLoaded 后启动,确保 Vite 编译的 JS 已加载
|
||
document.addEventListener('DOMContentLoaded', setupScreenClearedListener);
|
||
|
||
// ── 全屏特效事件监听(烟花/下雨/雷电)───────────────
|
||
window.addEventListener('chat:effect', (e) => {
|
||
const type = e.detail?.type;
|
||
if (type && typeof EffectManager !== 'undefined') {
|
||
EffectManager.play(type);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 管理员点击特效按钮,向后端 POST /command/effect
|
||
*
|
||
* @param {string} type 特效类型:fireworks / rain / lightning
|
||
*/
|
||
function triggerEffect(type) {
|
||
const roomId = window.chatContext?.roomId;
|
||
if (!roomId) return;
|
||
fetch('/command/effect', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
|
||
},
|
||
body: JSON.stringify({
|
||
room_id: roomId,
|
||
type
|
||
}),
|
||
}).then(r => r.json()).then(data => {
|
||
if (data.status !== 'success') alert(data.message);
|
||
}).catch(err => console.error('特效触发失败:', err));
|
||
}
|
||
window.triggerEffect = triggerEffect;
|
||
|
||
// ── 字号设置(持久化到 localStorage)─────────────────
|
||
/**
|
||
* 应用字号到聊天消息窗口,并保存到 localStorage
|
||
*
|
||
* @param {string|number} size 字号大小(px 数字)
|
||
*/
|
||
function applyFontSize(size) {
|
||
const px = parseInt(size, 10);
|
||
if (isNaN(px) || px < 10 || px > 30) return;
|
||
|
||
// 同时应用到公聊窗和包厢窗
|
||
const c1 = document.getElementById('chat-messages-container');
|
||
const c2 = document.getElementById('chat-messages-container2');
|
||
if (c1) c1.style.fontSize = px + 'px';
|
||
if (c2) c2.style.fontSize = px + 'px';
|
||
|
||
// 持久化(key 带房间 ID,不同房间各自记住)
|
||
const key = 'chat_font_size';
|
||
localStorage.setItem(key, px);
|
||
|
||
// 同步 select 显示
|
||
const sel = document.getElementById('font_size_select');
|
||
if (sel) sel.value = String(px);
|
||
}
|
||
window.applyFontSize = applyFontSize;
|
||
|
||
// 页面加载后从 localStorage 恢复之前保存的字号
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const saved = localStorage.getItem('chat_font_size');
|
||
if (saved) {
|
||
applyFontSize(saved);
|
||
}
|
||
});
|
||
|
||
// ── 发送消息(Enter 发送,防 IME 输入法重复触发)────────
|
||
// 用 isComposing 标记中文输入法的组词状态,组词期间过滤掉 Enter
|
||
let _imeComposing = false;
|
||
const _contentInput = document.getElementById('content');
|
||
|
||
// 中文/日文等 IME 组词开始
|
||
_contentInput.addEventListener('compositionstart', () => {
|
||
_imeComposing = true;
|
||
});
|
||
// 组词结束(确认候选词完成),给 10ms 缓冲让 keydown 先被过滤掉
|
||
_contentInput.addEventListener('compositionend', () => {
|
||
setTimeout(() => {
|
||
_imeComposing = false;
|
||
}, 10);
|
||
});
|
||
|
||
_contentInput.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
// IME 正在组词时(如选候选汉字),不触发发送
|
||
if (_imeComposing) return;
|
||
sendMessage(e);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 发送聊天消息(内带防重入锁,避免快速连按 Enter 重复提交)
|
||
*/
|
||
let _isSending = false; // 发送中防重入标记
|
||
async function sendMessage(e) {
|
||
if (e) e.preventDefault();
|
||
if (_isSending) return; // 上一次还没结束,忽略
|
||
_isSending = true;
|
||
|
||
// 前端禁言检查
|
||
if (isMutedUntil > Date.now()) {
|
||
const remaining = Math.ceil((isMutedUntil - Date.now()) / 1000);
|
||
const remainMin = Math.ceil(remaining / 60);
|
||
// 在聊天窗口显示持久提示,避免弹窗消失太快
|
||
const muteDiv = document.createElement('div');
|
||
muteDiv.className = 'msg-line';
|
||
muteDiv.innerHTML =
|
||
`<span style="color: #dc2626; font-weight: bold;">【提示】您正在禁言中,还需等待约 ${remainMin} 分钟(${remaining} 秒)后方可发言。</span>`;
|
||
const container2 = document.getElementById('say2');
|
||
if (container2) {
|
||
container2.appendChild(muteDiv);
|
||
container2.scrollTop = container2.scrollHeight;
|
||
}
|
||
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;
|
||
_isSending = false; // 释放发送锁,允许下次发送
|
||
}
|
||
}
|
||
|
||
|
||
// ── 设置房间公告 ─────────────────────────────────────
|
||
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);
|
||
}
|
||
}
|
||
|
||
// ── 管理员全员清屏 ─────────────────────────────────────
|
||
async function adminClearScreen() {
|
||
if (!confirm('确定要清除所有人的聊天记录吗?(悄悄话将保留)')) return;
|
||
|
||
try {
|
||
const res = await fetch('/command/clear-screen', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||
'content'),
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
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.title || '普通会员';
|
||
|
||
let levelInfo = '';
|
||
if (d.is_max_level) {
|
||
levelInfo = `级别(${d.user_level});经验(${d.exp_num});金币(${d.jjb}枚);已满级。`;
|
||
} else {
|
||
levelInfo = `级别(${d.user_level});经验(${d.exp_num});金币(${d.jjb}枚)。`;
|
||
}
|
||
|
||
// 本次获得的奖励提示
|
||
let gainInfo = '';
|
||
if (d.exp_gain > 0 || d.jjb_gain > 0) {
|
||
const parts = [];
|
||
if (d.exp_gain > 0) parts.push(`经验+${d.exp_gain}`);
|
||
if (d.jjb_gain > 0) parts.push(`金币+${d.jjb_gain}`);
|
||
gainInfo = `(本次: ${parts.join(', ')})`;
|
||
}
|
||
|
||
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} ${gainInfo}</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 onclick="reelFish()" style="text-decoration:underline; cursor:pointer; color:#dc2626;">[拉竿]</span> 按钮!</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>
|