功能:VIP 赞助会员系统

- 新建 vip_levels 表(名称、图标、颜色、经验/金币倍率、专属进入/离开模板)
- 默认4个等级种子:白银🥈(×1.5)、黄金🥇(×2.0)、钻石💎(×3.0)、至尊👑(×5.0)
- 后台 VIP 等级 CRUD 管理(新增/编辑/删除,配置模板和倍率)
- 后台用户编辑弹窗支持设置 VIP 等级和到期时间
- ChatController 心跳经验按 VIP 倍率加成
- FishingController 正向奖励按 VIP 倍率加成(负面惩罚不变)
- 在线名单显示 VIP 图标和管理员🛡️标识
- VIP 用户进入/离开使用专属颜色和标题
- 后台侧栏新增「👑 VIP 会员等级」入口
This commit is contained in:
2026-02-26 21:30:07 +08:00
parent ea06328885
commit fd3214eaff
22 changed files with 2293 additions and 19 deletions
+188 -4
View File
@@ -84,6 +84,27 @@
};
userList.appendChild(allDiv);
// ── AI 小助手(仅当全局开关开启时显示)──
if (window.chatContext.chatBotEnabled) {
let botDiv = document.createElement('div');
botDiv.className = 'user-item';
botDiv.style.background = 'linear-gradient(135deg, #e0f2fe, #f0fdf4)';
botDiv.style.borderLeft = '3px solid #22c55e';
botDiv.innerHTML = '<span style="font-size:14px; margin-right:3px;">🤖</span>' +
'<span class="user-name" style="color: #16a34a; font-weight: bold;">AI小助手</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';
@@ -112,10 +133,20 @@
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>
<span class="user-name">${username}</span>${badges}
`;
item.onclick = () => {
@@ -330,8 +361,16 @@
const sysDiv = document.createElement('div');
sysDiv.className = 'msg-line';
sysDiv.innerHTML =
`<span style="color: green">【欢迎】${msg}</span><span class="msg-time">(${timeStr})</span>`;
// 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();
});
@@ -343,7 +382,14 @@
const sysDiv = document.createElement('div');
sysDiv.className = 'msg-line sys-msg';
sysDiv.innerHTML = `<span style="color: gray">☆ ${user.username} 离开了聊天室 ☆</span>`;
// 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();
});
@@ -441,6 +487,15 @@
return;
}
// 如果发言对象是 AI 小助手,走专用机器人 API
const toUser = formData.get('to_user');
if (toUser === 'AI小助手') {
contentInput.value = '';
contentInput.focus();
await sendToChatBot(content);
return;
}
submitBtn.disabled = true;
try {
@@ -931,4 +986,133 @@
fishingTimer = null;
fishingReelTimeout = null;
}
// ── AI 聊天机器人 ──────────────────────────────────
let chatBotSending = false;
/**
* 发送消息给 AI 机器人
* 先在包厢窗口显示用户消息,再调用 API 获取回复
*/
async function sendToChatBot(content) {
if (chatBotSending) {
alert('AI 正在思考中,请稍候...');
return;
}
chatBotSending = true;
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 userDiv = document.createElement('div');
userDiv.className = 'msg-line';
userDiv.innerHTML = `<span class="msg-user" style="color: #000099;">${window.chatContext.username}</span>` +
`对<span style="color: #16a34a; font-weight: bold;">🤖AI小助手</span>说:` +
`<span class="msg-content">${escapeHtml(content)}</span>` +
` <span class="msg-time">(${timeStr})</span>`;
container2.appendChild(userDiv);
// 显示"思考中"提示
const thinkDiv = document.createElement('div');
thinkDiv.className = 'msg-line';
thinkDiv.innerHTML = '<span style="color: #16a34a;">🤖 <b>AI小助手</b> 正在思考中...</span>';
container2.appendChild(thinkDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
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();
const replyTime = new Date();
const replyTimeStr = replyTime.getHours().toString().padStart(2, '0') + ':' +
replyTime.getMinutes().toString().padStart(2, '0') + ':' +
replyTime.getSeconds().toString().padStart(2, '0');
if (res.ok && data.status === 'success') {
// 显示机器人回复
const botDiv = document.createElement('div');
botDiv.className = 'msg-line';
botDiv.style.background = '#f0fdf4';
botDiv.style.borderLeft = '3px solid #22c55e';
botDiv.style.padding = '4px 8px';
botDiv.style.margin = '2px 0';
botDiv.style.borderRadius = '4px';
botDiv.innerHTML = `<span style="color: #16a34a; font-weight: bold;">🤖 AI小助手</span>` +
`对<span class="msg-user" style="color: #000099;">${window.chatContext.username}</span>说:` +
`<span class="msg-content" style="color: #333;">${escapeHtml(data.reply)}</span>` +
` <span class="msg-time">(${replyTimeStr})</span>` +
` <span style="font-size:10px; color:#aaa;">[${data.provider}]</span>`;
container2.appendChild(botDiv);
} else {
// 显示错误信息
const errDiv = document.createElement('div');
errDiv.className = 'msg-line';
errDiv.innerHTML = `<span style="color: #dc2626;">🤖【AI小助手】${data.message || '回复失败,请稍后重试'}</span>` +
` <span class="msg-time">(${replyTimeStr})</span>`;
container2.appendChild(errDiv);
}
} catch (e) {
thinkDiv.remove();
const errDiv = document.createElement('div');
errDiv.className = 'msg-line';
errDiv.innerHTML = '<span style="color: #dc2626;">🤖【AI小助手】网络连接错误,请稍后重试</span>';
container2.appendChild(errDiv);
}
chatBotSending = false;
if (autoScroll) container2.scrollTop = container2.scrollHeight;
}
/**
* 清除与 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>