重构:好友面板独立为 friend-panel.blade.php

- 新建 resources/views/chat/partials/friend-panel.blade.php
  包含完整的 style / HTML / JS
  结构完全干净,无嵌套错误
- toolbar.blade.php:
  恢复至干净基础版本(回滚损坏内容)
  添加「好友」按钮(openFriendPanel)
  通过 @include('chat.partials.friend-panel') 引入面板
- FriendController::index() 返回 sub_time 和 pending 列表
This commit is contained in:
2026-03-01 13:47:51 +08:00
parent 4ced484419
commit 8120058948
2 changed files with 541 additions and 441 deletions
@@ -0,0 +1,539 @@
{{--
文件功能:好友列表浮窗面板
提供全局 JS API
window.openFriendPanel() 打开好友面板(自动刷新列表)
window.closeFriendPanel() 关闭好友面板
面板分为两个区域:
1. 📋 我关注的好友 —— 我已添加的人,显示互相状态、添加时间、删除按钮
2. 💌 对方已加我 —— 别人加了我但我未回加,显示对方添加时间、回加按钮
搜索栏支持输入用户名后按 Enter 或点击「➕ 添加好友」直接添加。
所有添加/删除操作调用与双击用户卡片完全相同的后端接口:
POST /friend/{username}/add
DELETE /friend/{username}/remove
依赖:
- window.chatContext.roomId 当前房间 ID(由 frame.blade.php 注入)
- meta[name="csrf-token"] CSRF 令牌
@author ChatRoom Laravel
@version 1.0.0
--}}
{{-- ════ 样式 ════ --}}
<style>
#friend-panel {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 9999;
justify-content: center;
align-items: center;
}
#friend-panel-inner {
background: #fff;
border-radius: 8px;
width: 480px;
max-width: 96vw;
max-height: 86vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, .3);
overflow: hidden;
}
#friend-panel-header {
background: linear-gradient(135deg, #336699, #5a8fc0);
color: #fff;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
#friend-panel-title {
font-size: 14px;
font-weight: bold;
flex: 1;
}
#friend-panel-close {
cursor: pointer;
font-size: 18px;
opacity: .8;
line-height: 1;
transition: opacity .15s;
}
#friend-panel-close:hover {
opacity: 1;
}
#friend-add-bar {
padding: 8px 12px;
border-bottom: 1px solid #e8eef5;
display: flex;
gap: 6px;
background: #f6faff;
flex-shrink: 0;
}
#friend-search-input {
flex: 1;
padding: 5px 10px;
border: 1px solid #aac;
border-radius: 4px;
font-size: 12px;
outline: none;
}
#friend-search-input:focus {
border-color: #336699;
}
#friend-search-btn {
padding: 5px 14px;
background: #336699;
color: #fff;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
cursor: pointer;
}
#friend-search-btn:hover {
background: #2a5580;
}
#friend-panel-body {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
background: #f6faff;
}
.fp-section-title {
font-size: 11px;
font-weight: bold;
color: #336699;
padding: 4px 0 6px;
border-bottom: 1px solid #cde;
margin-bottom: 6px;
}
.fp-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 5px;
background: #fff;
margin-bottom: 5px;
border: 1px solid #e0eaf5;
font-size: 12px;
}
.fp-row:hover {
background: #eef5ff;
}
.fp-avatar {
width: 28px;
height: 28px;
border-radius: 3px;
border: 1px solid #aac;
flex-shrink: 0;
object-fit: cover;
}
.fp-name {
font-weight: bold;
color: #224466;
flex: 1;
}
.fp-badge {
font-size: 10px;
padding: 1px 6px;
border-radius: 10px;
flex-shrink: 0;
}
.fp-badge-mutual {
background: #e8fde8;
color: #16a34a;
}
.fp-badge-onesided {
background: #fff3e0;
color: #b45309;
}
.fp-date {
font-size: 10px;
color: #999;
flex-shrink: 0;
}
.fp-action-btn {
padding: 3px 10px;
border: none;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
cursor: pointer;
transition: opacity .15s;
flex-shrink: 0;
}
.fp-action-btn:hover {
opacity: .8;
}
.fp-btn-add {
background: #16a34a;
color: #fff;
}
.fp-btn-remove {
background: #dc2626;
color: #fff;
}
#fp-notice {
font-size: 11px;
color: #888;
text-align: center;
padding: 4px 0 2px;
min-height: 18px;
}
.fp-empty {
text-align: center;
color: #aaa;
font-size: 12px;
padding: 16px 0;
}
</style>
{{-- ════ HTML ════ --}}
<div id="friend-panel">
<div id="friend-panel-inner">
{{-- 标题栏 --}}
<div id="friend-panel-header">
<span id="friend-panel-title">👥 我的好友</span>
<span id="friend-panel-close" onclick="closeFriendPanel()"></span>
</div>
{{-- 搜索/添加新好友 --}}
<div id="friend-add-bar">
<input id="friend-search-input" type="text" maxlength="20" placeholder="输入用户名搜索并添加好友…"
onkeydown="if(event.key==='Enter') friendSearch()">
<button id="friend-search-btn" onclick="friendSearch()"> 添加好友</button>
</div>
{{-- 操作结果提示 --}}
<div id="fp-notice"></div>
{{-- 列表主体(由 JS 动态填充) --}}
<div id="friend-panel-body">
<div class="fp-empty">加载中…</div>
</div>
</div>
</div>
{{-- ════ JavaScript ════ --}}
<script>
/**
* 好友面板控制器。
*
* 功能:
* 1. 打开/关闭好友面板
* 2. 获取并渲染「我关注的好友」和「对方已加我」两个列表
* 3. 添加/删除好友(调用与双击用户卡片相同的后端接口)
* 4. 搜索用户名并直接添加为好友
*/
(function() {
/**
* 获取当前房间 IDframe.blade.php 注入的 chatContext)。
*
* @returns {number|null}
*/
const roomId = () => window.chatContext?.roomId ?? null;
/**
* 获取 CSRF Token。
*
* @returns {string}
*/
const csrf = () => document.querySelector('meta[name="csrf-token"]').content;
/**
* 打开好友面板并自动拉取最新数据。
*/
window.openFriendPanel = function() {
document.getElementById('friend-panel').style.display = 'flex';
loadFriends();
};
/**
* 关闭好友面板。
*/
window.closeFriendPanel = function() {
document.getElementById('friend-panel').style.display = 'none';
};
// 点击遮罩关闭
document.getElementById('friend-panel').addEventListener('click', function(e) {
if (e.target === this) {
closeFriendPanel();
}
});
/**
* 在面板顶部显示操作结果提示文字。
*
* @param {string} msg 提示文字
* @param {string} color 文字颜色(CSS 颜色值)
*/
function setNotice(msg, color) {
const el = document.getElementById('fp-notice');
el.textContent = msg;
el.style.color = color || '#888';
}
/**
* 从服务端拉取好友数据并渲染列表。
* 调用 GET /friends 返回 { friends, pending }
*/
function loadFriends() {
document.getElementById('friend-panel-body').innerHTML =
'<div class="fp-empty">加载中…</div>';
setNotice('');
fetch('/friends', {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': csrf()
}
})
.then(r => r.json())
.then(data => renderFriends(data.friends || [], data.pending || []))
.catch(() => {
document.getElementById('friend-panel-body').innerHTML =
'<div class="fp-empty">加载失败,请重试</div>';
});
}
/**
* 渲染两个列表:我关注的好友 + 对方已加我(待回加)。
*
* @param {Array} friends 我已添加的好友数组
* @param {Array} pending 对方已加我但我未回加的数组
*/
function renderFriends(friends, pending) {
const body = document.getElementById('friend-panel-body');
body.innerHTML = '';
// ── 我关注的好友 ──
const h1 = document.createElement('div');
h1.className = 'fp-section-title';
h1.textContent = '📋 我关注的好友(' + friends.length + '';
body.appendChild(h1);
if (friends.length === 0) {
appendEmpty(body, '还没有添加任何好友');
} else {
friends.forEach(f => body.appendChild(makeFriendRow(f)));
}
// ── 对方已加我(待回加)──
const h2 = document.createElement('div');
h2.className = 'fp-section-title';
h2.style.marginTop = '10px';
h2.textContent = '💌 对方已加我,待我回加(' + pending.length + '';
body.appendChild(h2);
if (pending.length === 0) {
appendEmpty(body, '暂无');
} else {
pending.forEach(p => body.appendChild(makePendingRow(p)));
}
}
/**
* 向容器追加空状态提示。
*
* @param {HTMLElement} container 父容器
* @param {string} text 提示文字
*/
function appendEmpty(container, text) {
const el = document.createElement('div');
el.className = 'fp-empty';
el.textContent = text;
container.appendChild(el);
}
/**
* 创建「我关注的好友」行(头像/用户名/互相徽章/添加时间/删除按钮)。
*
* @param {object} f 好友数据 { username, headface, mutual, sub_time }
* @returns {HTMLElement}
*/
function makeFriendRow(f) {
const row = document.createElement('div');
row.className = 'fp-row';
const avatar = document.createElement('img');
avatar.className = 'fp-avatar';
avatar.src = '/images/headface/' + (f.headface || '1.gif');
avatar.alt = f.username;
const name = document.createElement('span');
name.className = 'fp-name';
name.textContent = f.username;
const badge = document.createElement('span');
badge.className = 'fp-badge ' + (f.mutual ? 'fp-badge-mutual' : 'fp-badge-onesided');
badge.textContent = f.mutual ? '💚 互相好友' : '👤 单向关注';
const date = document.createElement('span');
date.className = 'fp-date';
date.textContent = f.sub_time || '';
const btn = document.createElement('button');
btn.className = 'fp-action-btn fp-btn-remove';
btn.textContent = '删除';
btn.onclick = () => friendAction('remove', f.username, btn);
row.append(avatar, name, badge, date, btn);
return row;
}
/**
* 创建「待回加」行(头像/用户名/对方添加时间/回加按钮)。
*
* @param {object} p 待回加用户数据 { username, headface, added_at }
* @returns {HTMLElement}
*/
function makePendingRow(p) {
const row = document.createElement('div');
row.className = 'fp-row';
const avatar = document.createElement('img');
avatar.className = 'fp-avatar';
avatar.src = '/images/headface/' + (p.headface || '1.gif');
avatar.alt = p.username;
const name = document.createElement('span');
name.className = 'fp-name';
name.textContent = p.username;
const date = document.createElement('span');
date.className = 'fp-date';
date.textContent = p.added_at ? '他于 ' + p.added_at + ' 添加了我' : '';
const btn = document.createElement('button');
btn.className = 'fp-action-btn fp-btn-add';
btn.textContent = ' 回加';
btn.onclick = () => friendAction('add', p.username, btn);
row.append(avatar, name, date, btn);
return row;
}
/**
* 执行添加或删除好友操作。
* 调用与双击用户卡片完全相同的后端接口。
*
* @param {'add'|'remove'} action 操作类型
* @param {string} username 目标用户名
* @param {HTMLElement} btn 触发按钮(操作期间禁用)
*/
async function friendAction(action, username, btn) {
btn.disabled = true;
btn.style.opacity = '0.5';
setNotice('');
try {
const res = await fetch('/friend/' + encodeURIComponent(username) + '/' + action, {
method: action === 'remove' ? 'DELETE' : 'POST',
headers: {
'X-CSRF-TOKEN': csrf(),
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: roomId()
}),
});
const data = await res.json();
if (data.status === 'success') {
setNotice(data.message, '#16a34a');
setTimeout(loadFriends, 700); // 延迟刷新列表
} else {
setNotice(data.message || '操作失败', '#dc2626');
btn.disabled = false;
btn.style.opacity = '1';
}
} catch (e) {
setNotice('网络异常,请重试', '#dc2626');
btn.disabled = false;
btn.style.opacity = '1';
}
}
/**
* 通过搜索框添加新好友(Enter 或点击按钮触发)。
* 用户名经后端验证(不能加自己、不能重复添加等)。
*/
window.friendSearch = async function() {
const input = document.getElementById('friend-search-input');
const username = input.value.trim();
if (!username) {
setNotice('请输入用户名', '#b45309');
return;
}
const btn = document.getElementById('friend-search-btn');
btn.disabled = true;
setNotice('正在添加…');
try {
const res = await fetch('/friend/' + encodeURIComponent(username) + '/add', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrf(),
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: roomId()
}),
});
const data = await res.json();
if (data.status === 'success') {
setNotice(data.message, '#16a34a');
input.value = '';
setTimeout(loadFriends, 700);
} else {
setNotice(data.message || '添加失败', '#dc2626');
}
} catch (e) {
setNotice('网络异常', '#dc2626');
} finally {
btn.disabled = false;
}
};
})();
</script>
+2 -441
View File
@@ -166,447 +166,8 @@
</div>
</div>
{{-- ═══════════ 工具条相关 JS 函数 ═══════════ --}}
<script>
{{-- ═══════════ 好友面板弹窗 ═══════════ --}}
<
style >
#friend - panel {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z - index: 9999;
justify - content: center;
align - items: center;
}
#friend - panel - inner {
background: #fff;
border - radius: 8 px;
width: 480 px;
max - width: 96 vw;
max - height: 86 vh;
display: flex;
flex - direction: column;
box - shadow: 0 8 px 32 px rgba(0, 0, 0, .3);
overflow: hidden;
}
#friend - panel - header {
background: linear - gradient(135 deg, #336699,# 5 a8fc0);
color: #fff;
padding: 10 px 16 px;
display: flex;
align - items: center;
gap: 10 px;
flex - shrink: 0;
}
#friend - panel - title {
font - size: 14 px;
font - weight: bold;
flex: 1;
}
#friend - panel - close {
cursor: pointer;font - size: 18 px;opacity: .8;line - height: 1;
transition: opacity .15 s;
}
#friend - panel - close: hover {
opacity: 1;
}
/* 搜索添加栏 */
#friend - add - bar {
padding: 8 px 12 px;
border - bottom: 1 px solid #e8eef5;
display: flex;
gap: 6 px;
background: #f6faff;
flex - shrink: 0;
}
#friend - search - input {
flex: 1;
padding: 5 px 10 px;
border: 1 px solid #aac;
border - radius: 4 px;
font - size: 12 px;
outline: none;
}
#friend - search - input: focus {
border - color: #336699; }
# friend - search - btn {
padding: 5 px 14 px;
background: #336699; color: # fff;
border: none;border - radius: 4 px;
font - size: 12 px;font - weight: bold;
cursor: pointer;
}
#friend - search - btn: hover {
background: #2a5580; }
/* 列表主体 */
# friend - panel - body {
flex: 1;
overflow - y: auto;
padding: 10 px 12 px;
background: #f6faff;
}
.fp - section - title {
font - size: 11 px;
font - weight: bold;
color: #336699;
padding: 4px 0 6px;
border-bottom: 1px solid # cde;
margin - bottom: 6 px;
}
.fp - row {
display: flex;
align - items: center;
gap: 8 px;
padding: 6 px 8 px;
border - radius: 5 px;
background: #fff;
margin - bottom: 5 px;
border: 1 px solid #e0eaf5;
font - size: 12 px;
}
.fp - row: hover {
background: #eef5ff;
}
.fp - avatar {
width: 28 px;height: 28 px;
border - radius: 3 px;border: 1 px solid #aac;
flex - shrink: 0;object - fit: cover;
}
.fp - name {
font - weight: bold;
color: #224466; flex: 1; }
.fp-badge {
font-size: 10px;
padding: 1px 6px;
border-radius: 10px;
flex-shrink: 0;
}
.fp-badge-mutual { background:# e8fde8;
color: #16a34a; }
.fp-badge-onesided{ background:# fff3e0;
color: #b45309;
}
.fp - date {
font - size: 10 px;
color: #999; flex-shrink: 0; }
.fp-action-btn {
padding: 3px 10px;
border: none; border-radius: 3px;
font-size: 11px; font-weight: bold;
cursor: pointer; transition: opacity .15s;
flex-shrink: 0;
}
.fp-action-btn:hover { opacity: .8; }
.fp-btn-add { background:# 16 a34a;
color: #fff;
}
.fp - btn - remove {
background: #dc2626;color: #fff;
}
/* 提示 */
#fp - notice {
font - size: 11 px;
color: #888;
text-align:center; padding:4px 0 2px;
min-height:18px;
}
.fp-empty { text-align:center; color:# aaa;
font - size: 12 px;
padding: 16 px 0;
} <
/style>
<
div id = "friend-panel" >
<
div id = "friend-panel-inner" >
<
div id = "friend-panel-header" >
<
span id = "friend-panel-title" > 👥我的好友 < /span> <
span id = "friend-panel-close"
onclick = "closeFriendPanel()" > < /span> <
/div>
{{-- 搜索添加新好友 --}} <
div id = "friend-add-bar" >
<
input id = "friend-search-input"
type = "text"
maxlength = "20"
placeholder = "输入用户名搜索并添加好友…"
onkeydown = "if(event.key==='Enter')friendSearch()" >
<
button id = "friend-search-btn"
onclick = "friendSearch()" > ➕添加好友 < /button> <
/div>
<
div id = "fp-notice" > < /div>
<
div id = "friend-panel-body" >
<
div class = "fp-empty"
id = "fp-loading" > 加载中… < /div> <
/div> <
/div> <
/div>
<
script >
/**
* 好友面板控制器。
* 复用 /friend/{username}/add /friend/{username}/remove 接口,
* 与双击用户卡片执行的是完全相同的后端逻辑。
*/
(function() {
const roomId = () => window.chatContext?.roomId ?? null;
const csrf = () => document.querySelector('meta[name="csrf-token"]').content;
/** 打开面板并刷新数据 */
window.openFriendPanel = function() {
document.getElementById('friend-panel').style.display = 'flex';
loadFriends();
};
/** 关闭面板 */
window.closeFriendPanel = function() {
document.getElementById('friend-panel').style.display = 'none';
};
// 点击遮罩关闭
document.getElementById('friend-panel').addEventListener('click', function(e) {
if (e.target === this) closeFriendPanel();
});
/** 设置面板顶部提示文字 */
function setNotice(msg, color = '#888') {
const el = document.getElementById('fp-notice');
el.textContent = msg;
el.style.color = color;
}
/** 拉取好友数据并渲染 */
function loadFriends() {
const body = document.getElementById('friend-panel-body');
body.innerHTML = '<div class="fp-empty" id="fp-loading">加载中…</div>';
setNotice('');
fetch('/friends', {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': csrf()
}
})
.then(r => r.json())
.then(data => renderFriends(data.friends || [], data.pending || []))
.catch(() => {
body.innerHTML = '<div class="fp-empty">加载失败,请重试</div>';
});
}
/** 渲染两个列表 */
function renderFriends(friends, pending) {
const body = document.getElementById('friend-panel-body');
body.innerHTML = '';
// ── 我添加的好友 ──
const h1 = document.createElement('div');
h1.className = 'fp-section-title';
h1.textContent = `📋 我关注的好友(${friends.length}`;
body.appendChild(h1);
if (friends.length === 0) {
const empty = document.createElement('div');
empty.className = 'fp-empty';
empty.textContent = '还没有添加任何好友';
body.appendChild(empty);
} else {
friends.forEach(f => body.appendChild(makeFriendRow(f)));
}
// ── 待回应(别人加了我) ──
const h2 = document.createElement('div');
h2.className = 'fp-section-title';
h2.style.marginTop = '10px';
h2.textContent = `💌 对方已加我,待我回加(${pending.length}`;
body.appendChild(h2);
if (pending.length === 0) {
const empty = document.createElement('div');
empty.className = 'fp-empty';
empty.textContent = '暂无';
body.appendChild(empty);
} else {
pending.forEach(p => body.appendChild(makePendingRow(p)));
}
}
/**
* 创建「我的好友」行(含互相徽章、日期、删除按钮)。
*
* @param {object} f 好友数据
* @returns {HTMLElement}
*/
function makeFriendRow(f) {
const row = document.createElement('div');
row.className = 'fp-row';
row.id = 'fp-row-friend-' + f.username;
const avatar = document.createElement('img');
avatar.className = 'fp-avatar';
avatar.src = '/images/headface/' + (f.headface || '1.gif');
avatar.alt = f.username;
const name = document.createElement('span');
name.className = 'fp-name';
name.textContent = f.username;
const badge = document.createElement('span');
badge.className = 'fp-badge ' + (f.mutual ? 'fp-badge-mutual' : 'fp-badge-onesided');
badge.textContent = f.mutual ? '💚 互相好友' : '👤 单向关注';
const date = document.createElement('span');
date.className = 'fp-date';
date.textContent = f.sub_time || '';
const btn = document.createElement('button');
btn.className = 'fp-action-btn fp-btn-remove';
btn.textContent = '删除';
btn.onclick = () => friendAction('remove', f.username, btn);
row.append(avatar, name, badge, date, btn);
return row;
}
/**
* 创建「待回加」行(添加时间、添加好友按钮)。
*
* @param {object} p 待回加用户数据
* @returns {HTMLElement}
*/
function makePendingRow(p) {
const row = document.createElement('div');
row.className = 'fp-row';
row.id = 'fp-row-pending-' + p.username;
const avatar = document.createElement('img');
avatar.className = 'fp-avatar';
avatar.src = '/images/headface/' + (p.headface || '1.gif');
avatar.alt = p.username;
const name = document.createElement('span');
name.className = 'fp-name';
name.textContent = p.username;
const date = document.createElement('span');
date.className = 'fp-date';
date.textContent = p.added_at ? '他于 ' + p.added_at + ' 添加了我' : '';
const btn = document.createElement('button');
btn.className = 'fp-action-btn fp-btn-add';
btn.textContent = ' 回加';
btn.onclick = () => friendAction('add', p.username, btn, true);
row.append(avatar, name, date, btn);
return row;
}
/**
* 执行添加/删除好友操作(与双击用户卡片调用完全相同的后端接口)。
*
* @param {'add'|'remove'} action 操作类型
* @param {string} username 目标用户名
* @param {HTMLElement} btn 触发的按钮(用于禁用防重复)
* @param {boolean} isPending 是否来自 pending 区域
*/
async function friendAction(action, username, btn, isPending = false) {
btn.disabled = true;
btn.style.opacity = '0.5';
setNotice('');
try {
const isRemove = action === 'remove';
const res = await fetch(`/friend/${encodeURIComponent(username)}/${action}`, {
method: isRemove ? 'DELETE' : 'POST',
headers: {
'X-CSRF-TOKEN': csrf(),
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: roomId()
}),
});
const data = await res.json();
if (data.status === 'success') {
setNotice(data.message, '#16a34a');
// 小延迟后刷新列表
setTimeout(loadFriends, 700);
} else {
setNotice(data.message || '操作失败', '#dc2626');
btn.disabled = false;
btn.style.opacity = '1';
}
} catch (e) {
setNotice('网络异常,操作失败', '#dc2626');
btn.disabled = false;
btn.style.opacity = '1';
}
}
/**
* 搜索用户并添加为好友。
* 输入框中的用户名经后端验证后执行添加。
*/
window.friendSearch = async function() {
const input = document.getElementById('friend-search-input');
const username = input.value.trim();
if (!username) {
setNotice('请输入用户名', '#b45309');
return;
}
const btn = document.getElementById('friend-search-btn');
btn.disabled = true;
setNotice('正在添加…');
try {
const res = await fetch(`/friend/${encodeURIComponent(username)}/add`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrf(),
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: roomId()
}),
});
const data = await res.json();
if (data.status === 'success') {
setNotice(data.message, '#16a34a');
input.value = '';
setTimeout(loadFriends, 700);
} else {
setNotice(data.message || '添加失败', '#dc2626');
}
} catch (e) {
setNotice('网络异常', '#dc2626');
} finally {
btn.disabled = false;
}
};
})();
</script>
{{-- ═══════════ 好友面板(独立文件)═══════════ --}}
@include('chat.partials.friend-panel')
{{-- ═══════════ 工具条相关 JS 函数 ═══════════ --}}
<script>