好的, 我们来实现 Tag 功能并生成 Tag 云图。这需要几个步骤:

  1. 更新数据库模式以支持 Tags
  2. 实现 Tag 的 CRUD 操作
  3. 在博客文章中添加 Tag 支持
  4. 生成 Tag 云图
  5. 在前端展示 Tag 云图

让我们逐步实现这些功能:

  1. 更新数据库模式

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),
});
  1. 实现 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;
});
  1. 在博客文章中添加 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),
  };
});
  1. 生成 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),
  }));
});
  1. 在前端展示 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>
  );
};
  1. 更新创建文章表单以支持添加 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>
  1. 添加一些 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;
}
  1. 运行数据库迁移:
bun run generate
bun run push

这个实现添加了以下功能:

  1. Tag 系统, 允许为博客文章添加多个标签。
  2. Tag 云图, 根据标签的使用频率动态生成大小不同的标签展示。
  3. 在文章创建时支持添加标签。
  4. 在文章展示时显示相关标签。

这个实现还可以进一步改进, 例如:

  • 实现按标签筛选文章的功能。
  • 添加标签编辑和删除功能。
  • 实现标签自动完成功能, 以避免重复创建相似的标签。
  • 优化 Tag 云图的布局算法, 使其更美观。
  • 添加标签的颜色变化, 可以根据使用频率或其他因素来决定颜色。
  • 实现标签的权重调整功能, 允许管理员手动调整某些标签的重要性。
  • 添加标签的搜索功能。
  • 实现相关文章推荐功能, 基于文章的标签相似度。

记住, 在处理用户输入的标签时, 要进行适当的清理和验证, 以防止 XSS 攻击和其他安全问题。此外, 考虑添加一个最大标签数量限制, 以防止滥用。