简介:这份“意大利风味餐厅响应式网页”HTML源码面向Web前端初学者、课程设计学生及需要快速搭建餐饮品牌展示页的开发者,提供一套可直接研究的静态站点范例。页面基于HTML5、CSS3与JavaScript组织,重点展示响应式布局、栅格系统、动画过渡、图标字体、轮播与弹窗等常见前端模块,可用于练习多终端适配、页面组件拆分和交互脚本阅读。压缩包共120个文件,约4.42MB,其中2个HTML页面承载结构与内容,13个CSS与22个JavaScript文件负责样式和交互,另含PNG、JPG、GIF、SVG等图片素材及TTF、WOFF、EOT、OTF字体文件,素材与代码相对完整。已有97人学习或下载。读者可据此理解餐饮类网站的视觉层次、菜单与图片展示布局,提取可复用的导航、横幅、图集和响应式样式,也可作为个人作品集或小型展示站的原型进行二次修改。
1. 意式餐厅官网这类静态页,为什么值得拿来当响应式练手
上个月有个开餐厅的朋友找我,说想做个线上门面,预算只够一次改版,后台没人维护,问我能不能"简单点"。这种活儿我一般不上框架,直接找一份干净的 HTML 源码来改——这份意式风味餐厅的 web 前端页面就是典型样本:单页结构、纯 HTML/CSS/JS,桌面和 H5 移动端共用一套代码,首屏、招牌菜、品牌故事、门店信息、预订表单五个区块齐全。它不解决任何业务逻辑,解决的恰恰是最耗时间的那个问题:页面在 1440px 显示器上排版体面,在 375px 手机上不塌、不错位、按钮点得到。适合三类人——刚学完盒模型想找个完整页面练响应式的,接私活需要快速交付静态站的,以及想拿一份结构规整的模板做二次开发的。下面按目录、布局、脚本、排错、改造的顺序拆一遍。
2. 项目结构与技术选型:从 index.html 到 assets 的三层目录怎么分
拿到一份前端源码,我的习惯是先不打开浏览器,先把目录 tree 出来看一遍。目录乱的项目,后面改起来一定有血泪经验;目录清晰的项目,光看文件命名就知道作者有没有做过交付。
2.1 目录骨架与文件职责
这类单页餐厅站通常是扁平三层结构,页面在根目录,样式和脚本各自一个文件夹,图片按用途再分一级:
restaurant/ ├── index.html # 唯一入口页,五个 section 顺序排下来 ├── css/ │ ├── reset.css # 清默认样式,统一 box-sizing 与 margin │ ├── style.css # 主样式:变量、排版、区块布局 │ └── responsive.css # 媒体查询集中放这里,方便对照断点改 ├── js/ │ └── main.js # 导航折叠、滚动显现、表单校验 ├── images/ │ ├── banner/ # 首屏大图,一般 1920px 宽 │ ├── dishes/ # 菜品图,正方形为主 │ └── icons/ # 图标,能用 SVG 就别用 PNG └── README.md # 部署与替换说明| 文件 | 职责 | 改动频率 |
|---|---|---|
| index.html | 结构骨架与文案 | 低,改菜名改价格时动 |
| css/style.css | 颜色、字号、间距、区块布局 | 中,换品牌色时动 |
| css/responsive.css | 断点内的重排规则 | 低,除非加新模块 |
| js/main.js | 纯交互,不碰数据 | 低 |
| images/ | 全部视觉素材 | 高,店主最常换这个 |
这个分法的好处是职责边界清楚:店主想换张图,只动 images;想换主色调,只动 style.css 顶部的 CSS 变量;想调手机上卡片几列,只进 responsive.css。我见过不少同类源码把三段样式全塞在<style>标签里,改一个颜色要在 600 行里搜十六进制色值,那种项目接手就是给自己找麻烦。
2.2 技术选型:为什么用原生三件套而不是框架
标题写的是 HTML 源码,实际交付形态就是零构建的静态站。选原生而不是 Vue/React,不是保守,是场景决定的:
- 部署成本。静态站扔到任何静态托管或者一台装 nginx 的机器上就能跑,不需要 Node 环境、不需要 CI 打包产物。餐厅这种站一年改两次,多一层构建就多一层一年后没人会操作的负担。
- 首屏加载。整个站压缩后不过几百 KB,其中图片占大头。框架 runtime 那几十 KB 在这里换不来任何收益,反而拖慢移动端首屏。
- 可交接性。HTML/CSS 的可读性对非专业维护者最友好,店主找个懂点前端的兼职就能改文案。
代价也得说清楚:没有组件化,五个区块的卡片结构是复制粘贴出来的,改一处样式要确认其他四处没被误伤;没有路由,多页面就得建多个 html 文件。这些在单页餐厅站里都能接受。
要不要引 Bootstrap 这类栅格框架,取决于你想不想背它的类名。我的做法是只借它的思路不引它的文件:自己写一套container+row的最小栅格,或者干脆用 CSS Grid,二十行搞定,体积为零。图标同理,Font Awesome 用 CDN 引一次要几百 KB,且断网就变成方框,不如把用到的五六个图标抠成 SVG 内联进 HTML。
2.3 页面分区与语义化标签
结构上别用一堆 div 平铺,语义化标签在移动端还有个实际好处:读屏和搜索引擎都能识别区块边界,后期做锚点跳转也顺。骨架大致长这样:
<!-- index.html 主体骨架,只保留结构关键部分 --> <header class="site-header"> <a class="logo" href="#top">Trattoria</a> <!-- aria-controls 指向被控制的列表 id,读屏软件据此关联 --> <button class="nav-toggle" aria-expanded="false" aria-controls="main-nav"> <span class="sr-only">打开菜单</span> </button> <nav id="main-nav" class="site-nav" aria-label="主导航"> <ul class="nav-list"> <li><a href="#dishes">招牌菜</a></li> <li><a href="#story">品牌故事</a></li> <li><a href="#booking">在线预订</a></li> </ul> </nav> </header> <main id="top"> <section class="hero" id="hero"> <h1 class="hero-title">手工意面,每日现做</h1> <a class="btn btn-primary" href="#booking">预订座位</a> </section> <section class="dishes" id="dishes" aria-labelledby="dishes-title"> <h2 id="dishes-title">招牌菜</h2> <ul class="dish-grid"><!-- 卡片由 js 或静态复制生成 --></ul> </section> <section class="booking" id="booking"> <h2>在线预订</h2> <form id="booking-form" novalidate><!-- novalidate:交给自定义校验 --></form> </section> </main> <footer class="site-footer">营业时间 / 地址 / 电话</footer>几个点值得单独拎出来:header里那个按钮不是装饰,移动端导航的开关就是它,aria-expanded的初始值必须是"false"而不是省略,否则 JS 里拿到的状态和视觉状态会对不上;novalidate加在 form 上是刻意的,浏览器原生气泡样式在不同系统里长得完全不一样,餐厅站的表单通常要统一视觉,所以关掉原生校验走自定义;每个section给aria-labelledby指向自己的标题,读屏用户跳到这个区块时能听到"招牌菜,区块"。这些细节不影响页面长相,但决定这份源码是"能看"还是"能用"。
图片这块补一句,<img>必须带alt,纯装饰图写alt=""(空值不是省略),菜品图写具体菜名。加载性能上用loading="lazy"给首屏以下的图,首屏大图别加,加了反而拖慢 LCP。
3. 响应式布局的实现:媒体查询断点与 Grid/Flex 混合排版
响应式这三个字最容易做成玄学——同一个页面在 Chrome 的设备模拟器里完美,拿到真机上就多出一条横向滚动条。问题多半不在媒体查询写没写,而在断点怎么定、容器怎么约束、图片怎么兜底。
3.1 CSS 变量与断点设计
先把可变的东西提到:root,这是后期改品牌色最快的一条路:
/* css/style.css 顶部:设计令牌集中管理 */ :root { --c-primary: #8b2f2f; /* 意式餐厅常用的砖红,换品牌色只改这一行 */ --c-dark: #1f1b18; --c-bg: #faf7f2; --c-text: #3a332e; --font-title: "Playfair Display", Georgia, serif; --font-body: system-ui, -apple-system, "PingFang SC", sans-serif; --container: 1200px; /* 内容最大宽度 */ --gap: 24px; --radius: 6px; } .container { width: min(100% - 40px, var(--container)); /* 左右各留 20px 安全边距 */ margin-inline: auto; }width: min(100% - 40px, 1200px)这一行比我见过的大量max-width + padding + box-sizing组合都稳,窄屏时自动收缩并保留边距,宽屏时锁死在 1200px 居中,不用写任何媒体查询。
断点不要照抄设备的像素值,按内容断——你把浏览器窗口从宽慢慢拉窄,哪一行的卡片开始挤得难看,那里就是断点。餐饮站常见四档:
| 断点 | 目标场景 | 主要变化 |
|---|---|---|
| 1200px 以上 | 桌面宽屏 | 菜品 3~4 列,导航横排 |
| 992px | 小笔记本 / 横屏平板 | 菜品 3 列,首屏字号略降 |
| 768px | 竖屏平板 | 菜品 2 列,导航折叠为汉堡 |
| 480px | 手机 H5 | 全单列,按钮撑满宽度,字号提一档 |
最后一条是移动端最容易被忽略的:字号不是越小越省空间,手机上正文低于 14px 阅读就吃力,标题反而可以靠clamp()自适应。我一般写成font-size: clamp(1.75rem, 4vw, 3rem),让标题跟着视口平滑缩放,比在每个断点里手动改 font-size 省事得多。
3.2 首屏 Hero 与导航移动端折叠
首屏是唯一一个"必须一眼好看"的区块,它的实现套路很固定:背景图 + 半透明遮罩 + 居中文字。
/* 首屏:背景图铺满,用渐变遮罩保证文字对比度 */ .hero { min-height: 88vh; /* 不用 100vh,避开移动端地址栏收起时的跳动 */ display: grid; place-items: center; text-align: center; padding: 120px 20px 80px; background: linear-gradient(rgba(31, 27, 24, .55), rgba(31, 27, 24, .55)), url("../images/banner/hero-1920.jpg") center / cover no-repeat; } @media (max-width: 768px) { .hero { min-height: 70vh; /* 小屏换小图,省一半流量 */ background-image: linear-gradient(rgba(31, 27, 24, .6), rgba(31, 27, 24, .6)), url("../images/banner/hero-768.jpg"); } }min-height: 88vh而不是100vh,是被移动端浏览器地址栏的动态高度坑出来的习惯:100vh在部分浏览器里按最大视口算,地址栏一收起内容就顶出去一截。遮罩那一层不能省,浅色菜品图配白字对比度往往不到 3:1,在阳光下基本看不见。
导航的折叠逻辑是"宽屏横排、窄屏隐藏 + 按钮唤起":
/* 宽屏:导航直接横排,汉堡按钮不显示 */ .nav-toggle { display: none; } @media (max-width: 768px) { .nav-toggle { display: block; } /* 汉堡按钮现身 */ .site-nav { position: fixed; inset: 64px 0 auto 0; /* 贴在头部下方 */ background: var(--c-dark); transform: translateY(-110%); /* 默认收起,靠位移隐藏 */ transition: transform .3s ease; } .site-nav.is-open { transform: translateY(0); } /* JS 加类名后滑出 */ .nav-list { flex-direction: column; } .nav-list a { display: block; padding: 16px 24px; } }用transform而不是display: none做显隐,是为了保留那 0.3 秒的滑出动画;inset一行代替 top/right/bottom/left 四个属性。这里有个前提:site-header得是position: sticky或fixed且高度固定,否则inset的 64px 对不上,菜单会盖住 logo。我习惯给 header 显式写height: 64px而不是靠 padding 撑,就是为了让这个数值可预期。
3.3 卡片栅格与图片自适应
菜品区是响应式效果最直观的地方。Grid 的auto-fit + minmax能省掉大部分媒体查询:
/* 菜品栅格:列数随容器自动变化,最小 260px */ .dish-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--gap); list-style: none; padding: 0; } /* 卡片:固定宽高比,避免图片尺寸不一导致卡片高低不齐 */ .dish-card { background: #fff; border-radius: var(--radius); overflow: hidden; /* 裁掉图片圆角外的部分 */ box-shadow: 0 2px 12px rgba(0, 0, 0, .06); } .dish-card img { width: 100%; aspect-ratio: 4 / 3; /* 预留高度,图片加载前不塌陷 */ object-fit: cover; /* 裁切而非拉伸,菜品不变形 */ display: block; /* 去掉 inline 元素底部的空隙 */ } @media (max-width: 480px) { .dish-grid { grid-template-columns: 1fr; } /* 手机上强制单列 */ }aspect-ratio+object-fit: cover这一对组合解决两个老问题:一是图片比例不统一时卡片高度参差,二是图片没加载完时布局抖动(CLS)。display: block那行看着无关紧要,实际上不加的话 img 作为行内元素,下面会多出 3~4px 的基线空隙,一列卡片看下来就是"每张图下面莫名有条白缝"。
auto-fit和auto-fill的区别也顺带说一句:容器里卡片数量少时,auto-fit会把空轨道压掉、卡片撑满整行,auto-fill会保留空轨道、卡片维持最小宽度靠左堆。菜品数量不固定又想让布局饱满,选auto-fit。
4. 交互脚本拆解:汉堡菜单、滚动显现与表单校验
纯静态站的 JS 通常就三件事:导航开关、滚动动画、表单检查。代码量不大,但每一段都有一个不做就会翻车的细节。
4.1 汉堡菜单与 aria 状态
// js/main.js —— 移动端导航折叠 const navToggle = document.querySelector('.nav-toggle'); const siteNav = document.querySelector('.site-nav'); const navList = document.querySelector('.nav-list'); function closeNav() { siteNav.classList.remove('is-open'); navToggle.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; // 恢复滚动 } navToggle.addEventListener('click', () => { const isOpen = siteNav.classList.toggle('is-open'); // aria-expanded 必须同步,否则读屏用户听到的状态是错的 navToggle.setAttribute('aria-expanded', String(isOpen)); // 菜单展开时锁住背景滚动,不然手指划的是下面的页面 document.body.style.overflow = isOpen ? 'hidden' : ''; }); // 点击锚点后自动收起:移动端点完菜单还挡着半屏是最常见的体验问题 navList.addEventListener('click', (e) => { if (e.target.tagName === 'A' && siteNav.classList.contains('is-open')) closeNav(); }); // 窗口放大回桌面尺寸时清掉残留状态 window.addEventListener('resize', () => { if (window.innerWidth > 768) closeNav(); });逻辑说明:classList.toggle返回布尔值,直接喂给aria-expanded,视觉和语义就不会走岔。锁滚动的做法是给 body 设overflow: hidden,比给 html 设更稳,代价是页面会瞬间回到顶部——如果站点很长,需要额外记录window.scrollY并在关闭时还原。resize监听里那个判断尤其重要:不写的话,用户在手机横屏状态下打开菜单再转回桌面,导航会停留在固定定位的展开态,把整个页面盖住,这种 bug 只有真机测试才抓得到。
4.2 IntersectionObserver 滚动显现
区块淡入的效果,现在没必要再监听 scroll 事件算位置了,那套写法每滚一帧就触发一次布局查询,低端机上卡得明显。
// 区块进入视口时加类名,交给 CSS 做过渡 const revealItems = document.querySelectorAll('.reveal'); if ('IntersectionObserver' in window) { const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); io.unobserve(entry.target); // 播一次就注销,来回滚动不闪 } }); }, { root: null, // null 表示以视口为参照系 rootMargin: '0px 0px -15% 0px', // 底部内缩 15%,元素露头多一点才触发 threshold: 0.1 // 露出 10% 即算进入 }); revealItems.forEach((el) => io.observe(el)); } else { // 老浏览器兜底:直接显示,别让内容永远藏着 revealItems.forEach((el) => el.classList.add('is-visible')); }参数说明:rootMargin的负值是在收缩观察区域,让触发时机更靠上一点,视觉上像"滚到位了就浮现";threshold设 0.1 而不是 0.5,是因为长区块(比如品牌故事整段)可能永远到不了 50% 可见。兜底分支不能省,is-visible是控制 opacity 的类名,不加的话在旧环境里这些元素会一直是opacity: 0,页面看起来像空白——这是我踩过的最冤枉的一次坑。配套的 CSS 就三行:.reveal { opacity: 0; transform: translateY(24px); transition: .6s },.is-visible { opacity: 1; transform: none }。另外记得在@media (prefers-reduced-motion: reduce)里把动画关掉,这是无障碍的基本礼貌。
4.3 预订表单的前端校验
静态站没有后端,表单要么接第三方表单服务,要么走mailto:,要么就只做前端校验加一个提示。不管哪种,前端这道验收得写扎实:
const form = document.querySelector('#booking-form'); const phoneRe = /^1[3-9]\d{9}$/; // 手机号粗校验,别写得太死 form.addEventListener('submit', (e) => { e.preventDefault(); // 无后端,先拦住默认提交 const errors = []; const name = form.elements.name.value.trim(); const phone = form.elements.phone.value.trim(); const date = form.elements.date.value; if (name.length < 2) errors.push('请填写称呼'); if (!phoneRe.test(phone)) errors.push('手机号格式不对'); if (!date) { errors.push('请选择到店日期'); } else if (new Date(date) < new Date(new Date().toDateString())) { errors.push('日期不能早于今天'); // 按天比较,忽略时分秒 } if (errors.length) { form.querySelector('.form-error').textContent = errors[0]; // 只提示第一条,别刷屏 return; } form.querySelector('.form-error').textContent = ''; form.reset(); // 真接服务时,这里换成 fetch 提交,注意失败回滚的提示 alert('已收到预订,我们会电话确认'); });逻辑说明:form.elements.name比querySelector('[name=name]')稳,抓的是表单自身的控件集合,不受页面其他地方同名元素干扰。日期比较用new Date().toDateString()归零到当天,否则用户选今天会被判成"早于当前时间"——这个日期边界判断踩坑率极高。错误提示只显示第一条,是因为表单就四五个字段,一次性弹出五条红字反而让人无从下手;真实项目里通常改成给单字段加.is-invalid类,红框 + 字段下方小字,体验更细。regex那行刻意写得宽松,手机号号段会变,校验的目的是挡住明显乱填,不是搞一个永远滞后的号段库。
5. 本地运行与排错:五个高频翻车点的排查清单
源码双击 index.html 就能看,但"能打开"和"跑得对"是两回事。下面这五条是我每次接手静态站都会先过一遍的清单,按出现频率排。
5.1 起一个本地静态服务
file://协议下有两类问题绕不过去:fetch读本地 JSON 会被跨域策略拦掉,部分字体和图片的 MIME 类型也会判错。所以哪怕是最简单的验证,也建议起个服务:
# 项目根目录执行,换掉 8000 可避免和已有服务撞端口 python3 -m http.server 8000 # 或者用 Node,装了 npx 的话一行搞定 npx serve . -l 8000然后浏览器开http://localhost:8000。手机真机调试时,让手机和电脑连同一个网络,用电脑的局域网 IP 加端口访问,改完刷新就能看到移动端真实渲染,比设备模拟器可靠得多。改 CSS 后如果没生效,先按Ctrl/Cmd + Shift + R硬刷新,缓存这东西黑匣子得很。
5.2 五个具体翻车点:现象、原因、解决
其一:手机打开后页面整体缩小,字小得像蚂蚁。
现象是桌面端正常,手机上内容被压成小小一条。原因十有八九是<head>里缺视口声明,浏览器按 980px 的虚拟宽度渲染再整体缩放。解决:补上<meta name="viewport" content="width=device-width, initial-scale=1">,注意不要顺手加user-scalable=no,那会禁止双指放大,无障碍上是要被扣分的。
其二:媒体查询写了但完全不生效。
现象是把窗口拉窄,样式纹丝不动。常见原因有两个:一是媒体查询块的书写顺序在基础样式之前,被后面的同名规则覆盖了,CSS 没有"特异性更高"这回事,同权重就是后写的赢;二是把@media嵌套写在了选择器内部(原生 CSS 不支持,得用预处理器)。解决:所有媒体查询统一放到responsive.css,且在主样式之后引入,顺序从宽到窄排列。
其三:图片全裂,控制台一堆 404。
现象是文字排版正常,图片位置显示破图标。原因通常是路径大小写不一致——Windows 文件系统不区分大小写,Images/Banner.JPG在本地能显示,传到 Linux 服务器上就是 404;或者是 CSS 里url()的相对路径以 CSS 文件自身位置为基准,不少人误以为以 HTML 为基准。解决:文件名全部小写、用中划线不用空格或中文,路径写全相对路径,上传前在服务器环境里再验一遍。
其四:中文字体在他人电脑上变成宋体或黑体。
现象是设计稿里标题是衬线体,别人打开变成系统默认。原因是用了本地没装的字体,或者 Google Fonts 那行link在国内网络下超时,font-family一路回退到兜底。解决:font-family里至少配两级兜底("Playfair Display", Georgia, serif),对交付型项目干脆把字体文件下载到本地fonts/目录用@font-face引,配font-display: swap避免文字长时间不可见。中文字体文件动辄几 MB,一般只给标题用,正文老实走system-ui。
其五:横向多出一条神秘滚动条。
现象是手机上页面能左右晃,露出一截白边。原因通常是某个元素超宽——固定宽度的图片、100vw加 padding 撑出的溢出、或者是transform: translateX动画位移到视口外。排查手段很土但有效:在控制台跑一段脚本给所有元素加描边,超出的那个会露头。
// 临时排查:找出所有宽度超视口的元素 [...document.querySelectorAll('*')].forEach((el) => { if (el.getBoundingClientRect().right > document.documentElement.clientWidth + 1) { console.log(el, el.getBoundingClientRect().width); el.style.outline = '2px solid red'; } });解决思路是对症:图片加max-width: 100%; height: auto,容器别用100vw(它包含滚动条宽度,用100%更准),动画元素所在父级加overflow: hidden。修完记得把这段调试脚本删掉。
6. 二次开发改造:把静态页改成数据驱动的菜单模板
静态页最麻烦的维护场景是改菜单——店主换季调价,你得在 HTML 里找到那几个<li>一个个改图和文。做二次开发时,我第一步就是把这块数据抽出去。
6.1 用 JSON 驱动菜品卡片
把菜品信息挪到data/menu.json,结构保持扁平:
[ { "id": "pasta-01", "name": "手工宽面配慢炖牛肉酱", "price": 68, "img": "images/dishes/pasta-01.jpg", "tag": "招牌" }, { "id": "pizza-02", "name": "窑烤玛格丽特披萨", "price": 58, "img": "images/dishes/pizza-02.jpg", "tag": "" } ]渲染逻辑用模板字符串拼,注意转义和空值兜底:
// 用数据渲染菜品区,HTML 里只留一个空 <ul id="dish-grid"> const grid = document.querySelector('#dish-grid'); // 转义用户可填字段,价格和菜名理论上来自后台,别直接 innerHTML const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); function renderDish(item) { return ` <li class="dish-card reveal"> <img src="${esc(item.img)}" alt="${esc(item.name)}" loading="lazy" width="400" height="300"> <div class="dish-info"> ${item.tag ? `<span class="dish-tag">${esc(item.tag)}</span>` : ''} <h3>${esc(item.name)}</h3> <p class="dish-price">¥${Number(item.price).toFixed(0)}</p> </div> </li>`; } fetch('data/menu.json') .then((r) => { if (!r.ok) throw new Error(r.status); return r.json(); }) .then((list) => { grid.innerHTML = list.map(renderDish).join(''); // 新插入的节点要重新注册滚动显现,否则它们永远是透明的 document.querySelectorAll('.reveal:not(.is-visible)').forEach((el) => io.observe(el)); }) .catch(() => { grid.innerHTML = '<li class="dish-error">菜单加载失败,请刷新重试</li>'; });这段的关键不在渲染,在两个容易漏的地方:一是fetch必须跑在 http 服务下,直接双击 html 会报跨域,这跟第 5 章那条呼应;二是动态插入的.reveal节点没有被 IntersectionObserver 观察过,不重新observe就一直是opacity: 0,页面看起来像加载失败。width/height属性写死在 img 上,配合aspect-ratio能进一步压住布局抖动。
6.2 改造后的验证清单
改完别只看桌面端。我一般按这张表过一遍,每一项都能在五分钟内完成:
| 检查项 | 方法 | 通过标准 |
|---|---|---|
| 断点表现 | 开发者工具拖到 375 / 768 / 1024 / 1440 | 无横向滚动,卡片列数符合预期 |
| 菜单数据 | 改 JSON 里一条菜名和价格 | 刷新后两处都变,无需动 HTML |
| 交互状态 | 手机尺寸下开合导航、点锚点 | 菜单自动收起,body 恢复滚动 |
| 表单边界 | 选昨天的日期、填 10 位手机号 | 各自给出对应提示,不提交 |
| 加载性能 | Lighthouse 移动端跑一次 | 无 404,CLS 接近 0 |
如果后面要接真实预订,把form的 submit 换成fetch提交,注意加 loading 态和失败回滚:按钮置灰、文案改"提交中",失败时把用户填的内容留着别清空——这条我吃过亏,一次网络抖动把用户填了三分钟的信息清得干干净净,投诉电话打过来的时候只能道歉。
我的习惯是每接一个静态站,都在 README 里写清三件事:换图放哪个目录、改主色动哪个变量、改菜单改哪个 JSON。从那以后我每次交付前都强制走一遍上面的验证表,尤其是真机那一步——模拟器里再完美,也替不了手里那台旧手机的真实表现。希望帮到你。
本文还有配套的精品资源,点击获取