临沂市文章资讯

Vue3使用md-editor-v3的实例解读markdown文本

2026-03-24 14:33:01 浏览次数:3
详细信息
Vue3 使用 md-editor-v3 实例解读 Markdown 文本

一、安装与基本使用

1. 安装依赖

npm install md-editor-v3
# 或
yarn add md-editor-v3
# 或
pnpm add md-editor-v3

2. 基本组件使用

<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('![')) {
      result.images++;
    }
    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>

三、高级功能配置

1. 自定义工具栏

// 完全自定义工具栏
const customToolbars = [
  // 第一组
  {
    toolbarId: 'group1',
    name: '格式',
    icon: 'Format',
    child: ['bold', 'italic', 'underline']
  },
  // 第二组
  {
    toolbarId: 'group2',
    name: '列表',
    icon: 'List',
    child: ['unorderedList', 'orderedList', 'task']
  },
  // 单个按钮
  '|', // 分隔符
  'revoke',
  'next',
  'save'
];

2. 主题切换

<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>

3. 图片上传配置

// 使用 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);
    // 可以显示错误提示
  }
};

四、与后端集成

1. 保存到数据库

// 保存 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] : '无标题文档';
};

2. 从数据库加载

// 加载 Markdown 内容
const loadContent = async (id) => {
  try {
    const response = await axios.get(`/api/markdown/${id}`);
    markdownText.value = response.data.content;
  } catch (error) {
    console.error('加载失败:', error);
  }
};

五、最佳实践

1. 性能优化

// 使用防抖处理频繁保存
import { debounce } from 'lodash-es';

const autoSave = debounce((content) => {
  localStorage.setItem('auto-save', content);
}, 1000);

watch(markdownText, (newContent) => {
  autoSave(newContent);
});

2. 错误处理

<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>

六、常见问题解决

1. SSR 兼容性问题

// 解决 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;
});

2. 样式问题

/* 自定义样式覆盖 */
.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 项目中使用。

相关推荐