diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 9c35322..5aa0d07 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -711,7 +711,71 @@ if (ok) {
---
-### 7.9 全局大卡片通知 `window.chatBanner` ⭐
+### 7.9 全局右下角 Toast 通知卡片 `window.chatToast` ⭐
+
+> [!NOTE]
+> `chatToast` 是固定在**右下角**的轻量通知卡片,适用于实时事件通知(奖励到账、好友动态等)。
+> 与 `chatDialog` 不同,它不阻断操作流程,自动消失,可堆叠多条。
+
+#### 文件位置
+
+```
+resources/views/chat/partials/toast-notification.blade.php ← Toast 组件 HTML + JS
+resources/views/chat/frame.blade.php ← 已 @include,全页面可用
+```
+
+#### API 说明
+
+```javascript
+window.chatToast.show({
+ title: "标题文字", // 必填
+ message: "正文内容", // 必填,支持 HTML
+ icon: "🪙", // 可选,左侧 Emoji,默认 💬
+ color: "#f59e0b", // 可选,强调色,默认 #336699
+ duration: 6000, // 可选,自动消失毫秒,0 = 不自动消失,默认 6000
+ action: {
+ // 可选,操作按钮
+ label: "操作文字",
+ onClick: () => {
+ /* ... */
+ },
+ },
+});
+```
+
+#### 通过消息字段自动触发(后端控制)
+
+后端 broadcast 的消息中包含 `toast_notification` 字段,且接收方是当前用户时,前端脚本会自动弹出 Toast:
+
+```php
+// AdminCommandController::reward() 示例
+$msg['toast_notification'] = [
+ 'title' => '🪙 奖励金币到账',
+ 'message' => "{$admin->username} 向你发放了 {$amount} 枚金币!",
+ 'icon' => '🪙',
+ 'color' => '#f59e0b',
+ 'duration' => 8000,
+];
+```
+
+#### 使用场景
+
+| 场景 | 颜色 | 图标 |
+| ---------------- | --------------- | ---- |
+| 奖励金币到账 | `#f59e0b`(橙) | 🪙 |
+| 好友动态通知 | `#6b7280`(灰) | 👥 |
+| 礼物收到 | `#e11d48`(玫) | 🎁 |
+| 系统提示(普通) | `#336699`(蓝) | 💬 |
+| 等级晋升 | `#7c3aed`(紫) | 🌟 |
+
+#### 原 `showFriendToast` 迁移说明
+
+旧函数 `showFriendToast()` 已被 `window.chatToast.show()` 替代,好友删除通知已改用新 API。
+新增功能只需调用 `window.chatToast.show()`,**勿新增** `showFriendToast` 调用。
+
+---
+
+### 7.10 全局大卡片通知 `window.chatBanner` ⭐
> [!NOTE]
> `chatBanner` 是居中弹出的沉浸式大卡片通知组件,适用于好友通知、任命公告、系统广播等重要事件。与 `chatDialog` 不同,它**不阻断操作流程**,支持自动消失和自定义按钮。
diff --git a/app/Http/Controllers/AdminCommandController.php b/app/Http/Controllers/AdminCommandController.php
index a38309a..7bf4d28 100644
--- a/app/Http/Controllers/AdminCommandController.php
+++ b/app/Http/Controllers/AdminCommandController.php
@@ -545,17 +545,42 @@ class AdminCommandController extends Controller
'remark' => "发放奖励金币 {$amount} 枚给 {$targetUsername}",
]);
- // 向聊天室发送悄悄话通知接收者
+ // ① 聊天室公开公告(所有在场用户可见)
+ $publicMsg = [
+ 'id' => $this->chatState->nextMessageId($roomId),
+ 'room_id' => $roomId,
+ 'from_user' => '系统公告',
+ 'to_user' => '',
+ 'content' => "🪙 {$admin->username}({$positionName})向 {$targetUsername} 发放了 {$amount} 枚奖励金币!",
+ 'is_secret' => false,
+ 'font_color' => '#d97706',
+ 'action' => '',
+ 'sent_at' => now()->toDateTimeString(),
+ ];
+ $this->chatState->pushMessage($roomId, $publicMsg);
+ broadcast(new MessageSent($roomId, $publicMsg));
+ SaveMessageJob::dispatch($publicMsg);
+
+ // ② 接收者私信(含 toast_notification 触发右下角小卡片)
+ $freshJjb = $target->fresh()->jjb;
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统',
'to_user' => $targetUsername,
- 'content' => "🎁 {$admin->username}({$position->name})向你发放了 {$amount} 枚金币奖励!当前金币:{$target->fresh()->jjb} 枚。",
+ 'content' => "🎁 {$admin->username}({$positionName})向你发放了 {$amount} 枚金币奖励!当前金币:{$freshJjb} 枚。",
'is_secret' => true,
'font_color' => '#d97706',
'action' => '',
'sent_at' => now()->toDateTimeString(),
+ // 前端 toast-notification 组件识别此字段,弹出右下角通知卡片
+ 'toast_notification' => [
+ 'title' => '🪙 奖励金币到账',
+ 'message' => "{$admin->username}({$positionName})向你发放了 {$amount} 枚金币!",
+ 'icon' => '🪙',
+ 'color' => '#f59e0b',
+ 'duration' => 8000,
+ ],
];
$this->chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php
index 818a30e..4b3db9b 100644
--- a/resources/views/chat/frame.blade.php
+++ b/resources/views/chat/frame.blade.php
@@ -110,7 +110,7 @@
{{-- ═══════════ 全局自定义弹窗(替代原生 alert/confirm,全页面可用) ═══════════ --}}
@include('chat.partials.global-dialog')
-
+ @include('chat.partials.toast-notification')
{{-- ═══════════ 聊天室交互脚本(独立文件维护) ═══════════ --}}
@include('chat.partials.user-actions')
diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php
index bc70c0f..617d58d 100644
--- a/resources/views/chat/partials/scripts.blade.php
+++ b/resources/views/chat/partials/scripts.blade.php
@@ -494,6 +494,17 @@
return;
}
appendMessage(msg);
+ // 若消息携带 toast_notification 字段且当前用户是接收者,弹右下角小卡片
+ if (msg.toast_notification && msg.to_user === window.chatContext.username) {
+ const t = msg.toast_notification;
+ window.chatToast.show({
+ title: t.title || '通知',
+ message: t.message || '',
+ icon: t.icon || '💬',
+ color: t.color || '#336699',
+ duration: t.duration ?? 8000,
+ });
+ }
});
window.addEventListener('chat:kicked', (e) => {
@@ -673,20 +684,39 @@
.listen('.FriendRemoved', (e) => {
if (e.had_added_back) {
// 之前是互相好友,现在对方删除了我 → 提示可以同步删除
- showFriendToast(
- `� ${e.from_username} 已将你从好友列表移除。
- 你的好友列表中仍保留对方,可点击同步移除。`,
- '#6b7280', {
+ window.chatToast.show({
+ title: '好友通知',
+ message: `${e.from_username} 已将你从好友列表移除。
你的好友列表中仍保留对方,可点击同步移除。`,
+ icon: '👥',
+ color: '#6b7280',
+ duration: 10000,
+ action: {
label: `🗑️ 同步移除 ${e.from_username}`,
- username: e.from_username,
- action: 'remove'
- }
- );
+ onClick: async () => {
+ const url = `/friend/${encodeURIComponent(e.from_username)}/remove`;
+ const csrf = document.querySelector('meta[name="csrf-token"]')
+ ?.content ?? '';
+ await fetch(url, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-TOKEN': csrf,
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify({
+ room_id: window.chatContext?.roomId
+ }),
+ });
+ }
+ },
+ });
} else {
- showFriendToast(
- `� ${e.from_username} 已将你从他的好友列表移除。`,
- '#9ca3af'
- );
+ window.chatToast.show({
+ title: '好友通知',
+ message: `${e.from_username} 已将你从他的好友列表移除。`,
+ icon: '👥',
+ color: '#9ca3af',
+ });
}
});
}
@@ -1002,86 +1032,8 @@
}
};
- function showFriendToast(html, color = '#16a34a', action = null) {
- const toast = document.createElement('div');
- toast.style.cssText = `
- position: fixed; bottom: 24px; right: 24px; z-index: 999999;
- background: #fff; border-left: 4px solid ${color};
- border-radius: 8px; padding: 14px 18px; min-width: 260px; max-width: 320px;
- box-shadow: 0 8px 32px rgba(0,0,0,.18);
- font-size: 13px; color: #374151; line-height: 1.6;
- animation: fdSlideIn .3s ease;
- `;
-
- // 操作按钮 HTML
- let actionHtml = '';
- if (action) {
- actionHtml = `
-