vue3.0集成markdown编辑器、预览功能
一、安装
# 使用 npm
npm i @kangc/v-md-editor@next -S
# 使用 yarn
yarn add @kangc/v-md-editor@next
二、封装
1、项目中创建plugins文件夹
2、在 plugins 文件夹下创建 mdEditor 文件夹
3、在 mdEditor 文件夹下创建 index.js
以下为 mdEditor/index.js 内容
// 引入markdown编辑器
import VueMarkdownEditor from '@kangc/v-md-editor';
import '@kangc/v-md-editor/lib/style/base-editor.css';
// 因纳入预览插件
import VMdPreview from '@kangc/v-md-editor/lib/preview';
import '@kangc/v-md-editor/lib/style/preview.css';
//使用的是vuepress主题
import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
// 引入表情插件
// import createEmojiPlugin from '@kangc/v-md-editor/lib/plugins/emoji/index';
// import '@kangc/v-md-editor/lib/plugins/emoji/emoji.css';
// 引入对齐方式插件
import createAlignPlugin from '@kangc/v-md-editor/lib/plugins/align';
// 代码高亮工具
import Prism from 'prismjs';
// markdown 编辑器注册主题、代码高亮、表情插件、对齐方式插件
VueMarkdownEditor.use(vuepressTheme, {
Prism
})
// .use(createEmojiPlugin())
.use(createAlignPlugin());
// markdown 预览注册主题、代码高亮、对齐方式插件
VMdPreview.use(vuepressTheme, { Prism }).use(createAlignPlugin());
export { VueMarkdownEditor, VMdPreview };
4、在 plugins 文件夹下创建 index.js
以下为 plugins /index.js 内容
import { VueMarkdownEditor, VMdPreview } from './mdEditor/index';
const pluginList = [
VueMarkdownEditor,
VMdPreview
// 如果有使用到别的插件,引入放在这个位置就好
];
const plugins = {
install(app) {
// 批量注册插件 改用自动引入
Object.entries(pluginList).forEach(([, v]) => {
app.use(v);
});
}
};
export default plugins;
三、全局注册
在 main.js 文件中引入 plugins
import plugins from './plugins';
四、封装 markdown 编辑器
import { computed, ref, reactive } from 'vue';
import VueMarkdownEditor from '@kangc/v-md-editor';
import { uploadFile } from '@/api';
// import htmlToPdf from '@/utils/htmlToPdf';
const emit = defineEmits(['update:modelValue', 'change', 'save']);
const props = defineProps({
height: {
type: String,
default: '100%'
},
placeholder: {
type: String,
default: '请输入内容'
}
});
const leftToolbar = ref(
'undo redo clear | h bold italic strikethrough quote| customToolbar2 | ul ol table hr | emoji link image code | save'
);
// undo redo clear | h bold italic strikethrough quote | ul ol table hr | emoji link image code | save exportPDF
const toolbar = reactive({
exportPDF: {
icon: 'v-md-icon-arrow-down',
title: '导出PDF',
action(editor) {
// toolbar点击时触发的函数
console.log(editor);
// editor.save();
}
},
customToolbar2: {
title: '对齐方式',
icon: 'icon iconfont v-md-icon-align',
menus: [
{
name: '左对齐',
text: '左对齐',
action(editor) {
editor.insert(function (selected) {
const prefix = '::: align-left\n';
const suffix = ':::';
const placeholder = '请输入文本';
const content = selected || placeholder;
return {
text: `${prefix}${content}\n${suffix}`,
selected: content
};
});
}
},
{
name: '居中对齐',
text: '居中对齐',
action(editor) {
editor.insert(function (selected) {
const prefix = '::: align-center\n';
const suffix = ':::';
const placeholder = '请输入文本';
const content = selected || placeholder;
return {
text: `${prefix}${content}\n${suffix}`,
selected: content
};
});
}
},
{
name: '右对齐',
text: '右对齐',
action(editor) {
editor.insert(function (selected) {
const prefix = '::: align-right\n';
const suffix = ':::';
const placeholder = '请输入文本';
const content = selected || placeholder;
return {
text: `${prefix}${content}\n${suffix}`,
selected: content
};
});
}
}
]
}
});
const newValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
// 内容变化时触发的事件,text 为输入的内容,html 为解析之后的 html 字符串
const handleChange = (text, html) => {
const data = {
text,
html
};
emit('change', data);
};
// 上传本地图片,获取图片url, 插入编辑器
const handleUploadImage = async (event, insertImage, files) => {
const file = files[0];
const name = file.name.split('.')[0];
const url = await uploadImage(file);
insertImg(insertImage, url, name);
};
// 向编辑器插入图片
const insertImg = (insertImage, url, name) => {
insertImage({
url,
desc: name
// width: 'auto',
// height: 'auto',
});
};
// 上传图片 - 接口
const uploadImage = async file => {
try {
let imgUrl;
const formData = new FormData();
formData.append('file', file);
const { code, data, message } = await uploadFile(formData);
if (code === 200) {
imgUrl = data;
} else {
console.log(message);
}
return imgUrl;
} catch (error) {
console.log(error);
}
};
// 保存
const handleSave = (text, html) => {
const data = {
text,
html
};
emit('save', data);
};
@import url('@/styles/fonts/markdown-ext-font-icon/iconfont.css');
五、封装 markdown 预览
{{ anchor.title }}
import { ref, nextTick, watch } from 'vue';
import { getDocumentContent } from '@/api/document/document';
const props = defineProps({ height: String, currentId: [String, Number] });
const text = ref(``);
let titles = ref(null);
const previewContainer = ref();
const preview = ref();
const generateTitles = () => {
const anchors = preview.value.$el.querySelectorAll('h1,h2,h3,h4,h5,h6');
titles.value = Array.from(anchors).filter(title => !!title.innerText.trim());
if (!titles.value.length) {
titles.value = [];
return;
}
const hTags = Array.from(new Set(titles.value.map(title => title.tagName))).sort();
titles.value = titles.value.map(el => ({
title: el.innerText,
lineIndex: el.getAttribute('data-v-md-line'),
indent: hTags.indexOf(el.tagName)
}));
};
// 获取文档内容接口
const getDocument = async () => {
try {
const { code, data } = await getDocumentContent(props.currentId);
if (code === 200) {
text.value = data || '';
nextTick(() => {
generateTitles();
});
}
} catch (error) {
console.log(error);
}
};
// 导航跳转
const handleAnchorClick = anchor => {
const { lineIndex } = anchor;
const heading = preview.value.$el.querySelector(`[data-v-md-line="${lineIndex}"]`);
if (heading) {
preview.value.scrollToTarget({
target: heading,
scrollContainer: previewContainer.value,
top: 60
});
}
};
watch(
() => props.currentId,
value => {
if (value) {
getDocument();
}
},
{
deep: true,
immediate: true
}
);
.container {
display: flex;
height: calc(100vh - 144px);
border-radius: 6px;
overflow: hidden;
.nav,
.preview {
height: 100%;
background: #f2f3f7;
overflow-y: auto;
}
.nav {
width: 300px;
padding: 20px;
border-right: 1px solid #eee;
}
.preview {
width: 100%;
padding-left: 10px;
box-sizing: border-box;
}
:deep(.vuepress-markdown-body) {
background: #f2f3f7;
}
}
|