先看效果
组件结构
第一步 创建工具类
工具类名称随意,我的叫
toc.js
,如果你的改了引用的时候注意改下
let targetContent = ''
// 获取所有的标题元素
function getElements() {
const parser = new DOMParser();
const htmlDocument = parser.parseFromString(targetContent, 'text/html');
const elements = htmlDocument.querySelectorAll("h1,h2,h3,h4,h5,h6")
return elements;
}
// 获取父级的标题
function getMinSize() {
const elements = [...getElements()]
let arr = []
elements.forEach(item => {
arr.push(parseInt(item.nodeName.replace('H', '')))
})
return Math.min(...arr)
}
//获取所有的父级节点
function getFatherNode() {
let minSize = getMinSize()
const elements = Array.from(getElements());
const arr = []
elements.forEach((item, index) => {
let level = parseInt(item.nodeName.replace('H', ''))
if (level == minSize) {
let obj = { 'title': item.innerText, 'level': level, 'eleIndex': index }
arr.push(obj)
}
})
return arr
}
// 返回生成的toc数据结构
export function randerMarkDownToc(data) {
targetContent = data
const elements = getElements()
let fatherElements = getFatherNode()
let tocArr = []
fatherElements.forEach(item => {
let obj = { ...item, 'children': getChildrenArr(elements, item.eleIndex, item.level) }
tocArr.push(obj)
})
return tocArr
}
// 获取子级toc
function getChildrenArr(targetArr, startIndex, targetLevel) {
const childrenArr = []
for (let i = startIndex + 1; i < targetArr.length; i++) {
let level = parseInt(targetArr[i].nodeName.replace('H', ''))
if (targetLevel + 1 === level && (childrenArr.length === 0 || i - childrenArr[childrenArr.length - 1].eleIndex == 1)) {
if (i - startIndex > 1 && childrenArr.length == 0) {
return childrenArr
}
// 这里用的递归 俗称套娃
let obj = { 'title': targetArr[i].innerText, 'level': level, 'eleIndex': i, 'children': getChildrenArr(targetArr, i, level) }
childrenArr.push(obj)
}
}
return childrenArr
}
第二步 创建组件
创建父级组件 组件名称:MarkdownContainer.vue
<template>
<div id="markdown-toc" :class="!isFix ? 'markdown-toc-move' : ''">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>目录</span>
</div>
<ul>
<li v-for="item in tocArr" :key="item.title">
<a @click.prevent="toLink(item.title)">{{ item.title }}</a>
<MarkdownTocItem :tocArr="item.children" />
</li>
</ul>
</el-card>
</div>
</template>
<script>
import MarkdownTocItem from "./MarkdownTocItem.vue";
export default {
name: "MarkdownContainer",
components: { MarkdownTocItem },
props: ["tocArr"],
data() {
return {
isFix: true,
};
},
methods: {
toLink(remark) {
console.log("father");
const element = document.getElementById(remark);
const ofsHeight = element.offsetTop;
window.scrollTo({
top: ofsHeight - 110,
behavior: "smooth",
});
},
listenMove() {
let backImg = document.getElementById("back-image");
const rect = backImg.getBoundingClientRect();
const windowHeight = window.innerHeight;
if (windowHeight - rect.bottom <= 755) {
this.isFix = true;
} else {
this.isFix = false;
}
},
},
mounted() {
window.addEventListener("scroll", this.listenMove);
},
beforeDestroy() {
window.removeEventListener("scroll", this.listenMove);
},
};
</script>
<style lang="scss" scoped>
.markdown-toc-move {
top: 29% !important;
}
#markdown-toc {
position: fixed;
top: 430px;
right: 16px;
width: 233px;
height: 300px;
background-color: #f0f2f5;
margin: 0px !important;
overflow-y: scroll;
&::-webkit-scrollbar {
display: none; /* 隐藏滚动条(适用于 Chrome、Safari 等 WebKit 类浏览器)*/
}
::v-deep .el-card{
height: 100% !important;
}
::v-deep .el-card__header {
padding: 10px 0;
position: relative;
top: 0;
right: 0;
}
::v-deep .elel-card__body {
padding: 5px !important;
height: 300px;
}
.clearfix {
text-align: center;
span {
width: 100%;
font-size: 1.1rem;
font-weight: bold;
}
}
ul {
li {
list-style: none;
cursor: pointer;
a {
color: green;
text-decoration: none;
display: block;
width: 100%;
height: 100%;
&:hover {
color: orange;
}
}
}
}
}
@media screen and(max-width: 480px) {
#markdown-toc {
display: none;
}
}
</style>
创建子组件,用于递归toc, 组件名称:MarkdownTocItem.vue
<template>
<div class="markdown-toc-item">
<ul>
<li v-for="item in tocArr" :key="item.title">
<a @click.prevent="toLink(item.title)">{{ item.title }}</a>
<MarkdownTocItem :tocArr="item.children" />
</li>
</ul>
</div>
</template>
<script>
import MarkdownTocItem from "./MarkdownTocItem.vue";
export default {
name: "MarkdownTocItem",
components: { MarkdownTocItem },
props: ["tocArr"],
methods: {
toLink(remark) {
const element = document.getElementById(remark);
const ofsHeight = element.offsetTop;
window.scrollTo({
top: ofsHeight - 110,
behavior: "smooth",
});
},
},
};
</script>
<style lang="scss" scoped>
ul {
padding-left: 8px;
li {
list-style: none;
cursor: pointer;
a {
color: green;
text-decoration: none;
display: block;
width: 100%;
height: 100%;
&:hover {
color:orange
}
}
}
}
</style>
第三步 引用并使用组件 注意这里 要自己看着改 我只是提供了一些关键代码
import { randerMarkDownToc } from '@/utils/toc'
// 记得替换路径
import MarkdownContainer from '@/components/MarkDownTop/MarkdownContainer'
// 使用组件
<MarkdownContainer :tocArr="tocData" />
export default ({
// 注册组件
components: { MarkdownContainer },
data(){
return {
tocData:null
}
},
methods: {
getMarkDownContent(){
getArticle().then(resp => {
// 获取toc数据
this.tocData = randerMarkDownToc(resp.data.article.content)
})
}
}
})