# 兼容多终端响应式项目开发,标准规范 和 最佳实践

TIP

从本节开始,会带领 从 0 到 1,亲历 多终端企业真实项目开发,综合提升 CSS 布局 及 企业开发标准 和 规范 及 最佳实践。

PS 还原 UI、蓝湖标注切图 -> CSS 方法论、大厂 PC 端项目开发标准和规范 -> SEO 优化实践,手写代码 + Git 代码管理 + 云服务器项目部署 与 发布

image-20250114234458690

项目预览地址:https://www.arryblog.com/web/project/responsive/show.html (opens new window)

# 一、项目开发流程

TIP

以下是本次项目的开发流程(针对企业真实的大型项目开发,接到项目后,就要开会一起讨论,定项目的功能设计和开发流程、进度等)

# 1、第一步:我们需要分析整个项目有哪些具体的功能

TIP

  • 响应式功能 、主题皮肤切换功能
  • 大量 JavaScript 特效
    • 页面加载进度条、滑动二级下拉菜单
    • 垂直二级伸缩菜单、响应式二级菜单、抽屉式菜单
    • 吸顶盒菜单、右侧悬浮菜单
    • 数字动画、带左右按扭的轮播图、Tab 选项卡切换特效
    • 滑块跟随鼠标导航、导航吸顶盒效果
    • 楼梯式导航特效、点击弹出显示大图轮播
    • 页面滚动加载动画、点击返回页面顶部
  • 大量 CSS3 动画
    • 淡入淡出动画、鼠标悬停动画
    • 3D 翻转动画、滑动扫光效果、复杂的过渡动画

# 2、第二步:针对项目的功能,选择采用什么样的实现方案

TIP

  • 针对响应式功能,我选择自己手写响应式栅格系统来实现 (手写 media.css
  • 针对主题皮肤的切换功能,我选择采用 原生 JS + CSS 手动实现
  • 针对项目中的 JS 动画特效,播图我们选用 Swiper 插件来实现,其余动画均采用手写原生 JS 实现
  • CSS3 动画能用 animate.css 库实现的都用他实现,其它的均自己手写。

# 3、第三步:开始项目架构的搭建

TIP

由于受限于现阶段所学知识,该项目只是静态页面开发 + JavaScript 效果,暂时不涉及 前端框架相关内容,整个项目架构的搭建同时整合了一线互联网大厂项目最佳实践 和 规范。

为我们先一步建立企业项目开发思维流程 和 认知。

# 二、项目开发架构搭建

TIP

在正式进入项目具体功能开发前,我们需要先搭建好项目架构。本项目架构搭建,主要完成以下内容

  • 创建项目的目录结构
  • 重置默认样式
  • 设置项目通用字体样式
  • 定制主题皮肤
  • 准备项目所需要的图片素材
  • 实现响应式栅格系统
  • 准备好项目中用到 iconfont 图标
  • 使用 git 管理项目

# 1、创建项目的目录结构

TIP

  • 新建项目文件夹 response-web
  • response-web 中新建 cssjsimages 文件夹,分别用来存放 CSS、JS 文件和图片
  • css 文件夹中新建 reset.cssglobal.cssindex.css 文件,分别用来存放重置默认样式、全局通用样式,首页样式
response-web  // 项目文件夹 - 用来存文本项目所有内容
├─ css  // css文件夹,用来存放 css 文件
│  ├─ global.css  //  全局通用样式
│  ├─ index.css  // 项目首页样式
│  └─ reset.css  // 重置默认样式
├─ images // images 文件夹,用来存放项目图片素材
├─ index.html  // 项目首页
└─ js  // js 文件夹,用来存放 js 文件

# 2、重置默认样式

TIP

css/reset.css 文件中,添加如下 css,用来重置 html 标签的默认样式

点击查看完整源代码
@charset "utf-8";

/* --------------------重置样式-------------------------------- */

body,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
button,
input,
textarea,
th,
td {
  margin: 0;
  padding: 0;
}

/* 设置默认字体 */
body {
  font-size: 14px;
  font-style: normal;
  font-family: -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica
      neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe
      ui symbol, noto color emoji;
}

/* 字体太小用户体检不好,让small恢复12px */
small {
  font-size: 12px;
}

h1 {
  font-size: 18px;
}

h2 {
  font-size: 16px;
}

h3 {
  font-size: 14px;
}

h4,
h5,
h6 {
  font-size: 100%;
}

ul,
ol {
  list-style: none;
}

a {
  text-decoration: none;
  background-color: transparent;
}

a:hover,
a:active {
  outline-width: 0;
  text-decoration: none;
}

/* 重置表格 */
table {
  border-collapse: collapse;
  border-spacing: 0;
}

/* 重置hr */
hr {
  border: 0;
  height: 1px;
}

/* 图形图片 */
img {
  border-style: none;
}

img:not([src]) {
  display: none;
}

svg:not(:root) {
  overflow: hidden;
}

/* 下面的操作是针对于html5页面布局准备的,不支持ie6~8以及其他低版本的浏览器 */
html {
  /* 禁用系统默认菜单 */
  -webkit-touch-callout: none;
  /* 关闭iphone & Android的浏览器纵向和横向模式中自动调整字体大小的功能 */
  -webkit-text-size-adjust: 100%;
}

input,
textarea,
button,
a {
  /* 表单或者a标签在手机点击时会出现边框或彩色的背景区域,意思是去除点击背景框 */
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

/* 重置html5元素的默认样式 */
article,
aside,
details,
figcaption,
figure,
footer,
header,
main,
menu,
nav,
section,
summary {
  display: block;
}

audio,
canvas,
progress,
video {
  display: inline-block;
}

audio:not([controls]),
video:not([controls]) {
  display: none;
  height: 0;
}

progress {
  vertical-align: baseline;
}

mark {
  background-color: #ff0;
  color: #000;
}

sub,
sup {
  position: relative;
  font-size: 75%;
  line-height: 0;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

button,
input,
select,
textarea {
  font-size: 100%;
  outline: 0;
}

button,
input {
  overflow: visible;
}

button,
select {
  text-transform: none;
}

textarea {
  overflow: auto;
}

button,
html [type="button"],
[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
}

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

[type="checkbox"],
[type="radio"] {
  box-sizing: border-box;
  padding: 0;
}

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

[type="search"] {
  -webkit-appearance: textfield;
  outline-offset: -2px;
}

[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

::-webkit-input-placeholder {
  color: inherit;
  opacity: 0.54;
}

::-webkit-file-upload-button {
  -webkit-appearance: button;
  font: inherit;
}

关于标签默认样式及清除相关内容,可以参考博客 标签默认博客地 (opens new window) (opens new window) (opens new window)👆 中如下截图内容

image-20220815153351605

# 3、设置通用字体样式

TIP

通过对设计稿的分析,我们可以了解页面字体通用样式

  • 页面中大部分字体为 16px
  • 行间距为字体大小的 1.5 倍 (每个公司要求不一样,在实际开发中,以公司规范定为主)
  • 字体类型主要是微软雅黑
  • 字体颜色主要以#000 黑色为主

通过以上数据最后得到如下代码,添加到 ./css/global.css 文件中,作为全局通用样式

/* 项目通用字体样式 */
body {
  /* 字体大小: 16px  
    行高:为字体的 1.5倍 
    字体类型:微软雅黑 */
  font: 16px/1.5 Microsoft YaHei;
  color: #000; /* 字体颜色 */
}

# 4、定制主题皮肤

TIP

通过对设计稿的用色分析,可以定制出网站的皮肤主题。

以下是本项目三种不同皮肤主题的颜色(这部分主要由设计师来定)

image-20250112171414974

主题二和主题三,在这个项目中,只是主色调最深的色不一样,其它都一样

image-20250112153206435

注:

通过给出的三套皮肤色,我们可以定义三套皮肤主题。

css 文件夹中,新建 skin-theme.css 文件,用来保存当前网站的皮肤主题

通过更新 html 标签上自定义属性 data-theme 的值来切换到不同主题

/* 清绿色 turquoise 默认主题 */
:root,
html[data-theme="turquoise"] {
  /* 主色调 */
  --primary--color: #1ec28b; /* 主体色 */
  --light-gray-color: #f4f4f6; /* 浅灰色 */
  --white-color: #fff; /* 白色 */

  /* 字体色 */
  --font-black-color: #000; /* 黑色 */
  --font-dark-grey-color: #666; /* 深灰色 */
  --font-red-color: #ff0000; /* 红色 */
  --font-white-color: #fff; /* 白色 */

  /* 边框色 */
  --border-dark-grey-color: rgba(28, 28, 27, 0.22); /* 深灰色边框 */

  /* 辅助色 */
  --sub-dark-red-color: #ff6347; /* 深红色 */
  --sub-deep-orange-color: #ff8900; /* 深橘红色 */

  /* 网站底部色 */
  --footer-bg-color1: #2e2e2e; /* 网站底部背景色 */
  --footer-bg-color2: #212121; /* 网站底部背景色 */
  --footer-font-color: #d6d6d6; /* 网站底部背景色 */
}

/* 深黄色 #ffd336 */
html[data-theme="yellow"] {
  /* 主色调 */
  --primary--color: #ffd336; /* 主体色 */
  --light-gray-color: #f4f4f6; /* 浅灰色 */
  --white-color: #fff; /* 白色 */

  /* 字体色 */
  --font-black-color: #000; /* 黑色 */
  --font-dark-grey-color: #666; /* 深灰色 */
  --font-red-color: #ff0000; /* 红色 */
  --font-white-color: #fff; /* 白色 */

  /* 边框色 */
  --border-dark-grey-color: rgba(28, 28, 27, 0.22); /* 深灰色边框 */

  /* 辅助色 */
  --sub-dark-red-color: #ff6347; /* 深红色 */
  --sub-deep-orange-color: #ff8900; /* 深橘红色 */

  /* 网站底部色 */
  --footer-bg-color1: #2e2e2e; /* 网站底部背景色 */
  --footer-bg-color2: #212121; /* 网站底部背景色 */
  --footer-font-color: #d6d6d6; /* 网站底部背景色 */
}

/* 深红色 */
html[data-theme="red"] {
  /* 主色调 */
  --primary--color: #ff383f; /* 主体色 */
  --light-gray-color: #f4f4f6; /* 浅灰色 */
  --white-color: #fff; /* 白色 */

  /* 字体色 */
  --font-black-color: #000; /* 黑色 */
  --font-dark-grey-color: #666; /* 深灰色 */
  --font-red-color: #ff0000; /* 红色 */
  --font-white-color: #fff; /* 白色 */

  /* 边框色 */
  --border-dark-grey-color: rgba(28, 28, 27, 0.22); /* 深灰色边框 */

  /* 辅助色 */
  --sub-dark-red-color: #ff6347; /* 深红色 */
  --sub-deep-orange-color: #ff8900; /* 深橘红色 */

  /* 网站底部色 */
  --footer-bg-color1: #2e2e2e; /* 网站底部背景色 */
  --footer-bg-color2: #212121; /* 网站底部背景色 */
  --footer-font-color: #d6d6d6; /* 网站底部背景色 */
}

# 4.1、使用皮肤主题

TIP

  • index.html 页面中引用 skin-theme.css 文件
  • 然后,使用 skin-theme.css 文件中定义的主题皮肤色
<link rel="stylesheet" href="./css/skin-theme.css" />
<style>
    .box {
        width: 200px;
        height: 100px;
         /* 背景颜色使用css变量,在不同的皮肤主题下,背景颜色不一样 */
        background-color: var(--primary-color);
    }
</style>
</head>
<body>
    <div class="box">我是一个长方形,不同皮肤主题下,我的背景颜色不一样。</div>
</body>

image-20250112174050053

# 4.2、皮肤主题切换原理

TIP

通过更新 html 标签上自定义属性 data-theme 的值来切换到不同主题

<!-- 将 data-theme 的值更新为  red  、 yellow 、 turquoise 时,可以看到不同 -->
<html lang="en" data-theme="red">
  <!--  --->
</html>

data-theme 属性不同值的效果

data-theme="red" data-theme="yellow" data-theme="turquoise"
image-20250112180916343 image-20250112181026472 image-20250112174050053

# 5、准备项目开发所需的图片素材

TIP

把项目的 psd 设计稿标注好切图,然后上传到蓝湖 (在公司,一般都有设计师来完成)

项目 psd 设计稿已上传到钉钉群文件,自行下载。关于如何用 ps 标注切图,讲面讲过了。

如果不会,可以然钉钉群看如下视频

image-20250112185346892

打开蓝湖,找到项目设计稿,导出项目的所有切图(正常情况下,设计师会把标注好的图片取上合理的名字,但如果没有,你就需要对图重命名)

image-20250112185907916

将导出来的图片素材,复制到项目的 images 文件夹下

# 6、实现响应式栅格系统

TIP

根据以下 3 步,来确定响应式栅格系统的具体代码

  • 将页面分成 12 份,确定栅格布局不同份数所点比例
  • 确定项目对应的断点,这里我们采用标准的断点
屏幕大小 栅格布局中 class 名区分 断点(阈值)
超小屏(Extra small ) <576px
小屏 (Small) -sm 576px ~ 768px (含等于)
中屏 (Medium) -md 768px ~ 992px (含等于)
大屏 (Large) -lg 992px ~ 1200px(含等于)
超大屏 (X-Large) -xl 1200px ~ 1400px(含等于)
超大大屏(XX-Large) -xxl >1400px
  • 确定适配方案,以 PC 端优先
  • 按前面三步,实现响应式栅格系统(也就是书写 media.css样式)
点击查看完整源代码
/* 
第一:我们将页面分成12分
第二:我们选择的断点是行业标准断点
第三:我们选择的适配方案,是PC端优先
*/

/* ....这里的css样式,会在屏幕宽大于1400px时生效.... */

.col-xxl-1 {
  width: 8.333333%;
}
.col-xxl-2 {
  width: 16.6666667%;
}
.col-xxl-3 {
  width: 25%;
}

.col-xxl-4 {
  width: 33.33333333%;
}
.col-xxl-5 {
  width: 41.66666667%;
}
.col-xxl-6 {
  width: 50%;
}
.col-xxl-7 {
  width: 58.33333333%;
}
.col-xxl-8 {
  width: 66.6666667%;
}
.col-xxl-9 {
  width: 75%;
}
.col-xxl-10 {
  width: 83.33333333%;
}
.col-xxl-11 {
  width: 91.66666667%;
}
.col-xxl-12 {
  width: 100%;
}

/* 当屏幕宽度大于1200px ,但小于等于1400px时,显示如下样式 */
@media screen and (max-width: 1400px) {
  .col-xl-1 {
    width: 8.333333%;
  }
  .col-xl-2 {
    width: 16.6666667%;
  }
  .col-xl-3 {
    width: 25%;
  }

  .col-xl-4 {
    width: 33.33333333%;
  }
  .col-xl-5 {
    width: 41.66666667%;
  }
  .col-xl-6 {
    width: 50%;
  }
  .col-xl-7 {
    width: 58.33333333%;
  }
  .col-xl-8 {
    width: 66.6666667%;
  }
  .col-xl-9 {
    width: 75%;
  }
  .col-xl-10 {
    width: 83.33333333%;
  }
  .col-xl-11 {
    width: 91.66666667%;
  }
  .col-xl-12 {
    width: 100%;
  }
}

/* 当屏幕宽度大于992px ,但小于等于1200px时,显示如下样式 */
@media screen and (max-width: 1200px) {
  .col-lg-1 {
    width: 8.333333%;
  }
  .col-lg-2 {
    width: 16.6666667%;
  }
  .col-lg-3 {
    width: 25%;
  }

  .col-lg-4 {
    width: 33.33333333%;
  }
  .col-lg-5 {
    width: 41.66666667%;
  }
  .col-lg-6 {
    width: 50%;
  }
  .col-lg-7 {
    width: 58.33333333%;
  }
  .col-lg-8 {
    width: 66.6666667%;
  }
  .col-lg-9 {
    width: 75%;
  }
  .col-lg-10 {
    width: 83.33333333%;
  }
  .col-lg-11 {
    width: 91.66666667%;
  }
  .col-lg-12 {
    width: 100%;
  }
}

/* 当屏幕宽度大于768px ,但小于等于992px时,显示如下样式 */
@media screen and (max-width: 992px) {
  .col-md-1 {
    width: 8.333333%;
  }
  .col-md-2 {
    width: 16.6666667%;
  }
  .col-md-3 {
    width: 25%;
  }

  .col-md-4 {
    width: 33.33333333%;
  }
  .col-md-5 {
    width: 41.66666667%;
  }
  .col-md-6 {
    width: 50%;
  }
  .col-md-7 {
    width: 58.33333333%;
  }
  .col-md-8 {
    width: 66.6666667%;
  }
  .col-md-9 {
    width: 75%;
  }
  .col-md-10 {
    width: 83.33333333%;
  }
  .col-md-11 {
    width: 91.66666667%;
  }
  .col-md-12 {
    width: 100%;
  }
}

/* 当屏幕宽度大于576px ,但小于等于768px时,显示如下样式 */
@media screen and (max-width: 768px) {
  .col-sm-1 {
    width: 8.333333%;
  }
  .col-sm-2 {
    width: 16.6666667%;
  }
  .col-sm-3 {
    width: 25%;
  }

  .col-sm-4 {
    width: 33.33333333%;
  }
  .col-sm-5 {
    width: 41.66666667%;
  }
  .col-sm-6 {
    width: 50%;
  }
  .col-sm-7 {
    width: 58.33333333%;
  }
  .col-sm-8 {
    width: 66.6666667%;
  }
  .col-sm-9 {
    width: 75%;
  }
  .col-sm-10 {
    width: 83.33333333%;
  }
  .col-sm-11 {
    width: 91.66666667%;
  }
  .col-sm-12 {
    width: 100%;
  }
}

/* 当屏幕宽度小于等于576px时,显示如下样式 */
@media screen and (max-width: 576px) {
  .col-1 {
    width: 8.333333%;
  }
  .col-2 {
    width: 16.6666667%;
  }
  .col-3 {
    width: 25%;
  }

  .col-4 {
    width: 33.33333333%;
  }
  .col-5 {
    width: 41.66666667%;
  }
  .col-6 {
    width: 50%;
  }
  .col-7 {
    width: 58.33333333%;
  }
  .col-8 {
    width: 66.6666667%;
  }
  .col-9 {
    width: 75%;
  }
  .col-10 {
    width: 83.33333333%;
  }
  .col-11 {
    width: 91.66666667%;
  }
  .col-12 {
    width: 100%;
  }
}

# 7、下载项目中用到的 iconfont 图标

TIP

进入网站 iconfont 阿里图标库 (opens new window), 把下面图标加入到购物车,然后添加到项目,最后将所有图标下载下来。

image-20250113111307270

将存放图标对应文件夹名改为 iconfont ,放在当前项目根目录下。

温馨提示:

如果不会下载 iconfont 图标,可以进入 iconfont 阿里矢量图标库 (opens new window) 页面,找到如下图所示位置, 查看详细操作教程

image-20250113111609271

# 8、使用 Git 管理项目

TIP

按以下步骤,使用 Git 来管理项目

  • 第一步:安装 Git
  • 第二步:Git 的基础配置
  • 第三步:初始化 Git 本地仓库
  • 第四步:新建 .gitignore 文件 和 README.md 文件
  • 第五步:使用 Gitee,新建远程仓库
  • 第六步:添加远程仓库提交地址
  • 第七步:Git 提交,提交代码到本地仓库 和 远程仓库

如果不会操作 Git,可以进入 Git 初始化项目 (opens new window) 查看详细 Git 操作教程。(需要视频版教程,可以联系客服免费获取)

# 三、正式进入项目开发

TIP

首先分析整个设计稿,确定接下来的布局方向。

整个项目由 9 大版组成,各版块之间相互独立,核心内容在 1200px 区域中居中显示。

所以,我们采取从上到下,一个版块一个版块来实现,最后再实现侧边栏相关内容布局(如:楼梯式导航、皮肤主题切换、右侧悬浮菜单)

screencapture-arryblog-web-project-responsive-index-html-2025-01-13-14_22_04

接下来,在 index.html 页面按顺序引入 css 文件

<!DOCTYPE html>
<html lang="en" data-theme="turquoise">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>兼容多终端响应式网站</title>
    <!-- 重置 html 标签默认样式 -->
    <link rel="stylesheet" href="./css/reset.css" />
    <!-- 引入主题皮肤样式 -->
    <link rel="stylesheet" href="./css/skin-theme.css" />
    <!-- 引入全局样式 -->
    <link rel="stylesheet" href="./css/global.css" />
    <!-- 引入首页样式 -->
    <link rel="stylesheet" href="./css/index.css" />
    <!-- 引入媒体查询 栅格系统样式 -->
    <link rel="stylesheet" href="./css/media.css" />
  </head>
  <body></body>
</html>

# 四、网站头部导航

GIF2025-1-1411-32-35

以下是网站头部导航的具体实现步骤

# 1、第一步:渐变色背景的容器

image-20250113150649967

  • 因为导航和轮播图在同一个容器,所以在实现菜单前,先要创建好渐变色背景的容器
  • 背景色是一个从上到下的径向渐变,并且颜色有 40% 的透明度。
  • 因为我们这里的颜色是直接通过 CSS var 函数引用 主题皮肤中的自定义属性来实现的,所以只能通过以下方式实现背景色透明度
background-image: linear-gradient(
  to bottom,
  var(--primary--color),
  var(--white-color)
);
pacity: 0.4;
  • 这里我们采用伪元素来绘制这个渐变的背景,然后定位在当前容器上面。
  • 为了让菜单还有轮播图内容能显示在渐变背景上面,所以我们还要创建一个用来放 菜单和轮播图的 div 元素,然后采用定位,定位在渐变的背景色上面。
<style>
  .header {
    width: 100%;
    height: 758px;
    position: relative; /* 相对定位 */
  }
  .header::before,
  .header .header-content {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
  }
  .header::before {
    content: "";
    background-image: linear-gradient(
      to bottom,
      var(--primary--color),
      var(--white-color)
    );
    opacity: 0.4;
  }
  /* .header .header-content {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    } */
</style>

<!-- 网页头部 开始 -->
<header class="header">
  <div class="header-content">绝对定位元素 - 有来放置导航和轮播图</div>
</header>
<!-- 网页头部 结束 -->

# 2、第二步:制作顶部放置 logo 和 导航容器

image-20250113152535953

<style>
  .top {
    height: 80px;
  }
  /* 这个样式为通用样式,移到 global.css 中,但是这里为了演示,暂时放在这里。 */
  .layout {
    max-width: 1200px; /* 这里要用 max-width ,这样页面缩小时,容器也会缩小 */
    margin: 0 auto;
  }
  .top .top-content {
    height: inherit;
    background-color: red; /* 后面会去掉 */
  }
</style>

<!-- 网页头部 开始 -->
<header class="header">
  <div class="header-content">
    <!-- top开始 -->
    <div class="top">
      <div class="top-content layout"></div>
    </div>
    <!-- top结束 -->
  </div>
</header>
<!-- 网页头部 结束 -->

# 3、第三步:添加 logo 和 导航

TIP

利用 flex 弹性布局实现 logo 和导航水平两端对齐,同时垂直居中对齐

image-20250113153716621

<style>
  .top .top-content {
    /* 省略了部分css 具体见前面 */
    display: flex; /* 开启弹性布局 - 所有子项默认水平排列 */
    align-items: center; /* 垂直方向居中对齐 */
    justify-content: space-between; /* 水平方向两端对齐 */
  }
  .top ul.menu {
    display: flex; /* 开启弹性布局 - 所有子项默认水平排列 */
  }
</style>

<!-- top开始 -->
<div class="top">
  <div class="top-content layout">
    <!-- logo开始 -->
    <div class="logo">
      <a href="#">
        <img src="./images/logo.png" alt="艾编程logo" width="118" />
      </a>
    </div>
    <!-- logo开始 -->
    <!-- 导航开始 -->
    <nav>
      <ul class="menu">
        <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>
        <li><a href="">合作案例</a></li>
        <li><a href="">关于我们</a></li>
      </ul>
    </nav>
    <!-- 导航结束 -->
  </div>
</div>
<!-- top结束 -->

# 4、第四步:美化导航

image-20250113155227841

  • 为了让鼠标移入导航文字上下区域和左右间隙也能有移入效果,我们需要给 a 标签添加行高 80px 。
  • 行高 80px 也能让文字在垂直方向上距中对齐
.top .top-content {
  /* 省略了部分css 具体见前面 */
  /* 去掉 红色的背景 */
  /* background-color: red;  后面会去掉  */
}

.top ul.menu li a {
  display: block; /* 块级元素 */
  line-height: 80px; /* 行高等于父元素的高度 文字垂直居中显示 */
  padding-right: 16px; /* 右内边距 */
  padding-left: 15px; /* 左内边距 */
  color: var(--font-black-color);
  font-size: 18px;
}
/* 鼠标移入改变文字颜色 */
.top ul.menu li a:hover {
  color: var(--primary--color);
}

# 5、第五步:鼠标移入导航时,显示动态下划线

GIF2025-1-1316-13-03

  • a 标签 或 li 标签添加 ::after 伪元素,来实现动态下划线
  • 当定位元素的leftright 属性值都为 50% 时,相当于隐藏元素
ul.menu li {
  position: relative; /* 开启定位,为伪元素定位做准备 */
}

/* 给 a 标签 或 li标签添加 ::after 伪元素,来实现动态下划线 */
ul.menu li::after {
  content: "";
  position: absolute;
  /* 当定位元素的 left 和 right 属性值都为 50%时,相当于隐藏元素 */
  left: 50%;
  right: 50%;
  bottom: 0;
  height: 2px;
  background-color: var(--primary--color);
  transition: left 0.3s, right 0.3s; /* 为属性添加过渡动效果 */
}

/* 鼠标移入改变伪元素的宽 */
ul.menu li:hover::after {
  left: 0;
  right: 0;
}

# 6、第六步:实现二级下拉菜单

GIF2025-1-1316-38-34

二级菜单中的 a 标签会继承 一级菜单中的padding: 0 16px 0px 15px; 所以在这一步中,要重置 a 标签的 padding 值。

<style>
  ul.menu li .menu-dropdown {
    position: absolute;
    left: 0;
    right: 0;
    top: 80px;
    background-color: var(--white-color);
    transform-origin: top center; /* 设置变换的原点 顶部的中间 */
    transform: scaleY(0); /* 初始状态为折叠  缩放到0 元素完全看不见 */
    transition: transform 0.3s; /* 为属性添加过渡动效果 */
  }

  ul.menu li .menu-dropdown {
    padding-top: 2px;
    padding-bottom: 8px;
  }

  ul.menu li .menu-dropdown dd {
    overflow: hidden;
  }
  .top ul.menu li .menu-dropdown dd a {
    /* display: block; */
    line-height: 39px;
    padding: 0px; /* 重置 a 标签 继承的 样式*/
    text-align: center;
  }
  .top ul.menu li:hover .menu-dropdown {
    transform: scaleY(1); /* 初始状态为折叠  缩放到0 元素完全看不见 */
  }
</style>

<!-- 导航开始 -->
<nav>
  <ul class="menu">
    <li><a href="">网站首页</a></li>
    <li>
      <a href="">国内游</a>
      <dl class="menu-dropdown">
        <dd><a href="#">北京</a></dd>
        <dd><a href="#">三亚</a></dd>
        <dd><a href="#">广东</a></dd>
        <dd><a href="#">厦门</a></dd>
        <dd><a href="#">云南</a></dd>
      </dl>
    </li>
    <li>
      <a href="">出镜游</a>
      <dl class="menu-dropdown">
        <dd><a href="#">泰国</a></dd>
        <dd><a href="#">日本</a></dd>
        <dd><a href="#">美国</a></dd>
        <dd><a href="#">台湾</a></dd>
        <dd><a href="#">海岛</a></dd>
      </dl>
    </li>
    <li>
      <a href="">周边游</a>
      <dl class="menu-dropdown">
        <dd><a href="#">北海</a></dd>
        <dd><a href="#">桂林</a></dd>
        <dd><a href="#">张家界</a></dd>
        <dd><a href="#">凤凰</a></dd>
      </dl>
    </li>
    <li>
      <a href="">主题游</a>
      <dl class="menu-dropdown">
        <dd><a href="#">游学</a></dd>
        <dd><a href="#">自组</a></dd>
      </dl>
    </li>
    <li>
      <a href="">自由行</a>
      <dl class="menu-dropdown">
        <dd><a href="#">三亚</a></dd>
        <dd><a href="#">厦门</a></dd>
        <dd><a href="#">丽江</a></dd>
      </dl>
    </li>
    <li><a href="">合作案例</a></li>
    <li><a href="">关于我们</a></li>
  </ul>
</nav>
<!-- 导航结束 -->

# 7、鼠标移入二级菜单右移动画

GIF2025-1-1317-03-04

  • 利用 a 标签的伪类 ::before 来制作 菜单前面的小竖线,一开始使用 display:none 隐藏小竖线
  • 当鼠标移入到 dd 元素上时,让 a 的伪类 ::before 显示,同时,a 标签向右移动 10px
  • 给 a 标签加上 过渡动画,这样 a 标签向右移动时就有过渡动画效果
ul.menu li .menu-dropdown dd a {
  /* 省略了部分css 具体见前面 */
  position: relative; /* 开启定位,为伪元素定位做准备 */
  transition: transform 0.5s; /* 为属性添加过渡动效果 */
}

/* 制作竖线 */
.menu-dropdown dd a::before {
  content: "";
  position: absolute;
  top: 7px;
  bottom: 7px;
  left: 10px;
  width: 2px;
  background-color: var(--primary--color);
  display: none; /* 初始状态隐藏 */
}

/* 鼠标移入 a标签向右移动 10px */
.menu-dropdown dd:hover a {
  transform: translateX(10px);
}
/* 鼠标移入显示竖线 */
.menu-dropdown dd:hover a::before {
  display: block;
}

# 8、实现吸顶盒导航效果

GIF2025-1-1317-44-37

分析吸顶盒导航效果

  • 刚开始进到页面,导航就显示在他原来的位置(浏览器的顶部)。
  • 当浏览器的滚动条的滚动距离 > 1 时
    • 元素先移出屏幕,然后再从屏幕是上方淡入移下来,最后固定在浏览器顶部
    • 导航最外层容器背景变为白色,同时添加向下的阴影

吸顶盒导航实现原理

  • 我们可以定义一个 sticky class 类,来实现导航移入动画。
.sticky {
  width: 100%; /* 这个宽度一定要加 ,元素定位后,为行内块元素特性 */
  position: fixed;
  top: 0;
  box-shadow: 0 2px 5px #ddd; /* 添加阴影 */
  background-color: var(--white-color); /* 背景颜色 */
  animation: sticky-animation ease-out 0.5s both; /* 动画效果 */
}
/* 定义帧动画 */
@keyframes sticky-animation {
  0% {
    opacity: 0; /* 初始状态透明度为0 */
    transform: translateY(-100%); /* 初始状态向上移动自身距离 */
  }

  100% {
    opacity: 1; /* 结束状态透明度为1 */
    transform: translateY(0); /* 结束状态向上移动0 */
  }
}
  • 当浏览器滚动条滚动距离 > 1 时,给class='top' 的元素添加 sticky class ,实现盒子从上慢慢移下的效,否则,将 sticky class 从元素身上移除
<!-- top开始 -->
<!--
给 class="top" 元素添加 `id="J_top"` 方便我们通过 JS获取到该元素.
同时我们只要看到页面中元素的 id名是以大写字母 J_ 开头的,就知道当前元素有JS操作他。
-->
<div class="top" id="J_top">
  <!--- 省略部分 html 结构 -->
</div>
<!-- top开始 -->
/* 实现吸顶盒导航 */
function stickyMenu() {
  // 获取 top元素
  const _top = document.getElementById("J_top");
  // 监听浏览器的滚动事件
  window.addEventListener("scroll", function () {
    // 获取当前滚动条滚动的距离
    let scrollTop =
      document.documentElement.scrollTop || document.body.scrollTop;
    if (scrollTop > 1) {
      // 就给 top 元素添加 sticky 类名
      _top.classList.add("sticky");
    } else {
      _top.classList.remove("sticky");
    }
  });
}
stickyMenu(); // 调用函数

# 9、实现导航响应式效果

GIF2025-1-1318-18-36

当页面宽度 <=992px 时,导航隐藏,在最右侧显示 image-20250113175039595 图标

  • 通过媒体查询,在检测 页面视口宽 <=992px 时,将导航隐藏 (以下代码要放在所有 css 之后)
/* 以下 css  要写在所有页面样式之后 */
@media screen and (max-width: 992px) {
  /* 最后是隐藏整个 nav 标签,而不只是 ul
    .top ul.menu {
    display: none 
}
    */

  .top nav {
    display: none; /* 隐藏导航 */
  }
}
  • class='logo' 元素的后面,添加 image-20250113175039595 图标
  • 图标采用 iconfont 图标
<!-- 引入字体图标样式 -->
<link rel="stylesheet" href="./iconfont/iconfont.css" />
<!-- 引入全局样式 -->

<style>
  /* 控制栏目图标样式 */
  .nav-button {
    width: 30px;
    height: 30px;
    text-align: center;
    line-height: 30px;
    font-size: 24px;
    margin-right: 10px;
    display: none; /* 一开始隐藏图标 */
    cursor: pointer;
  }
</style>

<!-- logo开始 -->
<!--   省略部分html ,具体见前面 -->
<!-- logo开始 -->

<!-- 栏目图标开始 -->
<div class="nav-button iconfont icon-lanmu"></div>
<!-- 栏目图标结束 -->
  • 一开始图标是隐藏的,当 页面视口宽 <=992px 再将图标显示 ( 以下代码要放在所有 css 之后)
@media screen and (max-width: 992px) {
  .top nav {
    display: none; /* 隐藏导航 */
  }

  .nav-button {
    display: block; /* 显示导航按钮 */
  }
}

# 10、完整版代码

项目目录结构

response-web
├─ .gitignore
├─ css
│  ├─ global.css
│  ├─ index.css
│  ├─ media.css
│  ├─ reset.css
│  ├─ response.css
│  └─ skin-theme.css   // 主题
├─ iconfont
│  ├─ demo.css
│  ├─ demo_index.html
│  ├─ iconfont.css
│  ├─ iconfont.js
│  ├─ iconfont.json
│  ├─ iconfont.ttf
│  ├─ iconfont.woff
│  └─ iconfont.woff2
├─ images  // 图片内容省略
├─ index.html
├─ js
│  └─ menu.js
└─ README.md

index.html 网站首页

<!DOCTYPE html>
<!-- data-theme="yellow" -->
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>兼容多终端的响应式网站</title>
    <!-- 清除标签默认样式 -->
    <link rel="stylesheet" href="./css/reset.css" />
    <!-- 主题皮肤样式表 -->
    <link rel="stylesheet" href="./css/skin-theme.css" />
    <!-- 字体图标样式表 -->
    <link rel="stylesheet" href="./iconfont/iconfont.css" />
    <!-- 全局样式表 -->
    <link rel="stylesheet" href="./css/global.css" />
    <!-- 首页页面样式表 -->
    <link rel="stylesheet" href="./css/index.css" />
    <!-- 媒体查询 响应式栅格系统样式表 -->
    <link rel="stylesheet" href="./css/media.css" />
    <link rel="stylesheet" href="./css/response.css" />
  </head>
  <body>
    <header class="header">
      <div class="header-content">
        <!-- 网站头部开始 -->
        <div class="top" id="J_top">
          <div class="top-content layout">
            <!-- logo开始 -->
            <div class="logo">
              <a href="#">
                <img src="./images/logo.png" alt="艾编程logo" width="118" />
              </a>
            </div>
            <!-- logo结束 -->

            <!-- 栏目小图标 开始 -->
            <div class="nav-button iconfont icon-lanmu"></div>
            <!-- 栏目小图标 结束 -->
            <!-- 导航开始 -->
            <nav>
              <ul class="menu">
                <li>
                  <a href="">网站首页</a>
                </li>
                <li>
                  <a href="">国内游</a>
                  <dl class="menu-dropdown">
                    <dd><a href="#">北京</a></dd>
                    <dd><a href="#">三亚</a></dd>
                    <dd><a href="#">广东</a></dd>
                    <dd><a href="#">厦门</a></dd>
                    <dd><a href="#">云南</a></dd>
                  </dl>
                </li>
                <li>
                  <a href="">出镜游</a>
                  <dl class="menu-dropdown">
                    <dd><a href="#">泰国</a></dd>
                    <dd><a href="#">日本</a></dd>
                    <dd><a href="#">美国</a></dd>
                    <dd><a href="#">台湾</a></dd>
                    <dd><a href="#">海岛</a></dd>
                  </dl>
                </li>
                <li>
                  <a href="">周边游</a>
                  <dl class="menu-dropdown">
                    <dd><a href="#">北海</a></dd>
                    <dd><a href="#">桂林</a></dd>
                    <dd><a href="#">张家界</a></dd>
                    <dd><a href="#">凤凰</a></dd>
                  </dl>
                </li>
                <li>
                  <a href="">主题游</a>
                  <dl class="menu-dropdown">
                    <dd><a href="#">游学</a></dd>
                    <dd><a href="#">自组</a></dd>
                  </dl>
                </li>
                <li>
                  <a href="">自由行</a>
                  <dl class="menu-dropdown">
                    <dd><a href="#">三亚</a></dd>
                    <dd><a href="#">厦门</a></dd>
                    <dd><a href="#">丽江</a></dd>
                  </dl>
                </li>
                <li><a href="">合作案例</a></li>
                <li><a href="">关于我们</a></li>
              </ul>
            </nav>
            <!-- 导航结束 -->
          </div>
        </div>
        <!-- 网站头部结束 -->
      </div>
    </header>

    <!--js 代码一定要放在 /body 的最前-->
    <script src="./js/menu.js"></script>
  </body>
</html>
  • ./css/skin-theme.css 不同主题样式
/* 主题皮肤定制 */
/* 默认主题写在上面 */

/* 绿色主题 */
:root,
html[data-theme="turquoise"] {
  /* 主色调 */
  --primary--color: #1ec28b; /* 主体色 */

  --light-gray-color: #f4f4f6; /* 浅灰色 */
  --white-color: #fff; /* 白色 */

  /* 字体色 */
  --font-black-color: #000; /* 黑色 */
  --font-dark-grey-color: #666; /* 深灰色 */
  --font-red-color: #ff0000; /* 红色 */
  --font-white-color: #fff; /* 白色 */

  /* 边框色 */
  --border-dark-grey-color: rgba(28, 28, 27, 0.22); /* 深灰色边框 */

  /* 辅助色 */
  --sub-dark-red-color: #ff6347; /* 深红色 */
  --sub-deep-orange-color: #ff8900; /* 深橘红色 */

  /* 网站底部色 */
  --footer-bg-color1: #2e2e2e; /* 网站底部背景色 */
  --footer-bg-color2: #212121; /* 网站底部背景色 */
  --footer-font-color: #d6d6d6; /* 网站底部背景色 */
}

/* 黄色主题 */
html[data-theme="yellow"] {
  /* 主色调 */
  --primary--color: #ffd336; /* 主体色 */
  --light-gray-color: #f4f4f6; /* 浅灰色 */
  --white-color: #fff; /* 白色 */

  /* 字体色 */
  --font-black-color: #000; /* 黑色 */
  --font-dark-grey-color: #666; /* 深灰色 */
  --font-red-color: #ff0000; /* 红色 */
  --font-white-color: #fff; /* 白色 */

  /* 边框色 */
  --border-dark-grey-color: rgba(28, 28, 27, 0.22); /* 深灰色边框 */

  /* 辅助色 */
  --sub-dark-red-color: #ff6347; /* 深红色 */
  --sub-deep-orange-color: #ff8900; /* 深橘红色 */

  /* 网站底部色 */
  --footer-bg-color1: #2e2e2e; /* 网站底部背景色 */
  --footer-bg-color2: #212121; /* 网站底部背景色 */
  --footer-font-color: #d6d6d6; /* 网站底部背景色 */
}

/* 深红色主题 */
html[data-theme="red"] {
  /* 主色调 */
  --primary--color: #ff383f; /* 主体色 */
  --light-gray-color: #f4f4f6; /* 浅灰色 */
  --white-color: #fff; /* 白色 */

  /* 字体色 */
  --font-black-color: #000; /* 黑色 */
  --font-dark-grey-color: #666; /* 深灰色 */
  --font-red-color: #ff0000; /* 红色 */
  --font-white-color: #fff; /* 白色 */

  /* 边框色 */
  --border-dark-grey-color: rgba(28, 28, 27, 0.22); /* 深灰色边框 */

  /* 辅助色 */
  --sub-dark-red-color: #ff6347; /* 深红色 */
  --sub-deep-orange-color: #ff8900; /* 深橘红色 */

  /* 网站底部色 */
  --footer-bg-color1: #2e2e2e; /* 网站底部背景色 */
  --footer-bg-color2: #212121; /* 网站底部背景色 */
  --footer-font-color: #d6d6d6; /* 网站底部背景色 */
}
  • ./css/reset.css 清除标签默认样式
/* 清除默认样式*/
html,
body,
ul,
li,
dl,
dt,
dd {
  margin: 0;
  padding: 0;
}
li {
  list-style: none;
}
a {
  text-decoration: none; /* 去掉 a标签默认的下划线 */
}
  • ./css/global.css 全局通用样式
/* 全局通用样式 */
body {
  font: 16px/1.5 "微软雅黑";
  color: #000;
}

.layout {
  max-width: 1200px; /* 最大宽度 */
  margin: 0 auto;
}
  • ./css/response.css 响应式样式
/* 当浏览器的宽度小于等于 992px */
@media screen and (max-width: 992px) {
  /* 隐藏顶部导航 */
  .top nav {
    display: none;
  }
  /* 显示栏目图标 */
  .nav-button {
    display: block;
  }
}
  • ./css/index.css 首页样式
/* 当前项目首页用到的 css 样式 */
.header {
  width: 100%;
  height: 758px;
  /* background-image: linear-gradient(
    to bottom,
    var(--primary--color),
    var(--white-color)
    ); */
  /* opacity: 0.4; 设置元素透明 */
  position: relative; /* 相对定位 */
}
.header::before,
.header-content {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.header::before {
  content: "";
  background-image: linear-gradient(
    to bottom,
    var(--primary--color),
    var(--white-color)
  );
  opacity: 0.4;
}

.top {
  /* width:100%; */
  height: 80px;
  /* background-color: #ddd; */
  /* border: 1px solid red; */
}

.top .top-content {
  height: inherit;
  /* background-color: red; */
  display: flex;
  align-items: center;
  justify-content: space-between; /* 水平两端对齐 */
}
.top .top-content ul.menu {
  display: flex;
}

/* 一级导航样式 */
ul.menu li a {
  display: block;
  line-height: 80px;
  padding-right: 16px;
  padding-left: 15px;
  color: var(--font-black-color);
  font-size: 18px;
  /* border: 1px solid red; */
}
ul.menu li a:hover {
  color: var(--primary--color);
}
ul.menu li {
  position: relative; /* 相对定位 */
}
ul.menu li::after {
  content: "";
  position: absolute;
  bottom: 0px;
  left: 50%;
  right: 50%;
  height: 2px;
  background-color: var(--primary--color);
  transition: left 0.3s, right 0.3s;
}
ul.menu li:hover::after {
  left: 0;
  right: 0;
}

/* 二级下拉菜单 */
ul.menu li .menu-dropdown {
  position: absolute;
  left: 0;
  right: 0;
  top: 80px;
  background-color: var(--white-color);
  transform-origin: top center; /* 变换的原点在顶部的中心 */
  transform: scaleY(0); /* y轴缩小到0 看不到了*/
  transition: transform 0.3s; /* 过渡效果 */
}
ul.menu li .menu-dropdown {
  padding-top: 2px;
  padding-bottom: 8px;
}
ul.menu li .menu-dropdown dd {
  overflow: hidden;
}
ul.menu li .menu-dropdown dd a {
  line-height: 30px;
  padding: 0px;
  text-align: center;
  /* border: 1px solid red; */
  position: relative; /* 相对定位 */
  transition: transform 0.5s;
}
ul.menu li:hover .menu-dropdown {
  transform: scaleY(1); /* y轴放大到1 显示出正常大小 */
}
/* 绘制小竖线 */
.menu-dropdown dd a::before {
  content: "";
  position: absolute;
  top: 7px;
  bottom: 7px;
  left: 10px;
  width: 2px;
  background-color: var(--primary--color);
  display: none; /*隐藏*/
}
/* 鼠标移入 a标签向右移动 10px */
.menu-dropdown dd:hover a {
  transform: translateX(10px);
}
/* 鼠标移入显示竖线 */
.menu-dropdown dd:hover a::before {
  display: block;
}
.sticky {
  width: 100%;
  position: fixed;
  top: 0;
  background-color: var(--white-color);
  box-shadow: 0 2px 5px var(--border-dark-grey-color);
  /* 定义 animation 动画 */
  animation: sticky-animation 0.5s ease-out both;
}

@keyframes sticky-animation {
  0% {
    opacity: 0; /* 透明的 */
    transform: translateY(-100%); /* 向上移动自身的高度 */
  }
  100% {
    opacity: 1; /* 不透明 */
    transform: translateY(0); /* 向下移动到自身的高度 */
  }
}

/* 栏目图标样式 */
.nav-button {
  width: 34px;
  /* background: red; */
  margin-right: 10px;
  font-size: 30px;
  text-align: center;
  display: none;
}
  • ./js/menu.js 实现吸顶盒导航
function stickyMenu() {
  // 获取 top元素
  const _top = document.getElementById("J_top");
  // 监听浏览器的滚动事件
  window.addEventListener("scroll", function () {
    // 获取当前滚动条滚动的距离
    let scrollTop =
      document.documentElement.scrollTop || document.body.scrollTop;
    if (scrollTop > 1) {
      // 就给 top 元素添加 sticky 类名
      _top.classList.add("sticky");
    } else {
      _top.classList.remove("sticky");
    }
  });
}
stickyMenu(); // 调用函数

# 11、修复 bug 一

当页面宽度 <=1200px 时 ,logo 图标与浏览器左边添加 25px 的间距

/* 以下代码放入 response.css 文件中 */
@media screen and (max-width: 1200px) {
  .logo {
    margin-left: 25px;
    /* 如果需要过渡自然的活,可以加上过渡动画 */
  }
}

# 12、修复 bug 二

栏目小图标按设计稿要求,他与浏览器左边的间距为 30px

.nav-button {
  /* 省略部分css -- */
  margin-right: 30px; /* 这里的右边距为 30px */
  cursor: pointer; /* 鼠标样式-手指形状 */
}

# 五、垂直二级导航开发

TIP

  • 当浏览器屏幕缩小到 <=992px 时,水平导航会隐藏,从而在最右侧显示栏目小图标 image-20250113175039595。(这个功能我们在上一小节《网站顶部导航》已经实现了)。
  • 点击右侧栏目小图标 image-20250113175039595 ,会显示对应的二级菜单 ,效果如下

以下是具体的实现步骤

# 1、第一步:实现黑色半透明遮罩层

image-20250114121218995

黑色半透明遮罩层特点

  • 黑色半透明遮罩层的大小与浏览器屏幕一样大小,并且覆盖在页面其它内容的最上面。
  • 当我们滚动浏览器的滚动条时,黑色半透明遮罩层位置始终相对于浏览器固定不动。

通过上面的分析,我们知道,黑色半透明遮罩层是相对于浏览器固定定位,定位在浏览器的左上角。

# 1.1、具体实现

TIP

  • 创建一个 class 类名为 mask 的 div 作为 body 标签 的子元素,将其宽高设置与浏览器一样大小,同时背景为黑色半透明
  • 利用 fixed 固定定位,将其定位在浏览器的左上角
<style>
  /*  黑色半透明遮罩层 */
  .mask {
    position: fixed; /* 固定定位 */
    /* 
        top 与 bottom 拉伸盒子高与浏览器一样高
        left 与 right 拉伸盒子宽与浏览器一样高
        */
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;

    /* 这里的黑色半透明可以直接用 ` rgba(0,0,0,.5)` 实现 ,因为在任何主题下,遮罩层都是黑色的半透明的,不会被改成其它颜色*/
    background-color: rgba(0, 0, 0, 0.3);
    /* 一般我们会给黑色的半透明遮罩层添加  保证他会在的所有其它元素的最上面显示,不过在这个项目中,垂直菜单容器要在他上面显示*/
    z-index: 99;
  }
</style>

<!-- 黑色半透明遮罩层 开始 -->
<div class="mask"></div>
<!-- 黑色半透明遮罩层 结束 -->

# 2、第二步:制作垂直导航最外层黑色半透明容器

image-20250114123639915

  • 制作一个黑色的半透明遮罩层,宽 420px ,高同浏览器一样高。
  • 利用固定定位,将遮罩层定位在浏览器顶部的最左边

温馨注意:

  • 虽然 .mobile-nav 元素是放在绝对定位元素 .header-content 中,但他还是在默认的 html 层叠上下文中。因为 .header-content 是的 z-index 值是 auto,并不会创建自己的层叠上下文。
  • .mask 黑色半透明遮罩层是在 html 层叠上下文中,他写在</body>的前面,所以会覆盖在.mobile-nav 黑色半透明遮罩层上面,我们需要给.mobile-nav 元素 设置 z-index 来提升他的层级
<style>
  /* 移动端或 ipad 端 导航*/
  nav.mobile-nav {
    width: 420px;
    position: fixed; /* 固定定位 */
    left: 0;
    top: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.3);
    z-index: 100; /* 提升层级,让他显示在黑色半透明遮罩层的上面,这里的值一定要大于 .mask 选择器中 z-index的值  */
  }
</style>

<!-- 以下代码放在 class="header-content" div 容器中 -->

<!-- 移动 或 ipad端导航 开始-->
<nav class="mobile-nav"></nav>
<!-- 移动 或 ipad端导航 结束-->

# 3、第三步、制作导航关闭按扭

image-20250114130753435

  • 制作一个宽高为 50px 的小正方形,相对父元素 .mobile-nav 绝对定位,定位在与父元素上和右都为 30px 的位置
  • 利用 iconfont 阿里图标,给元素添加 关闭按扭的图标,将图标颜色设为白色

温馨提示

这里一定要将 line-height 行高的值设为 1 ,否则就会继承 body 选择器中的 line-height 值 1.5

<style>
  .colse-button {
    position: absolute;
    top: 30px;
    right: 30px;
    width: 50px;
    height: 50px;
    font-size: 50px;
    line-height: 1; /* 记得将行高设为 1 */
    color: #fff;
    cursor: pointer;
  }
</style>

<!-- 移动 或 ipad端导航 开始-->
<nav class="mobile-nav">
  <!-- 关闭按扭用的 iconfont 图标 -->
  <span class="colse-button iconfont icon-chacha1"></span>
</nav>
<!-- 移动 或 ipad端导航 结束-->

# 4、第四步:垂直二级导航的显示与隐藏

GIF2025-1-1418-08-20

  • 一开始遮罩层和移动导航容器都是隐藏的
/* 黑色半透明遮罩层 */
.mask {
  display: none;
}

/* 移动端或 ipad 端 导航*/
nav.mobile-nav {
  display: none;
}
  • 给栏目小图标添加 id="J_nav-icon"方便 JS 获取该元素
  • 给关闭按扭添加id='J_close-button'方便 JS 获取该元素
  • 给全屏的黑色半透明遮罩层添加 id="J_mask" ,方便 JS 获取该元素
  • 给垂直菜单添加 id="J_mobile-nav" ,方便 JS 获取该元素
<!-- 栏目小图标 开始 -->
<div class="nav-button iconfont icon-lanmu" id="J_nav-icon"></div>

<!-- 关闭按扭用的 iconfont 图标 -->
<span class="colse-button iconfont icon-chacha1" id="J_close-button"></span>

<!-- 黑色半透明遮罩层 开始 -->
<div class="mask" id="J_mask"></div>

<!-- 移动 或 ipad端导航 开始-->
<nav class="mobile-nav" id="J_mobile-nav"></nav>
  • JS 实现点击 栏目小图标显示菜单和黑色半透明遮罩层
  • JS 实现点击关闭按扭,关闭 垂直导航和黑色半透明遮罩层
// 获取栏目小图标
const navIcon = document.getElementById("J_nav-icon");
// 获取关闭菜单按扭
const closeButton = document.getElementById("J_close-button");
// 获取遮罩层
const mask = document.getElementById("J_mask");
// 获取垂直导航
const mobileNav = document.getElementById("J_mobile-nav");

// 给栏目小图标添加点击事件
navIcon.addEventListener("click", function () {
  // 显示黑色半 透明遮罩层
  mask.style.display = "block";
  // 显示菜单
  mobileNav.style.display = "block";
});

// 给关闭菜单按扭添加点击事件
closeButton.addEventListener("click", function () {
  // 关闭菜单
  mask.style.display = "none";
  mobileNav.style.display = "none";
});

# 5、第五步:添加一级菜单

image-20250114142558613

  • 设置菜单项与黑色遮罩层顶部,左边,右边的间距
  • 设置每一个菜单项的字体样式 和 间距
<style>
  /* 清除 h3 标签的默认外边距 */
  h3 {
    margin: 0;
    padding: 0;
  }
  .nav-list {
    padding-top: 92px;
    margin: 0px 30px;
  }
  .nav-list li h3,
  .nav-list li > a {
    display: block;
    font-size: 32px;
    color: #fff;
    font-weight: 400;
    margin-bottom: 20px;
    /* 行高 32 *1.5= 48px  通过行高 推出高为 48px */
    /* height: 48px; */
    cursor: pointer;
  }
</style>

<!-- 移动 或 ipad端导航 开始-->
<nav class="mobile-nav">
  <span class="colse-button iconfont icon-chacha1"></span>
  <ul class="nav-list">
    <li><a href="#">首页</a></li>
    <li><h3>国内游</h3></li>
    <li><h3>出镜游</h3></li>
    <li><h3>周边游</h3></li>
    <li><h3>主题游</h3></li>
    <li><h3>自由行</h3></li>
    <li><h3>合作案例</h3></li>
    <li><h3>关于我们</h3></li>
  </ul>
</nav>
<!-- 移动 或 ipad端导航 结束-->

# 6、第六步:为一级菜单添加向右的箭头

image-20250114144428631

  • 利用 span 标签 和 iconfont 图标来实现向右的箭头。
  • 箭头相对于父元素绝对定位,定位到距离父元素右侧 31px ,垂直居中位置
<style>
  .nav-list li h3 {
    position: relative; /* 相对定位 */
  }
  .nav-list li h3 span.arrow {
    /* 不要设置宽和高 阿里图标形成的图片本身占居空间是一个正方形*/
    position: absolute; /* 绝对定位 */
    right: 31px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 28px;
    line-height: 1; /* 行高为 1 */
  }
</style>

<!-- 移动 或 ipad端导航 开始-->
<nav class="mobile-nav">
  <span class="colse-button iconfont icon-chacha1"></span>
  <ul class="nav-list">
    <li><a href="#">首页</a></li>
    <li>
      <h3>
        国内游
        <span class="arrow iconfont icon-youjiantou2"></span>
      </h3>
    </li>
    <li>
      <h3>出镜游<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li>
      <h3>周边游<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li>
      <h3>主题游<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li>
      <h3>自由行<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li><h3>合作案例</h3></li>
    <li><h3>关于我们</h3></li>
  </ul>
</nav>
<!-- 移动 或 ipad端导航 结束-->

# 7、第七步:二级菜单展开始,箭头朝下的实现原理

image-20250114145615069

当点击按扭,展开二级菜单时,我们给 span 标签再添加一个为 arrow-up 的 class 类名,这个类名用来控制箭头向下。

<style>
  .nav-list li h3 span.arrow {
    /* 省略部分 CSS 样式,具体见前面*/
    transition: transform 0.3s; /* 添加过渡动画 */
  }
  .nav-list li h3 span.arrow-down {
    /* 在原来位移的基础上,再顺时间旋转 90deg */
    transform: translateY(-50%) rotate(90deg);
  }
</style>

<h3>
  国内游
  <span class="arrow iconfont icon-youjiantou2 arrow-down"></span>
</h3>

# 8、第八步:添加二级菜单

image-20250114151645161

温馨提示

在一级菜单中,我们给 a 标签添加的样式,会影响到这里,所以记得去做相关修改

<style>
  /* 垂直二级菜单 */
  .mobile-nav .nav-list li .menu-dropdown dd a {
    display: block;
    height: 64px;
    line-height: 64px;
    font-size: 32px;
    margin-bottom: 0px;
    color: #fff;
    text-indent: 64px;
  }
  .mobile-nav .nav-list li .menu-dropdown dd a:hover {
    background-color: var(--primary-color);
  }
</style>

<!-- 移动 或 ipad端导航 开始-->
<nav class="mobile-nav">
  <span class="colse-button iconfont icon-chacha1"></span>
  <ul class="nav-list">
    <li><a href="#">首页</a></li>
    <li>
      <h3>
        国内游
        <span class="arrow iconfont icon-youjiantou2 arrow-down"></span>
      </h3>
      <dl class="menu-dropdown">
        <dd><a href="#">北京</a></dd>
        <dd><a href="#">三亚</a></dd>
        <dd><a href="#">广东</a></dd>
        <dd><a href="#">厦门</a></dd>
        <dd><a href="#">云南</a></dd>
      </dl>
    </li>
    <li>
      <h3>出镜游<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li>
      <h3>周边游<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li>
      <h3>主题游<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li>
      <h3>自由行<span class="arrow iconfont icon-youjiantou2"></span></h3>
    </li>
    <li><h3>合作案例</h3></li>
    <li><h3>关于我们</h3></li>
  </ul>
</nav>
<!-- 移动 或 ipad端导航 结束-->

# 9、第九步:二级菜单展开 和 收缩 动画的实现原理

image-20250114154454675

  • 当点击一级菜单,会向下慢慢展开显示二级菜单,本质是在不断的改变 li元素的高度。
  • 一开始,**每个 li 的高度= h3 元素高 + margin-bottom 值 ** ,然后针对溢出 li 部分隐藏。
  • 当点击一级菜单时,动态计算当前li 的高度,然后修改 li 的高度。

每个 li 展开后高 = h3 元素高 + 20px 的 margin-bottom + dl 元素的占位高

li 展开后的高度,需要通过 JS 来自动计算。

.nav-list li {
  height: 68px;
  overflow: hidden;
  transition: height 0.3s; /* 为高度属性添加过渡动画 */
}
<!-- 移动 或 ipad端导航 开始-->
<nav class="mobile-nav">
  <span class="colse-button iconfont icon-chacha1"></span>
  <ul class="nav-list">
    <li><a href="#">首页</a></li>
    <li>
      <h3>
        国内游
        <span class="arrow iconfont icon-youjiantou2 arrow-down"></span>
      </h3>
      <dl class="menu-dropdown">
        <dd><a href="#">北京</a></dd>
        <dd><a href="#">三亚</a></dd>
        <dd><a href="#">广东</a></dd>
        <dd><a href="#">厦门</a></dd>
        <dd><a href="#">云南</a></dd>
      </dl>
    </li>
    <li>
      <h3>出镜游<span class="arrow iconfont icon-youjiantou2"></span></h3>
      <dl class="menu-dropdown">
        <dd><a href="#">泰国</a></dd>
        <dd><a href="#">日本</a></dd>
        <dd><a href="#">美国</a></dd>
        <dd><a href="#">台湾</a></dd>
        <dd><a href="#">海岛</a></dd>
      </dl>
    </li>
    <li>
      <h3>周边游<span class="arrow iconfont icon-youjiantou2"></span></h3>
      <dl class="menu-dropdown">
        <dd><a href="#">北海</a></dd>
        <dd><a href="#">桂林</a></dd>
        <dd><a href="#">张家界</a></dd>
        <dd><a href="#">凤凰</a></dd>
      </dl>
    </li>
    <li>
      <h3>主题游<span class="arrow iconfont icon-youjiantou2"></span></h3>
      <dl class="menu-dropdown">
        <dd><a href="#">游学</a></dd>
        <dd><a href="#">自组</a></dd>
      </dl>
    </li>
    <li>
      <h3>自由行<span class="arrow iconfont icon-youjiantou2"></span></h3>
      <dl class="menu-dropdown">
        <dd><a href="#">三亚</a></dd>
        <dd><a href="#">厦门</a></dd>
        <dd><a href="#">丽江</a></dd>
      </dl>
    </li>
    <li><h3>合作案例</h3></li>
    <li><h3>关于我们</h3></li>
  </ul>
</nav>
<!-- 移动 或 ipad端导航 结束-->

# 10、第十步:JS 实现二级菜单特效 - 事件委托

TIP

  • 当我们点击 h3 标签时,才会展开和收缩二级菜单,不过这里我们不打算给 h3 绑定点击事件。
  • 我们采用事件委托,将 h3 的点击事件委托给到他们共同的父元素 ul 来实现。

具体实现步骤

  • <ul class="nav-list"> 添加id="J_mobile-nav-list" 方便通过 Id 名来获取 ul 元素
<ul class="nav-list" id="J_mobile-nav-list">
  <!-- 省略更多的 html 标签  -->
</ul>
  • 采用事件代理,将 h3 的点击事件委托给到 ul
// 获取 ul 元素
const mobileNavList = document.getElementById("J_mobile-nav-list");
// 添加点击事件
mobileNavList.addEventListener("click", function (e) {
  const target = e.target; // 获取触发事件的元素
  // 如果触发事件的元素标签名不是 h3 则 啥也不做
  if (target.tagName.toLowerCase() !== "h3") return;

  // 如果触发事件的元素标签名是 h3,则执行这行注释之后的代码
});

# 11、第十一步: 如何判断当前菜单是展开还是折叠的

TIP

我们需要给每项菜单的 li 或 h3 标签的 S 对象添加一个属性 flag ,这个属性记录着当前的菜单状态

  • 如果 flag 值为 false 则表示当前菜单折叠
  • 如果 flag 值为 true 则表示当前菜单是展开的
// 获取 ul 元素
const mobileNavList = document.getElementById("J_mobile-nav-list");
// 添加点击事件
mobileNavList.addEventListener("click", function (e) {
  const target = e.target; // 获取触发事件的元素
  if (target.tagName.toLowerCase() !== "h3") return;

  // ---------------------- 新增JS ----------------------------------
  // 如果 target.flag 值为 false 或 undefined
  if (!target.flag) {
    // 当前菜单是折叠的,则可以展开
    // 更新菜单状态
    target.flag = true;
    alert("可以展开");
    // 菜单展开,本质就是动态修改 li 的高度,
    // 同给 span.arrow  元素添加 class 类名 arrow-down
  } else {
    // 当前菜单是展开的,可以折叠
    // 更新菜单状态
    target.flag = false;
    alert("可以折叠");
  }
});

# 12、第十二步:实现菜单展开和折叠

GIF2025-1-1417-53-04

菜单展开

  • 通过 JS 计算当前 li 展开后的高度 ,然后修改当前 li 的高

    • li 展开后高 = h3 的高 + h3 元素的 margin-bottom 值 + dl 元素的高(dl 元素没有外边距)
  • h3 标签中的 span 元素添加 class 类名 arrow-down ,实现 箭头向下

菜单收缩

  • li 收缩后高度 = h3 的高 + h3 元素的 margin-bottom 值
  • 移除 h3 标签中的 span 元素的 class 类名 arrow-down ,实现箭头向右
// 获取 ul 元素
const mobileNavList = document.getElementById("J_mobile-nav-list");
// 添加点击事件
mobileNavList.addEventListener("click", function (e) {
  const target = e.target; // 获取触发事件的元素
  if (target.tagName.toLowerCase() !== "h3") return;

  // 如果 target.flag 值为 false 或 undefined
  if (!target.flag) {
    // 当前菜单是折叠的,则可以展开
    // 更新菜单状态
    target.flag = true;

    // ---------------------- 新增JS ----------------------------------
    // 获取 h3 标签可视高 (包括 height +padding +border)
    const h3Height = target.offsetHeight;
    // 获取 h3 标签的向下外边距
    const marginBottom = parseInt(window.getComputedStyle(target).marginBottom);
    // 获取当前菜单二级菜单 dl 可视高 (dl 没有外边距)
    const dlHeight = target.nextElementSibling.offsetHeight;
    // 计算得到当前 li的高度
    const liHeight = h3Height + marginBottom + dlHeight;
    // 更新当前菜单 li 的高度
    target.parentNode.style.height = `${liHeight}px`;
  } else {
    // 当前菜单是展开的,可以折叠
    // 更新菜单状态
    target.flag = false;

    // ---------------------- 新增JS ----------------------------------
    // 获取 h3 标签可视高 (包括 height +padding +border)
    const h3Height = target.offsetHeight;
    // 获取 h3 标签的向下外边距
    const marginBottom = parseInt(window.getComputedStyle(target).marginBottom);
    // 计算得到当前 li的高度
    const liHeight = h3Height + marginBottom;
    // 更新当前菜单 li 的高度
    target.parentNode.style.height = `${liHeight}px`;
  }
});

# 13、第十三步:垂直二级导航移动端适配

GIF2025-1-1417-51-50

移动端二级导航的各种尺寸与 ipad 端不同,我们有以下两种方式来适配

# 13.1、适配方式一

TIP

通过媒体查询来检测当前视口宽,如果视口宽<=576px 时,则重新定义 CSS 的属性值

/* 以下 css 放在 response.css 文件中 */
/* 当页面宽度小于 576px 时,对应的菜单样式 */
@media screen and (max-width: 576px) {
  /* 垂直导航容器- ipad端或移动端导航*/
  nav.mobile-nav {
    width: 280px;
  }
  /* 关闭按扭 */
  .close-button {
    width: 30px;
    height: 30px;
    font-size: 30px;
  }
  .nav-list {
    padding-top: 54px;
    margin: 0 30px;
  }
  .nav-list li h3,
  .nav-list li a {
    font-size: 18px;
    /* height:27px; */
    /* 行高 1.5 * 18 = 27px  向上 4 或 5
        */
    margin-bottom: 10px;
  }
  .nav-list li {
    height: 37px; /* 导航收缩时的高度*/
  }
  /* 
    需要动态的计算当前 li的高度 
    收缩时 li高度 = h3 高度 + h3 的margin-bottom   =64px 
    展开时 li高度=h3 高度 + h3 的margin-bottom + dl可视区高
    */

  /* 箭头样式 */
  .nav-list li h3 span.arrow {
    /* border: 1px solid red; */
    font-size: 17px;
    right: 29px;
  }

  /* 移动端 二级导航样式 */
  .mobile-nav .menu-dropdown dd a {
    display: block;
    height: 36px;
    line-height: 36px;
    text-indent: 35px;
  }
  .mobile-nav .menu-dropdown {
    /* 27-9-4 */
    padding-bottom: 14px;
  }
}

# 13.2、适配方式二

  • 在 :root 选择器中,将不同的属性值,定义成一套 CSS 自定义属性
/* 以下 css 放在 skin-theme.css 文件中 */
:root {
  --nav-width: 420px; /* 导航容器宽 */
  --close-button-width: 50px; /* 关闭按钮宽 */
  --close-button-height: 50px; /* 关闭按钮高 */
  --close-button-font-size: 50px; /* 关闭按钮字体大小 */
  --close-button-right: 30px; /* 关闭按钮距父元素右距离 */
  --close-button-top: 30px; /* 关闭按钮距父元素上距离 */
  --nav-top: 92px; /* 整个菜单距顶部距离 */
  --nav-margin-left: 30px; /* 整个菜单左外边距 */
  --nav-margin-right: 30px; /* 整个菜单右外边距*/
  --menu-item-font-size: 32px; /* 菜单项字体大小 */
  --menu-item-margin-bottom: 16px; /* 菜单项底部外边距 */
  --menu-item-height: 64px; /* 菜单项高 */
  --arrow-font-size: 28px; /* 箭头字体大小 */
  --arrow-right: 41px; /* 箭头距父元素右距离 */
  --submenu-item-height: 64px; /* 子菜单项高 */
  --submenu-item-font-size: 32px; /* 子菜单项字体大小 */
  --submenu-item-text-indent: 60px; /* 子菜单项文本缩进 */
  --submenu-item-padding-bottom: 16px; /* 子菜单项底部外边距 */
}
  • 通过媒体查询来检测当前视口宽,如果视口宽<=576px 时,则重新定义一套 CSS 自定义属性
/* 以下 css 放在 skin-theme.css 文件中 */
@media screen and (max-width: 576px) {
  :root {
    --nav-width: 280px;
    --close-button-width: 30px; /* 关闭按钮宽 */
    --close-button-height: 30px; /* 关闭按钮高 */
    --close-button-font-size: 30px; /* 关闭按钮字体大小 */
    --close-button-right: 30px; /* 关闭按钮距父元素右距离 */
    --close-button-top: 30px; /* 关闭按钮距父元素上距离 */
    --nav-top: 54px;
    --nav-margin-left: 30px; /* 整个菜单左外边距 */
    --nav-margin-right: 30px; /* 整个菜单右外边距*/
    --menu-item-font-size: 18px;
    --menu-item-margin-bottom: 10px; /* 菜单项底部外边距 */
    --menu-item-height: 37px; /* 菜单项高 */
    --arrow-font-size: 17px; /* 箭头字体大小 */
    --arrow-right: 29px;
    --submenu-item-height: 36px; /* 子菜单项高 */
    --submenu-item-line-height: 36px; /* 子菜单项高 */
    --submenu-item-font-size: 18px; /* 子菜单项字体大小 */
    --submenu-item-text-indent: 35px; /* 子菜单项文本缩进 */
    --submenu-item-padding-bottom: 14px; /* 子菜单项底部外边距 */
  }
}
  • 将 CSS 代码中的属性值替换为 var 函数引用 CSS 自定义属性值

以下 CSS 和 上面的 自定义 CSS 属性 是当前效果完整 CSS

/* 黑色半透明遮罩层样式 */
.mask {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 99; /* 遮罩层在最上层 */
  display: none; /* 隐藏*/
}

/* 垂直导航容器- ipad端或移动端导航*/
nav.mobile-nav {
  /* width: 420px; */
  width: var(--nav-width);
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 100; /* 导航要在遮罩层的上面 */
  display: none; /*隐藏*/
}
/* 关闭按扭 */
.close-button {
  display: block;
  width: var(--close-button-width);
  height: var(--close-button-height);
  font-size: var(--close-button-font-size);
  /* width: 50px; */
  /* height: 50px; */
  /* font-size: 50px; */
  line-height: 1;
  /* background-color: red; */
  color: #fff;
  position: absolute;
  /* right: 30px; */
  right: var(--close-button-right);
  /* top: 30px; */
  top: var(--close-button-top);
  cursor: pointer;
}
.nav-list {
  /* padding-top: 92px; */
  padding-top: var(--nav-top);
  /* margin: 0 30px; */
  margin: 0 var(--nav-margin-right) 0 var(--nav-margin-left);
}
.nav-list li h3,
.nav-list li a {
  display: block;
  /* font-size: 32px; */
  font-size: var(--menu-item-font-size);
  /* height:48px; */
  /* 行高 1.5 * 32 = 48px;  (48-32)/2= 8
    32-16=16px
    */
  /* margin-bottom: 16px; */
  margin-bottom: var(--menu-item-margin-bottom);
  color: #fff;
  font-weight: 400;
  cursor: pointer;
}
.nav-list li {
  /* height: 64px; 导航收缩时的高度 */
  height: var(--menu-item-height);
  overflow: hidden;
  transition: height 0.3s; /* 过渡动画 */
}
/*
需要动态的计算当前 li的高度
收缩时 li高度 = h3 高度 + h3 的margin-bottom   =64px
展开时 li高度=h3 高度 + h3 的margin-bottom + dl可视区高
*/
.nav-list li h3 {
  position: relative;
}
/* 箭头样式 */
.nav-list li h3 span.arrow {
  /* border: 1px solid red; */
  /* font-size: 28px; */
  font-size: var(--arrow-font-size);
  line-height: 1;
  position: absolute;
  /* right: 41px; */
  right: var(--arrow-right);
  top: 50%;
  transform: translateY(-50%);
}
/* 向下箭头的样式 */
.nav-list li h3 span.arrow-down {
  transform: translateY(-50%) rotate(90deg);
}

/* 移动端 二级导航样式 */
.mobile-nav .menu-dropdown dd a {
  display: block;
  /* height: 64px; */
  height: var(--submenu-item-height);
  /* line-height: 64px; */
  line-height: var(--submenu-item-line-height);
  /* text-indent: 60px; */
  text-indent: var(--submenu-item-text-indent);
  font-size: var(--submenu-item-font-size);

  margin-bottom: 0;
}
.mobile-nav .menu-dropdown {
  /* padding-bottom: 12px; */
  padding-bottom: var(--submenu-item-padding-bottom);
}
.mobile-nav .menu-dropdown dd a:hover {
  background-color: var(--primary--color);
}

# 14、完整的源码

<!-- ipad端或移动端导航 开始-->
<nav class="mobile-nav" id="J_mobile-nav">
  <span class="close-button iconfont icon-chacha1" id="J_close-button"></span>

  <ul class="nav-list" id="J_moible-nav-list">
    <li><a href="#">首页</a></li>
    <li>
      <h3>
        国内游
        <span class="arrow iconfont icon-youjiantou2"></span>
      </h3>
      <dl class="menu-dropdown">
        <dd><a href="#">北京</a></dd>
        <dd><a href="#">三亚</a></dd>
        <dd><a href="#">广东</a></dd>
        <dd><a href="#">厦门</a></dd>
        <dd><a href="#">云南</a></dd>
      </dl>
    </li>
    <li>
      <h3>
        出镜游
        <span class="arrow iconfont icon-youjiantou2"></span>
      </h3>
      <dl class="menu-dropdown">
        <dd><a href="#">泰国</a></dd>
        <dd><a href="#">日本</a></dd>
        <dd><a href="#">美国</a></dd>
        <dd><a href="#">台湾</a></dd>
        <dd><a href="#">海岛</a></dd>
      </dl>
    </li>
    <li>
      <h3>
        周边游
        <span class="arrow iconfont icon-youjiantou2"></span>
      </h3>
      <dl class="menu-dropdown">
        <dd><a href="#">北海</a></dd>
        <dd><a href="#">桂林</a></dd>
        <dd><a href="#">张家界</a></dd>
        <dd><a href="#">凤凰</a></dd>
      </dl>
    </li>
    <li>
      <h3>
        主题游
        <span class="arrow iconfont icon-youjiantou2"></span>
      </h3>
      <dl class="menu-dropdown">
        <dd><a href="#">游学</a></dd>
        <dd><a href="#">自组</a></dd>
      </dl>
    </li>
    <li>
      <h3>
        自由行
        <span class="arrow iconfont icon-youjiantou2"></span>
      </h3>
      <dl class="menu-dropdown">
        <dd><a href="#">三亚</a></dd>
        <dd><a href="#">厦门</a></dd>
        <dd><a href="#">丽江</a></dd>
      </dl>
    </li>
    <li><a href="#">合作案例</a></li>
    <li><a href="#">关于我们</a></li>
  </ul>
</nav>
<!-- ipad端或移动端导航 结束-->

<!-- 黑色半透明遮罩层开始 -->
<div class="mask" id="J_mask"></div>
<!-- 黑色半透明遮罩层结束-->
subMenu();
function subMenu() {
  // 获取栏目小图标
  const navIcon = document.getElementById("J_nav-icon");
  // 获取遮罩层
  const mask = document.getElementById("J_mask");
  // 获取垂直导航容器
  const mobileNav = document.getElementById("J_mobile-nav");

  // 获取关闭按钮
  const closeButton = document.getElementById("J_close-button");

  // 绑定一个点击事件
  navIcon.addEventListener("click", function () {
    // 让遮罩层显示
    mask.style.display = "block";
    // 让垂直菜单显示
    mobileNav.style.display = "block";
  });

  // 给关闭按钮绑定一个点击事件
  closeButton.addEventListener("click", function () {
    // 让遮罩层隐藏
    mask.style.display = "none";
    // 让垂直菜单隐藏
    mobileNav.style.display = "none";
  });

  /* 实现二级伸缩菜单 */
  // 采用事件委托的方式
  // 首先要获取 ul 元素
  const mobileNavList = document.getElementById("J_moible-nav-list");
  // 给 ul 添加 click 点击事件
  mobileNavList.addEventListener("click", function (e) {
    // 获取触发事件的元素
    const target = e.target;
    // 判断触发事件的元素的标签名是不是 h3
    if (target.tagName.toLowerCase() !== "h3") return;

    // 如果是 h3标签,则执行后面的代码
    // 我要给 h3 标签(js对象)添加一个属性,用来记录当前菜单状态
    // flag  如果 flag 的值是 false 表示菜单是收缩的
    // 如果 flag 的值是 true 表示菜单是展开的
    // 没有属性时 target.flag 就是 undefined

    if (!target.flag) {
      // 当前菜单是收缩的,那我们就可以展开他
      // 更改菜单的状态
      target.flag = true; // 把菜单的状态改为展开
      // 实现菜单的展开效果
      // 展开的本质就是动态的计算 li 的高度
      /* 
    需要动态的计算当前 li的高度 
     收缩时 li高度 = h3 高度 + h3 的margin-bottom   =64px 
    展开时 li高度=h3 高度 + h3 的margin-bottom + dl可视区高
    */ // 获取h3的可视高(height+padding+border)
      const h3Height = target.offsetHeight;
      // 获取h3的 margin-bottom
      const h3MarginBottom = parseInt(
        window.getComputedStyle(target).marginBottom
      );
      // 还要获取二级菜单 dl的可视高
      const dlHeight = target.nextElementSibling.offsetHeight;
      // 计算当前li的高度
      const liHeight = h3Height + h3MarginBottom + dlHeight;
      // 设置当前li的高度
      target.parentNode.style.height = liHeight + "px";
    } else {
      // 当前菜单是展开的
      target.flag = false; // 把菜单的状态改为收缩

      const h3Height = target.offsetHeight;
      // 获取h3的 margin-bottom
      const h3MarginBottom = parseInt(
        window.getComputedStyle(target).marginBottom
      );
      // 计算当前li的高度
      const liHeight = h3Height + h3MarginBottom;
      // 设置当前li的高度
      target.parentNode.style.height = liHeight + "px";
    }
  });
}
上次更新时间: 1/16/2025, 1:39:22 AM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X