Vue生成markdown目录索引
# 开头
一直以来我的博客的文章内容页都缺少一个目录索引,由于我的文章使用的文本结构为markdown,而现有的插件无法满足我的需求,所以我只能通过过滤文章提取标题动态生成目录结构,同时通过获取各个标题的据顶部高度来实现点击跳转的功能。本教程的代码实现参考了vue使用marked.js实现markdown转html并提取标题生成目录 (opens new window)这篇文章,同时由于我使用Vue结合Vuetify的UI库,部分实现略有不同。
# 预期结果
这个是我的最终实现效果
# 代码实现
# 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
css部分我将默认的ul和li做了美化,同时使用了媒体查询,在大尺寸设备上我希望能够将目录固定,便于文章的浏览,同时希望目录的最大高度不要太高,以免目录太复杂导致超出文章高度无法查看。最终实现效果如下:
编辑 (opens new window)
上次更新: 2022/08/29, 16:40:19