diff --git a/resources/views/chat/partials/layout/input-bar.blade.php b/resources/views/chat/partials/layout/input-bar.blade.php
index 25cf6ea..eff8cf0 100644
--- a/resources/views/chat/partials/layout/input-bar.blade.php
+++ b/resources/views/chat/partials/layout/input-bar.blade.php
@@ -111,7 +111,31 @@ $welcomeMessages = [
style="font-size: 11px; padding: 1px 6px; background: #0f766e; color: #fff; border: none; border-radius: 2px; cursor: pointer;">
📷 图片
-
+
+
+
+
+
+
@if (
$user->user_level >= (int) \App\Models\Sysparam::getValue('level_announcement', '10') ||
$room->master == $user->username ||
diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php
index caebfd6..7e6ed23 100644
--- a/resources/views/chat/partials/scripts.blade.php
+++ b/resources/views/chat/partials/scripts.blade.php
@@ -10,6 +10,7 @@
6. 钓鱼小游戏(startFishing / reelFish / autoFish)
7. 发送消息(sendMessage,IME 防重触发)
8. 特效控制(triggerEffect / applyFontSize / toggleSoundMute)
+ 9. 系统播报屏蔽(toggleBlockMenu / toggleBlockedSystemSender)
已拆分至独立文件:
- window.chatBanner → chat-banner.blade.php
@@ -36,6 +37,8 @@
const toUserSelect = document.getElementById('to_user');
const onlineCount = document.getElementById('online-count');
const onlineCountBottom = document.getElementById('online-count-bottom');
+ const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = 'chat_blocked_system_senders';
+ const BLOCKABLE_SYSTEM_SENDERS = ['钓鱼播报', '星海小博士'];
// ── 消息区:手机端双触发打开用户名片(PC 端靠 ondblclick 内联属性)──
// span[data-u] 由 clickableUser() 生成,touchend 委托至容器避免每条消息单独绑定
@@ -67,6 +70,133 @@
let onlineUsers = {};
let autoScroll = true;
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
+ let blockedSystemSenders = new Set(loadBlockedSystemSenders());
+
+ /**
+ * 从 localStorage 读取已屏蔽的系统播报发送者列表。
+ *
+ * @returns {string[]}
+ */
+ function loadBlockedSystemSenders() {
+ try {
+ const saved = JSON.parse(localStorage.getItem(BLOCKED_SYSTEM_SENDERS_STORAGE_KEY) || '[]');
+ if (!Array.isArray(saved)) {
+ return [];
+ }
+
+ // 仅允许白名单内的系统播报项进入配置,避免脏数据污染。
+ return saved.filter(sender => BLOCKABLE_SYSTEM_SENDERS.includes(sender));
+ } catch (error) {
+ return [];
+ }
+ }
+
+ /**
+ * 将当前屏蔽配置持久化到 localStorage。
+ */
+ function persistBlockedSystemSenders() {
+ localStorage.setItem(
+ BLOCKED_SYSTEM_SENDERS_STORAGE_KEY,
+ JSON.stringify(Array.from(blockedSystemSenders))
+ );
+ }
+
+ /**
+ * 同步屏蔽菜单中的复选框状态。
+ */
+ function syncBlockedSystemSenderCheckboxes() {
+ const fishingCheckbox = document.getElementById('block-sender-fishing');
+ const doctorCheckbox = document.getElementById('block-sender-doctor');
+
+ if (fishingCheckbox) {
+ fishingCheckbox.checked = blockedSystemSenders.has('钓鱼播报');
+ }
+
+ if (doctorCheckbox) {
+ doctorCheckbox.checked = blockedSystemSenders.has('星海小博士');
+ }
+ }
+
+ /**
+ * 判断当前消息发送者是否已被用户屏蔽。
+ *
+ * @param {string} fromUser 发送者名称
+ * @returns {boolean}
+ */
+ function isBlockedSystemSender(fromUser) {
+ return blockedSystemSenders.has(String(fromUser || ''));
+ }
+
+ /**
+ * 从当前已渲染的聊天窗口中移除指定发送者的所有消息。
+ *
+ * @param {string} sender 发送者名称
+ */
+ function removeRenderedMessagesBySender(sender) {
+ [container, container2].forEach(targetContainer => {
+ if (!targetContainer) {
+ return;
+ }
+
+ targetContainer.querySelectorAll('[data-from-user]').forEach(node => {
+ if (node.dataset.fromUser === sender) {
+ node.remove();
+ }
+ });
+ });
+ }
+
+ /**
+ * 切换系统播报屏蔽菜单的显示状态。
+ *
+ * @param {Event} event 点击事件
+ */
+ function toggleBlockMenu(event) {
+ event.stopPropagation();
+ const menu = document.getElementById('block-menu');
+ const welcomeMenu = document.getElementById('welcome-menu');
+ const adminMenu = document.getElementById('admin-menu');
+
+ if (!menu) {
+ return;
+ }
+
+ if (welcomeMenu) {
+ welcomeMenu.style.display = 'none';
+ }
+
+ if (adminMenu) {
+ adminMenu.style.display = 'none';
+ }
+
+ syncBlockedSystemSenderCheckboxes();
+ menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
+ }
+
+ /**
+ * 更新指定系统播报项的屏蔽状态,并在勾选后立即清理当前窗口。
+ *
+ * @param {string} sender 系统播报发送者
+ * @param {boolean} blocked 是否屏蔽
+ */
+ function toggleBlockedSystemSender(sender, blocked) {
+ if (!BLOCKABLE_SYSTEM_SENDERS.includes(sender)) {
+ return;
+ }
+
+ if (blocked) {
+ blockedSystemSenders.add(sender);
+ // 勾选后立刻移除聊天室窗口内已显示的对应播报内容。
+ removeRenderedMessagesBySender(sender);
+ } else {
+ blockedSystemSenders.delete(sender);
+ }
+
+ persistBlockedSystemSenders();
+ syncBlockedSystemSenderCheckboxes();
+ }
+
+ syncBlockedSystemSenderCheckboxes();
/**
* 转义会员横幅文本,避免横幅层被注入 HTML。
@@ -319,12 +449,16 @@
event.stopPropagation();
const menu = document.getElementById('welcome-menu');
const adminMenu = document.getElementById('admin-menu');
+ const blockMenu = document.getElementById('block-menu');
if (!menu) {
return;
}
if (adminMenu) {
adminMenu.style.display = 'none';
}
+ if (blockMenu) {
+ blockMenu.style.display = 'none';
+ }
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
@@ -335,12 +469,16 @@
event.stopPropagation();
const menu = document.getElementById('admin-menu');
const welcomeMenu = document.getElementById('welcome-menu');
+ const blockMenu = document.getElementById('block-menu');
if (!menu) {
return;
}
if (welcomeMenu) {
welcomeMenu.style.display = 'none';
}
+ if (blockMenu) {
+ blockMenu.style.display = 'none';
+ }
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
@@ -441,6 +579,11 @@
if (adminMenu) {
adminMenu.style.display = 'none';
}
+
+ const blockMenu = document.getElementById('block-menu');
+ if (blockMenu) {
+ blockMenu.style.display = 'none';
+ }
});
// ── 动作选择 ──────────────────────────────────────
@@ -656,11 +799,19 @@
_maxMsgId = msg.id;
}
+ // 用户勾选屏蔽后,历史消息和实时消息统一在这里拦截,不再进入渲染流程。
+ if (isBlockedSystemSender(msg?.from_user)) {
+ return;
+ }
+
const isMe = msg.from_user === window.chatContext.username;
const fontColor = msg.font_color || '#000000';
const div = document.createElement('div');
div.className = 'msg-line';
+ if (msg?.from_user) {
+ div.dataset.fromUser = msg.from_user;
+ }
const timeStr = msg.sent_at || '';
let timeStrOverride = false;
@@ -1351,9 +1502,11 @@
}).catch(err => console.error('特效触发失败:', err));
}
window.toggleAdminMenu = toggleAdminMenu;
+ window.toggleBlockMenu = toggleBlockMenu;
window.runAdminAction = runAdminAction;
window.selectEffect = selectEffect;
window.triggerEffect = triggerEffect;
+ window.toggleBlockedSystemSender = toggleBlockedSystemSender;
// ── 字号设置(持久化到 localStorage)─────────────────
/**
@@ -1391,6 +1544,7 @@
const muted = localStorage.getItem('chat_sound_muted') === '1';
const muteChk = document.getElementById('sound_muted');
if (muteChk) muteChk.checked = muted;
+ syncBlockedSystemSenderCheckboxes();
});
// ── 特效禁音开关 ─────────────────────────────────────────────────
diff --git a/tests/Feature/ChatControllerTest.php b/tests/Feature/ChatControllerTest.php
index 95163bc..d9d927f 100644
--- a/tests/Feature/ChatControllerTest.php
+++ b/tests/Feature/ChatControllerTest.php
@@ -60,6 +60,24 @@ class ChatControllerTest extends TestCase
$response->assertDontSee('chatAvatarWidget');
}
+ /**
+ * 测试聊天室输入区会渲染系统播报屏蔽按钮与对应选项。
+ */
+ public function test_room_view_renders_block_system_sender_controls(): void
+ {
+ $room = Room::create(['room_name' => 'blockmenu']);
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)->get(route('chat.room', $room->id));
+
+ $response->assertOk();
+ $response->assertSee('🔕 屏蔽', false);
+ $response->assertSee('钓鱼播报');
+ $response->assertSee('星海小博士');
+ $response->assertSee('chat_blocked_system_senders');
+ $response->assertSee('toggleBlockedSystemSender');
+ }
+
/**
* 测试用户可以发送普通文本消息。
*/