功能:管理员全员清屏 + 离开提示趣味风格

- 新增 ScreenCleared 广播事件
- AdminCommandController 添加 clearScreen 方法(站长权限)
- ChatStateService 添加 clearMessages 方法
- chat.js 添加 ScreenCleared Echo 监听
- 前端:全员清屏按钮(红色🧹)+ 清屏处理逻辑(保留悄悄话)
- 离开提示改为与进入一致的趣味随机语风格(橙色【离开】标签)
This commit is contained in:
2026-02-26 23:05:56 +08:00
parent b80a74655c
commit 66f68bab85
7 changed files with 201 additions and 4 deletions

View File

@@ -0,0 +1,59 @@
<?php
/**
* 文件功能:管理员全员清屏广播事件
*
* 管理员触发清屏后,广播给房间内所有用户,前端监听后清除聊天记录(悄悄话除外)。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ScreenCleared implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* 构造函数
*
* @param int $roomId 房间ID
* @param string $operator 执行清屏的管理员用户名
*/
public function __construct(
public readonly int $roomId,
public readonly string $operator,
) {}
/**
* 广播频道
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PresenceChannel('room.'.$this->roomId),
];
}
/**
* 广播数据
*
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return [
'operator' => $this->operator,
];
}
}

View File

@@ -323,6 +323,39 @@ class AdminCommandController extends Controller
return response()->json(['status' => 'success', 'message' => '公告已发送']);
}
/**
* 管理员全员清屏
*
* 清除 Redis 中该房间的聊天记录缓存,并广播清屏事件通知所有用户前端清除消息。
* 前端只清除普通消息,保留悄悄话。
*
* @param Request $request 请求对象,需包含 room_id
* @return JsonResponse 操作结果
*/
public function clearScreen(Request $request): JsonResponse
{
$request->validate([
'room_id' => 'required|integer',
]);
$admin = Auth::user();
$roomId = $request->input('room_id');
$superLevel = (int) Sysparam::getValue('superlevel', '100');
// 需要站长权限才能全员清屏
if ($admin->user_level < $superLevel) {
return response()->json(['status' => 'error', 'message' => '仅站长可执行全员清屏'], 403);
}
// 清除 Redis 中该房间的消息缓存
$this->chatState->clearMessages($roomId);
// 广播清屏事件
broadcast(new \App\Events\ScreenCleared($roomId, $admin->username));
return response()->json(['status' => 'success', 'message' => '已执行全员清屏']);
}
/**
* 权限检查:管理员是否可对目标用户执行指定操作
*

View File

@@ -104,6 +104,17 @@ class ChatStateService
Redis::ltrim($key, -$maxKeep, -1);
}
/**
* 清除指定房间的所有消息缓存(管理员全员清屏)。
*
* @param int $roomId 房间ID
*/
public function clearMessages(int $roomId): void
{
$key = "room:{$roomId}:messages";
Redis::del($key);
}
/**
* 获取指定房间的新发言记录。
* 在高频长轮询或前端断线重连拉取时使用。

View File

@@ -56,6 +56,13 @@ export function initChat(roomId) {
window.dispatchEvent(
new CustomEvent("chat:title-updated", { detail: e }),
);
})
// 监听管理员全员清屏
.listen("ScreenCleared", (e) => {
console.log("全员清屏:", e);
window.dispatchEvent(
new CustomEvent("chat:screen-cleared", { detail: e }),
);
});
}

View File

@@ -95,6 +95,9 @@
<button type="button" onclick="promptAnnounceMessage()"
style="font-size: 11px; padding: 1px 6px; background: #7c3aed; color: #fff; border: none; border-radius: 2px; cursor: pointer;">📢
公屏</button>
<button type="button" onclick="adminClearScreen()"
style="font-size: 11px; padding: 1px 6px; background: #dc2626; color: #fff; border: none; border-radius: 2px; cursor: pointer;">🧹
清屏</button>
@endif
<button type="button" onclick="location.reload()"

View File

@@ -400,15 +400,37 @@
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 sys-msg';
// VIP 用户离开也带专属颜色
sysDiv.className = 'msg-line';
// 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>`;
`<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: gray">☆ ${user.username} 离开了聊天室 ☆</span>`;
sysDiv.innerHTML =
`<span style="color: #cc6600">【离开】${msg}</span><span class="msg-time">(${timeStr})</span>`;
}
container.appendChild(sysDiv);
scrollToBottom();
@@ -475,6 +497,41 @@
document.getElementById('room-title-display').innerText = e.detail.title;
});
// ── 管理员全员清屏事件 ───────────────────────
window.addEventListener('chat:screen-cleared', (e) => {
const operator = e.detail.operator;
// 清除公聊窗口say1所有消息
const say1 = document.getElementById('say');
if (say1) say1.innerHTML = '';
// 清除包厢窗口say2中非悄悄话的消息
const say2 = document.getElementById('say2');
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;
}
});
// ── 发送消息Enter 发送) ───────────────────────
document.getElementById('content').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -670,6 +727,32 @@
}
}
// ── 管理员全员清屏 ─────────────────────────────────────
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;

View File

@@ -93,6 +93,7 @@ Route::middleware(['chat.auth'])->group(function () {
Route::post('/command/freeze', [AdminCommandController::class, 'freeze'])->name('command.freeze');
Route::get('/command/whispers/{username}', [AdminCommandController::class, 'viewWhispers'])->name('command.whispers');
Route::post('/command/announce', [AdminCommandController::class, 'announce'])->name('command.announce');
Route::post('/command/clear-screen', [AdminCommandController::class, 'clearScreen'])->name('command.clear_screen');
});
// 强力特权层中间件:同时验证 chat.auth 登录态 和 chat.level:super 特权superlevel 由 sysparam 配置)