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;
|
|||
|
|
}
|