VUE实现一个购物车

Vue
45
0
0
2024-07-03
标签   Vue实践

引子

json-server 为前端带来后端服务

官网

  1. 全局安装 json-server 工具
yarn global add json-server
  1. 新建一个 json 文件夹
cd db
{
  "cart": [
    {
      "id": 1,
      "name": "“小金龙”龙年款实战贾莫兰特男子篮球鞋",
      "price": 899,
      "count": 14,
      "thumb": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/a43f1f52-6850-4cab-837f-b93ff752f16d/ja-1-ep-%E5%B0%8F%E9%87%91%E9%BE%99%E9%BE%99%E5%B9%B4%E6%AC%BE%E5%AE%9E%E6%88%98%E8%B4%BE%E8%8E%AB%E5%85%B0%E7%89%B9%E7%94%B7%E5%AD%90%E7%AF%AE%E7%90%83%E9%9E%8B-ZLQQx9.png"
    },
    {
      "id": 4,
      "name": "LeBron XXI EP 男子篮球鞋",
      "price": 1099,
      "count": 1,
      "thumb": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/ba3a7f48-77d9-49aa-ad2b-24d0df830bac/lebron-21-ep-%E7%94%B7%E5%AD%90%E7%AF%AE%E7%90%83%E9%9E%8B-wK6QND.png"
    },
    {
      "id": 5,
      "name": "Jordan Nu Retro 1 Low 复刻男子运动鞋",
      "price": 599,
      "count": 4,
      "thumb": "https://static.nike.com.cn/a/images/t_PDP_1280_v1/f_auto,b_rgb:f5f5f5,u_126ab356-44d8-4a06-89b4-fcdcc8df0245,c_scale,fl_relative,w_1.0,h_1.0,fl_layer_apply/17313c9a-52e8-4ade-b899-2e25f4e8e516/jordan-nu-retro-1-low-%E5%A4%8D%E5%88%BB%E7%94%B7%E5%AD%90%E8%BF%90%E5%8A%A8%E9%9E%8B-SsFwr0.png"
    },
    {
      "id": 6,
      "name": "Nike SB Force 58 男/女滑板鞋",
      "price": 399,
      "count": 1,
      "thumb": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/30ceab71-d94b-4cef-a768-d41bef344002/sb-force-58-%E7%94%B7-%E5%A5%B3%E6%BB%91%E6%9D%BF%E9%9E%8B-kkk6cJ.png"
    }
  ]
}
  1. 进入文件目录,启动后端接口服务
json-server --watch index.json
Demo 功能分析
  • 动态渲染购物车,购物车List存放于Vuex进行管理
  • 商品项的数字空间控制商品的数量
  • 动态计算商品数量及总价
  • 移除某一个商品
  • 清空购物车
基于脚手架创建项目

使用 VUEX 的一个思路

想象每个组件都分别为家中的成员:爸爸、妈妈、孩子们。但是,作为一个家庭,他们需要共享状态。在这个家庭中,充当看家狗的Vuex就是来帮助我们解决问题的。

当妈妈在超市看到打折的纸巾【理解为前端页面】,她就像是"dispatch"一个"action",也就是发送一个消息说:“我今天会买一大包纸巾。”,把这个消息告诉看家狗(Vuex的store), 看家狗听到了,理解了,然后对这条消息进行核查,“mutation”。核查没问题后,看家狗就会更新家庭购物清单的状态,也就是把纸巾加入购物清单。

然后,爸爸和孩子们,也就是其他的组件,就可以从看家狗那里获取最新的购物清单,来获取纸巾的购买消息,以确保不会重复购买。

不论是小组件还是大组件,只要知道这个购物清单的修改,都可以避免重复购买,从而达到整个大家庭数据共享,而且状态始终跟新,始终一致。这一切,都得益于我们可爱、忠诚、聪明的看家狗——Vuex。

模块化管理 VUEX

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart.js'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    cart
  }
})

store/modules/carts.js

import axios from 'axios'
export default {
  namespaced: true,
  state () {
    return {
      // 购物车数据的存储结构 [{},{}]
      list: []
    }
  },
  mutations: {
    updateList (state, newList) {
      state.list = newList
    },
    updateCount (state, obj) {
      // 根据 Id 找到对应的对象,更新 count 属性即可
      const goods = state.list.find(item => item.id === obj.id)
      goods.count = obj.newCount
    },
    delClickItem (state, id) {
      state.list = state.list.filter(item => item.id !== id)
    },
    clear (state) {
      state.list = []
    }
  },
  actions: {
    // 请求方式 get
    // 请求地址 http://localhost:3000/cart
    async getList (context) {
      const res = await axios.get('http://localhost:3000/cart')
      context.commit('updateList', res.data)
    },
    // 请求方式 patch
    // 请求地址 http://localhost:3000/cart/:id
    // 请求参数:
    // {
    //   name: 新值 【可选】
    //   price: 新值 【可选】
    //   count: 新值 【可选】
    //   thumb: 新值 【可选】
    // }
    async updateCountAsync (context, obj) {
      // 将修改更新同步到后台服务器
      await axios.patch(`http://localhost:3000/cart/${obj.id}`, {
        count: obj.newCount
      })
      // 将修改更新同步到 vuex
      context.commit('updateCount', {
        id: obj.id,
        newCount: obj.newCount
      })
    },
    async delItem (context, id) {
      await axios.delete(`http://localhost:3000/cart/${id}`).then((res) => {
        if (res.status === 200) {
          context.commit('delClickItem', id)
        }
      })
    },
    async clearAllItem (context) {
      const deletePromises = context.state.list.map(item => axios.delete(`http://localhost:3000/cart/${item.id}`))
      await Promise.all(deletePromises)
      context.commit('clear')
    }
  },
  getters: {
    // 商品总数量 累加count
    total (state) {
      return state.list.reduce((sum, item) => sum + item.count, 0)
    },
    // 商品总价格 累加count * price
    totalPrice (state) {
      return state.list.reduce((sum, item) => sum + item.count * item.price, 0)
    }
  }
}
App.vue
  • cart-header
  • cart-item
  • cart-footer
<template>
  <div class="app-container">
    <!-- Header 区域 -->
    <cart-header></cart-header>

    <!-- 商品 Item 项组件 -->
    <cart-item v-for="item in list" :key="item.id" :item="item"></cart-item>

    <!-- Foote 区域 -->
    <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'
import { mapState } from 'vuex'

export default {
  name: 'App',
  created () {
    this.$store.dispatch('cart/getList')
  },
  computed: {
    ...mapState('cart', ['list'])
  },
  components: {
    CartHeader,
    CartFooter,
    CartItem
  }
}
</script>

<style lang="less" scoped>
.app-container {
  padding: 50px 0;
  font-size: 14px;
}
</style>

一加载页面发起请求,从服务器拿到购物车的商品信息进行购物车列表的渲染

  created () {
    this.$store.dispatch('cart/getList')
  },

由于我们使用了 VUEX 来进行管理,所以在

cart-item
<template>
  <div class="goods-container">
    <!-- 左侧图片区域 -->
    <div class="left">
      <img :src="item.thumb" alt="" class="avatar">
      <span @click="delItem(item.id)">x</span>
    </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',
  methods: {
    btnClick (step) {
      const newCount = this.item.count + step
      const id = this.item.id
      console.log(id, newCount)
      if (newCount < 1) return
      this.$store.dispatch('cart/updateCountAsync', {
        id,
        newCount
      })
    },
    delItem (id) {
      try {
        this.$store.dispatch('cart/delItem', id)
      } catch (err) {
        console.log(err)
      }
    }
  },
  props: {
    item: {
      type: Object,
      required: true
    }
  }
}
</script>

<style lang="less" scoped>
.goods-container {
  display: flex;
  padding: 10px;
  + .goods-container {
    border-top: 1px solid #f8f8f8;
  }
  .left {
    position: relative;
    &:hover {
      span {
        opacity: 1;
      }
    }
    .avatar {
      width: 100px;
      height: 100px;
    }
    margin-right: 10px;
    span {
      opacity: 0;
      transition: all .5s;
      position: absolute;
      top: 5px;
      right: 5px;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      text-align: center;
      line-height: 20px;
      background-color: rgba(0,0,0,.1);
      color: rgba(0,0,0,.6);
    }
  }
  .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>
cart-footer
<template>
  <div class="footer-container">
    <!-- 中间的合计 -->
    <div>
      <span>共 {{ total }} 件商品,合计:</span>
      <span class="price">{{ totalPrice }}</span>
    </div>
    <!-- 右侧结算按钮 -->
    <button class="btn btn-success btn-settle" @click="clearList">结算</button>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  name: 'CartFooter',
  computed: {
    ...mapGetters('cart', ['total', 'totalPrice'])
  },
  methods: {
    clearList () {
      this.$store.dispatch('cart/clearAllItem')
    }
  }
}
</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: #000;
  border: none;
  color: white;
}
</style>
cart-header
<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: #000;
  text-align: center;
  color: white;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 999;
}
</style>

效果预览

小结

这是一个比较简单的 Demo