Initial commit: MyBlog full stack blog
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/* =====================================================================
|
||||
* 评论模块(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;
|
||||
}
|
||||
Reference in New Issue
Block a user