Initial commit: MyBlog full stack blog

This commit is contained in:
2026-08-22 22:28:41 +08:00
commit c61193f2af
64 changed files with 7290 additions and 0 deletions
+126
View File
@@ -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;
}