重构:聊天室所有 alert() 改为 window.chatDialog.alert()

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 使用文档
This commit is contained in:
2026-03-01 01:32:20 +08:00
parent 5c53b8cf2f
commit f951ec428d
2 changed files with 189 additions and 21 deletions

View File

@@ -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`<b><span><br>` 等) | 无 |
| `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: '<strong style="color:#a7f3d0;">你们现在互为好友 🎊</strong>',
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` 白名单处理(允许 `<b><strong><em><span><br>`),防止 XSS。
> - **按钮 action 白名单**:仅允许 `close | add_friend | remove_friend | link`,禁止任意 JS 注入。
---
## 八、常用命令速查
```bash

View File

@@ -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');
}
}