Files
chatroom/resources/views/chat/partials/friend-panel.blade.php

574 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{--
文件功能:好友列表浮窗面板
提供全局 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;
}
/* 在线状态小圆点 */
.fp-status {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10px;
font-weight: bold;
padding: 1px 6px;
border-radius: 10px;
flex-shrink: 0;
}
.fp-status-online {
background: #dcfce7;
color: #15803d;
}
.fp-status-offline {
background: #f1f5f9;
color: #94a3b8;
}
</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';
let hf = f.headface || '1.gif';
avatar.src = hf.startsWith('storage/') ? '/' + hf : '/images/headface/' + hf;
avatar.alt = f.username;
const name = document.createElement('span');
name.className = 'fp-name';
name.textContent = f.username;
// 在线状态标记(映名后)
const status = document.createElement('span');
status.className = 'fp-status ' + (f.is_online ? 'fp-status-online' : 'fp-status-offline');
status.textContent = f.is_online ? '🟢 在线' : '⚫ 离线';
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, status, 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';
let hf = p.headface || '1.gif';
avatar.src = hf.startsWith('storage/') ? '/' + hf : '/images/headface/' + hf;
avatar.alt = p.username;
const name = document.createElement('span');
name.className = 'fp-name';
name.textContent = p.username;
// 在线状态标记
const status = document.createElement('span');
status.className = 'fp-status ' + (p.is_online ? 'fp-status-online' : 'fp-status-offline');
status.textContent = p.is_online ? '🟢 在线' : '⚫ 离线';
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, status, 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>