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>
                <li
                  v-for="(nav, index) in navList"
                  :key="index"
                  :class="{ on: activeIndex === index }"
                  @click="currentClick(index)"
                >
                  <a href="javascript:;" @click="pageJump(nav.index)">{{
                    nav.title
                  }}</a>
                  <div
                    v-if="nav.children.length > 0"
                    class="menu-children-list"
                  >
                    <ul class="nav-list">
                      <li
                        v-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">
            <div
              class="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 `<h${level} id="data-${index++}">${text}</h${level}>`;
        } else {
          return `<h${level}>${text}</h${level}>`;
        }
      };

      return marked(this.content);
    },
  },
};
</script>

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

Css部分
<style scoped>
.content {
  padding: 8px 8px;
  font-size: 14px;
}
.body {
  margin-top: 24px;
  background: #f0f0f0;
  border-radius: 5px;
}
ul {
  list-style-type: none;
  padding: 2px 6px;
}
li {
  list-style-type: none;
  margin: 2px 6px;
}
a {
  color: #42b983;
  text-decoration: none;
}
@media screen and (min-width: 960px) {
  .link {
    padding-top: 100px;
    position: fixed;
    right: 25px;
    top: 100;
  }
  .link_cover {
    max-height: 400px;
    overflow: scroll;
    overflow-x: hidden;
    overflow-y: visible;
  }
}
@media screen and (min-width: 1060px) {
  .link {
    padding-top: 100px;
    position: fixed;
    right: 50px;
    top: 100;
  }
  .link_cover {
    max-height: 400px;
    overflow: scroll;
    overflow-x: hidden;
    overflow-y: visible;
  }
}
</style>

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

image-20200518103325059

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值