一、购物车案例
1.1、使用npm i
命令,安装依赖
1.2、编写页面布局
App.vue
<template>
<div class="app-container">
<Header></Header>
<Goods></Goods>
<Footer></Footer>
</div>
</template>
<script>
import Header from '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
export default {
components: {
Header,
Goods,
Footer
}
}
</script>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
padding-bottom: 50px;
}
</style>
1.3、获取购物车列表数据
使用npm i axios
命令,安装 axios 请求库。
App.vue
<template>
<div class="app-container">
<Header></Header>
<Goods></Goods>
<Footer></Footer>
</div>
</template>
<script>
// 导入 axios 请求库
import axios from 'axios'
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
export default {
data(){
return {
// 用来存储购物车的列表数据,默认为空数组
list: []
}
},
methods: {
// 封装请求列表数据的方法
async initCartList() {
// 调用 axios 的 get 方法,请求列表数据
const { data: res } = await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
if(res.status === 200){
this.list = res.list
}
}
},
created(){
// 调用请求数据的方法
this.initCartList()
},
components: {
Header,
Goods,
Footer
}
}
</script>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
padding-bottom: 50px;
}
</style>
1.4、父传子,渲染购物车列表
App.vue —> Goods.vue 父传子,采用自定义属性
的方式将数据由父组件传递给子组件。
App.vue
<template>
<div class="app-container">
<!-- Header 头部区域 -->
<Header></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"
></Goods>
<Footer></Footer>
</div>
</template>
<script>
// 导入 axios 请求库
import axios from 'axios'
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
export default {
data(){
return {
// 用来存储购物车的列表数据,默认为空数组
list: []
}
},
methods: {
// 封装请求列表数据的方法
async initCartList() {
// 调用 axios 的 get 方法,请求列表数据
const { data: res } = await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
if(res.status === 200){
this.list = res.list
}
}
},
created(){
// 调用请求数据的方法
this.initCartList()
},
components: {
Header,
Goods,
Footer
}
}
</script>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
padding-bottom: 50px;
}
</style>
Goods.vue
<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" />
<label class="custom-control-label" :for="'cb' + id">
<!-- 商品的缩略图 -->
<img :src="pic" alt="" />
</label>
</div>
</div>
<!-- 右侧信息区域 -->
<div class="goods-info">
<!-- 商品标题 -->
<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: {
required: true,
type: Number
},
// 要渲染的商品标题
title: {
default: '',
type: String
},
// 要渲染的商品图片
pic: {
default: '',
type: String
},
// 要渲染的商品单价
price: {
default: 0,
type: Number
},
// 要渲染的商品勾选状态
state: {
default: true,
type: Boolean
},
// 要渲染的商品购买数量
count: {
type: Number,
default: 1
}
}
}
</script>
<style lang="less" scoped>
.goods-container {
+ .goods-container {
border-top: 1px solid #efefef;
}
padding: 10px;
display: flex;
.thumb {
display: flex;
align-items: center;
img {
width: 100px;
height: 100px;
margin: 0 10px;
}
}
.goods-info {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
.goods-title {
font-weight: bold;
font-size: 12px;
}
.goods-info-bottom {
display: flex;
justify-content: space-between;
.goods-price {
font-weight: bold;
color: red;
font-size: 13px;
}
}
}
}
</style>
1.5、子传父、修改商品勾选状态
Goods.vue
<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 class="custom-control-label" :for="'cb' + id">
<!-- 商品的缩略图 -->
<img :src="pic" alt="" />
</label>
</div>
</div>
<!-- 右侧信息区域 -->
<div class="goods-info">
<!-- 商品标题 -->
<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 修改对应商品的勾选状态。
id: {
required: true,
type: Number
},
// 要渲染的商品的标题
title: {
default: '',
type: String
},
// 要渲染的商品的图片
pic: {
default: '',
type: String
},
// 商品的单价
price: {
default: 0,
type: Number
},
// 商品的勾选状态
state: {
default: true,
type: Boolean
},
// 商品的购买数量
count: {
type: Number,
default: 1
}
},
methods: {
// 只要复选框的选中状态发生了变化,就会调用这个处理函数
stateChange(e) {
const newState = e.target.checked
// 触发自定义事件
this.$emit('state-change', { id: this.id, value: newState })
}
}
}
</script>
App.vue
<template>
<div class="app-container">
<!-- Header 头部区域 -->
<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></Footer>
</div>
</template>
<script>
// 导入 axios 请求库
import axios from 'axios'
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
export default {
data() {
return {
// 用来存储购物车的列表数据,默认为空数组
list: []
}
},
created() {
// 调用请求数据的方法
this.initCartList()
},
methods: {
// 封装请求列表数据的方法
async initCartList() {
// 调用 axios 的 get 方法,请求列表数据
const { data: res } = await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
if (res.status === 200) {
this.list = res.list
}
},
// 接收子组件传递过来的数据
// e 的格式为 { id, value }
getNewState(e) {
this.list.some(item => {
if (item.id === e.id) {
item.goods_state = e.value
// 终止后续的循环
return true
}
})
}
},
components: {
Header,
Goods,
Footer
}
}
</script>
1.6、计算属性、子传父实现全选功能
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 class="custom-control-label" for="cbFull">全选</label>
</div>
<!-- 中间的合计 -->
<div>
<span>合计:</span>
<span class="total-price">¥{{ 0 }}</span>
</div>
<!-- 结算按钮 -->
<button type="button" class="btn btn-primary btn-settle">结算({{ 0 }})</button>
</div>
</template>
<script>
export default {
props: {
// 全选的状态
isfull: {
type: Boolean,
default: true
}
},
methods: {
// 监听到了全选的状态变化
fullChange(e) {
this.$emit('full-change', e.target.checked)
}
}
}
</script>
<style lang="less" scoped>
.footer-container {
font-size: 12px;
height: 50px;
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 {
display: flex;
align-items: center;
}
#cbFull {
margin-right: 5px;
}
.btn-settle {
height: 80%;
min-width: 110px;
border-radius: 25px;
font-size: 12px;
}
.total-price {
font-weight: bold;
font-size: 14px;
color: red;
}
</style>
App.vue
<template>
<div class="app-container">
<!-- Header 头部区域 -->
<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" @full-change="getFullState"></Footer>
</div>
</template>
<script>
// 导入 axios 请求库
import axios from 'axios'
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
export default {
data() {
return {
// 用来存储购物车的列表数据,默认为空数组
list: []
}
},
// 计算属性
computed: {
// 动态计算出全选的状态是 true 还是 false
fullState() {
return this.list.every(item => item.goods_state)
}
},
created() {
// 调用请求数据的方法
this.initCartList()
},
methods: {
// 封装请求列表数据的方法
async initCartList() {
// 调用 axios 的 get 方法,请求列表数据
const { data: res } = await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
if (res.status === 200) {
this.list = res.list
}
},
// 接收子组件传递过来的数据
// e 的格式为 { id, value }
getNewState(e) {
this.list.some(item => {
if (item.id === e.id) {
item.goods_state = e.value
// 终止后续的循环
return true
}
})
},
// 接收 Footer 子组件传递过来的全选按钮的状态
getFullState(val) {
this.list.forEach(item => (item.goods_state = val))
}
},
components: {
Header,
Goods,
Footer
}
}
</script>
1.7、计算属性、父传子实现商品总价和总数量
App.vue
<template>
<div class="app-container">
<!-- Header 头部区域 -->
<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 '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
export default {
data() {
return {
// 用来存储购物车的列表数据,默认为空数组
list: []
}
},
// 计算属性
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) => (total += item.goods_price * item.goods_count), 0)
},
// 已勾选商品的总数量
total() {
return this.list.filter(item => item.goods_state).reduce((t, item) => (t += item.goods_count), 0)
}
},
created() {
// 调用请求数据的方法
this.initCartList()
},
methods: {
// 封装请求列表数据的方法
async initCartList() {
// 调用 axios 的 get 方法,请求列表数据
const { data: res } = await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
if (res.status === 200) {
this.list = res.list
}
},
// 接收子组件传递过来的数据
// e 的格式为 { id, value }
getNewState(e) {
this.list.some(item => {
if (item.id === e.id) {
item.goods_state = e.value
// 终止后续的循环
return true
}
})
},
// 接收 Footer 子组件传递过来的全选按钮的状态
getFullState(val) {
this.list.forEach(item => (item.goods_state = val))
}
},
components: {
Header,
Goods,
Footer
}
}
</script>
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 class="custom-control-label" for="cbFull">全选</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>
1.8、使用 eventBus 实现数量动态增减及价格变化
Goods.vue
<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 class="custom-control-label" :for="'cb' + id">
<!-- 商品的缩略图 -->
<img :src="pic" alt="" />
</label>
</div>
</div>
<!-- 右侧信息区域 -->
<div class="goods-info">
<!-- 商品标题 -->
<h6 class="goods-title">{{ title }}</h6>
<div class="goods-info-bottom">
<!-- 商品价格 -->
<span class="goods-price">¥{{ price }}</span>
<!-- 商品的数量 -->
<Counter :num="count" :id="id"></Counter>
</div>
</div>
</div>
</template>
<script>
import Counter from '@/components/Counter/Counter.vue'
export default {
props: {
// 商品的 id
// 为啥在这里要封装一个 id 属性呢?
// 原因:将来,子组件中商品的勾选状态变化之后, 需要通过子 -> 父的形式,
// 通知父组件根据 id 修改对应商品的勾选状态。
id: {
required: true,
type: Number
},
// 要渲染的商品的标题
title: {
default: '',
type: String
},
// 要渲染的商品的图片
pic: {
default: '',
type: String
},
// 商品的单价
price: {
default: 0,
type: Number
},
// 商品的勾选状态
state: {
default: true,
type: Boolean
},
// 商品的购买数量
count: {
type: Number,
default: 1
}
},
methods: {
// 只要复选框的选中状态发生了变化,就会调用这个处理函数
stateChange(e) {
const newState = e.target.checked
// 触发自定义事件
this.$emit('state-change', { id: this.id, value: newState })
}
},
components: {
Counter
}
}
</script>
Counter.vue
<template>
<div class="number-container d-flex justify-content-center align-items-center">
<!-- 减 1 的按钮 -->
<button type="button" class="btn btn-light btn-sm" @click="sub">-</button>
<!-- 购买的数量 -->
<span class="number-box">{{ num }}</span>
<!-- 加 1 的按钮 -->
<button type="button" class="btn btn-light btn-sm" @click="add">+</button>
</div>
</template>
<script>
import bus from '@/components/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 lang="less" scoped>
.number-box {
min-width: 30px;
text-align: center;
margin: 0 5px;
font-size: 12px;
}
.btn-sm {
width: 30px;
}
</style>
App.vue
<template>
<div class="app-container">
<!-- Header 头部区域 -->
<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 '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'
import bus from '@/components/eventBus.js'
export default {
data() {
return {
// 用来存储购物车的列表数据,默认为空数组
list: []
}
},
// 计算属性
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) => (total += item.goods_price * item.goods_count), 0)
},
// 已勾选商品的总数量
total() {
return this.list.filter(item => item.goods_state).reduce((t, item) => (t += item.goods_count), 0)
}
},
created() {
// 调用请求数据的方法
this.initCartList()
bus.$on('share', val => {
this.list.some(item => {
if (item.id === val.id) {
item.goods_count = val.value
return true
}
})
})
},
methods: {
// 封装请求列表数据的方法
async initCartList() {
// 调用 axios 的 get 方法,请求列表数据
const { data: res } = await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
if (res.status === 200) {
this.list = res.list
}
},
// 接收子组件传递过来的数据
// e 的格式为 { id, value }
getNewState(e) {
this.list.some(item => {
if (item.id === e.id) {
item.goods_state = e.value
// 终止后续的循环
return true
}
})
},
// 接收 Footer 子组件传递过来的全选按钮的状态
getFullState(val) {
this.list.forEach(item => (item.goods_state = val))
}
},
components: {
Header,
Goods,
Footer
}
}
</script>
eventBus.js
import Vue from 'vue'
export default new Vue()