Vue生成markdown目录索引

开头

一直以来我的博客的文章内容页都缺少一个目录索引,由于我的文章使用的文本结构为markdown,而现有的插件无法满足我的需求,所以我只能通过过滤文章提取标题动态生成目录结构,同时通过获取各个标题的据顶部高度来实现点击跳转的功能。本教程的代码实现参考了vue使用marked.js实现markdown转html并提取标题生成目录这篇文章,同时由于我使用Vue结合Vuetify的UI库,部分实现略有不同。

预期结果

image-20200518102039632

这个是我的最终实现效果

代码实现

html部分
<template><div class="mx-5"><v-container grid-list-xl><v-row><v-col cols="12" md="3" class="link"><v-card class="mx-auto mt-2 link_cover"><div class="py-4 links"><h3 class="pl-3 pb-3">目录h3><ul><liv-for="(nav, index) in navList":key="index":class="{ on: activeIndex === index }"@click="currentClick(index)"><a href="javascript:;" @click="pageJump(nav.index)">{{nav.title}}a><divv-if="nav.children.length > 0"class="menu-children-list"><ul class="nav-list"><liv-for="(item, idx) in nav.children":key="idx":class="{ on: childrenActiveIndex === idx }"@click.stop="childrenCurrentClick(idx)"><a href="javascript:;" @click="pageJump(item.index)">{{item.title}}a>li>ul>div>li>ul>div>v-card>v-col><v-col cols="12" md="9"><div class="body"><divclass="content markdown-body"ref="helpDocs"v-html="compiledMarkdown">div>div>v-col>v-row>v-container>div>
template>

html部分我使用了Vuetify的UI库的row组件,将目录与文章内容分割开来。

JS部分
<script>
import marked from "marked";let rendererMD = new marked.Renderer();
marked.setOptions({renderer: rendererMD,gfm: true,tables: true,breaks: false,pedantic: false,sanitize: false,smartLists: true,smartypants: false,
});
export default {props: ["id"],data() {return {article: [],html: "",//文章内容navList: [],activeIndex: 0,docsFirstLevels: [],docsSecondLevels: [],childrenActiveIndex: 0,};},mounted() {this.getArticleDetail();},methods: {async getArticleDetail() {try {if (this.id) {const res = await this.$http.get(`/article?id=${this.id}`);this.article = res.data;this.html = this.article.html;global.console.log(this.article);document.getElementsByTagName("title")[0].innerText = this.article.title;}} catch (e) {global.console.log("文章获取异常");}//文章内容获取后渲染目录,避免目录无法及时获取内容this.navList = this.handleNavTree();this.getDocsFirstLevels(0);},childrenCurrentClick(index) {this.childrenActiveIndex = index;},getDocsFirstLevels(times) {// 解决图片加载会影响高度问题setTimeout(() => {let firstLevels = [];Array.from(document.querySelectorAll("h3"), (element) => {firstLevels.push(element.offsetTop - 60);});this.docsFirstLevels = firstLevels;if (times < 8) {this.getDocsFirstLevels(times + 1);}}, 500);},getDocsSecondLevels(parentActiveIndex) {let idx = parentActiveIndex;let secondLevels = [];let navChildren = this.navList[idx].children;if (navChildren.length > 0) {secondLevels = navChildren.map((item) => {return this.$el.querySelector(`#data-${item.index}`).offsetTop - 60;});this.docsSecondLevels = secondLevels;}},getLevelActiveIndex(scrollTop, docsLevels) {let currentIdx = null;let nowActive = docsLevels.some((currentValue, index) => {if (currentValue >= scrollTop) {currentIdx = index;return true;}});currentIdx = currentIdx - 1;if (nowActive && currentIdx === -1) {currentIdx = 0;} else if (!nowActive && currentIdx === -1) {currentIdx = docsLevels.length - 1;}return currentIdx;},pageJump(id) {this.titleClickScroll = true;//这里我与原作者的不太一样,发现原作者的scrollTop一直为0,所以使用了Vuetify自带的goTo事件this.$vuetify.goTo(this.$el.querySelector(`#data-${id}`).offsetTop - 40);setTimeout(() => (this.titleClickScroll = false), 100);},currentClick(index) {this.activeIndex = index;this.getDocsSecondLevels(index);},getTitle(content) {let nav = [];let tempArr = [];content.replace(/(#+)[^#][^\n]*?(?:\n)/g, function(match, m1) {let title = match.replace("\n", "");let level = m1.length;tempArr.push({title: title.replace(/^#+/, "").replace(/\([^)]*?\)/, ""),level: level,children: [],});});// 只处理二级到四级标题,以及添加与id对应的index值,这里还是有点bug,因为某些code里面的注释可能有多个井号nav = tempArr.filter((item) => item.level >= 2 && item.level <= 4);global.console.log(nav);let index = 0;return (nav = nav.map((item) => {item.index = index++;return item;}));},// 将一级二级标题数据处理成树结构handleNavTree() {let navs = this.getTitle(this.content);let navLevel = [3, 4];let retNavs = [];let toAppendNavList;navLevel.forEach((level) => {// 遍历一级二级标题,将同一级的标题组成新数组toAppendNavList = this.find(navs, {level: level,});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(retNavs, 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.html;},//此函数将markdown内容进一步的转换compiledMarkdown: function() {let index = 0;rendererMD.heading = function(text, level) {//我比较习惯三级和四级目录,这里看你喜欢if (level <= 4) {return `${level} id="data-${index++}">${text}${level}>`;} else {return `${level}>${text}${level}>`;}};return marked(this.content);},},
};
</script>

js部分需要安装marked这个库npm i -s marked,我参考了原作者的实现,大致就是利用marked先将markdown内容转化为标题带有id为data-${index++}的内容,后续提供数组的操作形成一个目录结构,具体实现可以自己研究一下。

Css部分

css部分我将默认的ul和li做了美化,同时使用了媒体查询,在大尺寸设备上我希望能够将目录固定,便于文章的浏览,同时希望目录的最大高度不要太高,以免目录太复杂导致超出文章高度无法查看。最终实现效果如下:

image-20200518103325059


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部