购物车案例
分析功能
界面逻辑
- 商品展示模块:添加减少商品
- 结算信息模块:计算总价、商品数、配送费、起送价
数据逻辑
- (单个)设计每个商品的数据结构、并添加属性(选中的数量)
- (一组)根据原始数据创建商品数据
- (界面所有)UI 上所需信息(计算总价、总件数、配送费、是否选中商品、是否满起送费)
目录
base
📦shopping-car
┣ 📜data.js
┣ 📜index.html
┗ 📜index.js
📦shopping-car
┣ 📜data.js
┣ 📜index.html
┗ 📜index.js
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>点单购物车</title>
</head>
<style>
* {
margin: 0;
padding: 0;
}
html,
body,
#container {
width: 100%;
height: 100vh;
}
.goods-list-container {
height: calc(100vh - 60px);
}
.btn-info {
display: flex;
justify-content: end;
align-items: center;
gap: 12px;
}
li {
text-align: right;
margin-bottom: 24px;
border-bottom: 1px solid #eee;
padding: 12px;
}
.btn {
}
.footer-container {
box-sizing: border-box;
height: 60px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background-color: aliceblue;
padding: 0px 12px;
}
</style>
<body>
<div class="page-container">
<div class="goods-list-container">
<ul class="goods-list"></ul>
</div>
<div class="footer-container">
<div>
<span class="total">总额:0元</span>
<span class="delivery-price">配送费:5元</span>
</div>
<div class="need-price">还差30元起送</div>
</div>
</div>
<script src="./data.js"></script>
<script src="./index.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>点单购物车</title>
</head>
<style>
* {
margin: 0;
padding: 0;
}
html,
body,
#container {
width: 100%;
height: 100vh;
}
.goods-list-container {
height: calc(100vh - 60px);
}
.btn-info {
display: flex;
justify-content: end;
align-items: center;
gap: 12px;
}
li {
text-align: right;
margin-bottom: 24px;
border-bottom: 1px solid #eee;
padding: 12px;
}
.btn {
}
.footer-container {
box-sizing: border-box;
height: 60px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background-color: aliceblue;
padding: 0px 12px;
}
</style>
<body>
<div class="page-container">
<div class="goods-list-container">
<ul class="goods-list"></ul>
</div>
<div class="footer-container">
<div>
<span class="total">总额:0元</span>
<span class="delivery-price">配送费:5元</span>
</div>
<div class="need-price">还差30元起送</div>
</div>
</div>
<script src="./data.js"></script>
<script src="./index.js"></script>
</body>
</html>
js
// 商品数据
class GoodsItem {
constructor(g) {
this.data = g;
this.choose = 0;
}
}
// 整个界面数据
class UIData {
constructor(g) {
this.data = g.map((goods) => new GoodsItem(goods));
this.deliveryShould = 30;
this.deliveryPrice = 5;
}
// 添加某件商品数量
increase(index) {
this.data[index].choose++;
}
// 减少某件商品数量
decrease(index) {
!!this.data[index].choose && this.data[index].choose--;
}
// 获取总价
getTotalPrice() {
return this.data.reduce((pre, cur) => {
return pre + cur.choose * cur.data.price;
}, 0);
}
// 获取选中商品总数量
getTotalChooseNumber() {
return this.data.reduce((pre, cur) => {
return pre + cur.choose;
}, 0);
}
// 是否选中商品
getIsHasChoose() {
return this.getTotalChooseNumber() > 0;
}
// 是否满配送费
getIsShouldDelivery() {
return this.getTotalPrice() >= this.deliveryShould;
}
}
// 界面操作
class UI {
constructor(g) {
this.uiData = new UIData(g);
this.doms = {
goodsContainer: document.querySelector(".goods-list"),
totalDom: document.querySelector(".total"),
deliveryPriceDom: document.querySelector(".delivery-price"),
needPriceDom: document.querySelector(".need-price"),
};
}
// 创建商品元素
createHtml() {
let html = "";
console.log("this.uiData", this.uiData);
for (let i = 0; i < this.uiData.data.length; i++) {
const item = this.uiData.data[i];
html += `<li>
<div>${item.data.title}</div>
<div>${item.data.price}</div>
<div class="btn-info">
<div class="btn btn-dec">-</div>
<div class="choose">${item.choose}</div>
<div class="btn btn-inc">+</div>
</div>
</li>`;
}
this.doms.goodsContainer.innerHTML = html;
}
// 增加某件商品
increase(index) {
this.uiData.increase(index);
this.updateGoodsItem(index);
this.updateBill();
}
// 减少某件商品
decrease(index) {
this.uiData.decrease(index);
this.updateGoodsItem(index);
this.updateBill();
}
// 更新某个商品的状态
updateGoodsItem(index) {
const goodsItem = this.doms.goodsContainer.children[index];
// 更新数量
const chooseDom = goodsItem.querySelector(".choose");
chooseDom.textContent = this.uiData.data[index].choose;
}
// 更新结算栏状态
updateBill() {
const total = this.uiData.getTotalPrice();
const needPrice = this.uiData.deliveryShould - total;
this.doms.totalDom.textContent = `商品费用:${total}元`;
this.doms.deliveryPriceDom.textContent = `配送费:${this.uiData.deliveryPrice}元`;
this.doms.needPriceDom.textContent = `还差${needPrice >= 0 ? needPrice : 0}元起送`;
}
}
// 初始化
const ui = new UI(goodsData);
// 创建商品元素
ui.createHtml();
// 注册事件
ui.doms.goodsContainer.addEventListener("click", (e) => {
if (e.target.classList.contains("btn-inc")) {
const index = +e.target.getAttribute("index");
ui.increase(index);
}
if (e.target.classList.contains("btn-dec")) {
const index = +e.target.getAttribute("index");
ui.decrease(index);
}
});
// 商品数据
class GoodsItem {
constructor(g) {
this.data = g;
this.choose = 0;
}
}
// 整个界面数据
class UIData {
constructor(g) {
this.data = g.map((goods) => new GoodsItem(goods));
this.deliveryShould = 30;
this.deliveryPrice = 5;
}
// 添加某件商品数量
increase(index) {
this.data[index].choose++;
}
// 减少某件商品数量
decrease(index) {
!!this.data[index].choose && this.data[index].choose--;
}
// 获取总价
getTotalPrice() {
return this.data.reduce((pre, cur) => {
return pre + cur.choose * cur.data.price;
}, 0);
}
// 获取选中商品总数量
getTotalChooseNumber() {
return this.data.reduce((pre, cur) => {
return pre + cur.choose;
}, 0);
}
// 是否选中商品
getIsHasChoose() {
return this.getTotalChooseNumber() > 0;
}
// 是否满配送费
getIsShouldDelivery() {
return this.getTotalPrice() >= this.deliveryShould;
}
}
// 界面操作
class UI {
constructor(g) {
this.uiData = new UIData(g);
this.doms = {
goodsContainer: document.querySelector(".goods-list"),
totalDom: document.querySelector(".total"),
deliveryPriceDom: document.querySelector(".delivery-price"),
needPriceDom: document.querySelector(".need-price"),
};
}
// 创建商品元素
createHtml() {
let html = "";
console.log("this.uiData", this.uiData);
for (let i = 0; i < this.uiData.data.length; i++) {
const item = this.uiData.data[i];
html += `<li>
<div>${item.data.title}</div>
<div>${item.data.price}</div>
<div class="btn-info">
<div class="btn btn-dec">-</div>
<div class="choose">${item.choose}</div>
<div class="btn btn-inc">+</div>
</div>
</li>`;
}
this.doms.goodsContainer.innerHTML = html;
}
// 增加某件商品
increase(index) {
this.uiData.increase(index);
this.updateGoodsItem(index);
this.updateBill();
}
// 减少某件商品
decrease(index) {
this.uiData.decrease(index);
this.updateGoodsItem(index);
this.updateBill();
}
// 更新某个商品的状态
updateGoodsItem(index) {
const goodsItem = this.doms.goodsContainer.children[index];
// 更新数量
const chooseDom = goodsItem.querySelector(".choose");
chooseDom.textContent = this.uiData.data[index].choose;
}
// 更新结算栏状态
updateBill() {
const total = this.uiData.getTotalPrice();
const needPrice = this.uiData.deliveryShould - total;
this.doms.totalDom.textContent = `商品费用:${total}元`;
this.doms.deliveryPriceDom.textContent = `配送费:${this.uiData.deliveryPrice}元`;
this.doms.needPriceDom.textContent = `还差${needPrice >= 0 ? needPrice : 0}元起送`;
}
}
// 初始化
const ui = new UI(goodsData);
// 创建商品元素
ui.createHtml();
// 注册事件
ui.doms.goodsContainer.addEventListener("click", (e) => {
if (e.target.classList.contains("btn-inc")) {
const index = +e.target.getAttribute("index");
ui.increase(index);
}
if (e.target.classList.contains("btn-dec")) {
const index = +e.target.getAttribute("index");
ui.decrease(index);
}
});
js
const goodsData = [
{
title: '美式',
price: 11
},
{
title: '加浓美式',
price: 15
},
{
title: '生椰拿铁',
price: 16
},
{
title: '生酪拿铁',
price: 16
}
]
const goodsData = [
{
title: '美式',
price: 11
},
{
title: '加浓美式',
price: 15
},
{
title: '生椰拿铁',
price: 16
},
{
title: '生酪拿铁',
price: 16
}
]
效果
思路总结
- 一环套一环,各司其职,一步一步实现,思路清晰
- 从单个商品,根据原始数据结构生成界面渲染和操作所需结构,商品列表只需构造生成即可(工厂模式)
- 到 UI 数据层,保存界面所有功能模块的数据(商品列表、结算栏),界面所有数据只需从这里获取
- 再到 UI 操作层,构造渲染页面所需数据并渲染界面、提供用户操作 API
学习视频链接:https://ke.qq.com/course/5892689/13883876927203921#term_id=106109971
思考
工作中都是现有 UI 界面再有数据,对接 API 存在这两种情况:
- 不确定接口参数格式
- 你页面时使用[]保存,后端需要以,分割的字符串
- 你页面时使用 true,false,后端定义 0,1 接收
- ...
- 不确定接口返回数据格式
- 如上情况反着来
准备一个参数转化、和一个接口返回数据转化的方法,这样只需在发起请求时调用下转化参数 API、接收返回结果时调用转化数据 API。但为了方便排查,最好还是统一字段、格式。
而关于网络请求方法管理
独立文件夹
功能模块下新建文件夹
传统是把所有 api 请求按页面创建文件放在一个 api 文件夹中,但是随着项目越来越大,页面、功能模块越来越多,查阅修改文件目录跨度就变大了,假如一个页面或者功能模块有多个行为发起接口请求,后续这个功能废弃了,需要删除代码,需要关注两处目录下的文件,如果是在功能模块文件夹下建立 API 文件夹的方式管理,后续删除移动、这样就只需关注一处。无论如何,API 最好根据 菜单->页面->功能模块分层,分级方式自定。