购物车项目实现步骤

305 阅读3分钟

购物车项目

项目完成后的样子

购物车全效果.gif

1.0 案例-购物车-项目初始化

目标: 初始化新项目, 清空不要的东西, 下载bootstrap库, 下载less模块

vue create shopcar
yarn add bootstrap@3.4.1
yarn add less less-loader@5.0.0 -D
图示:

image-20210307092110985.png

  1. 按照需求, 把项目页面拆分成几个组件, 在components下创建
  • MyHeader组件

  • MyFooter组件

  • MyGoods组件 - 商品

  • MyCount组件

  1. 然后引入到App.vue上注册
  2. 在main.js中引入bootStrap库
import "bootstrap/dist/css/bootstrap.css" // 引入第三方包里的某个css文件
MyHeader.vue
<template>
  <div class="my-header">购物车案例</div>
</template>

<script>
export default {

}
</script>

<style lang="less" scoped>
  .my-header {
    height: 45px;
    line-height: 45px;
    text-align: center;
    background-color: #1d7bff;
    color: #fff;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    z-index: 2;
  }
</style>
MyGoods.vue
<template>
  <div class="my-goods-item">
    <div class="left">
      <div class="custom-control custom-checkbox">
        <input type="checkbox" class="custom-control-input" id="input"
        >
        <label class="custom-control-label" for="input">
          <img src="http://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg" alt="">
        </label>
      </div>
    </div>
    <div class="right">
      <div class="top">商品名字</div>
      <div class="bottom">
        <span class="price">¥ 100</span>
        <span>
            数量组件
        </span>
      </div>
    </div>
  </div>
</template>

<script>
export default {

}
</script>

<style lang="less" scoped>
.my-goods-item {
  display: flex;
  padding: 10px;
  border-bottom: 1px solid #ccc;
  .left {
    img {
      width: 120px;
      height: 120px;
      margin-right: 8px;
      border-radius: 10px;
    }
    .custom-control-label::before,
    .custom-control-label::after {
      top: 50px;
    }
  }
  .right {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    .top{
        font-size: 14px;
        font-weight: 700;
    }
    .bottom {
      display: flex;
      justify-content: space-between;
      padding: 5px 0;
      align-items: center;
      .price {
        color: red;
        font-weight: bold;
      }
    }
  }
}

</style>

目标: 完成商品组件右下角商品组件的开发

image-20210223135638409.png

components/MyCount.vue
<template>
  <div class="my-counter">
    <button type="button" class="btn btn-light" >-</button>
    <input type="number" class="form-control inp" >
    <button type="button" class="btn btn-light">+</button>
  </div>
</template>

<script>
export default {
}
</script>

<style lang="less" scoped>
.my-counter {
  display: flex;
  .inp {
    width: 45px;
    text-align: center;
    margin: 0 10px;
  }
  .btn, .inp{
    transform: scale(0.9);
  }
}
</style>
components/MyFooter.vue
<template>
  <!-- 底部 -->
  <div class="my-footer">
    <!-- 全选 -->
    <div class="custom-control custom-checkbox">
      <input type="checkbox" class="custom-control-input" id="footerCheck">
      <label class="custom-control-label" for="footerCheck">全选</label>
    </div>
    <!-- 合计 -->
    <div>
      <span>合计:</span>
      <span class="price">¥ 0</span>
    </div>
    <!-- 按钮 -->
    <button type="button" class="footer-btn btn btn-primary">结算 ( 0 )</button>
  </div>
</template>

<script>
export default {
  
}
</script>

<style lang="less" scoped>
.my-footer {
  position: fixed;
  z-index: 2;
  bottom: 0;
  width: 100%;
  height: 50px;
  border-top: 1px solid #ccc;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 10px;
  background: #fff;

  .price {
    color: red;
    font-weight: bold;
    font-size: 15px;
  }
  .footer-btn {
    min-width: 80px;
    height: 30px;
    line-height: 30px;
    border-radius: 25px;
    padding: 0;
  }
}
</style>

1.1 案例-购物车-头部自定义

目的: 头部的标题, 颜色, 背景色可以随便修改, props类型的校验

思路

  1. 在MyHeader.vue中准备props里变量, 然后使用
  2. 在使用MyHeader.vue组件时, 传入相应的值 (color和backgroundColor)
MyHeader.vue
<template>
  <div class="my-header" :style="{backgroundColor: background, color}">{{ title }}</div>
</template>

<script>
// 目标: 让Header组件支持不同的项目 - 自定义
// 1. 分析哪些可以自定义 (背景色, 文字颜色, 文字内容)
// 2. (新) 可以对props的变量的值 进行校验
// 3. 内部使用props变量的值
// 4. 外部使用时, 遵守变量名作为属性名, 值的类型遵守
export default {
    props: {
        background: String, // 外部插入此变量的值, 必须是字符串类型, 否则报错
        color: {
            type: String, // 约束color值的类型
            default: "#fff" // color变量默认值(外部不给 我color传值, 使用默认值)
        },
        title: {
            type: String,
            required: true // 必须传入此变量的值
        }
    }
}
</script>

<style lang="less" scoped>
  .my-header {
    height: 45px;
    line-height: 45px;
    text-align: center;
    background-color: #1d7bff;
    color: #fff;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    z-index: 2;
  }
</style>
App.vue传入相应自定义的值
<MyHeader title="购物车案例"></MyHeader>

总结:

props: [] - 只能声明变量和接收, 不能类型校验

props: {} - 声明变量和校验类型规则 - 外部传入值不对则报错

1.2 案例-购物车-请求数据

目标: 使用axios把数据请求回来

数据地址: www.escook.cn/api/cart (get方式)

  1. 下载axios
yarn add axios
  1. main.js - 原型上挂载
// 目标: 请求数据 - 打印
// 1. 下载axios库, main.js - 全局绑定属性 (确保任意.vue文件可以都访问到这个axios方法)
import axios from 'axios'
// 2. 基础地址
axios.defaults.baseURL = "https://www.escook.cn"
// 3. axios方法添加到Vue的原型上
Vue.prototype.$axios = axios

new Vue({
    render: h => h(App),
}).$mount('#app')

  1. App.vue请求使用
<script>
export default {
  data(){
    return {
      list: [] // 商品所有数据
    }
  },
  created(){
    // 不必在自己引入axios变量, 而是直接使用全局属性$axios
    this.$axios({
      url: "/api/cart"
    }).then(res => {
      console.log(res);
      this.list = res.data.list
    })
  }
}
</script>

总结: 利用axios, 调用接口, 把数据请求回来

1.3 案例-购物车-数据渲染

目标: 把上面请求的数据, 铺设到页面上

App.vue
<MyGoods v-for="obj in list" 
         :key="obj.id"
         :gObj="obj"
></MyGoods>
MyGoods.vue
<template>
  <div class="my-goods-item">
    <div class="left">
      <div class="custom-control custom-checkbox">
        <!-- *重要:
          每个对象和组件都是独立的
          对象里的goods_state关联自己对应商品的复选框
         -->
         <!-- bug:
          循环的所有label的for都是input, id也都是input - 默认只有第一个生效
          解决: 每次对象里的id值(1, 2), 分别给id和for使用即可区分
          -->
        <input type="checkbox" class="custom-control-input" :id="gObj.id"
        v-model="gObj.goods_state"
        >
        <label class="custom-control-label" :for="gObj.id">
          <img :src="gObj.goods_img" alt="">
        </label>
      </div>
    </div>
    <div class="right">
      <div class="top">{{ gObj.goods_name }}</div>
      <div class="bottom">
        <span class="price">¥ {{ gObj.goods_price }}</span>
        <span>
            <MyCount :obj="gObj"></MyCount>
        </span>
      </div>
    </div>
  </div>
</template>

<script>
import MyCount from './MyCount'
export default {
    props: {
      gObj: Object
    },
    components: {
        MyCount
    }
}
</script>
MyCount.vue
<template>
  <div class="my-counter">
    <button type="button" class="btn btn-light" >-</button>
    <input type="number" class="form-control inp" v-model.number="obj.goods_count">
    <button type="button" class="btn btn-light" >+</button>
  </div>
</template>

<script>
export default {
  props: {
    obj: Object // 商品对象
  }
}
</script>

总结: 把各个组件关联起来, 把数据都铺设到页面上

1.4 案例-购物车-商品选中

问题: 点击发现总是第一个被选中

原来id和for都是"input"

但是id是唯一的啊, 所以用数据的id来作为标签的id, 分别独立, 为了兼容label点击图片也能选中的效果

<input type="checkbox" class="custom-control-input" :id="gObj.id"
       v-model="gObj.goods_state"
       >
<label class="custom-control-label" :for="gObj.id">
    <img :src="gObj.goods_img" alt="">
</label>

总结: lable的for值对应input的id, 点击label就能让对应input处于激活

1.5 案例-购物车-数量控制

目标: 点击+和-或者直接修改输入框的值影响商品购买的数量

<template>
  <div class="my-counter">
    <button type="button" class="btn btn-light" :disabled="obj.goods_count === 1" @click="obj.goods_count > 1 && obj.goods_count--">-</button>
    <input type="number" class="form-control inp" v-model.number="obj.goods_count">
    <button type="button" class="btn btn-light" @click="obj.goods_count++">+</button>
  </div>
</template>

<script>
// 目标: 商品数量 - 控制
// 1. 外部传入数据对象
// 2. v-model关联对象的goods_count属性和输入框 (双向绑定)
// 3. 商品按钮 +和-, 商品数量最少1件
// 4. 侦听数量改变, 小于1, 直接强制覆盖1
export default {
  props: {
    obj: Object // 商品对象
  },
  // 因为数量控制要通过对象"互相引用的关系"来影响外面对象里的数量值, 所以最好传 对象进来
  watch: {
    obj: {
      deep: true,
      handler(){ // 拿到商品数量, 判断小于1, 强制修改成1
        if (this.obj.goods_count < 1) {
          this.obj.goods_count = 1
        }
      }
    }
  }
}
</script>

1.6 案例-购物车-全选功能

目标: 在底部组件上, 完成全选功能

image-20210223141427684.png

思路:
  1. 点击获取它的选中状态
  2. 同步给上面每个小选框 - 而小选框的选中状态又在数组里
  3. 把数组传给MyFooter, 然后更新即可 - 因为对象都是引用关系的
MyFooter.vue
<template>
  <!-- 底部 -->
  <div class="my-footer">
    <!-- 全选 -->
    <div class="custom-control custom-checkbox">
      <input type="checkbox" class="custom-control-input" id="footerCheck" v-model="isAll">
      <label class="custom-control-label" for="footerCheck">全选</label>
    </div>
    <!-- 合计 -->
    <div>
      <span>合计:</span>
      <span class="price">¥ {{ allPrice }}</span>
    </div>
    <!-- 按钮 -->
    <button type="button" class="footer-btn btn btn-primary">结算 ( {{ allCount }} )</button>
  </div>
</template>

<script>
// 目标: 全选
// 1. v-model关联全选-复选框(v-model后变量计算属性)
// 2. 页面(视频层)v(true) -> 数据层(变量-) 计算属性(完整写法)
// 3. 把全选 true/false同步给所有小选框选中状态上

// 小选  -> 全选
// App.vue里list数组 -> MyFooter.vue
// isAll的get方法里, 统计状态影响全选框

// 目标: 总数量统计
// 1. allCount计算属性用 数组reduce+判断统计数量并返回

// 目标: 总价
// allPrice计算属性, 数组reduce+单价*数量, 判断选中, 才累加后返回

export default {
  props: {
    arr: Array
  },
  computed: {
    isAll: {
      set(val){ // val就是关联表单的值(true/false)
        this.$emit('changeAll', val)
      },
      get(){
        // 查找小选框关联的属性有没有不符合勾选的条件
        // 直接原地false
        return this.arr.every(obj => obj.goods_state === true)
      }
    },
    
    
  }
}
</script>
App.vue
<MyFooter @changeAll="allFn" :arr="list"></MyFooter>

<script>
methods: {
    allFn(bool){
      this.list.forEach(obj => obj.goods_state = bool)
      // 把MyFooter内的全选状态true/false同步给所有小选框的关联属性上
    }
  }
</script>

总结: 全选的v-model的值, 使用计算属性完整写法

1.7 案例-购物车-总数量

目标: 完成底部组件, 显示选中的商品的总数量

MyFooter.vue
allCount(){
    return this.arr.reduce((sum, obj) => {
        if (obj.goods_state === true) { // 选中商品才累加数量
            sum += obj.goods_count;
        }
        return sum;
    }, 0)
},

总结: 对象之间是引用关系, 对象值改变, 所有用到的地方都跟着改变

1.8 案例-购物车-总价

目标: 完成选中商品计算价格

components/MyFooter.vue
allPrice(){
      return this.arr.reduce((sum, obj) => {
        if (obj.goods_state){
          sum += obj.goods_count * obj.goods_price
        }
        return sum;
      }, 0)
    }

总结: 把数组传给了MyFooter组件, 统计总价

另一种

shopHeader
<template>
  <div>
    <div class="header">购物车案例</div>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
};
</script>

<style scoped lang="less">
.header {
  line-height: 60px;
  text-align: center;
  background: #1d7bff;
  color: #fff;
  font-size: 18px;
}
</style>

shopMiax
<template>
  <div>
    <div class="list">
      <div class="item">
        <input
          class="checkbox"
          type="checkbox"
          v-model="list.goods_state"
          :id="list.id"
        />
        <label :for="list.id"> <img :src="list.goods_img" alt="" /></label>

        <div class="info">
          <div class="top">{{ list.goods_name }}</div>
          <div class="bottom">
            <div class="price">¥{{ list.goods_price }}</div>
            <div class="number">
              <button @click="list.goods_count--">-</button>
              <input
                class="text"
                type="text"
                v-model.number="list.goods_count"
              />
              <button @click="list.goods_count++">+</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: ["list"],
  watch: {
    "list.goods_count": function (newVal) {
      //当数量小于一的时候
      if (this.list.goods_count < 1) {
        this.list.goods_count = 1; //把数量等于一,就是不能让输入框的数量小于一的意思,真实的购物车是要当数量小于一的时候,询问用户是否删除此商品,确认就向后台发送请求删除购物车的这个商品
      }
    },
  },
  data() {
    return {};
  },
};
</script>

<style scoped lang="less">
.list {
  border-top: 1px solid #eee;

  .item {
    display: flex;
    align-items: center;
    margin-bottom: 20px;

    .checkbox {
      margin: 0 20px;
    }
    img {
      width: 140px;
      height: 140px;
    }
    .info {
      display: flex;
      flex: 1;
      flex-direction: column;
      height: 140px;
      padding: 20px;
      justify-content: space-between;
      .top {
        // 限制显示三行名字
        display: -webkit-box;
        overflow: hidden;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
      }
      .bottom {
        display: flex;
        justify-content: space-between;
      }
      .number {
        .text {
          text-align: center;
        }
        display: flex;
        input {
          width: 30px;
        }
        button {
          width: 20px;
        }
      }
    }
  }
}
</style>

shopfoot
<template>
  <div>
    <div class="footer">
      <div>
        <input
          class="check"
          v-model="isAll"
          type="checkbox"
          name=""
          id=""
        />全选
      </div>
      <div class="total">合计¥{{ totalPrice }}</div>
      <button>结算{{ closePrice }}</button>
    </div>
  </div>
</template>

<script>
export default {
  props: ["list"],
  computed: {
    //计算属性实现单选控制全选,全选控制单选
    isAll: {
      //别人控制自己就是get
      get() {
        //单选控制全选
        return this.list.every((item) => item.goods_state);
      },
      //自己控制别人就是set
      set(isChecked) {
        //遍历数组,全选控制单选
        this.list.forEach((element) => {
          element.goods_state = isChecked;
        });
      },
    },
    //合计
    totalPrice() {
      let res = 0;
      //遍历数组查看那些商品处于被选中状态
      this.list.forEach((element) => {
        if (element.goods_state) {
          //当商品处于被选中的时候,他的数量乘于价格
          res += element.goods_count * element.goods_price;
        }
      });
      return res;
    },
    //结算
    closePrice() {
      let res = 0;
      //遍历一遍看看数组那些商品处于被选中状态
      this.list.forEach((element) => {
        if (element.goods_state) {
          //当商品处于被选中的时候,把选择的商品加数量赋予结算按钮
          res += element.goods_count;
        }
      });
      return res;
    },
  },
  data() {
    return {};
  },
  created() {},
  mounted() {},
  methods: {},
};
</script>

<style scoped lang="less">
.footer {
  width: 100%;
  display: flex;
  align-items: center;
  padding: 10px 20px;
  justify-content: space-between;
  border-top: 2px solid #eee;
  background-color: #fff;
  .check {
    margin-right: 5px;
  }
  button {
    border: none;
    background: #1d7bff;
    color: #fff;
    line-height: 30px;
    padding: 0 20px;
    border-radius: 15px;
  }
}
</style>

app
<template>
  <div class="bigBox">
    <div><ShopHeader /></div>
    <div class="lists">
      <ShopMiax v-for="item in list" :key="item.id" :list="item" />
    </div>

    <div><ShopFoot :list="list" /></div>
  </div>
</template>

<script>
import ShopHeader from "./components/shopHeader.vue"; //头部
import ShopMiax from "./components/shopMiax.vue"; //主要内容
import ShopFoot from "./components/shopFoot.vue"; //底部
import { getShop } from "./api/shop";
export default {
  components: {
    ShopHeader,
    ShopMiax,
    ShopFoot,
  },
  data() {
    return {
      list: [],
    };
  },
  created() {
    getShop().then((res) => {
      console.log(res);
      this.list = res.data.list;
    });
  },
};
</script>

<style lang="less">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
</style>

<style lang="less" scoped>
.bigBox {
  //设个大盒子把所有内容包裹起来,让内容单独滚动,头部盒子和底部盒子不动
  height: 100vh;
  display: flex;
  flex-direction: column;
  .lists {
    overflow: auto; //设置内容滚动
  }
}
</style>