第一部分:下拉菜单HTML/CSS基础实现
1.1 核心设计思路
⭐ 老师强调:下拉菜单的本质是"容器+两块区域"------一块是对外展示的可见区域(如"学位管理"),另一块是默认隐藏的下拉内容区(二级菜单),鼠标悬停时展示,离开时隐藏。
HTML结构:
<ul class="menu">
<li> <!-- 一级菜单项 -->
<a href="#">学位管理</a> <!-- 对外展示区域 -->
<ul class="model"> <!-- 下拉隐藏区域 -->
<li><a href="#">学位委员会</a></li>
<li><a href="#">政策法规</a></li>
</ul>
</li>
<!-- 更多菜单项 -->
</ul>
层级关系:
ul.menu(最外层容器)
└── li(一级菜单项,水平排列)
├── a(对外展示的标签)
└── ul.model(二级菜单,默认隐藏)
└── li(二级菜单项,垂直排列)
└── a
1.2 CSS 基础样式
/* 重置默认样式 */
* { padding: 0; margin: 0; }
li { list-style: none; }
a { text-decoration: none; color: #333; }
/* 菜单容器:弹性盒子实现水平排列 */
.menu {
width: 800px;
background: rgb(235, 235, 235);
margin: 180px auto;
display: flex;
justify-content: space-between;
}
/* ⭐ 一级li:子选择器 > 只选中直接子级 */
.menu > li {
width: 180px; /* 每个菜单项宽度 */
text-align: center;
position: relative; /* 为二级菜单提供定位参照 */
}
/* 二级菜单:绝对定位脱离文档流 */
.model {
position: absolute;
background: rgb(235, 235, 235);
width: 100%; /* 相对于父级li的宽度 */
height: 0; /* 初始高度0,配合过渡 */
overflow: hidden; /* 隐藏超出部分 */
transition: all 0.5s ease; /* 平滑过渡 */
}
⭐ 老师强调 :用
>子选择器(如.menu > li)只选中一级li,避免误选二级li。直接写li会命中所有层级的li,容易造成样式错乱。
1.3 关键知识点:定位与脱离文档流
⭐ 老师强调 :灰色区域的高度默认会被内部最高的子元素撑起来。若不想让二级
ul撑起父容器高度,需给它设position: absolute使其脱离文档流,空间不再保留,父容器便不被撑高。
定位参照系:
| 定位方式 | 参照物 | 说明 |
|---|---|---|
absolute(父级无relative) |
浏览器视口 | 相对整个页面定位 |
absolute(父级有relative) |
父级元素 | 相对最近的定位父元素 |
relative(父级) |
自身原始位置 | 为子元素提供定位参照 |
/* 父级设relative,子级absolute相对父级定位 */
.menu > li {
position: relative; /* 作为子元素参照 */
}
.model {
position: absolute; /* 相对父级li定位 */
width: 100%; /* 占满父级宽度 */
}
⭐ 老师强调 :脱离默认布局不等于切断父子关系,百分比参照系始终是父级。子元素用
%仍是相对于父级计算。
1.4 内联元素 vs 块级元素
⭐ 老师强调 :给
a标签加padding撑不开外层容器,因为a标签是内联级元素 ,就像沙子堆在石头旁,没法把石头的边界撑开。要撑起父级,得把a改成块级元素。
/* 解决:将a标签设为块级元素 */
.menu a {
display: block; /* 块级化,能撑开父容器 */
padding: 5px;
}
内联 vs 块级:
| 特性 | 内联元素(inline) |
块级元素(block) |
|---|---|---|
| 是否独占一行 | ❌ | ✅ |
| 宽高是否生效 | ❌ | ✅ |
| padding是否撑开父级 | ❌ | ✅ |
第二部分:JS交互实现(鼠标悬停版)
2.1 完整代码
window.onload = function() {
// 获取所有一级菜单项
var list = document.querySelectorAll(".menu > li");
for (var i = 0; i < list.length; i++) {
// 鼠标移入:展开
list[i].onmouseenter = function() {
var model = this.querySelector(".model");
if (model) {
// 计算二级菜单总高度
var liList = model.querySelectorAll("li");
var height = liList[0].clientHeight * liList.length;
model.style.height = height + "px";
}
};
// 鼠标移出:收起
list[i].onmouseleave = function() {
var model = this.querySelector(".model");
if (model) {
model.style.height = "0";
}
};
}
};
2.2 执行顺序与元素加载
⭐ 老师强调 :用选择器找元素时,常因JS执行时元素尚未生成而获取不到。解决办法:加
defer延迟加载,或把逻辑绑在window.onload上。
<!-- 方案1:defer延迟加载 -->
<script src="js/下拉框.js" defer></script>
<!-- 方案2:window.onload -->
<script>
window.onload = function() {
// 确保元素已加载
};
</script>
2.3 动态高度计算
⭐ 老师强调 :固定高度写死(如200px)会导致内容溢出。正确做法是动态计算:获取二级菜单下所有
li,用单个li高度乘以li数量,得到总高度。
var model = this.querySelector(".model");
var liList = model.querySelectorAll("li");
// clientHeight = 内容区 + 填充区(不含边框)
var height = liList[0].clientHeight * liList.length;
model.style.height = height + "px";
clientHeight 说明:
┌─────────────────────┐
│ ┌───────────────┐ │
│ │ content │ │ ← 内容区
│ ├───────────────┤ │
│ │ padding │ │ ← 填充区
│ └───────────────┘ │
└─────────────────────┘
clientHeight = content + padding(不含边框)
2.4 if 判断中的真假值
// 用if判断元素是否存在,避免报错
if (this.querySelector(".model")) {
// 存在才执行
var model = this.querySelector(".model");
// ...
}
JS真假值规则:
| 类型 | false 情况 |
true 情况 |
|---|---|---|
| 数字 | 0、NaN |
其他数值 |
| 布尔 | false |
true |
| 字符串 | ""(空字符串) |
非空字符串 |
| 对象 | null |
任何非空对象 |
第三部分:CSS过渡动画(Transition)
3.1 什么是过渡动画
⭐ 老师强调 :元素状态变化默认是"嗖"一下瞬间完成。
transition把瞬间变化变成平滑渐变过程------就像灯一下亮和调光器渐亮的区别。
.test {
width: 200px;
height: 100px;
background: skyblue;
transition: all 3s ease; /* 所有属性3秒渐变 */
}
.test:hover {
width: 500px;
height: 300px;
background: red;
}
transition 子属性:
| 属性 | 说明 | 示例值 |
|---|---|---|
transition-property |
指定要过渡的属性 | width、all |
transition-duration |
过渡持续时间 | 3s、0.5s |
transition-timing-function |
速度曲线 | ease、linear |
timing-function 常用值:
| 值 | 效果 |
|---|---|
ease |
慢→快→慢(默认) |
linear |
匀速 |
ease-in |
先慢后快 |
ease-out |
先快后慢 |
3.2 ⭐ display 不支持过渡
⭐ 老师强调 :
display: none→display: block不支持过渡动画,显隐都是"嗖"一下。想平滑展开/收起,不能用display,要用高度(height)配合overflow: hidden。
/* ❌ display不支持过渡 */
.model {
display: none;
transition: all 0.5s ease; /* 无效! */
}
.model:hover {
display: block;
}
/* ✅ 高度支持过渡 */
.model {
height: 0;
overflow: hidden;
transition: all 0.5s ease; /* 有效! */
}
.model:hover {
height: 200px; /* 配合JS动态计算更好 */
}
支持过渡 vs 不支持过渡:
| 属性 | 是否支持过渡 |
|---|---|
width、height |
✅ |
color、background |
✅ |
opacity |
✅ |
display |
❌ |
position |
❌ |
第四部分:点击交互与事件传播
4.1 点击版本完整代码
window.onload = function() {
var list = document.querySelectorAll(".menu > li");
var modelList = document.querySelectorAll(".model");
for (var i = 0; i < list.length; i++) {
list[i].onclick = function(e) {
// ⭐ 阻止事件传播(关键!)
e.stopPropagation();
// 先全部隐藏
for (var j = 0; j < modelList.length; j++) {
modelList[j].style.height = 0;
}
// 再显示当前项
var model = this.querySelector(".model");
if (model) {
var liList = model.querySelectorAll("li");
var height = liList[0].clientHeight * liList.length;
model.style.height = height + "px";
}
};
}
// 点击空白区域全部收起
document.onclick = function() {
for (var j = 0; j < modelList.length; j++) {
modelList[j].style.height = 0;
}
};
};
核心逻辑:
点击菜单项
│
▼
1. 阻止事件传播(stopPropagation)
│
▼
2. 全部隐藏(所有model高度设为0)
│
▼
3. 显示当前项(只有被点击的model展开)
4.2 事件传播机制
⭐ 老师强调 :点击嵌套结构中的子元素时,不仅触发自身
click,还会因事件冒泡 触发父级click。就像涟漪效应------点外层会波及内层。
点击子元素时的事件传播路径:
document → ... → 父级li → 子元素
↑
事件冒泡方向(从内到外)
事件传播的两种行为:
| 阶段 | 方向 | 说明 |
|---|---|---|
| 捕获 | 从外到内 | 从document到目标元素 |
| 冒泡 | 从内到外 | 从目标元素到document(默认) |
4.3 阻止事件传播:stopPropagation()
⭐ 老师强调 :用
onclick无法阻止事件传播。改用addEventListener配合e.stopPropagation(),可阻止事件继续冒泡到父级。
// ❌ onclick无法阻止传播
list[i].onclick = function(e) {
// 无法阻止父级触发
};
// ✅ addEventListener + stopPropagation
list[i].onclick = function(e) {
e.stopPropagation(); // 阻止事件向上传播
// 只触发当前元素的逻辑
};
阻止传播的效果:
点击前:点击子元素 → 子元素click触发 → 父元素click也触发
点击后:点击子元素 → 子元素click触发 → 父元素click不触发
4.4 点击空白收回
// 点击document空白区域,全部收起
document.onclick = function() {
for (var j = 0; j < modelList.length; j++) {
modelList[j].style.height = 0;
}
};
⭐ 老师强调:这个逻辑是"先关所有灯,再只开当前房间那盏"。用"先全部隐藏再定向显示"的思路,比记录之前谁显示更简洁,避免状态管理混乱。
第五部分:鼠标悬停版 vs 点击版对比
| 对比项 | 悬停版(hover) | 点击版(click) |
|---|---|---|
| 触发方式 | 鼠标移入/移出 | 鼠标点击 |
| 收起方式 | 自动(移出即收) | 手动(点击其他区域) |
| 事件传播问题 | 无 | 需stopPropagation() |
| 适用场景 | 传统导航菜单 | 移动端/下拉选择器 |
| 代码复杂度 | 较简单 | 较复杂 |
附录一:完整代码汇总
HTML(下拉框.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>下拉菜单</title>
<link rel="stylesheet" href="css/下拉框.css">
<script src="js/下拉框.js" defer></script>
</head>
<body>
<ul class="menu">
<li>
<a href="#">学位管理</a>
<ul class="model">
<li><a href="#">学位委员会</a></li>
<li><a href="#">政策法规</a></li>
</ul>
</li>
<li>
<a href="#">导师管理</a>
<ul class="model">
<li><a href="#">学位委员会</a></li>
<li><a href="#">政策法规</a></li>
<li><a href="#">常置信息</a></li>
<li><a href="#">优博优硕</a></li>
</ul>
</li>
<li>
<a href="#">学生工作</a>
<ul class="model">
<li><a href="#">学位委员会</a></li>
<li><a href="#">政策法规</a></li>
<li><a href="#">常置信息</a></li>
<li><a href="#">政策法规</a></li>
<li><a href="#">常置信息</a></li>
<li><a href="#">优博优硕</a></li>
</ul>
</li>
<li>
<a href="#">下载专区</a>
<ul class="model">
<li><a href="#">学位委员会</a></li>
<li><a href="#">政策法规</a></li>
<li><a href="#">常置信息</a></li>
<li><a href="#">优博优硕</a></li>
</ul>
</li>
</ul>
</body>
</html>
CSS(下拉框.css)
* { padding: 0; margin: 0; }
li { list-style: none; }
a {
text-decoration: none;
color: #333;
padding: 5px;
display: block;
}
a:hover {
background: #333;
opacity: 0.6;
color: #fff;
}
.menu {
width: 800px;
background: rgb(235, 235, 235);
margin: 180px auto;
display: flex;
justify-content: space-between;
}
.menu > li {
width: 180px;
text-align: center;
position: relative;
}
.model {
position: absolute;
background: rgb(235, 235, 235);
width: 100%;
height: 0;
overflow: hidden;
transition: all 0.5s ease;
}
JavaScript(点击版 - 下拉框.js)
window.onload = function() {
var list = document.querySelectorAll(".menu > li");
var modelList = document.querySelectorAll(".model");
for (var i = 0; i < list.length; i++) {
list[i].onclick = function(e) {
e.stopPropagation();
for (var j = 0; j < modelList.length; j++) {
modelList[j].style.height = 0;
}
var model = this.querySelector(".model");
if (model) {
var liList = model.querySelectorAll("li");
var height = liList[0].clientHeight * liList.length;
model.style.height = height + "px";
}
};
}
document.onclick = function() {
for (var j = 0; j < modelList.length; j++) {
modelList[j].style.height = 0;
}
};
};