目录
今天用Vue实现的一个手机端的购物车案例,着重阐述组件化思想的优势,将页面的内容分成各个模块进行书写,然后数据传输,父传子、子传父、兄弟数据共享等,这方面知识不牢固的话可以关注一下右方vue的专栏:Vue专栏 里面详解介绍了vue的知识,今天的案例也是借助黑马的相关案例及其接口,既是分享也是回顾。
前期准备
因为本案例是借助vue的框架进行实现的,所以我们需要先搭建vue-cli脚手架,具体的搭建过程请看右方链接 vue-cli脚手架的搭建与使用 ,搭建完成后,需要根据案例的具体的图片功能点要求给components文件下新建子组件文件,案例实现结果以及父子组件的框架如下:
写项目之前先选择好自己想要的手机版本是什么,我这里就用最常见的iPhone 6/7/8 这个手机尺寸了,如果想更换手机版本,可以自行在浏览器进行更换。
为了语义化,这边我把components文件名更改为shopping,不该也没关系。
Header
根据上文图片案例,我们先从上面也是最简单的Header来写,为了便于数据的管理,我们把标题设置为自定义属性,允许使用者自定义标题的内容。
Header.vue子组件代码
-
<template>
-
<div class="header-container">{{title}}
</div>
-
</template>
-
<script>
-
export
default {
-
props:{
-
// 声明 title 自定义属性,允许使用者自定义标题的内容
-
title:{
-
default:
'',
-
type:
String
-
}
-
}
-
}
-
</script>
-
<style lang="less" scoped>
-
.header-container{
-
font-size:
12px;
-
height:
45px;
-
width:
100%;
-
background-color:
#008c8c;
-
display: flex;
-
justify-content: center;
-
align-items: center;
-
color:
#fff;
-
position: fixed;
-
top:
0;
-
z-index:
999;
-
}
-
</style>
App.vue父组件代码
-
<template>
-
<div class="app-container">
-
<!-- 头部区域 -->
-
<Header title="购物车案例">
</Header>
-
</div>
-
</template>
-
<script>
-
// 导入需要的组件
-
import
Header
from
'@/shopping/Header/Header.vue'
-
export
default {
-
components:{
-
Header
-
}
-
}
-
</script>
-
<style lang="less" scoped>
-
.app-container{
-
padding-top:
45px;
-
padding-bottom:
50px
-
}
-
</style>
-
Goods
现在开始从项目的内容入手,因为子组件的内容肯定不能写死了,所以需要将父组件的值传入到子组件,而父组件的值从哪来?这里需要借助接口来获取自己要渲染的商品列表数据,而借助接口传值需要使用ajax或者是axios,所以我们需要先在当前项目下安装 axios,命令如下:
npm install axios -S
安装完成之后,在App.vue 父组件下 使用axios调用接口进行使用:很明显,我们先在data里面定义一个空数组,如果接收到接口里面的数据状态为200,就把接口里面的数据传递到我们定义的list里面,具体方法如下:
在控制台打印的接口数据如下,可以方便的查看接口里面的属性:
Goods.vue子组件代码
因为子组件的复选框的数据是没有和父组件的数据进行联通的,如果不把子组件修改复选框的状态的值传到App.vue父组件上,父组件上的goods_state是不会发生变化的,所以要通过自定义事件进行子向父传值,将复选框的修改状态传递到父组件上面。
-
<template>
-
<div class="goods-container">
-
<!-- 左侧图片 -->
-
<div class="thumb">
-
<!-- 复选框 -->
-
<div class="custom-control custom-checkbox">
-
<input type="checkbox" class="custom-control-input" :id="'cb'+id" :checked="state" @change="stateChange" />
-
<label :for="'cb'+id" class="custom-control-label">
-
<!-- 商品的缩略图 -->
-
<img :src="pic" alt="">
-
</label>
-
</div>
-
</div>
-
<!-- 右侧信息区域 -->
-
<div class="goods-infos">
-
<!-- 商品标题 -->
-
<h6 class="goods-title">{{title}}
</h6>
-
<div class="goods-info-bottom">
-
<!-- 商品价格 -->
-
<span class="goods-price">¥{{price}}
</span>
-
<!-- 商品的数量 -->
-
</div>
-
</div>
-
</div>
-
</template>
-
-
<script>
-
export
default {
-
props:{
-
// 商品的id,将来子组件中商品的勾选状态变化之后,需要通过子 -> 父的形式,通知父组件根据id修改对应商品的修改状态
-
id:{
-
require:
true,
-
type:
Number
-
},
-
// 要渲染的商品的标题
-
title:{
-
default:
'',
-
type:
String
-
},
-
// 要渲染的商品的图片
-
pic:{
-
default:
'',
-
type:
String
-
},
-
// 商品的单价
-
price:{
-
default:
0,
-
type:
Number
-
},
-
// 商品的勾选状态
-
state:{
-
default:
true,
-
type:
Boolean
-
},
-
},
-
methods:{
-
// 只有复选框的选中状态发生了变化就会调用这个处理函数
-
stateChange(
e){
-
const newState = e.
target.
checked;
-
this.$emit(
'state-change', {
id:
this.
id,
value:newState});
-
}
-
},
-
}
-
</script>
-
-
<style lang="less" scoped>
-
.goods-container{
-
+
.goods-container{
-
border-top:
1px solid
#efefef
-
}
-
-
padding:
10px;
-
display: flex;
-
.thumb{
-
display: flex;
/*display:flex 意思是弹性布局,它能够扩展和收缩 flex 容器内的元素,以最大限度地填充可用空间。*/
-
align-items: center;
/* 设置项目交叉轴方向上的对齐方式 */
-
img{
-
width:
80px;
-
height:
80px;
-
margin:
10px;
-
}
-
.custom-control{
-
width:
114px;
-
height:
105px;
-
}
-
}
-
.goods-infos{
-
display: flex;
-
flex-direction: column;
/*灵活的项目将垂直显示,正如一个列一样。在这里插入图片描述*/
-
justify-content: space-between;
/* 均匀排列每个元素首个元素放置于起点,末尾元素放置于终点 */
-
height:
100px;
-
flex:
1;
-
.goods-title{
-
font-size:
12px;
-
font-weight: bold;
-
}
-
.goods-info-bottom{
-
display: flex;
-
justify-content: space-between;
-
.goods-price{
-
font-weight: bold;
-
color: red;
-
font-size:
13px;
-
}
-
}
-
}
-
}
-
</style>
App.vue父组件代码
父组件通过接收子组件自定义的事件名,通过将函数methods里面的方法判断,来进行动态的改变 list.goods_state 里面的值。
-
<template>
-
<div class="app-container">
-
<!-- 头部区域 -->
-
<Header title="购物车案例">
</Header>
-
<!-- 循环渲染每一个商品的信息 -->
-
<Goods
-
v-for=
"item in list"
-
:key=
"item.id"
-
:id=
"item.id"
-
:title=
"item.goods_name"
-
:pic=
"item.goods_img"
-
:price=
"item.goods_price"
-
:state=
"item.goods_state"
-
@
state-change=
"getNewState"
-
>
-
</Goods>
-
</div>
-
</template>
-
<script>
-
// 导入 axios 请求库
-
import axios
from
'axios'
-
// 导入需要的组件
-
import
Header
from
'@/shopping/Header/Header.vue'
-
import
Goods
from
'@/shopping/Goods/Goods.vue'
-
export
default {
-
data(
){
-
return {
-
// 用来存储购物车的列表数据,默认为空数组
-
list:[]
-
}
-
},
-
methods:{
-
// 封装请求列表数据的方法
-
async
initCarList(
){
-
// 调用 axios 的 get 方法,请求列表数据
-
const {
data:res} =
await axios.
get(
"https://www.escook.cn/api/cart")
-
console.
log(res);
-
if(res.
status ===
200){
-
this.
list = res.
list
-
}
-
},
-
// 接收子组件传递过来的数据
-
getNewState(
val){
-
this.
list.
some(
item => {
-
if(item.
id === val.
id){
-
item.
goods_state = val.
value
-
// 终止后续循环
-
return
true
-
}
-
})
-
},
-
},
-
components:{
-
Header,
Goods
-
},
-
created(
){
-
// 调用请求数据的方法
-
this.
initCarList()
-
}
-
}
-
</script>
-
<style lang="less" scoped>
-
.app-container{
-
padding-top:
45px;
-
padding-bottom:
50px
-
}
-
</style>
-
Footer
现在实现购物车底部的全选、总计、以及结算的功能样式,因为数据也不能写死了,所以需要我们进行数据绑定,然后通过父组件获取的数据进行传值。
Footer.vue子组件代码
-
<template>
-
<div class="footer-container">
-
<!-- 左侧的全选 -->
-
<div class="custom-control custom-checkbox">
-
<input type="checkbox" class="custom-control-input" id="cbFull" :checked="isfull" @change="fullchange">
-
<label for="cbFull" class="custom-control-label">全选
</label>
-
</div>
-
<!-- 中间的合计 -->
-
<div>
-
<span>合计:
</span>
-
<span class="total-price">¥{{amount.toFixed(2)}}
</span>
-
</div>
-
<!-- 结算按钮 -->
-
<button type="button" class="btn btn-primary btn-settle">结算({{all}})
</button>
-
</div>
-
</template>
-
-
<script>
-
export
default {
-
props:{
-
// 全选的状态
-
isfull:{
-
type:
Boolean,
-
default:
true
-
},
-
// 总价格
-
amount:{
-
type:
Number,
-
default:
0
-
},
-
// 已勾选的商品的总数量
-
all:{
-
type:
Number,
-
default:
0
-
}
-
},
-
methods:{
-
// 监听到了全选的状态变化
-
fullchange(
e){
-
this.$emit(
'full-change',e.
target.
checked)
-
}
-
}
-
}
-
</script>
-
-
<style lang="less" scoped>
-
.footer-container{
-
font-size:
12px;
-
height:
60px;
-
width:
100%;
-
border-top:
1px solid
#efefef;
-
position: fixed;
-
bottom:
0;
-
background-color:
#fff;
-
display: flex;
-
justify-content: space-between;
-
align-items: center;
-
padding:
0
10px;
-
.custom-checkbox{
-
font-size:
13px;
-
display: flex;
-
align-items: center;
-
.custom-control-label{
-
margin-bottom: -
5px;
-
}
-
#cbFull{
-
margin-right:
5px;
-
}
-
}
-
.total-price{
-
font-weight: bold;
-
font-size:
14px;
-
color: red;
-
}
-
.btn-settle{
-
height:
70%;
-
min-width:
110px;
-
border-radius:
25px;
-
font-size:
12px;
-
}
-
-
}
-
</style>
App.vue父组件代码
-
<template>
-
<div class="app-container">
-
<!-- 头部区域 -->
-
<Header title="购物车案例">
</Header>
-
<!-- 循环渲染每一个商品的信息 -->
-
<Goods
-
v-for=
"item in list"
-
:key=
"item.id"
-
:id=
"item.id"
-
:title=
"item.goods_name"
-
:pic=
"item.goods_img"
-
:price=
"item.goods_price"
-
:state=
"item.goods_state"
-
@
state-change=
"getNewState"
-
>
-
</Goods>
-
<!-- Footer区域 -->
-
<Footer :isfull="fullState" :amount="amt" :all="total" @full-change="getFullState">
</Footer>
-
</div>
-
</template>
-
<script>
-
// 导入 axios 请求库
-
import axios
from
'axios'
-
// 导入需要的组件
-
import
Header
from
'@/shopping/Header/Header.vue'
-
import
Goods
from
'@/shopping/Goods/Goods.vue'
-
import
Footer
from
'@/shopping/Footer/Footer.vue'
-
export
default {
-
data(
){
-
return {
-
// 用来存储购物车的列表数据,默认为空数组
-
list:[]
-
}
-
},
-
methods:{
-
// 封装请求列表数据的方法
-
async
initCarList(
){
-
// 调用 axios 的 get 方法,请求列表数据
-
const {
data:res} =
await axios.
get(
"https://www.escook.cn/api/cart")
-
console.
log(res);
-
if(res.
status ===
200){
-
this.
list = res.
list
-
}
-
},
-
// 接收子组件传递过来的数据
-
getNewState(
val){
-
this.
list.
some(
item => {
-
if(item.
id === val.
id){
-
item.
goods_state = val.
value
-
// 终止后续循环
-
return
true
-
}
-
})
-
},
-
// 接收 Footer 子组件传递过来的全选按钮的状态
-
getFullState(
val){
-
this.
list.
forEach(
item => item.
goods_state = val)
-
}
-
},
-
computed:{
-
// 动态计算出全选的状态是 true 还是 false
-
fullState(
){
-
return
this.
list.
every(
item => item.
goods_state)
-
},
-
// 已勾选的商品总价格
-
amt(
){
-
// 1.先filter过滤
-
// 2.再reduce累加
-
return
this.
list.
filter(
item=>item.
goods_state).
reduce(
(total,item)=>{
-
return total+=item.
goods_price * item.
goods_count
-
},
0)
-
},
-
// 已勾选商品的总数量
-
total(
){
-
return
this.
list.
filter(
item => item.
goods_state).
reduce(
(t,item)=>{
-
return t+=item.
goods_count
-
},
0)
-
}
-
},
-
components:{
-
Header,
Goods,
Footer
-
},
-
created(
){
-
// 调用请求数据的方法
-
this.
initCarList()
-
}
-
}
-
</script>
-
<style lang="less" scoped>
-
.app-container{
-
padding-top:
45px;
-
padding-bottom:
50px
-
}
-
</style>
-
Counter
因为count是修改商品的数量的,所以修改的数量要直接修改到父组件的App.vue里面的数据,而要想直接修改App.vue数据是不可能的,因为Counter组件外面还嵌套一层Goods组件,App与Counter相当于爷孙的关系,所以我们可以通过eventBus让Counter直接去修改App里面的值。
eventBus.js文件
-
import
Vue
from
'vue'
-
export
default
new
Vue()
Counter.vue子组件
因为Counter是嵌套在Goods组件里面的,所以我们还需要在Goods组件去引用Counter子组件
给Goods的props属性在设置一个count,用来表明商品的数量。
-
<template>
-
<div class="number-container d-flex justify-content-center align-items-center">
-
<!-- 减 1 的按钮 -->
-
<button type="button" class="btn btn-light bnt-sm" @click="sub">-
</button>
-
<!-- 购买的数量 -->
-
<span class="number-box">{{num}}
</span>
-
<!-- 加 1 的按钮 -->
-
<button type="button" class="btn btn-light bnt-sm" @click="add">+
</button>
-
</div>
-
</template>
-
-
<script>
-
// 导入eventBus文件
-
import bus
from
'@/shopping/eventBus.js'
-
export
default {
-
props:{
-
// 接收商品的id值,将来使用 EventBus 方案,把数量传递到 App.vue 的时候,需要通知 App 组件,更新哪个商品的数量
-
id:{
-
type:
Number,
-
required:
true
-
},
-
// 接收到的 num 数量值
-
num:{
-
type:
Number,
-
default:
1
-
}
-
},
-
methods:{
-
// 点击按钮,数值+1
-
add(
){
-
// 要发送给 App 的数据格式为 {id,value}
-
// 其中,id是商品的id;value是商品最新的购买数量
-
const obj = {
id:
this.
id,
value:
this.
num+
1}
-
// 要做的事:通过 EventBus 把 obj 对象,发送给 App.vue 组件
-
bus.$emit(
'share',obj)
-
},
-
sub(
){
-
if(
this.
num-
1 ==
0)
return
-
// 要发送给 App 的数据格式为 {id,value}
-
// 其中,id是商品的id;value是商品最新的购买数量
-
const obj = {
id:
this.
id,
value:
this.
num-
1}
-
// 要做的事:通过 EventBus 把 obj 对象,发送给 App.vue 组件
-
bus.$emit(
'share',obj)
-
}
-
}
-
}
-
</script>
-
-
<style>
-
-
</style>
App.vue父组件代码
导入eventBus.js文件,通过bus.$on()方法,调用Counter传来的share里面的数据,通过传来的数据,来修改list里面的goods_count里面的值。
-
<template>
-
<div class="app-container">
-
<!-- 头部区域 -->
-
<Header title="购物车案例">
</Header>
-
<!-- 循环渲染每一个商品的信息 -->
-
<Goods
-
v-for=
"item in list"
-
:key=
"item.id"
-
:id=
"item.id"
-
:title=
"item.goods_name"
-
:pic=
"item.goods_img"
-
:price=
"item.goods_price"
-
:state=
"item.goods_state"
-
:count=
"item.goods_count"
-
@
state-change=
"getNewState"
-
>
-
</Goods>
-
<!-- Footer区域 -->
-
<Footer :isfull="fullState" :amount="amt" :all="total" @full-change="getFullState">
</Footer>
-
</div>
-
</template>
-
<script>
-
// 导入 axios 请求库
-
import axios
from
'axios'
-
// 导入需要的组件
-
import
Header
from
'@/shopping/Header/Header.vue'
-
import
Goods
from
'@/shopping/Goods/Goods.vue'
-
import
Footer
from
'@/shopping/Footer/Footer.vue'
-
// 导入eventBus文件
-
import bus
from
'@/shopping/eventBus.js'
-
export
default {
-
data(
){
-
return {
-
// 用来存储购物车的列表数据,默认为空数组
-
list:[]
-
}
-
},
-
methods:{
-
// 封装请求列表数据的方法
-
async
initCarList(
){
-
// 调用 axios 的 get 方法,请求列表数据
-
const {
data:res} =
await axios.
get(
"https://www.escook.cn/api/cart")
-
console.
log(res);
-
if(res.
status ===
200){
-
this.
list = res.
list
-
}
-
},
-
// 接收子组件传递过来的数据
-
getNewState(
val){
-
this.
list.
some(
item => {
-
if(item.
id === val.
id){
-
item.
goods_state = val.
value
-
// 终止后续循环
-
return
true
-
}
-
})
-
},
-
// 接收 Footer 子组件传递过来的全选按钮的状态
-
getFullState(
val){
-
this.
list.
forEach(
item => item.
goods_state = val)
-
}
-
},
-
computed:{
-
// 动态计算出全选的状态是 true 还是 false
-
fullState(
){
-
return
this.
list.
every(
item => item.
goods_state)
-
},
-
// 已勾选的商品总价格
-
amt(
){
-
// 1.先filter过滤
-
// 2.再reduce累加
-
return
this.
list.
filter(
item=>item.
goods_state).
reduce(
(total,item)=>{
-
return total+=item.
goods_price * item.
goods_count
-
},
0)
-
},
-
// 已勾选商品的总数量
-
total(
){
-
return
this.
list.
filter(
item => item.
goods_state).
reduce(
(t,item)=>{
-
return t+=item.
goods_count
-
},
0)
-
}
-
},
-
components:{
-
Header,
Goods,
Footer
-
},
-
created(
){
-
// 调用请求数据的方法
-
this.
initCarList()
-
bus.$on(
'share',
val=>{
-
this.
list.
some(
item=>{
-
if(item.
id === val.
id){
-
item.
goods_count = val.
value
-
return
true
-
}
-
})
-
})
-
}
-
}
-
</script>
-
<style lang="less" scoped>
-
.app-container{
-
padding-top:
45px;
-
padding-bottom:
50px
-
}
-
</style>
-
这个案例对初学vue者还是有很大的借鉴意义,通过此案例了解组件之间的数据共享的各种方式,不了解的可以先看下右边这篇文章 组件的数据共享 。通过项目案例将自己所学知识融会贯通这一点非常重要,多做项目对成长的帮助非常大,希望与诸位共
转载:https://blog.csdn.net/qq_53123067/article/details/128101078