126 lines
5.4 KiB
JavaScript
126 lines
5.4 KiB
JavaScript
/* =====================================================================
|
||
* 评论模块(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;
|
||
} |