直接上代码
引用组件
"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>