购物车案例
功能模块分析:
- 请求动态渲染购物车,数据存vuex
- 数字框控件修改数据
- 动态计算总价和总数量
目标1:构建cart购物车模块
说明:
- 既然明确数据要存vuex,建议分模块存
- 购物车数据存cart模块,将来还会由user模块,article模块
- 在store仓库新建module模块cart.js文件
export default {
namespace: true,//开启命名空间
state () {
return {
list: []
}
}
}
TIP:state的格式有两种:
都可以,但是分子模块提供数据的时候,建议用第二种函数形式
state: {
},
state () {
}
- 导入挂载到vuex仓库store的index.js上
import cart from './modules/cart'
export default new Vuex.Store({
modules: {
cart
}
})
目标2:通过json-server创建后端接口
在db文件夹下打开控制台输入
json-server --watch index.json
目标3:请求获取数据存入vuex,映射渲染
- 安装axios:
yarn add axios - 准备actions和mutations
- 调用action获取数据(数据已存入vuex)
- 动态渲染(mapState映射)
安装完axios后,在cart.js中准备actions和mutations
//在cart.js中
//记得导入axios
import axios from 'axios'
const state = {
list: []
}
const mutations = {
updateList (state, newList) {
state.list = newList
}
}
// 通过传递 { commit },你可以从该函数内部调用 commit 方法来更改状态。
const actions = {
async getList ({ commit }) {
try {
const res = await axios.get('/db/index.json')
const data = res.data
// 调用mutations方法去存
commit('updateList', data.cart)
} catch (error) {
console.error(error)
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
然后在app.vue中调用
// 写完actions的异步后,一进页面就调用
//调用cart里面的getList,可以得到数据结果
created () {
this.$store.dispatch('cart/getList')
},
接着在actions里调用mutations
context.commit('updateList', res.data)
!!!!!!!到这里,我们就把数据存好了
- 基于页面created调用actions,actions中调用mutations,最终把数据存到list中
数据存完const state = {
list: []
},自然下一步就是渲染了——>this.$store.state.cart.list——>父传子到组件cart-item,进行页面渲染
在app.vue渲染数据
//利用辅助函数获取数据
import { mapState } from 'vuex'
//找cart模块下的list数据
computed:{
...mapState('cart', ['list'])
},
//或者直接获取
computed: {
// 获取购物车数据
list () {
return this.$store.state.cart.list
}
}
动态渲染
<cart-item v-for="item in list" :key="item.id" :item="item"></cart-item>
在cart-item.vue,在这里面接收
props: {
item: {
type: Object,
required: true
}
}
然后再页面item.渲染就可以了
目标4:修改数量功能完成
注意:修改数据的话,前端的vuex数据和后端的json都要更新
在cart-item.vue中
<button class="btn btn-light" @click="btnclick(-1)">-</button>
在methods中:
methods: {
btnClick (step) {
const newCount = this.item.count + step
// 防止减到负数的一个小判断
if (newCount < 1) return
const id = this.item.id
console.log(id, newCount)
this.$store.dispatch('cart/updateCountAsync', {
id,
newCount
})
}
}
在actions中:
async updateCountAsync (context, obj) {
console.log(obj)
// `http://localhost:3000/cart ${obj.id}`
// 将修改更新同步到后台服务器
const res = await axios.patch(`http://localhost:3000/cart ${obj.id}`, {
count: obj.newCount
})
// 将修改更新同步到vuex
context.commit('updateCount', {
id: obj.id,
newCount: obj.newCount
})
console.log(res)
}
在mutations中:
updateCount (state, obj) {
// 根据id找到对应的对象,更新count属性即可
const goods = state.list.find(item => item.id === obj.id)
goods.count = obj.newCount
}
目标5:底部getters统计
- 提供getters
- 使用getters
const getters = {
total (state) {
return state.list.reduce((sum, item) => sum + item.count, 0)
},
totalPrice () {
return state.list.reduce((sum, item) => sum + item.count * item.price, 0)
}
}
import { mapGetters } from 'vuex'
export default {
name: 'CartFooter',
computed: {
...mapGetters('cart', ['total', 'totalPrice'])
}
}
<span>共 {{total}} 件商品,合计:</span>
<span class="price">¥{{ totalPrice }}</span>
完整代码:
/db/index.json
{
"cart": [
{
"id": 100001,
"name": "低帮城市休闲户外鞋天然牛皮COOLMAX纤维",
"price": 128,
"count": 9,
"thumb": "https://yanxuan-item.nosdn.127.net/3a56a913e687dc2279473e325ea770a9.jpg"
},
{
"id": 100002,
"name": "网易味央黑猪猪肘330g*1袋",
"price": 39,
"count": 5,
"thumb": "https://yanxuan-item.nosdn.127.net/d0a56474a8443cf6abd5afc539aa2476.jpg"
},
{
"id": 100003,
"name": "KENROLL男女简洁多彩一片式室外拖",
"price": 128,
"count": 2,
"thumb": "https://yanxuan-item.nosdn.127.net/eb1556fcc59e2fd98d9b0bc201dd4409.jpg"
},
{
"id": 100004,
"name": "云音乐定制IN系列intar民谣木吉他",
"price": 589,
"count": 1,
"thumb": "https://yanxuan-item.nosdn.127.net/4d825431a3587edb63cb165166f8fc76.jpg"
}
]
}
APP.vue
<template>
<div class="app-container">
<!-- Header 区域 -->
<cart-header></cart-header>
<!-- 商品 Item 项组件 -->
<!-- :item="item"要动态渲染,所以把它传进去 -->
<cart-item v-for="item in list" :key="item.id" :item="item"></cart-item>
<!-- Footer 区域 -->
<cart-footer></cart-footer>
</div>
</template>
<script>
import CartHeader from '@/components/cart-header.vue'
import CartFooter from '@/components/cart-footer.vue'
import CartItem from '@/components/cart-item.vue'
export default {
name: 'App',
components: {
CartHeader,
CartFooter,
CartItem
},
// 写完actions的异步后,一进页面就调用
created () {
this.$store.dispatch('cart/getList')
},
computed: {
// 获取购物车数据
list () {
return this.$store.state.cart.list
}
}
}
</script>
<style lang="less" scoped>
.app-container {
padding: 50px 0;
font-size: 14px;
}
</style>
main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
Vue.prototype.$store = store
new Vue({
store,
render: h => h(App)
}).$mount('#app')
store/modules/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
cart
}
})
export default store
store/modules/cart.js
import axios from 'axios'
const state = {
list: []
}
const mutations = {
updateList (state, newList) {
state.list = newList
},
// obj里至少包含两项,id和newcount,分别指的是,改的是谁和改成什么
updateCount (state, obj) {
// 根据id找到对应的对象,更新count属性即可
const goods = state.list.find(item => item.id === obj.id)
goods.count = obj.newCount
}
}
// 通过传递 { commit },你可以从该函数内部调用 commit 方法来更改状态。
const actions = {
async getList ({ commit }) {
try {
// http://localhost:3000/cart
// db/index.json
const res = await axios.get('http://localhost:3000/cart')
console.log(res)
const data = res.data
// 调用mutations方法去存
commit('updateList', data)
} catch (error) {
console.error(error)
}
},
async updateCountAsync (context, obj) {
console.log(obj)
// `http://localhost:3000/cart ${obj.id}`
// 将修改更新同步到后台服务器
const res = await axios.patch(`http://localhost:3000/cart/${obj.id}`, {
count: obj.newCount
})
// 将修改更新同步到vuex
context.commit('updateCount', {
id: obj.id,
newCount: obj.newCount
})
console.log(res)
}
}
const getters = {
total (state) {
return state.list.reduce((sum, item) => sum + item.count, 0)
},
totalPrice () {
return state.list.reduce((sum, item) => sum + item.count * item.price, 0)
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
components/cart-item.vue
<template>
<div class="goods-container">
<!-- 左侧图片区域 -->
<div class="left">
<img :src="item.thumb" class="avatar" alt="">
</div>
<!-- 右侧商品区域 -->
<div class="right">
<!-- 标题 -->
<div class="title">{{item.name}}</div>
<div class="info">
<!-- 单价 -->
<span class="price">¥{{item.price}}</span>
<div class="btns">
<!-- 按钮区域 -->
<button class="btn btn-light" @click="btnClick(-1)">-</button>
<span class="count">{{item.count}}</span>
<button class="btn btn-light" @click="btnClick(+1)">+</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CartItem',
props: {
item: Object
},
methods: {
btnClick (step) {
const newCount = this.item.count + step
// 防止减到负数的一个小判断
if (newCount < 1) return
const id = this.item.id
console.log(id, newCount)
this.$store.dispatch('cart/updateCountAsync', {
id,
newCount
})
}
}
}
</script>
<style lang="less" scoped>
.goods-container {
display: flex;
padding: 10px;
+ .goods-container {
border-top: 1px solid #f8f8f8;
}
.left {
.avatar {
width: 100px;
height: 100px;
}
margin-right: 10px;
}
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
.title {
font-weight: bold;
}
.info {
display: flex;
justify-content: space-between;
align-items: center;
.price {
color: red;
font-weight: bold;
}
.btns {
.count {
display: inline-block;
width: 30px;
text-align: center;
}
}
}
}
}
.custom-control-label::before,
.custom-control-label::after {
top: 3.6rem;
}
</style>
components/cart-footer.vue
<template>
<div class="footer-container">
<!-- 中间的合计 -->
<div>
<span>共 {{total}} 件商品,合计:</span>
<span class="price">¥{{ totalPrice }}</span>
</div>
<!-- 右侧结算按钮 -->
<button class="btn btn-success btn-settle">结算</button>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'CartFooter',
computed: {
...mapGetters('cart', ['total', 'totalPrice'])
}
}
</script>
<style lang="less" scoped>
.footer-container {
background-color: white;
height: 50px;
border-top: 1px solid #f8f8f8;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 10px;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 999;
}
.price {
color: red;
font-size: 13px;
font-weight: bold;
margin-right: 10px;
}
.btn-settle {
height: 30px;
min-width: 80px;
margin-right: 20px;
border-radius: 20px;
background: #42b983;
border: none;
color: white;
}
</style>
components/cart-header.vue
<template>
<div class="header-container">购物车案例</div>
</template>
<script>
export default {
name: 'CartHeader'
}
</script>
<style lang="less" scoped>
.header-container {
height: 50px;
line-height: 50px;
font-size: 16px;
background-color: #42b983;
text-align: center;
color: white;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
}
</style>