实现移动端购物车导航

142 阅读1分钟

思路

  1. 向购物车组件ShoppingCart.vue传入商品列表数组lsit,商品列表数组中每个对象包含num属性,表示购买该商品的数量,在data中将list深拷贝给shoppingList【不能直接修改props】。
  2. 通过计算属性返回购买了的商品列表selectList,这里通过判断商品列表中的num是否为0筛选出selectList
  3. 通过计算属性返回购买的商品总数num,这里通过遍历shoppingListreduce累计返回num
  4. 通过计算属性返回购买商品的总价totalPrice,这里通过遍历shoppingListreduce累计计算返回
  5. 通过 watch 监听父组件传过来的商品list的变化,一旦发生变化就将新的list深拷贝给shoppingList
  6. 当修改了购物车组件的shoppingList之后通过触发自定义事件this.$emit("changeList",this.shoppingList)将修改后的商品列表回传给父组件,父组件接收并更新商品列表phoneList

代码

购物车子组件 ShoppingCart.vue

<template>
  <div>
    <!-- 购物车栏 -->
    <div
      class="shop-bottom-wrapper"
      :class="{
        'shopping-cart-top-display-right-animation':
          displayStyle && isShoppingCartTopDisplay,
        'shopping-cart-top-display-top-animation':
          !displayStyle && isShoppingCartTopDisplay,
      }"
    >
      <p class="top-description" v-if="isShoppingCartTopDisplay">
        可下单金额¥935,158.83
      </p>
      <div
        class="shop-wrapper"
        :class="
          shoppingCartAnimation
            ? 'shopping-cart-expansion-animation'
            : 'shopping-cart-shrink-animation'
        "
      >
        <div class="shop-img-wrapper" @click="shoppingDetailInfo()">
          <img :src="require('@/assets/icon/shop.png')" class="shop-img" />
          <div class="shop-num" v-if="num !== 0">{{ num }}</div>
        </div>
        <div class="shop-text-wrapper" v-if="num !== 0">
          <div class="price-wrapper">
            <span class="description">
              合计:¥ <span class="total-price">{{ totalPrice }}</span>
            </span>
          </div>
          <p class="description">优惠券抵扣:¥0.5</p>
        </div>
        <div class="button" v-if="num !== 0">提交订单</div>
      </div>
    </div>

    <div class="mask" v-if="dialog" @click.self="hideMask()"></div>
    <div class="dialog" :class="dialog ? 'dialog-display-animation' : ''">
      <p class="dialog-top-description">可下单金额¥935,158.83</p>
      <div class="clear-box" @click="clearShoppingCart()">
        <img :src="require('@/assets/image/dustbin.png')" />
        <span>清空商品</span>
      </div>
      <div class="shop-list">
        <div class="shop-item" v-for="item in selectList" :key="item.id">
          <img :src="item.imgSrc" />
          <div class="shop-text-wrapper">
            <h3>{{ item.title }}</h3>
            <p>¥ {{ item.price }}</p>
          </div>
          <div class="shop-button-box">
            <span @click="reduceNum(item.id)">-</span>
            <span>{{ item.num }}</span>
            <span @click="addNum(item.id)">+</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "ShoppingCart",
  props: {
    list: {
      type: Array,
      required: true,
      default() {
        return [];
      },
    },
  },
  data() {
    return {
      shoppingList: JSON.parse(JSON.stringify(this.list)),
      displayStyle: true, // 购物车顶部文字出现方式(右划出)
      shoppingCartAnimation: false, // 购物车展开动画
      dialog: false,
    };
  },
  computed: {
    // 选中的手机列表
    selectList() {
      return this.shoppingList.filter((item) => item.num !== 0);
    },

    // 选中的总数
    num() {
      return this.shoppingList.reduce((total, item) => {
        return (total = total + item.num);
      }, 0);
    },

    // 选中的总价
    totalPrice() {
      return this.shoppingList.reduce((total, item) => {
        return (total = total + item.num * parseFloat(item.price));
      }, 0);
    },

    // 购物车顶部文字是否出现
    isShoppingCartTopDisplay() {
      return this.shoppingCartAnimation && !this.dialog;
    },
  },
  watch: {
    num(n) {
      if (n > 0) {
        this.shoppingCartAnimation = true;
      } else {
        this.shoppingCartAnimation = false;
        this.displayStyle = true;
        this.dialog = false;
      }
    },
    // 监听父组件传过来的list的变化
    list: {
      handler(n) {
        this.shoppingList = JSON.parse(JSON.stringify(n));
      },
      deep: true,
    },
  },
  methods: {
    shoppingDetailInfo() {
      this.dialog = true;
    },

    hideMask() {
      this.dialog = false;
      this.displayStyle = false;
    },

    clearShoppingCart() {
      this.shoppingList.forEach((item) => {
        item.num = 0;
      });
      this.$emit("changeList", this.shoppingList);
      this.dialog = false;
    },

    reduceNum(id) {
      this.shoppingList.forEach((item) => {
        if (item.id === id && item.num !== 0) {
          item.num--;
        }
      });
      this.$emit("changeList", this.shoppingList);
    },

    addNum(id) {
      this.shoppingList.forEach((item) => {
        if (item.id === id) {
          item.num++;
        }
      });
      this.$emit("changeList", this.shoppingList);
    },
  },
};
</script>

<style lang="stylus" scoped>
/* 购物车顶部文字展开动画(右滑动) */
@keyframes shoppingCartTopDisplayRight
  from
    width 50px;
  to
    width 94%;
/* 购物车顶部文字展开动画(上滑动) */
@keyframes shoppingCartTopDisplayTop
  from
    height 0;
  to
    height 75px;
/* 购物车栏 */
.shop-bottom-wrapper
  position fixed;
  left 3%;
  bottom 80px;
  z-index 999;
  width 94%;
  height 75px;
  border-radius 25px;
  .top-description
    padding 2px 0 0 45px;
    font-size 12px;
    overflow hidden;
    white-space nowrap;
  .shop-wrapper
    position absolute;
    bottom 0;
    display flex;
    flex-direction row;
    align-items center;
    width 50px;
    height 50px;
    color white;
    border-radius 25px;
    background rgb(64, 63, 63);
    .shop-img-wrapper
      position relative;
      display flex;
      justify-content center;
      align-items center;
      width 50px;
      height 50px;
      .shop-img
        width 20px;
        height 20px;
      .shop-num
        position absolute;
        top 8px;
        right 5px;
        width 15px;
        height 15px;
        font-size 12px;
        line-height 15px;
        text-align center;
        background red;
        border-radius 50%;
    .shop-text-wrapper
      flex 1;
      padding-left 8px;
      overflow hidden;
      white-space nowrap;
      .description
        font-size 12px;
        .total-price
          font-size 16px;
    .button
      display flex;
      justify-content center;
      align-items center;
      width 120px;
      height 100%;
      background rgb(40, 196, 40);
      border-top-right-radius 25px;
      border-bottom-right-radius 25px;
      overflow hidden;
      white-space nowrap;
  /* 购物车展开动画 */
  .shopping-cart-expansion-animation
    width 100%;
    transition width 0.5s;
  /* 购物车收缩动画 */
  .shopping-cart-shrink-animation
    width 50px;
    transition width 0.5s;
/* 购物车顶部文字展开动画(右滑动) */
.shopping-cart-top-display-right-animation
  background #fff2d7;
  animation shoppingCartTopDisplayRight 0.5s;
  animation-fill-mode forwards;
/* 购物车顶部文字展开(上滑动) */
.shopping-cart-top-display-top-animation
  background #fff2d7;
  animation shoppingCartTopDisplayTop 0.8s;
  animation-fill-mode forwards;
/* 窗口 */
.dialog
  position fixed;
  bottom 70px;
  width 100%;
  height 0;
  border-top-left-radius 5px;
  border-top-right-radius 5px;
  background white;
  .dialog-top-description
    padding 5px 0;
    font-size 12px;
    text-align center;
    background #fff2d7;
    border-top-left-radius 10px;
    border-top-right-radius 10px;
  .clear-box
    padding 10px;
    font-size 12px;
    text-align right;
    color #DEDEDE;
    img
      width 15px;
      vertical-align middle;
    span
      padding-left 5px;
      vertical-align middle;
  .shop-list
    padding-bottom 50px;
    height 200px;
    overflow auto;
    .shop-item
      padding 10px;
      display flex;
      flex-direction row;
      align-items center;
      img
        width 35px;
        height 50px;
      .shop-text-wrapper
        flex 1;
        padding 0 10px;
        p
          color red;
      .shop-button-box
        width 80px;
        height 30px;
        border 1px solid #DEDEDE;
        border-radius 5px;
        span
          display inline-block;
          width 30%;
          height 100%;
          line-height 30px;
          text-align center;
          &:nth-child(2)
            width 40%;
            border-left 1px solid #DEDEDE;
            border-right 1px solid #DEDEDE;
.dialog-display-animation
  height 300px;
  transition height 0.8s;
/* 遮罩 */
.mask
  position fixed;
  top 0;
  left 0;
  width 100%;
  height 100%;
  background-color rgba(0, 0, 0, 0.5);
</style>

父组件 index.vue

<template>
  <div>
    <div class="phone-list">
      <div class="item-wrapper" v-for="(item, index) in phoneList" :key="index">
        <img :src="item.imgSrc" class="item-img" />
        <div class="text-wrapper">
          <h3>{{ item.title }}</h3>
          <p>¥ {{ item.price }}</p>
        </div>
        <div class="button-box" @click="addGoods(item.id)">
          +
          <div class="num" v-if="item.num !== 0">{{ item.num }}</div>
        </div>
      </div>
    </div>
    <shopping-cart
      :list="phoneList"
      @changeList="getNewPhoneList"
    ></shopping-cart>
  </div>
</template>

<script>
import ShoppingCart from "./components/ShoppingCart.vue";
export default {
  name: "shoppingBar",
  components: { ShoppingCart },
  data() {
    return {
      phoneList: [
        {
          id: "1",
          imgSrc: require("@/assets/image/phone1.jpg"),
          title: "OPPO Free系列 003",
          price: "1999",
          num: 0,
        },
        {
          id: "2",
          imgSrc: require("@/assets/image/phone2.jpg"),
          title: "OPPO Free系列 003",
          price: "2999",
          num: 0,
        },
        {
          id: "3",
          imgSrc: require("@/assets/image/phone3.jpg"),
          title: "OPPO Free系列 003",
          price: "3999",
          num: 0,
        },
        {
          id: "4",
          imgSrc: require("@/assets/image/phone3.jpg"),
          title: "OPPO Free系列 003",
          price: "4999",
          num: 0,
        },
        {
          id: "5",
          imgSrc: require("@/assets/image/phone3.jpg"),
          title: "OPPO Free系列 003",
          price: "5999",
          num: 0,
        },
        {
          id: "6",
          imgSrc: require("@/assets/image/phone3.jpg"),
          title: "OPPO Free系列 003",
          price: "6999",
          num: 0,
        },
        {
          id: "7",
          imgSrc: require("@/assets/image/phone3.jpg"),
          title: "OPPO Free系列 003",
          price: "7999",
          num: 0,
        },
      ],
    };
  },

  methods: {
    addGoods(id) {
      this.phoneList.forEach((item) => {
        if (item.id === id) {
          item.num++;
        }
      });
    },

    // 自定义事件的回调函数,获取更新后的phoneList
    getNewPhoneList(data) {
      this.phoneList = JSON.parse(JSON.stringify(data));
    },
  },
};
</script>

<style lang="stylus" scoped>
/* 手机列表 */
.phone-list
  padding-bottom 70px;
  .item-wrapper
    position relative;
    padding 20px;
    display flex;
    flex-direction row;
    align-items center;
    background rgb(245, 244, 244);
    .item-img
      width 50px;
      height 50px;
    .text-wrapper
      padding 0 15px;
      flex 1;
      p
        color red;
    .button-box
      position relative;
      width 25px;
      height 25px;
      display flex;
      justify-content center;
      align-items center;
      color white;
      border-radius 5px;
      background rgb(40, 196, 40);
      .num
        position absolute;
        top -9px;
        right -9px;
        display flex;
        justify-content center;
        align-items center;
        width 18px;
        height 18px;
        font-size 12px;
        background red;
        border-radius 5px;
</style>

小结

样式选择

1、动态选择多个class
:class="{ '类名1':变量, '类名2':变量}"
2、动态选择class2选一】
:class="变量? '类名1': '类名2'"