【Vue3】 - 解析Markdown内容生成侧边栏Toc目录索引
解析Markdown内容生成侧边栏Toc目录索引
一般来说,后端返回的数据可以直接使用 v-html 进行渲染,但通常不会包含对应的目录结构。因此,需要前端自行解析内容中的 h1 到 h6 标题,生成侧边栏目录索引,并为每个标题创建锚点,以便用户点击目录项时能够快速跳转到对应章节。
使用场景:技术文档、长篇博客或在线教程 需要 目录的地方
效果如下:
直接附上 代码:
<template><div class="markdown-with-toc"><!-- 主内容区域 --><div class="content-main"><div class="markdown-viewer"><divclass="markdown-content markdown-body"v-html="markdownContent"ref="contentRef"id="content"></div></div></div><!-- 目录侧边栏 --><div class="content-sidebar"><div class="table-of-contents"><div class="toc-header"><h3>目录</h3></div><div class="toc-content"><ul class="toc-list" v-if="headings.length > 0"><liv-for="heading in headings":key="heading.id":class="['toc-item',`toc-level-${heading.level}`,{ active: activeHeading === heading.id },]"@click="scrollToHeading(heading.id)"><a :href="`#${heading.id}`" @click.prevent>{{ heading.text }}</a></li></ul><div v-else class="no-headings">暂无目录</div></div></div></div></div>
</template><script setup>
import { ref, nextTick, watch, onMounted, onUnmounted } from "vue";
import hljs from "highlight.js";
import "highlight.js/styles/github.css";
import "github-markdown-css/github-markdown.css";const props = defineProps({markdownContent: {type: String,default: "",},
});const contentRef = ref(null);
const activeHeading = ref("heading-1");
const headings = ref([]);// 从 DOM 中提取标题数据
const extractHeadingsFromDOM = () => {nextTick(() => {const contentElement = document.querySelector("#content");if (!contentElement) return;const headingElements = contentElement.querySelectorAll("h1, h2, h3, h4, h5, h6");const newHeadings = [];headingElements.forEach((heading, index) => {const id = `heading-${index + 1}`;heading.setAttribute("id", id); // 同时设置IDnewHeadings.push({id,level: parseInt(heading.tagName.substring(1)), // H1 -> 1, H2 -> 2text: heading.textContent.trim(),});});headings.value = newHeadings;console.log("从 DOM 提取的标题:", newHeadings);// 高亮代码块highlightCodeBlocks();});
};// 高亮代码块
const highlightCodeBlocks = () => {nextTick(() => {const codeBlocks = document.querySelectorAll("pre code");codeBlocks.forEach((block) => {// 添加这行配置来忽略安全警告hljs.configure({ ignoreUnescapedHTML: true });hljs.highlightElement(block);});});
};// 滚动到指定标题
const scrollToHeading = (headingId) => {const element = document.getElementById(headingId);if (element) {const markdownViewer = document.querySelector(".markdown-viewer");if (markdownViewer) {const elementTop = element.offsetTop;const scrollOffset = elementTop - 80; // 留出一些顶部空间// 立即设置为活跃状态activeHeading.value = headingId;console.log("滚动到标题:", headingId);markdownViewer.scrollTo({top: scrollOffset,behavior: "smooth",});}}
};// 监听内容变化,重新提取标题
watch(() => props.markdownContent,() => {extractHeadingsFromDOM();},{ immediate: true }
);</script><style scoped>
/* 基础布局 */
.markdown-with-toc {height: 100%;display: flex;overflow: hidden;
}.content-main {flex: 1;overflow: hidden;
}.content-sidebar {width: 300px;flex-shrink: 0;
}.markdown-viewer {height: 100%;overflow-y: auto;padding: 20px;background: #fff;
}.markdown-content {max-width: none;box-sizing: border-box;
}/* 覆盖 github-markdown-css 的一些默认设置 */
.markdown-content.markdown-body {padding: 0;background-color: transparent;
}/* 目录样式 */
.table-of-contents {height: 100%;background: #f8f9fa;border-left: 1px solid #e1e4e8;display: flex;flex-direction: column;
}.toc-header {padding: 16px 20px;border-bottom: 1px solid #e1e4e8;background: #fff;
}.toc-header h3 {margin: 0;font-size: 16px;font-weight: 600;color: #24292e;
}.toc-content {flex: 1;overflow-y: auto;padding: 16px 0;
}.toc-list {list-style: none;margin: 0;padding: 0;
}.toc-item {margin: 0;padding: 0;
}.toc-item a {display: block;padding: 4px 20px;color: #586069;text-decoration: none;font-size: 14px;line-height: 1.5;border-left: 3px solid transparent;transition: all 0.2s ease;cursor: pointer;
}.toc-item a:hover {color: #0366d6;background-color: #f1f8ff;
}.toc-item.active a {color: #0366d6;background-color: #f1f8ff;border-left-color: #0366d6;font-weight: 600;
}/* 不同级别的标题缩进 */
.toc-level-1 a {padding-left: 20px;font-weight: 600;
}.toc-level-2 a {padding-left: 32px;
}.toc-level-3 a {padding-left: 44px;
}.toc-level-4 a {padding-left: 56px;
}.toc-level-5 a {padding-left: 68px;
}.toc-level-6 a {padding-left: 80px;
}.no-headings {padding: 20px;text-align: center;color: #586069;font-size: 14px;
}/* 滚动条样式 */
.toc-content::-webkit-scrollbar,
.markdown-viewer::-webkit-scrollbar {width: 6px;
}.toc-content::-webkit-scrollbar-track,
.markdown-viewer::-webkit-scrollbar-track {background: #f1f1f1;
}.toc-content::-webkit-scrollbar-thumb,
.markdown-viewer::-webkit-scrollbar-thumb {background: #c1c1c1;border-radius: 3px;
}.toc-content::-webkit-scrollbar-thumb:hover,
.markdown-viewer::-webkit-scrollbar-thumb:hover {background: #a8a8a8;
}/* 响应式设计 */
@media (max-width: 768px) {.markdown-with-toc {flex-direction: column;}.content-sidebar {width: 100%;height: 200px;order: -1;}.content-main {flex: 1;}
}
</style>
整个流程可以分为两大步:
- 解析标题,生成目录树(TOC) 和
- 实现目录与内容的交互(锚点跳转)。
演示Demo可以去 gitee 查看:https://gitee.com/RanGuMo/markdown-toc.git