好的,让我们来增强评论系统的功能,实现回复、评论分页和邮件通知功能。
第十五部分:增强评论系统
- 首先,我们需要更新数据库结构以支持嵌套评论。修改
src/db/schema.ts:
import { integer, sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core";
// ... 其他表定义保持不变
export const comments = sqliteTable("comments", {
id: integer("id").primaryKey({ autoIncrement: true }),
content: text("content").notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
postId: integer("post_id").notNull().references(() => posts.id),
userId: integer("user_id").notNull().references(() => users.id),
parentId: integer("parent_id").references(() => comments.id),
});运行数据库迁移以应用这些更改。
- 安装必要的依赖:
bun add nodemailer- 更新
src/routes/posts.ts文件,修改获取评论的逻辑以支持嵌套评论和分页:
import { eq, and, desc, sql } from "drizzle-orm";
import { comments } from "../db/schema";
import nodemailer from "nodemailer";
// ... 其他导入和代码保持不变
const COMMENTS_PER_PAGE = 10;
.get("/posts/:id/comments", async ({ params, query }) => {
const page = parseInt(query.page) || 1;
const offset = (page - 1) * COMMENTS_PER_PAGE;
const allComments = await db.select({
id: comments.id,
content: comments.content,
createdAt: comments.createdAt,
userId: comments.userId,
username: users.username,
parentId: comments.parentId,
})
.from(comments)
.leftJoin(users, eq(comments.userId, users.id))
.where(eq(comments.postId, parseInt(params.id)))
.orderBy(desc(comments.createdAt))
.all();
const totalComments = allComments.length;
const paginatedComments = allComments.slice(offset, offset + COMMENTS_PER_PAGE);
const nestedComments = buildNestedComments(paginatedComments);
return {
comments: nestedComments,
totalPages: Math.ceil(totalComments / COMMENTS_PER_PAGE),
currentPage: page,
};
})
.post("/posts/:id/comments", async ({ params, body, getCurrentUser }) => {
const user = await getCurrentUser();
if (!user) {
throw new Error("Not authorized");
}
const { content, parentId } = body;
const [newComment] = await db.insert(comments)
.values({
content,
postId: parseInt(params.id),
userId: user.id,
parentId: parentId ? parseInt(parentId) : null,
})
.returning();
// 获取文章作者的邮箱
const [post] = await db.select({
authorId: posts.authorId,
title: posts.title,
authorEmail: users.email,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.id, parseInt(params.id)))
.limit(1)
.all();
// 发送邮件通知
if (post && post.authorId !== user.id) {
await sendCommentNotification(post.authorEmail, post.title, user.username);
}
return newComment;
})
// 辅助函数:构建嵌套评论
function buildNestedComments(comments) {
const commentMap = new Map();
const rootComments = [];
comments.forEach(comment => {
commentMap.set(comment.id, { ...comment, replies: [] });
});
comments.forEach(comment => {
if (comment.parentId) {
const parentComment = commentMap.get(comment.parentId);
if (parentComment) {
parentComment.replies.push(commentMap.get(comment.id));
}
} else {
rootComments.push(commentMap.get(comment.id));
}
});
return rootComments;
}
// 辅助函数:发送邮件通知
async function sendCommentNotification(recipientEmail, postTitle, commenterUsername) {
const transporter = nodemailer.createTransport({
// 配置你的邮件服务器设置
host: "smtp.example.com",
port: 587,
secure: false,
auth: {
user: "your-email@example.com",
pass: "your-email-password",
},
});
await transporter.sendMail({
from: '"Your Blog" <noreply@yourblog.com>',
to: recipientEmail,
subject: `New comment on your post: ${postTitle}`,
text: `${commenterUsername} has commented on your post "${postTitle}". Check it out!`,
html: `<p>${commenterUsername} has commented on your post "<strong>${postTitle}</strong>". <a href="https://yourblog.com/posts/${postId}">Check it out!</a></p>`,
});
}- 更新
src/views/posts/show.eta文件,以支持嵌套评论和分页:
<% layout('../layouts/main.eta', { title: it.post.title }) %>
<!-- ... 文章内容部分保持不变 ... -->
<h2>Comments</h2>
<% if (it.user) { %>
<form id="commentForm" action="/posts/<%= it.post.id %>/comments" method="POST">
<textarea name="content" required></textarea>
<input type="hidden" name="parentId" id="parentId" value="">
<button type="submit">Add Comment</button>
</form>
<% } else { %>
<p>Please <a href="/login">login</a> to leave a comment.</p>
<% } %>
<div id="commentsContainer"></div>
<div id="pagination"></div>
<script>
let currentPage = 1;
function fetchComments(page = 1) {
fetch(`/posts/<%= it.post.id %>/comments?page=${page}`)
.then(response => response.json())
.then(data => {
renderComments(data.comments);
renderPagination(data.totalPages, data.currentPage);
currentPage = data.currentPage;
});
}
function renderComments(comments, container = document.getElementById('commentsContainer'), level = 0) {
container.innerHTML = '';
comments.forEach(comment => {
const commentElement = document.createElement('div');
commentElement.className = `comment level-${level}`;
commentElement.innerHTML = `
<p>${comment.content}</p>
<small>By ${comment.username} on ${new Date(comment.createdAt).toLocaleString()}</small>
<button onclick="replyTo(${comment.id})">Reply</button>
`;
container.appendChild(commentElement);
if (comment.replies && comment.replies.length > 0) {
const repliesContainer = document.createElement('div');
repliesContainer.className = 'replies';
commentElement.appendChild(repliesContainer);
renderComments(comment.replies, repliesContainer, level + 1);
}
});
}
function renderPagination(totalPages, currentPage) {
const paginationContainer = document.getElementById('pagination');
paginationContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const pageButton = document.createElement('button');
pageButton.textContent = i;
pageButton.onclick = () => fetchComments(i);
if (i === currentPage) {
pageButton.disabled = true;
}
paginationContainer.appendChild(pageButton);
}
}
function replyTo(commentId) {
document.getElementById('parentId').value = commentId;
document.getElementById('commentForm').scrollIntoView({ behavior: 'smooth' });
}
document.getElementById('commentForm').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST',
body: formData,
});
if (response.ok) {
form.reset();
document.getElementById('parentId').value = '';
fetchComments(currentPage);
}
});
fetchComments();
</script>
<style>
.comment {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ccc;
}
.replies {
margin-left: 20px;
}
.level-0 { background-color: #f9f9f9; }
.level-1 { background-color: #f0f0f0; }
.level-2 { background-color: #e6e6e6; }
</style>- 在
src/index.ts中添加新的路由来处理评论请求:
import { postRoutes } from "./routes/posts";
// ... 其他代码保持不变
app.get("/posts/:id/comments", async (context) => {
return await postRoutes.handle(context);
});
app.post("/posts/:id/comments", async (context) => {
return await postRoutes.handle(context);
});- 更新
public/styles/main.css文件,添加一些评论相关的样式:
/* ... 前面的样式保持不变 */
.comment {
margin-bottom: 15px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.comment p {
margin-bottom: 5px;
}
.comment small {
color: #666;
}
.replies {
margin-left: 20px;
border-left: 2px solid #ddd;
padding-left: 10px;
}
#pagination {
margin-top: 20px;
}
#pagination button {
margin-right: 5px;
padding: 5px 10px;
background-color: #f0f0f0;
border: 1px solid #ddd;
cursor: pointer;
}
#pagination button:disabled {
background-color: #ddd;
cursor: default;
}这些更改实现了增强的评论系统功能:
- 支持嵌套评论(回复功能)
- 实现了评论分页
- 添加了邮件通知功能,当有新评论时通知文章作者
主要改进包括:
- 更新了数据库结构以支持嵌套评论
- 修改了后端逻辑以处理嵌套评论和分页
- 更新了前端代码以显示嵌套评论和分页控件
- 添加了回复功能和相应的 UI 元素
- 实现了邮件通知功能
注意:为了使邮件通知功能正常工作,你需要配置一个有效的 SMTP 服务器。在生产环境中,你可能需要使用像 SendGrid 或 Mailgun 这样的邮件服务提供商。
这个增强的评论系统提供了更好的用户体验,允许更深入的讨论,并通过分页提高了性能。邮件通知功能也有助于提高用户参与度。
接下来,我们可以考虑添加以下功能:
- 文章搜索功能
- 用户头像上传
- 文章预览功能
- 标签云或热门标签功能
你希望继续哪个方向,或者有其他想法吗?