VUE购物车案例(组件间传参练习)

  • Post author:
  • Post category:vue



目录


⭕ 购物车效果图及组件关系图如下:


1 App.vue


2 Header.vue


3 Goods.vue


4 Counter.vue


5 Footer.vue


6 EventBus.js


⭕ 下列代码将组件Counter的传参方式进行了优化,取消了EventBus,改用插槽slot方式


1  App.vue


2  Goods.vue


3  Counter.vue






购物车效果图及组件关系图如下:



主要练习父子传参、子父传参、事件车。


其中Counter.vue–>App.vue 是通过EventBus




1 App.vue



<template>
  <div class="app-container">
    <!-- Header头部区域 -->
    <Header title="购物车案例" />
    <!-- 计算属性在定义的时候是方法,在使用时就是普通的属性 -->
    <!-- <p>{{ fullState }}</p> -->
    <!-- 父子传参 第一种方法,一个个传 -->
    <Goods
      v-for="l in list"
      :key="l.id"
      :id="l.id"
      :title="l.goods_name"
      :pic="l.goods_img"
      :price="l.goods_price"
      :state="l.goods_state"
      :count="l.goods_count"
      @state-change="getNewState"
    />
    <!-- 父子传参 第二种方法,直接在props定义一个对象传  ******不适合大型项目-->
    <!-- <Goods v-for="l in list" :goods="l" /> -->
    <Footer
      :isfull="fullState"
      :allnum="total"
      :amount="sum"
      @full-change="getFullState"
    />
    <h1>App 根组件</h1>
  </div>
</template>

<script>
import axios from "axios";
import Header from "@/components/Header/Header.vue";
import Goods from "@/components/Goods/Goods.vue";
import Footer from "@/components/Footer/Footer.vue";
import bus from "@/components/EventBus.js";
export default {
  components: {
    Header,
    Goods,
    Footer,
  },
  data() {
    return {
      list: [],
    };
  },
  methods: {
    async initCartList() {
      //调用get方法请求数据
      /*
      const res = await axios.get("https://www.escook.cn/api/cart");
      console.log(res);
      */
      const { data: res } = await axios.get("https://www.escook.cn/api/cart");
      console.log(res);
      //只要请求回来的数据,在页面渲染期间要用到,则必须转存到data中
      if (res.status === 200) {
        this.list = res.list;
      }
    },
    //接收子组件传过来的数据
    //e的格式为{ id, value }
    getNewState(e) {
      console.log("父组件接收到数据了", e);
      this.list.some((item) => {
        if (item.id === e.id) {
          item.goods_state = e.value;
          return true;
        }
      });
    },
    //接收Footer子组件传递过来的全选按钮的状态
    getFullState(val) {
      console.log("在app中拿到了全选的状态", val);
      this.list.forEach((item) => {
        item.goods_state = val;
      });
    },
  },
  computed: {
    //已勾选商品的总数量,t=累加的结果
    total() {
      return this.list
        .filter((item) => item.goods_state)
        .reduce((t, item) => (t += item.goods_count), 0);
    },
    //动态计算出全选的值是true还是false
    fullState() {
      return this.list.every((item) => item.goods_state);
    },
    //已勾选商品的总价格amt( {
    sum() {
      // 1.先 filter过滤
      // 2.再reduce累加
      return this.list
        .filter((item) => item.goods_state)
        .reduce((total, item) => (total += item.goods_price * item.goods_count), 0);
    },
  },
  created() {
    //调用请求数据的方法
    this.initCartList();
    //接收子组件Counter.vue传来的值
    bus.$on("share", (val) => {
      console.log("APP组件中接收到了值", val);
      this.list.some((item) => {
        if (item.id === val.id) {
          item.goods_count = val.value;
          return true;
        }
      });
    });
  },
};
</script>

<style lang="less" scoped>
.app-container {
  padding-top: 45px;
  padding-bottom: 50px;
}
</style>




2 Header.vue



<template>
  <div class="header-container">{{ title }}</div>
</template>

<script>
export default {
  props: {
    //声明title自定义属性,允许使用者自定义标题的内容
    title: {
      default: "",
      type: String,
    },
  },
};
</script>

<style lang="less" scoped>
.header-container {
  font-size: 12px;
  height: 45px;
  width: 100%;
  background-color: #1d7bff;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;
  position: fixed;
  top: 0;
  z-index: 999;
}
</style>




3 Goods.vue



<template>
  <div class="goods-container">
    <!-- 左侧图片 -->
    <div class="thumb">
      <div class="custom-control custom-checkbox">
        <!-- 复选框 -->
        <input
          type="checkbox"
          class="custom-control-input"
          :id="'cb' + id"
          :checked="state"
          @change="stateChange"
        />
        <label class="custom-control-label" :for="'cb' + id">
          <!-- 商品的缩略图 -->
          <img :src="pic" alt="" />
        </label>
      </div>
    </div>
    <!-- 右侧信息区域 -->
    <div class="goods-info">
      <!-- 商品标题 -->
      <h6 class="goods-title">{{ title }}</h6>
      <div class="goods-info-bottom">
        <!-- 商品价格 -->
        <!-- 父子传参 第一种方法 -->
        <span class="goods-price">{{ price }}</span>
        <!-- 父子传参 第二种方法 -->
        <!-- <span class="goods-price">{{ goods.goods_price }}</span> -->
        <!-- 商品的数量 -->
        <Counter :num="count" :id="id" />
      </div>
    </div>
  </div>
</template>

<script>
import Counter from "@/components/Counter/Counter";
export default {
  components: {
    Counter,
  },
  //父向子传值
  props: {
    //要渲染的商品
    /*
    //父子传参 第二种props方法接收
    goods: {
      type: Object,
      default: {},
    },
    */
    /* 为啥在这里要封装一个id属性呢 ? 原因:将来,子组件中商品的勾选状态变化之后,需要通过子→父的形式,通知父组件根据id 修改对应商品的勾选状态。*/
    id: {
      required: true, // id必须传
      type: Number,
    },
    title: {
      default: "",
      type: String,
    },
    pic: {
      default: "",
      type: String,
    },
    price: {
      default: "",
      type: Number,
    },
    state: {
      default: true,
      type: Boolean,
    },
    count: {
      default: 1,
      type: Number,
    },
  },
  methods: {
    //只要复选框的选中状态发生了变化,就会调用这个处理函数
    stateChange(e) {
      console.log("事件对象", e);
      const newState = e.target.checked;
      console.log("checked最新的状态", newState);
      //触发自定义事件
      this.$emit("state-change", { id: this.id, value: newState });
    },
  },
};
</script>

<style lang="less" scoped>
.goods-container {
  + .goods-container {
    border-top: 1px solid #efefef;
  }
  padding: 10px;
  display: flex;
  .thumb {
    display: flex;
    align-items: center;
    img {
      width: 100px;
      height: 100px;
      margin: 0 10px;
    }
  }

  .goods-info {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    flex: 1;
    .goods-title {
      font-weight: bold;
      font-size: 12px;
    }
    .goods-info-bottom {
      display: flex;
      justify-content: space-between;
      .goods-price {
        font-weight: bold;
        color: red;
        font-size: 13px;
      }
    }
  }
}
</style>








4 Counter.vue



<template>
  <div class="number-container d-flex justify-content-center align-items-center">
    <!-- 减 1 的按钮 -->
    <button @click="sub" type="button" class="btn btn-light btn-sm">-</button>
    <!-- 购买的数量 -->
    <span class="number-box">{{ num }}</span>
    <!-- 加 1 的按钮 -->
    <button @click="add" type="button" class="btn btn-light btn-sm">+</button>
  </div>
</template>

<script>
import bus from "@/components/EventBus.js";
export default {
  props: {
    //接收到父组件Goods.vue的值
    num: {
      type: Number,
      default: 1,
    },
    //使用EventBus用
    id: {
      required: true,
      type: Number,
    },
  },
  methods: {
    add() {
      //要发送给 App的数据格式为 { id, value }
      const obj = { id: this.id, value: this.num + 1 };
      console.log(obj);
      //要做的事情:通过EventBus把 obj对象,发送给 App. vue 组件
      bus.$emit("share", obj);
    },
    sub() {
      if (this.num - 1 === 0) return;
      const obj = { id: this.id, value: this.num - 1 };
      console.log(obj);
      bus.$emit("share", obj);
    },
  },
};
</script>

<style lang="less" scoped>
.number-box {
  min-width: 30px;
  text-align: center;
  margin: 0 5px;
  font-size: 12px;
}

.btn-sm {
  width: 30px;
}
</style>








5 Footer.vue









<template>
  <div class="footer-container">
    <!-- 左侧的全选 -->
    <div class="custom-control custom-checkbox">
      <input
        type="checkbox"
        class="custom-control-input"
        id="cbFull"
        :checked="isfull"
        @change="fullChange"
      />
      <label class="custom-control-label" for="cbFull">全选</label>
    </div>

    <!-- 中间的合计 -->
    <div>
      <span>合计:</span>
      <span class="total-price">¥{{ amount.toFixed(2) }}</span> 
    </div>

    <!-- 结算按钮 -->
    <button type="button" class="btn btn-primary btn-settle">结算({{ allnum }})</button>
  </div>
</template>

<script>
export default {
  props: {
    //全选的状态
    isfull: {
      type: Boolean,
      default: true,
    },
    //总价格
    amount: {
      type: Number,
      default: 0,
    },
    //总数量
    allnum: {
      type: Number,
      default: 0,
    },
  },
  methods: {
    fullChange(e) {
      console.log(e.target.checked);
      this.$emit("full-change", e.target.checked);
    },
  },
};
</script>

<style lang="less" scoped>
.footer-container {
  font-size: 12px;
  height: 50px;
  width: 100%;
  border-top: 1px solid #efefef;
  position: fixed;
  bottom: 0;
  background-color: #fff;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 10px;
}

.custom-checkbox {
  display: flex;
  align-items: center;
}

#cbFull {
  margin-right: 5px;
}

.btn-settle {
  height: 80%;
  min-width: 110px;
  border-radius: 25px;
  font-size: 12px;
}

.total-price {
  font-weight: bold;
  font-size: 14px;
  color: red;
}
</style>



6 EventBus.js


import Vue from 'vue'
export default new Vue()





下列代码将组件Counter的传参方式进行了优化,取消了EventBus,改用插槽slot方式

主要更新如下:



1  App.vue


<template>
  <div class="app-container">
    <!-- Header头部区域 -->
    <Header title="购物车案例" />
    <!-- 计算属性在定义的时候是方法,在使用时就是普通的属性 -->
    <!-- <p>{{ fullState }}</p> -->
    <!-- 父子传参 第一种方法,一个个传 -->
    <Goods
      v-for="l in list"
      :key="l.id"
      :id="l.id"
      :title="l.goods_name"
      :pic="l.goods_img"
      :price="l.goods_price"
      :state="l.goods_state"
      @state-change="getNewState"
    >
      <!-- Counter可以直接访问外层Goods组件的l里的数据 -->
      <Counter :num="l.goods_count" @num-change="getNewNum(l, $event)"></Counter>
    </Goods>
    <!-- 父子传参 第二种方法,直接在props定义一个对象传  ******不适合大型项目-->
    <!-- <Goods v-for="l in list" :goods="l" /> -->
    <Footer
      :isfull="fullState"
      :allnum="total"
      :amount="sum"
      @full-change="getFullState"
    />
    <h1>App 根组件</h1>
  </div>
</template>

<script>
import axios from "axios";
import Header from "@/components/Header/Header.vue";
import Goods from "@/components/Goods/Goods.vue";
import Footer from "@/components/Footer/Footer.vue";
import Counter from "@/components/Counter/Counter";
export default {
  components: {
    Header,
    Goods,
    Footer,
    Counter,
  },
  data() {
    return {
      list: [],
    };
  },
  methods: {
    async initCartList() {
      //调用get方法请求数据
      /*
      const res = await axios.get("https://www.escook.cn/api/cart");
      console.log(res);
      */
      const { data: res } = await axios.get("https://www.escook.cn/api/cart");
      console.log(res);
      //只要请求回来的数据,在页面渲染期间要用到,则必须转存到data中
      if (res.status === 200) {
        this.list = res.list;
      }
    },
    //接收子组件传过来的数据
    //e的格式为{ id, value }
    getNewState(e) {
      console.log("父组件接收到数据了", e);
      this.list.some((item) => {
        if (item.id === e.id) {
          item.goods_state = e.value;
          return true;
        }
      });
    },
    //接收Footer子组件传递过来的全选按钮的状态
    getFullState(val) {
      console.log("在app中拿到了全选的状态", val);
      this.list.forEach((item) => {
        item.goods_state = val;
      });
    },
    // 获取Counter组件发过来的最新的数量值
    getNewNum(l, e) {
      console.log("商品的l项----", l, "传过来的数量值e----", e);
      l.goods_count = e;
    },
  },
  computed: {
    //已勾选商品的总数量,t=累加的结果
    total() {
      return this.list
        .filter((item) => item.goods_state)
        .reduce((t, item) => (t += item.goods_count), 0);
    },
    //动态计算出全选的值是true还是false
    fullState() {
      return this.list.every((item) => item.goods_state);
    },
    //已勾选商品的总价格amt( {
    sum() {
      // 1.先 filter过滤
      // 2.再reduce累加
      return this.list
        .filter((item) => item.goods_state)
        .reduce((total, item) => (total += item.goods_price * item.goods_count), 0);
    },
  },
  created() {
    //调用请求数据的方法
    this.initCartList();
  },
};
</script>

<style lang="less" scoped>
.app-container {
  padding-top: 45px;
  padding-bottom: 50px;
}
</style>



2  Goods.vue


<template>
  <div class="goods-container">
    <!-- 左侧图片 -->
    <div class="thumb">
      <div class="custom-control custom-checkbox">
        <!-- 复选框 -->
        <input
          type="checkbox"
          class="custom-control-input"
          :id="'cb' + id"
          :checked="state"
          @change="stateChange"
        />
        <label class="custom-control-label" :for="'cb' + id">
          <!-- 商品的缩略图 -->
          <img :src="pic" alt="" />
        </label>
      </div>
    </div>
    <!-- 右侧信息区域 -->
    <div class="goods-info">
      <!-- 商品标题 -->
      <h6 class="goods-title">{{ title }}</h6>
      <div class="goods-info-bottom">
        <!-- 商品价格 -->
        <!-- 父子传参 第一种方法 -->
        <span class="goods-price">{{ price }}</span>
        <!-- 父子传参 第二种方法 -->
        <!-- <span class="goods-price">{{ goods.goods_price }}</span> -->
        <!-- 商品的数量 -->
        <!-- <Counter :num="count" :id="id" /> -->
        <slot></slot>
      </div>
    </div>
  </div>
</template>

<script>
import Counter from "@/components/Counter/Counter";
export default {
  components: {
    Counter,
  },
  //父向子传值
  props: {
    //要渲染的商品
    /*
    //父子传参 第二种props方法接收
    goods: {
      type: Object,
      default: {},
    },
    */
    /* 为啥在这里要封装一个id属性呢 ? 原因:将来,子组件中商品的勾选状态变化之后,需要通过子→父的形式,通知父组件根据id 修改对应商品的勾选状态。*/
    id: {
      required: true, // id必须传
      type: Number,
    },
    title: {
      default: "",
      type: String,
    },
    pic: {
      default: "",
      type: String,
    },
    price: {
      default: "",
      type: Number,
    },
    state: {
      default: true,
      type: Boolean,
    },
    count: {
      default: 1,
      type: Number,
    },
  },
  methods: {
    //只要复选框的选中状态发生了变化,就会调用这个处理函数
    stateChange(e) {
      console.log("事件对象", e);
      const newState = e.target.checked;
      console.log("checked最新的状态", newState);
      //触发自定义事件
      this.$emit("state-change", { id: this.id, value: newState });
    },
  },
};
</script>

<style lang="less" scoped>
.goods-container {
  + .goods-container {
    border-top: 1px solid #efefef;
  }
  padding: 10px;
  display: flex;
  .thumb {
    display: flex;
    align-items: center;
    img {
      width: 100px;
      height: 100px;
      margin: 0 10px;
    }
  }

  .goods-info {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    flex: 1;
    .goods-title {
      font-weight: bold;
      font-size: 12px;
    }
    .goods-info-bottom {
      display: flex;
      justify-content: space-between;
      .goods-price {
        font-weight: bold;
        color: red;
        font-size: 13px;
      }
    }
  }
}
</style>



3  Counter.vue


<template>
  <div class="number-container d-flex justify-content-center align-items-center">
    <!-- 减 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm">-</button>
    <!-- 购买的数量 -->
    <span class="number-box">{{ num }}</span>
    <!-- 加 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="add">+</button>
  </div>
</template>

<script>
export default {
  props: {
    //要展示的商品的数量
    num: {
      type: Number,
      default: 1,
    },
  },
  methods: {
    add() {
      // 通过自定义事件,把最新的数量值发送给父组件;
      this.$emit("num-change", this.num + 1);
    },
  },
};
</script>

<style lang="less" scoped>
.number-box {
  min-width: 30px;
  text-align: center;
  margin: 0 5px;
  font-size: 12px;
}

.btn-sm {
  width: 30px;
}
</style>



版权声明:本文为m0_66051368原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。