Files
guzhujushiBlog/frontend/comment.js
T

126 lines
5.4 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.
/* =====================================================================
* 评论模块(comment.js
* 评论列表(按 parent_id 分组展示回复)+ 发表评论 / 回复表单。
* 仅好友或博主可评论。
* ===================================================================== */
import { state } from "./state.js";
import { $, escapeHtml, formatDate, showToast } from "./utils.js";
import { api } from "./api.js";
import { openLoginModal } from "./auth.js";
import { applyFriend } from "./friend.js";
export async function renderComments(el, articleId) {
let comments = [];
try {
comments = await api(`/api/comment/list?article_id=${articleId}`);
} catch (err) { /* 好友文章对游客不可见评论,忽略 */ }
el.innerHTML = `<p class="muted">共 ${comments.length} 条评论</p>`;
const list = document.createElement("div");
list.className = "comment-list";
if (!comments.length) list.innerHTML = '<p class="empty">暂无评论,来抢沙发吧</p>';
// 按 parent_id 分组:顶层评论在前,回复跟随其父评论
const byId = new Map(comments.map((c) => [c.id, c]));
const topLevel = comments.filter((c) => c.parent_id == null);
const repliesByParent = {};
comments.filter((c) => c.parent_id != null).forEach((r) => {
(repliesByParent[r.parent_id] = repliesByParent[r.parent_id] || []).push(r);
});
topLevel.forEach((c) => {
list.appendChild(buildCommentItem(el, articleId, c, byId, false));
(repliesByParent[c.id] || []).forEach((r) => {
list.appendChild(buildCommentItem(el, articleId, r, byId, true));
});
});
el.appendChild(list);
// 评论 / 回复表单(仅好友或博主)
const canComment = state.user && (state.user.role === "friend" || state.user.role === "blogger");
if (canComment) {
el.appendChild(buildCommentForm(el, articleId, null));
} else {
const hint = document.createElement("p");
hint.className = "muted comment-hint";
if (state.user) {
hint.innerHTML = `仅好友或博主可以评论 <button class="btn btn-outline btn-sm" id="hint-apply">申请好友</button>`;
$("#hint-apply", hint).addEventListener("click", applyFriend);
} else {
hint.innerHTML = `登录并成为好友后可以评论 <button class="btn btn-outline btn-sm" id="hint-login">去登录</button>`;
$("#hint-login", hint).addEventListener("click", openLoginModal);
}
el.appendChild(hint);
}
}
function buildCommentItem(el, articleId, comment, byId, isReply) {
const item = document.createElement("div");
item.className = isReply ? "comment-item comment-reply" : "comment-item";
const parent = comment.parent_id && byId.has(comment.parent_id) ? byId.get(comment.parent_id) : null;
const parentLabel = parent ? `<span class="muted">回复 @${escapeHtml(parent.username || "匿名")}</span>` : "";
// 评论者头像:有头像显示图片,没有则显示空占位
const avatarHtml = comment.avatar
? `<img class="comment-avatar" src="${escapeHtml(comment.avatar)}" alt="">`
: '<span class="comment-avatar comment-avatar-empty"></span>';
item.innerHTML = `
<div class="comment-head">
<div class="comment-author">
${avatarHtml}
<strong>${escapeHtml(comment.username || "匿名")}</strong>
${parentLabel}
</div>
<span class="muted">${formatDate(comment.created_time)}</span>
</div>
<p>${escapeHtml(comment.content)}</p>`;
// 顶层评论显示“回复”按钮(仅好友/博主)
const canReply = state.user && (state.user.role === "friend" || state.user.role === "blogger");
if (canReply && !isReply) {
const replyBtn = document.createElement("button");
replyBtn.className = "btn btn-text btn-sm comment-reply-btn";
replyBtn.textContent = "回复";
replyBtn.addEventListener("click", () => {
let box = item.querySelector(".comment-reply-box");
if (box) { box.remove(); return; }
box = document.createElement("div");
box.className = "comment-reply-box";
box.appendChild(buildCommentForm(el, articleId, comment));
item.appendChild(box);
});
item.appendChild(replyBtn);
}
return item;
}
function buildCommentForm(el, articleId, parent) {
const form = document.createElement("div");
form.className = "comment-form";
const placeholder = parent
? `回复 @${parent.username || "匿名"}…`
: "写下你的评论…";
const btnText = parent ? "回复" : "发表评论";
form.innerHTML = `
<textarea class="comment-input" rows="2" maxlength="2000" placeholder="${escapeHtml(placeholder)}"></textarea>
<button class="btn btn-primary" type="button">${btnText}</button>`;
const textarea = $(".comment-input", form);
const btn = $("button", form);
btn.addEventListener("click", async () => {
const content = textarea.value.trim();
if (!content) { showToast("评论内容不能为空", "error"); return; }
if (content.length > 2000) { showToast("评论内容不能超过 2000 字", "error"); return; }
btn.disabled = true; btn.textContent = "发送中…";
try {
const body = parent
? { article_id: articleId, content, parent_id: parent.id }
: { article_id: articleId, content };
await api("/api/comment/add", { method: "POST", body });
showToast("评论成功", "success");
await renderComments(el, articleId);
} catch (err) {
showToast(err.message, "error");
}
});
return form;
}