markdown预览组件,匹配树形大纲

当前场景需求是markdown预览,右侧与树形大纲,支持收起跳转,并不包含编辑,封装markdown预览的vue组件。

实现效果

demo: ztrainwilliams.github.io/md-preview/

方案确定

基于@kangc/v-md-editor 、vue2 封装的 markdown 预览组件,封装包含功能:预览部分(代码高亮、katex 公式、流程图、Emoji、代码复制、代码行号、todoList、提示),基础@kangc补充;树形大纲部分,生成预览内容后收集标题节点再生成树形数据。

代码实现

右侧大纲部分

vue 复制代码
<template>
  <div class="md-preview">
    <v-md-preview :text="docDetail" ref="preview"></v-md-preview>
    <div v-if="showLinks" class="preview-links">
      <el-tree
        :data="data"
        :props="defaultProps"
        default-expand-all
        :expand-on-click-node="false"
        @node-click="handleClick"
      >
        <span slot-scope="{ node }">
          <span class="custom-tree-node-label" :title="node.label">
            {{ node.label }}
          </span>
        </span>
      </el-tree>
    </div>
  </div>
</template>
<script>
import Vue from "vue";
import VMdPreview from "@kangc/v-md-editor/lib/preview";
import "@kangc/v-md-editor/lib/style/preview.css";
import vuepressTheme from "@kangc/v-md-editor/lib/theme/vuepress.js";
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
import Prism from "prismjs";
import katex from "katex/dist/katex.min.js";
import "katex/dist/katex.css";
import createKatexPlugin from "@kangc/v-md-editor/lib/plugins/katex/npm";
import mermaid from 'mermaid/dist/mermaid.js'
import createMermaidPlugin from "@kangc/v-md-editor/lib/plugins/mermaid/npm";
import "@kangc/v-md-editor/lib/plugins/mermaid/mermaid.css";
import createCopyCodePlugin from "@kangc/v-md-editor/lib/plugins/copy-code/index";
import "@kangc/v-md-editor/lib/plugins/copy-code/copy-code.css";
import createEmojiPlugin from "@kangc/v-md-editor/lib/plugins/emoji/index";
import "@kangc/v-md-editor/lib/plugins/emoji/emoji.css";
import createTodoListPlugin from "@kangc/v-md-editor/lib/plugins/todo-list/index";
import "@kangc/v-md-editor/lib/plugins/todo-list/todo-list.css";
import createLineNumbertPlugin from "@kangc/v-md-editor/lib/plugins/line-number/index";
import createTipPlugin from "@kangc/v-md-editor/lib/plugins/tip/index";
import "@kangc/v-md-editor/lib/plugins/tip/tip.css";

VMdPreview.use(vuepressTheme, {
  Prism,
});
VMdPreview.use(createKatexPlugin(katex)); // katex 公式
VMdPreview.use(createMermaidPlugin(mermaid)); // 流程图
VMdPreview.use(createEmojiPlugin()); // Emoji
VMdPreview.use(createCopyCodePlugin()); // 代码复制
VMdPreview.use(createLineNumbertPlugin()); // 代码行号
VMdPreview.use(createTodoListPlugin()); // TODO List
VMdPreview.use(createTipPlugin()); // 提示
Vue.use(VMdPreview);

export default {
  name: "md-preview",
  props: {
    value: {
      type: String,
    },
    showLinks: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      docDetail: "",
      links: [],
      data: [],
      defaultProps: {
        children: "children",
        label: "title",
      },
    };
  },
  watch: {
    value: {
      handler(v) {
        this.docDetail = v;
        this.setLinks();
      },
      immediate: true,
    },
  },
  created() {},
  methods: {
    // 获取收集预览内容的标题节点
    setLinks() {
      if (!this.showLinks) return;
      this.$nextTick(() => {
        const anchors = this.$refs.preview
          ? this.$refs.preview.$el.querySelectorAll("h1,h2,h3,h4,h5,h6")
          : [];
        const titles = Array.from(anchors).filter(
          (title) => !!title.innerText.trim()
        );

        if (!titles.length) {
          this.links = [];
          this.setTreeData(this.links);
          return;
        }

        const hTags = Array.from(
          new Set(titles.map((title) => title.tagName))
        ).sort();
        
        this.links = titles.map((el) => ({
          title: el.innerText,
          lineIndex: el.getAttribute("data-v-md-line"),
          indent: hTags.indexOf(el.tagName),
        }));
        this.setTreeData(this.links);
      });
    },
    // 获取树形数据
    setTreeData(list) {
      const tocItems = [];

      list.forEach((row, index) => {
        const { title, indent } = row;
        const item = { indent, title, index: index + 1, ...row };

        if (tocItems.length === 0) {
          // 第一个 item 直接 push
          tocItems.push(item);
        } else {
          let lastItem = tocItems[tocItems.length - 1]; // 最后一个 item

          if (item.indent > lastItem.indent) {
            // item 是 lastItem 的 children
            for (let i = lastItem.indent + 1; i <= 6; i++) {
              const { children } = lastItem;
              if (!children) {
                // 如果 children 不存在
                lastItem.children = [item];
                break;
              }

              lastItem = children[children.length - 1]; // 重置 lastItem 为 children 的最后一个 item

              if (item.indent <= lastItem.indent) {
                // item indent 小于或等于 lastItem indent 都视为与 children 同级
                children.push(item);
                break;
              }
            }
          } else {
            // 置于最顶级
            tocItems.push(item);
          }
        }
      });
      this.data = tocItems;
    },
    // 点击跳转
    handleClick(item) {
      const heading = this.$refs.preview.$el.querySelector(
        `[data-v-md-line="${item.lineIndex}"]`
      );

      if (heading) {
        this.$refs.preview.scrollToTarget({
          target: heading,
          scrollContainer: window,
          top: 60,
        });
      }
    },
  },
};
</script>
<style lang="css">
.md-preview {
  display: flex;
  position: relative;
  padding-right: 240px;
}
.v-md-editor-preview {
  flex: 1;
}
.preview-links {
  width: 240px;
  position: fixed;
  z-index: 998;
  right: 0;
  top: 52px;
  max-width: 305px;
  height: calc(100vh - 52px);
  max-height: calc(100vh - 52px);
  overflow-y: auto;
  overflow-x: hidden;
}
.preview-links .el-tree {
  background-color: #fafafa;
}
.el-tree-node__content {
  padding: 2px 0;
  font-size: 14px;
  height: auto;
  min-height: 24px;
}
.custom-tree-node-label {
  white-space: normal;
}
</style>

仓库链接

  1. github.com/ZTrainWilli...
相关推荐
qq_256247056 小时前
除了“温度”,如何用 Penalty (惩罚) 治好 AI 的“复读机”毛病?
后端
工藤学编程6 小时前
零基础学AI大模型之CoT思维链和ReAct推理行动
前端·人工智能·react.js
徐同保6 小时前
上传文件,在前端用 pdf.js 提取 上传的pdf文件中的图片
前端·javascript·pdf
怕浪猫6 小时前
React从入门到出门第四章 组件通讯与全局状态管理
前端·javascript·react.js
内存不泄露6 小时前
基于Spring Boot和Vue 3的智能心理健康咨询平台设计与实现
vue.js·spring boot·后端
qq_12498707536 小时前
基于Spring Boot的电影票网上购票系统的设计与实现(源码+论文+部署+安装)
java·大数据·spring boot·后端·spring·毕业设计·计算机毕业设计
欧阳天风6 小时前
用setTimeout代替setInterval
开发语言·前端·javascript
麦兜*6 小时前
【Spring Boot】 接口性能优化“十板斧”:从数据库连接到 JVM 调优的全链路提升
java·大数据·数据库·spring boot·后端·spring cloud·性能优化
EndingCoder6 小时前
箭头函数和 this 绑定
linux·前端·javascript·typescript
郑州光合科技余经理6 小时前
架构解析:同城本地生活服务o2o平台海外版
大数据·开发语言·前端·人工智能·架构·php·生活