核心功能模块
1. 树形目录生成
通过解析 HTML 文档中的标题标签 或者 富文本框收集的数据(如 <h1>
到 <h5>
),生成符合 el-tree
数据格式的树形目录。
2. el-tree
渲染
使用 Element Plus 的 el-tree
组件展示树形目录,并通过节点点击事件触发滚动定位。
3. 滚动定位
通过 scrollToContent
方法,根据节点信息定位到对应的文档内容。
4. HTML 实体清理
利用 cleanHtmlEntities
函数清理标题中的 HTML 实体字符和多余空格,确保目录文本的可读性。
整合后的完整代码
包含树形目录生成、el-tree
配置、滚动定位逻辑以及 HTML 实体清理函数:
<template><div class="container"><!-- 左侧固定目录 --><div class="tree-container"><el-treeclass="custom-tree"default-expand-all:data="newToc"node-key="key":props="defaultProps"@node-click="scrollToContent"><template #default="{ node }"><span class="tree-node-label">{{ node.label }}</span></template></el-tree></div><!-- 右侧内容区域 --><div class="content-container"><div v-for="(item, index) in list" :key="item.id" :ref="setItemRef(item.id)" class="content-section"><div v-html="item.content"></div></div></div></div>
</template><script setup>
import { ref, onBeforeUpdate } from 'vue';// 定义 Props
const props = defineProps({list: {type: Array,required: true,default: () => [],},
});// 响应式变量
const newToc = ref([]); // 树形目录数据
const defaultProps = {children: 'children', // 子节点字段名label: 'label', // 显示文本字段名
};// 存储每个富文本框的 DOM 引用
const itemRefs = new Map();// 设置 DOM 引用
function setItemRef(id) {return (el) => {if (el) {itemRefs.set(id, el);}};
}/*** 滚动到对应内容*/
function scrollToContent(node) {const container = itemRefs.get(node.parentId);if (container) {const headings = container.querySelectorAll('h1, h2, h3, h4, h5');const target = headings[node.headingIndex];if (target) {const offsetTop = target.getBoundingClientRect().top + window.scrollY;const marginTopOffset = 70; // 考虑顶部导航栏高度const adjustedScrollPosition = offsetTop - marginTopOffset;window.scrollTo({top: adjustedScrollPosition,behavior: 'smooth',});}}
}/*** 清理 HTML 实体字符和多余空格*/
function cleanHtmlEntities(htmlString) {const tempDiv = document.createElement('div');tempDiv.innerHTML = htmlString;let text = tempDiv.textContent || tempDiv.innerText || '';return text.trim().replace(/\s+/g, ' ');
}/*** 生成树形目录*/
function generateToc() {newToc.value = [];const stack = [];props.list.forEach((item) => {const regex = /<h([1-5])[^>]*>(.*?)<\/h\1>/gi;let match;let headingIndex = 0;while ((match = regex.exec(item.content)) !== null) {const level = parseInt(match[1]); // 提取标题级别const title = cleanHtmlEntities(match[2]); // 提取标题文本并清理 HTML 实体const key = `${item.id}-${headingIndex}`; // 唯一键const currentNode = {parentId: item.id, // 所属富文本框的 IDheadingIndex, // 在该富文本框中第几个标题level, // 标题级别label: title, // 节点显示文本key, // 唯一键children: [], // 子节点};// 构建树形结构if (stack.length === 0 || level > stack[stack.length - 1].level) {if (stack.length > 0) {stack[stack.length - 1].children.push(currentNode);} else {newToc.value.push(currentNode);}stack.push(currentNode);} else {// 回溯到合适的父节点while (stack.length > 0 && level <= stack[stack.length - 1].level) {stack.pop();}if (stack.length > 0) {stack[stack.length - 1].children.push(currentNode);} else {newToc.value.push(currentNode);}stack.push(currentNode);}headingIndex++;}});
}// 在更新前清空 DOM 引用
onBeforeUpdate(() => {itemRefs.clear();
});// 初始化生成目录
generateToc();
</script><style scoped>
.container {display: flex;height: 100vh;
}.tree-container {width: 300px;height: 100%;overflow-y: auto;border-right: 1px solid #ddd;
}.custom-tree {background-color: transparent;
}.tree-node-label {font-size: 14px;color: #333;cursor: pointer;
}.content-container {flex: 1;padding: 20px;overflow-y: auto;
}.content-section {margin-bottom: 40px;
}
</style>
关键点解析
1. DOM 引用管理
- 使用
itemRefs
存储每个富文本框的 DOM 引用。 - 在
v-for
循环中,通过:ref="setItemRef(item.id)"
动态绑定 DOM 元素。
2. 滚动定位逻辑
scrollToContent
方法通过node.parentId
找到对应的富文本框容器。- 使用
querySelectorAll
获取所有标题元素,并根据node.headingIndex
定位到目标标题。 - 考虑顶部导航栏高度(
marginTopOffset
),调整滚动位置以确保目标内容完全可见。
3. HTML 实体清理
cleanHtmlEntities
函数通过创建临时div
元素提取纯文本内容。- 使用正则表达式移除多余的空格,确保标题文本整洁。
4. 动态更新
- 在组件更新前(
onBeforeUpdate
),清空itemRefs
,避免旧引用导致的内存泄漏。
应用场景与案例
场景 1:技术文档导航
假设我们有一个技术文档,包含多个章节和子章节:
<h1 id="section1">第一章:基础知识</h1>
<p>这里是基础知识部分。</p>
<h2 id="section1-1">1.1 定义</h2>
<p>定义内容。</p>
<h1 id="section2">第二章:进阶知识</h1>
<h2 id="section2-1">2.1 进阶概念</h2>
<p>进阶概念内容。</p>
生成的树形目录如下:
- 第一章:基础知识- 1.1 定义
- 第二章:进阶知识- 2.1 进阶概念
点击目录中的节点时,页面会平滑滚动到对应的内容部分。
场景 2:项目报告
在一个项目报告中,可能包含修订责任人和多个章节:
list = [{id: "1",type: "personnel",Names: "修订责任人",personNames: "张三"},{id: "2",type: "text",content: "<h1 id='overview'>项目概述</h1><p>这里是项目概述。</p><h2 id='background'>1.1 项目背景</h2><p>项目背景内容。</p>"}
];
生成的目录如下:
- 修订责任人:张三
- 项目概述- 1.1 项目背景
案例数据
const list = [{id: "1",type: "personnel",Names: "修订责任人",personNames: "李四"},{id: "2",type: "text",content: "<h1 id='chapter1'>第一章:基础知识</h1><p>这里是基础知识部分。</p><h2 id='definition'>1.1 定义</h2><p>定义内容。</p>"},{id: "3",type: "text",content: "<h1 id='chapter2'>第二章:进阶知识</h1><h2 id='concept'>2.1 进阶概念</h2><p>进阶概念内容。</p><h3 id='details'>2.1.1 细节说明</h3><p>细节说明内容。</p>"}
];
运行结果:
- 修订责任人:李四
- 第一章:基础知识- 1.1 定义
- 第二章:进阶知识- 2.1 进阶概念- 2.1.1 细节说明