From 3c2038e8fed8ebd8b0edb1d8ccda941abba62e81 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sun, 1 Mar 2026 00:54:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=BC=B9=E7=AA=97=E6=A0=B9=E6=8D=AE=E4=BA=92?= =?UTF-8?q?=E7=9B=B8=E7=8A=B6=E6=80=81=E6=98=BE=E7=A4=BA=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FriendAdded 事件: - 新增 hasAddedBack 字段(B 是否已回加 A) - Toast:已互相好友 → '你们现在互为好友 🎉' - Toast:未回加 → '但你还没有添加对方为好友' + [➕ 回加] 一键操作按钮 FriendRemoved 事件: - 新增 hadAddedBack 字段(之前是否互相好友) - Toast:之前互相好友 → 提示 + [🗑️ 同步移除] 一键操作按钮 - Toast:单向好友 → 简单通知,无操作按钮 Toast 改进: - 右上角 × 关闭按钮 - 快捷操作按钮支持 fetch 直接请求 - 完成后显示结果并自动关闭,延时改为 8 秒 --- app/Events/FriendAdded.php | 13 +- app/Events/FriendRemoved.php | 13 +- app/Http/Controllers/FriendController.php | 18 ++- .../views/chat/partials/scripts.blade.php | 123 +++++++++++++++--- 4 files changed, 136 insertions(+), 31 deletions(-) diff --git a/app/Events/FriendAdded.php b/app/Events/FriendAdded.php index 8d7390e..ca26064 100644 --- a/app/Events/FriendAdded.php +++ b/app/Events/FriendAdded.php @@ -5,6 +5,8 @@ * * 当用户 A 添加用户 B 为好友时,向 B 的私有频道广播此事件, * B 的客户端收到后展示弹窗通知。 + * 携带 has_added_back 字段:若 B 已将 A 加为好友则为 true(双向好友), + * 否则为 false,前端提示 B 可以点击回加。 * * @author ChatRoom Laravel * @@ -27,12 +29,14 @@ class FriendAdded implements ShouldBroadcast /** * 构造好友添加事件。 * - * @param string $fromUsername 发起添加的用户名 - * @param string $toUsername 被添加的用户名(接收通知方) + * @param string $fromUsername 发起添加的用户名(A) + * @param string $toUsername 被添加的用户名(B,接收通知方) + * @param bool $hasAddedBack B 是否已将 A 加为好友(互相添加=true) */ public function __construct( public readonly string $fromUsername, public readonly string $toUsername, + public readonly bool $hasAddedBack = false, ) {} /** @@ -44,9 +48,9 @@ class FriendAdded implements ShouldBroadcast } /** - * 广播负载:包含发起人信息,供前端弹窗使用。 + * 广播负载:包含发起人信息和互相好友状态,供前端弹窗使用。 * - * @return array + * @return array */ public function broadcastWith(): array { @@ -54,6 +58,7 @@ class FriendAdded implements ShouldBroadcast 'from_username' => $this->fromUsername, 'to_username' => $this->toUsername, 'type' => 'friend_added', + 'has_added_back' => $this->hasAddedBack, ]; } } diff --git a/app/Events/FriendRemoved.php b/app/Events/FriendRemoved.php index 4984f6a..6d58d07 100644 --- a/app/Events/FriendRemoved.php +++ b/app/Events/FriendRemoved.php @@ -5,6 +5,8 @@ * * 当用户 A 删除用户 B 为好友时,向 B 的私有频道广播此事件, * B 的客户端收到后展示弹窗通知。 + * 携带 hadAddedBack 字段:若 B 之前也把 A 加为好友(互相好友)则为 true, + * 前端可提示 B "是否同步移除对方"。 * * @author ChatRoom Laravel * @@ -27,12 +29,14 @@ class FriendRemoved implements ShouldBroadcast /** * 构造好友删除事件。 * - * @param string $fromUsername 发起删除的用户名 - * @param string $toUsername 被删除的用户名(接收通知方) + * @param string $fromUsername 发起删除的用户名(A) + * @param string $toUsername 被删除的用户名(B,接收通知方) + * @param bool $hadAddedBack B 之前是否也将 A 加为好友(互相好友=true) */ public function __construct( public readonly string $fromUsername, public readonly string $toUsername, + public readonly bool $hadAddedBack = false, ) {} /** @@ -44,9 +48,9 @@ class FriendRemoved implements ShouldBroadcast } /** - * 广播负载:包含发起人信息,供前端弹窗使用。 + * 广播负载:包含发起人信息和之前互相好友状态,供前端弹窗使用。 * - * @return array + * @return array */ public function broadcastWith(): array { @@ -54,6 +58,7 @@ class FriendRemoved implements ShouldBroadcast 'from_username' => $this->fromUsername, 'to_username' => $this->toUsername, 'type' => 'friend_removed', + 'had_added_back' => $this->hadAddedBack, ]; } } diff --git a/app/Http/Controllers/FriendController.php b/app/Http/Controllers/FriendController.php index eff1c62..a1a00c7 100644 --- a/app/Http/Controllers/FriendController.php +++ b/app/Http/Controllers/FriendController.php @@ -106,8 +106,13 @@ class FriendController extends Controller 'sub_time' => now(), ]); - // 广播给对方(仅对方可见) - broadcast(new FriendAdded($me->username, $username)); + // 检查 B 是否已将 A 加为好友(互相好友判断) + $hasAddedBack = FriendRequest::where('who', $username) + ->where('towho', $me->username) + ->exists(); + + // 广播给对方(仅对方可见),携带是否已回加的状态 + broadcast(new FriendAdded($me->username, $username, $hasAddedBack)); // 若对方在线,推送聊天区悄悄话 $this->notifyOnlineUser($username, $me->username, 'added', $request->input('room_id')); @@ -140,8 +145,13 @@ class FriendController extends Controller return response()->json(['status' => 'error', 'message' => '好友关系不存在'], 404); } - // 广播给对方 - broadcast(new FriendRemoved($me->username, $username)); + // 检查 B 之前是否也将 A 加为好友(删除前的互相状态) + $hadAddedBack = FriendRequest::where('who', $username) + ->where('towho', $me->username) + ->exists(); + + // 广播给对方,携带之前的互相好友状态 + broadcast(new FriendRemoved($me->username, $username, $hadAddedBack)); // 若对方在线,推送聊天区悄悄话 $this->notifyOnlineUser($username, $me->username, 'removed', $request->input('room_id')); diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 05a9512..0fd79be 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -663,49 +663,134 @@ const myName = window.chatContext.username; window.Echo.private(`user.${myName}`) .listen('.FriendAdded', (e) => { - showFriendToast( - `💚 ${e.from_username} 将你加为好友了!`, - '#16a34a' - ); + if (e.has_added_back) { + // 我已经把对方加了,现在对方也加了我 → 双向好友 + showFriendToast( + `💚 ${e.from_username} 将你加为好友了!
你们现在互为好友 🎉`, + '#16a34a' + ); + } else { + // 对方加了我,但我还没加对方 → 提示可以回加 + showFriendToast( + `💚 ${e.from_username} 将你加为好友了!
+ 但你还没有添加对方为好友。`, + '#16a34a', { + label: `➕ 回加 ${e.from_username}`, + username: e.from_username, + action: 'add' + } + ); + } }) .listen('.FriendRemoved', (e) => { - showFriendToast( - `💔 ${e.from_username} 已将你从好友列表移除。`, - '#6b7280' - ); + if (e.had_added_back) { + // 之前是互相好友,现在对方删除了我 → 提示可以同步删除 + showFriendToast( + `💔 ${e.from_username} 已将你从好友列表移除。
+ 你的好友列表中仍保留对方,可点击同步移除。`, + '#6b7280', { + label: `🗑️ 同步移除 ${e.from_username}`, + username: e.from_username, + action: 'remove' + } + ); + } else { + // 对方删我,但原来就是单向的 + showFriendToast( + `💔 ${e.from_username} 已将你从他的好友列表移除。`, + '#9ca3af' + ); + } }); } document.addEventListener('DOMContentLoaded', setupFriendNotification); /** - * 显示好友事件通知浮窗(类似任务弹窗,右下角淡入淡出)。 + * 显示好友事件通知浮窗(右下角淡入淡出)。 * - * @param {string} html 通知内容(支持 HTML) - * @param {string} color 左边框颜色 + * @param {string} html 通知内容(支持 HTML) + * @param {string} color 左边框 / 主题颜色 + * @param {object|null} action 可选操作按钮 { label, username, action:'add'|'remove' } */ - function showFriendToast(html, color = '#16a34a') { + 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.5; - animation: fdSlideIn .3s ease; cursor: pointer; + font-size: 13px; color: #374151; line-height: 1.6; + animation: fdSlideIn .3s ease; `; + + // 操作按钮 HTML + let actionHtml = ''; + if (action) { + actionHtml = ` +
+ +
`; + } + toast.innerHTML = ` -
💬 好友通知
-
${html}
+
+
+
💬 好友通知
+
${html}
+ ${actionHtml} +
+ +
`; - // 点击关闭 - toast.addEventListener('click', () => toast.remove()); + document.body.appendChild(toast); + + // 绑定操作按钮事件 + if (action) { + const btn = toast.querySelector('button[id^="friend-toast-btn"]'); + if (btn) { + btn.addEventListener('click', async () => { + btn.disabled = true; + btn.textContent = '处理中…'; + try { + const method = action.action === 'add' ? 'POST' : 'DELETE'; + const url = + `/friend/${encodeURIComponent(action.username)}/${action.action === 'add' ? 'add' : 'remove'}`; + const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? ''; + const res = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrf, + 'Accept': 'application/json' + }, + body: JSON.stringify({ + room_id: window.chatContext?.roomId + }), + }); + const data = await res.json(); + btn.textContent = data.status === 'success' ? '✅ 已完成' : '❌ 失败'; + btn.style.background = data.status === 'success' ? '#16a34a' : '#cc4444'; + setTimeout(() => toast.remove(), 2000); + } catch (e) { + btn.textContent = '❌ 网络错误'; + } + }); + } + } + // 5秒后自动消失 setTimeout(() => { toast.style.transition = 'opacity .5s'; toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); - }, 5000); + }, 8000); } // ── 全屏特效事件监听(烟花/下雨/雷电/下雪)─────────