From f951ec428dd2f8a5e74f14c0e2eba4eb6253de7c Mon Sep 17 00:00:00 2001 From: lkddi Date: Sun, 1 Mar 2026 01:32:20 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E5=AE=A4=E6=89=80=E6=9C=89=20alert()=20=E6=94=B9=E4=B8=BA=20wi?= =?UTF-8?q?ndow.chatDialog.alert()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts.blade.php 全部 21 处原生 alert() 替换: - 成功类 → chatDialog.alert(..., '提示', '#16a34a') - 失败/错误类 → chatDialog.alert(..., '操作失败', '#cc4444') - 网络异常类 → chatDialog.alert(..., '网络异常', '#cc4444') - 连接断开/踢出 → chatDialog.alert(..., '连接警告', '#b45309') - 一般提示 → chatDialog.alert(..., '提示', '#336699') DEVELOPMENT.md 新增 §7.9 window.chatBanner 使用文档 --- DEVELOPMENT.md | 168 ++++++++++++++++++ .../views/chat/partials/scripts.blade.php | 42 ++--- 2 files changed, 189 insertions(+), 21 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 5fdc215..9c35322 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -711,6 +711,174 @@ if (ok) { --- +### 7.9 全局大卡片通知 `window.chatBanner` ⭐ + +> [!NOTE] +> `chatBanner` 是居中弹出的沉浸式大卡片通知组件,适用于好友通知、任命公告、系统广播等重要事件。与 `chatDialog` 不同,它**不阻断操作流程**,支持自动消失和自定义按钮。 + +#### 文件位置 + +``` +resources/views/chat/partials/scripts.blade.php ← chatBanner 定义(window.chatBanner) +app/Events/BannerNotification.php ← 广播事件(后端推送) +app/Http/Controllers/Admin/BannerBroadcastController.php ← 管理员推送接口 +``` + +#### 前端 API + +```javascript +window.chatBanner.show(options); // 显示大卡片 +window.chatBanner.close(id); // 关闭指定 banner +``` + +#### `show(options)` 参数说明 + +| 参数 | 类型 | 说明 | 默认值 | +| ------------ | ---------- | ------------------------------------------- | --------------------------------- | +| `id` | `string` | 可选,banner DOM ID,同 ID 自动覆盖防重叠 | `'chat-banner-default'` | +| `icon` | `string` | 顶部 Emoji 图标 | 无 | +| `title` | `string` | 小标题(显示为 `══ 标题 ══`) | 无 | +| `name` | `string` | 大名字/主角行(自动 HTML 转义) | 无 | +| `body` | `string` | 主内容,支持有限 HTML(`
` 等) | 无 | +| `sub` | `string` | 副说明(小字,支持有限 HTML) | 无 | +| `gradient` | `string[]` | 渐变色数组,如 `['#4f46e5', '#db2777']` | `['#4f46e5','#7c3aed','#db2777']` | +| `titleColor` | `string` | 小标题字体颜色 | `'#fde68a'` | +| `autoClose` | `number` | 自动关闭时间(ms),`0` = 不自动关闭 | `5000` | +| `buttons` | `Button[]` | 按钮列表(见下) | 无(无按钮时不可点击) | + +`buttons` 数组元素: + +```typescript +{ + label: string, // 按钮文字 + color: string, // 背景色(CSS 颜色值) + onClick: (btn: HTMLElement, close: () => void) => void, // 点击回调 +} +``` + +#### 使用示例 + +```javascript +// ① 简单通知(5秒自动消失) +window.chatBanner.show({ + icon: "🎉💚🎉", + title: "好友通知", + name: "lkddi1", + body: "将你加为好友了!", + sub: '你们现在互为好友 🎊', + gradient: ["#065f46", "#059669", "#10b981"], + titleColor: "#a7f3d0", + autoClose: 5000, +}); + +// ② 带操作按钮(不自动关闭) +window.chatBanner.show({ + id: "friend-add-banner", + icon: "💚📩", + title: "好友申请", + name: "lkddi1", + body: "将你加为好友了!", + sub: "但你还没有回加对方为好友", + gradient: ["#1e3a5f", "#1d4ed8", "#0891b2"], + titleColor: "#bae6fd", + autoClose: 0, // 不自动关闭,等待用户操作 + buttons: [ + { + label: "➕ 回加好友", + color: "#10b981", + onClick: (btn, close) => { + btn.textContent = "处理中…"; + // ... 调用 API ... + close(); // 完成后手动关闭 + }, + }, + { + label: "稍后再说", + color: "rgba(255,255,255,0.15)", + onClick: (btn, close) => close(), + }, + ], +}); + +// ③ 手动关闭指定 banner +window.chatBanner.close("friend-add-banner"); +``` + +#### 后端推送(管理员) + +通过 `BannerNotification` 事件向**单个用户**或**整个房间**广播大卡片: + +```php +use App\Events\BannerNotification; + +// 推给单个用户(私有频道 user.{username}) +broadcast(new BannerNotification( + target: 'user', + targetId: 'lkddi', + options: [ + 'icon' => '📢', + 'title' => '系统通知', + 'body' => '你有一条新消息', + 'gradient' => ['#4f46e5', '#7c3aed'], + 'autoClose' => 6000, + ] +)); + +// 推给整个房间(Presence 频道 room.{id}) +broadcast(new BannerNotification( + target: 'room', + targetId: 1, + options: [ + 'icon' => '🎊', + 'title' => '活动公告', + 'body' => '双倍积分活动已开始!', + 'gradient' => ['#065f46', '#059669'], + 'autoClose' => 8000, + ] +)); +``` + +也可通过管理员 HTTP 接口(仅超级管理员): + +``` +POST /admin/banner/broadcast +Content-Type: application/json + +{ + "target": "room", + "target_id": 1, + "options": { + "icon": "📢", + "title": "公告", + "body": "服务器将于 10 分钟后重启", + "gradient": ["#374151", "#4b5563"], + "autoClose": 10000 + } +} +``` + +#### 渐变配色速查 + +| 场景 | 渐变数组 | +| ----------- | --------------------------------------------- | +| 好友 / 成功 | `['#065f46', '#059669', '#10b981']`(绿色) | +| 申请 / 信息 | `['#1e3a5f', '#1d4ed8', '#0891b2']`(蓝色) | +| 任命 / 荣耀 | `['#4f46e5', '#7c3aed', '#db2777']`(紫粉) | +| 撤销 / 中性 | `['#374151', '#4b5563', '#6b7280']`(灰色) | +| 警告 / 紧急 | `['#7f1d1d', '#991b1b', '#dc2626']`(红色) | +| 系统 / 特权 | `['#1e1b4b', '#312e81', '#4338ca']`(深蓝紫) | + +#### 安全说明 + +> [!IMPORTANT] +> +> - **前端控制台调用** `window.chatBanner.show()` 只影响**用户自己的浏览器**,无法推送给他人。 +> - **HTTP 推送接口** `POST /admin/banner/broadcast` 受三层中间件保护(`chat.auth` + `chat.has_position` + `chat.level:super`),普通用户调用返回 403。 +> - **内容净化**:`body`、`sub`、`title` 字段经过 `strip_tags` 白名单处理(允许 `
`),防止 XSS。 +> - **按钮 action 白名单**:仅允许 `close | add_friend | remove_friend | link`,禁止任意 JS 注入。 + +--- + ## 八、常用命令速查 ```bash diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index deeefcb..f9cd75a 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -498,7 +498,7 @@ window.addEventListener('chat:kicked', (e) => { if (e.detail.username === window.chatContext.username) { - alert("您已被管理员踢出房间!" + (e.detail.reason ? "\n原因:" + e.detail.reason : "")); + window.chatDialog.alert('您已被管理员踢出房间!' + (e.detail.reason ? ' 原因:' + e.detail.reason : ''), '系统通知', '#cc4444'); window.location.href = "{{ route('rooms.index') }}"; } }); @@ -1107,7 +1107,7 @@ type }), }).then(r => r.json()).then(data => { - if (data.status !== 'success') alert(data.message); + if (data.status !== 'success') window.chatDialog.alert(data.message, '操作失败', '#cc4444'); }).catch(err => console.error('特效触发失败:', err)); } window.triggerEffect = triggerEffect; @@ -1238,10 +1238,10 @@ contentInput.value = ''; contentInput.focus(); } else { - alert('发送失败: ' + (data.message || JSON.stringify(data.errors))); + window.chatDialog.alert('发送失败: ' + (data.message || JSON.stringify(data.errors)), '操作失败', '#cc4444'); } } catch (error) { - alert('网络连接错误,消息发送失败!'); + window.chatDialog.alert('网络连接错误,消息发送失败!', '网络错误', '#cc4444'); console.error(error); } finally { submitBtn.disabled = false; @@ -1273,12 +1273,12 @@ if (data.status === 'success') { const marquee = document.getElementById('announcement-text'); if (marquee) marquee.textContent = newText.trim(); - alert('公告已更新!'); + window.chatDialog.alert('公告已更新!', '提示', '#16a34a'); } else { - alert(data.message || '更新失败'); + window.chatDialog.alert(data.message || '更新失败', '操作失败', '#cc4444'); } } catch (e) { - alert('设置公告失败:' + e.message); + window.chatDialog.alert('设置公告失败:' + e.message, '操作失败', '#cc4444'); } } @@ -1303,10 +1303,10 @@ }); const data = await res.json(); if (!res.ok || data.status !== 'success') { - alert(data.message || '发送失败'); + window.chatDialog.alert(data.message || '发送失败', '操作失败', '#cc4444'); } } catch (e) { - alert('发送失败:' + e.message); + window.chatDialog.alert('发送失败:' + e.message, '操作失败', '#cc4444'); } } @@ -1329,10 +1329,10 @@ }); const data = await res.json(); if (!res.ok || data.status !== 'success') { - alert(data.message || '清屏失败'); + window.chatDialog.alert(data.message || '清屏失败', '操作失败', '#cc4444'); } } catch (e) { - alert('清屏失败:' + e.message); + window.chatDialog.alert('清屏失败:' + e.message, '操作失败', '#cc4444'); } } @@ -1454,7 +1454,7 @@ // 检测登录态失效 if (response.status === 401 || response.status === 419) { - alert('⚠️ 您的登录已失效(可能超时或在其他设备登录),请重新登录。'); + window.chatDialog.alert('⚠️ 您的登录已失效(可能超时或在其他设备登录),请重新登录。', '连接警告', '#b45309'); window.location.href = '/'; return; } @@ -1510,7 +1510,7 @@ heartbeatFailCount++; if (heartbeatFailCount >= MAX_HEARTBEAT_FAILS) { - alert('⚠️ 与服务器的连接已断开,请检查网络后重新登录。'); + window.chatDialog.alert('⚠️ 与服务器的连接已断开,请检查网络后重新登录。', '连接警告', '#b45309'); window.location.href = '/'; return; } @@ -1617,7 +1617,7 @@ const data = await res.json(); if (data.status === 'success') { - alert('头像修改成功!'); + window.chatDialog.alert('头像修改成功!', '提示', '#16a34a'); const myName = window.chatContext.username; if (onlineUsers[myName]) { onlineUsers[myName].headface = data.headface; @@ -1625,10 +1625,10 @@ renderUserList(); closeAvatarPicker(); } else { - alert(data.message || '修改失败'); + window.chatDialog.alert(data.message || '修改失败', '操作失败', '#cc4444'); } } catch (e) { - alert('网络错误'); + window.chatDialog.alert('网络错误', '网络异常', '#cc4444'); } btn.disabled = false; @@ -1656,7 +1656,7 @@ const data = await res.json(); if (!res.ok || data.status !== 'success') { - alert(data.message || '钓鱼失败'); + window.chatDialog.alert(data.message || '钓鱼失败', '操作失败', '#cc4444'); btn.disabled = false; return; } @@ -1704,7 +1704,7 @@ }, data.wait_time * 1000); } catch (e) { - alert('网络错误:' + e.message); + window.chatDialog.alert('网络错误:' + e.message, '网络异常', '#cc4444'); btn.disabled = false; } } @@ -1757,7 +1757,7 @@ } if (autoScroll) container2.scrollTop = container2.scrollHeight; } catch (e) { - alert('网络错误:' + e.message); + window.chatDialog.alert('网络错误:' + e.message, '网络异常', '#cc4444'); } resetFishingBtn(); @@ -1784,7 +1784,7 @@ */ async function sendToChatBot(content) { if (chatBotSending) { - alert('AI 正在思考中,请稍候...'); + window.chatDialog.alert('AI 正在思考中,请稍候...', '提示', '#336699'); return; } chatBotSending = true; @@ -1859,7 +1859,7 @@ container2.appendChild(sysDiv); if (autoScroll) container2.scrollTop = container2.scrollHeight; } catch (e) { - alert('清除失败:' + e.message); + window.chatDialog.alert('清除失败:' + e.message, '操作失败', '#cc4444'); } }