Vue购物车案例

327 阅读4分钟

购物车案例

image.png

功能模块分析:

  • 请求动态渲染购物车,数据存vuex
  • 数字框控件修改数据
  • 动态计算总价和总数量

目标1:构建cart购物车模块

说明:

  • 既然明确数据要存vuex,建议分模块存
  • 购物车数据存cart模块,将来还会由user模块,article模块
  1. 在store仓库新建module模块cart.js文件

image.png

  export default {
  namespace: true,//开启命名空间
  state () {
    return {
      list: []
    }
  }
}

TIP:state的格式有两种:

都可以,但是分子模块提供数据的时候,建议用第二种函数形式

  state: {
  },
  state () {
  }
  1. 导入挂载到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,映射渲染

  1. 安装axios: yarn add axios
  2. 准备actions和mutations
  3. 调用action获取数据(数据已存入vuex)
  4. 动态渲染(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统计

  1. 提供getters
  2. 使用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>