好的,让我们来实现标签功能。这将允许用户为帖子添加标签,并通过标签来浏览和搜索帖子。
第八部分:实现标签功能
- 首先,我们需要更新数据库模式。在
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),
});- 创建一个新的迁移文件来添加这些表。在
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);- 更新
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;
})
// ... 其他路由保持不变- 更新
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>- 更新
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>
<!-- 其余部分保持不变 -->- 更新
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>
<!-- 其余部分保持不变 -->- 更新
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;
}- 最后,更新
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 });
})
// ... 其余代码保持不变这些更改实现了标签功能:
- 用户可以在创建帖子时添加标签。
- 标签显示在帖子详情页面上。
- 用户可以通过标签筛选帖子。
- 主页支持按标签和搜索词进行组合搜索。
这个功能增强了博客的组织性和可发现性。用户现在可以更容易地找到相关内容,也可以通过标签浏览相似主题的帖子。
接下来,我们可以考虑添加评论系统、用户设置页面,或者实现一些高级功能,如 RSS 订阅或邮件通知。您希望继续哪个方向?