前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >VUE实现一个购物车

VUE实现一个购物车

作者头像
HelloWorldZ
发布2024-03-20 19:01:13
1090
发布2024-03-20 19:01:13
举报
文章被收录于专栏:前端开发前端开发

引子

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

官网

  1. 全局安装 json-server 工具
代码语言:javascript
复制
yarn global add json-server
  1. 新建一个 json 文件夹
代码语言:javascript
复制
cd db
代码语言:javascript
复制
{
  "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. 进入文件目录,启动后端接口服务
代码语言:javascript
复制
json-server --watch index.json
Demo 功能分析
  • 动态渲染购物车,购物车List存放于Vuex进行管理
  • 商品项的数字空间控制商品的数量
  • 动态计算商品数量及总价
  • 移除某一个商品
  • 清空购物车
基于脚手架创建项目
使用 VUEX 的一个思路

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

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

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

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

模块化管理 VUEX

store/index.js

代码语言:javascript
复制
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

代码语言:javascript
复制
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
代码语言:javascript
复制
<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>

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

代码语言:javascript
复制
  created () {
    this.$store.dispatch('cart/getList')
  },

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

cart-item
代码语言:javascript
复制
<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
代码语言:javascript
复制
<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
代码语言:javascript
复制
<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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-01-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引子
    • json-server 为前端带来后端服务
      • Demo 功能分析
        • 基于脚手架创建项目
          • 使用 VUEX 的一个思路
            • 模块化管理 VUEX
              • App.vue
                • cart-item
                  • cart-footer
                    • cart-header
                    • 效果预览
                    • 小结
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档