好的,让我们来实现标签功能。这将允许用户为帖子添加标签,并通过标签来浏览和搜索帖子。

第八部分:实现标签功能

  1. 首先,我们需要更新数据库模式。在 src/db/schema.ts 中添加以下内容:
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
 
// ... 现有的表定义保持不变
 
export const tags = sqliteTable("tags", {
  id: integer("id").primaryKey(),
  name: text("name").notNull().unique(),
});
 
export const postTags = sqliteTable("post_tags", {
  postId: integer("post_id").notNull().references(() => posts.id),
  tagId: integer("tag_id").notNull().references(() => tags.id),
});
  1. 创建一个新的迁移文件来添加这些表。在 drizzle 目录中创建一个新文件,例如 0003_add_tags.sql
CREATE TABLE tags (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);
 
CREATE TABLE post_tags (
  post_id INTEGER NOT NULL,
  tag_id INTEGER NOT NULL,
  FOREIGN KEY (post_id) REFERENCES posts(id),
  FOREIGN KEY (tag_id) REFERENCES tags(id)
);
 
CREATE INDEX idx_post_tags_post_id ON post_tags(post_id);
CREATE INDEX idx_post_tags_tag_id ON post_tags(tag_id);
  1. 更新 src/routes/posts.ts 以支持标签:
import { Elysia, t } from "elysia";
import { db } from "../db";
import { posts, users, tags, postTags } from "../db/schema";
import { eq, like, sql } from "drizzle-orm";
import { authMiddleware } from "../middleware/auth";
 
const POSTS_PER_PAGE = 10;
 
export const postRoutes = new Elysia()
  .use(authMiddleware)
  .get("/posts", async ({ query }) => {
    const page = parseInt(query.page as string) || 1;
    const search = (query.search as string) || '';
    const tag = (query.tag as string) || '';
    const offset = (page - 1) * POSTS_PER_PAGE;
    
    let postsQuery = 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));
 
    if (search) {
      postsQuery = postsQuery.where(like(posts.title, `%${search}%`));
    }
 
    if (tag) {
      postsQuery = postsQuery
        .innerJoin(postTags, eq(posts.id, postTags.postId))
        .innerJoin(tags, eq(postTags.tagId, tags.id))
        .where(eq(tags.name, tag));
    }
 
    const [allPosts, totalCount] = await Promise.all([
      postsQuery
        .limit(POSTS_PER_PAGE)
        .offset(offset)
        .orderBy(posts.createdAt)
        .all(),
      db.select({ count: sql`count(*)` }).from(postsQuery.as('subquery')).get().then(result => result.count),
    ]);
 
    const totalPages = Math.ceil(totalCount / POSTS_PER_PAGE);
 
    return {
      posts: allPosts,
      currentPage: page,
      totalPages,
      hasNextPage: page < totalPages,
      hasPrevPage: page > 1,
      search,
      tag,
    };
  })
  .post("/posts", async ({ body, set, getCurrentUser }) => {
    const user = await getCurrentUser();
    if (!user) {
      set.status = 401;
      return { error: "Not authorized" };
    }
 
    const { title, content, tags: tagNames } = body;
 
    const post = await db.insert(posts).values({
      title,
      content,
      authorId: user.id,
    }).returning().get();
 
    if (tagNames && tagNames.length > 0) {
      for (const tagName of tagNames) {
        let tag = await db.select().from(tags).where(eq(tags.name, tagName)).get();
        if (!tag) {
          tag = await db.insert(tags).values({ name: tagName }).returning().get();
        }
        await db.insert(postTags).values({ postId: post.id, tagId: tag.id }).run();
      }
    }
 
    return post;
  })
  // ... 其他路由保持不变
  1. 更新 src/views/create-post.eta 以支持添加标签:
<% layout('./layouts/main.eta', { title: 'Create Post' }) %>
 
<h1>Create New Post</h1>
<form hx-post="/posts" hx-swap="outerHTML">
    <div>
        <label for="title">Title:</label>
        <input type="text" id="title" name="title" required>
    </div>
    <div>
        <label for="content">Content:</label>
        <textarea id="content" name="content" required></textarea>
    </div>
    <div>
        <label for="tags">Tags (comma separated):</label>
        <input type="text" id="tags" name="tags">
    </div>
    <button type="submit">Create Post</button>
</form>
 
<script>
    document.body.addEventListener('htmx:afterRequest', function(event) {
        if (event.detail.successful) {
            window.location.href = '/posts/' + JSON.parse(event.detail.xhr.response).id;
        } else {
            alert('Failed to create post. Please try again.');
        }
    });
</script>
  1. 更新 src/views/post.eta 以显示标签:
<% layout('./layouts/main.eta', { title: it.post.title }) %>
 
<article id="post-<%= it.post.id %>">
    <h1><%= it.post.title %></h1>
    <p>By <a href="/users/<%= it.post.authorId %>"><%= it.post.author %></a> on <%= new Date(it.post.createdAt).toLocaleDateString() %></p>
    <div><%= it.post.content %></div>
    <% if (it.post.tags && it.post.tags.length > 0) { %>
        <div class="tags">
            Tags:
            <% it.post.tags.forEach(function(tag) { %>
                <a href="/?tag=<%= tag %>" class="tag"><%= tag %></a>
            <% }) %>
        </div>
    <% } %>
</article>
 
<!-- 其余部分保持不变 -->
  1. 更新 src/views/home.eta 以支持标签搜索:
<% layout('./layouts/main.eta', { title: 'Home' }) %>
 
<h1>Latest Posts</h1>
 
<form action="/" method="get" class="search-form">
  <input type="text" name="search" value="<%= it.search %>" placeholder="Search posts...">
  <input type="text" name="tag" value="<%= it.tag %>" placeholder="Filter by tag...">
  <button type="submit">Search</button>
</form>
 
<!-- 其余部分保持不变 -->
 
<div class="pagination">
  <% if (it.hasPrevPage) { %>
    <a href="?page=<%= it.currentPage - 1 %>&search=<%= it.search %>&tag=<%= it.tag %>">Previous</a>
  <% } %>
  
  <span>Page <%= it.currentPage %> of <%= it.totalPages %></span>
  
  <% if (it.hasNextPage) { %>
    <a href="?page=<%= it.currentPage + 1 %>&search=<%= it.search %>&tag=<%= it.tag %>">Next</a>
  <% } %>
</div>
 
<!-- 其余部分保持不变 -->
  1. 更新 public/styles/main.css 以添加标签样式:
/* ... 前面的样式保持不变 */
 
.tags {
    margin-top: 1rem;
}
 
.tag {
    display: inline-block;
    background-color: #f0f0f0;
    padding: 0.2rem 0.5rem;
    margin-right: 0.5rem;
    border-radius: 3px;
    font-size: 0.9rem;
    color: #333;
    text-decoration: none;
}
 
.tag:hover {
    background-color: #e0e0e0;
}
  1. 最后,更新 src/index.ts 中的主页路由以包含标签:
// ... 前面的代码保持不变
 
.get("/", async ({ query, set, getCurrentUser }) => {
  const postsData = await postRoutes.handle({ method: 'GET', path: '/posts', query });
  const user = await getCurrentUser();
  set.headers['Content-Type'] = 'text/html';
  return await renderFile(path.join(__dirname, "views", "home.eta"), { ...postsData, user });
})
 
// ... 其余代码保持不变

这些更改实现了标签功能:

  1. 用户可以在创建帖子时添加标签。
  2. 标签显示在帖子详情页面上。
  3. 用户可以通过标签筛选帖子。
  4. 主页支持按标签和搜索词进行组合搜索。

这个功能增强了博客的组织性和可发现性。用户现在可以更容易地找到相关内容,也可以通过标签浏览相似主题的帖子。

接下来,我们可以考虑添加评论系统、用户设置页面,或者实现一些高级功能,如 RSS 订阅或邮件通知。您希望继续哪个方向?