迁移好友面板脚本到Vite模块
This commit is contained in:
@@ -4,6 +4,7 @@ export { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
|||||||
export { bindGlobalDialogControls } from "./chat-room/dialog.js";
|
export { bindGlobalDialogControls } from "./chat-room/dialog.js";
|
||||||
export { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js";
|
export { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js";
|
||||||
export { bindChatImageUploadControl } from "./chat-room/image-upload.js";
|
export { bindChatImageUploadControl } from "./chat-room/image-upload.js";
|
||||||
|
export { bindFriendPanelControls, closeFriendPanel, friendSearch, loadFriends, openFriendPanel } from "./chat-room/friend-panel.js";
|
||||||
export { closeChatImageLightbox, initChatImageLightboxEvents, openChatImageLightbox } from "./chat-room/lightbox.js";
|
export { closeChatImageLightbox, initChatImageLightboxEvents, openChatImageLightbox } from "./chat-room/lightbox.js";
|
||||||
export { bindMobileDrawerControls } from "./chat-room/mobile-drawer.js";
|
export { bindMobileDrawerControls } from "./chat-room/mobile-drawer.js";
|
||||||
export { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js";
|
export { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js";
|
||||||
@@ -36,6 +37,7 @@ import { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
|||||||
import { bindGlobalDialogControls } from "./chat-room/dialog.js";
|
import { bindGlobalDialogControls } from "./chat-room/dialog.js";
|
||||||
import { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js";
|
import { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js";
|
||||||
import { bindChatImageUploadControl } from "./chat-room/image-upload.js";
|
import { bindChatImageUploadControl } from "./chat-room/image-upload.js";
|
||||||
|
import { bindFriendPanelControls, closeFriendPanel, friendSearch, loadFriends, openFriendPanel } from "./chat-room/friend-panel.js";
|
||||||
import { closeChatImageLightbox, initChatImageLightboxEvents, openChatImageLightbox } from "./chat-room/lightbox.js";
|
import { closeChatImageLightbox, initChatImageLightboxEvents, openChatImageLightbox } from "./chat-room/lightbox.js";
|
||||||
import { bindMobileDrawerControls } from "./chat-room/mobile-drawer.js";
|
import { bindMobileDrawerControls } from "./chat-room/mobile-drawer.js";
|
||||||
import { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js";
|
import { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js";
|
||||||
@@ -72,6 +74,11 @@ if (typeof window !== "undefined") {
|
|||||||
applyFontSize,
|
applyFontSize,
|
||||||
bindChatFontSizeControl,
|
bindChatFontSizeControl,
|
||||||
bindChatImageUploadControl,
|
bindChatImageUploadControl,
|
||||||
|
bindFriendPanelControls,
|
||||||
|
closeFriendPanel,
|
||||||
|
friendSearch,
|
||||||
|
loadFriends,
|
||||||
|
openFriendPanel,
|
||||||
bindMobileDrawerControls,
|
bindMobileDrawerControls,
|
||||||
bindWelcomeMenuControls,
|
bindWelcomeMenuControls,
|
||||||
CHAT_FONT_SIZE_STORAGE_KEY,
|
CHAT_FONT_SIZE_STORAGE_KEY,
|
||||||
@@ -103,10 +110,14 @@ if (typeof window !== "undefined") {
|
|||||||
|
|
||||||
window.closeChatImageLightbox = closeChatImageLightbox;
|
window.closeChatImageLightbox = closeChatImageLightbox;
|
||||||
window.openChatImageLightbox = openChatImageLightbox;
|
window.openChatImageLightbox = openChatImageLightbox;
|
||||||
|
window.closeFriendPanel = closeFriendPanel;
|
||||||
|
window.friendSearch = friendSearch;
|
||||||
|
window.openFriendPanel = openFriendPanel;
|
||||||
window.applyFontSize = applyFontSize;
|
window.applyFontSize = applyFontSize;
|
||||||
bindGlobalDialogControls();
|
bindGlobalDialogControls();
|
||||||
bindChatFontSizeControl();
|
bindChatFontSizeControl();
|
||||||
bindChatImageUploadControl();
|
bindChatImageUploadControl();
|
||||||
|
bindFriendPanelControls();
|
||||||
bindChatRightPanelControls();
|
bindChatRightPanelControls();
|
||||||
bindMobileDrawerControls();
|
bindMobileDrawerControls();
|
||||||
bindWelcomeMenuControls();
|
bindWelcomeMenuControls();
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
// 好友面板事件绑定与列表渲染,替代 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -251,14 +251,13 @@
|
|||||||
{{-- 标题栏 --}}
|
{{-- 标题栏 --}}
|
||||||
<div id="friend-panel-header">
|
<div id="friend-panel-header">
|
||||||
<span id="friend-panel-title">👥 我的好友</span>
|
<span id="friend-panel-title">👥 我的好友</span>
|
||||||
<span id="friend-panel-close" onclick="closeFriendPanel()">✕</span>
|
<span id="friend-panel-close" data-friend-panel-close>✕</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- 搜索/添加新好友 --}}
|
{{-- 搜索/添加新好友 --}}
|
||||||
<div id="friend-add-bar">
|
<div id="friend-add-bar">
|
||||||
<input id="friend-search-input" type="text" maxlength="20" placeholder="输入用户名搜索并添加好友…"
|
<input id="friend-search-input" type="text" maxlength="20" placeholder="输入用户名搜索并添加好友…">
|
||||||
onkeydown="if(event.key==='Enter') friendSearch()">
|
<button id="friend-search-btn" data-friend-search>➕ 添加好友</button>
|
||||||
<button id="friend-search-btn" onclick="friendSearch()">➕ 添加好友</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- 操作结果提示 --}}
|
{{-- 操作结果提示 --}}
|
||||||
@@ -270,304 +269,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ════ JavaScript ════ --}}
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 好友面板控制器。
|
|
||||||
*
|
|
||||||
* 功能:
|
|
||||||
* 1. 打开/关闭好友面板
|
|
||||||
* 2. 获取并渲染「我关注的好友」和「对方已加我」两个列表
|
|
||||||
* 3. 添加/删除好友(调用与双击用户卡片相同的后端接口)
|
|
||||||
* 4. 搜索用户名并直接添加为好友
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
/**
|
|
||||||
* 获取当前房间 ID(frame.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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user