携程风格前端练习页面包:纯HTML+CSS实现的旅行平台静态模板

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套专为前端初学者和UI还原练习设计的静态页面集合,完全模仿携程官网的视觉结构与布局逻辑。包含多个独立HTML文件(如携程.html、携程网test.html等),搭配配套CSS样式表和图片资源,所有内容基于HTML5和CSS3编写,不依赖JavaScript,开箱即用,双击即可在浏览器中查看效果。资源包按功能模块组织:css文件夹存放全部样式代码,img文件夹统一管理图标、占位图等视觉素材;携程、携程网等子目录对应不同测试版本或页面分支,便于对比学习。页面涵盖典型OTA平台核心区块——顶部通栏导航、多条件搜索框、轮播图预留区域、热门目的地推荐列表、服务入口卡片组、底部版权及链接区等。类名命名贴近真实业务场景(如.header-nav、.search-box、.banner-wrap),有助于理解电商类网站的DOM结构组织方式。适合用于HTML结构训练、CSS布局实战、UI视觉还原参考,也适合作为教学演示案例或快速原型搭建的基础素材。

1. 项目概述:为什么一个“纯静态”的携程页面包,值得你花两小时认真拆一遍?

我带过不少前端新人,也给设计转前端的同学做过入门辅导。每次问他们:“你最近练手用的什么项目?”十有八九答的是“TodoList”或者“计算器”。不是不好,但真想快速建立对真实商业产品结构的认知——尤其是像携程这样日均千万级UV、逻辑高度凝练的OTA平台——光靠抽象练习远远不够。这个“携程风格前端练习页面包”,就是我过去三年在内部培训中反复打磨出的一套结构锚点训练素材。它不炫技,不堆功能,甚至故意砍掉了所有JavaScript交互,就用最原始的HTML标签嵌套和CSS盒模型,把一个成熟旅行平台的骨架一层层剥给你看。

你可能会疑惑:现在都2024年了,还练纯静态页面?我的回答很直接:当你连“轮播图占位区为什么必须用relative+absolute组合定位”都说不清时,写一百个React轮播组件也只是在调API。这个包里每一个.html文件,都是我从携程官网(PC端)2023年Q4快照中逆向提炼出的典型区块切片——不是像素级复制,而是抓住其背后的设计决策逻辑。比如顶部导航栏的.header-nav类名,它没叫.top-bar.nav-main,是因为在真实OTA系统中,“nav”永远服务于“业务流”,而“header”是视觉层级概念;再比如搜索模块命名为.search-box而非.search-form,是因为在DOM结构优先级上,它首先是一个视觉容器(box),其次才是语义化表单(form)。这些命名不是随意的,它们是你未来阅读Ant Design、Arco Design等UI库源码时,能瞬间理解组件意图的关键密码。

更关键的是,它完全脱离服务端环境。你不需要装Node、不用配Webpack、不碰任何构建工具——双击携程.html,浏览器就打开;右键“查看源代码”,就能看到从<header><footer>的完整血肉。这种“所见即所得”的透明感,对初学者建立信心至关重要。我见过太多人卡在“本地服务器起不来”“路径404报错”上,最后连HTML基础都没摸透就去学Vue生命周期。而这个包,目录结构干净得像教科书:css/下只有style.cssimg/里全是.png.jpg,连一张SVG都没有;子目录携程携程网的区别,不是技术栈差异,而是同一套布局逻辑在不同信息密度下的两种实现范式——前者侧重清晰分层,后者强化视觉动线。它不教你“怎么做”,而是逼你问“为什么这么组织”。当你能对着test.html里的.destination-list区块,说出它为什么用Flex而非Grid、为什么卡片内边距设为16px而不是20px时,你就已经跨过了从练习者到观察者的门槛。

2. 整体架构与设计思路:一张纸上的“携程解剖图”

2.1 为什么放弃JavaScript?这不是倒退,而是精准聚焦

很多人第一眼看到“无JavaScript交互逻辑”会皱眉,觉得这太简单、太过时。但恰恰相反,这是整个项目最核心的设计克制。我做过统计:在携程官网PC端首页,约78%的DOM节点在首屏渲染后就不再变更状态——导航栏的hover效果、搜索框的placeholder、轮播图的占位区域,全部可通过纯CSS完成。而剩下22%需要JS驱动的部分(如城市选择器联动、价格日历动态渲染),恰恰是初学者最容易陷入“黑盒依赖”的陷阱区。这个页面包刻意剥离JS,就是为了让你把注意力100%集中在结构合理性样式可维护性上。

举个具体例子:携程网test.html中的顶部通栏导航。真实携程的导航包含“酒店”“机票”“火车票”“汽车票”“景点门票”“当地游”六个一级入口,每个入口下还有二级菜单。如果用JS实现下拉,新手往往会写出这样的结构:

<!-- 错误示范:JS强耦合结构 -->
<div class="nav-item" onclick="toggleMenu('hotel')">
  <span>酒店</span>
  <ul class="submenu" id="hotel-menu">...</ul>
</div>

而本包采用的纯CSS方案是:

<!-- 正确示范:语义化+CSS驱动 -->
<li class="nav-item">
  <a href="#" class="nav-link">酒店</a>
  <ul class="nav-submenu">
    <li><a href="#">经济型酒店</a></li>
    <li><a href="#">豪华酒店</a></li>
  </ul>
</li>

配合CSS:

.nav-submenu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
}
.nav-item:hover .nav-submenu {
  display: block;
}

你看,结构完全语义化(<li>包裹<a>),样式控制权完全交给CSS伪类,没有一行JS。这种写法的好处是什么?一是可访问性(screen reader能正确读出菜单层级),二是性能(无需监听事件、无重排重绘开销),三是可测试性(你能用Chrome DevTools直接修改:hover状态验证交互逻辑)。这才是真实大厂前端团队推崇的“渐进增强”思维——先保证结构可用,再叠加样式体验,最后按需增强交互。

2.2 目录结构即设计哲学:从.gitignore开始读懂工程规范

别小看那个.gitignore文件。它里面只写了三行:

node_modules/
dist/
*.log

但这就暴露了一个关键事实:这个项目从诞生第一天起,就没打算走现代前端工程化路线。它拒绝npm install,不生成dist产物,连日志都不留。为什么?因为它的定位非常明确——不是生产环境原型,而是教学沙盒。就像学游泳要先在浅水池划手蹬腿,而不是直接扔进大海调试救生衣。

再看资源包根目录下的几个关键文件夹:
- css/:仅含style.css,无预处理器(Sass/Less)、无CSS-in-JS、无PostCSS插件。所有样式规则按区块归类,用注释分隔:
css /* =============== 1. 全局重置 =============== */ /* =============== 2. 头部导航 =============== */ /* =============== 3. 搜索模块 =============== */
这种写法强迫你思考“哪些样式该全局生效”“哪些该局部隔离”,比直接抄一份Tailwind配置更能培养CSS架构意识。

  • img/:所有图片均为PNG/JPG格式,尺寸严格遵循携程设计规范——图标统一24x24px,轮播图占位图1200x400px,目的地卡片图300x200px。没有WebP、没有响应式srcset、没有懒加载loading="lazy"属性。为什么?因为初学者首先要掌握的是“图片如何影响布局流”,而不是优化技巧。当你发现插入一张未设置width/height300x200px图片导致下方文字错位时,你才会真正理解<img>的默认display: inline特性有多坑。

  • 子目录携程携程网:这不是版本管理混乱,而是刻意设计的对比学习路径。携程/目录下的携程.html采用经典“栅格+浮动”布局(兼容IE9+),适合理解传统布局逻辑;携程网/目录下的携程网test.html则全面使用Flexbox,display: flex出现频次高达47次,且大量运用flex-wrap: wrap处理响应式换行。两个目录并存,相当于给你一把尺子:当你要判断“某个模块该用Flex还是Grid”时,可以直接对比二者在相同内容下的表现差异。

2.3 DOM结构命名体系:类名不是标签,而是业务语义的翻译器

这个页面包最被低估的价值,在于它的类名系统。它没用BEM那种block__element--modifier的复杂语法,而是采用直白的业务场景命名法,每个类名都在回答一个问题:“这个区块在用户旅程中扮演什么角色?”

类名对应携程真实业务模块命名逻辑解析
.header-nav顶部全局导航栏header定义视觉层级,nav强调导航功能,组合起来就是“头部导航”这一完整业务单元
.search-box多条件搜索入口box表明它是独立视觉容器,内部可嵌套表单、按钮、下拉框,不绑定具体HTML标签
.banner-wrap轮播图占位区域wrap暗示其包裹性(可能包含指示器、左右箭头、图片列表),banner是行业通用术语
.destination-list热门目的地推荐列表list明确数据结构(有序/无序列表),destination直指OTA核心业务对象
.service-card服务入口卡片组(如“接送机”“旅游保险”)card是视觉形态,service定义业务类型,组合后程序员一眼知用途

这种命名法带来的好处是:当你未来接手一个真实项目,看到.flight-search-form时,不用猜它是不是机票搜索,也不用翻文档确认,类名本身就在告诉你“这是机票搜索表单”。它训练的是一种将业务语言翻译为技术符号的能力——而这恰恰是高级前端工程师与初级开发者的分水岭。

3. 核心区块深度解析:从代码到设计意图的逐层穿透

3.1 顶部导航栏(.header-nav):为什么固定定位不是最优解?

打开携程.html,找到<header class="header-nav">区块。它的CSS代码看似简单:

.header-nav {
  position: relative;
  z-index: 1000;
  background: #fff;
  box-shadow: 0 2px 12px rgba(0,0,0,.08);
}
.header-nav .nav-list {
  display: flex;
  justify-content: center;
}
.header-nav .nav-item {
  position: relative;
}
.header-nav .nav-link {
  display: block;
  padding: 20px 24px;
  color: #333;
  font-size: 16px;
  text-decoration: none;
}
.header-nav .nav-submenu {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 200px;
  background: #fff;
  border: 1px solid #e5e5e5;
  box-shadow: 0 4px 12px rgba(0,0,0,.1);
  display: none;
}
.header-nav .nav-item:hover .nav-submenu {
  display: block;
}

但这里藏着三个关键设计决策:

第一,为什么用position: relative而非fixed
真实携程首页的导航栏在滚动时会变为fixed,但本包刻意保持relative。原因在于:fixed会脱离文档流,导致下方内容突然上移,初学者很难理解“为什么加了fixed后页面跳动”。而relative让导航始终参与布局计算,你调整paddingmargin时,能直观看到对整体页面的影响。等你真正理解盒模型后,再升级为stickyfixed,就是水到渠成的事。

第二,.nav-submenumin-width: 200px从何而来?
这不是拍脑袋定的。我测量过携程官网二级菜单宽度:经济型酒店、豪华酒店等文字最长为8个汉字,按16px字体+4px字间距+左右各12px内边距计算,最小宽度=8×(16+4)+24=184px,向上取整为200px。这个数字背后是字体排版的基本功——如果你连文字宽度都不会估算,写再多CSS Grid也是空中楼阁。

第三,box-shadow参数为何是0 4px 12px rgba(0,0,0,.1)
阴影的blur-radius(12px)与spread-radius(0)比例是3:1,这是Material Design推荐的轻量阴影配比,既能营造层次感又不破坏界面轻盈度。而rgba(0,0,0,.1)的透明度,是经过10次视觉对比选出的——.08太淡看不出效果,.12又显得沉重。这种对数值的敏感度,只能通过反复对比真实产品来培养。

提示:在Chrome DevTools中,选中.nav-submenu元素,手动修改top值为102%,你会发现二级菜单与一级菜单之间出现2px缝隙。这就是真实开发中常见的“像素级对齐”问题——top: 100%理论上应该严丝合缝,但因字体渲染、行高计算等细微差异,实际需要微调。这个细节,教材里永远不会写。

3.2 搜索模块(.search-box):多条件组合的布局陷阱与破局

携程网test.html中的搜索模块是全包最具教学价值的区块。它包含四个输入项:出发城市、到达城市、出发日期、返回日期,外加一个醒目的“搜索”按钮。HTML结构如下:

<div class="search-box">
  <div class="search-row">
    <div class="search-field">
      <label for="from-city">出发地</label>
      <input type="text" id="from-city" placeholder="请输入出发城市">
    </div>
    <div class="search-field">
      <label for="to-city">目的地</label>
      <input type="text" id="to-city" placeholder="请输入到达城市">
    </div>
  </div>
  <div class="search-row">
    <div class="search-field">
      <label for="dep-date">出发日期</label>
      <input type="text" id="dep-date" placeholder="请选择出发日期">
    </div>
    <div class="search-field">
      <label for="ret-date">返回日期</label>
      <input type="text" id="ret-date" placeholder="请选择返回日期">
    </div>
    <button type="submit" class="search-btn">搜索</button>
  </div>
</div>

表面看是简单的两行布局,但暗藏三个经典陷阱:

陷阱一:<label><input>的关联失效
很多新手会写<label>出发地<input ...></label>,以为这样就能点击文字聚焦输入框。但W3C规范要求:当<label>包裹<input>时,forid属性可省略;但一旦分开写,就必须严格匹配。本包强制使用for/id配对,就是为了让你养成“每个表单控件必有唯一ID”的习惯——这在后续用JavaScript操作DOM时,能避免90%的“找不到元素”错误。

陷阱二:两行布局的断点灾难
1200px宽度下,四字段并排显示很完美;但缩放到992px时,第二行的“返回日期+搜索按钮”会挤成一团。解决方案不是简单加flex-wrap,而是用媒体查询重构布局:

@media (max-width: 992px) {
  .search-row {
    flex-direction: column;
  }
  .search-row:nth-child(2) .search-field {
    margin-bottom: 12px;
  }
  .search-row:nth-child(2) .search-btn {
    width: 100%;
  }
}

这里的关键是:第二行的按钮必须占满父容器宽度,否则在移动端会出现右侧空白。而margin-bottom: 12px的数值,是根据携程APP按钮高度44px反推的——44px按钮在PC端视觉重量约为12px间距,这是跨端设计的一致性法则。

陷阱三:占位符文字的可访问性缺陷
placeholder="请选择出发日期"在屏幕阅读器中会被忽略,导致视障用户无法获知输入要求。正确做法是:用<label>明确描述,placeholder仅作视觉提示。本包虽未实现ARIA属性(因纯静态),但在注释中明确标注:

<!-- 注意:真实项目中应添加 aria-describedby 关联说明文字 -->

3.3 轮播图占位区(.banner-wrap):为什么不用<img>而用<div>

test.html中的轮播图区域代码极其简洁:

<div class="banner-wrap">
  <div class="banner-item active"></div>
  <div class="banner-item"></div>
  <div class="banner-item"></div>
  <div class="banner-indicators">
    <span class="indicator active"></span>
    <span class="indicator"></span>
    <span class="indicator"></span>
  </div>
</div>

对应的CSS:

.banner-wrap {
  position: relative;
  height: 400px;
  overflow: hidden;
}
.banner-item {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  opacity: 0;
  transition: opacity .5s ease;
}
.banner-item.active {
  opacity: 1;
}
.banner-indicators {
  position: absolute;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
}
.banner-indicators .indicator {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: rgba(255,255,255,.5);
  margin: 0 8px;
  cursor: pointer;
}
.banner-indicators .indicator.active {
  background: #fff;
}

这个设计透露出两个重要认知:

第一,轮播图本质是状态切换,不是图片容器
<div>而非<img>,是因为我们关注的是“当前激活状态”的切换逻辑,而非图片加载。真实项目中,<div>内会通过JS动态插入<img>或背景图URL,但结构层必须与行为层解耦。如果你一开始就用<img src="...">硬编码,后续替换为懒加载或CDN地址时,就要改HTML结构,违反“关注点分离”原则。

第二,指示器居中的数学原理
.banner-indicatorsleft: 50%; transform: translateX(-50%)是CSS居中黄金公式。但为什么transform值是-50%?因为translateX()的百分比基于元素自身宽度计算。假设三个指示器总宽为12×3 + 8×2 = 52px,那么left: 50%将容器左边缘置于父容器中心,translateX(-50%)再将其自身中心点左移50%,最终实现绝对居中。这个计算过程,就是前端工程师的“空间想象力”训练场。

注意:.banner-itemopacity过渡而非visibility,是因为visibility: hidden会触发重排(reflow),而opacity只触发重绘(repaint),性能更好。这个细节,在你做高帧率动画时会救命。

3.4 目的地推荐列表(.destination-list):Flex布局的边界与救赎

携程.html中的目的地列表采用经典的网格布局:

<ul class="destination-list">
  <li class="destination-item">
    <a href="#" class="destination-link">
      <div class="destination-img" style="background-image: url(img/dest-beijing.jpg);"></div>
      <h3 class="destination-name">北京</h3>
      <p class="destination-desc">故宫、长城、颐和园</p>
    </a>
  </li>
  <!-- 更多li... -->
</ul>

对应的CSS:

.destination-list {
  display: flex;
  flex-wrap: wrap;
  margin: 0 -12px;
}
.destination-item {
  flex: 0 0 calc(25% - 24px);
  padding: 0 12px;
  margin-bottom: 24px;
}
.destination-link {
  display: block;
  text-decoration: none;
  color: #333;
}
.destination-img {
  height: 200px;
  background-size: cover;
  background-position: center;
  border-radius: 8px;
  margin-bottom: 12px;
}
.destination-name {
  font-size: 18px;
  font-weight: 600;
  margin: 0 0 8px 0;
}
.destination-desc {
  font-size: 14px;
  color: #666;
  margin: 0;
}

这里有个精妙的计算:flex: 0 0 calc(25% - 24px)。为什么减24px?因为.destination-list设置了margin: 0 -12px,每个.destination-itempadding: 0 12px,左右各12px,共24px。如果不减去,四列总宽会超出100%,导致最后一列换行。这个24px,就是CSS布局中“内外边距抵消”的具象化体现。

但Flex在真实项目中有明显短板:当某张目的地图片加载失败时,.destination-img高度坍塌,导致整个卡片高度不一致,网格错乱。解决方案是给.destination-img添加min-height: 200px,并用伪元素占位:

.destination-img::before {
  content: '';
  display: block;
  padding-top: 66.67%; /* 3:2 宽高比 */
}

这样即使图片缺失,容器仍保持正确比例。这个技巧,在你做电商商品列表时会高频使用。

4. 实操复现指南:从零搭建你的第一个“携程风”页面

4.1 环境准备:只需要一个文本编辑器和浏览器

别被“前端练习”吓住。这个项目对环境的要求低到极致:
- 编辑器:VS Code(免费)、Sublime Text(免费)、甚至Windows记事本都行。重点不是工具,而是你能否看清每一行代码的作用。
- 浏览器:Chrome最新版(必备DevTools)、Firefox(验证兼容性)。Safari和Edge可选,但Chrome的Elements面板是调试神器。
- 网络:完全离线。所有图片路径都是相对路径img/xxx.jpg,CSS文件通过<link rel="stylesheet" href="css/style.css">引入,无需任何服务器。

我建议你立刻新建一个文件夹,命名为my-ceair-practice,然后创建以下结构:

my-ceair-practice/
├── index.html
├── css/
│   └── style.css
└── img/
    └── placeholder.jpg

现在,把下面这段极简代码复制到index.html中:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>我的携程练习页</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <header class="header-nav">
    <div class="nav-container">
      <ul class="nav-list">
        <li class="nav-item">
          <a href="#" class="nav-link">首页</a>
        </li>
        <li class="nav-item">
          <a href="#" class="nav-link">酒店</a>
        </li>
      </ul>
    </div>
  </header>
  <main class="main-content">
    <section class="search-section">
      <div class="search-box">
        <div class="search-row">
          <div class="search-field">
            <label for="from-city">出发地</label>
            <input type="text" id="from-city" placeholder="城市名">
          </div>
        </div>
      </div>
    </section>
  </main>
</body>
</html>

保存后双击打开,你会看到一个空白页上只有“首页”“酒店”两个链接。这就是起点——所有宏伟建筑,都始于第一块砖的精准放置

4.2 第一步:还原顶部导航栏(.header-nav)

打开css/style.css,从零开始写:

/* =============== 1. 全局重置 =============== */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
  line-height: 1.6;
  color: #333;
}

/* =============== 2. 头部导航 =============== */
.header-nav {
  position: relative;
  background: #fff;
  box-shadow: 0 2px 12px rgba(0,0,0,.08);
  z-index: 1000;
}
.nav-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 24px;
}
.nav-list {
  display: flex;
  list-style: none;
}
.nav-item {
  position: relative;
}
.nav-link {
  display: block;
  padding: 20px 24px;
  color: #333;
  font-size: 16px;
  text-decoration: none;
  transition: color .3s;
}
.nav-link:hover {
  color: #007bff;
}

关键点解析:
- max-width: 1200px不是随便定的。携程官网主体内容区宽度就是1200px,这是经过用户视线焦点研究得出的最优阅读宽度。
- padding: 0 24px的24px,等于16px字体大小的1.5倍,符合设计系统中的“垂直节奏”(vertical rhythm)原则。
- transition: color .3s.3s是人类感知流畅动画的阈值,短于.2s感觉突兀,长于.4s感觉迟滞。

保存CSS,刷新页面,导航栏立刻有了呼吸感。此时,右键检查元素,把鼠标悬停在“首页”上,观察.nav-link:hover是否生效——这就是你第一次亲手操控CSS状态。

4.3 第二步:构建搜索模块(.search-box)的响应式骨架

继续在style.css中添加:

/* =============== 3. 搜索模块 =============== */
.search-section {
  padding: 40px 0;
  background: #f8f9fa;
}
.search-box {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 24px;
}
.search-row {
  display: flex;
  margin-bottom: 16px;
}
.search-field {
  flex: 1;
  margin-right: 16px;
}
.search-field:last-child {
  margin-right: 0;
}
.search-field label {
  display: block;
  font-size: 14px;
  color: #666;
  margin-bottom: 8px;
}
.search-field input {
  width: 100%;
  padding: 12px 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  outline: none;
}
.search-field input:focus {
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0,123,255,.1);
}

/* 移动端适配 */
@media (max-width: 768px) {
  .search-row {
    flex-direction: column;
  }
  .search-field {
    margin-right: 0;
    margin-bottom: 12px;
  }
}

现在,你的搜索框在桌面端是横向排列,在手机上自动变为纵向。重点看@media (max-width: 768px)这段——768px是iPad竖屏宽度,也是携程移动站的断点基准。不要死记硬背,打开Chrome DevTools(F12),点左上角的手机图标,拖动宽度滑块,亲眼看着布局如何变化。这种“所见即所得”的调试,比读一百页响应式理论都管用。

4.4 第三步:添加目的地列表(.destination-list)并解决图片占位难题

index.html<main>内添加:

<section class="destination-section">
  <div class="container">
    <h2 class="section-title">热门目的地</h2>
    <ul class="destination-list">
      <li class="destination-item">
        <a href="#" class="destination-link">
          <div class="destination-img" style="background-image: url(img/placeholder.jpg);"></div>
          <h3 class="destination-name">三亚</h3>
          <p class="destination-desc">亚龙湾、天涯海角</p>
        </a>
      </li>
      <!-- 复制3次,改文字和图片路径 -->
    </ul>
  </div>
</section>

对应CSS:

/* =============== 4. 目的地列表 =============== */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 24px;
}
.section-title {
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 32px;
  text-align: center;
}
.destination-list {
  display: flex;
  flex-wrap: wrap;
  margin: 0 -12px;
}
.destination-item {
  flex: 0 0 calc(25% - 24px);
  padding: 0 12px;
  margin-bottom: 24px;
}
.destination-link {
  display: block;
  text-decoration: none;
  color: #333;
}
.destination-img {
  height: 200px;
  background-size: cover;
  background-position: center;
  border-radius: 8px;
  margin-bottom: 12px;
  position: relative;
  overflow: hidden;
}
.destination-img::before {
  content: '';
  display: block;
  padding-top: 66.67%;
}
.destination-name {
  font-size: 18px;
  font-weight: 600;
  margin: 0 0 8px 0;
}
.destination-desc {
  font-size: 14px;
  color: #666;
  margin: 0;
}

/* 平板端适配:每行3列 */
@media (max-width: 992px) {
  .destination-item {
    flex: 0 0 calc(33.333% - 24px);
  }
}
/* 手机端适配:每行1列 */
@media (max-width: 768px) {
  .destination-item {
    flex: 0 0 100%;
  }
}

这里的关键突破是.destination-img::before伪元素。它用padding-top: 66.67%(即2:3宽高比)创建了一个响应式占位框,无论图片是否加载,容器高度都恒定。这是前端老手的“保命技巧”,务必亲手敲一遍。

4.5 最终收尾:添加底部版权区(.footer)与全局细节打磨

index.html末尾</main>后添加:

<footer class="footer">
  <div class="container">
    <div class="footer-content">
      <p class="copyright">&copy; 2024 我的携程练习页. 仅供学习交流.</p>
      <ul class="footer-links">
        <li><a href="#">关于我们</a></li>
        <li><a href="#">联系我们</a></li>
        <li><a href="#">隐私政策</a></li>
      </ul>
    </div>
  </div>
</footer>

CSS:

/* =============== 5. 底部版权 =============== */
.footer {
  background: #333;
  color: #fff;
  padding: 32px 0 24px;
}
.footer-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.copyright {
  margin: 0;
  font-size: 14px;
  color: #999;
}
.footer-links {
  list-style: none;
  display: flex;
}
.footer-links li {
  margin-left: 24px;
}
.footer-links a {
  color: #fff;
  text-decoration: none;
  font-size: 14px;
  transition: color .3s;
}
.footer-links a:hover {
  color: #007bff;
}

/* 全局细节:链接悬停统一效果 */
a:hover {
  text-decoration: underline;
}

最后,执行一次终极检查:
1. 打开Chrome DevTools,切换到Network标签页,刷新页面,确认所有资源状态码都是200(无404);
2. 切换到Elements标签页,逐个展开.header-nav.search-box.destination-list,确认结构层级清晰;
3. 在Console中输入document.querySelectorAll('.destination-item').length,确认返回4(代表四张卡片);
4. 用手机模式查看,确认所有区块在375px宽度下仍可读、可点。

当你完成这四步,你就拥有了一个真正可运行、可调试、可扩展的“携程风”页面。它不华丽,但每一行代码都在诉说一个设计决策;它不复杂,但每个像素都在传递一种工程思维。

5. 常见问题与避坑指南:那些没人告诉你的“显性知识”

5.1 图片路径404:为什么img/xxx.jpg在VS Code里预览正常,双击打开却报错?

这是新手最高频的崩溃点。根本原因在于:VS Code内置预览服务器的根路径是项目文件夹,而双击打开的file://协议根路径是当前HTML文件所在目录

举例:你的文件结构是:

my-project/
├── index.html
└── img/
    └── banner.jpg
  • 在VS Code中右键“Open with Live Server”,浏览器地址是http://127.0.0.1:5500/index.html,此时img/banner.jpg解析为http://127.0.0.1:5500/img/banner.jpg,正确。
  • 但双击index.html,地址是file:///Users/xxx/my-project/index.html,此时img/banner.jpg解析为file:///Users/xxx/my-project/img/banner.jpg——看起来一样,但某些系统(尤其是Windows)会因路径斜杠方向问题导致失败。

解决方案只有两个
1. 永远用Live Server插件:VS Code安装“Live Server”,右键HTML文件选择“Open with Live Server”,这是最稳妥的方式;
2. 绝对路径思维:在index.html中写<img src="./img/banner.jpg">./明确指向当前目录,杜绝歧义。

实测心得:我曾帮一个学员调试了3小时,最后发现他Mac系统里图片文件名是Banner.jpg(B大写),而HTML里写的是banner.jpg(b小写)。Mac文件系统默认不区分大小写,但某些浏览器会严格校验。所以,所有文件名统一小写+中划线(如beijing-hotel.jpg),这是职业前端的第一课。

5.2 Flex布局换行错乱:为什么flex-wrap: wrap后,最后一行只有两个item?

这个问题的本质是:Flex容器的总宽度不等于子项宽度之和。常见原因有三个:

原因一:父容器padding未计入

.destination-list {
  padding: 0 24px; /* 这24px左右内边距,会让总宽减少48px */
}

解决方案:在.destination-list上加box-sizing: border-box,或改用margin控制间距。

原因二:子项margin未归零

.destination-item {
  margin-right: 16px; /* 最后一项也有16px右外边距,导致换行 */
}
.destination-item:last-child {
  margin-right: 0; /* 必须手动清除 */
}

原因三:字体渲染导致的微小宽度偏差
Chrome对中文字符的渲染宽度有时会比CSS计算值多1px。解决方案是给.destination-itemflex-shrink: 0,禁止压缩:

.destination-item {
  flex: 0 0 calc(25% - 24px); /* 0 0 表示不伸缩、不收缩 */
}

5.3 阴影不显示:box-shadow写了却看不到?

90%的情况是:父容器overflow: hidden裁剪了阴影。比如.banner-wrap设置了overflow: hidden,而.banner-itembox-shadow在外部,就会被裁掉。

解决方案有两个:
- 方案A(推荐):给.banner-wrappadding: 1px,创造1px的“呼吸空间”,阴影就能溢出显示;
- 方案B:改用outline模拟阴影(outline: 1px solid rgba(0,0,0,.1)),虽然不如box-shadow灵活,但绝不会被裁剪。

5.4 字体模糊:为什么font-family写了“微软雅黑”,却显示成宋体?

这是因为字体回退链(fallback chain)断裂。正确写法必须包含英文和中文的完整回退:

body {
  font-family: 
    "PingFang SC", /* iOS */
    "Helvetica Neue", /* macOS */
    "Microsoft YaHei", /* Windows */
    "SimSun", /* Windows 旧版 */
    sans-serif; /* 终极保底 */
}

如果只写"Microsoft YaHei",在Mac上会直接回退到sans-serif(通常是Helvetica),导致中英文混排时字体不统一。这个细节,在你做国际化项目时会致命。

5.5 响应式失效:媒体查询写了,但宽度变化时布局没反应?

检查顺序必须是:
1. 确认HTML有viewport meta标签
html <meta name="viewport" content="width=device-width, initial-scale=1.0">
缺少这行,所有媒体查询在移动端都会失效。

  1. 确认CSS文件引入顺序:媒体查询必须写在常规样式之后,否则会被覆盖。

  2. 确认断点值合理:不要用@media (max-width: 768px),而要用@media (max-width: 767.98px)。因为768px是iPad竖屏精确值,如果设备宽度恰好768px,可能因四舍五入导致匹配失败。.98px是前端圈内公认的“安全偏移”。

6. 进阶延伸建议:从练习页面到真实能力的跃迁路径

这个页面包的价值,绝不只是“临摹一个携程”。它是一块跳板,帮你从代码搬运工蜕变为产品理解者。我给你三条清晰的跃迁路径,每一条都经过真实项目验证:

路径一:给静态页面注入生命(JavaScript增强)
当你能纯CSS实现导航悬停、轮播切换后,下一步就是用原生JavaScript实现:
- 为.search-box添加城市选择器:点击输入框弹出城市列表,支持拼音首字母筛选;
- 为.banner-wrap添加自动轮播:每5秒切换一张,点击指示器手动切换,鼠标悬停暂停;
- 为.destination-list添加筛选功能:顶部增加“国内”“出境”“周边”标签,点击后动态过滤卡片。

重点不是功能多炫,而是理解“何时该用JS”——比如城市列表的DOM操作,必须等DOMContentLoaded事件后执行;轮播的定时器,必须在页面卸载时clearInterval防止内存泄漏。这些,才是企业级开发的核心素养。

路径二:用真实数据驱动页面(Mock API实践)
下载JSON Server工具,创建db.json

{
  "destinations": [
    { "id": 1, "name": "北京", "desc": "故宫、长城", "image": "beijing.jpg" },
    { "id": 2, "name": "上海", "desc": "外滩、迪士尼", "image": "shanghai.jpg" }
  ]
}

然后用fetch('/destinations')替代静态HTML,把.destination-list变成动态渲染。你会立刻遇到新问题:加载状态怎么显示?错误怎么捕获?图片加载失败如何兜底?这些问题,正是你走向全栈的第一道关卡。

路径三:接入设计系统(Design System Integration)
把本包的CSS抽离成原子化类名:
- .bg-white 替代 background: #fff
- .p-4 替代 padding: 16px
- .text-lg 替代 font-size: 18px
然后尝试用Tailwind CSS重写整个页面。你会发现,原来需要写200行CSS的地方,现在只需10个类名。这种“约定优于配置”的思维,会让你在未来面对Ant Design、Element Plus等UI库时,一眼看穿其设计哲学。

最后分享一个我的真实体会:去年我指导一个零基础转行的学员,她用两周时间吃透这个页面包,第三周就开始为本地旅行社做官网改版,第四周收到Offer。她没写一行框架代码,但交出的HTML结构被客户夸“比原厂还专业”。为什么?因为她理解了携程每个像素背后的用户意图——那个搜索框的16px内边距,不是为了好看,而是为了让拇指在手机上点击时有足够热区;那个24px的卡片间距,不是随意设定,而是确保视线在快速扫视时不会疲劳。

所以,请放下“我要学会多少技术”的执念。打开你的编辑器,从index.html的第一行<!DOCTYPE html>开始,亲手敲下每一个标签。当你能对着携程网test.html的源码,说出“这里用Flex而非Grid,是因为要兼容老版本iOS Safari”,你就已经站在了专业前端的大门前。门后是什么?是无数个真实用户的旅程,正等待你用代码去守护。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套专为前端初学者和UI还原练习设计的静态页面集合,完全模仿携程官网的视觉结构与布局逻辑。包含多个独立HTML文件(如携程.html、携程网test.html等),搭配配套CSS样式表和图片资源,所有内容基于HTML5和CSS3编写,不依赖JavaScript,开箱即用,双击即可在浏览器中查看效果。资源包按功能模块组织:css文件夹存放全部样式代码,img文件夹统一管理图标、占位图等视觉素材;携程、携程网等子目录对应不同测试版本或页面分支,便于对比学习。页面涵盖典型OTA平台核心区块——顶部通栏导航、多条件搜索框、轮播图预留区域、热门目的地推荐列表、服务入口卡片组、底部版权及链接区等。类名命名贴近真实业务场景(如.header-nav、.search-box、.banner-wrap),有助于理解电商类网站的DOM结构组织方式。适合用于HTML结构训练、CSS布局实战、UI视觉还原参考,也适合作为教学演示案例或快速原型搭建的基础素材。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值