非常好,让我们依次实现这三个功能:用户头像上传、文章归档功能和评论系统。这些功能将大大提升博客的用户体验和互动性。

第二十部分:用户头像上传

  1. 首先,更新 src/db/schema.ts 文件,在 users 表中添加 avatar 字段:
export const users = sqliteTable("users", {
  // ... 其他字段保持不变
  avatar: text("avatar"),
});
  1. 创建一个新的迁移文件 src/db/migrations/0005_add_user_avatar.ts
export async function up(db) {
  await db.run(sql`
    ALTER TABLE users ADD COLUMN avatar TEXT;
  `);
}
 
export async function down(db) {
  await db.run(sql`
    ALTER TABLE users DROP COLUMN avatar;
  `);
}
  1. 运行迁移:
bun run migrate
  1. 安装文件上传处理库:
bun add @elysiajs/files
  1. 更新 src/routes/users.ts 文件,添加头像上传功能:
import { Elysia } from "elysia";
import { db } from "../db";
import { users } from "../db/schema";
import { eq } from "drizzle-orm";
import { files } from "@elysiajs/files";
 
export const userRoutes = new Elysia()
  .use(files())
  .get("/profile", async ({ session, render }) => {
    if (!session.user) {
      return { redirect: "/login" };
    }
    const user = await db.select().from(users).where(eq(users.id, session.user.id)).get();
    return render("users/profile", { user });
  })
  .post("/profile/avatar", async ({ session, files, set }) => {
    if (!session.user) {
      set.status = 401;
      return { error: "Unauthorized" };
    }
 
    const avatar = files.avatar;
    if (!avatar) {
      set.status = 400;
      return { error: "No file uploaded" };
    }
 
    const fileName = `${Date.now()}_${avatar.name}`;
    await Bun.write(`public/uploads/${fileName}`, avatar.buffer);
 
    await db.update(users).set({ avatar: `/uploads/${fileName}` }).where(eq(users.id, session.user.id));
 
    set.redirect = "/profile";
  });
  1. 创建 src/views/users/profile.eta 文件:
<% layout('../layouts/main.eta', { title: 'User Profile' }) %>
 
<h1>User Profile</h1>
 
<% if (it.user.avatar) { %>
  <img src="<%= it.user.avatar %>" alt="User Avatar" style="width: 100px; height: 100px; object-fit: cover;">
<% } %>
 
<form action="/profile/avatar" method="POST" enctype="multipart/form-data">
  <input type="file" name="avatar" accept="image/*">
  <button type="submit">Upload Avatar</button>
</form>
 
<h2>Username: <%= it.user.username %></h2>
<p>Email: <%= it.user.email %></p>

第二十一部分:文章归档功能

  1. 更新 src/routes/posts.ts 文件,添加归档路由:
import { sql } from "drizzle-orm";
 
export const postRoutes = new Elysia()
  // ... 保留现有的路由
  .get("/archive", async ({ render }) => {
    const archives = await db
      .select({
        year: sql<number>`strftime('%Y', ${posts.createdAt})`.as('year'),
        month: sql<number>`strftime('%m', ${posts.createdAt})`.as('month'),
        count: sql<number>`count(*)`.as('count'),
      })
      .from(posts)
      .groupBy(sql`year, month`)
      .orderBy(sql`year DESC, month DESC`)
      .all();
 
    return render("posts/archive", { archives });
  })
  .get("/archive/:year/:month", async ({ params, render }) => {
    const { year, month } = params;
    const archivePosts = await db
      .select({
        id: posts.id,
        title: posts.title,
        createdAt: posts.createdAt,
      })
      .from(posts)
      .where(sql`strftime('%Y', ${posts.createdAt}) = ${year} AND strftime('%m', ${posts.createdAt}) = ${month}`)
      .orderBy(posts.createdAt)
      .all();
 
    return render("posts/archive-month", { year, month, posts: archivePosts });
  });
  1. 创建 src/views/posts/archive.eta 文件:
<% layout('../layouts/main.eta', { title: 'Blog Archive' }) %>
 
<h1>Blog Archive</h1>
 
<ul>
  <% it.archives.forEach(archive => { %>
    <li>
      <a href="/archive/<%= archive.year %>/<%= archive.month.padStart(2, '0') %>">
        <%= new Date(archive.year, archive.month - 1).toLocaleString('default', { month: 'long', year: 'numeric' }) %>
        (<%= archive.count %> posts)
      </a>
    </li>
  <% }) %>
</ul>
  1. 创建 src/views/posts/archive-month.eta 文件:
<% layout('../layouts/main.eta', { title: `Archive for ${it.month}/${it.year}` }) %>
 
<h1>Archive for <%= new Date(it.year, it.month - 1).toLocaleString('default', { month: 'long', year: 'numeric' }) %></h1>
 
<ul>
  <% it.posts.forEach(post => { %>
    <li>
      <a href="/posts/<%= post.id %>"><%= post.title %></a>
      <small>(<%= new Date(post.createdAt).toLocaleDateString() %>)</small>
    </li>
  <% }) %>
</ul>
 
<a href="/archive">Back to Archive</a>

第二十二部分:评论系统

  1. 更新 src/db/schema.ts 文件,添加评论表:
export const comments = sqliteTable("comments", {
  id: integer("id").primaryKey(),
  postId: integer("post_id").notNull().references(() => posts.id),
  userId: integer("user_id").notNull().references(() => users.id),
  content: text("content").notNull(),
  createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});
  1. 创建一个新的迁移文件 src/db/migrations/0006_add_comments.ts
export async function up(db) {
  await db.run(sql`
    CREATE TABLE comments (
      id INTEGER PRIMARY KEY,
      post_id INTEGER NOT NULL,
      user_id INTEGER NOT NULL,
      content TEXT NOT NULL,
      created_at TIMESTAMP NOT NULL,
      FOREIGN KEY (post_id) REFERENCES posts(id),
      FOREIGN KEY (user_id) REFERENCES users(id)
    );
  `);
}
 
export async function down(db) {
  await db.run(sql`DROP TABLE comments;`);
}
  1. 运行迁移:
bun run migrate
  1. 更新 src/routes/posts.ts 文件,添加评论相关的路由:
import { comments } from "../db/schema";
 
export const postRoutes = new Elysia()
  // ... 保留现有的路由
  .get("/posts/:id", async ({ params, render, session }) => {
    const post = await db
      .select({
        id: posts.id,
        title: posts.title,
        content: posts.content,
        createdAt: posts.createdAt,
        author: users.username,
      })
      .from(posts)
      .leftJoin(users, eq(posts.authorId, users.id))
      .where(eq(posts.id, parseInt(params.id)))
      .get();
 
    const postComments = await db
      .select({
        id: comments.id,
        content: comments.content,
        createdAt: comments.createdAt,
        author: users.username,
      })
      .from(comments)
      .leftJoin(users, eq(comments.userId, users.id))
      .where(eq(comments.postId, parseInt(params.id)))
      .orderBy(comments.createdAt)
      .all();
 
    return render("posts/show", { post, comments: postComments, user: session.user });
  })
  .post("/posts/:id/comments", async ({ params, body, session, set }) => {
    if (!session.user) {
      set.status = 401;
      return { error: "Unauthorized" };
    }
 
    const { content } = body;
    await db.insert(comments).values({
      postId: parseInt(params.id),
      userId: session.user.id,
      content,
      createdAt: new Date(),
    });
 
    set.redirect = `/posts/${params.id}`;
  });
  1. 更新 src/views/posts/show.eta 文件,添加评论部分:
<% layout('../layouts/main.eta', { title: it.post.title }) %>
 
<h1><%= it.post.title %></h1>
<p>By <%= it.post.author %> on <%= new Date(it.post.createdAt).toLocaleDateString() %></p>
<div><%= it.post.content %></div>
 
<h2>Comments</h2>
<% if (it.comments.length > 0) { %>
  <ul>
    <% it.comments.forEach(comment => { %>
      <li>
        <p><%= comment.content %></p>
        <small>By <%= comment.author %> on <%= new Date(comment.createdAt).toLocaleString() %></small>
      </li>
    <% }) %>
  </ul>
<% } else { %>
  <p>No comments yet.</p>
<% } %>
 
<% if (it.user) { %>
  <h3>Add a comment</h3>
  <form action="/posts/<%= it.post.id %>/comments" method="POST">
    <textarea name="content" required></textarea>
    <button type="submit">Submit Comment</button>
  </form>
<% } else { %>
  <p><a href="/login">Log in to comment</a></p>
<% } %>
  1. 更新 public/styles/main.css 文件,添加一些评论相关的样式:
/* ... 前面的样式保持不变 */
 
.comments-list {
  list-style-type: none;
  padding: 0;
}
 
.comment {
  border: 1px solid #ddd;
  padding: 10px;
  margin-bottom: 10px;
}
 
.comment-meta {
  font-size: 0.8em;
  color: #666;
}
 
.comment-form textarea {
  width: 100%;
  height: 100px;
  margin-bottom: 10px;
}

这些更改实现了三个新功能:

  1. 用户头像上传:用户现在可以上传和更新他们的头像。
  2. 文章归档功能:提供了一个按年月组织的文章归档页面。
  3. 评论系统:用户可以在文章下方查看和发表评论。

主要改进包括:

  • 增强了用户个人资料的个性化。
  • 提供了更好的内容组织和导航方式。
  • 增加了用户互动和参与度。

这些功能大大提升了博客的功能性和用户体验。用户现在可以更个性化地展示自己,更容易找到过去的文章,并且可以与其他读者进行交流。

接下来,我们可以考虑添加以下功能来进一步完善博客系统:

  1. 相关文章推荐
  2. 社交媒体分享按钮
  3. 文章阅读计数器
  4. 用户权限管理(管理员/作者/读者)

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