vue中使用markdown并根据markdown标题标签生成侧导航

洪哲彦
2023-12-01

1.安装mavon-editor

引入markdwon编辑器

<el-scrollbar style="height:100%">
    <mavon-editor v-model="mdContent" ref="md" :toolbars="toolbars" @change="change"               
     @save="submit" @imgAdd="$imgAdd" @imgDel="$imgDel" class="mavonClass" />
     <div class="buttonClass">
        <el-button @click="submit" type="primary">保存</el-button>
      </div>
</el-scrollbar>
import { mavonEditor } from 'mavon-editor';
import 'mavon-editor/dist/css/index.css';
import toolbars from 'markDownToolbars';
var rendererMD = new marked.Renderer();
marked.setOptions({
  renderer: rendererMD,
  gfm: true,
  tables: true,
  breaks: false,
  pedantic: false,
  sanitize: false,
  smartLists: true,
  smartypants: false
});
export default {
   data () {
       return {
         mdContent: '', // markdown语法的内容, 如果没有设置为'',不能设置为null,为null会报错
         toolbars: toolbars,  // markdown提示栏
         img_file: {},
         htmlCont: '' // 转化成html存储的内容
       }
   },
   components: {
        mavonEditor
   },
   methods: {
        change (value, render) {
             this.$nextTick(() => {
                this.mdContent = value;
                this.htmlCont = render;
              })
        },
        // 上传图片
        $imgAdd (pos, $file) {
          // 第一步.将图片上传到服务器.
          var formdata = new FormData();
          formdata.append('file', $file);
          this.img_file[pos] = $file;
          axios.post(`${this.productAppBase}/fileUpload/upload`, formdata)
          .then(res => {
              let _res = res.data;
               // 第二步将返回的url替换到文本原位置![...](0) -> ![...](url)
              this.$refs.md.$img2Url(pos, this.productAppBase + _res.url);
              console.log('res=>', res);
            })
    },
        // 删除图片
        $imgDel (pos) {
          delete this.img_file[pos];
        },
   }
}

 markDownToolbars.js

let toolbars = {
  bold: true, // 粗体
  italic: true, // 斜体
  header: true, // 标题
  underline: true, // 下划线
  strikethrough: true, // 中划线
  mark: true, // 标记
  superscript: true, // 上角标
  subscript: true, // 下角标
  quote: true, // 引用
  ol: true, // 有序列表
  ul: true, // 无序列表
  link: true, // 链接
  imagelink: true, // 图片链接
  code: true, // code
  table: true, // 表格
  fullscreen: true, // 全屏编辑
  readmodel: true, // 沉浸式阅读
  htmlcode: true, // 展示html源码
  help: true, // 帮助
  /* 1.3.5 */
  undo: true, // 上一步
  redo: true, // 下一步
  trash: true, // 清空
  // save: true, // 保存(触发events中的save事件)
  /* 1.4.2 */
  navigation: true, // 导航目录
  /* 2.1.8 */
  alignleft: true, // 左对齐
  aligncenter: true, // 居中
  alignright: true, // 右对齐
  /* 2.2.1 */
  // subfield: true, // 单双栏模式
  preview: true // 预览
}
export default toolbars;

2.安装marked(解析markdwon语法)

<el-container>
      <el-aside width="300px" class="leftNav">
        <el-scrollbar style="height:100%">
          <bc-menu v-if="navList.length > 0" ref="bcMenu" :menuData="navList"></bc-menu>
        </el-scrollbar>
      </el-aside>
      <el-main class="rightCont">
        <el-scrollbar style="height:100%" ref="helpDocs" @scroll="handleScroll">
          <div class="main-cont">
            <div class="markdownBox" v-html="compiledMarkdown"></div>
          </div>
        </el-scrollbar>
      </el-main>
    </el-container>
import marked from 'marked';
import bcMenu from 'bcMenu';  // elementui 菜单
export default {
  data () {
    return {
      navList: [],
      activeIndex: 0,
      docsFirstLevels: [],
    }
  },
  components: {
    bcMenu
  },
  mounted () {
    if (this.mdContent) {
      this.navList = this.handleNavTree();
      this.getDocsFirstLevels(0);
    }
  },
  methods: {
    change (value, render) {
      this.$nextTick(() => {
        this.mdContent = value;
        this.htmlCont = render;
      })
      // render 为 markdown 解析后的结果[html]
    }
    // markdown方法结束
    getDocsFirstLevels (times) {
      // 解决图片加载会影响高度问题
      setTimeout(() => {
        let firstLevels = [];
        Array.from(document.querySelectorAll('h2'), element => {
          firstLevels.push(element.offsetTop - 60)
        })
        this.docsFirstLevels = firstLevels;

        if (times < 8) {
          this.getDocsFirstLevels(times + 1);
        }
      }, 500);
    },
    // 
    handleScroll () {
      // 根据滚动右侧内容定位到左侧菜单
      if (this.$refs['helpDocs']) {
        let scrollTop = this.$refs['helpDocs'].wrap.scrollTop;
        let _article = document.querySelectorAll('.step-jump')
        _article.forEach((item, index) => {
          if (scrollTop >= item.offsetTop - 70) {
            this.$refs.bcMenu.getCurrent(`index-${index}`);
          }
        })
      }
    },
    getTitle (content) {
      let nav = [];
      let tempArr = [];
      content.replace(/(#+)[^#][^\n]*?(?:\n)/g, function (match, m1, m2) {
        let title = match.replace('\n', '');
        if (title.indexOf('</font>') > -1) {
          return false;
        }
        let level = m1.length;
        tempArr.push({
          name: title.replace(/^#+/, '').replace(/\([^)]*?\)/, ''),
          level: level,
          children: [],
          icon: 'icon-dian'
        });
      });
      // 处理菜单,以及添加与id对应的index值
      nav = tempArr.filter(item => item.level <= 4 && item.level > 1) || [];
      // 设置大标题
      let nameFind = tempArr.find(item => item.level == 1) || {};
      this.name = nameFind.name
      let index = 0;
      // eslint-disable-next-line no-return-assign
      return nav = nav.map(item => {
        item.index = index++;
        item.code = item.index;
        item.anchor = `index-${item.index}`;
        return item;
      });
    },
    // 将标题数据处理成树结构
    handleNavTree () {
      let navs = this.getTitle(this.content);
      // 设置了4级导航
      let navLevel = [1, 2, 3, 4];
      let retNavs = [];
      let toAppendNavList, parentNavList = [];

      navLevel.forEach(level => {
        // 遍历标题,将同一级的标题组成新数组
        toAppendNavList = this.find(navs, {
          level: level
        });
        parentNavList = this.find(navs, {
          level: level - 1
        });
        if (retNavs.length === 0) {
          // 处理一级标题
          retNavs = retNavs.concat(toAppendNavList);
        } else {
          // 处理其他标题,并将其他标题添加到对应的父级标题的children中
          toAppendNavList.forEach(item => {
            item = Object.assign(item);
            let parentNavIndex = this.getParentIndex(navs, item.index);
            return this.appendToParentNav(parentNavList, parentNavIndex, item);
          });
        }
      });
      return retNavs;
    },
    find (arr, condition) {
      return arr.filter(item => {
        for (let key in condition) {
          if (condition.hasOwnProperty(key) && condition[key] !== item[key]) {
            return false;
          }
        }
        return true;
      });
    },
    getParentIndex (nav, endIndex) {
      for (var i = endIndex - 1; i >= 0; i--) {
        if (nav[endIndex].level > nav[i].level) {
          return nav[i].index;
        }
      }
    },
    appendToParentNav (nav, parentIndex, newNav) {
      let index = this.findIndex(nav, {
        index: parentIndex
      });
      nav[index].children = nav[index].children.concat(newNav);
    },
    findIndex (arr, condition) {
      let ret = -1;
      arr.forEach((item, index) => {
        for (var key in condition) {
          if (condition.hasOwnProperty(key) && condition[key] !== item[key]) {
            return false;
          }
        }
        ret = index;
      });
      return ret;
    }
  },
  computed: {
    content () {
      return this.mdContent
    },
    compiledMarkdown: function () {
      let index = 0, that = this;
      rendererMD.heading = function (text, level) {
        // 导航
        if (level <= 4 && level != 1) {
          return `<h${level} id="index-${index++}" class="step-jump">${text}</h${level}>`;
        } else {
          return `<h${level}>${text}</h${level}>`;
        }
      };
      rendererMD.code = function (code, language) {
        code = code.replace(/\r\n/g, '<br>')
        code = code.replace(/\n/g, '<br>');
        return `<div class="text">${code}</div>`;
      };
      return marked(this.content);
    }
  }
}

 

 类似资料: