资讯详情

资讯详情

纯HTML和CSS实现购物车静态页面:布局细节与踩坑记录

购物车页面大概是前端练手项目里含金量被低估得最严重的一个。看起来平淡无奇但真正动手去复刻一个接近主流电商App的购物车静态界面时你会发现布局、语义、样式细节全都用得上。我最近的练习就是把这个页面从零写了一遍没有框架没有JavaScript纯HTML和CSS对照着App里的实际效果一点一点把结构、间距、颜色、圆角对齐。这篇文章把我整个实现过程、踩过的坑以及一些比较隐蔽的布局细节记录下来希望对正在练前端的你有一点参考价值。对刚学完HTML和CSS的同学来说购物车是一个特别合适的“毕业设计”它不像官网首页那么简单也不像后端管理后台那么枯燥既有列表、又有表单控件、还有固定在底部的操作栏做一轮下来你对Flex布局、定位、伪元素这些基础知识的理解会和之前完全不一样。1. 项目概览与设计思路1.1 我为什么偏偏选购物车作为练手项目先说一个我自己的判断前端练手练的不应该是“写得出来”而是“想得清楚”。所谓想得清楚是指看到一个界面之后能在脑子里把它拆成一个个模块再进一步拆成一个一个的HTML节点和CSS规则。购物车页面在这一点上极其合适。它不像三栏布局那样简单到没有挑战也不像数据大屏那样一上来就被各种不可控的图表占满注意力。购物车页面的复杂度刚刚好而且结构和交互套路高度统一做一次就能沉淀出一套可以复用在其他电商页面上的布局思路。购物车页面的界面元素也足够丰富顶部有导航栏中间是商品列表每个商品卡片里又有复选框、图片、标题、规格、价格、数量控件底部还挂着一个固定结算栏。一个页面里几乎包含了常见的电商场景元素。尤其难得的是这个页面里的所有元素都适合用布局手段来表现不需要复杂动画不需要大量的状态管理只要HTML和CSS写得扎实页面的还原度就能达到八九成。另一个让我选择它的原因是可验证性。界面做得好不好直接对照着手机里的电商App看一眼就知道。你不需要等后端接口不需要调数据库一个浏览器加一个编辑器就能完成整个验证闭环。这种即时反馈对养成“苛刻地看自己作品”的习惯很有帮助。1.2 页面结构与核心模块拆解我把这个静态页面拆成三个大区域六个子模块对应关系是这样的区域子模块涉及知识点顶部导航返回入口、页面标题、管理入口三栏Flex布局、文字居中、sticky定位商品列表商品卡片容器纵向列表组织、卡片间距控制商品卡片内部复选框、图片、信息区、数量控件Grid列布局、自定义复选框、步进器样式底部结算栏全选、合计金额、结算按钮fixed定位、z-index层级、按钮样式这样拆完页面就不再是一张图而是一堆可以单独攻克的子问题。我实际做题的顺序也基本遵循这个拆法先搭整体骨架再填充各模块最后统一调间距和颜色。列表里商品卡片如果多的话可以做成循环结构但静态阶段我建议至少写两个商品卡片一个选中、一个未选中这样能同时验证两种状态下的样式。别小看这个细节很多同学写第一个卡片的时候一切正常复制出第二个卡片后就发现复选框、对齐方式开始出问题原因通常就是某个固定尺寸写死了没考虑到多个商品并排或叠加的情况。1.3 技术选型为什么坚持纯HTML和CSS这个项目我坚持用纯HTML和CSS原因主要有三个。第一购物车静态界面的核心是布局和样式还原不是交互逻辑用不到JavaScript。第二在一个页面里同时练到Flex、Grid、定位、伪元素机会很集中没必要把时间花在搭框架上。第三先做静态界面可以为后面加JavaScript做准备——如果你连结构都写不顺手直接上动态交互只会被DOM操作搅得焦头烂额。需要说明的是我这里的“静态”不是“简陋”的意思。静态界面同样要精确到像素级复选框的勾选样式要好看数量控件的按钮要对齐价格数字的字体和大小要协调。这些细节做得到位后面接上JS才真正像那么回事。2. 从零搭建购物车页面的HTML骨架2.1 头部导航区的结构设计与语义购物车页面的头部在主流电商App里基本都是一个套路左边返回中间标题右边一个“管理”或者“编辑”入口。这种三栏结构用Flex来实现是最顺手的。header classcart-header button classheader-back aria-label返回 svg width20 height20 viewBox0 0 20 20 path dM12 4l-6 6 6 6 strokecurrentColor stroke-width1.8 fillnone stroke-linecapround stroke-linejoinround/ /svg /button h1 classheader-title购物车/h1 button classheader-edit管理/button /header这里有一点要注意很多同学会把返回箭头直接写死成一个字符比如写成‹。这种方式在原型阶段完全可行但如果要做到更接近真实效果建议改用内联SVG或者icon字体。字符箭头在不同系统的字体渲染下粗细和位置会有明显差异。我为了偷懒直接用了字符结果在真机上看发现箭头明显偏上最后还是换成了SVG。标题用h1是语义上的考虑购物车页面是整页唯一的内容主体用h1表达最高层级标题对屏幕阅读器也更友好。管理按钮用button而不是a标签是因为它表达的是一个操作动作而不是一个跳转链接。类似这种语义细节在静态页面里很容易被忽略但它是前端功底的一部分。2.2 商品列表区的DOM组织与类名约定商品列表是整个页面的重心。一个典型的商品卡片包含四块内容选中复选框、商品图片、商品名称和规格信息、价格和数量控件。我把它们组织成一个grid容器第一列是复选框第二列是商品图片第三列是商品信息区。信息区里面再分成标题、规格、底部价格数量行。section classcart-list div classcart-item label classitem-check input typecheckbox classcheck-input checked / span classcheck-area/span /label div classitem-pic img srchttps://picsum.photos/200/200?random1 alt商品图片 / /div div classitem-info h2 classitem-name经典黑白T恤 基础款 纯棉圆领短袖/h2 p classitem-spec颜色黑色尺码M/p div classitem-bottom span classitem-price¥99.00/span div classnum-stepper button classnum-btn num-minus typebutton−/button input classnum-input typetext value1 / button classnum-btn num-plus typebutton/button /div /div /div /div !-- 第二个商品卡片未选中 -- div classcart-item !-- 结构同上input不写checked -- /div /section类名方面我习惯用类似BEM但更宽松的命名方式不会让页面类名太长但又能通过.cart-item这样一眼看出它属于哪个模块。这个习惯在纯静态阶段看起来无所谓一旦后面引入JavaScript或者交给别人维护命名规范的价值就会显现出来。两个商品卡片之间我建议用margin-bottom或gap来分隔。如果使用gap记得把外层容器设为flex-column或者grid单纯div之间的margin有时候会被折叠处理起来比较烦。2.3 底部结算栏的结构设计底部结算栏的最大特点是永远固定在最下方用户无论滚动到哪里都能看到。结构上分为左右两组左边是全选入口右边是合计金额和结算按钮。footer classcart-footer label classfooter-check input typecheckbox classcheck-input / span classcheck-area/span span classfooter-check-text全选/span /label div classfooter-right span classfooter-total合计em¥99.00/em/span button classfooter-submit结算(1)/button /div /footer注意这里的合计金额我用的是em标签因为金额是全选状态下页面里最有视觉强调需求的内容em在语义上刚好合适。当然你也可以用strong不同团队的习惯不同但都要避免用一个没有语义的div加一堆类名去表达。另外在全选按钮和结算按钮之间通常还会有一个删除按钮在编辑模式下出现。静态界面阶段可以先不做但设计结构时预留一个位置后面加需求会轻松很多。3. 核心CSS布局与样式细节3.1 头部导航的Flex方案与标题居中问题头部看起来简单但真正写的时候有一个居中问题。我一开始是直接用flex的space-between来处理左边按钮、中间标题、右边按钮结果发现左右按钮的实际宽度并不相同。“管理”按钮有两个汉字宽度接近50px而返回按钮只有二十多像素space-between根本不会让标题居中。.cart-header { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; height: 48px; border-bottom: 1px solid var(--border-color); background: #fff; position: sticky; top: 0; z-index: 10; } .header-title { font-size: 18px; font-weight: 600; position: absolute; left: 50%; transform: translateX(-50%); }解决办法是把标题绝对定位到中间。left:50%加上transform:translateX(-50%)是经典写法transform平移的是元素自身宽度的一半所以无论标题多长都能居中。左右按钮留在正常文档流里靠flex的space-between落在两端。有一点要注意绝对定位的标题会脱离文档流如果页面很宽标题和左右按钮可能发生重叠。解决思路是给左右按钮设定一个最小宽度或者把两边都做成固定宽度的容器。主流电商App的标题一般会用两个字的词像“购物车”只要左右按钮最小宽度不小于标题宽度就不会有冲突。我还给header加了position:sticky和top:0让头部在列表滚动时也能保持可见。页面上商品一多页面高度超过视口是必然的这时候sticky头部和fixed结算栏搭配起来体验就非常接近真实App了。3.2 自定义复选框三种方案对比与实现原生checkbox在不同系统和浏览器上的外观差异实在太大了直接拿来做界面会和旁边的圆角、字体风格完全脱节。这里通常有三种做法第一种是保留原生input用accent-color简单改颜色省事但外观不可控第二种是彻底隐藏input用span或者伪元素绘制兼容性最好第三种是用第三方图标库配合label模拟多一次依赖。我这次采用的是第二种最稳也最干净。.check-input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; } .check-area { display: inline-block; width: 20px; height: 20px; border: 1px solid #ddd; border-radius: 50%; background: #fff; position: relative; transition: background 0.2s, border-color 0.2s; flex: none; } .check-input:checked .check-area { background: var(--primary); border-color: var(--primary); } .check-input:checked .check-area::after { content: ; position: absolute; left: 6px; top: 3px; width: 5px; height: 10px; border: 2px solid #fff; border-top: none; border-left: none; transform: rotate(45deg); }有几个关键细节第一input不能直接display:none因为这样在iOS Safari下label点击的焦点状态行为会异常更可靠的方式是用绝对定位加opacity:0把它挪出视觉范围但不移除。第二选中状态的样式主要通过input:checked .check-area这个兄弟选择器来生效。第三对勾用伪元素和旋转边框实现宽度和厚度要协调好20px的方框配2px边框、5px宽10px高的对勾是我试过比较协调的比例。这个复选框样式有一个好处完全纯CSS不需要JavaScript。静态界面里虽然点击没有实际联动但样式已经具备了后面写JS时只需要监听change事件去切换checked属性样式会自动跟随更新。3.3 商品卡片信息区的Grid布局商品卡片内部我采用的是grid三列布局列宽分别对应复选框、图片和剩余空间这样做的好处是三个区域的宽度控制非常直观。.cart-item { display: grid; grid-template-columns: 24px 96px 1fr; gap: 12px; padding: 16px; align-items: start; background: #fff; border-radius: var(--radius-card); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); } .item-pic img { width: 96px; height: 96px; object-fit: cover; border-radius: 8px; background: var(--bg-page); } .item-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }刚开始我也试过用flex嵌套来实现同样的效果外层的flex需要给信息区设置flex:1图片又要固定宽高写起来不复杂但每次调整间距都要多想一想哪个元素在伸缩。改成grid之后24px、96px、1fr三列一眼看完后续调整尺寸只需要改模板。商品图片我用了固定96x96的尺寸并且加了object-fit:cover这样即使商品图本身不是正方形也能在容器里被裁成整齐的正方形不会把卡片撑高。给图片设置背景色也很重要在图片加载失败或者还没加载出来时这个底色能让占位区域看起来不那么突兀。信息区内部标题使用单行截断规格信息单独一行放在标题下方价格和数量控件放在最底部的flex行里justify-content:space-between让价格在左、步进器在右。一个卡片内的内容层级就这么出来了。3.4 数量控件步进器的细节实现数量控件是购物车页面最容易出错的组件。常见的三个问题是按钮和输入框挤在一起不好看、输入框宽度撑破布局、垂直方向对不齐。.num-stepper { display: flex; align-items: center; border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; background: #fff; } .num-btn { width: 34px; height: 32px; background: #f7f7f7; font-size: 18px; line-height: 1; } .num-input { width: 40px; height: 32px; border: none; border-left: 1px solid var(--border-color); border-right: 1px solid var(--border-color); text-align: center; font-size: 14px; -moz-appearance: textfield; } .num-input::-webkit-outer-spin-button, .num-input::-webkit-inner-spin-button { -webkit-appearance: none; }我的做法是用一个外层容器包裹按钮和输入框容器设置border-radius和overflow:hidden内部元素不再单独设圆角。这样整个步进器看起来像一个整体边缘不会出现拼接的缝隙。输入框的上下滚动箭头要手动去掉否则在Chrome下会显示两个小三角。尺寸上按钮宽34px、输入框宽40px、高度统一32px。输入框宽度太小数字多时会溢出太大又会把步进器拉到不平衡。40px配合font-size:14px和text-align:center最多能显示两位数对于购物车场景基本够用。还有一点配色细节按钮背景和输入框背景稍微区分一下按钮用浅灰输入框用白色视觉上会清晰很多。如果全用同色用户很难一眼找到可点击区域。3.5 固定底部结算栏的定位与层级底部结算栏肯定要用fixed这个没什么可犹豫的但fixed有一个容易忽略的问题它会脱离文档流导致页面底部内容被挡住。解决方法是给body加一个padding-bottom数值要大于结算栏高度最好再富裕出一点空间。body { padding-bottom: calc(var(--footer-height) 16px); } .cart-footer { position: fixed; left: 0; right: 0; bottom: 0; height: var(--footer-height); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; background: #fff; box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.05); z-index: 100; padding-bottom: env(safe-area-inset-bottom); }我用了阴影来模拟结算栏顶部的分隔线box-shadow: 0 -1px 4px rgba(0,0,0,0.05)这条阴影很淡靠近看才有存在感。如果你想要更明显的分隔线也可以加border-top但阴影和边框不要同时用否则视觉会很脏。z-index设置为100已经足够避开绝大多数内容层级。如果页面里有弹窗、抽屉等其他浮层再单独约定层级。4. 像素级还原间距、颜色与字体4.1 用CSS变量管理全页设计规范当页面里的颜色和间距开始多起来纯粹靠脑子记忆就很危险。比如主色在复选框、价格、结算按钮里各用了一次如果三处分别写死后面想调整品牌色就要逐个找。我这次在页面顶部定义了一组变量:root { --primary: #e93b3d; --border-color: #eee; --bg-page: #f5f5f5; --text-main: #333; --text-sub: #999; --radius-card: 12px; --radius-control: 16px; --footer-height: 56px; }用变量之后调整设计统一性变得很快。比如发现按钮圆角太大了只需要改一处。这种习惯在大型项目里几乎是一种强制规范但即使在小型练手项目里提前用上也能帮你建立视觉一致性的意识。如果你用的是VSCode装一个支持CSS变量跳转的插件后面修改变量时会很舒服。不过这类工具属于锦上添花就算不用变量本身的收益也已经很可观了。4.2 价格数字的排版技巧价格是购物车页面里最重要的信息但很多人会忽略它的排版。直接写¥99.00人民币符号会跟数字一样大视觉上很钝。我的做法是把符号缩小并且让价格字体稍重一些。.item-price { font-size: 18px; font-weight: 700; color: var(--price-color); } .footer-total em { font-style: normal; color: var(--price-color); font-size: 20px; font-weight: 700; }如果你用的是文本符号可以用一些技巧来缩小符号但有些字体渲染下¥字符的定位会不稳定。更稳妥的方案是用小号span包裹符号或者直接使用一个等宽的数字字体栈。数字字体方面可以给价格设置font-variant-numeric: tabular-nums让数字以等宽方式渲染这样金额从99变成199时数字不会左右晃动。如果用一个局部字体栈比如Helvetica Neue配合sans-serif数字的观感也会好很多。顺便提一下结算栏里的大金额文字节奏要拉开“合计”用小号浅色后面的金额用大号红色加粗一眼扫过去用户就知道自己需要付多少钱。4.3 圆角、阴影和边框的使用分寸卡片加圆角和阴影能让页面层次感更强但特别容易用过头。我的习惯是卡片用12px圆角阴影用0 2px 8px rgba(0,0,0,0.04)非常淡分隔线用浅灰的border-bottom按钮类控件圆角用16px比卡片更圆一点形成对比。关于1px边框还有一个小技巧在移动设备的高分辨率屏上普通border:1px实际上会显得偏粗。更精细的做法是用transform:scaleY(0.5)把伪元素边框压细或者用背景渐变模拟0.5px的线。我这个静态页面主要做原型展示所以用了普通边框如果是正式上线的还原建议在头部和卡片分隔线这些地方做处理。还有一个容易踩的小坑给卡片加box-shadow和border-top同时存在时可能在顶部出现一条意外的亮边。因为阴影在顶部也会有少量投影与边框叠加后看起来像双层。避免的方式是二者只取其一。5. 实战中踩过的坑与排查技巧5.1 复选框在Safari上不听话这是我的第一个教训。一开始我给input直接写样式Chrome里一切都好但打开Safari一看复选框还是系统原生的样子怎么改都不动。原因是Safari对原生控件外观的自定义能力非常有限只有少数几个属性生效。最后只能回归最稳妥的方案彻底隐藏原生input用span重绘。前面代码里已经写得很详细这里不再重复。想提醒的是调试的时候不要只在Chrome里看至少用模拟器看一下Safari和安卓原生浏览器的效果。另外隐藏input的写法是opacity:0加绝对定位不要随手给display:none。display:none会让点击事件和焦点行为变得奇怪特别是在label包裹input的场景下。5.2 在线图片加载导致布局抖动测试时我用的是在线占位图第一次打开页面时列表会出现一阵明显的跳动图片还没加载时卡片高度很矮图片加载后突然抽高把下面卡片往下推。这种情况在静态界面里显得特别糙。解决办法就是提前给图片容器占好尺寸固定width和height或者用aspect-ratio声明比例。我采用的是固定96x96加上object-fit:cover无论原图比例如何都不会破坏布局。如果你希望图片能适配不同屏幕也可以用aspect-ratio: 1配合width:100%这样容器会维持正方形比例。不过我这次更倾向于固定像素因为购物车列表里的图片尺寸在主流App里基本都是定值固定方式更接近真实。5.3 fixed底部栏遮挡滚动内容fixed底部栏脱离文档流后页面最后一条商品很可能会被盖住。只给body加padding-bottom还不够如果padding-bottom恰好等于footer高度最后一条内容的底部会和footer贴得严严实实视觉上像粘在一起。我建议padding-bottom比footer高度多出16px左右留一点呼吸空间。还有一个更细致的做法在列表容器上设置margin-bottom或者给结算栏留一个占位空div。但占位div多一个元素我一般不推荐。padding-bottom是最直接的。5.4 小屏适配与底部安全区域在窄屏手机上商品名称如果不做截断会把整个卡片撑高价格和数量控件甚至会挤到对齐错乱。所以我给.item-name加了单行截断。你也可以做成两行截断用-webkit-line-clamp:2但要注意配合overflow:hidden和display:-webkit-box才能生效。至于iPhone底部的安全区域结算栏需要处理env(safe-area-inset-bottom)。如果直接就写padding:0 16px在全面屏手机上按钮会被底部手势条压住点击区域也变小。我之前的做法是把footer的padding-bottom改成calc(8px env(safe-area-inset-bottom))这样底部间距在真机上才是安全的。这一条在浏览器模拟器里看不到效果需要真机测试。如果你手头没有iPhone可以在Chrome的开发者工具里选择iPhone机型模拟虽然不完全精确但至少能验证方向。5.5 相邻边框线的重影问题这个页面里步进器的按钮、输入框、外层容器之间涉及多条边框。如果不加处理三条边框拼在一起会显得很重甚至出现粗细不匀。我的做法是只在容器上画完整的border内部元素只保留需要的分隔线输入框加左右border按钮不加边框这样最终视觉上就是一条1px的线。同样的道理也适用于卡片之间。如果两张卡片都有上下边框中间会出现两条线看起来比预期更粗。要么卡片之间用间距和阴影分隔要么统一使用border-bottom并在末尾去掉最后一条。从主流App的视觉习惯看间距加阴影的方式更干净。6. 完整Demo代码可直接运行的购物车静态界面下面这套代码可以直接保存成一个html文件浏览器打开就能看效果。商品图片用的是在线占位图网络正常的情况下会自动加载。你也可以替换成你自己的图片地址。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title购物车静态界面Demo/title style :root { --primary: #e93b3d; --price-color: #e93b3d; --border-color: #eee; --bg-page: #f5f5f5; --text-main: #333; --text-sub: #999; --radius-card: 12px; --radius-control: 16px; --footer-height: 56px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { background: var(--bg-page); font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, Arial, sans-serif; color: var(--text-main); padding-bottom: calc(var(--footer-height) 16px); } button { border: none; background: none; cursor: pointer; font: inherit; } /* header */ .cart-header { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; height: 48px; border-bottom: 1px solid var(--border-color); background: #fff; position: sticky; top: 0; z-index: 10; } .header-back, .header-edit { display: flex; align-items: center; color: var(--text-main); font-size: 16px; } .header-title { font-size: 18px; font-weight: 600; position: absolute; left: 50%; transform: translateX(-50%); } /* list */ .cart-list { padding: 12px; display: flex; flex-direction: column; gap: 12px; } .cart-item { display: grid; grid-template-columns: 24px 96px 1fr; gap: 12px; padding: 16px; align-items: start; background: #fff; border-radius: var(--radius-card); box-shadow: 0 2px 8px rgba(0,0,0,0.04); } .item-pic img { width: 96px; height: 96px; object-fit: cover; border-radius: 8px; background: var(--bg-page); } .item-name { font-size: 15px; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .item-spec { font-size: 12px; color: var(--text-sub); margin-top: 6px; } .item-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; } .item-price { font-size: 18px; font-weight: 700; color: var(--price-color); } /* stepper */ .num-stepper { display: flex; align-items: center; border: 1px solid var(--border-color); border-radius: var(--radius-control); overflow: hidden; background: #fff; } .num-btn { width: 34px; height: 32px; background: #f7f7f7; font-size: 18px; line-height: 1; } .num-input { width: 40px; height: 32px; border: none; border-left: 1px solid var(--border-color); border-right: 1px solid var(--border-color); text-align: center; font-size: 14px; -moz-appearance: textfield; } .num-input::-webkit-outer-spin-button, .num-input::-webkit-inner-spin-button { -webkit-appearance: none; } /* custom checkbox */ .item-check, .footer-check { display: inline-flex; align-items: center; cursor: pointer; position: relative; } .check-input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; } .check-area { display: inline-block; width: 20px; height: 20px; border: 1px solid #ddd; border-radius: 50%; background: #fff; position: relative; transition: background 0.2s, border-color 0.2s; flex: none; } .check-input:checked .check-area { background: var(--primary); border-color: var(--primary); } .check-input:checked .check-area::after { content: ; position: absolute; left: 6px; top: 3px; width: 5px; height: 10px; border: 2px solid #fff; border-top: none; border-left: none; transform: rotate(45deg); } /* footer */ .cart-footer { position: fixed; left: 0; right: 0; bottom: 0; height: var(--footer-height); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; background: #fff; box-shadow: 0 -1px 4px rgba(0,0,0,0.05); z-index: 100; padding-bottom: env(safe-area-inset-bottom); } .footer-check-text { margin-left: 6px; font-size: 14px; } .footer-right { display: flex; align-items: center; gap: 12px; } .footer-total { font-size: 14px; color: var(--text-sub); } .footer-total em { font-style: normal; color: var(--price-color); font-size: 20px; font-weight: 700; } .footer-submit { height: 36px; padding: 0 20px; border-radius: 18px; background: var(--primary); color: #fff; font-size: 14px; font-weight: 600; box-shadow: 0 4px 10px rgba(233,59,61,0.2); } /style /head body header classcart-header button classheader-back aria-label返回 svg width20 height20 viewBox0 0 20 20 path dM12 4l-6 6 6 6 strokecurrentColor stroke-width1.8 fillnone stroke-linecapround stroke-linejoinround/ /svg /button h1 classheader-title购物车/h1 button classheader-edit管理/button /header section classcart-list !-- 商品1选中状态 -- div classcart-item label classitem-check input typecheckbox classcheck-input checked / span classcheck-area/span /label div classitem-pic img srchttps://picsum.photos/200/200?random1 alt商品图片 / /div div classitem-info h2 classitem-name经典黑白T恤 基础款 纯棉圆领短袖/h2 p classitem-spec颜色黑色尺码M/p div classitem-bottom span classitem-price¥99.00/span div classnum-stepper button classnum-btn num-minus typebutton−/button input classnum-input typetext value1 / button classnum-btn num-plus typebutton/button /div /div /div /div !-- 商品2未选中状态 -- div classcart-item label classitem-check input typecheckbox classcheck-input / span classcheck-area/span /label div classitem-pic img srchttps://picsum.photos/200/200?random2 alt商品图片 / /div div classitem-info h2 classitem-name运动水杯 便携大容量 户外健身吸管杯/h2 p classitem-spec颜色蓝色容量750ml/p div classitem-bottom span classitem-price¥69.00/span div classnum-stepper button classnum-btn num-minus typebutton−/button input classnum-input typetext value1 / button classnum-btn num-plus typebutton/button /div /div /div /div /section footer classcart-footer label classfooter-check input typecheckbox classcheck-input / span classcheck-area/span span classfooter-check-text全选/span /label div classfooter-right span classfooter-total合计em¥99.00/em/span button classfooter-submit结算(1)/button /div /footer /body /html运行之后你应该能看到一个接近主流电商App观感的购物车列表头部置顶、两张商品卡分别演示选中和未选中状态、底部结算栏固定不动“合计”里的金额和“结算(1)”形成清晰的视觉重点。如果想练练手可以试着改几个地方把勾选样式改成方形勾选框、把步进器改成输入框直接可编辑、或者给卡片加删除按钮的占位。每个改动都会牵动样式足够你琢磨一阵了。7. 从静态到动态后续升级方向7.1 全选和单选联动静态界面完成后最自然的升级就是加JavaScript。全选和单选联动是购物车交互里最经典的一块点击全选按钮所有商品的checked都变成true取消其中一个全选状态变成false。实现思路是监听所有checkbox的change事件用一个方法扫描当前选中数量只要有一个未选中全选就取消勾选。这一步会让前面的自定义复选框真正发挥价值因为样式已经准备好JS只需要操控checked属性视觉会自动跟随。7.2 数量变化与合计金额实时计算数量步进器的交互也是下一步必做的点击加号数量加一点击减号数量减一输入框直接输入数字时还要处理边界值不能小于1不能大于库存。每次数量变化都需要重新计算当前商品的小计和整个列表的合计。这里有个细节数量输入框的值是字符串做加法之前一定要用parseInt或者Number转换否则会出现1加1的字符串拼接这种低级bug。另一个细节是input的change事件只在失焦时触发如果你希望实时计算监听input事件会更合适。7.3 商品移除与本地存储再往后做可以给每个商品卡片增加移除按钮以及把购物车数据存到localStorage。购物车的数据结构建议用一个数组每个商品对象包含id、名称、规格、价格、数量、选中状态页面的渲染就基于这个数组来完成。当用户刷新页面从localStorage恢复数据购物车就能保持连续性。这一步其实就是从“静态界面”向“完整业务页面”跨进的关键。你会发现静态阶段定义好的DOM结构和类名会让后续的JS逻辑清晰很多。这也是我坚持先做静态页面的一个重要原因。7.4 静态经验如何衔接到框架学习如果你接下来打算学React或Vue这个购物车页面也是很合适的移植对象。商品卡片可以抽成一个商品项组件结算栏是一个独立组件页面则是组装这些组件的容器。静态阶段写下的HTML结构和CSS类名几乎可以一比一迁移到组件的模板中。框架里的状态管理其实就是把原来JS里维护的购物车数据数组改成响应式的数据源。理解了静态界面和交互逻辑之间的边界后面理解数据驱动视图会顺畅很多。最后说点个人体会。我写这个静态购物车页面最大的收获不是学会了某个属性而是养成了先拆结构、再写代码、最后抠细节的习惯。很多同学上手就写写到一半发现模块之间互相影响再回头调整结构耗时又容易乱。先从静止的HTML骨架开始把结构定清楚后面的CSS和JS都会顺很多。购物车这个页面虽然小但足够让你完整走一遍这个流程。如果你也在练前端建议真的动手写一遍而不是只看别人的代码。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →