简介一款基于HTML5、JavaScript与CSS构建的移动端点餐系统DEMO聚焦餐饮App的菜单浏览、菜品分类、购物车、订单评价等核心场景适合Web前端初学者、移动端开发入门者也适合需要课程设计或毕业设计原型的高校学生代码可直接运行并二次扩展。压缩包共93个文件大小仅1.23MB包含23个HTML页面、7个CSS样式表、7个JavaScript脚本、30个PNG、22个JPG与3个GIF图片另有一个.project工程配置文件HTML负责页面结构与内容分区CSS承担界面配色、尺寸和移动端响应式布局JS实现轮播图、触屏滑动、购物车增删、订单联动等交互逻辑完整演示了前端三要素如何协同工作。当前已有778人学习下载文件目录按功能拆分清晰适合快速定位菜单页、购物车页、订单页等模块作为移动端页面工程和交互设计的入门模板也能充当课程设计或毕业设计的蓝本帮助读者快速梳理出一套点餐应用的前端实现思路。1. 移动端点餐H5为什么这套DEMO值得拆开看移动端点餐最麻烦的不是菜单展示而是购物车状态同步和触摸滚动。最近拆了一套用HTML5、CSS、JavaScript直接手写的APP点餐系统DEMO压缩包只有几百KB跑起来却覆盖了菜单分类、商品详情、购物车、订单提交、退款和评价全部流程。有意思的是它还在用jQuery 1.3.2和touchSlider这类老组件但正因为老代码逻辑直观没有框架黑盒适合想搞懂移动端页面交互原理的开发者。无论是做毕设还是接手传统Web项目这套DEMO都能提供不少可复用的页面骨架和交互写法。下面对照源码把布局、触摸事件和订单流程的关键处逐一说明。2. 从文件结构到CSS分层点餐系统的页面骨架怎么组织点餐系统不像后台管理系统页面多、跳转快每一屏承载的内容相对单一。这套DEMO用多个HTML文件组织页面而不是做成SPA这种结构在传统Web项目里非常常见好处是页面间解耦一个文件坏了不影响其他页面。先看清文件组织再动CSS会顺畅很多。2.1 文件清单每个页面在点餐流程里负责什么解压后先别急着开浏览器把文件树理一遍。压缩包里的页面和样式是配套的页面命名已经透露出业务划分。├── index.html # 首页搜索、轮播、分类入口 ├── recommend2.html # 推荐菜品列表 ├── vegetableplate.html # 素菜分类页 ├── meatdish.html # 荤菜分类页 ├── hot2.html # 热销榜 ├── new2.html # 新品列表 ├── discount2.html # 折扣菜品 ├── snack2.html # 小吃分类 ├── goodsdetails.html # 商品详情 ├── shopcar.html # 购物车 ├── order.html # 订单提交 ├── refund.html # 退款说明/申请 ├── evaluate.html # 评价页面 ├── css/ │ ├── common.css # 基础重置与公用样式 │ ├── index.css # 首页专属样式 │ ├── subpage.css # 二级页面通用样式 │ ├── menu.css # 菜单页样式 │ ├── shopcar.css # 购物车样式 │ └── order.css # 订单样式 ├── js/ │ ├── jquery-1.3.2.js │ ├── touchslider.dev.js │ ├── touchScroll.js │ ├── run.js │ ├── shopcar.js │ └── order.js └── img/ # 菜品图、按钮图、背景图从这个结构能看出页面职责是按点餐动作拆的浏览菜单、看详情、加购、下单、售后。每个HTML对应一个具体场景页面标题和文件名一一对应后期维护时想改哪个流程直接找同名文件即可。img目录里cp1.jpg到cp11.jpg是菜品图top_bg.png、search_bg.png这类是界面背景元素量不大但说明了图片资源和页面样式是紧密绑定的。2.2 CSS分层common.css到shopcar.css各管什么这套DEMO的CSS按“公用层 - 页面层”划分虽然没用SCSS变量和BEM命名但文件边界很清楚。看一个CSS文件不要只看类名要看它被哪些页面引用以及里面定义的组件是否跨页面复用。CSS文件主要职责引用范围common.css重置默认边距、字体、公共按钮、价格颜色、通用弹层全部页面index.css首页搜索栏、轮播区、分类宫格、推荐位布局首页及部分二级页subpage.css列表页头图、返回栏、卡片间距菜单、详情、评价等二级页menu.css菜品分类标签、菜品卡片、加号按钮各分类菜单页shopcar.css购物车列表、步进器、底部结算栏购物车页order.css订单表单、配送方式、提交按钮订单提交页调试时可以遵循一个习惯全局性的间距和颜色改common.css单个页面的布局改对应页面CSS避免到处覆盖。这套DEMO中2common.css这类文件存在冗余但常见做法是保持一个公用样式表后续新页面直接复用。2.3 响应式布局怎么写从固定宽度到媒体查询点餐页面的核心使用场景是手机但这套DEMO里没有引入rem或vw方案大量宽度使用百分比和固定像素混合。对于快速交付的DEMO这种做法能跑但遇到不同屏幕适配会吃力。常见做法是保留原有结构在CSS末尾追加媒体查询把关键字体、触控按钮和底部结算栏单独适配。media screen and (max-width: 480px) { .food-card .price { font-size: 14px; } .food-card .cart-btn { width: 32px; height: 32px; } .cart-bar { width: 100%; bottom: 0; left: 0; } .category-nav { overflow-x: auto; -webkit-overflow-scrolling: touch; } }这段代码做了三件事在480px以下屏幕缩小价格字号让加购按钮保持足够触控面积把购物车结算栏固定在底部并禁止横向溢出。-webkit-overflow-scrolling: touch是iOS上启用惯性滚动必须写的属性否则手指滑动列表会显得生硬。如果后续要适配iPhone SE这类小屏把max-width: 480px改成min-width: 320px和max-width: 374px分段控制即可。2.4 HTML5语义化在DEMO里的实际落点虽然源码里很多地方还是div嵌套但改造时优先用header、footer、section、nav替换高频区域。首页的结构可以这样理解顶部header放搜索和定位中间section放轮播下面nav放分类入口底部footer固定放购物车入口。语义化不是给机器看的而是让后来维护的人扫一眼结构就知道页面分成几块。这套DEMO的index.html虽然写成了div但类名已经接近语义化比如search_bg、category-nav顺着类名就能找到对应区域。3. 触摸滑动与购物车状态JavaScript交互层拆解页面布局只能解决“看起来像点餐系统”真正的使用体验取决于两个交互触摸滑动是否跟手购物车状态是否即时更新。这套DEMO用的是jQuery 1.3.2配合两个老牌组件——touchslider.dev.js负责轮播滑动touchScroll.js负责列表区域滚动。这两个组件几十行代码就实现了核心功能搞懂它们对理解移动端触摸事件很有帮助。3.1 touchSlider和touchScroll老组件的调用方式轮播组件在这套DEMO里一般用在首页展示位调用方式比较固定。从源码看touchSlider.dev.js内部封装了touchstart、touchmove、touchend三个事件通过计算位移量切换容器位置。var bannerSlider touchSlider({ slide: .banner-list, // 轮播容器选择器 dot: .banner-dot, // 底部指示器选择器 auto: 3000, // 自动轮播间隔0表示关闭 effect: slide // slide: 滑动切换fade: 淡入淡出 });参数里auto是按毫秒计算的3000代表3秒自动切换一张。DEMO里常见的坑是轮播图数量不固定时dot指示器和slide容器没有同步刷新解决方案是在动态添加轮播项后重新调用一次初始化函数。effect参数如果改成fade触摸滑动的效果会变成半透明过渡但组件内部依赖的位移计算逻辑不同不建议轮播图超过5张时使用。区域滚动用touchScroll.js它的作用是固定一个容器高度让内部内容独立滚动菜单分类页常用。new TouchScroll({ wrap: document.getElementById(categoryWrap), target: document.querySelector(.category-inner), vertical: true });wrap是外层容器必须有固定高度target是实际滚动的子元素高度如果小于外层则滚动失效。vertical: true表示纵向滚动横向滚动设为false后需要配合overflow-x: auto才能正常滑动。这两个组件都依赖原生DOM事件不额外引入库性能上比setInterval实现平滑很多。3.2 购物车逻辑商品加入、数量变更与总价计算购物车是点餐系统的核心状态shopcar.js里维护了一个简单的对象字典。代码不长但把增删改查和总价计算都收纳在了一起后期想接入后端接口也很容易替换。var Cart { items: {}, add: function(id, name, price, num) { if (this.items[id]) { this.items[id].num num; } else { this.items[id] { name: name, price: price, num: num }; } this.render(); }, reduce: function(id) { if (!this.items[id]) return; this.items[id].num--; if (this.items[id].num 0) { delete this.items[id]; } this.render(); }, total: function() { var sum 0; for (var k in this.items) { sum this.items[k].price * this.items[k].num; } return sum.toFixed(2); }, render: function() { var box document.getElementById(cartList); if (!box) return; box.innerHTML ; for (var k in this.items) { var item this.items[k]; var li document.createElement(li); li.innerHTML span item.name /span button onclickCart.reduce(\ k \)-/button i item.num /i button onclickCart.add(\ k \,\ item.name \,\ item.price \,1)/button; box.appendChild(li); } document.getElementById(totalPrice).innerHTML this.total(); } };这里用items对象作为商品字典键为菜品ID值为包含名称、价格和数量的子对象。total()将每个商品的价格乘数量累加最后用toFixed(2)保留两位小数。注意reduce里对数量减到0的商品执行了delete购物车列表就不会出现数量为0的项目。内联onclick在DEMO里没问题但正式项目建议改成事件绑定避免字符串拼接错误。如果菜品价格有小数price必须以数字类型传入否则字符串拼接会出现“5.91”等于“5.91”这种错误。3.3 事件委托与动态菜品列表菜单页面里的菜品卡片往往通过循环生成直接给每个按钮绑定click事件会产生大量监听器而且后续动态添加商品时事件会丢失。常见做法是使用事件委托把监听器挂到外层列表容器上。但注意这套DEMO用的是jQuery 1.3.2还没有.on()方法得用.delegate()。$(#menuList).delegate(.add-btn, click, function() { var btn $(this); Cart.add( btn.attr(data-id), btn.attr(data-name), parseFloat(btn.attr(data-price)), 1 ); });delegate接收三个参数子元素选择器、事件类型、处理函数。事件冒泡到#menuList时由框架判断触发元素是否匹配.add-btn。这里用parseFloat转换价格就是为了避免购物车计算时的字符串类型问题。>function validateOrder(form) { var receiver form.receiver.value.trim(); var phone form.phone.value.trim(); var address form.address.value.trim(); var errors []; if (receiver.length 2) { errors.push(收货人姓名至少2个字符); } if (!/^1\d{10}$/.test(phone)) { errors.push(手机号必须是11位数字); } if (address ) { errors.push(配送地址不能为空); } if (errors.length 0) { alert(errors.join(\n)); return false; } return true; }trim()去掉首尾空格避免用户误触空格导致校验失败。正则/^1\d{10}$/要求必须1开头后面10位数字一共11位简单够用。如果要支持座机或400电话这个正则需要再放宽。校验不通过时alert集中提示比逐字段红框更容易让新手理解但生产环境一般会换成页面内联提示因为alert会被移动端浏览器拦截。4.2 订单状态流转用数组代替散落的if else订单提交后前端往往需要根据状态显示不同文案比如“待支付”“制作中”“配送中”“已完成”。直接在页面里写几十个if else很难维护更清晰的做法是维护一个状态数组用索引或标识映射到DOM。var ORDER_STATUS [ { key: pending, label: 待支付, cssClass: status-gray }, { key: paid, label: 已支付, cssClass: status-blue }, { key: making, label: 制作中, cssClass: status-orange }, { key: delivery, label: 配送中, cssClass: status-blue }, { key: done, label: 已完成, cssClass: status-green }, { key: refund, label: 已退款, cssClass: status-red } ]; function renderOrderStatus(statusKey) { var status null; for (var i 0; i ORDER_STATUS.length; i) { if (ORDER_STATUS[i].key statusKey) { status ORDER_STATUS[i]; break; } } if (status) { document.getElementById(orderStatus).innerHTML status.label; document.getElementById(orderStatus).className status.cssClass; } }这个写法把“状态文案”和“状态样式”集中在一起新增一个状态只需要往数组里加一项。renderOrderStatus只负责根据key查找并更新DOM代码里没有业务判断。如果后端返回的状态码是数字把数组里的key改成数字即可。这套DEMO的order.js里没有这个封装是常见的重构方向。4.3 评价交互星级选择与文本提交评价页面evaluate.html的核心是星级选择通常由五个li标签组成点击某个星级后前面的节点全部激活。这里用jQuery遍历下标来控制样式类。function setRating(score) { $(#starList li).each(function(index) { if (index score) { $(this).addClass(active); } else { $(this).removeClass(active); } }); $(#ratingScore).val(score); }score是用户点击的星级数从1到5。each回调里的index从0开始所以index score的判断能正确点亮前score个星星。#ratingScore是隐藏域用来在表单提交时携带评分值。注意评价文本区域一般还需要做字数限制常见做法是监听keyup事件实时统计字符数超过规定长度时截断。退款页面refund.html相对简单主要是展示退款原因和协议说明点击提交后回到订单列表。整个订单、评价、退款链路前端只负责展示和校验真正的状态持久化需要配合后端接口。但学习这套DEMO时先用localStorage把订单数据存本地就能完整走通整个流程。5. 支付倒计时与hash跳转让DEMO在演示时不露怯静态DEMO最尴尬的环节是点击“提交订单”之后不知道该跳去哪。演示时观众只能看着你打开console手动改数据。与其等着露出破绽不如直接加一个支付成功页再用hash做页面跳转十几行代码就能让流程看起来像真正的产品。(function() { var remain 60; var timer setInterval(function() { remain--; var dom document.getElementById(countdown); if (dom) { dom.innerHTML remain; } if (remain 0) { clearInterval(timer); location.hash #/order; } }, 1000); window.addEventListener(hashchange, function() { var hash location.hash; if (hash #/pay-success) { remain 60; } }); })();这段代码放在支付成功页里页面加载后开始倒计时每秒更新页面上的countdown节点倒计时归零自动跳转到订单列表。location.hash改变会触发hashchange事件所以当用户通过浏览器前进后退回到支付成功页时倒计时会重置为60秒避免继续使用旧计时器。要实现这个流程先建一个pay-success.html在里面放一个span idcountdown60/span然后让order.html提交按钮的点击事件末尾加上location.hash #/pay-success。如果不想增加新页面还可以在现有order.html里放一个隐藏的div倒计时结束后直接把div的display切换为block用样式覆盖原本的提交按钮。这种做法适合演示环境下不想增加多余文件的场景。最后补一个细节倒计时期间要给用户一个终止操作比如“取消支付”按钮否则产品经理会追着你要补单逻辑。把取消按钮绑定到clearInterval(timer)即可注意清理定时器后不要再次触发hashchange里的重置逻辑。本文还有配套的精品资源点击获取
