基於Vuex實現小米商城購物車

前言

上學期利用課餘時間學習了Vue.js、Node.js,一直想作個完整的項目 實踐 一下,但以前在學校並無那麼多的時間。如今剛好有時間,就想着作一個項目鞏固以前學到的東西。javascript

思來想去,最後決定模仿 小米商城 作一個電商項目,目前已經差很少作完了,本文就購物車模塊的實現進行總結。css

說明

完整項目代碼倉庫: https://github.com/hai-27/vue-store

項目部署在阿里雲服務器,預覽連接: http://106.15.179.105 (沒有兼容移動端,請使用PC訪問)。html

本文僅對前端部分進行總結,後端採用 Node.js(Koa)+Mysql 實現,詳細代碼請移步 https://github.com/hai-27/store-server前端

新人發帖,如有不對的地方,請多多指教 ^_^vue

效果

話很少說,先看效果
java

實現步驟

1. 靜態頁面準備

shoppingCart.png

頁面使用了 element-uiIcon 圖標 el-checkbox el-input-numberel-popoverel-button,全部在main.js須要引入element-ui。ios

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

頁面代碼以下:git

說明: 爲了方便,此處直接放最終的代碼。github

<template>
  <div class="shoppingCart">
    <!-- 購物車頭部 -->
    <div class="cart-header">
      <div class="cart-header-content">
        <p>
          <i class="el-icon-shopping-cart-full" style="color:#ff6700; font-weight: 600;">
          </i>
          個人購物車
        </p>
        <span>舒適提示:產品是否購買成功,以最終下單爲準哦,請儘快結算</span>
      </div>
    </div>
    <!-- 購物車頭部END -->

    <!-- 購物車主要內容區 -->
    <div class="content" v-if="getShoppingCart.length>0">
      <ul>
        <!-- 購物車表頭 -->
        <li class="header">
          <div class="pro-check">
            <el-checkbox v-model="isAllCheck">全選</el-checkbox>
          </div>
          <div class="pro-img"></div>
          <div class="pro-name">商品名稱</div>
          <div class="pro-price">單價</div>
          <div class="pro-num">數量</div>
          <div class="pro-total">小計</div>
          <div class="pro-action">操做</div>
        </li>
        <!-- 購物車表頭END -->

        <!-- 購物車列表 -->
        <li class="product-list" v-for="(item,index) in getShoppingCart" :key="item.id">
          <div class="pro-check">
            <el-checkbox :value="item.check" @change="checkChange($event,index)">
            </el-checkbox>
          </div>
          <div class="pro-img">
            <router-link :to="{ 
            path: '/goods/details', 
            query: {productID:item.productID} 
            }">
              <img :src="$target + item.productImg" />
            </router-link>
          </div>
          <div class="pro-name">
            <router-link
              :to="{ path: '/goods/details', query: {productID:item.productID} }"
            >{{item.productName}}</router-link>
          </div>
          <div class="pro-price">{{item.price}}元</div>
          <div class="pro-num">
            <el-input-number
              size="small"
              :value="item.num"
              @change="handleChange($event,index,item.productID)"
              :min="1"
              :max="item.maxNum"
            ></el-input-number>
          </div>
          <div class="pro-total pro-total-in">{{item.price*item.num}}元</div>
          <div class="pro-action">
            <el-popover placement="right">
              <p>肯定刪除嗎?</p>
              <div style="text-align: right; margin: 10px 0 0">
                <el-button
                  type="primary"
                  size="mini"
                  @click="deleteItem($event,item.id,item.productID)"
                >肯定</el-button>
              </div>
              <i class="el-icon-error" slot="reference" style="font-size: 18px;"></i>
            </el-popover>
          </div>
        </li>
        <!-- 購物車列表END -->
      </ul>
      <div style="height:20px;background-color: #f5f5f5"></div>
      <!-- 購物車底部導航條 -->
      <div class="cart-bar">
        <div class="cart-bar-left">
          <span>
            <router-link to="/goods">繼續購物</router-link>
          </span>
          <span class="sep">|</span>
          <span class="cart-total">
            共
            <span class="cart-total-num">{{getNum}}</span> 件商品,已選擇
            <span class="cart-total-num">{{getCheckNum}}</span> 件
          </span>
        </div>
        <div class="cart-bar-right">
          <span>
            <span class="total-price-title">合計:</span>
            <span class="total-price">{{getTotalPrice}}元</span>
          </span>
          <router-link :to="getCheckNum > 0 ? '/confirmOrder' : ''">
            <div :class="getCheckNum > 0 ? 'btn-primary' : 'btn-primary-disabled'">
            去結算
            </div>
          </router-link>
        </div>
      </div>
      <!-- 購物車底部導航條END -->
    </div>
    <!-- 購物車主要內容區END -->

    <!-- 購物車爲空的時候顯示的內容 -->
    <div v-else class="cart-empty">
      <div class="empty">
        <h2>您的購物車仍是空的!</h2>
        <p>快去購物吧!</p>
      </div>
    </div>
    <!-- 購物車爲空的時候顯示的內容END -->
  </div>
</template>

2. 建立Vuex

/store/index.jssql

import Vue from 'vue'
import Vuex from 'vuex'

import shoppingCart from './modules/shoppingCart'

Vue.use(Vuex)

export default new Vuex.Store({
  strict: true,
  modules: {
    shoppingCart
  }
})

/store/modules/shoppingCart.js

export default {
  state: {
    shoppingCart: []
    // shoppingCart結構
    /* 
    shoppingCart = {
      id: "", // 購物車id
      productID: "", // 商品id
      productName: "", // 商品名稱
      productImg: "", // 商品圖片
      price: "", // 商品價格
      num: "", // 商品數量
      maxNum: "", // 商品限購數量
      check: false // 是否勾選
    }
    */
  }
}

3. 同步購物車狀態

思路:

  • 在根組件App.vue監聽用戶登陸狀態;
  • 若是用戶已經登陸,從數據庫獲取用戶的購物車數據,把獲取到數據更新到vuex;
  • 用戶沒有登陸,把vuex中購物車的狀態設置爲空。

代碼以下:

import { mapActions } from "vuex";
import { mapGetters } from "vuex";

computed: {
  ...mapGetters(["getUser", "getNum"])
},
methods: {
  ...mapActions(["setShoppingCart"]),
}
watch: {
  // 獲取vuex的登陸狀態
  getUser: function(val) {
    if (val === "") {
      // 用戶沒有登陸
      this.setShoppingCart([]);
    } else {
      // 用戶已經登陸,獲取該用戶的購物車信息
      this.$axios
        .post("/api/user/shoppingCart/getShoppingCart", {
          user_id: val.user_id
        })
        .then(res => {
          if (res.data.code === "001") {
            // 001 爲成功, 更新vuex購物車狀態
            this.setShoppingCart(res.data.shoppingCartData);
          } else {
            // 提示失敗信息
            this.notifyError(res.data.msg);
          }
        })
        .catch(err => {
          return Promise.reject(err);
        });
    }
  }
}

vuex的mutations:

setShoppingCart (state, data) {
    // 設置購物車狀態
    state.shoppingCart = data;
},

vuex的actions

setShoppingCart({ commit }, data) {
  commit('setShoppingCart', data);
}

4. 動態生成購物車頁面

思路:

  • 經過vuex中的getters.getShoppingCart獲取購物車的狀態;
  • 使用v-if判斷購物車是否存在商品;
  • 若是存在,使用v-for生成購物車列表;
  • 若是不存在,顯示購物車爲空的時候顯示的內容;

購物車html僞代碼:

<div class="shoppingCart">
  <div class="content" v-if="getShoppingCart.length>0">
    <ul>
      <li class="header">
        <!-- 購物車表頭部分,省略詳細代碼 -->
      </li>
      <li class="product-list" v-for="(item,index) in getShoppingCart" :key="item.id">
        <!-- 購物車列表部分,省略詳細代碼 -->
      </li>
    </ul>
  </div>
  <!-- 購物車爲空的時候顯示的內容 -->
  <div v-else class="cart-empty">
    <div class="empty">
      <h2>您的購物車仍是空的!</h2>
      <p>快去購物吧!</p>
    </div>
    
  </div>
</div>

vuex的getters:

getShoppingCart(state) {
  // 獲取購物車狀態
  return state.shoppingCart;
}

5.添加商品到購物車

思路:

  • 用戶在商品的詳情頁,經過點擊加入購物車按鈕,調用點擊事件addShoppingCart;
  • 先向後端發起添加購物車的請求,根據返回信息操做vuex;
  • 該商品第一次加入購物車,經過vuex的 Actions (unshiftShoppingCart)把後端返回的購物車信息插入vuex;
  • 該商品已經在購物車,經過vuex的 Actions (addShoppingCartNum)把該商品數量+1;
  • 商品數量達到限購數量,禁止點擊加入購物車按鈕。

html:

<el-button class="shop-cart" :disabled="dis" @click="addShoppingCart">
  加入購物車
</el-button>

邏輯代碼以下:

methods: {
  ...mapActions(["unshiftShoppingCart", "addShoppingCartNum"]),
  // 加入購物車
  addShoppingCart() {
    // 判斷是否登陸,沒有登陸則顯示登陸組件
    if (!this.$store.getters.getUser) {
      this.$store.dispatch("setShowLogin", true);
      return;
    }
    // 向後端發起請求,把商品信息插入數據庫的購物車表
    this.$axios
      .post("/api/user/shoppingCart/addShoppingCart", {
        user_id: this.$store.getters.getUser.user_id,
        product_id: this.productID
      })
      .then(res => {
        switch (res.data.code) {
          case "001":
            // 新加入購物車成功
            this.unshiftShoppingCart(res.data.shoppingCartData[0]);
            this.notifySucceed(res.data.msg);
            break;
          case "002":
            // 該商品已經在購物車,數量+1
            this.addShoppingCartNum(this.productID);
            this.notifySucceed(res.data.msg);
            break;
          case "003":
            // 商品數量達到限購數量
            this.dis = true;
            this.notifyError(res.data.msg);
            break;
          case "401":
            // 沒有登陸
            this.$store.dispatch("setShowLogin", true);
            this.notifyError(res.data.msg);
            break;
          default:
            this.notifyError(res.data.msg);
        }
      })
      .catch(err => {
        return Promise.reject(err);
      });
  }
}

vuex的mutations:

unshiftShoppingCart(state, data) {
  // 添加商品到購物車
  // 用於在商品詳情頁點擊添加購物車,後臺添加成功後,更新vuex狀態
  state.shoppingCart.unshift(data);
},
addShoppingCartNum(state, productID) {
  // 增長購物車商品數量
  // 用於在商品詳情頁點擊添加購物車,後臺返回002,「該商品已在購物車,數量 +1」,更新vuex的商品數量
  for (let i = 0; i < state.shoppingCart.length; i++) {
    const temp = state.shoppingCart[i];
    if (temp.productID == productID) {
      if (temp.num < temp.maxNum) {
        temp.num++;
      }
    }
  }
}

vuex的actions:

unshiftShoppingCart({ commit }, data) {
  commit('unshiftShoppingCart', data);
},
addShoppingCartNum({ commit }, productID) {
  commit('addShoppingCartNum', productID);
}

6. 刪除購物車中的商品

思路:

  • 購物車每一個商品都有一個刪除按鈕,用戶點擊刪除按鈕,先彈出確認對話框;
  • 當用戶選擇確認刪除,調用點擊事件deleteItem($event,item.id,item.productID);
  • 經過點擊事件獲取到購物車id和商品id;
  • 先向後端發起刪除購物車的請求,根據返回信息操做vuex;
  • 刪除成功,經過vuex的 Actions (deleteShoppingCart),把該商品從購物車刪除;
  • 若是刪除失敗,提示相關信息。

html:

<div class="pro-action">
  <el-popover placement="right">
    <p>肯定刪除嗎?</p>
    <div style="text-align: right; margin: 10px 0 0">
      <el-button type="primary" size="mini" 
      @click="deleteItem($event,item.id,item.productID)">肯定</el-button>
    </div>
    <i class="el-icon-error" slot="reference" style="font-size: 18px;"></i>
  </el-popover>
</div>

邏輯代碼以下:

methods: {
  // 向後端發起刪除購物車的數據庫信息請求
  deleteItem(e, id, productID) {
    this.$axios
      .post("/api/user/shoppingCart/deleteShoppingCart", {
        user_id: this.$store.getters.getUser.user_id,
        product_id: productID
      })
      .then(res => {
        switch (res.data.code) {
          case "001":
            // 刪除成功
            // 更新vuex狀態
            this.deleteShoppingCart(id);
            // 提示刪除成功信息
            this.notifySucceed(res.data.msg);
            break;
          default:
            // 提示刪除失敗信息
            this.notifyError(res.data.msg);
        }
      })
      .catch(err => {
        return Promise.reject(err);
      });
  }
}

vuex的mutations:

deleteShoppingCart(state, id) {
  // 根據購物車id刪除購物車商品
  for (let i = 0; i < state.shoppingCart.length; i++) {
    const temp = state.shoppingCart[i];
    if (temp.id == id) {
      state.shoppingCart.splice(i, 1);
    }
  }
}

vuex的actions:

deleteShoppingCart({ commit }, id) {
  commit('deleteShoppingCart', id);
}

7. 修改購物車商品的數量

思路:

  • 購物車每一個商品都有一個計數器,能夠點擊加減按鈕修改商品數量,或者直接在input輸入框輸入商品數量進行修改。計數器使用了 element-uiel-input-number實現。
  • 經過計數器的 change 事件獲取到新的數量、購物車商品的索引(即數組的索引)、商品id;
  • 先向後端發起修改購物車商品數量的請求,根據返回信息操做vuex;
  • 修改爲功,經過vuex的 Actions (updateShoppingCart),修改購物車商品的數量;
  • 修改失敗,提示失敗信息。其中:數量小於一、數量是否達到限購數量(這兩種狀況,前端有設置校驗,通常不會出現)。

html:

<div class="pro-num">
  <el-input-number 
  size="small" 
  :value="item.num" 
  @change="handleChange($event,index,item.productID)" 
  :min="1"
  :max="item.maxNum"
  >
</el-input-number>

邏輯代碼以下:

// 修改商品數量的時候調用該函數
handleChange(currentValue, key, productID) {
  // 當修改數量時,默認勾選
  this.updateShoppingCart({ key: key, prop: "check", val: true });
  // 向後端發起修改購物車商品數量的請求
  this.$axios
    .post("/api/user/shoppingCart/updateShoppingCart", {
      user_id: this.$store.getters.getUser.user_id,
      product_id: productID,
      num: currentValue
    })
    .then(res => {
      switch (res.data.code) {
        case "001":
          // 001表明修改爲功
          // 更新vuex狀態
          this.updateShoppingCart({
            key: key,
            prop: "num",
            val: currentValue
          });
          // 提示修改爲功信息
          this.notifySucceed(res.data.msg);
          break;
        default:
          // 提示修改失敗信息
          this.notifyError(res.data.msg);
      }
    })
    .catch(err => {
      return Promise.reject(err);
    });
}

vuex的mutations:

updateShoppingCart(state, payload) {
  // 更新購物車
  // 可更新商品數量和是否勾選
  // 用於購物車點擊勾選及加減商品數量
  if (payload.prop == "num") {
    // 判斷效果的商品數量是否大於限購數量或小於1
    if (state.shoppingCart[payload.key].maxNum < payload.val) {
      return;
    }
    if (payload.val < 1) {
      return;
    }
  }
  // 根據商品在購物車的數組的索引和屬性更改
  state.shoppingCart[payload.key][payload.prop] = payload.val;
}

vuex的actions:

updateShoppingCart({ commit }, payload) {
  commit('updateShoppingCart', payload);
}

8. 是否勾選商品

思路:

  • 購物車每一個商品都有一個勾選框,使用element-uiel-checkbox實現,結算時提交所有勾選的商品。
  • 經過 change 事件獲取到勾選框的狀態(true或false)、購物車商品的索引(即數組的索引);
  • 經過vuex的 Actions (updateShoppingCart),修改購物車商品的勾選狀態;

html:

<div class="pro-check">
  <el-checkbox :value="item.check" @change="checkChange($event,index)"></el-checkbox>
</div>

邏輯代碼以下:

checkChange(val, key) {
  // 更新vuex中購物車商品是否勾選的狀態
  this.updateShoppingCart({ key: key, prop: "check", val: val });
}

說明: 此處使用的vuex的mutationsvuex和actions,和修改商品數量的是同一個,兩個場景,經過傳遞的參數不一樣進行區分。修改商品數量時傳遞參數是{ key: key, prop: "num", val: val },是否勾選商品傳遞的參數是{ key: key, prop: "check", val: val },請注意prop的變化。

9. 是否全選商品

思路:

  • 購物車設置了一個全選框,經過v-model綁定isAllCheck
  • isAllCheck值是經過計算屬性的 getter獲取vuex中的getters.getIsAllCheck;
  • vuex中的getters.getIsAllCheck經過遍歷購物車數組,判斷每個商品勾選狀態,只要有一個商品沒有勾選,getIsAllCheck均爲false,不然爲true;
  • 當點擊全選框,經過計算屬性的 setter 調用vuex的Actions (checkAll),更改每一個商品的勾選狀態,從而修改全選框的狀態。

html:

<div class="pro-check">
  <el-checkbox v-model="isAllCheck">全選</el-checkbox>
</div>

邏輯代碼以下:

computed: { 
  isAllCheck: {
    get() {
      return this.$store.getters.getIsAllCheck;
    },
    set(val) {
      this.checkAll(val);
    }
  }
}

vuex的getters:

getIsAllCheck(state) {
  // 判斷是否全選
  let isAllCheck = true;
  for (let i = 0; i < state.shoppingCart.length; i++) {
    const temp = state.shoppingCart[i];
    // 只要有一個商品沒有勾選當即return false;
    if (!temp.check) {
      isAllCheck = false;
      return isAllCheck;
    }
  }
  return isAllCheck;
}

vuex的mutations:

checkAll(state, data) {
  // 點擊全選按鈕,更改每一個商品的勾選狀態
  for (let i = 0; i < state.shoppingCart.length; i++) {
    state.shoppingCart[i].check = data;
  }
}

vuex的actions

checkAll({ commit }, data) {
  commit('checkAll', data);
}

10. 計算購物車中商品的總數量

在購物車頁面和根組件的頂部導航欄使用。

vuex的getters:

getNum(state) {
  // 購物車商品總數量
  let totalNum = 0;
  for (let i = 0; i < state.shoppingCart.length; i++) {
    const temp = state.shoppingCart[i];
    totalNum += temp.num;
  }
  return totalNum;
}

11. 計算購物車中勾選的商品總數量

在購物車頁面和結算頁面使用。

vuex的getters:

getCheckNum(state) {
  // 獲取購物車勾選的商品總數量
  let totalNum = 0;
  for (let i = 0; i < state.shoppingCart.length; i++) {
    const temp = state.shoppingCart[i];
    if (temp.check) {
      totalNum += temp.num;
    }
  }
  return totalNum;
}

12. 計算購物車中勾選的商品總價格

在購物車頁面和結算頁面使用。

vuex的getters:

getTotalPrice(state) {
  // 購物車勾選的商品總價格
  let totalPrice = 0;
  for (let i = 0; i < state.shoppingCart.length; i++) {
    const temp = state.shoppingCart[i];
    if (temp.check) {
      totalPrice += temp.price * temp.num;
    }
  }
  return totalPrice;
}

13.生成購物車中勾選的商品詳細信息

在結算頁面使用。

vuex的getters:

getCheckGoods(state) {
  // 獲取勾選的商品信息
  // 用於確認訂單頁面
  let checkGoods = [];
  for (let i = 0; i < state.shoppingCart.length; i++) {
    const temp = state.shoppingCart[i];
    if (temp.check) {
      checkGoods.push(temp);
    }
  }
  return checkGoods;
}

總結

至此,購物車的前端部分已經所有實現:從數據庫同步購物車數據,根據購物車數據動態生成購物車頁面,添加商品到購物車,刪除購物車中的商品,修改購物車商品的數量,是否勾選購物車商品,是否全選購物車商品, 計算購物車中商品的總數量,計算購物車中勾選的商品總數量,計算購物車中勾選的商品總價格,生成購物車中勾選的商品詳細信息。

後記

結束了,新人第一次發帖,如有不對的地方,請多多指教 ^_^

本文是基於完整項目,就購物車模塊的實現進行總結。

完整項目代碼倉庫:https://github.com/hai-27/vue-store

項目預覽連接: http://106.15.179.105 (沒有兼容移動端,請使用PC訪問)。

喜歡本文的同窗,不妨點個贊,若是能給完整項目代碼倉庫加個Star就更好了,謝謝 ^_^

對這個項目會作更多的總結,感興趣的同窗能夠點個關注。

感謝你的閱讀!

做者 hai-27

2020年3月8日

相關文章
相關標籤/搜索