好的,让我们来增强评论系统的功能,实现回复、评论分页和邮件通知功能。

第十五部分:增强评论系统

  1. 首先,我们需要更新数据库结构以支持嵌套评论。修改 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),
});

运行数据库迁移以应用这些更改。

  1. 安装必要的依赖:
bun add nodemailer
  1. 更新 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>`,
  });
}
  1. 更新 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>
  1. 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);
});
  1. 更新 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;
}

这些更改实现了增强的评论系统功能:

  1. 支持嵌套评论(回复功能)
  2. 实现了评论分页
  3. 添加了邮件通知功能,当有新评论时通知文章作者

主要改进包括:

  • 更新了数据库结构以支持嵌套评论
  • 修改了后端逻辑以处理嵌套评论和分页
  • 更新了前端代码以显示嵌套评论和分页控件
  • 添加了回复功能和相应的 UI 元素
  • 实现了邮件通知功能

注意:为了使邮件通知功能正常工作,你需要配置一个有效的 SMTP 服务器。在生产环境中,你可能需要使用像 SendGrid 或 Mailgun 这样的邮件服务提供商。

这个增强的评论系统提供了更好的用户体验,允许更深入的讨论,并通过分页提高了性能。邮件通知功能也有助于提高用户参与度。

接下来,我们可以考虑添加以下功能:

  1. 文章搜索功能
  2. 用户头像上传
  3. 文章预览功能
  4. 标签云或热门标签功能

你希望继续哪个方向,或者有其他想法吗?