自动生成树形目录结构:与 `el-tree` 和滚动定位结合的完整解决方案
核心功能模块
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-tree
class="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, // 所属富文本框的 ID
headingIndex, // 在该富文本框中第几个标题
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 细节说明