新增:管理员命令系统(警告/踢出/禁言/冻结/查看私信/站长公屏)

- 新建 AdminCommandController 处理6个管理操作命令
- 注册管理员命令路由 /command/*
- 更新 UserKicked 事件增加原因字段
- 更新 UserMuted 事件支持自定义提示消息
- 重构用户名片弹窗管理面板:警告/踢出/禁言/冻结按钮
- 站长专属:查看私信记录、📢公屏讲话按钮
- 被踢出时显示踢出原因
This commit is contained in:
2026-02-26 22:27:49 +08:00
parent f5d8a593c9
commit 14c4effefa
7 changed files with 595 additions and 53 deletions

View File

@@ -3,6 +3,8 @@
/**
* 文件功能:用户被踢出房间广播事件
*
* 管理员踢出/冻结用户时触发,前端监听后强制该用户跳转至大厅。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
@@ -21,18 +23,20 @@ class UserKicked implements ShouldBroadcast
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
* 构造函数
*
* @param int $roomId 房间ID
* @param string $username 被踢出的用户昵称
* @param string $reason 踢出原因
*/
public function __construct(
public readonly int $roomId,
public readonly string $username,
public readonly string $reason = '',
) {}
/**
* Get the channels the event should broadcast on.
* 广播频道
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
@@ -44,7 +48,7 @@ class UserKicked implements ShouldBroadcast
}
/**
* 获取广播时的数据
* 广播数据
*
* @return array<string, mixed>
*/
@@ -52,7 +56,7 @@ class UserKicked implements ShouldBroadcast
{
return [
'username' => $this->username,
'message' => "用户 [{$this->username}] 已被踢出聊天室。",
'reason' => $this->reason,
];
}
}

View File

@@ -31,6 +31,7 @@ class UserMuted implements ShouldBroadcast
public readonly int $roomId,
public readonly string $username,
public readonly int $muteTime,
public readonly string $message = '',
) {}
/**
@@ -52,9 +53,9 @@ class UserMuted implements ShouldBroadcast
*/
public function broadcastWith(): array
{
$statusMessage = $this->muteTime > 0
? "用户 [{$this->username}] 已被系统封口 {$this->muteTime} 次发言时间"
: "用户 [{$this->username}] 已被解除封口";
$statusMessage = $this->message ?: ($this->muteTime > 0
? "用户 [{$this->username}] 已被禁言 {$this->muteTime} 分钟"
: "用户 [{$this->username}] 已被解除禁言");
return [
'username' => $this->username,

View File

@@ -0,0 +1,355 @@
<?php
/**
* 文件功能:管理员聊天室实时命令控制器
*
* 提供管理员在聊天室内对用户执行的管理操作:
* 警告(=J)、踢出(=T)、禁言(=B)、冻结(=Y)、查看私信(=S)、站长公屏讲话。
*
* 对应原 ASP 文件DOUSER.ASP / KILLUSER.ASP / LOCKIP.ASP / NEWSAY.ASP
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Jobs\SaveMessageJob;
use App\Models\Message;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\ChatStateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redis;
class AdminCommandController extends Controller
{
/**
* 构造函数:注入聊天状态服务
*/
public function __construct(
private readonly ChatStateService $chatState,
) {}
/**
* 警告用户(=J 理由)
*
* 向目标用户发送系统警告消息,所有人可见。
*
* @param Request $request 请求对象,需包含 username, room_id, reason
* @return JsonResponse 操作结果
*/
public function warn(Request $request): JsonResponse
{
$request->validate([
'username' => 'required|string',
'room_id' => 'required|integer',
'reason' => 'nullable|string|max:200',
]);
$admin = Auth::user();
$targetUsername = $request->input('username');
$roomId = $request->input('room_id');
$reason = $request->input('reason', '请注意言行');
// 权限检查
if (! $this->canManage($admin, $targetUsername)) {
return response()->json(['status' => 'error', 'message' => '权限不足'], 403);
}
// 广播警告消息
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => "⚠️ 管理员 <b>{$admin->username}</b> 警告 <b>{$targetUsername}</b>{$reason}",
'is_secret' => false,
'font_color' => '#dc2626',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
return response()->json(['status' => 'success', 'message' => "已警告 {$targetUsername}"]);
}
/**
* 踢出用户(=T 理由)
*
* 将目标用户从聊天室踢出,清除其 Redis 在线状态。
*
* @param Request $request 请求对象,需包含 username, room_id, reason
* @return JsonResponse 操作结果
*/
public function kick(Request $request): JsonResponse
{
$request->validate([
'username' => 'required|string',
'room_id' => 'required|integer',
'reason' => 'nullable|string|max:200',
]);
$admin = Auth::user();
$targetUsername = $request->input('username');
$roomId = $request->input('room_id');
$reason = $request->input('reason', '违反聊天室规则');
if (! $this->canManage($admin, $targetUsername)) {
return response()->json(['status' => 'error', 'message' => '权限不足'], 403);
}
// 从 Redis 在线列表移除
$this->chatState->userLeave($roomId, $targetUsername);
// 广播踢出消息(通知前端强制该用户跳转)
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => "🚫 管理员 <b>{$admin->username}</b> 已将 <b>{$targetUsername}</b> 踢出聊天室。原因:{$reason}",
'is_secret' => false,
'font_color' => '#dc2626',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
// 广播踢出事件(前端监听后强制跳转)
broadcast(new \App\Events\UserKicked($roomId, $targetUsername, $reason));
return response()->json(['status' => 'success', 'message' => "已踢出 {$targetUsername}"]);
}
/**
* 禁言用户(=B 分钟数)
*
* 使用 Redis TTL 自动过期机制,禁止用户发言指定分钟数。
*
* @param Request $request 请求对象,需包含 username, room_id, duration
* @return JsonResponse 操作结果
*/
public function mute(Request $request): JsonResponse
{
$request->validate([
'username' => 'required|string',
'room_id' => 'required|integer',
'duration' => 'required|integer|min:1|max:1440',
]);
$admin = Auth::user();
$targetUsername = $request->input('username');
$roomId = $request->input('room_id');
$duration = $request->input('duration');
if (! $this->canManage($admin, $targetUsername)) {
return response()->json(['status' => 'error', 'message' => '权限不足'], 403);
}
// 设置 Redis 禁言标记TTL 自动过期
$muteKey = "mute:{$roomId}:{$targetUsername}";
Redis::setex($muteKey, $duration * 60, now()->toDateTimeString());
// 广播禁言消息
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => "🔇 管理员 <b>{$admin->username}</b> 已将 <b>{$targetUsername}</b> 禁言 {$duration} 分钟。",
'is_secret' => false,
'font_color' => '#dc2626',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
// 广播禁言事件(前端禁用输入框)
broadcast(new \App\Events\UserMuted($roomId, $targetUsername, $duration, "管理员 {$admin->username} 已将您禁言 {$duration} 分钟"));
return response()->json(['status' => 'success', 'message' => "已禁言 {$targetUsername} {$duration} 分钟"]);
}
/**
* 冻结用户账号(=Y 理由)
*
* 将用户账号状态设为冻结,禁止登录。
*
* @param Request $request 请求对象,需包含 username, reason
* @return JsonResponse 操作结果
*/
public function freeze(Request $request): JsonResponse
{
$request->validate([
'username' => 'required|string',
'room_id' => 'required|integer',
'reason' => 'nullable|string|max:200',
]);
$admin = Auth::user();
$targetUsername = $request->input('username');
$roomId = $request->input('room_id');
$reason = $request->input('reason', '违反聊天室规则');
if (! $this->canManage($admin, $targetUsername)) {
return response()->json(['status' => 'error', 'message' => '权限不足'], 403);
}
// 冻结用户账号(将等级设为 -1 表示冻结)
$target = User::where('username', $targetUsername)->first();
if (! $target) {
return response()->json(['status' => 'error', 'message' => '用户不存在'], 404);
}
$target->user_level = -1;
$target->save();
// 从所有房间移除
$rooms = $this->chatState->getUserRooms($targetUsername);
foreach ($rooms as $rid) {
$this->chatState->userLeave($rid, $targetUsername);
}
// 广播冻结消息
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => "🧊 管理员 <b>{$admin->username}</b> 已冻结 <b>{$targetUsername}</b> 的账号。原因:{$reason}",
'is_secret' => false,
'font_color' => '#dc2626',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
// 广播踢出事件
broadcast(new \App\Events\UserKicked($roomId, $targetUsername, "账号已被冻结:{$reason}"));
return response()->json(['status' => 'success', 'message' => "已冻结 {$targetUsername} 的账号"]);
}
/**
* 查看用户私信(=S
*
* 管理员查看指定用户最近的悄悄话记录。
*
* @param string $username 目标用户名
* @return JsonResponse 私信记录列表
*/
public function viewWhispers(string $username): JsonResponse
{
$admin = Auth::user();
$superLevel = (int) Sysparam::getValue('superlevel', '100');
if ($admin->user_level < $superLevel) {
return response()->json(['status' => 'error', 'message' => '仅站长可查看私信'], 403);
}
// 查询最近 50 条悄悄话(发送或接收)
$messages = Message::where('is_secret', true)
->where(function ($q) use ($username) {
$q->where('from_user', $username)
->orWhere('to_user', $username);
})
->orderByDesc('id')
->limit(50)
->get(['id', 'from_user', 'to_user', 'content', 'sent_at']);
return response()->json([
'status' => 'success',
'username' => $username,
'messages' => $messages,
]);
}
/**
* 站长公屏讲话
*
* 站长发送全聊天室公告,以特殊样式显示。
*
* @param Request $request 请求对象,需包含 content, room_id
* @return JsonResponse 操作结果
*/
public function announce(Request $request): JsonResponse
{
$request->validate([
'content' => 'required|string|max:500',
'room_id' => 'required|integer',
]);
$admin = Auth::user();
$superLevel = (int) Sysparam::getValue('superlevel', '100');
if ($admin->user_level < $superLevel) {
return response()->json(['status' => 'error', 'message' => '仅站长可发布公屏讲话'], 403);
}
$roomId = $request->input('room_id');
$content = $request->input('content');
// 广播站长公告
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统公告',
'to_user' => '大家',
'content' => "📢 站长 <b>{$admin->username}</b> 讲话:{$content}",
'is_secret' => false,
'font_color' => '#b91c1c',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
return response()->json(['status' => 'success', 'message' => '公告已发送']);
}
/**
* 权限检查:管理员是否可管理目标用户
*
* 管理员等级必须高于目标用户等级,且不能操作自己。
*
* @param User $admin 管理员用户
* @param string $targetUsername 目标用户名
* @return bool 是否有权限
*/
private function canManage(User $admin, string $targetUsername): bool
{
$superLevel = (int) Sysparam::getValue('superlevel', '100');
// 必须是管理员(达到踢人等级)
$kickLevel = (int) Sysparam::getValue('level_kick', '5');
if ($admin->user_level < $kickLevel) {
return false;
}
// 不能操作自己
if ($admin->username === $targetUsername) {
return false;
}
// 目标用户等级必须低于操作者
$target = User::where('username', $targetUsername)->first();
if ($target && $target->user_level >= $admin->user_level) {
return false;
}
return true;
}
}

View File

@@ -94,44 +94,30 @@
userInfo: {},
isMuting: false,
muteDuration: 5,
showWhispers: false,
whisperList: [],
showAnnounce: false,
announceText: '',
async fetchUser(username) {
try {
const res = await fetch('/user/' + encodeURIComponent(username));
this.userInfo = await res.json();
this.showUserModal = true;
this.isMuting = false;
} catch (e) {
alert('获取资料失败');
}
const data = await res.json();
if (data.status === 'success') {
this.userInfo = data.data;
this.showUserModal = true;
this.isMuting = false;
this.showWhispers = false;
this.whisperList = [];
}
} catch (e) { console.error(e); }
},
async kickUser() {
if (!confirm('确定要将 ' + this.userInfo.username + ' 踢出房间吗?')) return;
const reason = prompt('踢出原因(可留空):', '违反聊天室规则');
if (reason === null) return;
try {
const res = await fetch('/user/' + encodeURIComponent(this.userInfo.username) + '/kick', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ room_id: window.chatContext.roomId })
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) {
alert('网络异常');
}
},
async muteUser() {
try {
const res = await fetch('/user/' + encodeURIComponent(this.userInfo.username) + '/mute', {
const res = await fetch('/command/kick', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
@@ -139,20 +125,133 @@
'Accept': 'application/json'
},
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
reason: reason || '违反聊天室规则'
})
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) { alert('网络异常'); }
},
async muteUser() {
try {
const res = await fetch('/command/mute', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
duration: this.muteDuration
})
});
const data = await res.json();
if (data.status === 'success') {
alert(data.message);
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) {
alert('网络异常');
}
} catch (e) { alert('网络异常'); }
},
async warnUser() {
const reason = prompt('警告原因:', '请注意言行');
if (reason === null) return;
try {
const res = await fetch('/command/warn', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
reason: reason || '请注意言行'
})
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) { alert('网络异常'); }
},
async freezeUser() {
if (!confirm('确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!')) return;
const reason = prompt('冻结原因:', '严重违规');
if (reason === null) return;
try {
const res = await fetch('/command/freeze', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: this.userInfo.username,
room_id: window.chatContext.roomId,
reason: reason || '严重违规'
})
});
const data = await res.json();
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
}
} catch (e) { alert('网络异常'); }
},
async loadWhispers() {
try {
const res = await fetch('/command/whispers/' + encodeURIComponent(this.userInfo.username));
const data = await res.json();
if (data.status === 'success') {
this.whisperList = data.messages;
this.showWhispers = true;
} else {
alert(data.message);
}
} catch (e) { alert('网络异常'); }
},
async sendAnnounce() {
if (!this.announceText.trim()) return;
try {
const res = await fetch('/command/announce', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=&quot;csrf-token&quot;]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
content: this.announceText,
room_id: window.chatContext.roomId,
})
});
const data = await res.json();
if (data.status === 'success') {
this.announceText = '';
this.showAnnounce = false;
} else {
alert(data.message);
}
} catch (e) { alert('网络异常'); }
}
}">
<div x-show="showUserModal" style="display: none;" class="modal-overlay"
@@ -185,7 +284,7 @@
<div class="profile-detail" x-text="userInfo.sign || '这家伙很懒,什么也没留下'"></div>
</div>
{{-- 操作按钮 --}}
{{-- 普通操作按钮 --}}
<div class="modal-actions" x-show="userInfo.username !== window.chatContext.username">
<button class="btn-whisper"
x-on:click="document.getElementById('to_user').value = userInfo.username; document.getElementById('content').focus(); showUserModal = false;">
@@ -204,17 +303,55 @@
<div style="padding: 0 16px 12px;"
x-show="userInfo.username !== window.chatContext.username && userInfo.user_level < {{ Auth::user()->user_level }}">
<div style="font-size: 11px; color: #c00; margin-bottom: 6px; font-weight: bold;">管理操作</div>
<div style="display: flex; gap: 8px;">
<button class="btn-kick" style="flex:1; padding: 5px; border-radius: 4px;"
x-on:click="kickUser()">踢出</button>
<button class="btn-mute" style="flex:1; padding: 5px; border-radius: 4px;"
x-on:click="isMuting = !isMuting">禁言</button>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #fef3c7; border: 1px solid #f59e0b; cursor: pointer;"
x-on:click="warnUser()">⚠️ 警告</button>
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #fee2e2; border: 1px solid #ef4444; cursor: pointer;"
x-on:click="kickUser()">🚫 踢出</button>
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #e0e7ff; border: 1px solid #6366f1; cursor: pointer;"
x-on:click="isMuting = !isMuting">🔇 禁言</button>
<button
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #dbeafe; border: 1px solid #3b82f6; cursor: pointer;"
x-on:click="freezeUser()">🧊 冻结</button>
</div>
</div>
<div class="mute-form" x-show="isMuting" style="display: none;">
<input type="number" x-model="muteDuration" min="1" placeholder="分钟">
<span style="font-size: 11px; color: #b86e00;">分钟</span>
<button x-on:click="muteUser()">执行</button>
{{-- 禁言表单 --}}
<div x-show="isMuting" style="display: none; padding: 0 16px 12px;">
<div style="display: flex; gap: 6px; align-items: center;">
<input type="number" x-model="muteDuration" min="1" max="1440" placeholder="分钟"
style="width: 60px; padding: 4px; border: 1px solid #ccc; border-radius: 3px; font-size: 11px;">
<span style="font-size: 11px; color: #b86e00;">分钟</span>
<button x-on:click="muteUser()"
style="padding: 4px 12px; background: #6366f1; color: #fff; border: none; border-radius: 3px; font-size: 11px; cursor: pointer;">执行</button>
</div>
</div>
@endif
{{-- 站长专属操作 --}}
@if (Auth::user()->user_level >= (int) \App\Models\Sysparam::getValue('superlevel', '100'))
<div style="padding: 0 16px 12px;" x-show="userInfo.username !== window.chatContext.username">
<div style="font-size: 11px; color: #7c3aed; margin-bottom: 6px; font-weight: bold;">站长操作</div>
<button
style="width: 100%; padding: 5px; border-radius: 4px; font-size: 11px; background: #f3e8ff; border: 1px solid #a855f7; cursor: pointer;"
x-on:click="loadWhispers()">🔍 查看私信记录</button>
</div>
{{-- 私信记录展示区 --}}
<div x-show="showWhispers"
style="display: none; padding: 0 16px 12px; max-height: 200px; overflow-y: auto;">
<div style="font-size: 11px; color: #666; margin-bottom: 4px;"
x-text="'最近 ' + whisperList.length + ' 条悄悄话:'"></div>
<template x-for="w in whisperList" :key="w.id">
<div style="font-size: 11px; padding: 3px 0; border-bottom: 1px solid #f0f0f0;">
<span style="color: #6366f1;" x-text="w.from_user"></span>
<span style="color: #059669;" x-text="w.to_user"></span>
<span x-text="w.content"></span>
<span style="color: #aaa; font-size: 10px;" x-text="'(' + w.sent_at + ')'"></span>
</div>
</template>
<div x-show="whisperList.length === 0" style="font-size: 11px; color: #aaa;">暂无悄悄话记录</div>
</div>
@endif
</div>
@@ -245,7 +382,8 @@
<span style="font-size:12px; color:#666;">当前选中:</span>
<img id="avatar-preview" src="/images/headface/{{ $user->usersf ?: '1.GIF' }}"
style="width:40px; height:40px; border:2px solid #336699; border-radius:4px;">
<span id="avatar-selected-name" style="font-size:12px; color:#333;">{{ $user->usersf ?: '未设置' }}</span>
<span id="avatar-selected-name"
style="font-size:12px; color:#333;">{{ $user->usersf ?: '未设置' }}</span>
<button id="avatar-save-btn" disabled onclick="saveAvatar()"
style="margin-left:auto; padding:5px 16px; background:#336699; color:#fff; border:none;
border-radius:3px; font-size:12px; cursor:pointer;">确定更换</button>

View File

@@ -90,6 +90,13 @@
<button type="button" id="fishing-btn" onclick="startFishing()"
style="font-size: 11px; padding: 1px 6px; background: #2563eb; color: #fff; border: none; border-radius: 2px; cursor: pointer;">🎣
钓鱼</button>
@if ($user->user_level >= (int) \App\Models\Sysparam::getValue('superlevel', '100'))
<button type="button" onclick="promptAnnounceMessage()"
style="font-size: 11px; padding: 1px 6px; background: #7c3aed; color: #fff; border: none; border-radius: 2px; cursor: pointer;">📢
公屏</button>
@endif
<button type="button" onclick="location.reload()"
style="font-size: 11px; padding: 1px 6px; background: #64748b; color: #fff; border: none; border-radius: 2px; cursor: pointer;">🔄
清屏</button>

View File

@@ -415,7 +415,7 @@
window.addEventListener('chat:kicked', (e) => {
if (e.detail.username === window.chatContext.username) {
alert("您已被管理员踢出房间!");
alert("您已被管理员踢出房间!" + (e.detail.reason ? "\n原因:" + e.detail.reason : ""));
window.location.href = "{{ route('rooms.index') }}";
}
});
@@ -632,6 +632,34 @@
}
}
// ── 站长公屏讲话 ─────────────────────────────────────
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;

View File

@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\AdminCommandController;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\ChatBotController;
use App\Http\Controllers\ChatController;
@@ -84,6 +85,14 @@ Route::middleware(['chat.auth'])->group(function () {
// ---- AI 聊天机器人 ----
Route::post('/chatbot/chat', [ChatBotController::class, 'chat'])->name('chatbot.chat');
Route::post('/chatbot/clear', [ChatBotController::class, 'clearContext'])->name('chatbot.clear');
// ---- 管理员命令(聊天室内实时操作)----
Route::post('/command/warn', [AdminCommandController::class, 'warn'])->name('command.warn');
Route::post('/command/kick', [AdminCommandController::class, 'kick'])->name('command.kick');
Route::post('/command/mute', [AdminCommandController::class, 'mute'])->name('command.mute');
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');
});
// 强力特权层中间件:同时验证 chat.auth 登录态 和 chat.level:super 特权superlevel 由 sysparam 配置)