当前位置: 首页 > article >正文

自动生成树形目录结构:与 `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 细节说明


http://www.kler.cn/a/594588.html

相关文章:

  • WordPress系统获取webshell的攻略
  • 请为下面的html添加一个修改按钮,以便对书名、价格进行修改
  • 【FastGPT】利用知识库创建AI智能助手
  • C++动态库中的静态调用和动态调用,延迟加载
  • 再学:ERC721扩展、ERC1155、SBT,OpenSeaNFT市场 NFT Market 习题讲解
  • Multisim学习-01 特点安装使用和第一个仿真实例
  • 智慧人大系统(源码+文档+讲解+演示)
  • [极客大挑战 2019]Upload_3.19BUUCTF练习day3(2)
  • 《信息系统安全》(第一次上机实验报告)
  • 虚拟机如何扩容磁盘
  • PHP实现用户登录与注册功能
  • ruoyi 小程序使用笔记
  • 第四周日志-用网络请求理解bp(2)
  • React如何导入md5,把密码password进行md5加密
  • LeetCode hot 100—只出现一次的数字
  • 目标检测YOLO实战应用案例100讲-面向交通复杂目标场景的机器视觉检测技术研究(续)
  • 初识Brainstorm(matlab)
  • 2025年汽车加气站操作工考试精选题库
  • 数据库的两种模式
  • L1-005-008