编辑文本的 2 种方式:1、富文本; 2、markdown。这 2 种编辑形式在前端中有很多第 3 方库。
一、创建文本基本结构
1-1 创建文本基本结构,主要分为三部分:
1、article-create 页面:基本结构
2、Editor 组件:富文本编辑器
3、Markdown 组件: markdown 编辑器
src 目录下的项目结构:
1 2 3 4 5
| views / article - create / components / Editor.vue;
views / article - create / components / Markdown.vue;
views / article - create / index.vue;
|
1-2 创建文本父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <template> <div class="article-create"> <el-card> <el-input class="title-input" placeholder="请输入标题" v-model="title" maxlength="20" clearable > </el-input> <el-tabs v-model="activeName"> <el-tab-pane label="markdown" name="markdown"> <markdown></markdown> </el-tab-pane> <el-tab-pane label="富文本" name="editor"> <editor></editor> </el-tab-pane> </el-tabs> </el-card> </div> </template>
<script setup> import Editor from "./components/Editor.vue"; import Markdown from "./components/Markdown.vue"; import { ref } from "vue";
const activeName = ref("markdown"); const title = ref(""); </script>
<style lang="scss" scoped> .title-input { margin-bottom: 20px; } </style>
|
1-3 markdown 编辑器:tui.editor
安装 plugin:
1
| npm i @toast-ui/editor@3.0.2
|
渲染 markdown 基本结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="markdown-container"> <!-- 渲染区 --> <div id="markdown-box"></div> <div class="bottom"> <el-button type="primary" @click="onSubmitClick">提交</el-button> </div> </div> </template>
<script setup> import {} from "vue"; </script>
<style lang="scss" scoped> .markdown-container { .bottom { margin-top: 20px; text-align: right; } } </style>
|
初始化 editor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script setup> import MkEditor from '@toast-ui/editor' import '@toast-ui/editor/dist/toastui-editor.css'
import { onMounted } from 'vue' import { useStore } from 'vuex'
let mkEditor
let el onMounted(() => { el = document.querySelector('#markdown-box') initEditor() })
const store = useStore() const initEditor = () => { mkEditor = new MkEditor({ el, height: '500px', previewStyle: 'vertical' })
mkEditor.getMarkdown() } </script>
|
新建文本及新建文本的提交
接口定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
export const createArticle = (data) => { return request({ url: "/article/create", method: "POST", data, }); };
export const articleEdit = (data) => { return request({ url: "/article/edit", method: "POST", data, }); };
|
注意:markwodn 和富文本最终都会处理提交事件,将提交合并到一个模块实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { createArticle, articleEdit } from "@/api/article"; import { ElMessage } from "element-plus";
export const commitArticle = async (data) => { const res = await createArticle(data); ElMessage.success("创建成功!"); return res; }; export const editArticle = async (data) => { const res = await articleEdit(data); ElMessage.success("编辑成功!"); return res; };
import { commitArticle } from "./commit";
|
提交文本事件的触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const props = defineProps({ title: { required: true, type: String } })
const emits = defineEmits(['onSuccess']) ...
const onSubmitClick = async () => { await commitArticle({ title: props.title, content: mkEditor.getHTML() }) mkEditor.reset() emits('onSuccess') }
|
父组件中处理传递的 title,处理 onSuccess 事件
1 2 3 4
| const onSuccess = () => { title.value = ""; };
|
markdown 文本编辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| <template> <div class="article-detail-container"> <h2 class="title">{{ detail.title }}</h2> <div class="header"> <span class="author"> 作者:{{ detail.author }} </span> <span class="time"> 时间: {{ $filters.relativeTime(detail.publicDate) }} </span> <el-button type="text" class="edit" @click="onEditClick">编辑</el-button> </div> <div class="content" v-html="detail.content"></div> </div> </template> <script setup> import { ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { articleDetail } from '@/api/article'
const route = useRoute() const articleId = route.params.id const detail = ref({}) const getArticleDetail = async () => { detail.value = await articleDetail(articleId) } getArticleDetail()
const router = useRouter() const onEditClick = () => { router.push(`/article/editor/${articleId}`) } </script>
<style lang="scss" scoped> .article-detail-container { .title { font-size: 22px; text-align: center; padding: 12px 0; }
.header { padding: 26px 0;
.author { font-size: 14px; color: #555666; margin-right: 20px; }
.time { font-size: 14px; color: #999aaa; margin-right: 20px; }
.edit { float: right; } }
.content { font-size: 14px; padding: 20px 0; border-top: 1px solid #d4d4d4; ; } } </style>
|
将数据传递给 markdown 组件
1 2 3 4 5 6 7 8 9 10 11 12
| <markdown :title="title" :detail="detail" @onSuccess="onSuccess"></markdown>
// 数据接收 const props = defineProps({ title: { required: true, type: String }, detail: { type: Object } })
|
使用 watch 检测数据变化,存在 detail 时,将 detail 赋值给 mkEditor
1 2 3 4 5 6 7 8 9 10 11 12
| watch( () => props.detail, (val) => { if (val && val.content) { mkEditor.setHTML(val.content); } }, { immediate: true, } );
|
markdown 组件中处理提交事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const onSubmitClick = async () => { if (props.detail && props.detail._id) { await editArticle({ id: props.detail._id, title: props.title, content: mkEditor.getHTML(), }); } else { await commitArticle({ title: props.title, content: mkEditor.getHTML(), }); }
mkEditor.reset(); emits("onSuccess"); };
|
1-4 富文本编辑器:wangEditor
安装 plugin
富文本的基本组件结构和 markdown 组件基本一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| <template> <div class="editor-container"> <div id="editor-box"></div> <div class="bottom"> <el-button type="primary" @click="onSubmitClick">提交</el-button> </div> </div> </template>
<script setup> import E from 'wangeditor' import { onMounted, defineProps, defineEmits, watch } from 'vue' import { useStore } from 'vuex' import { commitArticle, editArticle } from './commit'
const props = defineProps({ title: { required: true, type: String }, detail: { type: Object } })
const emits = defineEmits(['onSuccess'])
const store = useStore()
// Editor实例 let editor // 处理离开页面切换语言导致 dom 无法被获取 let el onMounted(() => { el = document.querySelector('#editor-box') initEditor() })
const initEditor = () => { editor = new E(el) editor.config.zIndex = 1 editor.config.showMenuTooltips = true editor.config.menuTooltipPosition = 'down'
editor.create() }
watch( () => props.detail, val => { if (val && val.content) { editor.txt.html(val.content) } }, { immediate: true } )
const onSubmitClick = async () => { if (props.detail && props.detail._id) { await editArticle({ id: props.detail._id, title: props.title, content: editor.txt.html() }) } else { await commitArticle({ title: props.title, content: editor.txt.html() }) }
editor.txt.html('') emits('onSuccess') } </script>
<style lang="scss" scoped> .editor-container { .bottom { margin-top: 20px; text-align: right; } } </style>
|