新增 屏蔽消息功能
This commit is contained in:
@@ -111,7 +111,31 @@ $welcomeMessages = [
|
|||||||
style="font-size: 11px; padding: 1px 6px; background: #0f766e; color: #fff; border: none; border-radius: 2px; cursor: pointer;">
|
style="font-size: 11px; padding: 1px 6px; background: #0f766e; color: #fff; border: none; border-radius: 2px; cursor: pointer;">
|
||||||
📷 图片
|
📷 图片
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div style="position:relative;display:inline-block;" id="block-btn-wrap">
|
||||||
|
<button type="button" onclick="toggleBlockMenu(event)"
|
||||||
|
style="font-size:11px;padding:1px 6px;background:#475569;color:#fff;border:none;border-radius:2px;cursor:pointer;">
|
||||||
|
🔕 屏蔽
|
||||||
|
</button>
|
||||||
|
<div id="block-menu"
|
||||||
|
onclick="event.stopPropagation()"
|
||||||
|
style="display:none;position:absolute;bottom:calc(100% + 6px);left:0;z-index:10020;min-width:168px;padding:10px;background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;box-shadow:0 10px 24px rgba(15,23,42,.18);">
|
||||||
|
<div style="font-size:10px;color:#475569;padding:0 2px 8px;">选择后立即隐藏对应播报</div>
|
||||||
|
<label
|
||||||
|
style="display:flex;align-items:center;gap:6px;font-size:12px;color:#1e293b;cursor:pointer;padding:4px 2px;">
|
||||||
|
<input type="checkbox" id="block-sender-fishing"
|
||||||
|
onchange="toggleBlockedSystemSender('钓鱼播报', this.checked)">
|
||||||
|
钓鱼播报
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
style="display:flex;align-items:center;gap:6px;font-size:12px;color:#1e293b;cursor:pointer;padding:4px 2px;">
|
||||||
|
<input type="checkbox" id="block-sender-doctor"
|
||||||
|
onchange="toggleBlockedSystemSender('星海小博士', this.checked)">
|
||||||
|
星海小博士
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (
|
@if (
|
||||||
$user->user_level >= (int) \App\Models\Sysparam::getValue('level_announcement', '10') ||
|
$user->user_level >= (int) \App\Models\Sysparam::getValue('level_announcement', '10') ||
|
||||||
$room->master == $user->username ||
|
$room->master == $user->username ||
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
6. 钓鱼小游戏(startFishing / reelFish / autoFish)
|
6. 钓鱼小游戏(startFishing / reelFish / autoFish)
|
||||||
7. 发送消息(sendMessage,IME 防重触发)
|
7. 发送消息(sendMessage,IME 防重触发)
|
||||||
8. 特效控制(triggerEffect / applyFontSize / toggleSoundMute)
|
8. 特效控制(triggerEffect / applyFontSize / toggleSoundMute)
|
||||||
|
9. 系统播报屏蔽(toggleBlockMenu / toggleBlockedSystemSender)
|
||||||
|
|
||||||
已拆分至独立文件:
|
已拆分至独立文件:
|
||||||
- window.chatBanner → chat-banner.blade.php
|
- window.chatBanner → chat-banner.blade.php
|
||||||
@@ -36,6 +37,8 @@
|
|||||||
const toUserSelect = document.getElementById('to_user');
|
const toUserSelect = document.getElementById('to_user');
|
||||||
const onlineCount = document.getElementById('online-count');
|
const onlineCount = document.getElementById('online-count');
|
||||||
const onlineCountBottom = document.getElementById('online-count-bottom');
|
const onlineCountBottom = document.getElementById('online-count-bottom');
|
||||||
|
const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = 'chat_blocked_system_senders';
|
||||||
|
const BLOCKABLE_SYSTEM_SENDERS = ['钓鱼播报', '星海小博士'];
|
||||||
|
|
||||||
// ── 消息区:手机端双触发打开用户名片(PC 端靠 ondblclick 内联属性)──
|
// ── 消息区:手机端双触发打开用户名片(PC 端靠 ondblclick 内联属性)──
|
||||||
// span[data-u] 由 clickableUser() 生成,touchend 委托至容器避免每条消息单独绑定
|
// span[data-u] 由 clickableUser() 生成,touchend 委托至容器避免每条消息单独绑定
|
||||||
@@ -67,6 +70,133 @@
|
|||||||
let onlineUsers = {};
|
let onlineUsers = {};
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
|
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。
|
* 转义会员横幅文本,避免横幅层被注入 HTML。
|
||||||
@@ -319,12 +449,16 @@
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const menu = document.getElementById('welcome-menu');
|
const menu = document.getElementById('welcome-menu');
|
||||||
const adminMenu = document.getElementById('admin-menu');
|
const adminMenu = document.getElementById('admin-menu');
|
||||||
|
const blockMenu = document.getElementById('block-menu');
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (adminMenu) {
|
if (adminMenu) {
|
||||||
adminMenu.style.display = 'none';
|
adminMenu.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
if (blockMenu) {
|
||||||
|
blockMenu.style.display = 'none';
|
||||||
|
}
|
||||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,12 +469,16 @@
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const menu = document.getElementById('admin-menu');
|
const menu = document.getElementById('admin-menu');
|
||||||
const welcomeMenu = document.getElementById('welcome-menu');
|
const welcomeMenu = document.getElementById('welcome-menu');
|
||||||
|
const blockMenu = document.getElementById('block-menu');
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (welcomeMenu) {
|
if (welcomeMenu) {
|
||||||
welcomeMenu.style.display = 'none';
|
welcomeMenu.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
if (blockMenu) {
|
||||||
|
blockMenu.style.display = 'none';
|
||||||
|
}
|
||||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +579,11 @@
|
|||||||
if (adminMenu) {
|
if (adminMenu) {
|
||||||
adminMenu.style.display = 'none';
|
adminMenu.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockMenu = document.getElementById('block-menu');
|
||||||
|
if (blockMenu) {
|
||||||
|
blockMenu.style.display = 'none';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 动作选择 ──────────────────────────────────────
|
// ── 动作选择 ──────────────────────────────────────
|
||||||
@@ -656,11 +799,19 @@
|
|||||||
_maxMsgId = msg.id;
|
_maxMsgId = msg.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户勾选屏蔽后,历史消息和实时消息统一在这里拦截,不再进入渲染流程。
|
||||||
|
if (isBlockedSystemSender(msg?.from_user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isMe = msg.from_user === window.chatContext.username;
|
const isMe = msg.from_user === window.chatContext.username;
|
||||||
const fontColor = msg.font_color || '#000000';
|
const fontColor = msg.font_color || '#000000';
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'msg-line';
|
div.className = 'msg-line';
|
||||||
|
if (msg?.from_user) {
|
||||||
|
div.dataset.fromUser = msg.from_user;
|
||||||
|
}
|
||||||
|
|
||||||
const timeStr = msg.sent_at || '';
|
const timeStr = msg.sent_at || '';
|
||||||
let timeStrOverride = false;
|
let timeStrOverride = false;
|
||||||
@@ -1351,9 +1502,11 @@
|
|||||||
}).catch(err => console.error('特效触发失败:', err));
|
}).catch(err => console.error('特效触发失败:', err));
|
||||||
}
|
}
|
||||||
window.toggleAdminMenu = toggleAdminMenu;
|
window.toggleAdminMenu = toggleAdminMenu;
|
||||||
|
window.toggleBlockMenu = toggleBlockMenu;
|
||||||
window.runAdminAction = runAdminAction;
|
window.runAdminAction = runAdminAction;
|
||||||
window.selectEffect = selectEffect;
|
window.selectEffect = selectEffect;
|
||||||
window.triggerEffect = triggerEffect;
|
window.triggerEffect = triggerEffect;
|
||||||
|
window.toggleBlockedSystemSender = toggleBlockedSystemSender;
|
||||||
|
|
||||||
// ── 字号设置(持久化到 localStorage)─────────────────
|
// ── 字号设置(持久化到 localStorage)─────────────────
|
||||||
/**
|
/**
|
||||||
@@ -1391,6 +1544,7 @@
|
|||||||
const muted = localStorage.getItem('chat_sound_muted') === '1';
|
const muted = localStorage.getItem('chat_sound_muted') === '1';
|
||||||
const muteChk = document.getElementById('sound_muted');
|
const muteChk = document.getElementById('sound_muted');
|
||||||
if (muteChk) muteChk.checked = muted;
|
if (muteChk) muteChk.checked = muted;
|
||||||
|
syncBlockedSystemSenderCheckboxes();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 特效禁音开关 ─────────────────────────────────────────────────
|
// ── 特效禁音开关 ─────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -60,6 +60,24 @@ class ChatControllerTest extends TestCase
|
|||||||
$response->assertDontSee('chatAvatarWidget');
|
$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');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试用户可以发送普通文本消息。
|
* 测试用户可以发送普通文本消息。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user