Files
chatroom/resources/js/chat-room/friend-panel.js
T

383 lines
11 KiB
JavaScript
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.
// 好友面板事件绑定与列表渲染,替代 Blade 内联脚本。
let friendPanelEventsBound = false;
/**
* 获取当前房间 ID。
*
* @returns {number|null}
*/
function roomId() {
return window.chatContext?.roomId ?? null;
}
/**
* 获取 CSRF Token。
*
* @returns {string}
*/
function csrf() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
}
/**
* 在面板顶部显示操作结果提示文字。
*
* @param {string} message 提示文字
* @param {string} color 文字颜色
* @returns {void}
*/
function setNotice(message, color = "#888") {
const notice = document.getElementById("fp-notice");
if (!notice) {
return;
}
notice.textContent = message;
notice.style.color = color;
}
/**
* 打开好友面板并自动拉取最新好友数据。
*
* @returns {void}
*/
export function openFriendPanel() {
const panel = document.getElementById("friend-panel");
if (!panel) {
return;
}
panel.style.display = "flex";
loadFriends();
}
/**
* 关闭好友面板。
*
* @returns {void}
*/
export function closeFriendPanel() {
const panel = document.getElementById("friend-panel");
if (panel) {
panel.style.display = "none";
}
}
/**
* 从服务端读取好友列表,接口返回 friends 与 pending 两组数据。
*
* @returns {void}
*/
export function loadFriends() {
const body = document.getElementById("friend-panel-body");
if (!body) {
return;
}
body.innerHTML = '<div class="fp-empty">加载中…</div>';
setNotice("");
fetch("/friends", {
headers: {
Accept: "application/json",
"X-CSRF-TOKEN": csrf(),
},
})
.then((response) => response.json())
.then((data) => renderFriends(data.friends || [], data.pending || []))
.catch(() => {
body.innerHTML = '<div class="fp-empty">加载失败,请重试</div>';
});
}
/**
* 渲染好友列表与待回加列表。
*
* @param {Array<Record<string, unknown>>} friends 我已添加的好友
* @param {Array<Record<string, unknown>>} pending 对方已加我但我未回加的用户
* @returns {void}
*/
function renderFriends(friends, pending) {
const body = document.getElementById("friend-panel-body");
if (!body) {
return;
}
body.innerHTML = "";
const friendTitle = document.createElement("div");
friendTitle.className = "fp-section-title";
friendTitle.textContent = `📋 我关注的好友(${friends.length}`;
body.appendChild(friendTitle);
if (friends.length === 0) {
appendEmpty(body, "还没有添加任何好友");
} else {
friends.forEach((friend) => body.appendChild(makeFriendRow(friend)));
}
const pendingTitle = document.createElement("div");
pendingTitle.className = "fp-section-title";
pendingTitle.style.marginTop = "10px";
pendingTitle.textContent = `💌 对方已加我,待我回加(${pending.length}`;
body.appendChild(pendingTitle);
if (pending.length === 0) {
appendEmpty(body, "暂无");
} else {
pending.forEach((pendingUser) => body.appendChild(makePendingRow(pendingUser)));
}
}
/**
* 向容器追加空状态提示。
*
* @param {HTMLElement} container 父容器
* @param {string} text 提示文字
* @returns {void}
*/
function appendEmpty(container, text) {
const empty = document.createElement("div");
empty.className = "fp-empty";
empty.textContent = text;
container.appendChild(empty);
}
/**
* 解析好友头像路径。
*
* @param {unknown} headface 头像字段
* @returns {string}
*/
function resolveFriendAvatar(headface) {
const avatar = String(headface || "1.gif");
return avatar.startsWith("storage/") ? `/${avatar}` : `/images/headface/${avatar}`;
}
/**
* 创建我关注的好友行。
*
* @param {Record<string, unknown>} friend 好友数据
* @returns {HTMLElement}
*/
function makeFriendRow(friend) {
const row = document.createElement("div");
row.className = "fp-row";
const username = String(friend.username || "");
const avatar = document.createElement("img");
avatar.className = "fp-avatar";
avatar.src = resolveFriendAvatar(friend.headface);
avatar.alt = username;
const name = document.createElement("span");
name.className = "fp-name";
name.textContent = username;
const status = document.createElement("span");
status.className = `fp-status ${friend.is_online ? "fp-status-online" : "fp-status-offline"}`;
status.textContent = friend.is_online ? "🟢 在线" : "⚫ 离线";
const badge = document.createElement("span");
badge.className = `fp-badge ${friend.mutual ? "fp-badge-mutual" : "fp-badge-onesided"}`;
badge.textContent = friend.mutual ? "💚 互相好友" : "👤 单向关注";
const date = document.createElement("span");
date.className = "fp-date";
date.textContent = String(friend.sub_time || "");
const button = document.createElement("button");
button.className = "fp-action-btn fp-btn-remove";
button.textContent = "删除";
button.addEventListener("click", () => {
void friendAction("remove", username, button);
});
row.append(avatar, name, status, badge, date, button);
return row;
}
/**
* 创建待回加用户行。
*
* @param {Record<string, unknown>} pendingUser 待回加用户数据
* @returns {HTMLElement}
*/
function makePendingRow(pendingUser) {
const row = document.createElement("div");
row.className = "fp-row";
const username = String(pendingUser.username || "");
const avatar = document.createElement("img");
avatar.className = "fp-avatar";
avatar.src = resolveFriendAvatar(pendingUser.headface);
avatar.alt = username;
const name = document.createElement("span");
name.className = "fp-name";
name.textContent = username;
const status = document.createElement("span");
status.className = `fp-status ${pendingUser.is_online ? "fp-status-online" : "fp-status-offline"}`;
status.textContent = pendingUser.is_online ? "🟢 在线" : "⚫ 离线";
const date = document.createElement("span");
date.className = "fp-date";
date.textContent = pendingUser.added_at ? `他于 ${pendingUser.added_at} 添加了我` : "";
const button = document.createElement("button");
button.className = "fp-action-btn fp-btn-add";
button.textContent = " 回加";
button.addEventListener("click", () => {
void friendAction("add", username, button);
});
row.append(avatar, name, status, date, button);
return row;
}
/**
* 添加或删除好友,与双击用户卡片复用同一组后端接口。
*
* @param {"add"|"remove"} action 操作类型
* @param {string} username 目标用户名
* @param {HTMLButtonElement} button 触发按钮
* @returns {Promise<void>}
*/
async function friendAction(action, username, button) {
button.disabled = true;
button.style.opacity = "0.5";
setNotice("");
try {
const response = 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 response.json();
if (data.status === "success") {
setNotice(data.message, "#16a34a");
// 延迟刷新,等待后端好友状态和通知链路落定。
setTimeout(loadFriends, 700);
return;
}
setNotice(data.message || "操作失败", "#dc2626");
button.disabled = false;
button.style.opacity = "1";
} catch (error) {
setNotice("网络异常,请重试", "#dc2626");
button.disabled = false;
button.style.opacity = "1";
}
}
/**
* 通过搜索框按用户名添加好友,具体校验仍交给后端。
*
* @returns {Promise<void>}
*/
export async function friendSearch() {
const input = document.getElementById("friend-search-input");
if (!(input instanceof HTMLInputElement)) {
return;
}
const username = input.value.trim();
if (!username) {
setNotice("请输入用户名", "#b45309");
return;
}
const button = document.getElementById("friend-search-btn");
if (!(button instanceof HTMLButtonElement)) {
return;
}
button.disabled = true;
setNotice("正在添加…");
try {
const response = 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 response.json();
if (data.status === "success") {
setNotice(data.message, "#16a34a");
input.value = "";
// 延迟刷新,等待后端好友状态和通知链路落定。
setTimeout(loadFriends, 700);
} else {
setNotice(data.message || "添加失败", "#dc2626");
}
} catch (error) {
setNotice("网络异常", "#dc2626");
} finally {
button.disabled = false;
}
}
/**
* 绑定好友面板关闭、遮罩关闭与搜索入口事件。
*
* @returns {void}
*/
export function bindFriendPanelControls() {
if (friendPanelEventsBound || typeof document === "undefined") {
return;
}
friendPanelEventsBound = true;
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
if (event.target.closest("[data-friend-panel-close]")) {
event.preventDefault();
closeFriendPanel();
return;
}
if (event.target.closest("[data-friend-search]")) {
event.preventDefault();
void friendSearch();
return;
}
const panel = event.target.closest("#friend-panel");
// 只在点击遮罩本身时关闭,避免点击内容区误关。
if (panel && event.target === panel) {
closeFriendPanel();
}
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Enter" || event.target?.id !== "friend-search-input") {
return;
}
event.preventDefault();
void friendSearch();
});
}