购物车项目
项目完成后的样子
1.0 案例-购物车-项目初始化
目标: 初始化新项目, 清空不要的东西, 下载bootstrap库, 下载less模块
vue create shopcar
yarn add bootstrap@3.4.1
yarn add less less-loader@5.0.0 -D
图示:
-
按照需求, 把项目页面拆分成几个组件, 在components下创建
-
MyHeader组件
-
MyFooter组件
-
MyGoods组件 - 商品
-
MyCount组件
-
然后引入到App.vue上注册
-
在main.js中引入bootStrap库
import "bootstrap/dist/css/bootstrap.css" // 引入第三方包里的某个css文件
MyHeader.vue
<template>
<div class="my-header">购物车案例</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.my-header {
height: 45px;
line-height: 45px;
text-align: center;
background-color: #1d7bff;
color: #fff;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 2;
}
</style>
MyGoods.vue
<template>
<div class="my-goods-item">
<div class="left">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="input"
>
<label class="custom-control-label" for="input">
<img src="http://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg" alt="">
</label>
</div>
</div>
<div class="right">
<div class="top">商品名字</div>
<div class="bottom">
<span class="price">¥ 100</span>
<span>
数量组件
</span>
</div>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.my-goods-item {
display: flex;
padding: 10px;
border-bottom: 1px solid #ccc;
.left {
img {
width: 120px;
height: 120px;
margin-right: 8px;
border-radius: 10px;
}
.custom-control-label::before,
.custom-control-label::after {
top: 50px;
}
}
.right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.top{
font-size: 14px;
font-weight: 700;
}
.bottom {
display: flex;
justify-content: space-between;
padding: 5px 0;
align-items: center;
.price {
color: red;
font-weight: bold;
}
}
}
}
</style>
目标: 完成商品组件右下角商品组件的开发
components/MyCount.vue
<template>
<div class="my-counter">
<button type="button" class="btn btn-light" >-</button>
<input type="number" class="form-control inp" >
<button type="button" class="btn btn-light">+</button>
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.my-counter {
display: flex;
.inp {
width: 45px;
text-align: center;
margin: 0 10px;
}
.btn, .inp{
transform: scale(0.9);
}
}
</style>
components/MyFooter.vue
<template>
<!-- 底部 -->
<div class="my-footer">
<!-- 全选 -->
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="footerCheck">
<label class="custom-control-label" for="footerCheck">全选</label>
</div>
<!-- 合计 -->
<div>
<span>合计:</span>
<span class="price">¥ 0</span>
</div>
<!-- 按钮 -->
<button type="button" class="footer-btn btn btn-primary">结算 ( 0 )</button>
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.my-footer {
position: fixed;
z-index: 2;
bottom: 0;
width: 100%;
height: 50px;
border-top: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
background: #fff;
.price {
color: red;
font-weight: bold;
font-size: 15px;
}
.footer-btn {
min-width: 80px;
height: 30px;
line-height: 30px;
border-radius: 25px;
padding: 0;
}
}
</style>
1.1 案例-购物车-头部自定义
目的: 头部的标题, 颜色, 背景色可以随便修改, props类型的校验
思路
- 在MyHeader.vue中准备props里变量, 然后使用
- 在使用MyHeader.vue组件时, 传入相应的值 (color和backgroundColor)
MyHeader.vue
<template>
<div class="my-header" :style="{backgroundColor: background, color}">{{ title }}</div>
</template>
<script>
// 目标: 让Header组件支持不同的项目 - 自定义
// 1. 分析哪些可以自定义 (背景色, 文字颜色, 文字内容)
// 2. (新) 可以对props的变量的值 进行校验
// 3. 内部使用props变量的值
// 4. 外部使用时, 遵守变量名作为属性名, 值的类型遵守
export default {
props: {
background: String, // 外部插入此变量的值, 必须是字符串类型, 否则报错
color: {
type: String, // 约束color值的类型
default: "#fff" // color变量默认值(外部不给 我color传值, 使用默认值)
},
title: {
type: String,
required: true // 必须传入此变量的值
}
}
}
</script>
<style lang="less" scoped>
.my-header {
height: 45px;
line-height: 45px;
text-align: center;
background-color: #1d7bff;
color: #fff;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 2;
}
</style>
App.vue传入相应自定义的值
<MyHeader title="购物车案例"></MyHeader>
总结:
props: [] - 只能声明变量和接收, 不能类型校验
props: {} - 声明变量和校验类型规则 - 外部传入值不对则报错
1.2 案例-购物车-请求数据
目标: 使用axios把数据请求回来
数据地址: www.escook.cn/api/cart (get方式)
-
下载axios
yarn add axios
-
main.js - 原型上挂载
// 目标: 请求数据 - 打印
// 1. 下载axios库, main.js - 全局绑定属性 (确保任意.vue文件可以都访问到这个axios方法)
import axios from 'axios'
// 2. 基础地址
axios.defaults.baseURL = "https://www.escook.cn"
// 3. axios方法添加到Vue的原型上
Vue.prototype.$axios = axios
new Vue({
render: h => h(App),
}).$mount('#app')
-
App.vue请求使用
<script>
export default {
data(){
return {
list: [] // 商品所有数据
}
},
created(){
// 不必在自己引入axios变量, 而是直接使用全局属性$axios
this.$axios({
url: "/api/cart"
}).then(res => {
console.log(res);
this.list = res.data.list
})
}
}
</script>
总结: 利用axios, 调用接口, 把数据请求回来
1.3 案例-购物车-数据渲染
目标: 把上面请求的数据, 铺设到页面上
App.vue
<MyGoods v-for="obj in list"
:key="obj.id"
:gObj="obj"
></MyGoods>
MyGoods.vue
<template>
<div class="my-goods-item">
<div class="left">
<div class="custom-control custom-checkbox">
<!-- *重要:
每个对象和组件都是独立的
对象里的goods_state关联自己对应商品的复选框
-->
<!-- bug:
循环的所有label的for都是input, id也都是input - 默认只有第一个生效
解决: 每次对象里的id值(1, 2), 分别给id和for使用即可区分
-->
<input type="checkbox" class="custom-control-input" :id="gObj.id"
v-model="gObj.goods_state"
>
<label class="custom-control-label" :for="gObj.id">
<img :src="gObj.goods_img" alt="">
</label>
</div>
</div>
<div class="right">
<div class="top">{{ gObj.goods_name }}</div>
<div class="bottom">
<span class="price">¥ {{ gObj.goods_price }}</span>
<span>
<MyCount :obj="gObj"></MyCount>
</span>
</div>
</div>
</div>
</template>
<script>
import MyCount from './MyCount'
export default {
props: {
gObj: Object
},
components: {
MyCount
}
}
</script>
MyCount.vue
<template>
<div class="my-counter">
<button type="button" class="btn btn-light" >-</button>
<input type="number" class="form-control inp" v-model.number="obj.goods_count">
<button type="button" class="btn btn-light" >+</button>
</div>
</template>
<script>
export default {
props: {
obj: Object // 商品对象
}
}
</script>
总结: 把各个组件关联起来, 把数据都铺设到页面上
1.4 案例-购物车-商品选中
问题: 点击发现总是第一个被选中
原来id和for都是"input"
但是id是唯一的啊, 所以用数据的id来作为标签的id, 分别独立, 为了兼容label点击图片也能选中的效果
<input type="checkbox" class="custom-control-input" :id="gObj.id"
v-model="gObj.goods_state"
>
<label class="custom-control-label" :for="gObj.id">
<img :src="gObj.goods_img" alt="">
</label>
总结: lable的for值对应input的id, 点击label就能让对应input处于激活
1.5 案例-购物车-数量控制
目标: 点击+和-或者直接修改输入框的值影响商品购买的数量
<template>
<div class="my-counter">
<button type="button" class="btn btn-light" :disabled="obj.goods_count === 1" @click="obj.goods_count > 1 && obj.goods_count--">-</button>
<input type="number" class="form-control inp" v-model.number="obj.goods_count">
<button type="button" class="btn btn-light" @click="obj.goods_count++">+</button>
</div>
</template>
<script>
// 目标: 商品数量 - 控制
// 1. 外部传入数据对象
// 2. v-model关联对象的goods_count属性和输入框 (双向绑定)
// 3. 商品按钮 +和-, 商品数量最少1件
// 4. 侦听数量改变, 小于1, 直接强制覆盖1
export default {
props: {
obj: Object // 商品对象
},
// 因为数量控制要通过对象"互相引用的关系"来影响外面对象里的数量值, 所以最好传 对象进来
watch: {
obj: {
deep: true,
handler(){ // 拿到商品数量, 判断小于1, 强制修改成1
if (this.obj.goods_count < 1) {
this.obj.goods_count = 1
}
}
}
}
}
</script>
1.6 案例-购物车-全选功能
目标: 在底部组件上, 完成全选功能
思路:
- 点击获取它的选中状态
- 同步给上面每个小选框 - 而小选框的选中状态又在数组里
- 把数组传给MyFooter, 然后更新即可 - 因为对象都是引用关系的
MyFooter.vue
<template>
<!-- 底部 -->
<div class="my-footer">
<!-- 全选 -->
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="footerCheck" v-model="isAll">
<label class="custom-control-label" for="footerCheck">全选</label>
</div>
<!-- 合计 -->
<div>
<span>合计:</span>
<span class="price">¥ {{ allPrice }}</span>
</div>
<!-- 按钮 -->
<button type="button" class="footer-btn btn btn-primary">结算 ( {{ allCount }} )</button>
</div>
</template>
<script>
// 目标: 全选
// 1. v-model关联全选-复选框(v-model后变量计算属性)
// 2. 页面(视频层)v(true) -> 数据层(变量-) 计算属性(完整写法)
// 3. 把全选 true/false同步给所有小选框选中状态上
// 小选 -> 全选
// App.vue里list数组 -> MyFooter.vue
// isAll的get方法里, 统计状态影响全选框
// 目标: 总数量统计
// 1. allCount计算属性用 数组reduce+判断统计数量并返回
// 目标: 总价
// allPrice计算属性, 数组reduce+单价*数量, 判断选中, 才累加后返回
export default {
props: {
arr: Array
},
computed: {
isAll: {
set(val){ // val就是关联表单的值(true/false)
this.$emit('changeAll', val)
},
get(){
// 查找小选框关联的属性有没有不符合勾选的条件
// 直接原地false
return this.arr.every(obj => obj.goods_state === true)
}
},
}
}
</script>
App.vue
<MyFooter @changeAll="allFn" :arr="list"></MyFooter>
<script>
methods: {
allFn(bool){
this.list.forEach(obj => obj.goods_state = bool)
// 把MyFooter内的全选状态true/false同步给所有小选框的关联属性上
}
}
</script>
总结: 全选的v-model的值, 使用计算属性完整写法
1.7 案例-购物车-总数量
目标: 完成底部组件, 显示选中的商品的总数量
MyFooter.vue
allCount(){
return this.arr.reduce((sum, obj) => {
if (obj.goods_state === true) { // 选中商品才累加数量
sum += obj.goods_count;
}
return sum;
}, 0)
},
总结: 对象之间是引用关系, 对象值改变, 所有用到的地方都跟着改变
1.8 案例-购物车-总价
目标: 完成选中商品计算价格
components/MyFooter.vue
allPrice(){
return this.arr.reduce((sum, obj) => {
if (obj.goods_state){
sum += obj.goods_count * obj.goods_price
}
return sum;
}, 0)
}
总结: 把数组传给了MyFooter组件, 统计总价
另一种
shopHeader
<template>
<div>
<div class="header">购物车案例</div>
</div>
</template>
<script>
export default {
data() {
return {};
},
};
</script>
<style scoped lang="less">
.header {
line-height: 60px;
text-align: center;
background: #1d7bff;
color: #fff;
font-size: 18px;
}
</style>
shopMiax
<template>
<div>
<div class="list">
<div class="item">
<input
class="checkbox"
type="checkbox"
v-model="list.goods_state"
:id="list.id"
/>
<label :for="list.id"> <img :src="list.goods_img" alt="" /></label>
<div class="info">
<div class="top">{{ list.goods_name }}</div>
<div class="bottom">
<div class="price">¥{{ list.goods_price }}</div>
<div class="number">
<button @click="list.goods_count--">-</button>
<input
class="text"
type="text"
v-model.number="list.goods_count"
/>
<button @click="list.goods_count++">+</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["list"],
watch: {
"list.goods_count": function (newVal) {
//当数量小于一的时候
if (this.list.goods_count < 1) {
this.list.goods_count = 1; //把数量等于一,就是不能让输入框的数量小于一的意思,真实的购物车是要当数量小于一的时候,询问用户是否删除此商品,确认就向后台发送请求删除购物车的这个商品
}
},
},
data() {
return {};
},
};
</script>
<style scoped lang="less">
.list {
border-top: 1px solid #eee;
.item {
display: flex;
align-items: center;
margin-bottom: 20px;
.checkbox {
margin: 0 20px;
}
img {
width: 140px;
height: 140px;
}
.info {
display: flex;
flex: 1;
flex-direction: column;
height: 140px;
padding: 20px;
justify-content: space-between;
.top {
// 限制显示三行名字
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.bottom {
display: flex;
justify-content: space-between;
}
.number {
.text {
text-align: center;
}
display: flex;
input {
width: 30px;
}
button {
width: 20px;
}
}
}
}
}
</style>
shopfoot
<template>
<div>
<div class="footer">
<div>
<input
class="check"
v-model="isAll"
type="checkbox"
name=""
id=""
/>全选
</div>
<div class="total">合计¥{{ totalPrice }}</div>
<button>结算{{ closePrice }}</button>
</div>
</div>
</template>
<script>
export default {
props: ["list"],
computed: {
//计算属性实现单选控制全选,全选控制单选
isAll: {
//别人控制自己就是get
get() {
//单选控制全选
return this.list.every((item) => item.goods_state);
},
//自己控制别人就是set
set(isChecked) {
//遍历数组,全选控制单选
this.list.forEach((element) => {
element.goods_state = isChecked;
});
},
},
//合计
totalPrice() {
let res = 0;
//遍历数组查看那些商品处于被选中状态
this.list.forEach((element) => {
if (element.goods_state) {
//当商品处于被选中的时候,他的数量乘于价格
res += element.goods_count * element.goods_price;
}
});
return res;
},
//结算
closePrice() {
let res = 0;
//遍历一遍看看数组那些商品处于被选中状态
this.list.forEach((element) => {
if (element.goods_state) {
//当商品处于被选中的时候,把选择的商品加数量赋予结算按钮
res += element.goods_count;
}
});
return res;
},
},
data() {
return {};
},
created() {},
mounted() {},
methods: {},
};
</script>
<style scoped lang="less">
.footer {
width: 100%;
display: flex;
align-items: center;
padding: 10px 20px;
justify-content: space-between;
border-top: 2px solid #eee;
background-color: #fff;
.check {
margin-right: 5px;
}
button {
border: none;
background: #1d7bff;
color: #fff;
line-height: 30px;
padding: 0 20px;
border-radius: 15px;
}
}
</style>
app
<template>
<div class="bigBox">
<div><ShopHeader /></div>
<div class="lists">
<ShopMiax v-for="item in list" :key="item.id" :list="item" />
</div>
<div><ShopFoot :list="list" /></div>
</div>
</template>
<script>
import ShopHeader from "./components/shopHeader.vue"; //头部
import ShopMiax from "./components/shopMiax.vue"; //主要内容
import ShopFoot from "./components/shopFoot.vue"; //底部
import { getShop } from "./api/shop";
export default {
components: {
ShopHeader,
ShopMiax,
ShopFoot,
},
data() {
return {
list: [],
};
},
created() {
getShop().then((res) => {
console.log(res);
this.list = res.data.list;
});
},
};
</script>
<style lang="less">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>
<style lang="less" scoped>
.bigBox {
//设个大盒子把所有内容包裹起来,让内容单独滚动,头部盒子和底部盒子不动
height: 100vh;
display: flex;
flex-direction: column;
.lists {
overflow: auto; //设置内容滚动
}
}
</style>