简介:这份HTML5购物网站案例面向前端初学者、课程设计或毕业项目参考者,围绕带登录入口的静态购物网站展开,帮助读者理解从页面搭建到购物车交互的完整实现思路。压缩包共48个文件,以4个HTML页面、5个CSS样式表、8个JavaScript脚本及30余张PNG/JPG图片为主,并含1个JSON配置,整体约5.28MB,覆盖页面结构、样式美化、轮播与三级菜单、登录验证和购物车逻辑等模块。已有1043人学习下载。读者可从中参考HTML5表单与客户端存储、CSS3过渡动画和响应式布局、jQuery事件处理与动态DOM操作,以及localStorage保存购物车商品、实时计算总价等做法,适合用来拆解静态电商站点的目录组织与交互实现思路。
1. 只学过前端三件套,能不能交出一个 html5 购物网站带登录
课程设计选题一下来,十个里有六个是电商站,但真正能跑起来的没几个,大部分人卡在同一个地方:只学了 HTML、CSS、JS 和一点 jQuery,没有后端接口,于是登录怎么存、购物车怎么记、刷新之后数据还在不在,全靠现场编。这套 html5 购物网站带登录 的方案就是奔着这个卡点去的——纯静态文件,双击index.html就能打开,注册登录、商品列表、分类筛选、加入购物车、确认订单、我的订单全流程闭环,数据用 localStorage 落地,不依赖服务器和数据库。它适合两类人:一类是要交毕设或课设、需要一套结构完整、讲得清楚设计取舍的方案;另一类是页面已经写得挺熟、但没做过带状态的小项目,想照着走一遍完整流程的开发者。下面按需求、架构、实现、避坑、验收的顺序推进,每一章都能对应到具体的文件和代码。
2. 需求拆解与技术选型:四条主线、七个页面和一份目录约定
动手写第一行 HTML 之前,先把"要做成什么样"钉死。静态购物站最容易犯的错不是技术难,而是范围失控——一会儿想加秒杀,一会儿想加优惠券,最后连登录都没跑通。我的习惯是先切出四条主线,每条主线对应几个页面、几张数据表,剩下的功能一律进"二期"。
2.1 功能边界:登录、商品、购物车、结算四条主线
第一条是账号主线,包含注册、登录、退出、登录态保持、未登录拦截。第二条是商品主线,包含列表展示、分类筛选、关键词搜索、详情查看。第三条是购物车主线,包含加入、改数量、删除、勾选、合计。第四条是交易主线,包含确认订单、填写收货信息、生成订单号、订单列表。
这四条主线之间存在明确的依赖关系:购物车要读登录态决定存哪个 key,结算要读购物车决定算多少钱,订单要读登录态决定归属谁。把依赖关系画清楚,后面写代码时就不会出现"购物车页要引三个 js 还互相打架"的情况。
| 页面 | 文件 | 核心功能 | 依赖数据 |
|---|---|---|---|
| 首页 | index.html | 轮播、推荐位、分类入口 | goods |
| 商品列表 | list.html | 分类筛选、搜索、分页、加购 | goods、cart |
| 商品详情 | detail.html | 图文详情、数量选择、加购 | goods、cart |
| 购物车 | cart.html | 改数量、删除、勾选、合计 | cart、goods |
| 登录注册 | login.html | 双 Tab 切换、表单校验、回跳 | users、current_user |
| 确认订单 | order.html | 收货信息、金额核对、提交 | cart、orders |
| 个人中心 | mine.html | 用户信息、我的订单列表 | current_user、orders |
七个页面里有五个需要登录态,所以守卫脚本要放在公共位置,不能每个页面复制一遍。这一点在后面第 4 章会展开。
2.2 技术选型:原生 JS 和 jQuery 不是二选一,是分工
很多人纠结"都 2024 年了还用不用 jQuery"。在这个项目里,答案不是用不用,而是各干各的活。
原生 JS 负责三件事:localStorage 的读写与序列化、JSON.parse/stringify的容错、ES6 模板字符串拼 HTML。这三件事 jQuery 封装的收益很低,而且localStorage本身就是同步 API,套一层$.ajax式的思维反而容易写错。
jQuery 负责另外三件事:DOM 批量操作($wrap.html(html))、事件委托(.on('click', '.btn-add', fn))、以及少量动画(加购飞入、提示条淡出)。尤其是事件委托,商品卡片是动态渲染出来的,如果用document.querySelectorAll('.btn-add').forEach(btn => btn.onclick = ...),每次重新渲染都得重新绑一遍,漏一次就是"点了没反应"。用$(document).on('click', '.btn-add', fn)绑在父级上,渲染多少次都不用管。
提示:不引入 Vue/React 不是技术保守,是答辩现场的现实约束。静态文件加 CDN 引入 jQuery,双击就能跑;一旦上构建工具,现场换个电脑、断个网,
node_modules没装全,演示直接卡住。选型的第一原则是"交付场景能跑"。
jQuery 用 3.7 的压缩版,通过<script src="js/jquery-3.7.1.min.js"></script>本地引入,不走外链 CDN,理由同上——断网也能演示。
2.3 页面清单与目录约定
目录结构提前定好,后面加文件就不会乱。我一般按"页面平铺 + 资源分目录"的方式组织,好处是每个 HTML 的引用路径都是同级的css/、js/,不会出现../../这种一看就头疼的相对路径。
shopping-site/ ├── index.html # 首页:轮播 + 推荐位 + 分类入口 ├── list.html # 商品列表:分类、搜索、分页 ├── detail.html # 商品详情:图文 + 数量选择 + 加购 ├── cart.html # 购物车:改数量、删除、勾选、合计 ├── login.html # 登录 / 注册双 Tab ├── order.html # 确认订单 + 提交 ├── mine.html # 个人中心 + 我的订单 ├── css/ │ ├── base.css # CSS 变量、重置、通用按钮与提示条 │ └── page.css # 各页面样式 + 媒体查询 ├── js/ │ ├── db.js # localStorage 统一封装 │ ├── data.js # 商品初始数据(常量数组,非 json 文件) │ ├── guard.js # 登录态守卫 │ ├── cart.js # 购物车增删改与金额计算 │ ├── goods.js # 商品渲染、筛选、搜索 │ └── order.js # 下单与订单列表 └── img/ # 商品图与占位图这里有个关键决定:商品数据放在js/data.js里作为一个常量数组,而不是data/goods.json。原因在避坑那一章会细说,简单讲就是本地file://协议下读 json 会被拦,而<script>引入的 js 文件不受影响。
CSS 变量统一放在base.css的:root里,主色、圆角、间距各定一套,后面调风格只改一处:
/* base.css —— 全局变量与重置 */ :root { --brand: #e93b3d; --text: #333; --text-light: #999; --bg: #f5f5f5; --radius: 8px; --gap: 12px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { background: var(--bg); color: var(--text); font-size: 14px; } .btn { border: none; border-radius: var(--radius); cursor: pointer; }box-sizing: border-box这一行必须放在最前面,不然后面所有带 padding 的卡片宽度都会超出容器,移动端一测就露馅。
3. 总体架构与数据模型:三层结构、四张表和一条渲染链路
静态站没有后端,但它仍然需要分层。分层不是为了好看,是为了让"数据从哪来、在哪改、怎么显示"三件事不搅在一起。我见过太多课设把 localStorage 的读写散落在七八个文件里,最后购物车金额对不上,查两小时发现是某处JSON.parse没加 try-catch。
3.1 分层:视图层、状态层、数据层的边界
整个站点按三层切:
视图层就是七个 HTML 页面加上page.css,只负责结构和样式,不写业务逻辑。页面里最多出现一个<script>,用来调初始化函数。
状态层是guard.js、cart.js、goods.js、order.js这四个文件,负责"当前登录的是谁、购物车里有什么、订单列表长什么样"。所有业务判断都在这层,页面拿到的永远是已经算好的结果。
数据层是db.js,唯一一个直接碰localStorage的文件。其他任何文件都不允许出现localStorage.getItem,只能调DB.get()/DB.set()。
这条规矩听起来死板,但它解决的是最大的一个坑:key 拼写不一致。今天写cart,明天写shop_cart,后天写cartList,数据就"丢"了,而且丢得无声无息。统一入口之后,key 前缀只在一处定义。
// db.js —— 唯一的 localStorage 出入口 const DB = { prefix: 'shop_', // 读取并反序列化,解析失败时返回默认值而不是抛异常 get(key, def = null) { try { const raw = localStorage.getItem(this.prefix + key); return raw === null ? def : JSON.parse(raw); } catch (e) { console.error('[DB] 解析失败:', key, e); return def; } }, // 写入前统一序列化,调用方不用关心 JSON 细节 set(key, val) { localStorage.setItem(this.prefix + key, JSON.stringify(val)); }, remove(key) { localStorage.removeItem(this.prefix + key); } };get里的try-catch不是防御性编程的摆设。用户手动在 DevTools 里改过值、或者上一版数据结构变更导致旧数据格式不匹配,JSON.parse都会直接抛错,整个页面白屏。有了兜底,最差情况是"数据回到初始状态",而不是"页面打不开"。
3.2 数据模型:用户、商品、购物车、订单四张表
没有数据库,但脑子里得有表。四张表加一个会话对象,字段设计如下:
| 表名 | key | 关键字段 | 说明 |
|---|---|---|---|
| 用户表 | shop_users | id, username, password, phone, createTime | password 存哈希值,不存明文 |
| 商品表 | shop_goods | id, name, price, originPrice, cover, category, stock, sales, desc | price 单位为「分」 |
| 购物车表 | shop_cart_ | goodsId, count, checked | 按用户隔离,未登录用 guest |
| 订单表 | shop_orders | orderNo, userId, items, total, address, status, createTime | total 单位为「分」 |
| 会话对象 | shop_current_user | id, username | 只存最小必要信息 |
三个设计点值得单独说。
第一,价格用整数「分」存储。129.00 元存成12900,展示时再除以 100。这不是洁癖,是必须——0.1 + 0.2 === 0.30000000000000004在购物车合计里会实打实地显示出来,验收的时候老师一眼就能看见。
第二,购物车 key 带用户 ID。同一个浏览器里,A 登录加了三个商品,退出,B 登录,购物车必须是空的。如果用固定 key,两个人的购物车会混在一起,这是最容易被追问的点。
第三,订单里存商品快照而不只是 goodsId。商品价格改了、下架了,历史订单的金额不能跟着变。items里存{ goodsId, name, price, count },这样订单详情页不用回查商品表也能完整展示。
商品表的初始数据写成常量数组:
// data.js —— 商品初始数据,首次运行时写入 localStorage const GOODS_SEED = [ { id: 1001, name: '无线蓝牙耳机', price: 12900, originPrice: 19900, cover: 'img/p1.jpg', category: '数码', stock: 50, sales: 328, desc: '半入耳设计,单次续航 6 小时,充电仓总计 24 小时。' }, { id: 1002, name: '纯棉基础款T恤', price: 5900, originPrice: 9900, cover: 'img/p2.jpg', category: '服饰', stock: 120, sales: 1024, desc: '260g 重磅纯棉,圆领不易变形,四色可选。' }, { id: 1003, name: '便携保温杯 500ml', price: 8900, originPrice: 12900, cover: 'img/p3.jpg', category: '家居', stock: 80, sales: 217, desc: '316 不锈钢内胆,保温 12 小时,一键弹盖。' } ];真实项目里这个数组会有几十条,建议至少准备 12 条、覆盖 4 个分类,这样分页和筛选才有东西可演示。
3.3 一次渲染的完整链路:从 localStorage 到商品卡片
从数据到页面,走的是"取数 → 过滤 → 拼串 → 注入 → 委托"五步。以列表页为例:
// goods.js —— 商品渲染主流程 // 1) 取数:首次运行把种子数据落库,之后都从 localStorage 读 function initGoods() { const saved = DB.get('goods'); if (saved && saved.length) return saved; DB.set('goods', GOODS_SEED); return GOODS_SEED; } // 2) 转义:商品名可能含 < > " 等字符,直接拼进模板会破坏结构 function escapeHtml(str) { return String(str).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]); } // 3) 渲染:拼串 + 一次注入,比循环 append 快得多 function renderGoods(list, $wrap) { if (!list.length) { $wrap.html('<li class="empty">没有找到相关商品</li>'); return; } const html = list.map(g => ` <li class="goods-card"><!-- login.html —— 双 Tab 结构 --> <div class="tabs"> <button class="tab active">// 用户名:3-16 位,中英文、数字、下划线 const USERNAME_RE = /^[a-zA-Z0-9_\u4e00-\u9fa5]{3,16}$/; // 注册 $('#register-form').on('submit', function (e) { e.preventDefault(); const username = $.trim($('#reg-username').val()); const password = $('#reg-password').val(); const phone = $.trim($('#reg-phone').val()); if (!USERNAME_RE.test(username)) return toast('用户名需 3-16 位,中英文、数字、下划线'); if (password.length < 6) return toast('密码至少 6 位'); if (phone && !/^1\d{10}$/.test(phone)) return toast('手机号格式不正确'); const users = DB.get('users', []); // 重名校验要忽略大小写,否则 Test 和 test 是两个人 if (users.some(u => u.username.toLowerCase() === username.toLowerCase())) { return toast('该用户名已被注册'); } users.push({ id: Date.now(), username, password: hash(password), phone, createTime: new Date().toLocaleString() }); DB.set('users', users); toast('注册成功,请登录'); $('.tab[data-tab="login"]').trigger('click'); });toast是一个自己写的轻提示函数,三行就够:创建一个固定定位的 div,塞文字,setTimeout两秒后移除。不用引任何 UI 库。
重名校验的toLowerCase()是血泪经验。如果不加,用户注册了Admin,再有人注册admin也能成功,登录的时候两个账号没法区分,测试时一测就出问题。
4.2 会话保持与页面守卫:current_user 加回跳地址
登录成功后,往shop_current_user里写{ id, username }。为什么不全量存用户对象?因为密码哈希不该在会话里多待一份,而且用户改了昵称之后,会话里的旧数据会跟用户表不一致。
真正体现设计水平的是守卫。最朴素的做法是"进订单页时判断,没登录就跳首页",问题是用户登录完回不到刚才想去的页面。正确做法是把当前地址编进redirect参数:
// guard.js —— 页面守卫,页面顶部同步调用 function requireLogin() { const user = DB.get('current_user'); if (user) return user; // 记住当前页(含查询串),登录后原路返回 const current = location.pathname.split('/').pop() + location.search; const redirect = encodeURIComponent(current); location.href = 'login.html?redirect=' + redirect; return null; } // 登录成功后读取回跳地址 function backAfterLogin() { const redirect = new URLSearchParams(location.search).get('redirect'); location.href = redirect ? decodeURIComponent(redirect) : 'index.html'; }pathname.split('/').pop()是为了兼容file://打开时pathname带完整磁盘路径的情况,只取文件名部分。encodeURIComponent必须加,因为list.html?cat=数码这种带中文的地址不编码会丢参数。
页面里的用法是"先守卫、后初始化":
// order.html 的入口脚本 $(function () { const user = requireLogin(); // 未登录会直接跳走,后面不执行 if (!user) return; initOrderPage(user); });注意requireLogin之后必须return。location.href赋值是异步跳转,后面的代码在当前事件循环里还会继续执行,不加return会出现"跳转瞬间闪一下订单内容"的现象。
4.3 购物车增删改与金额计算:以「分」为单位的整数方案
购物车的核心不是增删改,是"同一件商品加两次不能出现两条记录"和"合计金额必须精确"。
// cart.js —— 购物车核心 const Cart = { // 未登录用 guest 车,登录后用 cart_<uid>,天然隔离 key() { const u = DB.get('current_user'); return u ? 'cart_' + u.id : 'cart_guest'; }, list() { return DB.get(this.key(), []); }, // 加购:已存在则累加数量,不存在则新增 add(goodsId, count = 1) { const list = this.list(); const item = list.find(i => i.goodsId === goodsId); if (item) { item.count += count; } else { list.push({ goodsId, count, checked: true }); } DB.set(this.key(), list); this.badge(); }, // 改数量:数量小于 1 时按删除处理 update(goodsId, count) { let list = this.list(); const item = list.find(i => i.goodsId === goodsId); if (!item) return; if (count < 1) { list = list.filter(i => i.goodsId !== goodsId); } else { item.count = count; } DB.set(this.key(), list); }, // 合计:全程整数(分)运算,最后一步才转元 total(list) { const goods = DB.get('goods', []); const cents = list .filter(i => i.checked) .reduce((sum, i) => { const g = goods.find(x => x.id === i.goodsId); return g ? sum + g.price * i.count : sum; }, 0); return (cents / 100).toFixed(2); }, // 角标:只要已加购的总件数,不受勾选影响 badge() { const n = this.list().reduce((s, i) => s + i.count, 0); $('.cart-badge').text(n).toggle(n > 0); } };total里先price * count累加成整数分,再统一/100转元,全程没有小数参与运算,浮点误差从源头被掐断。如果反过来,先转成元再乘再累加,59.00 * 3就可能变成177.00000000000003。
购物车页的渲染要点是"删除用过滤、勾选用标记":
// 删除某一项:filter 生成新数组,避免 splice 在循环里改索引 $('#cart-list').on('click', '.btn-del', function () { const id = Number($(this).data('id')); const list = Cart.list().filter(i => i.goodsId !== id); DB.set(Cart.key(), list); renderCart(); });filter而不是splice,是因为splice会改变原数组长度,如果外面还套着一层forEach,索引会错位,表现就是"删一个,少两个"。这个 bug 排查起来极费时间,直接用不可变写法就没这问题。
结算的时候把购物车按checked过滤出来,写进订单:
// order.js —— 提交订单 function submitOrder(user, address) { const goods = DB.get('goods', []); const cart = Cart.list().filter(i => i.checked); if (!cart.length) return toast('请先勾选要结算的商品'); // 快照:把商品名和当时的价格一起存进订单,防止后续改价影响历史订单 const items = cart.map(i => { const g = goods.find(x => x.id === i.goodsId); return { goodsId: g.id, name: g.name, price: g.price, count: i.count }; }); const total = items.reduce((s, i) => s + i.price * i.count, 0); const orders = DB.get('orders', []); orders.unshift({ orderNo: createOrderNo(), userId: user.id, items, total, address, status: '待付款', createTime: new Date().toLocaleString() }); DB.set('orders', orders); // 只清掉已结算的,未勾选的留在购物车 DB.set(Cart.key(), Cart.list().filter(i => !i.checked)); location.href = 'mine.html'; }订单号用时间戳加三位随机数,保证同一秒内多次下单也不会重复:
function createOrderNo() { const d = new Date(); const p = n => String(n).padStart(2, '0'); const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}` + `${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`; return stamp + Math.floor(Math.random() * 900 + 100); }5. 避坑与常见问题排查:静态站最容易翻车的五个点
下面这五条,全是静态购物站的高频事故。每一条我都见过至少两次,症状相似但原因不同,按"现象 → 原因 → 解决"记下来,出问题时能省掉大半排查时间。
5.1 刷新之后购物车清零
现象:加购三件商品,角标显示 3,按 F5 刷新,角标消失,购物车页显示空。
原因:数据只存在内存里。很多人把购物车写成let cart = []这个模块级变量,push完就直接渲染,从没调过DB.set;或者调了,但 key 写成了'cart',读的时候用的是'cart_' + uid,读写不一致。
解决:坚持"写操作必落库"这一条纪律,任何修改购物车的函数末尾都要有一行DB.set(this.key(), list)。排查时打开 DevTools 的 Application → Local Storage,看shop_cart_xxx这个 key 到底存不存在、内容对不对。如果 key 存在但内容是旧值,说明写入被覆盖了;如果 key 根本不存在,说明写入路径没走到。
5.2 合计金额出现 0.30000000000000004
现象:两件 0.1 元的商品,合计显示成0.30000000000000004。
原因:JavaScript 的 Number 是 IEEE 754 双精度浮点,十进制小数在二进制里无法精确表示,加减乘除都会累积误差。
解决:所有价格字段以「分」为整数存储,运算全程用整数,只在最后展示时(cents / 100).toFixed(2)。如果数据源已经是小数(比如从别处导入),先在入口处Math.round(price * 100)转成分,再进入计算链路。这条改起来工作量不大,但必须一开始就定,中途改会牵动所有读写价格的地方。
5.3 动态渲染出来的按钮点不动
现象:商品列表第一次加载能加购,切换分类重新渲染后,按钮完全没反应,控制台也不报错。
原因:事件绑在了被替换掉的 DOM 节点上。$('.btn-add').on('click', fn)这种写法只对执行那一刻存在的元素生效,$wrap.html(html)一注入,旧节点被销毁,新节点上没有任何监听器。
解决:一律用事件委托,绑在不会被替换的祖先元素上——$('#goods-list').on('click', '.btn-add', fn)。第二个参数是选择器,jQuery 会在事件冒泡到#goods-list时判断event.target是否匹配,匹配才触发。判断标准很简单:如果这行绑定代码执行时,目标元素还没出现在 DOM 里,就必须用委托。
5.4 退出登录后按返回键还能看到订单
现象:点退出,跳回首页,按浏览器后退,订单页的内容又显示出来了。
原因:只清了界面没清状态,或者清了但页面没做进入时的校验。location.href跳转不等于销毁页面,浏览器前进后退会走缓存,$(function(){})里的初始化代码不一定重跑。
解决:两道保险。第一,退出登录时不仅要DB.remove('current_user'),还要把依赖登录态的界面元素一起清掉;第二,所有需要登录的页面在入口处同步调requireLogin(),未登录直接跳走。跳转用location.replace()而不是location.href,这样当前页不会留在历史记录里。另外要在pageshow事件里再校验一次,应对浏览器从往返缓存恢复页面的情况:
// 从 bfcache 恢复时再校验一次登录态 window.addEventListener('pageshow', function (e) { if (e.persisted && !DB.get('current_user')) { location.replace('login.html'); } });5.5 双击打开页面,商品列表一片空白
现象:用编辑器内置服务器跑得好好的,直接双击index.html打开,商品区空白,控制台报跨域错误。
原因:file://协议下,fetch、$.getJSON、XMLHttpRequest读本地 json 文件都会被同源策略拦掉。这是浏览器的安全限制,不是代码写错了。
解决:商品初始数据改成js/data.js里的常量数组,用<script src="js/data.js"></script>引入。script 标签加载本地文件不受这个限制,双击能跑,拷到 U 盘里换台电脑也能跑。这一条对答辩场景特别重要——你永远不知道答辩机房的网络和环境是什么状态。
注意:换电脑演示前,先手动清一次 localStorage。旧电脑上的测试数据(尤其是脏的用户表和订单表)跟着浏览器走不到新机器,但如果是在同一台机器上反复演示,残留数据会让"注册重名""订单列表一堆测试单"这类问题冒出来。
6. 验收与答辩:五步自检清单和三个必答问题
项目写完不等于能交。我一般会按下面这张清单走一遍,全程用"刚拿到电脑的人"的视角操作,不跳步。
| 步骤 | 操作 | 通过标准 |
|---|---|---|
| 1 | 清空 localStorage,双击 index.html | 首页正常,商品有数据,无控制台报错 |
| 2 | 未登录直接访问 order.html | 跳到登录页,地址带 redirect 参数 |
| 3 | 注册新账号并登录 | 登录成功,自动回到 order.html |
| 4 | 加购三件、改数量、删一件、取消勾选一件 | 角标与合计同步变化,刷新后不变 |
| 5 | 提交订单,退出登录,换账号登录 | 新账号购物车为空,看不到上一个账号的订单 |
第 5 步最关键,它同时验证了购物车隔离和订单归属。很多人卡在这里才发现购物车 key 没带用户 ID。
答辩时被问得最多的三个问题,提前准备好答案:
"没有后端,登录有什么意义?"答:这套设计把数据访问收敛在db.js一层,所有业务代码只调DB.get/set,不直接碰存储。后续接真实后端时,只需要把db.js换成 ajax 封装,业务层代码基本不动。分层的目的就是让"换存储"这件事只影响一个文件。
"密码存 localStorage 安全吗?"答:不安全,也不假装安全。当前存的是轻量哈希,只为了避免明文直接躺在 DevTools 里被一眼看到,真实项目必须由服务端做加盐慢哈希。这一点在代码注释里写清楚了,答辩时主动说出来比被问出来好。
"数据量大了怎么办?"答:localStorage 单域名大约 5MB,商品数据在千条级别、图片走文件路径不占空间的情况下完全够用。真到瓶颈,第一件事是把商品列表改成分页拉取,第二件事是换 IndexedDB。当前分页已经做了,每页 8 条,所以演示时不会卡。
最后一件事,演示前把浏览器缩放调到 100%,窗口拉成手机宽度再走一遍——响应式网格如果在窄屏塌了,前面所有功能做得再好,视觉分会掉一大截。
我最早做这类项目的时候,功能全写完才想起测刷新,结果发现购物车压根没落库,返工改了小半天。从那以后我每次写完一个写操作,都强制走一遍"改数据 → F5 → 看还在不在"的流程,三秒钟的事,能挡掉后面几小时的排查。希望帮到你。
本文还有配套的精品资源,点击获取