最终效果图:
这里我们将头部区域、列表区域、以及列表里的增加和减少商品数量的区域、以及底部计算区域分别单独封装为一个组件,如下:
大致实现步骤如下:
1. 安装项目所需的第三方包
通过 npm install bootstrap@4.6.0 --save 安装 bootstrap
通过 npm install less less-loader@5.0.0 --save 安装 less
main.js – 引入bootstrap样式:
import 'bootstrap/dist/css/bootstrap.css'
2.根据上面的分析在 components 里分别创建 MyHeader 、MyFooter 、MyGoods 、MyCount 组件
完成每个组件的结构和样式 ,然后在 App.vue 里引入注册和使用
注意:MyCount 组件应该在 MyGoods 里引入注册和使用
App.vue,
<template>
<div id="app">
<MyHeader></MyHeader>
<div class="main">
<MyGoods></MyGoods>
</div>
<MyFooter></MyFooter>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyGoods from './components/MyGoods'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {
MyHeader,
MyGoods,
MyFooter
}
}
</script>
<style scoped lang='less'>
.main {
padding: 45px 0 50px 0;
}
</style>
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>
<MyCount></MyCount>
</span>
</div>
</div>
</div>
</template>
<script>
import MyCount from './MyCount'
export default {
components: {
MyCount
}
}
</script>
<style lang="less" scoped>
.my-goods-item {
display: flex;
padding: 10px;
border-bottom: 1px solid #ccc;
overflow: hidden;
.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>
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>
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>
最后效果如下:
3.实现头部组件自定义
这样可以适应不同的情况,例如:
于是我们可以使其背景色、文字颜色以及文字内容通过外界传入,这样便可适应不同背景、不同文字颜色以及不同文字内容,需要使用 props 接收外界传递进来的值
MyHeader.vue,
<div class="my-header"
:style="{backgroundColor: background, color: color}"
>{{title}}</div>
props: {
// 注意:外界在使用时,需要遵守变量名作为属性名,值的类型也要遵守
background: String, // 外界传入此变量的类型为字符串
color: {
type: String, // 约束 color 值的类型
default: '#fff', // 当前 color 的默认值(外界不传时使用默认值)
},
title: {
type: String,
required: true // 必须传入此变量的值
}
}
App.vue,
<MyHeader background="red" title="购物车案例"></MyHeader>
最后效果如下:
注意:
props有哪2种定义方式, 区别是?
props: [] - 只声明变量, 不能类型校验
props: {} - 声明变量和校验类型规则 - 不对则报错
4.请求数据
使用 axios调用接口 – 获取购物车案例数据
通过 npm install axios@0.24.0 --save 安装 axios
在 main.js 里引入:
import axios from 'axios'
// 配置基础地址
axios.defaults.baseURL = 'https://www.escook.cn'
// 将 axios 添加到 vue 原型上,这样就可在项目的任意地方使用
Vue.prototype.$axios = axios
在 App.vue 里的 created 生命周期里请求数据
created () {
// 因为 axios 已经挂载到了全局上,因此无需引入,直接使用即可
this.$axios({
method: 'GET',
url: '/api/cart'
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})
}
请求到的结果如下:
5.将请求到的数据渲染到页面
将数据通过MyGoods组件展示出来
在 App.vue 里定义变量保存请求到的数据,通过 v-for 渲染到页面
App.vue
<MyGoods v-for="obj in list" :key="obj.id" :obj='obj'></MyGoods>
data () {
return {
list: [], // 保存商品的所有数据
}
},
created () {
// 因为 axios 已经挂载到了全局上,因此无需引入,直接使用即可
this.$axios({
method: 'GET',
url: '/api/cart'
}).then(res => {
// console.log(res);
this.list = res.data.list
}).catch(err => {
console.log(err);
})
}
然后在 App.vue 里通过 v-bind 传递给 MyGoods组件 ,在 MyGoods组件 里通过 props 接收
MyGoods.vue,
props: {
obj: Object
}
在 MyGoods组件 里的 input 标签里通过 v-model 进行关联,因为我需要通过一个变量或者是属性来关联其是否选中
MyGoods.vue,
<!--
每个对象和组件都是独立的
这里就利用对象里的 goods_state 来关联自己对应
商品的复选框
-->
<input type="checkbox" class="custom-control-input" id="input" v-model="obj.goods_state">
修改 MyGoods组件 里对应的数据
MyGoods.vue,
<div class="my-goods-item">
<div class="left">
<div class="custom-control custom-checkbox">
<!--
每个对象和组件都是独立的
这里就利用对象里的 goods_state 来关联自己对应
商品的复选框
-->
<input type="checkbox" class="custom-control-input" id="input" v-model="obj.goods_state">
<label class="custom-control-label" for="input">
<img :src="obj.goods_img" alt="">
</label>
</div>
</div>
<div class="right">
<div class="top">{{obj.goods_name}}</div>
<div class="bottom">
<span class="price">¥ {{obj.goods_price}}</span>
<span>
<MyCount :obj='obj'></MyCount>
</span>
</div>
</div>
</div>
注意:商品数量增减这个我们需要将数据传递给 MyCount组件--这里 MyGoods组件是父组件, MyCount组件是子组件,因此在 MyGoods组件 里通过 v-bind 将数据传递给 MyGoods组件, MyGoods组件通过 props 接收,在 MyGoods组件 的 input 标签中通过 v-model 来实现数量的双向绑定
MyGoods.vue,
<span>
<MyCount :obj='obj'></MyCount>
</span>
MyCount.vue,
<input type="number" class="form-control inp" v-model="obj.goods_count">
props: {
// 因为数量控制要通过对象“互相引用的关系”来影响外面对象的数量值,
// 所有最好传 对象
obj: Object
}
最后效果如下:
6.实现商品选择效果
目标:点击复选框完成选中效果;点击商品图片完成复选框 – 选中效果
这里此时会有一个 bug 那就当点击不同的复选框或者图片时会出现选不上的效果甚至是点击当前复选框或者图片选中的却是其他商品,这是因为所有循环的 label 的 for 都是 input,id 也是 input,默认只有第一个生效。解决方案就是让每次对象里的 id 值,分别给 id 和 for 使用即可区分,也就是:
MyGoods.vue,
<input type="checkbox" class="custom-control-input" :id="obj.id" v-model="obj.goods_state">
<label class="custom-control-label" :for="obj.id">
<img :src="obj.goods_img" alt="">
</label>
最终效果如下:
7.实现商品数量控制
目标:点击或输入改变商品数量
在 MyCount.vue 给两个 button 按钮分别绑定点击事件,以实现商品数量的增减,且当商品数量为 1 时,禁用 “减” 按钮
MyCount.vue,
<button type="button" class="btn btn-light" :disabled='obj.goods_count === 1' @click="subFn">-</button>
<button type="button" class="btn btn-light" @click="addFn">+</button>
methods: {
// 增加
addFn(){
this.obj.goods_count++
},
// 减少
subFn(){
// if(this.obj.goods_count > 1){
// this.obj.goods_count--
// }else {
// return
// }
// 有了 :disabled='obj.goods_count === 1' 判断后就可以直接按照如下写
this.obj.goods_count--
}
}
效果如下:
实现输入改变商品数量:
利用侦听器(whtch)来监听用户输入的数字
MyCount.vue,
watch: {
obj: {
deep: true, // 开启深度监听
handler(){
// 当商品数量小于0时,强制改为1
if(this.obj.goods_count < 0){
this.obj.goods_count = 1
}
}
}
}
最终效果如下:
8.实现全选功能
目标:点击全选影响所有小选、点击小选影响全选
在 MyFooter.vue 里的 全选框 中利用 v-model 进行关联,因为 全选 框的选中状态将来也会受到商品里的 复选框 的影响,因此 v-model 后的变量应该使用计算属性。这里因为页面的 v-model 的 true/false 需要传递给 数据层,因此需要使用计算属性的完整写法
MyFooter.vue,
<input type="checkbox" class="custom-control-input" id="footerCheck" v-model="isAll">
computed: {
isAll: {
set(val){ // val 就是关联表单的值(true/false)
},
get(){}
}
}
这里得到全选框的值后(true/false),就应该回传到 App.vue 里再同步给所有的 小选框,就需要用到子传父
App.vue,
<MyFooter @changeAll='allFn'></MyFooter>
methods: {
allFn(bool){
// 将 MyFooter 内的全选状态 true/false 同步给所有小选框的关联属性上
this.list.forEach(obj => obj.goods_state = bool)
}
}
MyFooter.vue,
computed: {
isAll: {
set(val){ // val 就是关联表单的值(true/false)
// console.log(val);
this.$emit('changeAll', val)
},
get(){}
}
}
这样即可实现全选功能
接下来就是实现小选框的选中状态影响全选框的选中状态:
在 MyFooter.vue 里 computed 的 get 方法里,统计小选框的选中状态给全选框
在 App.vue 里通过 v-bind 将 list 数组传递给小选框,因为 list 数组里保存了所有小选框的状态
App.vue,
<MyFooter @changeAll='allFn' :arr='list'></MyFooter>
MyFooter.vue,
props: {
arr: Array
},
computed: {
isAll: {
set(val){ // val 就是关联表单的值(true/false)
// console.log(val);
this.$emit('changeAll', val)
},
get(){
// 查找小选框关联的属性有没有不符合勾选的条件
// every() 查找不符合条件的并返回 false
return this.arr.every(obj => obj.goods_state === true)
}
}
}
这样也就实现小选框的选中状态影响全选框的选中状态
最后效果如下:
9.实现总数量
统计已选中商品的总数量在右下角按钮显示
这里这个右下角的 button 受上面商品也就是小选框的选中与否的影响,因此也需要用到计算属性
MyFooter.vue,
<!-- 按钮 -->
<button type="button" class="footer-btn btn btn-primary">结算 ( {{allCount}} )</button>
computed: {
allCount(){
/**reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值
* array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
* total 必需。初始值, 或者计算结束后的返回值。
currentValue 必需。当前元素
currentIndex 可选。当前元素的索引
arr 可选。当前元素所属的数组对象。
initialValue 可选。传递给函数的初始值
*/
/**
* 这里的 0 其实就是第一个参数 sum 的初始值
* obj 遍历数组里面的每个对象
*/
return this.arr.reduce((sum, obj) => {
// 判断是否选中商品
if(obj.goods_state === true){
sum += obj.goods_count
}
return sum
}, 0)
}
}
最终效果:
10.统计商品的总价
统计已选中商品的总价格
这里的总价格一样也是通过小选框的统而来,因此也需要用计算属性
在统计数组时,要判断勾选状态才能进行累加,累加好数据后返回给 allPrice 变量显示
MyFootet.vue,
<!-- 合计 -->
<div>
<span>合计:</span>
<span class="price">¥ {{allPrice}}</span>
</div>
// 统计总价
allPrice(){
return this.arr.reduce((sum, obj) => {
if(obj.goods_state === true){
sum += obj.goods_count * obj.goods_price
}
return sum
}, 0)
}
到这里购物车案例就已经全部实现了
最终效果如下:
所有源码如下:
App.vue
<template>
<div id="app">
<MyHeader background="red" title="购物车案例"></MyHeader>
<div class="main">
<MyGoods v-for="obj in list" :key="obj.id" :obj='obj'></MyGoods>
</div>
<MyFooter @changeAll='allFn' :arr='list'></MyFooter>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyGoods from './components/MyGoods'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {
MyHeader,
MyGoods,
MyFooter
},
data () {
return {
list: [], // 保存商品的所有数据
}
},
created () {
// 因为 axios 已经挂载到了全局上,因此无需引入,直接使用即可
this.$axios({
method: 'GET',
url: '/api/cart'
}).then(res => {
// console.log(res);
this.list = res.data.list
}).catch(err => {
console.log(err);
})
},
methods: {
allFn(bool){
// 将 MyFooter 内的全选状态 true/false 同步给所有小选框的关联属性上
this.list.forEach(obj => obj.goods_state = bool)
}
}
}
</script>
<style scoped lang='less'>
.main {
padding: 45px 0 50px 0;
}
</style>
MyHeader.vue
<template>
<div class="my-header"
:style="{backgroundColor: background, color: color}"
>{{title}}</div>
</template>
<script>
export default {
props: {
// 注意:外界在使用时,需要遵守变量名作为属性名,值的类型也要遵守
background: String, // 外界传入此变量的类型为字符串
color: {
type: String, // 约束 color 值的类型
default: '#fff', // 当前 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>
MyGoods.vue
<template>
<div class="my-goods-item">
<div class="left">
<div class="custom-control custom-checkbox">
<!--
每个对象和组件都是独立的
这里就利用对象里的 goods_state 来关联自己对应
商品的复选框
-->
<input type="checkbox" class="custom-control-input" :id="obj.id" v-model="obj.goods_state">
<label class="custom-control-label" :for="obj.id">
<img :src="obj.goods_img" alt="">
</label>
</div>
</div>
<div class="right">
<div class="top">{{obj.goods_name}}</div>
<div class="bottom">
<span class="price">¥ {{obj.goods_price}}</span>
<span>
<MyCount :obj='obj'></MyCount>
</span>
</div>
</div>
</div>
</template>
<script>
import MyCount from './MyCount'
export default {
components: {
MyCount
},
props: {
obj: Object
}
}
</script>
<style lang="less" scoped>
.my-goods-item {
display: flex;
padding: 10px;
border-bottom: 1px solid #ccc;
overflow: hidden;
.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>
MyCount.vue
<template>
<div class="my-counter">
<button type="button" class="btn btn-light" :disabled='obj.goods_count === 1' @click="subFn">-</button>
<input type="number" class="form-control inp" v-model.number="obj.goods_count">
<button type="button" class="btn btn-light" @click="addFn">+</button>
</div>
</template>
<script>
export default {
props: {
// 因为数量控制要通过对象“互相引用的关系”来影响外面对象的数量值,
// 所有最好传 对象
obj: Object // 商品对象
},
methods: {
// 增加
addFn(){
this.obj.goods_count++
},
// 减少
subFn(){
// if(this.obj.goods_count > 1){
// this.obj.goods_count--
// }else {
// return
// }
// 有了 :disabled='obj.goods_count === 1' 判断后就可以直接按照如下写
this.obj.goods_count--
}
},
watch: {
obj: {
deep: true, // 开启深度监听
handler(){
// 当商品数量小于0时,强制改为1
if(this.obj.goods_count < 0){
this.obj.goods_count = 1
}
}
}
}
}
</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>
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>
export default {
props: {
arr: Array
},
computed: {
isAll: {
set(val){ // val 就是关联表单的值(true/false)
// console.log(val);
this.$emit('changeAll', val)
},
get(){
// 查找小选框关联的属性有没有不符合勾选的条件
// every() 查找不符合条件的并返回 false
return this.arr.every(obj => obj.goods_state === true)
}
},
// 统计数量
allCount(){
/**reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值
* array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
* total 必需。初始值, 或者计算结束后的返回值。
currentValue 必需。当前元素
currentIndex 可选。当前元素的索引
arr 可选。当前元素所属的数组对象。
initialValue 可选。传递给函数的初始值
*/
/**
* 这里的 0 其实就是第一个参数 sum 的初始值
* obj 遍历数组里面的每个对象
*/
return this.arr.reduce((sum, obj) => {
// 判断是否选中商品
if(obj.goods_state === true){
sum += obj.goods_count
}
return sum
}, 0)
},
// 统计总价
allPrice(){
return this.arr.reduce((sum, obj) => {
if(obj.goods_state === true){
sum += obj.goods_count * obj.goods_price
}
return sum
}, 0)
}
}
}
</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>