Vue.js结合Element-UI实现markdown目录

引入marked

npm install marked -S

父子组件传值以及函数调用

我的父组件是文章详情页,子组件是左侧菜单栏
在这里插入图片描述

父组件代码
<template>
  <el-container>
    <title></title>
    <Markdown :psMsg=navList @callFather="pageJump"></Markdown>
    <el-main>
      <vue-canvas-nest></vue-canvas-nest>
      <el-dropdown>
          <span class="el-dropdown-link">
            {{$t('common.lang')}}<i class="el-icon-arrow-down el-icon--right"></i>
          </span>
        <el-dropdown-menu slot="dropdown">
        <el-dropdown-item @click.native = "switchLang('zh')">{{$t('common.lang-zh')}}</el-dropdown-item>
        <el-dropdown-item @click.native = "switchLang('en')">{{$t('common.lang-en')}}</el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
      <div id="appsingle" v-loading="this.loading" :element-loading-text="$t('common.load-text')">
        <div class="grid-content bg-puprple-light" v-for="(value, key, index) in singleblog">
            <el-row type="flex" class="row-bg" justify="space-around">
              <el-col :span="21">
                <div class="grid-content bg-puprple-light">
                  <h1>{{ value.fields.title }}</h1>
                  <div>
                    <span style="color: #7d7d7d;font-size: small"><i class="el-icon-date"></i> 发表于:{{ value.fields.timestamp | formatDate }}</span>
                    <el-divider direction="vertical"></el-divider>
                    <span style="color: #7d7d7d;font-size: small"><i class="el-icon-user-solid"></i> 作者:{{ value.fields.authorname }}</span>
                    <el-divider direction="vertical"></el-divider>
                    <span style="color: #7d7d7d;font-size: small"><i class="el-icon-document"></i><router-link style="color: #7D7D7D" :to="'/category/'+ value.fields.category"> 分类:{{ value.fields.category }}</router-link></span>
                  </div>
                  <br>
                  <span style="color: #7d7d7d;font-size: small"><i class="el-icon-collection-tag"></i> 标签:</span>
                  <div style="display: inline" v-for="(tag) in tags">
                    <el-tag size="small"><router-link style="color: #7D7D7D" :to="'/tag/'+ tag">{{ tag }}</router-link></el-tag>&nbsp;
                  </div>
                  <br>
                  <div class="bodymarkdown" style="text-align: left;line-height: 2em;font-size: 17px" v-html="markdownhtml"></div>
                </div>
              </el-col>
            </el-row>
            <div class="donate">
              <el-popover
                placement="bottom"
                trigger="click"
                width="210">
                <el-image
                  style="width:210px; height: 300px;text-align: center"
                  :src="wechatUrl"
                  :fit="none"></el-image>
                <el-button icon="el-icon-coin" type="info" slot="reference">{{$t('common.Single.donate')}}</el-button>
              </el-popover>
            </div>
            </div>
            <br>
            <el-row type="flex" class="row-bg" justify="space-around">
              <el-col :span="21">
                <div class="grid-content bg-puprple-light">
                  <p class="author-text"><b>版权声明:</b>本文为博主「请叫我算术嘉」的原创文章,遵循 CC 4.0 BY-SA 版权协议,禁止转载。</p>
                  <p class="author-text"><b>本文链接:</b><router-link style="color: #4D4D4D;" :to="this.$route.path">https://www.blog.guanacossj.com{{ this.$route.path }}</router-link></p>
                </div>
              </el-col>
            </el-row>
            <div class="back">
              <el-popover
                placement="top"
                title="返回上一页"
                width="200"
                trigger="hover"
                content="可返回上次浏览的归档列表。">
                <el-button slot="reference" type="primary" icon="el-icon-caret-left" circle @click="back"></el-button>
              </el-popover>
            </div>
            <div class="prev-next">
              <div class="prev-article">
                <i class="el-icon-caret-left"></i>
              </div>
              <router-link :to="'/post/'+prev_article_id"><div class="prev-article" v-html="prev_article_title.substr(0,25)+'...'"></div></router-link>
              <div class="next-article">
                <i class="el-icon-caret-right"></i>
              </div>
              <router-link :to="'/post/'+next_article_id"><div class="next-article" v-html="next_article_title.substr(0,25)+'...'"></div></router-link>
            </div>
        </div>
      <el-backtop target=".el-main"></el-backtop>
    </el-main>
  </el-container>
</template>

<script>
import moment from 'moment';
import "../assets/tango.css";
import Markdown from "./Markdown";
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 {
        name: "Single",
        components: { Markdown },
        data () {
          return {
            wechatUrl: "https://www.guanacossj.com/media/articlebodypics/wechatpay.png",
            singleId: 1,
            singleblog: [],
            titleName: "",
            markdownhtml: "",
            prev_article_title: "已经是第一篇了",
            next_article_title: "已经是最后一篇了",
            prev_article_id: 0,
            next_article_id: 0,
            loading: true,
            tags: [],
            navList: [],
            activeIndex: 0,
            docsFirstLevels: [],
            docsSecondLevels: [],
            childrenActiveIndex: 0,
            html: ""
          }
        },
        created: function () {
          // this.getId();
        },
        watch: {
          '$route':'showSingleBlog'
        },
        mounted: function () {
          this.showSingleBlog();
        },
        filters: {
	        /*
	         时间格式自定义 只需把字符串里面的改成自己所需的格式
	        */
	        formatDate:function(date) {
	        	return moment(date).format("YYYY-MM-DD HH:mm:ss");
	        }
        },
        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);
          },
        },
        methods: {
          back(){
            this.$router.go(-1);
          },
          skip(url){
           window.open(url, target='_blank')
          },
          activeSon(){
            this.fatherMethod()
          },
          skiplocal(url){
            location.href = url
          },
          switchLang(val){
            this.$i18n.locale=val;//此处val为 zh 或者 en
            sessionStorage.setItem('lang', val);
          },
          getId() {
            this.singleId = this.$route.params.id;
          },
          showSingleBlog () {
            sessionStorage.setItem("detail", true);
            this.$http.get('https://www.guanacossj.com/blog/getsinglearticle/' + this.$route.params.id,{
                _timeout:5000,
                onTimeout :(request) => {
                    this.$message.error(this.$t('common.timeout'));
                    this.loading = false
                  }
                }).then((response) => {
                var res = JSON.parse(response.bodyText);
                if (res.error_num === 0) {
                  this.tags = res['list'][0]['fields']['tags'];
                  this.loading = false;
                  this.markdownhtml = res.markdown;
                  this.html = res['list'][0].fields.body;
                  if (res.prev_article_title !== ""){
                    this.prev_article_id = res.prev_article_id;
                    this.prev_article_title = res.prev_article_title;
                  }else {
                    this.prev_article_title = "已经是第一篇了"
                  }
                  if (res.next_article_title !== ""){
                    this.next_article_id = res.next_article_id;
                    this.next_article_title = res.next_article_title;
                  }else {
                    this.next_article_title = "已经是最后一篇了"
                  }
                  this.singleblog = res['list'];
                  document.title = res['list'][0].fields.title;
                  this.navList = this.handleNavTree();
                  this.getDocsFirstLevels(0);
                } else {
                  this.$message.error('查询博客详情失败');
                }
              })
          },
          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;
          },
          goAnchor(selector) {
            selector = selector.replace(/^\s+|\s+$/g,"");
            const anchor = document.getElementById(selector);//获取元素
            if(anchor) {
                setTimeout(()=>{//页面没有渲染完成时是无法滚动的,因此设置延迟
                    anchor.scrollIntoView(); //js的内置方法,可滚动视图位置至元素位置
                },100);
            }
          },
          pageJump(id) {
            this.titleClickScroll = true;
            //这里我与原作者的不太一样,发现原作者的scrollTop一直为0,所以使用了Vuetify自带的goTo事件
            // this.$vuetify.goTo(this.$el.querySelector(`#${id}`).offsetTop - 40);
            // setTimeout(() => (this.titleClickScroll = false), 100);
            this.goAnchor(id);
          },
          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);
            let index = 0;
            return (nav = nav.map((item) => {
              item.index = index++;
              return item;
            }));
          },
          handleNavTree() {
            const navs = this.getTitle(this.content)
            navs.forEach((item) => {
              item.parent = this.getParentIndex(navs, item.index)
            })
            return this.filterArray(navs)
          },
          filterArray(data, parent) {
            const self = this
            var tree = []
            var temp
            for (var i = 0; i < data.length; i++) {
              if (data[i].parent === parent) {
                var obj = data[i]
                temp = self.filterArray(data, data[i].index)
                if (temp.length > 0) {
                  obj.children = temp
                }
                tree.push(obj)
              }
            }
            return tree
          },
          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;
          },
        }
    }
</script>

<style scoped>
  .el-main{
    margin-bottom: 20px;
  }
  .el-menu{
    box-shadow: 0 4px 4px rgba(0, 0, 0, .30), 0 0 6px rgba(0, 0, 0, .04)
  }
  .el-row {
    margin-bottom: 20px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .el-col {
    border-radius: 4px;
  }
  .bg-purple-dark {
    background: #99a9bf;
  }
  .bg-purple {
    background: #d3dce6;
  }
  .bg-purple-light {
    background: #e5e9f2;
  }
  .grid-content {
    border-radius: 4px;
    min-height: 36px;
  }
  .row-bg {
    padding: 10px 0;
    /*background-color: #f9fafc;*/
    background-color: rgba(255, 255, 255, 0);
    box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)
  }
  #appsingle {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    /*color: #2c3e50;*/
    color: #4d4d4d;
    margin-top: 30px;
  }
  .prev-next{
    display: inline;
  }
  .el-dropdown {
    float: right;
  }
  .el-dropdown-link {
    cursor: pointer;
  }
  .next-article {
    color: #4D4D4D;
    float: right;
    display: inline;
    font-size: 15px;
    font-weight: bold;
  }
  .prev-article {
    color: #4D4D4D;
    float: left;
    display: inline;
    font-size: 15px;
    font-weight: bold;
  }
  .back {
    margin-bottom: 20px;
  }
  .author-text {
    text-align: left;
  }
  a {
    text-decoration: none;
  }
  .router-link-active {
    text-decoration: none;
  }
</style>

当把文章主题内容传进来后,需要把navList变量传给子组件,也就是左侧的菜单栏,然后,点击对应的目录,父组件需要跳转到对应位置

子组件代码
<template>
  <el-aside width="230px" style="margin-left: 14%;">
      <el-menu
        :default-active="$route.path"
        class="el-menu-vertical-demo"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
        style="height: 440px">
        <div class="blogtitlebox">
          <div class="blogtitle">{{$t('common.blog-name')}}</div>
        </div>
        <br>
      <el-menu-item index="/home" @click="skiplocal('/home')">
        <template slot="title">
          <i class="el-icon-location"></i>
          <span style="font-weight: bold">{{$t('common.home')}}</span>
        </template>
      </el-menu-item>
      <el-menu-item index="/archive" @click="skiplocal('/archive')">
        <template slot="title">
        <i class="el-icon-document"></i>
        <span style="font-weight: bold">{{$t('common.archive')}}</span>
        </template>
      </el-menu-item>
      <el-menu-item index="/category" @click="skiplocal('/category')">
        <i class="el-icon-menu"></i>
        <span slot="title" style="font-weight: bold">{{$t('common.category')}}</span>
      </el-menu-item>
      <el-menu-item index="/list" @click="skiplocal('/list')">
        <i class="el-icon-search"></i>
        <span slot="title" style="font-weight: bold">{{$t('common.search')}}</span>
      </el-menu-item>
      <el-menu-item index="/about" @click="skiplocal('/about')">
        <i class="el-icon-user"></i>
        <span slot="title" style="font-weight: bold">{{$t('common.about')}}</span>
      </el-menu-item>
        <el-menu-item index="/love" @click="skiplocal('/love')">
        <i class="el-icon-ice-cream"></i>
        <span slot="title" style="font-weight: bold">{{$t('common.love')}}</span>
      </el-menu-item>
    </el-menu>
      <p></p>
      <el-menu
      class="el-menu-vertical-demo"
      background-color="#545c64"
      text-color="#fff">
          <p class="mulu">{{$t('common.index')}}</p>
          <div class="mulu_detail">
            <ul>
            <div style="color: #fff" v-for="(nav, index) in psMsg" :key="index" :class="{ 'active': activeIndex === index }" @click="currentClick(index)"> <a href="javascript:" @click="pageJump(nav.title)">{{ nav.title }}</a>
             <div v-if="nav.children.length &gt; 0" class="menu-children-list">
              <ul class="nav-list">
               <p v-for="(item, idx) in nav.children" :key="idx" :class="{ on: childrenActiveIndex === idx }" @click.stop="childrenCurrentClick(idx)"> <a href="javascript:;" @click="pageJump(item.title)">{{ item.title }}</a> </p>
              </ul>
             </div>
            </div>
           </ul>
          </div>
    </el-menu>
    </el-aside>
</template>

<script>
export default {
name: "Markdown",
  data() {
    return {
      circleUrl: "https://www.guanacossj.com/media/jia/IMG_0323.JPG",
      activeIndex: 0
    }
  },
  props: {
    psMsg: Array,
  },
  mounted() {
  },
  methods: {
    skip(url){
      window.open(url, target='_blank')
    },
    skiplocal(url){
      sessionStorage.removeItem("detail");
      location.href = url
    },
    currentClick(index) {
      this.activeIndex = index;
      this.getDocsSecondLevels(index);
    },
    childrenCurrentClick(index) {
      this.childrenActiveIndex = index;
    },
    pageJump(id) {
      this.titleClickScroll = true;
      //传给父组件
      this.$emit('callFather', id);
    },
    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;
      }
    },
  }
}
</script>

<style scoped>
  .el-menu{
    box-shadow: 0 4px 4px rgba(0, 0, 0, .30), 0 0 6px rgba(0, 0, 0, .04)
  }
  .el-menu-item:hover {
    color: #ffd04b !important;
  }
  a{
    text-decoration: none;
    color: white;
  }
  .el-footer {
    color: #333;
    text-align: center;
    line-height: 20px;
  }
  .blogtitlebox {
    text-align: center;
    font-size: 21px;
    font-weight: bold;
    color: white;
    height: 80px;
    background-color: #222222;
    /*align-items: center;*/
    /*top:50%;*/
    /*position: absolute;*/
    line-height: 75px;
  }
  .blogtitle {
    display: inline-block;
    vertical-align: middle;
  }
  .mulu {
    text-align: center;
    font-weight: bold;
    color: #ffd04b;
    font-size: 18px;
    padding-top: 15px;
  }
  .mulu_detail {
    width: 205px;
    padding-top: 10px;
    padding-bottom: 25px;
    font-size: 14px;
    font-weight: bold;
    line-height:25pt;
    color: white !important;
  }
  .tag-links{
    height: 45px;
    text-align: center;
    font-size: 14px;
    line-height: 45px;
    width: 100%;
    color: #fff !important;
    /*margin: 0 auto;*/
  }
  .el-link-github {
    color: #fff !important;
    font-size: 14px;
  }
  .el-link-github:hover {
    color: #ffd04b !important;
  }
  .el-link-email {
    font-size: 14px;
    color: #fff !important;
  }
  .el-link-email:hover {
    color: #ffd04b !important;
  }
  .el-menu-item.is-active {
    background: rgb(67, 74, 80) !important;
  }
  .el-submenu__title.is-active {
    background: #6db6ff !important;
  }
  .el-dropdown {
    float: right;
  }
  a {
    text-decoration: none;
  }
  @media screen and (min-width: 230px) {
  .link {
    padding-top: 100px;
    position: fixed;
    right: 25px;
    top: 100px;
  }
  .link_cover {
    max-height: 400px;
    overflow: scroll;
    overflow-x: hidden;
    overflow-y: visible;
  }
}
@media screen and (min-width: 230px) {
  .link {
    padding-top: 100px;
    position: fixed;
    right: 50px;
    top: 100px;
  }

  .link_cover {
    max-height: 400px;
    overflow: scroll;
    overflow-x: hidden;
    overflow-y: visible;
  }

  li {
    list-style-type: none;
  }

  .active a {
    color: #ffd04b;
    font-size: 16px;
    font-weight: bold;
  }
}
</style>

然后子组件把对应的id名称传回父组件,并调用函数pageJump,实现跳转,子组件同时实现高亮。
其中,高亮部分用到了动态class,当前索引是动态调用active这个样式

<div style="color: #fff" v-for="(nav, index) in psMsg" :key="index" :class="{ 'active': activeIndex === index }" @click="currentClick(index)"> <a href="javascript:" @click="pageJump(nav.title)">{{ nav.title }}</a> 
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值