编辑文本的 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 '@toast-ui/editor/dist/i18n/zh-cn'
import { onMounted } from 'vue'
import { useStore } from 'vuex'

// Editor实例
let mkEditor
// 处理离开页面切换语言导致 dom 无法被获取
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
// src/api/article.js
/**
* 创建文章
*/
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
// src/views/article-create/components/commit.js
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;
};

// 将此方法导入到markdown.vue中
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
// markdown.vue
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
// 路径为src/views/article-detail/index.vue
<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

1
npm i wangeditor@4.7.6

富文本的基本组件结构和 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>