目錄
- 需求
- 需求分析
- 組件分析
- 組件通信
- 開發(fā)
- 準備環(huán)境
- 準備模塊結(jié)構(gòu)
- 商品列表組件
- 展示商品列表
- 添加購物車
- 我的購物車組件
- 購物車列表
- 商品數(shù)量和統(tǒng)計功能
- 刪除購物車商品
- 購物車列表組件
- 購物車列表
- 全選操作
- 數(shù)字加減并統(tǒng)計小計
- 刪除功能
- 統(tǒng)計總數(shù)量和總錢數(shù)
- 處理金額小數(shù)的問題
- 本地存儲
- 完整案例
上一節(jié)介紹了Vuex的核心原理及簡單使用,這里來一個實際案例
image
需求
- 商品列表展示商品、價格和【加入購物車】按鈕
- 點擊【加入購物車】按鈕加入購物車,【我的購物車】提示數(shù)量增加
- 【我的購物車】按鈕
- 鼠標懸停出現(xiàn)
popover,展示購物車里面的商品,價格數(shù)量,【刪除】按鈕,還有總數(shù)量和總價格,還有【去購物車】按鈕 - 【刪除】按鈕可以刪除整個商品,總價和數(shù)量都會改變
- 點擊【去購物車】按鈕可以跳到購物車界面
- 鼠標懸停出現(xiàn)
- 展示多選框,商品,單價,數(shù)量及【加減按鈕車】,小計,【刪除】按鈕,總量和總價,【結(jié)算】按鈕
- 數(shù)量加減改變數(shù)量,小計,總數(shù)量和總價
- 【刪除】按鈕刪除整個商品
- 多選框不選中的不計入總數(shù)量和總價格。
- 刷新頁面,狀態(tài)還在,存在本地存儲中
需求分析
組件分析
- 路由組件
- 商品列表(①)
- 購物車列表(②)
- 我的購物車彈框組件(③)
組件通信
②和③都依賴購物車的數(shù)據(jù),①中點擊添加購物車,主要把數(shù)據(jù)傳遞給②和③,②和③之間的數(shù)據(jù)修改也互相依賴,如果沒有Vuex需要花時間精力在如何在組件中傳值上。
開發(fā)
準備環(huán)境
- 下載模板vuex-cart-demo-template,里面已經(jīng)將路由組件、樣式組件和數(shù)據(jù)都寫好了,我們只要負責(zé)實現(xiàn)功能即可。項目中還有一個
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())
// 模擬商品數(shù)據(jù)
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可以訪問到數(shù)據(jù),訪問http://localhost:8080/可以訪問到頁面
準備模塊結(jié)構(gòu)
- 在
store文件夾中創(chuàng)建modules文件夾,創(chuàng)建兩個模塊products.js和cart.js
image
- 在
products.js和cart.js文件中搭建基本結(jié)構(gòu)
const state = {}
const getters = {}
const mutations = {}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
index.js中導(dǎo)入并且引用模塊
import Vue from 'vue'
import Vuex from 'vuex'
// 1. 導(dǎo)入模塊
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中要實現(xiàn)下面的方法
- 在
state中定義一個屬性記錄所有的商品數(shù)據(jù)- 在
mutations中添加方法去修改商品數(shù)據(jù)- 在
actions中添加方法異步向接口請求數(shù)據(jù)
// 導(dǎo)入axios
import axios from 'axios'
const state = {
// 記錄所有商品
products: []
}
const getters = {}
const mutations = {
// 給products賦值
setProducts (state, payLoad) {
state.products = payLoad
}
}
const actions = {
// 異步獲取商品,第一個是context上下文,解構(gòu)出來要commit
async getProducts ({ commit }) {
// 請求接口
const { data } = await axios({
method: 'GET',
url: 'http://127.0.0.1:3000/products'
})
// 將獲取的數(shù)據(jù)將結(jié)果存儲到state中
commit('setProducts', data)
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
products.vue中將原來的data刪除,導(dǎo)入模塊并使用
<script>
// 導(dǎo)入需要的模塊
import { mapActions, mapState } from 'vuex'
export default {
name: 'ProductList',
// 創(chuàng)建計算屬性,映射products數(shù)據(jù),因為開啟了命名空間,這里添加了命名空間的寫法,后面是映射的屬性products
computed: {
...mapState('products', ['products'])
},
// 把actions里面的方法映射進來,第一個依舊是命名空間的寫法
methods: {
...mapActions('products', ['getProducts'])
},
// 組件創(chuàng)建之后調(diào)用getProducts獲取數(shù)據(jù)
created () {
this.getProducts()
}
}
</script>
- 打開瀏覽器,可以看到商品界面已經(jīng)出現(xiàn)了三個商品。
添加購物車
把當前點擊的商品存儲到一個位置,將來在購物車列表組件中可以訪問到,所以需要一個位置記錄所有的購物車數(shù)據(jù),這個數(shù)據(jù)在多個組件中可以共享,所以將這個數(shù)據(jù)放在cart模塊中
- 在模塊
cart.js中寫數(shù)據(jù)
const state = {
// 記錄購物車商品數(shù)據(jù)
cartProducts: []
}
const getters = {}
const mutations = {
// 第二個是payLoad,傳過來的商品對象
addToCart (state, product) {
// 1. 沒有商品時把該商品添加到數(shù)組中,并增加count,isChecked,totalPrice
// 2. 有該商品時把商品數(shù)量加1,選中,計算小計
// 判斷有沒有該商品,返回該商品
const prod = state.cartProducts.find(item => item.id === product.id)
if (prod) {
// 該商品數(shù)量+1
prod.count++
// 選中
prod.isChecked = true
// 小計 = 數(shù)量 * 單價
prod.totalPrice = prod.count * prod.price
} else {
// 給商品列表添加一個新商品
state.cartProducts.push({
// 原來products的內(nèi)容
...product,
// 數(shù)量
count: 1,
// 選中
isChecked: true,
// 小計為當前單價
totalPrice: product.price
})
}
}
}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
- 在
products.vue中導(dǎo)入cart的添加購物車mutation
<template>
<div>
...
<el-table
:data="products"
style="width: 100%">
...
<el-table-column
prop="address"
label="操作">
<!-- 這一行可以通過插槽獲取作用域數(shù)據(jù) -->
<!-- <template slot-scope="scope"> 這是2.6之前的寫法,2.6之后已經(jīng)過時了換成下里面的寫法了-->
<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']),
// 將添加購物商品的數(shù)據(jù)映射到methods中
...mapMutations('cart', ['addToCart'])
},
created () {
this.getProducts()
}
}
</script>
<style></style>
- 點開瀏覽器,可以點擊加入購物車按鈕,點開調(diào)試臺可以看到數(shù)據(jù)的變化
image
我的購物車組件
- 購買商品列表
- 統(tǒng)計購物車總數(shù)和總價
- 刪除按鈕
購物車列表
- 在
component/pop-cart.vue中導(dǎo)入購物車數(shù)據(jù)
<template>
<el-popover
width="350"
trigger="hover"
>
<!-- 這里是cartProducts的數(shù)據(jù),不需要修改 -->
<el-table :data="cartProducts" size="mini">
<el-table-column property="title" width="130" label="商品"></el-table-column>
...
</el-table>
...
</el-popover>
</template>
<script>
// 導(dǎo)入vuex模塊
import { mapState } from 'vuex'
export default {
name: 'PopCart',
computed: {
// 把cart模塊中的cartProducts導(dǎo)入
...mapState('cart', ['cartProducts'])
}
}
</script>
<style></style>
- 打開瀏覽器,點擊商品添加購物車,可以看到彈窗里有新加的商品
image
商品數(shù)量和統(tǒng)計功能
- 因為總數(shù)和總量可以用
store中的getters來寫,因為是對數(shù)據(jù)的簡單修改,在cart.js的getters中這么寫:
const getters = {
// 接收state為參數(shù),返回結(jié)果
totalCount (state) {
// 返回數(shù)組中某個元素的和,用reduce方法
// reduce方法接收兩個參數(shù),第一個參數(shù)是函數(shù),第二個參數(shù)是起始數(shù)(這里從0開始)
// 函數(shù)內(nèi)部接收兩個參數(shù),第一個參數(shù)是求和變量,第二個數(shù)組的元素
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>
<!-- 總數(shù)和總量也改成插值表達式 -->
<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導(dǎo)入
import { mapGetters, mapState } from 'vuex'
export default {
name: 'PopCart',
computed: {
...mapState('cart', ['cartProducts']),
// 把cart模塊中的totalCount和totalPrice導(dǎo)入
...mapGetters('cart', ['totalCount', 'totalPrice'])
}
}
</script>
<style>
</style>
- 打開瀏覽器,添加兩個商品,可以看到徽章和總計都發(fā)生了變化
image
刪除購物車商品
刪除商品要修改cart模塊中的state,所以要在cart模塊中添加一個mutation
- 在
card的mutation中添加
const mutations = {
addToCart (state, product) {
...
},
// 刪除購物車商品,第二個參數(shù)是商品id
deleteFromCart (state, prodId) {
// 使用數(shù)組的findIndex獲取索引
const index = state.cartProducts.findIndex(item => item.id === prodId)
// 判斷這個是不是等于-1,如果不是說明有這個商品,就執(zhí)行后面的刪除該元素
// 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>
// 導(dǎo)入mapMutations模塊
import { mapGetters, mapMutations, mapState } from 'vuex'
export default {
name: 'PopCart',
computed: {
...
},
methods: {
// 把cart模塊中的deleteFromCart映射到methods中
...mapMutations('cart', ['deleteFromCart'])
}
}
</script>
<style></style>
- 在瀏覽器中預(yù)覽,添加商品之后點擊刪除按鈕當前商品刪除成功
購物車列表組件
- 購物車列表
- 全選操作
- 數(shù)字加減并統(tǒng)計小計
- 刪除功能
- 統(tǒng)計選中商品價格數(shù)量
購物車列表
- 在views/cart.vue中引入vuex
<template>
<div>
...
<!-- 這里也要寫成cartProducts -->
<el-table
:data="cartProducts"
style="width: 100%"
>
...
</el-table>
...
</div>
</template>
<script>
// 導(dǎo)入vuex
import { mapState } from 'vuex'
export default {
name: 'Cart',
computed: {
// 將cartProducts映射到computed中
...mapState('cart', ['cartProducts'])
}
}
</script>
<style></style>
- 在瀏覽器中看,添加商品到我的購物車,購物車列表中有了對應(yīng)的數(shù)據(jù)
image
全選操作
- 點擊子
checkbox,選中變不選中,不選中變選中- 子
checkbox的狀態(tài)是其商品的isChecked的值決定 - 使用
mutation
- 子
- 點擊父
checkbox的時候,子checkbox與父保持一致,并且會重新進行計算值。全部點中子checkbox,父checkbox也會選中- 父
checkbox的狀態(tài),是購物車頁面單獨顯示的,不需要寫到store中, 直接寫到當前組件。 - 其依賴子
checkbox的isChecked狀態(tài),所以使用計算屬性 - 改變父
checkbox的狀態(tài),store的子狀態(tài)也需要改變,不需要定義方法,設(shè)置其set方法即可
- 父
- 先寫改變子
checkbox狀態(tài)的mutation
const mutations = {
addToCart (state, product) {
...
},
deleteFromCart (state, prodId) {
...
},
// 改變所有商品的isChecked屬性
// 需要兩個參數(shù),第二個是checkbox的狀態(tài)
updateAllProductChecked (state, checked) {
// 給每個商品的isChecked屬性為checkbox狀態(tài)
state.cartProducts.forEach(prod => {
prod.isChecked = checked
})
},
// 改變某個商品的isChecked屬性
// 需要兩個屬性,第二個是商品對象,這里是解構(gòu),一個是checked,一個是id
updateProductChecked (state, {
checked,
prodId
}) {
// 找到對應(yīng)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的狀態(tài),不能直接更改狀態(tài)
4.1 先綁定其isChecked屬性
4.2 注冊改變事件change,當checkbox改變的時候調(diào)用change,接收兩個參數(shù),id就通過scope.row獲取,checked狀態(tài)就通過$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的狀態(tài),因為有g(shù)et和set所以直接寫成對象形式
checkedAll: {
// 返回當前購物車的商品是否都是選中狀態(tài),如果有一個沒有選中直接返回false
get () {
return this.cartProducts.every(prod => prod.isChecked)
},
// 狀態(tài)改變的時候觸發(fā)的方法,需要一個參數(shù),checkbox的狀態(tài)
set (value) {
this.updateAllProductChecked(value)
}
}
},
methods: {
// 1. 將cart模塊的mutations映射到methods
...mapMutations('cart', ['updateAllProductChecked', 'updateProductChecked'])
}
}
</script>
<style></style>
- 打開瀏覽器,選中商品進入購物車,可以對全選框進行點擊
數(shù)字加減并統(tǒng)計小計
- 在
cart模塊中,定義一個mutation方法,更新商品
const mutations = {
...
// 更新商品,把商品id和count進行解構(gòu)
updateProduct (state, { prodId, count }) {
// 找到當前商品
const prod = state.cartProducts.find(prod => prod.id === prodId)
// 如果找到了就更新數(shù)量和總價
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>
- 在數(shù)字框中進行方法綁定
<el-table-column
prop="count"
label="數(shù)量">
<!-- 這里先定義一個插槽,綁定value是count,定義一個改變的change方法,將updateProduct傳入兩個參數(shù),一個是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>
- 在瀏覽器中查看,添加商品之后,修改數(shù)字,會有對應(yīng)的商品數(shù)量和小計
image
刪除功能
- 之前已經(jīng)在
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>
- 瀏覽器中,添加商品之后進入購物車頁面,點擊刪除按鈕可以刪除整個商品。
統(tǒng)計總數(shù)量和總錢數(shù)
統(tǒng)計的過程中需要添加條件,判斷當前商品是否是選中狀態(tài)。
- 在
cart.js的getters中添加商品數(shù)量和總價的方法,并且對選中狀態(tài)進行判斷
const getters = {
totalCount (state) {
...
},
totalPrice () {
...
},
// 選中的商品數(shù)量
checkedCount (state) {
// 返回前判斷是否是選中狀態(tài),如果是就進行添加,并且返回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中導(dǎo)入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">結(jié)算</el-button>
</div>
處理金額小數(shù)的問題
多添加商品的時候發(fā)現(xiàn)商品金額會出現(xiàn)很多位小數(shù)的問題,所以這里進行處理
-
mutations中會價格的乘積進行保留兩位小數(shù)的操作
const mutations = {
// 添加商品
addToCart (state, product) {
const prod = state.cartProducts.find(item => item.id === product.id)
if (prod) {
prod.count++
prod.isChecked = true
// 小計 = 數(shù)量 * 單價
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
// 保留兩位小數(shù)
prod.totalPrice = (count * prod.price).toFixed(2)
}
}
}
- 在
getters中將總價進行保留兩位小數(shù),記得轉(zhuǎn)化成數(shù)字
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)
}
}
本地存儲
刷新頁面,購物車的數(shù)據(jù)就會消失,因為我們把數(shù)據(jù)添加到了內(nèi)存中存儲,而實際購物的時候,有兩種存儲方式:
- 如果用戶登錄,購物車的數(shù)據(jù)是在服務(wù)器中
- 如果用戶沒有登錄,購物車的數(shù)據(jù)是存在本地存儲中
現(xiàn)在實現(xiàn)本地存儲的功能
- 首先在
cart.js中,首次進入界面的時候,從本地獲取數(shù)據(jù)
const state = {
// 從本地獲取購物車商品數(shù)據(jù),如果沒有初始化為空數(shù)組
cartProducts: JSON.parse(window.localStorage.getItem('cart-products')) || []
}
- 在
mutations中更改數(shù)據(jù),所以每次更改過的數(shù)據(jù),都需要記錄到本地存儲中,這里使用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]
})
- 刷新瀏覽器可以看到購物車的商品列表的數(shù)據(jù)還存在。