简单树形菜单
引言
在网页开发中,树形菜单是一种非常实用的,它可以清晰地展示具有层级关系的数据,并且能够方便用户进行导航和操作。
整体思路
整个项目主要分为三个部分:HTML 结构搭建、CSS 样式设计和 JavaScript 交互逻辑实现。通过 XMLHttpRequest 对象从 JSON 文件中获取数据,将数据转换为树形结构,然后将树形结构渲染为 HTML 元素,并添加点击事件处理逻辑,实现菜单的展开 / 收起和内容的切换。
效果展示
点击不同的节点会有不同的页面显示,并且菜单是在页面的左侧,引入的iframe是在页面的右侧。




代码总览
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1"><title>树形结构示例</title><style>body {display: flex;margin: 0;padding: 0;}.left {width: 220px;background-color: #98D5FF;display: flex;flex-direction: column;overflow-y: auto;/* height: 100vh; */}.content {display: flex;align-items: center;padding: 10px;height: 35px;cursor: pointer;}.content img {margin-right: 8px;}.right {background-color: seagreen;flex-grow: 1;height: 100vh;}.box {display: none;margin-left: 20px;}.imgBot {margin-left: 100px;}.imgLeft {margin-right: 10px;}iframe {width: 100%;height: 100%;}</style></head><body><script>let data;let xhr = new XMLHttpRequest();xhr.open('get', './js/trees.json', true);xhr.send();//回调函数,(放一些比较依赖data的代码)确保数据请求成功后在进行数据渲染xhr.onreadystatechange = function() {if (xhr.readyState == 4 && xhr.status == 200) {let text = xhr.responseText;data = JSON.parse(text);// 开始构建:调用parseTree函数,pid为0开始构建树结构,并将结果存到treeData里let treeData = parseTree(data, 0);// 渲染树形结构并插入到body中(相当于重新写了一下body)document.body.innerHTML = renderTree(treeData);// 创建右侧 iframe(插入的页面)//1.创建一个divlet iframeDiv = document.createElement('div');//2.给这个div起名iframeDiv.className = 'right';// 3.把iframe插入到这个div中,后续 通过点击的树形节点更新srciframeDiv.innerHTML = '<iframe id="contentFrame" src="" frameborder="0"></iframe>';//4.把创建好的div插入到bodydocument.body.appendChild(iframeDiv);}};//递归构建树形结构的函数function parseTree(data, pid) {// 用来存储树状的数据let tree = [];for (let i in data) {//如果数据的 pid 属性等于传入的 pid(父级的id)//⭐子集的pid和父级的id是保持一致的,这样才能完成递归if (data[i].pid === pid) {tree.push(data[i]);//递归调用parseTree,传data和当前数据的id作为pid(上面函数里是data和pid),为了查找当前数据的子集然后用child(子集)方法插入到当前数据后。tree[tree.length - 1].child = parseTree(data, data[i].id);}}// 返回构建的树形结构return tree;}//转化函数(HTML结构)function renderTree(data) {// 这个div是左边菜单的底板let str = '<div class="left">';for (let i in data) {//如果当前数据有子节点的话,就为该数据生成一个包含子节点的htmlif (data[i].child.length > 0) {str +=`<div class="textBox"><div class="content"><img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />${data[i].name}<img class="imgBot" src="./img/向下箭头.png" width="20px" height="20px"/></div><div class="box" style="display:none;">`;//递归调用renderTree函数,生成当前节点的子节点(只有在有子节点的条件下才递归,因为要去检查子节点有没有子节点,没有的话就结束)str += renderTree(data[i].child);//拼结束标签str += '</div></div>';//如果当前数据没有子节点的话,就添加普通元素} else {str +=`<div class="textBox"><div class="content"><img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />${data[i].name}</div></div>`;}}str += '</div>';return str;}// 监听点击事件(给body上),切换子节点显示/隐藏,并更新 iframe 的 srcdocument.body.addEventListener('click', function(e) {//获取点击的目标元素(全局的)let target = e.target;//向上查找父元素,找到有content元素while (target && !target.classList.contains('content')) {target = target.parentElement;}//如果找到了if (target) {// 通过 target.nextElementSibling 获取 target 元素的下一个兄弟元素(即紧接着 .content 元素之后的元素)。let sibling = target.nextElementSibling;// 如果该兄弟元素有box元素if (sibling && sibling.classList.contains('box')) {// 展开或收起子节点sibling.style.display = sibling.style.display === 'none' ? 'block' : 'none';}// 点击叶子节点,更新 iframe//通过target.textContent获取到点击的目标元素文本let name = target.textContent.trim();let item = null;//1.通过循环在data中找与name相匹配的数据for (let i = 0; i < data.length; i++) {// 2.如果相匹配if (data[i].name === name) {// 3.就存到item里item = data[i];break;}}// 如果找到匹配的数据且path路径不为空,则更新iframe的srcif (item && item.path) {document.getElementById('contentFrame').src = item.path;}}});</script></body>
</html>
css 部分
body {display: flex;margin: 0;padding: 0;}.left {width: 220px;background-color: #98D5FF;display: flex;flex-direction: column;overflow-y: auto;/* height: 100vh; */}.content {display: flex;align-items: center;padding: 10px;height: 35px;cursor: pointer;}.content img {margin-right: 8px;}.right {background-color: seagreen;flex-grow: 1;height: 100vh;}.box {display: none;margin-left: 20px;}.imgBot {margin-left: 100px;}.imgLeft {margin-right: 10px;}iframe {width: 100%;height: 100%;}
JS部分
1.数据请求:
使用 XMLHttpRequest 对象从 ./js/trees.json 文件中获取数据。当请求成功后,将数据解析为 JSON 对象,并调用 parseTree 函数将数据转换为树形结构。调用 renderTree 函数将树形结构转换为 HTML 字符串,并插入到页面的 body 元素中。创建一个包含 iframe 的 div 元素,并添加到页面的 body 元素中。
let data;let xhr = new XMLHttpRequest();xhr.open('get', './js/trees.json', true);xhr.send();//回调函数,(放一些比较依赖data的代码)确保数据请求成功后在进行数据渲染xhr.onreadystatechange = function() {if (xhr.readyState == 4 && xhr.status == 200) {let text = xhr.responseText;data = JSON.parse(text);// 开始构建:调用parseTree函数,pid为0开始构建树结构,并将结果存到treeData里let treeData = parseTree(data, 0);// 渲染树形结构并插入到body中(相当于重新写了一下body)document.body.innerHTML = renderTree(treeData);// 创建右侧 iframe(插入的页面)//1.创建一个divlet iframeDiv = document.createElement('div');//2.给这个div起名iframeDiv.className = 'right';// 3.把iframe插入到这个div中,后续 通过点击的树形节点更新srciframeDiv.innerHTML = '<iframe id="contentFrame" src="" frameborder="0"></iframe>';//4.把创建好的div插入到bodydocument.body.appendChild(iframeDiv);
2.构建树形结构的函数
通过递归的方式将数据转换为树形结构,每个节点的子节点存储在 child 属性中。
function parseTree(data, pid):定义一个递归函数,用于将扁平的数据转换为树形结构。
let tree = [];:创建一个空数组 tree 用于存储树形结构的数据。
for (let i in data):遍历数据数组。
if (data[i].pid === pid):如果当前数据的 pid 属性等于传入的 pid,表示该数据是当前节点的子节点。
tree.push(data[i]);:将该数据添加到 tree 数组中。
tree[tree.length - 1].child = parseTree(data, data[i].id);:递归调用 parseTree 函数,查找当前数据的子节点,并将结果存储在 child 属性中。
function parseTree(data, pid) {// 用来存储树状的数据let tree = [];for (let i in data) {//如果数据的 pid 属性等于传入的 pid(父级的id)//⭐子集的pid和父级的id是保持一致的,这样才能完成递归if (data[i].pid === pid) {tree.push(data[i]);//递归调用parseTree,传data和当前数据的id作为pid(上面函数里是data和pid),为了查找当前数据的子集然后用child(子集)方法插入到当前数据后。tree[tree.length - 1].child = parseTree(data, data[i].id);}}// 返回构建的树形结构return tree;}
3.转化函数
根据树形结构生成 HTML 字符串,对于有子节点的菜单项,添加展开 / 收起的箭头图标。
function renderTree(data):定义一个函数,用于将树形结构转换为 HTML 字符串。
data 是树形结构的数据。
let str = '<div class="left">';:初始化一个字符串 str,用于存储 HTML 代码。
for (let i in data):遍历树形结构的数据。
if (data[i].child.length > 0):如果当前元素有子节点,则生成包含子节点的 HTML 代码。str += ...:拼接 HTML 代码,包括左侧的图片、节点名称和向下箭头图标。
str += renderTree(data[i].child);:递归调用 renderTree 函数,生成子节点的 HTML 代码。str += '</div></div>';:拼接结束标签。
else:如果当前元素没有子节点,则生成普通的 HTML 代码。
str += '</div>';:拼接结束标签。return str;:返回生成的 HTML 字符串。
function renderTree(data) {// 这个div是左边菜单的底板let str = '<div class="left">';for (let i in data) {//如果当前数据有子节点的话,就为该数据生成一个包含子节点的htmlif (data[i].child.length > 0) {str +=`<div class="textBox"><div class="content"><img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />${data[i].name}<img class="imgBot" src="./img/向下箭头.png" width="20px" height="20px"/></div><div class="box" style="display:none;">`;//递归调用renderTree函数,生成当前节点的子节点(只有在有子节点的条件下才递归,因为要去检查子节点有没有子节点,没有的话就结束)str += renderTree(data[i].child);//拼结束标签str += '</div></div>';//如果当前数据没有子节点的话,就添加普通元素} else {str +=`<div class="textBox"><div class="content"><img class="imgLeft" src="./img/圣诞礼物.png" width="20px" />${data[i].name}</div></div>`;}}str += '</div>';return str;}
4.点击事件处理函数
监听 body 元素的点击事件,当点击菜单项时,切换子菜单的显示 / 隐藏状态。当点击叶子节点时,根据节点的 path 属性更新 iframe 的 src 属性,显示相应的内容。
document.body.addEventListener('click', function(e):为页面的 body 元素添加一个点击事件监听。
let target = e.target;:获取点击事件的目标元素
while (target && !target.classList.contains('content')):向上查找父元素,直到找到具有 content 类名的元素。
if (target):如果找到了具有 content 类名的元素的话就let sibling = target.nextElementSibling;:获取目标元素的下一个兄弟元素。
if (sibling && sibling.classList.contains('box')):如果兄弟元素具有 box 类名,说明它是子菜单。
sibling.style.display = sibling.style.display === 'none' ? 'block' : 'none';:切换子菜单的显示状态,实现展开和收起的效果。
let name = target.textContent.trim();:获取目标元素的文本内容,并去除首尾的空格。
let item = null;:初始化一个变量 item,用于存储匹配的元素。
for (let i = 0; i < data.length; i++):遍历 data 数组,查找与目标元素文本内容匹配的元素。
if (data[i].name === name):如果找到匹配的元素。
item = data[i];:将匹配的元素存储在 item 变量中。break;:跳出循环。
if (item && item.path):如果找到匹配的元素且该元素具有 path 属性。document.getElementById('contentFrame').src = item.path;:将 iframe 的 src 属性设置为该元素的 path 属性值,从而在 iframe 中加载对应的页面。
document.body.addEventListener('click', function(e) {//获取点击的目标元素(全局的)let target = e.target;//向上查找父元素,找到有content元素while (target && !target.classList.contains('content')) {target = target.parentElement;}//如果找到了if (target) {// 通过 target.nextElementSibling 获取 target 元素的下一个兄弟元素(即紧接着 .content 元素之后的元素)。let sibling = target.nextElementSibling;// 如果该兄弟元素有box元素if (sibling && sibling.classList.contains('box')) {// 展开或收起子节点sibling.style.display = sibling.style.display === 'none' ? 'block' : 'none';}// 点击叶子节点,更新 iframe//通过target.textContent获取到点击的目标元素文本let name = target.textContent.trim();let item = null;//1.通过循环在data中找与name相匹配的数据for (let i = 0; i < data.length; i++) {// 2.如果相匹配if (data[i].name === name) {// 3.就存到item里item = data[i];break;}}// 如果找到匹配的数据且path路径不为空,则更新iframe的srcif (item && item.path) {document.getElementById('contentFrame').src = item.path;}}});
相关文章:
简单树形菜单
引言 在网页开发中,树形菜单是一种非常实用的,它可以清晰地展示具有层级关系的数据,并且能够方便用户进行导航和操作。 整体思路 整个项目主要分为三个部分:HTML 结构搭建、CSS 样式设计和 JavaScript 交互逻辑实现。通过 XMLHt…...
Windows 靶机常见服务、端口及枚举工具与方法全解析:SMB、LDAP、NFS、RDP、WinRM、DNS
在渗透测试中,Windows 靶机通常会运行多种服务,每种服务都有其默认端口和常见的枚举工具及方法。以下是 Windows 靶机常见的服务、端口、枚举工具和方法的详细说明: 1. SMB(Server Message Block) 端口 445/TCP&…...
RNN实现阿尔茨海默症的诊断识别
本文为为🔗365天深度学习训练营内部文章 原作者:K同学啊 一 导入数据 import torch.nn as nn import torch.nn.functional as F import torchvision,torch from sklearn.preprocessing import StandardScaler from torch.utils.data import TensorDatase…...
14-6-1C++STL的list
(一)list容器的基本概念 list容器简介: 1.list是一个双向链表容器,可高效地进行插入删除元素 2.list不可以随机存取元素,所以不支持at.(pos)函数与[ ]操作符 (二)list容器头部和尾部的操作 list对象的默…...
Redis事务机制详解与Springboot项目中的使用
Redis 的事务机制允许将多个命令打包在一起,作为一个原子操作来执行。虽然 Redis 的事务与关系型数据库的事务有所不同,但它仍然提供了一种确保多个命令顺序执行的方式。以下是 Redis 事务机制的详细解析: 1. Redis 事务的基本概念 Redis 事…...
DeepSeek-R1,用Ollama跑起来
# DeepSeek-R1横空出世,超越OpenAI-o1,教你用Ollama跑起来 使用Ollama在本地运行DeepSeek-R1的操作指南。 DeepSeek-R1作为第一代推理模型,在数学、代码和推理任务上表现优异,与OpenAI-o1模型不相上下。 将此类模型部署到本地&am…...
Leecode刷题C语言之组合总和②
执行结果:通过 执行用时和内存消耗如下: int** ans; int* ansColumnSizes; int ansSize;int* sequence; int sequenceSize;int** freq; int freqSize;void dfs(int pos, int rest) {if (rest 0) {int* tmp malloc(sizeof(int) * sequenceSize);memcpy(tmp, seque…...
YOLOv8改进,YOLOv8检测头融合DynamicHead,并添加小目标检测层(四头检测),适合目标检测、分割等,全网独发
摘要 作者提出一种新的检测头,称为“动态头”,旨在将尺度感知、空间感知和任务感知统一在一起。如果我们将骨干网络的输出(即检测头的输入)视为一个三维张量,其维度为级别 空间 通道,这样的统一检测头可以看作是一个注意力学习问题,直观的解决方案是对该张量进行全自…...
【PyQt】QThread快速创建多线程任务
pyqt通过QThread快速创建多线程任务 在 PyQt5 中使用多线程时,需要注意 GUI 线程(主线程) 和 工作线程 的分离。PyQt5 的主线程负责处理 GUI 事件,如果在主线程中执行耗时任务,会导致界面卡顿甚至无响应。因此&#x…...
智能码二维码的成本效益分析
以下是智能码二维码的成本效益分析: 成本方面 硬件成本 标签成本:二维码标签本身价格低廉,即使进行大规模应用,成本也相对较低。如在智能仓储中,塑料托盘加二维码方案的标签成本几乎可以忽略不计4。扫描设备成本&…...
企业财务管理系统的需求设计和实现
该作者的原创文章目录: 生产制造执行MES系统的需求设计和实现 企业后勤管理系统的需求设计和实现 行政办公管理系统的需求设计和实现 人力资源管理HR系统的需求设计和实现 企业财务管理系统的需求设计和实现 董事会办公管理系统的需求设计和实现 公司组织架构…...
Springboot集成Swagger和Springdoc详解
Springboot2.x集成Swagger21. Springboot匹配版本2.7.0~2.7.18(其它版本需要自己去调试匹配)2. 首先导入Swagger2匹配的依赖项3. 导入依赖后创建配置文件SwaggerConfig4. Swagger集成完后,接下来接口的配置Springboot3.x集成Springdoc1. Springboot3.x依赖Springdoc配置2. 在…...
类和对象(4)——多态:方法重写与动态绑定、向上转型和向下转型、多态的实现条件
目录 1. 向上转型和向下转型 1.1 向上转型 1.2 向下转型 1.3 instanceof关键字 2. 重写(overidde) 2.1 方法重写的规则 2.1.1 基础规则 2.1.2 深层规则 2.2 三种不能重写的方法 final修饰 private修饰 static修饰 3. 动态绑定 3.1 动态绑…...
ui-automator定位官网文档下载及使用
一、ui-automator定位官网文档简介及下载 AndroidUiAutomator:移动端特有的定位方式,uiautomator是java实现的,定位类型必须写成java类型 官方地址:https://developer.android.com/training/testing/ui-automator.html#ui-autom…...
董事会办公管理系统的需求设计和实现
该作者的原创文章目录: 生产制造执行MES系统的需求设计和实现 企业后勤管理系统的需求设计和实现 行政办公管理系统的需求设计和实现 人力资源管理HR系统的需求设计和实现 企业财务管理系统的需求设计和实现 董事会办公管理系统的需求设计和实现 公司组织架构…...
ESP32和STM32在处理中断方面的区别
为了通俗地讲解ESP32和STM32在处理中断方面的区别,我们可以把它们想象成两个不同的“智能管家”系统,各自负责管理一个家庭(即嵌入式项目)的各种任务。我们将重点放在如何处理突发事件(即中断)上。 ESP32 …...
零售业革命:改变行业的顶级物联网用例
mpro5 产品负责人Ruby Whipp表示,技术进步持续重塑零售业,其中物联网(IoT)正引领这一变革潮流。 研究表明,零售商们正在采用物联网解决方案,以提升运营效率并改善顾客体验。这些技术能够监控运营的各个方面…...
字符串算法笔记
字符串笔记 说到字符串,首先我们要注意的就是字符串的输入以及输出,因为字符串的输入格式以及要求也分为很多种,我们就来说几个比较常见的格式 g e t s gets gets 我们先来说这个函数的含义...
在Ubuntu上用Llama Factory命令行微调Qwen2.5的简单过程
半年多之前写过一个教程:在Windows上用Llama Factory微调Llama 3的基本操作_llama-factory windows-CSDN博客 如果用命令行做的话,前面的步骤可以参考上面这个博客。安装好环境后, 用自我认知数据集微调Lora模块:data/identity.j…...
ThinkPhp伪静态设置后,访问静态资源也提示找不到Controller
ThinkPhp没有配置伪静态时,除了默认的IndexController能访问,其他路由Controller都访问不到,提示404错误。配置了伪静态后就解决了这个问题。 但是当我的ThinkPhp后台项目中有静态资源放在public目录(或子目录)中需要…...
(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...
【1】跨越技术栈鸿沟:字节跳动开源TRAE AI编程IDE的实战体验
2024年初,人工智能编程工具领域发生了一次静默的变革。当字节跳动宣布退出其TRAE项目(一款融合大型语言模型能力的云端AI编程IDE)时,技术社区曾短暂叹息。然而这一退场并非终点——通过开源社区的接力,TRAE在WayToAGI等…...
Copilot for Xcode (iOS的 AI辅助编程)
Copilot for Xcode 简介Copilot下载与安装 体验环境要求下载最新的安装包安装登录系统权限设置 AI辅助编程生成注释代码补全简单需求代码生成辅助编程行间代码生成注释联想 代码生成 总结 简介 尝试使用了Copilot,它能根据上下文补全代码,快速生成常用…...
简约商务通用宣传年终总结12套PPT模版分享
IOS风格企业宣传PPT模版,年终工作总结PPT模版,简约精致扁平化商务通用动画PPT模版,素雅商务PPT模版 简约商务通用宣传年终总结12套PPT模版分享:商务通用年终总结类PPT模版https://pan.quark.cn/s/ece1e252d7df...
el-amap-bezier-curve运用及线弧度设置
文章目录 简介示例线弧度属性主要弧度相关属性其他相关样式属性完整示例链接简介 el-amap-bezier-curve 是 Vue-Amap 组件库中的一个组件,用于在 高德地图 上绘制贝塞尔曲线。 基本用法属性path定义曲线的路径,可以是多个弧线段的组合。stroke-weight线条的宽度。stroke…...
vue-14(使用 ‘router.push‘ 和 ‘router.replace‘ 进行编程导航)
使用 ‘router.push’ 和 ‘router.replace’ 进行编程导航 编程导航是使用 Vue Router 构建动态和交互式 Web 应用程序的一个重要方面。它允许您根据应用程序逻辑、用户作或特定条件控制用户的导航流。您可以使用 router.push 和 router.replace 方法以编程方式导航到不同的路…...
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
Redis 集群报错 CROSSSLOT Keys in request dont hash to the same slot 的原因及解决方案 1. 错误原因 在 Redis 集群模式下,数据根据 哈希槽(Slot) 分散存储在不同的节点上(默认 16384 个槽)。当执行涉及多个 key …...
