期末大作业年年有,但“HTML+CSS+JavaScript购物商城”这个题目几乎快被写烂了。我每年都能看到一批学生交上来的作业长一个样:白底、蓝色链接、表格布局、商品图是百度搜的、点击“加入购物车”没反应、刷新一下购物车全没了。说实话,这种作业老师看得想打瞌睡,分数自然不会高。
但我也见过有人用同样的题目拿到全班最高分。差别不在功能多不多,而在三个东西:页面看起来是否像一个真的商城、交互逻辑是否闭环、代码结构是否经得起老师随口一问。这篇文章就把我总结的一套“期末大作业级购物商城”完整方案拆给你看,从页面规划到核心代码,从布局技巧到答辩常问的问题,全部给出可以直接抄的答案和必须理解的原理。
先说结论:这个作业想拿高分,不需要后端、不需要框架,甚至不需要一行PHP或数据库。纯HTML+CSS+JavaScript完全够用,关键看你怎么组织代码、怎么把交互做完整,以及能不能在答辩的时候把设计思路说清楚。
1. 动手写代码之前,先把商城“画”出来
很多同学拿到题就打开编辑器开始敲,敲到哪算哪,最后页面乱七八糟。我建议你先花一晚上干件事:用纸或任意绘图工具把商城长什么样画出来。这个环节直接决定你后面是顺风顺水还是处处返工。
1.1 页面清单:一个完整的商城需要哪些页面
一个标准购物商城,最低限度需要这么几个页面:
- 首页(index.html):商品列表 + 推荐位 + 导航栏
- 商品详情页(detail.html):单件商品的大图、价格、描述、加购按钮
- 购物车页(cart.html):已加购商品清单、数量修改、总价计算、结账按钮
- 结算确认页(checkout.html):模拟填写收货信息并确认订单
期末大作业不建议再做登录页、注册页,除非你的题目明确要求。原因很简单:登录注册页面做出来之后,你势必要处理“登录态”的问题,这在纯前端里只能用localStorage模拟,而一旦模拟登录,你就得写大量业务逻辑,白白消耗时间,答辩时还可能被老师追问“密码存在哪、安不安全”,自己给自己挖坑。商城本质是“逛、选、买”三步,页面越聚焦,完成度越高。
1.2 首页的模块划分
首页是整个作业的门面,老师打开浏览器第一眼看到的就是它。我的建议是首页从上到下分成五个区块:
- 顶部导航栏:Logo、搜索框、导航菜单(首页、全部商品、购物车图标带数字角标)
- 轮播图Banner:三到四张图自动切换,可以用JS控制,也可以用CSS动画
- 商品分类筛选区:用几个按钮(全部、手机数码、电脑办公、家用电器)切换商品列表
- 商品列表区:网格布局的商品卡片,每张卡片包含商品图、名称、价格、评价数、加购按钮
- 页脚区:版权信息、关于我们等占位文本
为什么首页要这么多区块?因为老师在验收作业时,会在前30秒内判断你的页面“像不像一个商城”。只要有导航栏、轮播图、筛选按钮、商品卡片这几个视觉锚点,哪怕细节糙一点,第一印象分就已经拿到了。
1.3 目录结构提前规划好
不要把所有文件堆在一个文件夹里。我推荐这种结构:
code复制shopping-mall/
├── index.html // 首页
├── detail.html // 商品详情页
├── cart.html // 购物车页
├── checkout.html // 结算页
├── css/
│ ├── common.css // 全局公共样式
│ ├── index.css // 首页单独样式
│ ├── detail.css // 详情页样式
│ └── cart.css // 购物车样式
├── js/
│ ├── data.js // 模拟商品数据
│ ├── utils.js // 公共工具函数
│ ├── cart.js // 购物车逻辑(全局共用)
│ ├── index.js // 首页交互
│ ├── detail.js // 详情页交互
│ └── cart-page.js // 购物车页面交互
└── images/ // 本地图片
这种拆分方式在答辩时非常加分。老师问“你们的项目结构是怎么组织的”,你可以直接说出每个文件负责什么,这体现的是工程化思维,而大多数学生只会把代码全塞进一个文件里。哪怕你的代码量完全一样,不同的组织方式在老师眼中的档次完全不同。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. HTML语义化:骨架打扎实,后面才能站得稳
HTML是整个商城的骨架。很多同学喜欢满屏div,但语义化标签既能让代码更可读,也是期末作业的一个隐性加分项。老师一眼扫过去看到section、article、aside,就知道你平时是真看了点规范。
2.1 首页HTML骨架和要点
以首页为例,核心结构建议这样写:
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/common.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<!-- 顶部导航栏 -->
<header class="site-header">
<div class="container header-inner">
<div class="logo">
<a href="index.html">品鲜商城</a>
</div>
<form class="search-box" id="searchForm">
<input type="text" placeholder="搜索商品" id="searchInput">
<button type="submit">搜索</button>
</form>
<nav class="main-nav">
<ul>
<li><a href="index.html" class="active">首页</a></li>
<li><a href="#product-list">全部商品</a></li>
<li><a href="cart.html" id="cartLink">购物车(<span id="cartCount">0</span>)</a></li>
</ul>
</nav>
</div>
</header>
<!-- 轮播图 -->
<section class="banner" id="banner">
<div class="banner-slides" id="bannerSlides">
<div class="slide active"><img src="images/banner1.jpg" alt="促销活动1"></div>
<div class="slide"><img src="images/banner2.jpg" alt="促销活动2"></div>
<div class="slide"><img src="images/banner3.jpg" alt="促销活动3"></div>
</div>
<button class="banner-prev" id="bannerPrev">❮</button>
<button class="banner-next" id="bannerNext">❯</button>
<div class="banner-dots" id="bannerDots"></div>
</section>
<!-- 商品列表 -->
<section class="product-section" id="product-list">
<div class="container">
<div class="section-title">
<h2>全部商品</h2>
<div class="filter-btns" id="filterBtns">
<button class="filter-btn active" data-category="all">全部</button>
<button class="filter-btn" data-category="phone">手机数码</button>
<button class="filter-btn" data-category="computer">电脑办公</button>
<button class="filter-btn" data-category="appliance">家用电器</button>
</div>
</div>
<div class="product-grid" id="productGrid">
<!-- 商品卡片由JS动态渲染 -->
</div>
</div>
</section>
<!-- 页脚 -->
<footer class="site-footer">
<div class="container">
<p>品鲜商城 期末大作业项目</p>
<p>仅供学习交流使用</p>
</div>
</footer>
<script src="js/data.js"></script>
<script src="js/utils.js"></script>
<script src="js/cart.js"></script>
<script src="js/index.js"></script>
</body>
</html>
这里有几个细节值得说:
第一,商品列表区用空的div占位,由JavaScript从数据文件里动态生成卡片。这样做的好处是数据和页面分离,老师问到你数据从哪来,你直接指data.js。而且以后想改商品内容,只需改数据文件,不需要动HTML。
第二,导航栏的购物车角标是一个span标签,id叫cartCount,这为后面JS统一更新购物车数量预留了挂载点。
第三,搜索框用的是form标签而不是单纯的div,因为form支持在输入框里按回车就触发提交。需要注意阻止表单默认提交行为,否则页面会刷新导致状态丢失,这会在JS部分讲到。
2.2 商品详情页和购物车页的HTML设计要点
商品详情页的核心信息层级是:面包屑导航(可选)、商品大图、商品名称、价格、库存、数量选择器、加入购物车按钮、商品参数列表。布局上一般左侧放图、右侧放信息,下面再放“商品详情”描述区。
购物车页则用表格或列表列出商品项,每行包含:勾选框、商品缩略图、商品名、单价、数量加减按钮、小计、删除按钮。表格下方是结算栏,包含商品总件数、总价、结算按钮。
这两页的HTML骨架我在后面结合JS逻辑一起展开,因为它们的核心不在静态结构,而在动态交互。
3. CSS布局与美化:别再用table布局了,Flex才是期末作业的颜值担当
CSS是拉开作业分差的关键。同样是数据展示,有的页面看起来像2005年的学校官网,有的看起来像模像样,差别几乎全在布局方式和视觉细节上。期末大作业建议放弃浮动布局,主用Flex,局部可以再加一点点Grid。Flex的代码更短、更好理解,而且天然适配不同屏幕宽度。
3.1 Flex布局的核心用法
Flex里我建议你重点掌握三个场景:
场景一:导航栏水平排列。左侧Logo、中间搜索框、右侧菜单,用justify-content: space-between实现两端对齐,中间元素自动居中:
css复制.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
}
场景二:商品卡片网格。这是整个商城最重要的布局。用flex-wrap让卡片自动换行,再配合一个固定的卡片宽度,就能实现响应式网格:
css复制.product-grid {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: flex-start;
}
.product-card {
width: 240px;
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s ease, transform 0.3s ease;
background: #fff;
}
这里有个关键技巧:flex容器里的子元素宽度自适应有几种实现方式。你可以让子元素用flex: 1均分剩余空间,也可以像上面这样固定宽度加flex-wrap,让它们自动折行。实际项目用后者的场景更多,因为商品卡片的尺寸通常需要固定,否则图片会被拉伸变形。这里的子元素宽度自适应,本质上是“卡片自身固定宽度、容器自动决定每行放几张”。
场景三:按钮和输入框的水平排列。搜索框、数量加减器这类小模块,用Flex都能轻松搞定:
css复制.search-box {
display: flex;
flex: 0 1 360px;
}
.search-box input {
flex: 1;
height: 38px;
border: 2px solid #ff6700;
border-right: none;
border-radius: 4px 0 0 4px;
padding: 0 12px;
font-size: 14px;
}
.search-box button {
width: 70px;
background: #ff6700;
color: #fff;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
3.2 视觉美化的细节
纯功能页面哪怕功能再完备,如果视觉粗糙,老师也会在“美观度”这一栏给你低分。我建议至少做好这几个视觉细节:
-
主色调控制在一到两个。最常见的商城配色是橙色(如京东的红色、淘宝的橙色)+深灰文字。我下面的示例统一用#ff6700(小米橙)做主色,文字用#333,辅助色用#ff1a00。切忌五颜六色往页面上堆。
-
圆角和阴影。卡片圆角8px,按钮圆角4px,卡片hover时提升阴影并轻微上移,这个小动效成本极低但非常提升质感:
css复制.product-card:hover {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
transform: translateY(-4px);
}
- 字体统一。全局设置font-family为系统字体栈,中文环境下显示效果更稳定:
css复制body {
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: #333;
margin: 0;
padding: 0;
background: #f5f5f5;
}
- CSS变量管理主色。如果课程里讲过CSS自定义属性,用变量定义主色会显得非常专业:
css复制:root {
--primary-color: #ff6700;
--text-color: #333;
--border-color: #e0e0e0;
--bg-color: #f5f5f5;
}
之后所有用到主色的地方写var(--primary-color)即可。答辩时老师如果问“如果你想统一改主题色怎么办”,你就能直接回答“改:root里定义的那个变量就行”。这个细节非常小,但能明显提升技术评分。
- 商品卡片内图片区域固定高度,object-fit: cover裁剪图片避免变形:
css复制.product-card .product-img {
width: 100%;
height: 200px;
overflow: hidden;
}
.product-card .product-img img {
width: 100%;
height: 100%;
object-fit: cover;
}
很多同学的图一变形,整个页面档次瞬间掉了两个档。object-fit: cover是解决这个问题的万能钥匙,务必记住。
3.3 轮播图的CSS
轮播图常见实现是:所有slide横向排列在一个容器里,通过给容器设置transform: translateX(-index * 100%)来实现切换。对应CSS关键部分:
css复制.banner {
width: 100%;
max-height: 400px;
overflow: hidden;
position: relative;
margin-bottom: 30px;
}
.banner-slides {
display: flex;
transition: transform 0.5s ease;
}
.slide {
min-width: 100%;
height: 400px;
}
.slide img {
width: 100%;
height: 100%;
object-fit: cover;
}
JS部分负责把轮播索引变成对应的translateX值,这个在下一节里给完整代码。
4. JavaScript交互逻辑:购物车才是整个作业的灵魂
如果HTML和CSS决定了老师的第一印象,那JavaScript的交互深度直接决定了你是及格还是优秀。而商城项目里最核心、最容易出彩、也最容易被问倒的交互,就是购物车。
4.1 模拟商品数据:data.js怎么写
没有后端,商品数据从哪来?答案是写死在JS文件里。但写死也有讲究,不能直接在页面里复制粘贴一堆HTML,而是用一个数组存商品对象,再通过JS渲染到页面:
javascript复制// js/data.js
var products = [
{
id: 1,
name: "无线蓝牙耳机 Pro",
category: "phone",
price: 299,
rating: 4.8,
reviewCount: 2356,
image: "images/headphone.jpg",
description: "主动降噪、30小时续航、入耳式设计"
},
{
id: 2,
name: "智能手机 X5",
category: "phone",
price: 1999,
rating: 4.6,
reviewCount: 10234,
image: "images/phone.jpg",
description: "6.5英寸全面屏、5000mAh大电池、128GB存储"
},
// ... 建议准备8到12条数据,覆盖四个分类
];
这里有几条实操建议:
- 图片尽量用本地图片,不要直接用网络图片链接。网络图片在答辩时如果断网,页面上的图片全部挂掉,画面非常尴尬。本地图片随便放几张占位图即可,实在找不到合适的图可以用简单的纯色渐变块加文字替代。
- 每条数据至少要包含id、name、category、price、image五个字段,因为购物车逻辑和分类筛选都依赖它们。
- 如果想让页面有“数据感”,可以给商品加rating和reviewCount字段,渲染时显示星级评价和销量。这会让商城看起来更真实。
4.2 购物车核心逻辑:加购、更新数量、算总价
购物车的本质是什么?本质上就是“一个数据结构 + 一组操作函数”。这里最关键的设计决策是:购物车数据要存在哪里?我推荐用localStorage,因为刷新后数据不丢,这比单纯用全局变量保存购物车高了不止一个档次。
localStorage是浏览器提供的一个本地存储API,以键值对形式存取数据,存储在浏览器中,关闭页面再打开数据依然在。结合JSON.stringify和JSON.parse就能存对象数组。对期末作业来说,这就是最简明可行的“无后端持久化”方案。
下面这一段我会写得非常细,因为购物车是整个作业信息量最大、答辩时最可能被追问的地方。
先创建一个全局的购物车管理对象,放在cart.js里,这样首页、详情页、购物车页都能引用。创建对象而不写散落的全局函数,代码组织更清晰。
javascript复制// js/cart.js
var Cart = {
// 从 localStorage 读取购物车
getItems: function() {
var items = localStorage.getItem("cartItems");
return items ? JSON.parse(items) : [];
},
// 保存购物车到 localStorage
saveItems: function(items) {
localStorage.setItem("cartItems", JSON.stringify(items));
},
// 加入购物车
add: function(productId, quantity) {
var items = this.getItems();
var found = null;
for (var i = 0; i < items.length; i++) {
if (items[i].id === productId) {
found = items[i];
break;
}
}
if (found) {
found.quantity += quantity;
} else {
items.push({
id: productId,
quantity: quantity
});
}
this.saveItems(items);
this.updateNavCartCount();
},
// 修改数量(delta 可以是 1 或 -1)
changeQuantity: function(productId, delta) {
var items = this.getItems();
for (var i = 0; i < items.length; i++) {
if (items[i].id === productId) {
items[i].quantity += delta;
if (items[i].quantity <= 0) {
items.splice(i, 1);
}
break;
}
}
this.saveItems(items);
this.updateNavCartCount();
},
// 删除某个商品
remove: function(productId) {
var items = this.getItems();
for (var i = 0; i < items.length; i++) {
if (items[i].id === productId) {
items.splice(i, 1);
break;
}
}
this.saveItems(items);
this.updateNavCartCount();
},
// 获取购物车中商品总数
getTotalQuantity: function() {
var items = this.getItems();
var total = 0;
for (var i = 0; i < items.length; i++) {
total += items[i].quantity;
}
return total;
},
// 获取购物车总金额
getTotalPrice: function() {
var items = this.getItems();
var total = 0;
for (var i = 0; i < items.length; i++) {
var item = items[i];
var product = getProductById(item.id); // 在 data.js 中定义
if (product) {
total += product.price * item.quantity;
}
}
return total;
},
// 更新导航栏购物车数量角标
updateNavCartCount: function() {
var cartCountElement = document.getElementById("cartCount");
if (cartCountElement) {
cartCountElement.textContent = this.getTotalQuantity();
}
}
};
注意购物车items里只存id和quantity,不存商品名称价格等信息。原因是:价格和名称以data.js里的商品数据为准,避免购物车数据“过期”出现价格不一致。这是真实项目中的一个设计原则——“单一数据来源”。比如你改了商品价格,购物车里存的价格是旧的,结算时就对不上,所以购物车只存id和数量,渲染时现查商品信息,这个设计非常值得写在答辩稿里。
每一个操作函数末尾都调用updateNavCartCount,这样无论你在哪个页面操作购物车,导航栏的数字都能自动刷新。这个细节会让你的作业交互显得非常完整。
4.3 商品列表渲染与分类筛选
首页的商品列表区是空的div,由JS在页面加载时填充。这里用两个核心函数:一个根据分类筛选商品并渲染网格;一个是把单个商品转成卡片HTML。看这段代码:
javascript复制// js/index.js
// 渲染商品卡片
function renderProductGrid(category, keyword) {
var grid = document.getElementById("productGrid");
var list = [];
var i;
// 先按分类筛选
if (category === "all") {
list = products.slice();
} else {
for (i = 0; i < products.length; i++) {
if (products[i].category === category) {
list.push(products[i]);
}
}
}
// 再按关键词筛选(如果有搜索)
if (keyword) {
var keywordLower = keyword.toLowerCase();
var filtered = [];
for (i = 0; i < list.length; i++) {
if (list[i].name.toLowerCase().indexOf(keywordLower) !== -1 ||
list[i].description.toLowerCase().indexOf(keywordLower) !== -1) {
filtered.push(list[i]);
}
}
list = filtered;
}
// 渲染
var html = "";
for (i = 0; i < list.length; i++) {
var p = list[i];
html += '<div class="product-card">' +
'<a href="detail.html?id=' + p.id + '">' +
'<div class="product-img">' +
'<img src="' + p.image + '" alt="' + p.name + '">' +
'</div>' +
'<div class="product-info">' +
'<h3>' + p.name + '</h3>' +
'<div class="product-rating">' +
'评分 ' + p.rating + ' | ' + p.reviewCount + '条评价' +
'</div>' +
'<div class="product-price">¥' + p.price + '</div>' +
'</div>' +
'</a>' +
'<button class="add-cart-btn" data-id="' + p.id + '">加入购物车</button>' +
'</div>';
}
if (list.length === 0) {
html = '<p class="empty-tip">没有找到相关商品</p>';
}
grid.innerHTML = html;
}
这里有个很实用的设计:商品图片和标题部分包裹在a标签里,点击跳转到详情页,跳转时用URL的query参数带上商品id(detail.html?id=1)。详情页再从URL里读出id,用getProductById(id)找到对应商品,渲染详情。这是纯前端多页面之间传递参数的最标准方式。
加入购物车按钮不在a标签内,避免点击按钮时触发页面跳转。按钮的data-id属性存了商品id,等事件绑定后通过dataset.id读取。
事件绑定部分有个面试级别的细节:不能直接给每个按钮绑click,因为商品列表是动态渲染的,之后筛选、搜索都会导致按钮重新生成。应该用事件委托,把click绑定到容器上:
javascript复制// 事件委托:点击加入购物车
document.getElementById("productGrid").addEventListener("click", function(e) {
var target = e.target;
if (target.classList.contains("add-cart-btn")) {
var id = parseInt(target.getAttribute("data-id"));
Cart.add(id, 1);
alert("已加入购物车!");
}
});
// 分类筛选按钮
document.getElementById("filterBtns").addEventListener("click", function(e) {
if (e.target.classList.contains("filter-btn")) {
var btns = document.querySelectorAll(".filter-btn");
for (var i = 0; i < btns.length; i++) {
btns[i].classList.remove("active");
}
e.target.classList.add("active");
renderProductGrid(e.target.getAttribute("data-category"));
}
});
为什么要用事件委托而不是直接querySelectorAll(".add-cart-btn")然后逐个绑定?因为当用户点击筛选按钮重建商品列表后,新生成的按钮没有绑定任何事件,直接绑定的话第二次筛选之后加购就会失效。事件委托把监听器挂在父容器上,无论子元素如何增删重建,事件都能被捕获。这个知识点是期末作业答辩时的“高光知识点”,老师一问“你如何给动态生成的元素绑定事件”,你能答出事件委托,技术分直接上一个台阶。
表单提交也要处理,阻止搜索框刷新页面:
javascript复制document.getElementById("searchForm").addEventListener("submit", function(e) {
e.preventDefault(); // 阻止默认提交行为,防止页面刷新
var keyword = document.getElementById("searchInput").value.trim();
renderProductGrid("all", keyword);
});
4.4 轮播图实现
轮播图的JS逻辑是:一个索引currentIndex,每张slide对应索引值,切换时把轮播容器的transform设置为translateX(-currentIndex * 100%),因为每张slide的min-width是100%,所以-100%显示第二张、-200%显示第三张:
javascript复制var currentSlide = 0;
var slideCount = document.querySelectorAll(".slide").length;
function goToSlide(index) {
if (index < 0) {
index = slideCount - 1;
} else if (index >= slideCount) {
index = 0;
}
currentSlide = index;
document.getElementById("bannerSlides").style.transform =
"translateX(-" + (currentSlide * 100) + "%)";
updateDots();
}
function nextSlide() {
goToSlide(currentSlide + 1);
}
// 自动播放
var autoTimer = setInterval(nextSlide, 3000);
// 左右按钮
document.getElementById("bannerPrev").addEventListener("click", function() {
goToSlide(currentSlide - 1);
});
document.getElementById("bannerNext").addEventListener("click", function() {
goToSlide(currentSlide + 1);
});
// 鼠标悬浮暂停自动播放
var banner = document.getElementById("banner");
banner.addEventListener("mouseenter", function() {
clearInterval(autoTimer);
});
banner.addEventListener("mouseleave", function() {
autoTimer = setInterval(nextSlide, 3000);
});
这里有一个常见的坑,就是自动播放与手动点击的冲突。学生版本最常见的问题是:用户正在看第二张,自动播放三秒后又切到下一张,根本没有时间阅读Banner内容。解决办法虽然简单(鼠标悬浮暂停),但很少有人在作业中加上。你加上了,就是一个亮点。
上面的代码中要注意:每次进入鼠标悬浮时,原有的timer还被clear掉,mouseleave重新创建,这没问题。但如果用户多次快速移入移出,可能有多个timer并发。这是个小瑕疵,期末作业可以不处理,但如果想做得更严谨,可以把autoTimer设为null并在创建前先clear:
javascript复制banner.addEventListener("mouseenter", function() {
if (autoTimer) clearInterval(autoTimer);
autoTimer = null;
});
banner.addEventListener("mouseleave", function() {
if (autoTimer) clearInterval(autoTimer);
autoTimer = setInterval(nextSlide, 3000);
});
4.5 商品详情页的参数传递与渲染
详情页从URL里读取商品id的方式很简单:
javascript复制// js/detail.js
function getQueryParam(name) {
var query = window.location.search.substring(1);
var params = query.split("&");
for (var i = 0; i < params.length; i++) {
var parts = params[i].split("=");
if (parts[0] === name) {
return decodeURIComponent(parts[1]);
}
}
return null;
}
var productId = parseInt(getQueryParam("id"));
var product = getProductById(productId);
if (product) {
// 渲染商品名称、图片、价格、描述等
document.getElementById("detailName").textContent = product.name;
document.getElementById("detailPrice").textContent = "¥" + product.price;
document.getElementById("detailDesc").textContent = product.description;
document.getElementById("detailImg").src = product.image;
document.title = product.name + " - 品鲜商城";
}
数量加减和加购按钮:
javascript复制var detailQuantity = 1;
document.getElementById("quantityMinus").addEventListener("click", function() {
if (detailQuantity > 1) {
detailQuantity--;
document.getElementById("quantityInput").value = detailQuantity;
}
});
document.getElementById("quantityPlus").addEventListener("click", function() {
detailQuantity++;
document.getElementById("quantityInput").value = detailQuantity;
});
document.getElementById("addCartDetailBtn").addEventListener("click", function() {
if (product) {
Cart.add(product.id, detailQuantity);
alert("已加入购物车!");
}
});
4.6 购物车页渲染与修改/删除
购物车页是逻辑最密集的页面。它要做四件事:读取购物车数据并渲染列表、修改数量、删除商品、计算总价。我先给渲染函数:
javascript复制// js/cart-page.js
function renderCart() {
var items = Cart.getItems();
var tbody = document.getElementById("cartTableBody");
var totalPrice = 0;
var totalCount = 0;
var html = "";
for (var i = 0; i < items.length; i++) {
var item = items[i];
var product = getProductById(item.id);
if (!product) continue;
var subtotal = product.price * item.quantity;
totalPrice += subtotal;
totalCount += item.quantity;
html += '<tr>' +
'<td>' +
'<img src="' + product.image + '" alt="' + product.name + '" class="cart-img">' +
'</td>' +
'<td>' + product.name + '</td>' +
'<td>¥' + product.price + '</td>' +
'<td>' +
'<div class="quantity-control">' +
'<button class="qty-minus" data-id="' + product.id + '">-</button>' +
'<span class="qty-num">' + item.quantity + '</span>' +
'<button class="qty-plus" data-id="' + product.id + '">+</button>' +
'</div>' +
'</td>' +
'<td class="subtotal">¥' + subtotal.toFixed(2) + '</td>' +
'<td><button class="remove-btn" data-id="' + product.id + '">删除</button></td>' +
'</tr>';
}
if (items.length === 0) {
html = '<tr><td colspan="6" class="empty-cart">购物车是空的,去逛逛吧</td></tr>';
}
tbody.innerHTML = html;
document.getElementById("totalPrice").textContent = "¥" + totalPrice.toFixed(2);
document.getElementById("totalCount").textContent = totalCount;
}
这里之所以用table而不是div列表,是因为购物车天然是二维表格式数据,table在语义上更清晰,也方便老师一眼看出每个字段的对应关系。为了视觉好看,给table加一些样式,去掉默认边框,并用条纹背景区分行。
事件部分继续用委托。因为是动态渲染的行,用事件委托处理数量加减和删除:
javascript复制document.getElementById("cartTableBody").addEventListener("click", function(e) {
var target = e.target;
var id = parseInt(target.getAttribute("data-id"));
if (target.classList.contains("qty-plus")) {
Cart.changeQuantity(id, 1);
renderCart();
} else if (target.classList.contains("qty-minus")) {
Cart.changeQuantity(id, -1);
renderCart();
} else if (target.classList.contains("remove-btn")) {
if (confirm("确定要删除这件商品吗?")) {
Cart.remove(id);
renderCart();
}
}
});
这里有几个点需要说明:
- changeQuantity内部如果数量减到0会自动删除该项,这实现了一步到位。
- 每次修改后调用renderCart()重新渲染,同时刷新总价。数据从localStorage里重新读取,保证视图与数据一致。
- toFixed(2)让价格显示两位小数,避免出现300.00000001这类浮点运算误差。
关于浮点数,有一种情况需要特别注意:JS的浮点运算并不精确,0.1+0.2的结果并不是0.3。由于单价和数量相乘后可能出现后面一长串小数,渲染时必须用toFixed(2)做格式化。这是所有前端商城作业里都会遇到的细节坑,提前处理掉,答辩时就少一个被老师点出来的问题。
4.7 结算页的模拟逻辑
结算页做一个朴素的信息填写表单即可:收货人姓名、手机号、收货地址。页面加载时复用Cart.getTotalPrice()渲染应付总额,点击“提交订单”后清空购物车并显示订单成功提示。
javascript复制// js/checkout.js
function renderCheckout() {
document.getElementById("checkoutTotalPrice").textContent =
"¥" + Cart.getTotalPrice().toFixed(2);
}
document.getElementById("checkoutForm").addEventListener("submit", function(e) {
e.preventDefault();
// 简单校验非空
var name = document.getElementById("nameInput").value.trim();
var phone = document.getElementById("phoneInput").value.trim();
var address = document.getElementById("addressInput").value.trim();
if (!name || !phone || !address) {
alert("请完整填写收货人信息");
return;
}
// 模拟提交
alert("订单提交成功!感谢购买。");
localStorage.removeItem("cartItems");
Cart.updateNavCartCount();
window.location.href = "index.html";
});
手机号可以再做一层正则校验:/^1[3-9]\d{9}$/,让判断更专业:
javascript复制if (!/^1[3-9]\d{9}$/.test(phone)) {
alert("请输入有效的手机号");
return;
}
如果老师不要求正则匹配,这里也能说明你掌握了表单校验的思路。
4.8 公共脚本的加载顺序问题
页面底部引入script的顺序有讲究:data.js(数据源)必须最先加载,其次是utils.js(工具函数)、cart.js(依赖getProductById和购物车逻辑)、最后才是各页面的交互脚本。因为JS是从上到下顺序执行的,前面的文件里定义的函数和变量,后面的文件才能用。这个顺序本身也要在答辩时讲出来,体现你对依赖关系的理解。
5. 期末答辩必问的五个问题与应对思路
老师不会只在台下看你的页面,大多数情况下都会问几个问题来确认代码是不是你自己写的、你到底理不理解自己在写什么。我把高频问题列出来,每个附上“怎么答才能让老师点头”的思路。
问题一:你的购物车数据存在哪里?刷新页面为什么还在?
标准答法:存在浏览器的localStorage里。localStorage是浏览器提供的一种键值对存储,容量约5MB。每次修改购物车时用localStorage.setItem存JSON字符串,读取时用getItem配合JSON.parse解析成数组。因为数据已经持久化到浏览器本地,所以刷新页面后依然存在。
加分补一句:localStorage只能存字符串,所以纯对象需要序列化。这也是为什么代码里到处要用JSON.stringify和JSON.parse。
问题二:你的页面之间数据是怎么传递的?
标准答法:详情页通过URL的query参数(?id=1)接收商品ID,页面加载时用window.location.search解析出id,然后到全局商品数据中查找对应商品并渲染。购物车数据则通过localStorage跨页面共享,所有页面都从localStorage读取同一份购物车数据。
问题三:你的商品数据是写死的还是动态的?如果要改成从后端获取,怎么做?
标准答法:目前数据是写在一个data.js文件里的商品数组。如果要改成从后端获取,只需要把这个数组改成用fetch或axios从接口拉取,然后渲染逻辑不用变。例如把var products = [...]替换成fetch("/api/products").then(res => res.json()).then(data => { ... })即可。这说明你把渲染逻辑和数据获取层做了分离。
问题四:给动态生成的按钮绑定事件,你是怎么处理事件失效问题的?
标准答法:用事件委托。因为商品列表是动态渲染的,直接把事件绑定到每个按钮上,新生成的按钮不会有事件。所以我把click事件绑定在它们的父容器上,利用事件冒泡机制,通过e.target判断点击的是不是目标按钮。无论子元素怎么增删,事件都能正常工作。
问题五:你的购物车为什么只存id和数量,不存商品名和价格?
标准答法:为了避免数据不一致。如果购物车里存了商品名和价格,万一我修改了商品信息,购物车里的旧数据还是旧的。只存id和数量,渲染时再从全局商品数据里查最新信息,这就保证了数据的单一来源。
老师在期末验收时最看重的不是代码能跑,而是你能不能说得出来“为什么这样做”。所以你在写代码的时候,每写一个关键点就问自己一句“老师问这个代码为什么这么写,我怎么答”,提前把答案想好,你的答辩就稳了。
6. 实测过程中常见的坑和最后的检查清单
期末考试前如果把下面这些坑都提前踩掉,现场就不会出洋相。
第一个坑:图片路径错误。如果你用相对路径引用图片,预览页面时正常,但你把整个文件夹拷到别的电脑上打开时路径就变了。解决办法是检查好images目录和页面文件的相对位置,确认所有src路径都正确。可以直接在浏览器按F12打开开发者工具,Console里看到404就说明路径有问题。
第二个坑:中文字符乱码。所有HTML文件的head里必须写,保证中文字符正常显示。不写的话浏览器可能默认用其他编码解析,整个页面变成乱码。
第三个坑:JavaScript报错导致页面白屏。常见错误:某个函数名拼写不一致、引号少写了一半、getElementById的参数写错了。最简单的排查方法:按F12打开控制台,看到红色报错信息时,双击定位到出错的js文件和行号,一条条修。
第四个坑:轮播图的定时器冲突。如果你在多个地方调用setInterval做轮播,又没有在切换页面时清掉定时器,可能出现两个定时器同时运行。解决方法是只用一个全局定时器变量,任何地方切换前先clearInterval再重新准备。
第五个坑:IE浏览器兼容问题。期末作业现场大概率是用Chrome或Edge,通常不要求兼容IE或老版本浏览器。但如果你用了ES6的箭头函数、模板字符串等新语法,又恰好被老师用浏览器打开时版本较老,会有报错风险。最稳妥的写法是继续用var和function,不要用let、const和箭头函数,除非你能确定部署环境一定是最新版浏览器。
最后,把检查清单过一遍:
- 布局:首页、详情页、购物车页、结算页之间导航能否正确跳转
- 数据:首页商品列表渲染是否完整,分类筛选是否正常
- 交互:点击加购后,导航栏角标数字是否变化,刷新后数字是否保留
- 购物车页:数量加减是否正确,删到0是否自动移除,总价是否实时更新
- 结算页:空表单提交是否有提示,正常填写后是否清空购物车并弹窗成功
- 控制台:F12打开,确认没有任何红色的JS报错
- 代码规范:HTML/CSS/JS分别放在对应目录,页面引入路径全部正确
按这条链路把项目从头到尾点一遍,确认每个环节都通了,这个作业基本就能稳坐优秀档了。这套方案看起来内容不多,但把每个细节吃透,你不仅拿到了代码,更拿到了一个能说清楚逻辑的项目。祝期末顺利。
