npm install md-editor-v3
# 或
yarn add md-editor-v3
# 或
pnpm add md-editor-v3
<template>
<div class="markdown-container">
<!-- 编辑器模式 -->
<MdEditor v-model="text" @on-upload-img="onUploadImg" />
<!-- 预览模式 -->
<MdPreview :modelValue="text" />
<!-- 仅预览特定内容 -->
<MdCatalog :editorId="editorId" :scrollElement="scrollElement" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import { MdEditor, MdPreview, MdCatalog } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
const text = ref('# Hello Markdown\n\n这是一段 **加粗** 文本');
const editorId = 'my-editor';
const scrollElement = document.documentElement;
// 图片上传处理
const onUploadImg = async (files, callback) => {
const res = await Promise.all(
files.map(file => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
};
reader.readAsDataURL(file);
});
})
);
callback(res);
};
</script>
<template>
<div class="markdown-demo">
<div class="container">
<!-- 工具栏 -->
<div class="toolbar">
<button @click="toggleMode">
{{ isEditMode ? '预览模式' : '编辑模式' }}
</button>
<button @click="exportMarkdown">导出</button>
</div>
<div class="content-wrapper">
<!-- 编辑器/预览器 -->
<div class="editor-section" v-if="isEditMode">
<MdEditor
v-model="markdownText"
:editorId="editorId"
:toolbars="toolbars"
@on-upload-img="handleImageUpload"
@on-save="handleSave"
:theme="theme"
:previewTheme="previewTheme"
:codeTheme="codeTheme"
:style="editorStyle"
/>
</div>
<div class="preview-section" v-else>
<MdPreview
:modelValue="markdownText"
:editorId="editorId"
:previewTheme="previewTheme"
:codeTheme="codeTheme"
/>
</div>
<!-- 目录导航 -->
<div class="catalog-section" v-if="showCatalog">
<div class="catalog-title">目录</div>
<MdCatalog
:editorId="editorId"
:scrollElement="scrollElement"
/>
</div>
</div>
<!-- 解析结果展示 -->
<div class="parsed-section" v-if="showParsedResult">
<h3>解析结果</h3>
<pre>{{ parsedResult }}</pre>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { MdEditor, MdPreview, MdCatalog } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
// 响应式数据
const markdownText = ref(`# Vue3 Markdown 编辑器示例
## 特性介绍
- 🎉 **实时预览** - 编辑时同步预览
- 📁 **文件上传** - 支持图片上传
- 📋 **代码高亮** - 多种主题可选
- 📑 **目录生成** - 自动生成文章目录
## 代码示例
\`\`\`javascript
// Vue3 组件示例
import { ref } from 'vue';
import { MdEditor } from 'md-editor-v3';
export default {
setup() {
const text = ref('# Hello World');
return { text };
}
};
\`\`\`
## 表格示例
| 功能 | 状态 | 说明 |
|------|------|------|
| 编辑 | ✅ | 支持完整编辑功能 |
| 预览 | ✅ | 实时预览效果 |
| 上传 | ✅ | 支持图片上传 |
## 数学公式(可选)
$$
\\frac{1}{\\sqrt{2\\pi}\\sigma}e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}
$$
> 提示:这是一个功能完整的 Markdown 编辑器示例
`);
// 状态管理
const isEditMode = ref(true);
const showCatalog = ref(true);
const showParsedResult = ref(false);
const theme = ref('light'); // 'light' 或 'dark'
const previewTheme = ref('default');
const codeTheme = ref('atom');
const editorId = 'demo-editor';
// 工具栏配置
const toolbars = [
'bold',
'underline',
'italic',
'strikeThrough',
'sub',
'sup',
'quote',
'unorderedList',
'orderedList',
'codeRow',
'code',
'link',
'image',
'table',
'mermaid',
'katex',
'revoke',
'next',
'save',
'pageFullscreen',
'fullscreen',
'preview',
'htmlPreview',
'catalog'
];
// 编辑器样式
const editorStyle = {
height: '500px',
width: '100%'
};
// 滚动元素(目录使用)
let scrollElement = null;
// 计算属性:解析结果
const parsedResult = computed(() => {
const lines = markdownText.value.split('\n');
const result = {
title: '',
headings: [],
codeBlocks: 0,
images: 0,
links: 0,
wordCount: markdownText.value.replace(/[#\*\`\-\+\[\]\(\)]/g, '').split(/\s+/).length
};
lines.forEach(line => {
// 提取标题
if (line.startsWith('# ')) {
result.title = line.replace('# ', '');
} else if (line.startsWith('## ')) {
result.headings.push(line.replace('## ', ''));
}
// 统计代码块
if (line.startsWith('```')) {
result.codeBlocks++;
}
// 统计图片和链接
if (line.includes(' && !line.startsWith('![')) {
result.links++;
}
});
// 仅计算完整的代码块
result.codeBlocks = Math.floor(result.codeBlocks / 2);
return result;
});
// 方法定义
const toggleMode = () => {
isEditMode.value = !isEditMode.value;
};
const toggleCatalog = () => {
showCatalog.value = !showCatalog.value;
};
const exportMarkdown = () => {
const blob = new Blob([markdownText.value], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.md';
a.click();
URL.revokeObjectURL(url);
};
const handleImageUpload = async (files, callback) => {
console.log('上传文件:', files);
// 模拟上传过程
const promises = files.map(file => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
// 在实际项目中,这里应该是上传到服务器
// 返回服务器返回的图片URL
resolve({
url: e.target.result,
title: file.name
});
};
reader.readAsDataURL(file);
});
});
const urls = await Promise.all(promises);
callback(urls.map(item => item.url));
};
const handleSave = (value, html) => {
console.log('保存内容:', value);
console.log('HTML内容:', html);
alert('内容已保存(控制台查看)');
};
// 初始化
onMounted(() => {
scrollElement = document.documentElement;
});
// 清理
onUnmounted(() => {
// 清理操作
});
// 监听变化
const unwatch = watch(markdownText, (newVal) => {
console.log('内容变化,长度:', newVal.length);
localStorage.setItem('markdown-draft', newVal);
});
</script>
<style scoped>
.markdown-demo {
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
max-width: 1200px;
margin: 0 auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.toolbar {
padding: 10px 20px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
display: flex;
gap: 10px;
}
.toolbar button {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.toolbar button:hover {
background: #0056b3;
}
.content-wrapper {
display: flex;
min-height: 500px;
}
.editor-section,
.preview-section {
flex: 1;
padding: 20px;
overflow: auto;
}
.catalog-section {
width: 250px;
border-left: 1px solid #e0e0e0;
padding: 20px;
background: #fafafa;
overflow-y: auto;
}
.catalog-title {
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.parsed-section {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-top: 1px solid #e0e0e0;
}
.parsed-section h3 {
margin-top: 0;
}
.parsed-section pre {
background: #fff;
padding: 15px;
border-radius: 4px;
overflow: auto;
}
@media (max-width: 768px) {
.content-wrapper {
flex-direction: column;
}
.catalog-section {
width: 100%;
border-left: none;
border-top: 1px solid #e0e0e0;
}
}
</style>
// 完全自定义工具栏
const customToolbars = [
// 第一组
{
toolbarId: 'group1',
name: '格式',
icon: 'Format',
child: ['bold', 'italic', 'underline']
},
// 第二组
{
toolbarId: 'group2',
name: '列表',
icon: 'List',
child: ['unorderedList', 'orderedList', 'task']
},
// 单个按钮
'|', // 分隔符
'revoke',
'next',
'save'
];
<template>
<div>
<select v-model="theme" @change="changeTheme">
<option value="light">浅色主题</option>
<option value="dark">深色主题</option>
<option value="default">默认主题</option>
</select>
<MdEditor
v-model="text"
:theme="theme"
:previewTheme="previewTheme"
:codeTheme="codeTheme"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
const theme = ref('light');
const previewTheme = ref('default');
const codeTheme = ref('github');
// 可用的代码主题
const codeThemes = [
'atom', 'github', 'gradient', 'kimbie', 'paraiso', 'qtcreator', 'stackoverflow'
];
const changeTheme = () => {
// 同时切换预览主题
previewTheme.value = theme.value === 'dark' ? 'vuepress' : 'default';
};
</script>
// 使用 Axios 上传到服务器
const handleImageUpload = async (files, callback) => {
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
try {
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
// 假设服务器返回 { data: ['url1', 'url2'] }
callback(response.data.data);
} catch (error) {
console.error('上传失败:', error);
// 可以显示错误提示
}
};
// 保存 Markdown 内容
const saveContent = async () => {
try {
const response = await axios.post('/api/save-markdown', {
content: markdownText.value,
title: extractTitle(markdownText.value),
updatedAt: new Date().toISOString()
});
if (response.data.success) {
alert('保存成功');
}
} catch (error) {
console.error('保存失败:', error);
}
};
// 提取标题
const extractTitle = (content) => {
const match = content.match(/^# (.+)$/m);
return match ? match[1] : '无标题文档';
};
// 加载 Markdown 内容
const loadContent = async (id) => {
try {
const response = await axios.get(`/api/markdown/${id}`);
markdownText.value = response.data.content;
} catch (error) {
console.error('加载失败:', error);
}
};
// 使用防抖处理频繁保存
import { debounce } from 'lodash-es';
const autoSave = debounce((content) => {
localStorage.setItem('auto-save', content);
}, 1000);
watch(markdownText, (newContent) => {
autoSave(newContent);
});
<template>
<div>
<MdEditor
v-if="!error"
v-model="text"
@on-error="handleError"
/>
<div v-else class="error-message">
编辑器加载失败: {{ error }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const error = ref(null);
const handleError = (err) => {
error.value = err.message;
console.error('编辑器错误:', err);
};
</script>
// 解决 SSR 下的导入问题
import { defineClientComponent } from 'vitepress';
const MdEditor = defineClientComponent(() =>
import('md-editor-v3').then(mod => mod.MdEditor)
);
// 或使用动态导入
const MdEditor = shallowRef(null);
onMounted(async () => {
const module = await import('md-editor-v3');
MdEditor.value = module.MdEditor;
});
/* 自定义样式覆盖 */
.md-editor {
font-family: 'Microsoft YaHei', sans-serif;
}
.md-editor-dark {
--md-bk-color: #1e1e1e;
}
/* 调整预览样式 */
.md-editor-preview-wrapper {
line-height: 1.6;
}
/* 代码块样式 */
.md-editor-preview-wrapper pre {
border-radius: 6px;
}
这个完整的实例涵盖了 md-editor-v3 的主要功能,包括编辑、预览、目录生成、图片上传、主题切换等,可以直接在 Vue3 项目中使用。