目录
- 需要
-
需要剖析
- 组件剖析
- 组件通信
-
开发
- 筹备环境
- 筹备模块构造
-
商品列表组件
- 展现商品列表
- 增加购物车
-
我的购物车组件
- 购物车列表
- 商品数量和统计性能
- 删除购物车商品
-
购物车列表组件
- 购物车列表
- 全选操作
- 数字加减并统计小计
- 删除性能
- 统计总数量和总钱数
- 解决金额小数的问题
- 本地存储
- 残缺案例
上一节介绍了 Vuex
的外围原理及简略应用,这里来一个理论案例
需要
-
商品列表展现商品、价格和【退出购物车】按钮
- 点击【退出购物车】按钮退出购物车,【我的购物车】提醒数量减少
-
【我的购物车】按钮
- 鼠标悬停呈现
popover
,展现购物车外面的商品,价格数量,【删除】按钮,还有总数量和总价格,还有【去购物车】按钮 - 【删除】按钮能够删除整个商品,总价和数量都会扭转
- 点击【去购物车】按钮能够跳到购物车界面
- 鼠标悬停呈现
-
展现多选框,商品,单价,数量及【加减按钮车】,小计,【删除】按钮,总量和总价,【结算】按钮
- 数量加减扭转数量,小计,总数量和总价
- 【删除】按钮删除整个商品
- 多选框不选中的不计入总数量和总价格。
- 刷新页面,状态还在,存在本地存储中
需要剖析
组件剖析
-
路由组件
- 商品列表(①)
- 购物车列表(②)
- 我的购物车弹框组件(③)
组件通信
②和③都依赖购物车的数据,①中点击增加购物车,次要把数据传递给②和③,②和③之间的数据批改也相互依赖,如果没有 Vuex
须要花工夫精力在如何在组件中传值上。
开发
筹备环境
- 下载模板 vuex-cart-demo-template,外面曾经将路由组件、款式组件和数据都写好了,咱们只有负责实现性能即可。我的项目中还有一个
server.js
的文件,这个是node
用来模仿接口的。
const _products = [{ id: 1, title: 'iPad Pro', price: 500.01},
{id: 2, title: 'H&M T-Shirt White', price: 10.99},
{id: 3, title: 'Charli XCX - Sucker CD', price: 19.99}
]
app.use(express.json())
// 模仿商品数据
app.get('/products', (req, res) => {res.status(200).json(_products)
})
// 模仿领取
app.post('/checkout', (req, res) => {res.status(200).json({success: Math.random() > 0.5
})
})
- 首先
npm install
装置依赖,之后node server
将接口跑起来,而后再增加终端输出npm run serve
让我的项目跑起来,这个时候拜访http://127.0.0.1:3000/products
能够拜访到数据,拜访http://localhost:8080/
能够拜访到页面
筹备模块构造
- 在
store
文件夹中创立modules
文件夹,创立两个模块products.js
和cart.js
- 在
products.js
和cart.js
文件中搭建根本构造
const state = {}
const getters = {}
const mutations = {}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
index.js
中导入并且援用模块
import Vue from 'vue'
import Vuex from 'vuex'
// 1. 导入模块
import products from './modules/products'
import cart from './modules/cart'
Vue.use(Vuex)
export default new Vuex.Store({state: {},
mutations: { },
actions: { },
// 2. 援用模块
modules: {
products,
cart
}
})
商品列表组件
- 展现商品列表
- 增加购物车
展现商品列表
- 在
products.js
中要实现上面的办法
- 在
state
中定义一个属性记录所有的商品数据- 在
mutations
中增加办法去批改商品数据- 在
actions
中增加办法异步向接口申请数据
// 导入 axios
import axios from 'axios'
const state = {
// 记录所有商品
products: []}
const getters = {}
const mutations = {
// 给 products 赋值
setProducts (state, payLoad) {state.products = payLoad}
}
const actions = {
// 异步获取商品,第一个是 context 上下文,解构进去要 commit
async getProducts ({commit}) {
// 申请接口
const {data} = await axios({
method: 'GET',
url: 'http://127.0.0.1:3000/products'
})
// 将获取的数据将后果存储到 state 中
commit('setProducts', data)
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
products.vue
中将原来的data
删除,导入模块并应用
<script>
// 导入须要的模块
import {mapActions, mapState} from 'vuex'
export default {
name: 'ProductList',
// 创立计算属性,映射 products 数据,因为开启了命名空间,这里增加了命名空间的写法,前面是映射的属性 products
computed: {...mapState('products', ['products'])
},
// 把 actions 外面的办法映射进来,第一个仍旧是命名空间的写法
methods: {...mapActions('products', ['getProducts'])
},
// 组件创立之后调用 getProducts 获取数据
created () {this.getProducts()
}
}
</script>
- 关上浏览器,能够看到商品界面曾经呈现了三个商品。
增加购物车
把以后点击的商品存储到一个地位,未来在购物车列表组件中能够拜访到,所以须要一个地位记录所有的购物车数据,这个数据在多个组件中能够共享,所以将这个数据放在 cart
模块中
- 在模块
cart.js
中写数据
const state = {
// 记录购物车商品数据
cartProducts: []}
const getters = {}
const mutations = {
// 第二个是 payLoad,传过来的商品对象
addToCart (state, product) {
// 1. 没有商品时把该商品增加到数组中,并减少 count,isChecked,totalPrice
// 2. 有该商品时把商品数量加 1,选中,计算小计
// 判断有没有该商品,返回该商品
const prod = state.cartProducts.find(item => item.id === product.id)
if (prod) {
// 该商品数量 +1
prod.count++
// 选中
prod.isChecked = true
// 小计 = 数量 * 单价
prod.totalPrice = prod.count * prod.price
} else {
// 给商品列表增加一个新商品
state.cartProducts.push({
// 原来 products 的内容
...product,
// 数量
count: 1,
// 选中
isChecked: true,
// 小计为以后单价
totalPrice: product.price
})
}
}
}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
products.vue
中导入cart
的增加购物车mutation
<template>
<div>
...
<el-table
:data="products"
style="width: 100%">
...
<el-table-column
prop="address"
label="操作">
<!-- 这一行能够通过插槽获取作用域数据 -->
<!-- <template slot-scope="scope"> 这是 2.6 之前的写法,2.6 之后曾经过期了换成下外面的写法了 -->
<template v-slot="scope">
<!-- 增加点击事件,传入以后列表 -->
<el-button @click="addToCart(scope.row)"> 退出购物车 </el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import {mapActions, mapMutations, mapState} from 'vuex'
export default {
name: 'ProductList',
computed: {...mapState('products', ['products'])
},
methods: {...mapActions('products', ['getProducts']),
// 将增加购物商品的数据映射到 methods 中
...mapMutations('cart', ['addToCart'])
},
created () {this.getProducts()
}
}
</script>
<style></style>
- 点开浏览器,能够点击退出购物车按钮,点开调试台能够看到数据的变动
我的购物车组件
- 购买商品列表
- 统计购物车总数和总价
- 删除按钮
购物车列表
- 在
component/pop-cart.vue
中导入购物车数据
<template>
<el-popover
width="350"
trigger="hover"
>
<!-- 这里是 cartProducts 的数据,不须要批改 -->
<el-table :data="cartProducts" size="mini">
<el-table-column property="title" width="130" label="商品"></el-table-column>
...
</el-table>
...
</el-popover>
</template>
<script>
// 导入 vuex 模块
import {mapState} from 'vuex'
export default {
name: 'PopCart',
computed: {
// 把 cart 模块中的 cartProducts 导入
...mapState('cart', ['cartProducts'])
}
}
</script>
<style></style>
- 关上浏览器,点击商品增加购物车,能够看到弹窗里有新加的商品
商品数量和统计性能
- 因为总数和总量能够用
store
中的getters
来写,因为是对数据的简略批改,在cart.js
的getters
中这么写:
const getters = {
// 接管 state 为参数,返回后果
totalCount (state) {
// 返回数组中某个元素的和,用 reduce 办法
// reduce 办法接管两个参数,第一个参数是函数,第二个参数是起始数(这里从 0 开始)
// 函数外部接管两个参数,第一个参数是求和变量,第二个数组的元素
return state.cartProducts.reduce((sum, prod) => sum + prod.count, 0)
},
// 与下面同样写法
totalPrice () {return state.cartProducts.reduce((sum, prod) => sum + prod.totalPrice, 0)
}
}
- 在
components/pop-cart.vue
中援用
<template>
<el-popover
width="350"
trigger="hover"
>
...
<div>
<!-- 总数和总量也改成插值表达式 -->
<p> 共 {{totalCount}} 件商品 共计¥{{totalPrice}}</p>
<el-button size="mini" type="danger" @click="$router.push({name:'cart'})"> 去购物车 </el-button>
</div>
<!-- 徽章这里,将 value 批改成 totalCount -->
<el-badge :value="totalCount" class="item" slot="reference">
<el-button type="primary"> 我的购物车 </el-button>
</el-badge>
</el-popover>
</template>
<script>
// 把 mapGetters 导入
import {mapGetters, mapState} from 'vuex'
export default {
name: 'PopCart',
computed: {...mapState('cart', ['cartProducts']),
// 把 cart 模块中的 totalCount 和 totalPrice 导入
...mapGetters('cart', ['totalCount', 'totalPrice'])
}
}
</script>
<style>
</style>
- 关上浏览器,增加两个商品,能够看到徽章和总计都产生了变动
删除购物车商品
删除商品要批改 cart
模块中的 state
,所以要在cart
模块中增加一个mutation
- 在
card
的mutation
中增加
const mutations = {addToCart (state, product) {...},
// 删除购物车商品,第二个参数是商品 id
deleteFromCart (state, prodId) {
// 应用数组的 findIndex 获取索引
const index = state.cartProducts.findIndex(item => item.id === prodId)
// 判断这个是不是等于 -1,如果不是阐明有这个商品,就执行前面的删除该元素
// splice 接管删除元素的索引,第二个元素是删除几个元素,这里写 1
index !== -1 && state.cartProducts.splice(index, 1)
}
}
- 在
components/pop-cart.vue
中援用
<template>
<el-popover
width="350"
trigger="hover"
>
<el-table :data="cartProducts" size="mini">
...
<el-table-column label="操作">
<!-- 获取以后元素的 id,增加 slot 插槽 -->
<template v-slot="scope">
<el-button size="mini" @click="deleteFromCart(scope.row.id)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
...
</el-popover>
</template>
<script>
// 导入 mapMutations 模块
import {mapGetters, mapMutations, mapState} from 'vuex'
export default {
name: 'PopCart',
computed: {...},
methods: {
// 把 cart 模块中的 deleteFromCart 映射到 methods 中
...mapMutations('cart', ['deleteFromCart'])
}
}
</script>
<style></style>
- 在浏览器中预览,增加商品之后点击删除按钮以后商品删除胜利
购物车列表组件
- 购物车列表
- 全选操作
- 数字加减并统计小计
- 删除性能
- 统计选中商品价格数量
购物车列表
- 在 views/cart.vue 中引入 vuex
<template>
<div>
...
<!-- 这里也要写成 cartProducts -->
<el-table
:data="cartProducts"
style="width: 100%"
>
...
</el-table>
...
</div>
</template>
<script>
// 导入 vuex
import {mapState} from 'vuex'
export default {
name: 'Cart',
computed: {
// 将 cartProducts 映射到 computed 中
...mapState('cart', ['cartProducts'])
}
}
</script>
<style></style>
- 在浏览器中看,增加商品到我的购物车,购物车列表中有了对应的数据
全选操作
-
点击子
checkbox
,选中变不选中,不选中变选中- 子
checkbox
的状态是其商品的isChecked
的值决定 - 应用
mutation
- 子
-
点击父
checkbox
的时候,子checkbox
与父保持一致,并且会从新进行计算值。全副点中子checkbox
,父checkbox
也会选中- 父
checkbox
的状态,是购物车页面独自显示的,不须要写到store
中,间接写到以后组件。 - 其依赖子
checkbox
的isChecked
状态,所以应用计算属性 - 扭转父
checkbox
的状态,store
的子状态也须要扭转,不须要定义方法,设置其set
办法即可
- 父
- 先写扭转子
checkbox
状态的mutation
const mutations = {addToCart (state, product) {...},
deleteFromCart (state, prodId) {...},
// 扭转所有商品的 isChecked 属性
// 须要两个参数,第二个是 checkbox 的状态
updateAllProductChecked (state, checked) {
// 给每个商品的 isChecked 属性为 checkbox 状态
state.cartProducts.forEach(prod => {prod.isChecked = checked})
},
// 扭转某个商品的 isChecked 属性
// 须要两个属性,第二个是商品对象,这里是解构,一个是 checked,一个是 id
updateProductChecked (state, {
checked,
prodId
}) {
// 找到对应 id 的商品对象
const prod = state.cartProducts.find(item => item.id === prodId)
// 如果商品对象存在就给其 isChecked 进行赋值
prod && (prod.isChecked = checked)
}
}
- 在
views/cart.vue
中进行引入批改
- 引入
mutation
- 找到父
checkbox
绑定计算属性 - 定义
checkbox
计算属性,实现get
和set
- 子
checkbox
中应用
<template>
<div>
...
<el-table
:data="cartProducts"
style="width: 100%"
>
<el-table-column
width="55">
<template v-slot:header>
<!-- 2. 这里绑定一个 v -model,计算属性 -->
<el-checkbox size="mini" v-model="checkedAll">
</el-checkbox>
</template>
<!-- 4. 这里不能间接绑定 v -model,因为咱们绑定的是 vuex 的状态,不能间接更改状态
4.1 先绑定其 isChecked 属性
4.2 注册扭转事件 change,当 checkbox 扭转的时候调用 change,接管两个参数,id 就通过 scope.row 获取,checked 状态就通过 $event 获取 -->
<template v-slot="scope">
<el-checkbox
size="mini"
:value="scope.row.isChecked"
@change="updateProductChecked({
prodId: scope.row.id,
checked: $event
})"
>
</el-checkbox>
</template>
</el-table-column>
...
</el-table>
...
</div>
</template>
<script>
import {mapMutations, mapState} from 'vuex'
export default {
name: 'Cart',
computed: {...mapState('cart', ['cartProducts']),
// 3. 父 checkbox 的状态,因为有 get 和 set 所以间接写成对象模式
checkedAll: {
// 返回以后购物车的商品是否都是选中状态,如果有一个没有选中间接返回 false
get () {return this.cartProducts.every(prod => prod.isChecked)
},
// 状态扭转的时候触发的办法,须要一个参数,checkbox 的状态
set (value) {this.updateAllProductChecked(value)
}
}
},
methods: {
// 1. 将 cart 模块的 mutations 映射到 methods
...mapMutations('cart', ['updateAllProductChecked', 'updateProductChecked'])
}
}
</script>
<style></style>
- 关上浏览器,选中商品进入购物车,能够对全选框进行点击
数字加减并统计小计
- 在
cart
模块中,定义一个mutation
办法,更新商品
const mutations = {
...
// 更新商品,把商品 id 和 count 进行解构
updateProduct (state, { prodId, count}) {
// 找到以后商品
const prod = state.cartProducts.find(prod => prod.id === prodId)
// 如果找到了就更新数量和总价
if (prod) {
prod.count = count
prod.totalPrice = count * prod.price
}
}
}
- 去
cart.vue
中增加一个mapMutations
<script>
...
export default {
...
methods: {
// 将 cart 模块的 mutations 映射到 methods
...mapMutations('cart', [
'updateAllProductChecked',
'updateProductChecked',
'updateProduct'
])
}
}
</script>
- 在数字框中进行办法绑定
<el-table-column
prop="count"
label="数量">
<!-- 这里先定义一个插槽,绑定 value 是 count,定义一个扭转的 change 办法,将 updateProduct 传入两个参数,一个是 id,一个是以后 input 的值 $event -->
<template v-slot="scope">
<el-input-number :value="scope.row.count" @change="updateProduct({
prodId: scope.row.id,
count: $event
})"size="mini"></el-input-number>
</template>
</el-table-column>
- 在浏览器中查看,增加商品之后,批改数字,会有对应的商品数量和小计
删除性能
- 之前曾经在
cart.js
的模块中有了删除商品的mutation
,这里间接应用,在cart.vue
中增加
<script>
...
export default {
...
methods: {
// 将 cart 模块的 mutations 映射到 methods
...mapMutations('cart', [
'updateAllProductChecked',
'updateProductChecked',
'updateProduct',
'deleteFromCart'
])
}
}
</script>
- 在下面的删除按钮中定义方法
<el-table-column
label="操作">
<!-- 定义一个插槽,删除按钮绑定事件,传入商品 id -->
<template v-slot="scope">
<el-button size="mini"
@click="deleteFromCart(scope.row.id)"> 删除 </el-button>
</template>
</el-table-column>
- 浏览器中,增加商品之后进入购物车页面,点击删除按钮能够删除整个商品。
统计总数量和总钱数
统计的过程中须要增加条件,判断以后商品是否是选中状态。
- 在
cart.js
的getters
中增加商品数量和总价的办法,并且对选中状态进行判断
const getters = {totalCount (state) {...},
totalPrice () {...},
// 选中的商品数量
checkedCount (state) {
// 返回前判断是否是选中状态,如果是就进行增加,并且返回 sum
return state.cartProducts.reduce((sum, prod) => {if (prod.isChecked) {sum += prod.count}
return sum
}, 0)
},
// 选中的商品价格,同理下面
checkedPrice () {return state.cartProducts.reduce((sum, prod) => {if (prod.isChecked) {sum += prod.totalPrice}
return sum
}, 0)
}
}
- 在
cart.vue
中导入mapGetters
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
export default {
name: 'Cart',
computed: {...mapState('cart', ['cartProducts']),
// 将 cart 模块中的 getters 映射到 computed 中
...mapGetters('cart', ['checkedCount', 'checkedPrice']),
...
},
...
}
</script>
- 在总价格处援用
<div>
<p> 已选 <span>{{checkedCount}}</span> 件商品,总价:<span>{{checkedPrice}}</span></p>
<el-button type="danger"> 结算 </el-button>
</div>
解决金额小数的问题
多增加商品的时候发现商品金额会呈现很多位小数的问题,所以这里进行解决
mutations
中会价格的乘积进行保留两位小数的操作
const mutations = {
// 增加商品
addToCart (state, product) {const prod = state.cartProducts.find(item => item.id === product.id)
if (prod) {
prod.count++
prod.isChecked = true
// 小计 = 数量 * 单价
prod.totalPrice = (prod.count * prod.price).toFixed(2)
console.log(prod.totalPrice)
} else {...}
},
// 更新商品
updateProduct (state, { prodId, count}) {const prod = state.cartProducts.find(prod => prod.id === prodId)
if (prod) {
prod.count = count
// 保留两位小数
prod.totalPrice = (count * prod.price).toFixed(2)
}
}
}
- 在
getters
中将总价进行保留两位小数,记得转化成数字
const getters = {
// 价格总计
totalPrice () {return state.cartProducts.reduce((sum, prod) => sum + Number(prod.totalPrice), 0).toFixed(2)
},
// 选中的商品价格
checkedPrice () {return state.cartProducts.reduce((sum, prod) => {if (prod.isChecked) {sum += Number(prod.totalPrice)
}
return sum
}, 0).toFixed(2)
}
}
本地存储
刷新页面,购物车的数据就会隐没,因为咱们把数据增加到了内存中存储,而理论购物的时候,有两种存储形式:
- 如果用户登录,购物车的数据是在服务器中
- 如果用户没有登录,购物车的数据是存在本地存储中
当初实现本地存储的性能
- 首先在
cart.js
中,首次进入界面的时候,从本地获取数据
const state = {
// 从本地获取购物车商品数据,如果没有初始化为空数组
cartProducts: JSON.parse(window.localStorage.getItem('cart-products')) || []}
- 在
mutations
中更改数据,所以每次更改过的数据,都须要记录到本地存储中,这里应用vuex
的插件,在index.js
中
...
Vue.use(Vuex)
const myPlugin = store => {store.subscribe((mutation, state) => {// mutation 的格局为 { type, payload}
// type 外面的格局是 cart/cartProducts
// state 的格局为 {cart, products}
if (mutation.type.startsWith('cart/')) {
// 本地存储 cartProducts
window.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))
}
})
}
export default new Vuex.Store({
...
// 将 myPlugin 挂载到 Store 上
plugins: [myPlugin]
})
- 刷新浏览器能够看到购物车的商品列表的数据还存在。
残缺案例
vuex-cart-temp