好的, 我们来实现 Tag 功能并生成 Tag 云图。这需要几个步骤:
- 更新数据库模式以支持 Tags
- 实现 Tag 的 CRUD 操作
- 在博客文章中添加 Tag 支持
- 生成 Tag 云图
- 在前端展示 Tag 云图
让我们逐步实现这些功能:
- 更新数据库模式
在 src/db/schema.ts 中添加 Tag 相关的表:
export const tags = sqliteTable('tags', {
id: integer('id').primaryKey(),
name: text('name').notNull().unique(),
});
export const postTags = sqliteTable('post_tags', {
id: integer('id').primaryKey(),
postId: integer('post_id').notNull().references(() => posts.id),
tagId: integer('tag_id').notNull().references(() => tags.id),
});- 实现 Tag 的 CRUD 操作
在 src/index.ts 中添加 Tag 相关的路由:
import { tags, postTags } from './db/schema';
// ... 其他导入和代码
// 创建新标签
app.post('/api/tag', async ({ body }) => {
const { name } = body as { name: string };
const newTag = await db.insert(tags).values({ name }).returning().get();
return newTag;
});
// 获取所有标签
app.get('/api/tags', async () => {
const allTags = await db.select().from(tags).all();
return allTags;
});
// 为文章添加标签
app.post('/api/post/:postId/tag', async ({ params, body }) => {
const { tagId } = body as { tagId: number };
const newPostTag = await db.insert(postTags).values({ postId: parseInt(params.postId), tagId }).returning().get();
return newPostTag;
});
// 获取文章的所有标签
app.get('/api/post/:postId/tags', async ({ params }) => {
const postTags = await db.select()
.from(postTags)
.innerJoin(tags, eq(postTags.tagId, tags.id))
.where(eq(postTags.postId, parseInt(params.postId)))
.all();
return postTags.map(pt => pt.tags);
});
// 获取带有标签计数的所有标签
app.get('/api/tags/count', async () => {
const tagCounts = await db.select({
id: tags.id,
name: tags.name,
count: sql<number>`count(${postTags.id})`.as('count'),
})
.from(tags)
.leftJoin(postTags, eq(tags.id, postTags.tagId))
.groupBy(tags.id)
.all();
return tagCounts;
});- 在博客文章中添加 Tag 支持
更新 src/index.ts 中的创建文章和获取文章的路由:
app.post('/api/post', async ({ body, getUser }) => {
const user = await getUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const { title, content, tags } = body as { title: string; content: string; tags: string[] };
const newPost = await db.insert(posts).values({ title, content, authorId: user.id }).returning().get();
// 添加标签
for (const tagName of tags) {
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: newPost.id, tagId: tag.id }).run();
}
return newPost;
});
app.get('/api/post/:id', async ({ params, getUser }) => {
Const user = await getUser ();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const post = await db.select().from(posts).where(eq(posts.id, parseInt(params.id))).get();
if (!post) {
return new Response('Post not found', { status: 404 });
}
const postTags = await db.select()
.from(postTags)
.innerJoin(tags, eq(postTags.tagId, tags.id))
.where(eq(postTags.postId, post.id))
.all();
return {
...post,
tags: postTags.map(pt => pt.tags.name),
};
});- 生成 Tag 云图
我们可以使用一个简单的算法来生成 Tag 云图的数据。在 src/index.ts 中添加以下路由:
app.get('/api/tagcloud', async () => {
const tagCounts = await db.select({
id: tags.id,
name: tags.name,
count: sql<number>`count(${postTags.id})`.as('count'),
})
.from(tags)
.leftJoin(postTags, eq(tags.id, postTags.tagId))
.groupBy(tags.id)
.all();
const maxCount = Math.max(...tagCounts.map(t => t.count));
const minCount = Math.min(...tagCounts.map(t => t.count));
const fontSize = (count: number) => {
const minSize = 12;
const maxSize = 36;
return Math.floor(((count - minCount) / (maxCount - minCount)) * (maxSize - minSize) + minSize);
};
return tagCounts.map(tag => ({
id: tag.id,
name: tag.name,
count: tag.count,
size: fontSize(tag.count),
}));
});- 在前端展示 Tag 云图
更新 src/components/Blog.tsx 以显示 Tag 云图和文章的 Tags:
import React, { useState, useEffect } from 'react';
import Editor from './Editor';
import { marked } from 'marked';
// ... 其他接口定义
interface Tag {
id: number;
name: string;
count: number;
size: number;
}
export const Blog: React.FC<BlogProps> = ({ posts, user }) => {
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
fetch('/api/tagcloud')
.then(res => res.json())
.then(setTags);
}, []);
// ... 其他状态和函数
return (
<div>
<h1>My Blog</h1>
<div className="tag-cloud">
{tags.map(tag => (
<span key={tag.id} style={{ fontSize: `${tag.size}px`, margin: '5px' }}>
{tag.name}
</span>
))}
</div>
{/* ... 其他JSX */}
<div id="posts">
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<div dangerouslySetInnerHTML={{ __html: marked(post.content) }} />
<div>
Tags: {post.tags?.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
<small>Created at: {new Date(post.createdAt).toLocaleString()}</small>
{/* ... 其他文章相关的JSX */}
</article>
))}
</div>
</div>
);
};- 更新创建文章表单以支持添加 Tags
在 src/components/Blog.tsx 中的表单部分添加 Tag 输入:
<form id="new-post-form" hx-post="/api/post" hx-swap="beforeend" hx-target="#posts" onSubmit={handleSubmit}>
<input
type="text"
name="title"
placeholder="Title"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Editor onChange={setContent} />
<input type="hidden" name="content" value={content} />
<input
type="text"
name="tags"
placeholder="Tags (comma separated)"
value={tags}
onChange={(e) => setTags(e.target.value)}
/>
<button type="submit">Create Post</button>
</form>- 添加一些 CSS 样式
在 src/public/styles.css 中添加以下样式:
.tag-cloud {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
.tag-cloud span {
display: inline-block;
padding: 5px;
margin: 5px;
background-color: #f0f0f0;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.3s;
}
.tag-cloud span:hover {
background-color: #e0e0e0;
}
.tag {
display: inline-block;
padding: 2px 5px;
margin-right: 5px;
background-color: #e0e0e0;
border-radius: 3px;
font-size: 0.8em;
}- 运行数据库迁移:
bun run generate
bun run push这个实现添加了以下功能:
- Tag 系统, 允许为博客文章添加多个标签。
- Tag 云图, 根据标签的使用频率动态生成大小不同的标签展示。
- 在文章创建时支持添加标签。
- 在文章展示时显示相关标签。
这个实现还可以进一步改进, 例如:
- 实现按标签筛选文章的功能。
- 添加标签编辑和删除功能。
- 实现标签自动完成功能, 以避免重复创建相似的标签。
- 优化 Tag 云图的布局算法, 使其更美观。
- 添加标签的颜色变化, 可以根据使用频率或其他因素来决定颜色。
- 实现标签的权重调整功能, 允许管理员手动调整某些标签的重要性。
- 添加标签的搜索功能。
- 实现相关文章推荐功能, 基于文章的标签相似度。
记住, 在处理用户输入的标签时, 要进行适当的清理和验证, 以防止 XSS 攻击和其他安全问题。此外, 考虑添加一个最大标签数量限制, 以防止滥用。