Vue3集成v-md-editor:预览组件文章目录功能 支持点击跳转、选中高亮、目录自动定位

581 阅读1分钟

直接上代码

引用组件

"splitpanes": "^3.1.5",
"@kangc/v-md-editor": "^2.3.15",
"axios": "^1.4.0",

main.ts

// @ts-ignore
import VMdPreview from '@kangc/v-md-editor/lib/preview.js';
import '@kangc/v-md-editor/lib/style/preview.css';
// @ts-ignore
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
import '@kangc/v-md-editor/lib/theme/style/github.css';
import hljs from 'highlight.js'
VMdPreview.use(githubTheme, {
    Hljs: hljs,
});

markdown.vue

<template>
  <splitpanes class="default-theme">
    <Pane style="display: flow; background-color: #FFFFFF">
      <div style="overflow: auto; height: 100%;" class="divsroll" ref="blogRef" id="markdowndiv">
        <v-md-preview ref="previewRef" :text="markdownContent"></v-md-preview>
      </div>
    </Pane>
    <Pane :min-size="10" :size="15" style="display: flow">
      <div ref="directoryRef">
        <div v-for="anchor in titles" :key="anchor"
             style="cursor: pointer; font-size: 12px"
             :style="{ padding: `5px 0 5px ${anchor.indent * 20}px`,color: directoryId === anchor.id ? '#409eff' : 'black' }"
             @click="directoryClick(anchor)" class="directory-item" :id="anchor.id">
          {{ anchor.title }}
        </div>
      </div>
    </Pane>
  </splitpanes>
</template>

<script setup lang="ts">


import {nextTick, onMounted, ref} from "vue";
import axios from "axios";
import {Pane, Splitpanes} from "splitpanes";

const filePath = "/src/assets/help-api.md";

let markdownContent = ref("")
let titles = ref<any>([])

//调用后端接口获取博客数据
const getBlog = async () => {
  try {
    const response = await axios.get(filePath);
    markdownContent.value = response.data;

    await nextTick()

    directoryInit();
  } catch (error) {
    console.error('Error fetching markdown file:', error);
  }
}

const previewRef = ref()

//初始化目录树
const directoryInit = () => {
  const anchors = previewRef.value.$el.querySelectorAll('h1,h2,h3,h4,h5,h6');
  const arr = Array.from(anchors).filter((title) => !!title.innerText.trim());
  if (!arr.length) {
    titles.value = [];
    return;
  }
  const hTags = Array.from(new Set(arr.map((title) => title.tagName))).sort();
  titles.value = arr.map((el) => ({
    id: 'directory-' + el.getAttribute('data-v-md-line'),
    title: el.innerText,
    lineIndex: el.getAttribute('data-v-md-line'),
    indent: hTags.indexOf(el.tagName),
    pixel: el.getBoundingClientRect().top - 60
  }));
}

let directoryId = ref('')

//目录点击事件
const directoryClick = (anchor: any) => {
  const {lineIndex} = anchor;
  const heading = previewRef.value.$el.querySelector(`[data-v-md-line="${lineIndex}"]`);
  if (heading) {
    removeScrollEventListener()
    directoryId.value = anchor.id
    previewRef.value.scrollToTarget({
      target: heading,
      scrollContainer: document.getElementById("markdowndiv"),
      top: 60,
    });
    setTimeout(() => {
      addScrollEventListener()
    }, 200);
  }
}

const blogRef = ref(null)
const directoryRef = ref(null)

//滚动事件监听
const scrollEventListener = () => {
  let pixel = blogRef.value.scrollTop + blogRef.value.offsetTop + 1
  const title = titles.value.reduce((prev, curr) => {
    if (curr.pixel <= pixel && (prev === null || pixel - curr.pixel < pixel - prev.pixel)) {
      return curr;
    }
    return prev;
  }, null);
  if (title) {
    directoryRef.value.scrollTop = (directoryRef.value.scrollHeight * title.pixel) / blogRef.value.scrollHeight
    directoryId.value = title.id
  }
}

//注册滚动事件
const addScrollEventListener = () => {
  blogRef.value.addEventListener('scroll', scrollEventListener);
}

//销毁滚动事件
const removeScrollEventListener = () => {
  blogRef.value.removeEventListener('scroll', scrollEventListener);
}

onMounted(() => {
  getBlog()
  addScrollEventListener()
})

</script>

<style scoped lang="scss">

</style>