当前位置: 首页 > article >正文

使用 md-editor-v3 开发自定义 Markdown 编辑器组件

在日常项目中,开发一个功能丰富的 Markdown 编辑器对于提升用户体验和编辑效率非常重要。本文将介绍如何使用 md-editor-v3 来实现一个带有自定义工具栏和功能的 Markdown 编辑器组件。
Github: https://github.com/imzbf/md-editor-v3

一、项目结构和依赖

本示例使用 Vue 3 和 TypeScript 进行开发。核心依赖包括 md-editor-v3、@element-plus/icons-vue、@vavt/v3-extension 等。

yarn add md-editor-v3

使用现有的语言和主题等,如日语

yarn add @vavt/cm-extension

使用toolbar的现有组件,例如将内容导出为PDF

yarn add @vavt/v3-extension

二、代码实现详解

  1. 基础模板结构
    我们在模板中使用了 MdEditor 组件,并定义了一些自定义工具栏(toolbars中的自定义工具使用数字代替,从0开始)和弹窗。
<template>
  <div w-full>
    <MdEditor
      v-model="editorText"
      :footers="['markdownTotal', '=', 0, 'scrollSwitch']"
      :theme="theme"
      :toolbars="toolbars"
      @on-save="onSave"
      @on-change="handleChange"
      @on-upload-img="onUploadImg"
    >
      <template #defToolbars>
        <ExportPDF :model-value="editorText" @on-progress="onProgress" @on-success="onSuccess" />
       <!--      自定义工具栏NormalToolbar,不需要可删除-->
        <NormalToolbar title="mark" @on-click="customHandler">
          <template #trigger>
            <el-icon>
              <VideoCameraFilled />
            </el-icon>
          </template>
        </NormalToolbar>
   <!--      自定义工具栏ModalToolbar,不需要可删除-->
        <ModalToolbar
          :visible="visible"
          height="200px"
          modal-title="测试"
          title="video"
          width="400px"
          @on-close="closeModal"
          @on-adjust="adjustModal"
          @on-click="openModal"
        >
          <template #trigger>
            <el-icon>
              <List />
            </el-icon>
          </template>
        </ModalToolbar>
      </template>
      <template #defFooters>
        <NormalFooterToolbar>{{ parseTime(new Date()) }}</NormalFooterToolbar>
      </template>
    </MdEditor>
  </div>
</template>
2. 脚本逻辑
在 <script setup> 部分,我们定义了组件的核心逻辑和交互方法。

typescript
复制代码
<script lang="ts" setup>
import { defineEmits, defineProps, ref, watch } from 'vue';
import { MdEditor, ModalToolbar, NormalFooterToolbar, NormalToolbar, Themes, ToolbarNames } from 'md-editor-v3';
import { upload } from '@/utils/request';
import { List, VideoCameraFilled } from '@element-plus/icons-vue';
import { ExportPDF } from '@vavt/v3-extension';
import modal from '@/plugins/modal';
import { parseTime } from '@/utils/ruoyi';

// 定义传入参数和事件
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
});

// 工具栏配置
const toolbars = ref<ToolbarNames[]>([
  'revoke', 'next', '-', 'bold', 'underline', 'italic', 'strikeThrough', '-', 'title', 'sub', 
  'sup', 'quote', 'unorderedList', 'orderedList', 'task', 'codeRow', 'code', '-', 'link', 
  'image', 'table', 'mermaid', 'katex', '-', 0, 1, 2, '=', 'save', 'prettier', 'pageFullscreen', 
  'catalog', 'preview', 'previewOnly', 'htmlPreview', 'github'
]);

const editorText = ref(props.modelValue);
const visible = ref(false);

// 主题切换
const isDark = useDark({ storageKey: 'useDarkKey', valueDark: 'dark', valueLight: 'light' });
const theme = ref<Themes>(isDark.value ? 'dark' : 'light');

// 监听主题变化
watch(isDark, () => {
  theme.value = isDark.value ? 'dark' : 'light';
});

// 监听传入的值变化
watch(() => props.modelValue, (newValue) => {
  editorText.value = newValue;
});

// 更新父组件的 v-model
watch(editorText, (newValue) => {
  emit('update:modelValue', newValue);
});

// 保存操作
const onSave = (value: string) => {
  console.log(value);
};

// 文本改变操作
const handleChange = (text: string) => {
  editorText.value = text;
};

// 自定义工具栏方法
const customHandler = () => {
  alert('自定义菜单');
};

// 弹窗调整方法
const adjustModal = () => {
  visible.value = false;
};

// 弹窗开启方法
const openModal = () => {
  visible.value = true;
};

// 关闭弹窗
const closeModal = () => {
  visible.value = false;
};

// 导出 PDF 进度条
const onProgress = (progress: number) => {
  console.log(progress);
};

// PDF 导出成功
const onSuccess = () => {
  modal.msgSuccess('PDF导出成功!');
};

// 图片上传
const onUploadImg = async (files: File[], callback: (url: { alt: string; title: string; url: any }[]) => void) => {
  const res = await Promise.all(
    files.map((file) => {
      return new Promise((resolve, reject) => {
        const form = new FormData();
        form.append('file', file);
        upload(form)
          .then((res) => resolve(res))
          .catch((error) => reject(error));
      });
    })
  );
  callback(
    res.map((item: any) => ({
      url: item.data.url,
      alt: item.data.fileName,
      title: item.data.fileName
    }))
  );
};
</script>

三、功能实现

  1. 自定义工具栏和弹窗
    我们通过 和 组件定义了自定义工具栏和弹窗,用于扩展 Markdown 编辑器的功能,例如添加视频插入按钮和自定义弹窗。

  2. 图片上传
    实现了 onUploadImg 方法,支持异步上传图片,并通过回调返回图片的 URL、alt 和 title 属性。

  3. 主题切换
    使用了 useDark 实现编辑器的暗黑模式切换,用户体验更加灵活。

  4. PDF 导出
    使用 @vavt/v3-extension 提供的 ExportPDF 组件实现导出 PDF 功能,并监听导出进度和完成情况。

四、只读模式

<template>
  <div class="yk-md-editor base-bg-box">
    <MdCatalog v-if="showCatalog" :editor-id="id" :scroll-element="scrollElement" :theme="theme" />
    <MdPreview :editor-id="id" :model-value="modelValue" :theme="theme" />
  </div>
</template>

<script setup>
import { MdCatalog, MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';

const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  },
  showCatalog: {
    type: Boolean,
    default: true
  }
});
// 是否暗黑模式
const isDark = useDark({
  storageKey: 'useDarkKey',
  valueDark: 'dark',
  valueLight: 'light'
});
const theme = ref(isDark.value ? 'dark' : 'light');
// 匹配菜单颜色
watch(isDark, () => {
  theme.value = isDark.value ? 'dark' : 'light';
});
const id = 'preview-only';
const scrollElement = document.documentElement;
</script>

<style lang="scss" scoped>
@import '@/assets/styles/variables.module.scss';

.yk-md-editor {
  border-radius: 5px;
  overflow: hidden;
  margin: 12px 0;
}

.md-editor {
  --md-bk-color: $base-bg-box;
}
</style>

五、总结

通过上述步骤,我们实现了一个功能强大、易于扩展的 Markdown 编辑器组件。希望这篇文章能帮助到你在项目中构建类似的编辑器。
转载自: https://web.yujky.cn/app/article/article-edit/index/1855647538840924161

具体效果可登录我的网站体验
https://web.yujky.cn/
租户:体验租户
用户名:cxks
密码: cxks123在这里插入图片描述


http://www.kler.cn/a/393050.html

相关文章:

  • 关于GCC内联汇编(也可以叫内嵌汇编)的简单学习
  • docker:docker: Get https://registry-1.docker.io/v2/: net/http: request canceled
  • linux c/c++最高效的计时方法
  • C++中的栈(Stack)和堆(Heap)
  • golang如何实现sse
  • 【练习案例】30个 CSS Javascript 加载器动画效果
  • MySQL技巧之跨服务器数据查询:基础篇-删除语句如何写
  • CSS教程(三)- CSS 三大特性
  • 系统上线后发现bug,如何回退版本?已经产生的新业务数据怎么办?
  • CSS 编写位置详解及优先级分析
  • windows C#-LINQ概述
  • win32com库基于wps对Word文档的基础操作
  • 手动安装Ubuntu系统中的network-manager包(其它包同理)
  • DNS面临的4大类共计11小类安全风险及防御措施
  • 【go从零单排】go语言中testing的几种类型
  • Kafka参数了解
  • Find My电子体温计|苹果Find My技术与体温计结合,智能防丢,全球定位
  • PostgreSQL序列:创建、管理与高效应用指南
  • [ Linux 命令基础 7 ] Linux 命令详解-磁盘管理相关命令
  • 《高级 SQL 技巧:提升查询效率与灵活性》
  • Bootstrap和jQuery开发案例
  • 动态规划 —— 子数组系列-环形子数组的最大和
  • react中如何在一张图片上加一个灰色蒙层,并添加事件?
  • C#进阶-快速了解IOC控制反转及相关框架的使用
  • 2024-09-01 - 分布式集群网关 - LoadBalancer - 阿里篇 - 流雨声
  • Spring Boot项目的配置文件有哪些?加载优先级谁最高?配置优先级谁最高?