好的,让我们来实现支持 Markdown 和富文本编辑的博客文章编辑器功能。我们将使用 TinyMCE 作为富文本编辑器,并添加一个切换按钮来在富文本和 Markdown 模式之间切换。

第十四部分:实现高级博客文章编辑器

  1. 首先,我们需要安装一些新的依赖:
bun add tinymce @tinymce/tinymce-elysia marked
  1. 更新 src/views/posts/new.eta 文件,添加编辑器和切换按钮:
<% layout('../layouts/main.eta', { title: 'New Post' }) %>
 
<h1>Create a New Post</h1>
 
<form action="/posts" method="POST" id="postForm">
    <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" style="display:none;"></textarea>
        <div id="editor"></div>
    </div>
    <div>
        <label for="categories">Categories (comma-separated):</label>
        <input type="text" id="categories" name="categories">
    </div>
    <button type="button" id="toggleEditor">Toggle Markdown/Rich Text</button>
    <button type="submit">Create Post</button>
</form>
 
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
let isMarkdownMode = false;
let editor;
 
tinymce.init({
    selector: '#editor',
    plugins: 'advlist autolink lists link image charmap print preview anchor searchreplace visualblocks code fullscreen insertdatetime media table paste code help wordcount',
    toolbar: 'undo redo | formatselect | bold italic backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
    height: 500,
    setup: function(ed) {
        editor = ed;
    }
});
 
document.getElementById('toggleEditor').addEventListener('click', function() {
    isMarkdownMode = !isMarkdownMode;
    const editorElement = document.getElementById('editor');
    const contentElement = document.getElementById('content');
 
    if (isMarkdownMode) {
        const content = editor.getContent();
        contentElement.value = turndownService.turndown(content);
        editor.hide();
        contentElement.style.display = 'block';
    } else {
        const content = contentElement.value;
        editor.setContent(marked.parse(content));
        contentElement.style.display = 'none';
        editor.show();
    }
});
 
document.getElementById('postForm').addEventListener('submit', function(e) {
    e.preventDefault();
    if (isMarkdownMode) {
        document.getElementById('content').value = document.getElementById('content').value;
    } else {
        document.getElementById('content').value = editor.getContent();
    }
    this.submit();
});
</script>
  1. 更新 src/routes/posts.ts 文件,修改创建文章的路由以处理 HTML 内容:
.post("/posts", async ({ body, getCurrentUser }) => {
    const user = await getCurrentUser();
    if (!user) {
      throw new Error("Not authorized");
    }
 
    const { title, content, categories } = body;
 
    const newPost = await db.transaction(async (tx) => {
      const [post] = await tx.insert(posts).values({
        title,
        content,
        authorId: user.id,
      }).returning();
 
      if (categories && categories.length > 0) {
        const categoryNames = categories.split(',').map(c => c.trim());
        for (const categoryName of categoryNames) {
          let [category] = await tx.select().from(categories).where(eq(categories.name, categoryName)).limit(1).all();
          if (!category) {
            [category] = await tx.insert(categories).values({ name: categoryName }).returning();
          }
          await tx.insert(postCategories).values({ postId: post.id, categoryId: category.id });
        }
      }
 
      return post;
    });
 
    return newPost;
  })
  1. 更新 src/views/posts/show.eta 文件,以正确显示 HTML 内容:
<% 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>
 
<% if (it.post.categories && it.post.categories.length > 0) { %>
    <p>Categories: 
        <% it.post.categories.forEach((category, index) => { %>
            <a href="/?category=<%= category %>"><%= category %></a><%= index < it.post.categories.length - 1 ? ', ' : '' %>
        <% }) %>
    </p>
<% } %>
 
<div><%~ it.post.content %></div>
 
<!-- ... 其他部分保持不变 -->
  1. 创建一个新的视图文件 src/views/posts/edit.eta
<% layout('../layouts/main.eta', { title: 'Edit Post' }) %>
 
<h1>Edit Post</h1>
 
<form action="/posts/<%= it.post.id %>" method="POST" id="postForm">
    <div>
        <label for="title">Title:</label>
        <input type="text" id="title" name="title" value="<%= it.post.title %>" required>
    </div>
    <div>
        <label for="content">Content:</label>
        <textarea id="content" name="content" style="display:none;"><%= it.post.content %></textarea>
        <div id="editor"></div>
    </div>
    <div>
        <label for="categories">Categories (comma-separated):</label>
        <input type="text" id="categories" name="categories" value="<%= it.post.categories.join(', ') %>">
    </div>
    <button type="button" id="toggleEditor">Toggle Markdown/Rich Text</button>
    <button type="submit">Update Post</button>
</form>
 
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
// 使用与 new.eta 相同的 JavaScript 代码
</script>
  1. src/routes/posts.ts 中添加编辑文章的路由:
.get("/posts/:id/edit", async ({ params, getCurrentUser }) => {
  const user = await getCurrentUser();
  if (!user) {
    throw new Error("Not authorized");
  }
 
  const post = await db.select({
    id: posts.id,
    title: posts.title,
    content: posts.content,
    authorId: posts.authorId,
    categories: categories.name,
  })
    .from(posts)
    .leftJoin(postCategories, eq(posts.id, postCategories.postId))
    .leftJoin(categories, eq(postCategories.categoryId, categories.id))
    .where(eq(posts.id, parseInt(params.id)))
    .all();
 
  if (post.length === 0 || post[0].authorId !== user.id) {
    throw new Error("Post not found or not authorized");
  }
 
  const categories = post.map(p => p.categories).filter(Boolean);
  return { ...post[0], categories };
})
.post("/posts/:id", async ({ params, body, getCurrentUser }) => {
  const user = await getCurrentUser();
  if (!user) {
    throw new Error("Not authorized");
  }
 
  const { title, content, categories } = body;
 
  const updatedPost = await db.transaction(async (tx) => {
    const [post] = await tx.update(posts)
      .set({ title, content })
      .where(and(eq(posts.id, parseInt(params.id)), eq(posts.authorId, user.id)))
      .returning();
 
    if (!post) {
      throw new Error("Post not found or not authorized");
    }
 
    await tx.delete(postCategories).where(eq(postCategories.postId, post.id));
 
    if (categories && categories.length > 0) {
      const categoryNames = categories.split(',').map(c => c.trim());
      for (const categoryName of categoryNames) {
        let [category] = await tx.select().from(categories).where(eq(categories.name, categoryName)).limit(1).all();
        if (!category) {
          [category] = await tx.insert(categories).values({ name: categoryName }).returning();
        }
        await tx.insert(postCategories).values({ postId: post.id, categoryId: category.id });
      }
    }
 
    return post;
  });
 
  return updatedPost;
})
  1. 更新 src/views/posts/show.eta 文件,添加编辑按钮:
<% if (it.user && it.user.id === it.post.authorId) { %>
    <a href="/posts/<%= it.post.id %>/edit">Edit Post</a>
<% } %>
  1. 最后,更新 public/styles/main.css 文件,添加一些编辑器相关的样式:
/* ... 前面的样式保持不变 */
 
#editor {
    min-height: 300px;
    border: 1px solid #ccc;
    padding: 10px;
}
 
#toggleEditor {
    margin-bottom: 10px;
}

这些更改实现了支持 Markdown 和富文本编辑的高级博客文章编辑器:

  1. 使用 TinyMCE 作为富文本编辑器。
  2. 添加了在 Markdown 和富文本模式之间切换的功能。
  3. 实现了文章编辑功能,允许作者编辑自己的文章。
  4. 更新了文章创建和编辑表单,以支持新的编辑器功能。
  5. 修改了后端逻辑,以正确处理 HTML 内容。

这个高级编辑器为用户提供了更灵活的内容创作选项,既可以使用所见即所得的富文本编辑,也可以使用 Markdown 语法。

接下来,我们可以考虑添加以下功能:

  1. 文章搜索功能
  2. 用户头像上传
  3. 评论系统的改进(如回复功能、评论分页等)
  4. 文章预览功能

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