MarkdownEditor Markdown 编辑器
基于 marked 的 Markdown 编辑器组件,支持实时预览和语法高亮。
基础用法
vue
<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownEditor } from '@vben/common-ui'
const content = ref('')
</script>
<template>
<MarkdownEditor v-model="content" />
</template>自定义高度
vue
<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownEditor } from '@vben/common-ui'
const content = ref('')
</script>
<template>
<MarkdownEditor
v-model="content"
height="600px"
/>
</template>分屏模式
vue
<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownEditor } from '@vben/common-ui'
const content = ref('')
</script>
<template>
<MarkdownEditor
v-model="content"
mode="split"
/>
</template>只读模式
vue
<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownEditor } from '@vben/common-ui'
const content = ref(`# 标题
这是一段 **Markdown** 内容。
- 列表项 1
- 列表项 2
- 列表项 3
`)
</script>
<template>
<MarkdownEditor
v-model="content"
readonly
/>
</template>API
Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| modelValue | 绑定值(Markdown 字符串) | string | '' |
| height | 编辑器高度 | string | '500px' |
| placeholder | 占位文本 | string | '请输入 Markdown 内容...' |
| readonly | 是否只读 | boolean | false |
| disabled | 是否禁用 | boolean | false |
| mode | 显示模式 | 'edit' | 'preview' | 'split' | 'split' |
| theme | 代码高亮主题 | string | 'github' |
| uploadImage | 自定义图片上传 | (file: File) => Promise<string> | - |
| toolbar | 工具栏配置 | array | 默认工具栏 |
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| update:modelValue | 内容变化时触发 | (value: string) => void |
| change | 内容变化时触发 | (value: string) => void |
| focus | 获得焦点时触发 | () => void |
| blur | 失去焦点时触发 | () => void |
Methods
| 方法名 | 说明 | 参数 |
|---|---|---|
| getMarkdown | 获取 Markdown 内容 | - |
| getHTML | 获取 HTML 内容 | - |
| clear | 清空内容 | - |
| focus | 聚焦编辑器 | - |
| insertText | 插入文本 | (text: string) => void |
完整示例
vue
<script setup lang="ts">
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { MarkdownEditor } from '@vben/common-ui'
import axios from 'axios'
const content = ref(`# Markdown 编辑器
## 功能特性
- 实时预览
- 语法高亮
- 图片上传
- 代码块支持
## 代码示例
\`\`\`javascript
function hello() {
console.log('Hello, World!')
}
\`\`\`
## 表格
| 列1 | 列2 | 列3 |
|-----|-----|-----|
| 值1 | 值2 | 值3 |
`)
const editorRef = ref()
const mode = ref<'edit' | 'preview' | 'split'>('split')
// 自定义图片上传
const handleUploadImage = async (file: File) => {
// 检查文件大小
if (file.size > 2 * 1024 * 1024) {
message.error('图片大小不能超过 2MB')
throw new Error('图片过大')
}
// 上传图片
const formData = new FormData()
formData.append('file', file)
try {
const { data } = await axios.post('/api/upload/image', formData)
return data.url
} catch (error) {
message.error('图片上传失败')
throw error
}
}
const handleChange = (value: string) => {
console.log('内容变化:', value)
}
const handleSave = () => {
const markdown = editorRef.value?.getMarkdown()
const html = editorRef.value?.getHTML()
console.log('Markdown:', markdown)
console.log('HTML:', html)
message.success('保存成功')
}
const handleClear = () => {
editorRef.value?.clear()
}
const handleInsertCode = () => {
const code = `\`\`\`javascript
function example() {
console.log('示例代码')
}
\`\`\``
editorRef.value?.insertText(code)
}
</script>
<template>
<div class="editor-container">
<div class="editor-toolbar">
<a-space>
<a-radio-group v-model:value="mode" button-style="solid">
<a-radio-button value="edit">编辑</a-radio-button>
<a-radio-button value="split">分屏</a-radio-button>
<a-radio-button value="preview">预览</a-radio-button>
</a-radio-group>
<a-button @click="handleInsertCode">插入代码</a-button>
<a-button type="primary" @click="handleSave">保存</a-button>
<a-button @click="handleClear">清空</a-button>
</a-space>
</div>
<MarkdownEditor
ref="editorRef"
v-model="content"
:mode="mode"
height="600px"
:upload-image="handleUploadImage"
@change="handleChange"
/>
</div>
</template>
<style scoped>
.editor-container {
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
}
.editor-toolbar {
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #d9d9d9;
}
</style>Markdown 语法
标题
markdown
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题文本样式
markdown
**粗体文本**
*斜体文本*
~~删除线~~
`行内代码`列表
markdown
# 无序列表
- 项目 1
- 项目 2
- 项目 3
# 有序列表
1. 第一项
2. 第二项
3. 第三项链接和图片
markdown
[链接文本](https://example.com)
代码块
markdown
```javascript
function hello() {
console.log('Hello, World!')
}
```引用
markdown
> 这是一段引用文本
> 可以有多行表格
markdown
| 列1 | 列2 | 列3 |
|-----|-----|-----|
| 值1 | 值2 | 值3 |
| 值4 | 值5 | 值6 |分隔线
markdown
---
***
___工具栏配置
默认工具栏
javascript
const defaultToolbar = [
'bold', // 粗体
'italic', // 斜体
'strikethrough', // 删除线
'heading', // 标题
'code', // 行内代码
'quote', // 引用
'unordered-list', // 无序列表
'ordered-list', // 有序列表
'link', // 链接
'image', // 图片
'table', // 表格
'horizontal-rule',// 分隔线
'preview', // 预览
'fullscreen', // 全屏
'guide' // 帮助
]自定义工具栏
vue
<script setup lang="ts">
const toolbar = [
'bold',
'italic',
'heading',
'code',
'link',
'image',
'preview'
]
</script>
<template>
<MarkdownEditor
v-model="content"
:toolbar="toolbar"
/>
</template>主题定制
代码高亮主题
vue
<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownEditor } from '@vben/common-ui'
const content = ref('')
const theme = ref('github')
const themes = [
'github',
'monokai',
'dracula',
'atom-one-dark',
'vs2015'
]
</script>
<template>
<div>
<a-select v-model:value="theme" style="width: 200px; margin-bottom: 16px">
<a-select-option v-for="t in themes" :key="t" :value="t">
{{ t }}
</a-select-option>
</a-select>
<MarkdownEditor
v-model="content"
:theme="theme"
/>
</div>
</template>图片处理
图片上传
typescript
const handleUploadImage = async (file: File) => {
// 检查文件类型
if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) {
message.error('只支持 JPG、PNG、GIF 格式')
throw new Error('格式不支持')
}
// 检查文件大小
if (file.size > 2 * 1024 * 1024) {
message.error('图片大小不能超过 2MB')
throw new Error('图片过大')
}
// 上传图片
const formData = new FormData()
formData.append('file', file)
const { data } = await axios.post('/api/upload/image', formData)
return data.url
}粘贴图片
vue
<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownEditor } from '@vben/common-ui'
const content = ref('')
const handlePaste = async (event: ClipboardEvent) => {
const items = event.clipboardData?.items
if (!items) return
for (const item of items) {
if (item.type.indexOf('image') !== -1) {
const file = item.getAsFile()
if (file) {
// 上传图片
const url = await handleUploadImage(file)
// 插入图片
editorRef.value?.insertText(``)
}
}
}
}
</script>
<template>
<MarkdownEditor
v-model="content"
@paste="handlePaste"
/>
</template>导出功能
导出 Markdown
typescript
const handleExportMarkdown = () => {
const markdown = editorRef.value?.getMarkdown()
const blob = new Blob([markdown], { type: 'text/markdown' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'document.md'
link.click()
URL.revokeObjectURL(url)
}导出 HTML
typescript
const handleExportHTML = () => {
const html = editorRef.value?.getHTML()
const blob = new Blob([html], { type: 'text/html' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'document.html'
link.click()
URL.revokeObjectURL(url)
}导出 PDF
typescript
import html2pdf from 'html2pdf.js'
const handleExportPDF = () => {
const html = editorRef.value?.getHTML()
const element = document.createElement('div')
element.innerHTML = html
html2pdf()
.from(element)
.save('document.pdf')
}快捷键
| 快捷键 | 功能 |
|---|---|
| Ctrl/Cmd + B | 粗体 |
| Ctrl/Cmd + I | 斜体 |
| Ctrl/Cmd + K | 插入链接 |
| Ctrl/Cmd + Shift + I | 插入图片 |
| Ctrl/Cmd + Shift + C | 插入代码块 |
| Ctrl/Cmd + Shift + Q | 插入引用 |
| Ctrl/Cmd + Shift + L | 插入无序列表 |
| Ctrl/Cmd + Shift + O | 插入有序列表 |
| Ctrl/Cmd + Shift + T | 插入表格 |
| Ctrl/Cmd + Shift + H | 插入分隔线 |
| Ctrl/Cmd + Shift + P | 切换预览 |
| F11 | 全屏 |
注意事项
注意
- Markdown 内容会转换为 HTML,需要注意 XSS 防护
- 图片上传需要配置服务器地址
- 建议限制图片大小,避免影响性能
- 代码高亮需要引入对应的主题样式
提示
- 使用
uploadImage自定义图片上传逻辑 - 可以通过
mode属性切换显示模式 - 使用
getMarkdown()获取 Markdown 内容 - 使用
getHTML()获取 HTML 内容 - 支持拖拽上传图片