好的,让我们来实现支持 Markdown 和富文本编辑的博客文章编辑器功能。我们将使用 TinyMCE 作为富文本编辑器,并添加一个切换按钮来在富文本和 Markdown 模式之间切换。
第十四部分:实现高级博客文章编辑器
- 首先,我们需要安装一些新的依赖:
bun add tinymce @tinymce/tinymce-elysia marked- 更新
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>- 更新
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;
})- 更新
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>
<!-- ... 其他部分保持不变 -->- 创建一个新的视图文件
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>- 在
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;
})- 更新
src/views/posts/show.eta文件,添加编辑按钮:
<% if (it.user && it.user.id === it.post.authorId) { %>
<a href="/posts/<%= it.post.id %>/edit">Edit Post</a>
<% } %>- 最后,更新
public/styles/main.css文件,添加一些编辑器相关的样式:
/* ... 前面的样式保持不变 */
#editor {
min-height: 300px;
border: 1px solid #ccc;
padding: 10px;
}
#toggleEditor {
margin-bottom: 10px;
}这些更改实现了支持 Markdown 和富文本编辑的高级博客文章编辑器:
- 使用 TinyMCE 作为富文本编辑器。
- 添加了在 Markdown 和富文本模式之间切换的功能。
- 实现了文章编辑功能,允许作者编辑自己的文章。
- 更新了文章创建和编辑表单,以支持新的编辑器功能。
- 修改了后端逻辑,以正确处理 HTML 内容。
这个高级编辑器为用户提供了更灵活的内容创作选项,既可以使用所见即所得的富文本编辑,也可以使用 Markdown 语法。
接下来,我们可以考虑添加以下功能:
- 文章搜索功能
- 用户头像上传
- 评论系统的改进(如回复功能、评论分页等)
- 文章预览功能
你希望继续哪个方向,或者有其他想法吗?