是根据渡一大师课来写的,如有什么地方存在问题,还请大家在评论区指出来。ど⁰̷̴͈꒨⁰̷̴͈う♡~
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>
common.css
@import url('https://at.alicdn.com/t/c/font_3555577_me2a6tdmvu8.css');
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 0.125vw;
}
body {
font-size: 35rem;
-webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased;
user-select: none;
font-family: 'Microsoft Yahei', 'sans-serif';
}
a {
color: inherit;
text-decoration: none;
}
.iconfont {
font-size: inherit;
}
container.css
.container {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: calc(100% - 100rem);
display: flex;
color: #333;
}
.menu {
background: #f5f5f5;
width: 190rem;
overflow-y: scroll;
padding-bottom: 50rem;
flex: 0 0 auto;
}
.menu::-webkit-scrollbar {
width: 0;
}
.menu-item {
height: 141rem;
display: flex;
justify-content: center;
align-items: center;
padding: 0 30rem;
position: relative;
}
.menu-item span {
font-size: 30rem;
line-height: 40rem;
max-height: 80rem;
overflow: hidden;
}
.menu-item.active {
font-weight: bold;
background: #fff;
}
.menu-item.active::before {
content: '';
position: absolute;
left: 0;
height: 100%;
width: 7.5rem;
background: #3190e8;
}
.goods-list {
flex-grow: 1;
overflow-y: scroll;
}
.goods-list::-webkit-scrollbar {
width: 0;
}
.goods-item {
border-bottom: 1rem solid #f8f8f8;
padding: 30rem 20rem;
display: flex;
}
.goods-pic {
width: 200rem;
height: 200rem;
object-fit: contain;
border: 1rem solid rgba(0, 0, 0, 0.06);
flex: 0 0 auto;
}
.goods-info {
flex: 1 1 auto;
padding: 0 35rem;
overflow: hidden;
}
.goods-title {
font-size: 35rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 20rem;
}
.goods-desc {
font-size: 24rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 20rem;
}
.goods-sell {
color: #858687;
font-size: 24rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 20rem;
display: flex;
}
.goods-sell span:first-child {
margin-right: 18rem;
}
.goods-confirm {
display: flex;
justify-content: space-between;
}
.goods-price {
display: flex;
font-size: 35rem;
font-weight: bold;
color: #f60;
align-items: flex-end;
}
.goods-price-unit {
font-size: 25rem;
margin-bottom: 4rem;
font-weight: normal;
}
.goods-btns {
display: flex;
justify-content: center;
align-items: center;
}
.goods-btns .iconfont {
width: 40rem;
height: 40rem;
background: #4a90e1;
color: #fff;
border-radius: 50%;
font-size: 23rem;
line-height: 40rem;
text-align: center;
}
.goods-btns span {
margin: 0 15rem;
display: none;
}
.goods-btns .i-jianhao {
border: 1rem solid #4a90e1;
background: #fff;
color: #4a90e1;
font-weight: bold;
display: none;
}
.goods-item.active span {
display: block;
}
.goods-item.active .i-jianhao {
display: block;
}
footer.css
.footer {
height: 100rem;
color: #fff;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
display: flex;
z-index: 10;
}
.footer-car-container {
flex-grow: 1;
background: #3d3d3f;
padding-left: 175rem;
position: relative;
}
.footer-car {
position: absolute;
width: 118rem;
height: 118rem;
border: 9rem solid #444;
left: 25rem;
top: -35rem;
border-radius: 50%;
background: inherit;
display: flex;
justify-content: center;
align-items: center;
font-size: 60rem;
}
.footer-car-badge {
position: absolute;
width: 35rem;
height: 35rem;
background: #ec5533;
font-size: 25rem;
text-align: center;
border-radius: 50%;
line-height: 35rem;
right: 0;
top: 0;
display: none;
}
.footer-car.active {
background: #4a90e1;
}
.footer-car.active .footer-car-badge {
display: block;
}
.footer-car.animate {
animation: footer-car-animate 500ms ease-in-out;
}
@keyframes footer-car-animate {
0% {
transform: scale(1);
}
25% {
transform: scale(0.8);
}
50% {
transform: scale(1.1);
}
75% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}
.footer-car-price {
font-size: 40rem;
display: flex;
margin-top: 5rem;
}
.footer-car-tip {
font-size: 20rem;
margin-left: 6rem;
}
.footer-pay {
background: #535356;
width: 250rem;
font-size: 35rem;
line-height: 100rem;
text-align: center;
}
.footer-pay > * {
display: block;
width: 100%;
height: 100%;
}
.footer-pay a {
display: none;
}
.footer-pay span {
font-size: 30rem;
}
.footer-pay.active {
background: #76d572;
}
.footer-pay.active a {
display: block;
}
.footer-pay.active span {
display: none;
}
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;
/* i元素过渡动画,i不应该设置匀速动画。cubic-bezier函数,x1,y1,x2,y2分别代表起点和终点的坐标 */
transition: 0.5s cubic-bezier(0.5, -0.5, 1, 1);
}
data.js
var goods = [
{
pic: './assets/g1.png',
title: '椰云拿铁',
// desc是富文本
desc: `1人份【年度重磅,一口吞云】
√原创椰云topping,绵密轻盈到飞起!
原创瑞幸椰云™工艺,使用椰浆代替常规奶盖
打造丰盈、绵密,如云朵般细腻奶沫体验
椰香清甜饱满,一口滑入口腔
【饮用建议】请注意不要用吸管,不要搅拌哦~`,
sellNumber: 200,//销量,月售
favorRate: 95,//好评率
price: 32,//价格
},
{
pic: './assets/g2.png',
title: '生椰拿铁',
desc: `1人份【YYDS,无限回购】
现萃香醇Espresso,遇见优质冷榨生椰浆,椰香浓郁,香甜清爽,带给你不一样的拿铁体验!
主要原料:浓缩咖啡、冷冻椰浆、原味调味糖浆
图片及包装仅供参考,请以实物为准。建议送达后尽快饮用。到店饮用口感更佳。`,
sellNumber: 1000,
favorRate: 100,
price: 19.9,
},
{
pic: './assets/g3.png',
title: '加浓 美式',
desc: `1人份【清醒加倍,比标美多一份Espresso】
口感更佳香醇浓郁,回味持久
图片仅供参考,请以实物为准。建议送达后尽快饮用。`,
sellNumber: 200,
favorRate: 93,
price: 20.3,
},
{
pic: './assets/g4.png',
title: '瓦尔登蓝钻瑞纳冰',
desc: `1人份【爆款回归!蓝色治愈力量】
灵感来自下澄明、碧蓝之境---瓦尔登湖。含藻蓝蛋白,梦幻蓝色源自天然植物成分,非人工合成色素,融入人气冷榨生椰浆,椰香浓郁,清冽冰爽;底部添加Q弹小料,0脂原味晶球,光泽剔透,如钻石般blingbling。搭配奶油顶和彩虹色棉花糖,满足你的少女心~
【去奶油小提示】由于去掉奶油后顶料口味会受影响,为保证口感,选择“去奶油”选项时将同时去掉奶油及顶料,请注意哦!【温馨提示】瑞纳冰系列产品形态为冰沙,无法进行少冰、去冰操作,请您谅解。【图片仅供参考,请以实物为准】`,
sellNumber: 17,
favorRate: 80,
price: 38,
},
{
pic: './assets/g5.png',
title: '椰云精萃美式',
desc: `1人份【不用吸管 大口吞云!】
1杯热量*≈0.6个苹果!
原创瑞幸椰云™工艺,将「椰浆」变成绵密、丰盈的“云朵”,口感绵密顺滑!0乳糖植物基,清爽轻负担!
*数据引自《中国食物成分表》第六版,苹果每100克可食部分中能量约为53千卡,以每个苹果250克/个计,1杯椰云精萃美式约80千卡,相当于约0.6个苹果。
【图片仅供参考,请以实物为准】`,
sellNumber: 50,
favorRate: 90,
price: 21.12,
},
];
index.js
// 这里利用ES6中的类来实现单件商品的数据,这种写法是一种面向对象编程的写法,优势是可以将数据和方法进行封装,
// 单件商品的数据
class UIGoods {
// 构造函数
constructor(g) {
this.data = g; // 初始化data属性为传入的参数g
this.choose = 0; // 初始化choose属性为0
// this.totalPrice=0;// 初始化totalPrice属性为0,但这样会导致每次计算总价都要重新计算,造成数据冗余
// 数据冗余的解决方法:将totalPrice属性设置为私有属性,通过getter方法来获取和设置值
// 数据冗余的优缺点:性能下降,但可以减少代码量。且可以提高代码的可读性、可维护性
}
/**
* 获取、计算总价
* @return {number} 总价
*/
getTotalPrice() {
return this.data.price * this.choose;
}
// 是否选中了此件商品
isChoose() {
// 如果choose大于0,则表示选中了此件商品
return this.choose > 0;
}
/*
选择的数量+1
这么写的好处是,可以避免多次判断,提高性能。而且可以提高代码的可读性、可维护性。如果想要在原有基础上增加数量,可以直接调用increase方法
*/
increase() {
this.choose++;
}
// 选择的数量-1
decrease() {
// 如果选择的数量为0,则表示没有选择此件商品,不需要减
if (this.choose === 0) {
return;
}
this.choose--;
}
}
// 整个界面的数据
class UIData {
constructor() {
// 数组
var uiGoods = [];
// 遍历原始数据,将原始数据转换为单件商品的数据
for (var i = 0; i < goods.length; i++) {
var uig = new UIGoods(goods[i]);
uiGoods.push(uig);
}
// 将单件商品的数据保存到uiGoods属性中
this.uiGoods = uiGoods;
// 起送标准 => 还差多少钱起送
this.deliveryThreshold = 30;
// 起送标准 => 起送价格,配送费用
this.deliveryPrice = 5;
}
// 总价
getTotalPrice() {
var sum = 0;
// 遍历循环
for (var i = 0; i < this.uiGoods.length; i++) {
var g = this.uiGoods[i];
sum += g.getTotalPrice();
}
return sum;
}
// 增加某件商品的选中数量,通过下标拿到对应的商品
increase(index) {
this.uiGoods[index].increase();
}
// 减少某件商品的选中数量
decrease(index) {
this.uiGoods[index].decrease();
}
// 得到总共的选择数量
getTotalChooseNumber() {
var sum = 0;
for (var i = 0; i < this.uiGoods.length; i++) {
sum += this.uiGoods[i].choose;
}
return sum;
}
// 判断购物车中有没有东西
hasGoodsInCar() {
// 这里调用了getTotalChooseNumber,如果数量大于0,那就代表购物车中有东西
return this.getTotalChooseNumber() > 0;
}
// 是否跨过了起送标准
isCrossDeliveryThreshold() {
return this.getTotalPrice() >= this.deliveryThreshold;
}
// 判断是否选中了商品,利用了商品的下标
isChoose(index) {
return this.uiGoods[index].isChoose();
}
}
// 整个界面
class UI {
constructor() {
// 商品数据,对商品进行封装,不管是对商品进行增删改查,还是对商品进行计算,都可以统一封装到一个对象中
this.uiData = new UIData();
// 是用来获取元素的,因为我们是用js来操作dom,所以需要用到dom元素。这doms里面包含了各种dom元素
this.doms = {
// 获取商品列表容器
goodsContainer: document.querySelector(".goods-list"),
// 获取配送费
deliveryPrice: document.querySelector(".footer-car-tip"),
// 获取页脚
footerPay: document.querySelector(".footer-pay"),
// 获取页脚内的span
footerPayInnerSpan: document.querySelector(".footer-pay span"),
// 获取总价
totalPrice: document.querySelector(".footer-car-total"),
// 获取购物车
car: document.querySelector(".footer-car"),
// 获取购物车的角标
badge: document.querySelector(".footer-car-badge"),
};
// 获取购物车的位置
var carRect = this.doms.car.getBoundingClientRect();
// 获取购物车的中心点
var jumpTarget = {
x: carRect.left + carRect.width / 2,
y: carRect.top + carRect.height / 5,
};
// 获取购物车的中心点
this.jumpTarget = jumpTarget;
// 创建商品列表 => 是为了循环创建商品列表元素
this.createHTML();
// 更新页脚
this.updateFooter();
// 监听各种事件
this.listenEvent();
}
// 监听各种事件
listenEvent() {
// 购物车动画
this.doms.car.addEventListener("animationend", function () {
this.classList.remove("animate");
});
}
// 根据商品数据创建商品列表元素
/*
两种方式(这里用第一种方式)
1.遍历商品数据,将商品数据转换为html字符串 => 优点:可以减少代码量、简单,但性能下降、执行效率低。因为字符串得parse html,得解析成dom元素,且无法直接操作dom元素(总的来说,就是执行效率低,开发效率高)
2.一个一个创建商品列表元素 => 优点:性能高、执行效率高,但代码量多,不好维护。而且不需要解析成dom元素,可以直接操作dom元素(总的来说,执行效率高,开发效率低)
*/
createHTML() {
// 生成html字符串
var html = "";
// 遍历商品数据
for (var i = 0; i < this.uiData.uiGoods.length; i++) {
// 获取单个商品数据
var g = this.uiData.uiGoods[i];
// 将单个商品数据转换为html字符串,这里进行了拼接。因为这里是拼接,所以需要使用+=。而且拼接之后就不用在html中添加<div>了
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 index="${i}" class="iconfont i-jianhao"></i>
<span>${g.choose}</span>
<i index="${i}" class="iconfont i-jiajianzujianjiahao"></i>
</div>
</div>
</div>
</div>`;
}
// 将html字符串添加到商品列表容器中
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) {
// 获取商品元素
var goodsDom = this.doms.goodsContainer.children[index];
// 判断是否选中
if (this.uiData.isChoose(index)) {
// 选中,就加上active类
goodsDom.classList.add("active");
} else {
// 未选中,就去掉active类
goodsDom.classList.remove("active");
}
// 更新数量
var span = goodsDom.querySelector(".goods-btns span");
// 将span元素的文本设置为当前商品的数量
span.textContent = this.uiData.uiGoods[index].choose;
}
// 更新页脚
updateFooter() {
// 得到总价数据
var total = this.uiData.getTotalPrice();
// 设置配送费
this.doms.deliveryPrice.textContent = `配送费¥${this.uiData.deliveryPrice}`;
// 设置起送费还差多少
if (this.uiData.isCrossDeliveryThreshold()) {
// 到达起送点,就加上active类
this.doms.footerPay.classList.add("active");
} else {
// 没有到达起送点,就去掉active类
this.doms.footerPay.classList.remove("active");
// 如果没有到达起送点,就更新还差多少钱
var dis = this.uiData.deliveryThreshold - total;
// 四舍五入。因为计算机中小数点计算不准,所以要四舍五入
dis = Math.round(dis);
// 更新span元素的文本
this.doms.footerPayInnerSpan.textContent = `还差¥${dis}元起送`;
}
// 设置总价,toFixed(2)表示保留两位小数 => 是因为小数点计算不准,所以要四舍五入。利用toFixed(2)将总价变为了字符串格式的数字
this.doms.totalPrice.textContent = total.toFixed(2);
// 设置购物车的样式状态
if (this.uiData.hasGoodsInCar()) {
// 有商品,就加上active类
this.doms.car.classList.add("active");
} else {
// 没有商品,就去掉active类
this.doms.car.classList.remove("active");
}
// 设置购物车中的数量,得到总数
this.doms.badge.textContent = this.uiData.getTotalChooseNumber();
}
// 购物车动画
carAnimate() {
this.doms.car.classList.add("animate");
// 去除动画结束事件,但是这样写的话,动画结束事件会被执行两次。而且每写一次,都会有一次动画结束事件。直接在开始写一个动画结束事件,在动画结束事件中去掉动画结束事件,这样就只会执行一次动画结束事件。=> 上面的listenEvent()方法中有写过,效果一样的
// this.doms.car.addEventListener("animationend", function () {
// // 这里的this是动画事件触发的元素
// this.classList.remove("animate");
// });
}
// 抛物线跳跃的元素
jump(index) {
// 找到对应商品的加号
var btnAdd = this.doms.goodsContainer.children[index].querySelector(
".i-jiajianzujianjiahao"
);
// 得到加号的坐标
var rect = btnAdd.getBoundingClientRect();
// 起始坐标
var start = {
x: rect.left,
y: rect.top,
};
// 跳吧
var div = document.createElement("div");
// div的样式
div.className = "add-to-car";
var i = document.createElement("i");
// i的样式
i.className = "iconfont i-jiajianzujianjiahao";
// 设置初始位置,div管横向,i管纵向
div.style.transform = `translateX(${start.x}px)`;
i.style.transform = `translateY(${start.y}px)`;
// 将i添加到div中
div.appendChild(i);
// 将div添加到body中
document.body.appendChild(div);
// 强行渲染
div.clientWidth;
// 设置结束位置,添加一个过渡效果
div.style.transform = `translateX(${this.jumpTarget.x}px)`;
i.style.transform = `translateY(${this.jumpTarget.y}px)`;
var that = this;
// 过渡结束后,删除div => 是为了防止动画结束后,div还在body中,导致动画结束后,div还会执行动画结束事件
div.addEventListener(
"transitionend",
function () {
div.remove();
// 购物车动画得到触发
that.carAnimate();
},
{
once: true, // 事件仅触发一次
}
);
}
}
var ui = new UI();
// 事件
// 添加点击事件
ui.doms.goodsContainer.addEventListener("click", function (e) {
// 判断点击的元素是否是加号
if (e.target.classList.contains("i-jiajianzujianjiahao")) {
// 得到商品的索引,如果点击的是加好,就加1,如果点击的是减号,就减1。这里将索引(字符串)转成数字类型
var index = +e.target.getAttribute("index");
ui.increase(index);
} else if (e.target.classList.contains("i-jianhao")) {
var index = +e.target.getAttribute("index");
ui.decrease(index);
}
});
// 按键事件,如果用户按了+号,就输出Equal,如果按了-号,就输出Minus。这里只是演示,实际开发中,按键事件要用到keydown事件。
window.addEventListener("keypress", function (e) {
if (e.code === "Equal") {
ui.increase(0);
} else if (e.code === "Minus") {
ui.decrease(0);
}
});
/*
也可以用这种方式写,但是用这种方式写的话,会导致代码冗余
function UIGoods(g) {
this.data = g;
this.choose = 1;
}
UIGoods.prototype.getTotalPrice = function(){
return this.data.price * this.choose;
}
UIGoods.prototype.isChoose = function(){
return this.choose >0;
}
new UIGoods(goods[0]);*/
// 提前优化的优缺点:永远不要提前优化,因为优化之后,代码会变得更复杂、可读性会变差。
// 什么时候优化:出问题的时候,优化一下。要么就是出问题了,要么就是出问题了,要么就是出问题了。