本文最后更新于837 天前,其中的信息可能已经过时,如有错误请评论留言
这次是着重实现JS功能部分,而且代码量较大,就不把其他内容展示出来了,看一下大致的界面,如下:

HTML
HTML部分如下:
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>丑团外卖</title>
<link rel="shortcut icon" href="./assets/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="./css/common.css" />
<link rel="stylesheet" href="./css/container.css" />
<link rel="stylesheet" href="./css/footer.css" />
<link rel="stylesheet" href="./css/add-to-car.css" />
</head>
<body>
<div class="container">
<div class="menu">
<div class="menu-item active"><span>推荐</span></div>
<div class="menu-item"><span>热销</span></div>
<div class="menu-item"><span>折扣</span></div>
<div class="menu-item"><span>夏日冰咖必喝榜</span></div>
<div class="menu-item"><span>进店必喝</span></div>
<div class="menu-item"><span>只喝美式</span></div>
<div class="menu-item"><span>酷爽特调水果冰萃</span></div>
<div class="menu-item"><span>经典奶咖</span></div>
<div class="menu-item"><span>创意奶咖</span></div>
<div class="menu-item"><span>瑞纳冰季</span></div>
<div class="menu-item"><span>不喝咖啡</span></div>
<div class="menu-item"><span>轻食甜品</span></div>
<div class="menu-item"><span>热卖套餐</span></div>
</div>
<div class="goods-list">
</div>
</div>
<div class="footer">
<div class="footer-car-container">
<div class="footer-car">
<i class="iconfont i-gouwuchefill"></i>
<span class="footer-car-badge">0</span>
</div>
<div class="footer-car-price">
<span class="footer-car-unit">¥</span>
<span class="footer-car-total">0.00</span>
</div>
<div class="footer-car-tip">配送费¥0</div>
</div>
<div class="footer-pay">
<a href="">去结算</a>
<span>还差¥0元起送</span>
</div>
</div>
<!-- <div class="add-to-car">
<i class="iconfont i-jiajianzujianjiahao"></i>
</div> -->
<script src="./js/data.js"></script>
<script src="./js/index.js"></script>
</body>
</html>
JS
js功能部分如下,其中购物车动画的CSS会在后面给出来
// index.js
/* ES6新语法,看不懂的用后面注释里的写法 */
/* 单件商品的数据 */
class UIGoods {
// 构造函数
constructor(g) {
this.data = g
this.choose = 0
}
// 获取商品总价
getTotalPrice() {
return this.data.price * this.choose
}
// 是否选中了此件商品
isChoose() {
return this.choose > 0
}
// 选择的商品数量加 1
increase() {
this.choose++
}
// 选择的商品数量减 1
decrease() {
if(this.choose === 0) return
this.choose--
}
}
/* 与上面写法效果一样,上面的是ES6的新语法
构造函数,使用对象来调用方法
function UIGoods(g) {
this.data = g
this.choose = 0
}
获取商品总价
UIGoods.prototype.getTotalPrice = function () {
return this.data.price * this.choose
}
是否选中了此件商品
UIGoods.prototype.isChoose = function () {
return this.choose > 0
}
选择的商品数量加 1
UIGoods.prototype.increase = function () {
return this.choose++
}
选择的商品数量减 1
UIGoods.prototype.decrease = function () {
if(this.choose === 0) return
return this.choose--
}
*/
/* 整个界面的数据 */
class UIData {
constructor() {
let UiGoods = []
goods.map(g => {
UiGoods.push(new UIGoods(g))
})
this.UiGoods = UiGoods
// 起送门槛
this.deliveryThreshold = 30
// 配送费
this.deliveryPrice = 5
}
// 获取总价
getTotalPrice() {
let sum = 0
this.UiGoods.map(g => {
sum += g.getTotalPrice()
})
return sum
}
// 是否选中了某件商品
isChoose(index) {
return this.UiGoods[index].isChoose()
}
// 增加某件商品的选中数量
increase(index) {
this.UiGoods[index].increase()
}
// 减少某件商品的选中数量
decrease(index) {
this.UiGoods[index].decrease()
}
// 得到总共的选择数量
getTotalChooseNumber() {
let sum = 0
this.UiGoods.map(g => {
sum += g.choose
})
return sum
}
// 购物车中是否有商品
hasGoodsInCar() {
return this.getTotalChooseNumber() > 0
}
// 价格是否跨过了配送门槛
isCrossDeliveryThreshold() {
return this.getTotalPrice() >= this.deliveryThreshold
}
}
/* 整个界面 */
class UI{
constructor() {
this.uiData = new UIData()
this.doms = {
goodsContainer: document.querySelector('.goods-list'),
deliveryPrice: document.querySelector('.footer-car-tip'),
footerPay: document.querySelector('.footer-pay'),
footerPayInnerSpan: document.querySelector('.footer-pay span'),
totalPrice: document.querySelector('.footer-car-total'),
car: document.querySelector('.footer-car'),
badge: document.querySelector('.footer-car-badge'),
}
// 购物车图标位置 也是后面添加图标跳跃动画的目标点
let carRect = this.doms.car.getBoundingClientRect()
let jumpTarget = {
x: carRect.left + carRect.width / 2, // 距左边的距离加上宽度的一半
y: carRect.top + carRect.height / 5 // 距顶部的距离加上一小段距离,随意写,这里用了高度的 1 / 5
}
this.jumpTarget = jumpTarget
this.createHTML()
this.updateFooter()
this.listenEvent()
}
// 监听各种事件
listenEvent() {
this.doms.car.addEventListener('animationend', function () {
this.classList.remove('animate')
// this.doms.car.classList.remove('animate') 这要用箭头函数就这么写
})
}
// 创建商品列表的HTML
createHTML() {
let HTML = ''
this.uiData.UiGoods.map((g, i) => {
HTML += `<div class="goods-item">
<img src="${g.data.pic}" alt="" class="goods-pic" />
<div class="goods-info">
<h2 class="goods-title">${g.data.title}</h2>
<p class="goods-desc">${g.data.desc}</p>
<p class="goods-sell">
<span>月售 ${g.data.sellNumber}</span>
<span>好评率${g.data.favorRate}%</span>
</p>
<div class="goods-confirm">
<p class="goods-price">
<span class="goods-price-unit">¥</span>
<span>${g.data.price}</span>
</p>
<div class="goods-btns">
<i data-index="${i}" class="iconfont i-jianhao"></i>
<span>${g.choose}</span>
<i data-index="${i}" class="iconfont i-jiajianzujianjiahao"></i>
</div>
</div>
</div>
</div>`
})
this.doms.goodsContainer.innerHTML = HTML
}
// 增加某件商品的选中数量
increase(index) {
this.uiData.increase(index)
this.updateGoodsItem(index)
this.updateFooter()
this.jump(index)
}
// 减少某件商品的选中数量
decrease(index) {
this.uiData.decrease(index)
this.updateGoodsItem(index)
this.updateFooter()
}
// 更新某件商品的显示状态
updateGoodsItem(index) {
let goodsDom = this.doms.goodsContainer.children[index]
if (this.uiData.isChoose(index)) {
goodsDom.classList.add('active')
} else {
goodsDom.classList.remove('active')
}
let span = goodsDom.querySelector('.goods-btns span')
span.textContent = this.uiData.UiGoods[index].choose
}
// 更新页脚
updateFooter() {
// 得到总价数据
let total = this.uiData.getTotalPrice()
// 显示配送费
this.doms.deliveryPrice.textContent = `配送费¥${this.uiData.deliveryPrice}`
// 显示是否够起送以及还差多少元起送
if (this.uiData.isCrossDeliveryThreshold()) {
// 到达起送门槛
this.doms.footerPay.classList.add('active')
} else {
this.doms.footerPay.classList.remove('active')
// 更新还差多少元起送
let dis = this.uiData.deliveryThreshold - total
dis = Math.round(dis) // 四舍五入
this.doms.footerPayInnerSpan.textContent = `还差¥${dis}元起送`
}
// 显示总价
this.doms.totalPrice.textContent = total.toFixed(2) // 保留两位小数
// 设置购物车图标的样式状态
if (this.uiData.hasGoodsInCar()) {
this.doms.car.classList.add('active')
} else {
this.doms.car.classList.remove('active')
}
// 设置购物车图标上的数字
this.doms.badge.textContent = this.uiData.getTotalChooseNumber()
}
// 购物车动画
carAnimate() {
this.doms.car.classList.add('animate')
}
// 添加商品按钮实现抛物线跳跃动画
jump(index) {
// 找到对应商品的加号图标
let btnAdd = this.doms.goodsContainer.children[index].querySelector('.i-jiajianzujianjiahao')
// 得到按钮的位置信息
let rect = btnAdd.getBoundingClientRect()
// 跳跃动画的起始坐标
let start = {
x: rect.left,
y: rect.top
}
// 实现跳跃动画
let div = document.createElement('div')
div.className = 'add-to-car'
let i = document.createElement('i')
i.className = 'iconfont i-jiajianzujianjiahao'
// 设置初始位置
div.style.transform = `translateX(${start.x}px)`
// 实现抛物线,外面的div横向匀速运动,里面的 i 数值加速运动
i.style.transform = `translateY(${start.y}px)`
div.appendChild(i)
document.body.appendChild(div)
// 强制渲染 读取位置使浏览器强制 reflow ,不然不会有过渡,只会直接渲染到目标位置
div.clientWidth
// 设置结束位置
div.style.transform = `translateX(${this.jumpTarget.x}px)`
// 实现抛物线,外面的div横向匀速运动,里面的 i 数值加速运动
i.style.transform = `translateY(${this.jumpTarget.y}px)`
// transition 结束的事件监听
div.addEventListener('transitionend', () => {
div.remove()
this.carAnimate()
}, {
once: true // 事件仅触发一次,不然 i 还会冒泡到 div 再触发一次事件
})
}
}
let ui = new UI()
/* 事件绑定 */
ui.doms.goodsContainer.addEventListener('click', function (e) {
if (e.target.classList.contains('i-jiajianzujianjiahao')) {
let index = e.target.dataset.index // H5的 data-index 属性
ui.increase(index)
} else if (e.target.classList.contains('i-jianhao')){
let index = e.target.dataset.index // H5的 data-index 属性
ui.decrease(index)
}
})
add-to-car.css
// add-to-car.css
.add-to-car {
position: fixed;
color: #fff;
font-size: 23rem;
line-height: 40rem;
text-align: center;
z-index: 9;
margin-left: -20rem;
margin-top: -20rem;
left: 0;
top: 0;
/* 平抛运动的水平方向,水平匀速 */
transition: 0.5s linear;
}
.add-to-car .iconfont {
width: 40rem;
height: 40rem;
background: #4a90e1;
border-radius: 50%;
display: block;
/* 平抛运动的竖直方向,使用贝塞尔曲线 */
transition: 0.5s cubic-bezier(0.5, -0.5, 1, 1)
}
拓展
UIGoods 部分使用属性修饰符来写,更大程度保障安全性
// index.js
/* 单件商品的数据 */
class UIGoods {
constructor(g) {
Object.defineProperty(this, 'data', {
get: function () {
return g
},
set: function (value) {
throw new Error('data 是只读属性,不能修改')
},
configurable: false, // 不能重新更改配置
})
let internalChooseValue = 0
Object.defineProperty(this, 'choose', {
get: function () {
return internalChooseValue
},
set: function (value) {
if (typeof value !== 'number') {
throw new Error('choose属性必须是数字')
}
var temp = parseInt(value)
if (temp !== value) {
throw new Error('choose属性必须是整数')
}
if (value < 0) {
throw new Error('choose属性必须大于等于 0')
}
internalChooseValue = value
},
configurable: false, // 不可以重新更改配置
})
}
代码量较大,细细斟酌,研究研究,三四天通透之后可以尝试自己写一个复现一下


