HMVue5.2【vue基础综合案例-购物车】

 1 案例效果 & 实现步骤、初始项目

1-1 课件

 

1-2 初始项目装包&运行

 

 

npm install

npm audit fix

npm run serve

http://localhost:8080/

2 导入、注册、使用Header组件

<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>
View Code

 

 

 

 

 

 

3 基于axios请求列表数据

3-1 实现步骤

3-2 初步实现

 

 

 

 

 

 

3-3 请求回来的数据需要转存到data中

 

 

4 循环渲染商品信息

4-1 初步实现

<template>
  <div class="goods-container">
    <!-- 左侧图片 -->
    <div class="thumb">
      <div class="custom-control custom-checkbox">
        <!-- 复选框 -->
        <input type="checkbox" class="custom-control-input" id="cb1" :checked="true" />
        <label class="custom-control-label" for="cb1">
          <!-- 商品的缩略图 -->
          <img src="../../assets/logo.png" alt="" />
        </label>
      </div>
    </div>
    <!-- 右侧信息区域 -->
    <div class="goods-info">
      <!-- 商品标题 -->
      <h6 class="goods-title">商品名称商品名称商品名称商品名称</h6>
      <div class="goods-info-bottom">
        <!-- 商品价格 -->
        <span class="goods-price">¥0</span>
        <!-- 商品的数量 -->
      </div>
    </div>
  </div>
</template>

<script>
export default {}
</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>
View Code

 

 

 

 

 

 

 

4-2 为Goods组件封装商品的相关信息

 

 

 父组件向子组件传值---自定义属性  (props、v-bind、{{}})

 

 

 

 

 

 

 

 

 

 

 

 4-3 props两种封装方案的优缺点对比分析

 

 

 

 

 

 

 

 

 

 方案1好

 

 

 

 

 

 

5 修改商品的勾选状态

5-1 问题分析

 

 

 

 

 

 

 

 

 

解决方案:子组件向父组件传值---自定义事件

 

 

5-2 功能实现

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

6 Footer组件使用 & 全选功能

6-1 导入、注册、使用Footer组件

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

    <!-- 中间的合计 -->
    <div>
      <span>合计:</span>
      <span class="total-price">¥{{ 0 }}</span>
    </div>

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

<script>
export default {}
</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>
View Code

 

 

 

 

6-2 全选功能

计算属性、父向子自定义属性传值、子向父自定义事件传值

 

 

 

 

 实现了所有商品被勾选后,全选框会自动被勾选;但此时,还没实现全选框控制每件商品的勾选状态

 

 

 

 

 

 

 

 

 

 

 

 

至此,功能完整实现

 

 

 

6 计算商品的总价格

在父组件App中通过计算属性计算出总价格

通过自定义属性将总价格传递给子组件Footer

 

 

 

 

 

 

 

 

 

 

 

 

7 Counter组件

 

 

7-1 导入、注册、使用Counter组件

<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">1</span>
    <!-- 加 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm">+</button>
  </div>
</template>

<script>
export default {}
</script>

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

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

 

 

 

 

7-2 把实际购买数量传给Counter

 

 

 

 

 

 

 

 

 

 

 

7-3 组件Count向组件App发送数据(实现商品数量的+-)

Counter-->Goods-->App,一层一层往外传值可以实现,但是太麻烦,推荐以下方案:

利用EventBus实现:孙子组件向爷爷组件传值(EventBus不仅仅可以实现兄弟组件间的传值,还可以实现嵌套较深的组件与外层组件的传值)

 

 

 

 

 

7-5 动态计算已勾选商品的总数量

计算属性、父传子

 

 

 

 

 

8 源码

App.vue

<template>
<div class="app-container">
    <!-- 3、使用组件 -->
    <Header title="购物车案例"></Header>
    <!-- 循环渲染每一个商品的信息(要循环谁,给谁加v-for) -->
    <Goods v-for="item in list" :key="item.id" 
            :id="item.id"
            :title="item.goods_name" 
            :img="item.goods_img"
            :price="item.goods_price"
            :state="item.goods_state"
            :count="item.goods_count"
            @state-change="getNewState"> 
            <!-- @state-change绑定子组件Goods的自定义事件state-change -->
    </Goods>
    <Footer 
        :isfull="fullState" 
        :amount="totalPrice" 
        :allNum="totalNum"
        @full-change="getFullState">
    </Footer>
    <!--
        子组件Footer的自定义属性isfull、amount用来实现父向子传值 
        子组件Footer的自定义事件full-change用来实现子向父传值 
        编码规范:先指令,再绑定,后事件
    -->
</div>
</template>



<script>
import bus from '@/components/eventBus.js'
//导入axios请求库
import axios from 'axios'
//1、导入组件(注意:自己封装的组件,在导入命名时建议首字母大写)
import Header from '@/components/Header/Header.vue'
import Goods from '@/components/Goods/Goods.vue'
import Footer from '@/components/Footer/Footer.vue'

export default {
    data(){
        return{
            //用来存储购物车的列表数据,默认为空数组
            list: [],
        }
    },
    //计算属性(定义时是方法格式,使用时当属性使用,实质就是属性)
    computed: { 
        //动态计算出全选的状态是true还是false
        fullState(){
            // return this.list.every(item => item.goods_state===true) 
            /*
            list.every()返回布尔值,
            当list的每一项item的goods_state都为true是才返回true,
            否则(只要有一个item不满足)返回false
            此处,item.goods_state===true可以简写为item.goods_state
            */
            return this.list.every(item => item.goods_state) 
        },
        // 已勾选商品的总价格计算
        totalPrice(){
            // 1. 先 filter 过滤  2. 再 reduce 累加
            /*
            const result = this.list
                            .filter(item => item.goods_state===true)
                            .reduce((sum, item) => {
                                    return sum += item.goods_price*item.goods_count
                            },0)
            return result
            */
            //简写
            return this.list
                .filter(item => item.goods_state)
                .reduce((sum, item) => (sum+=item.goods_price*item.goods_count), 0)
        },
        // 已勾选商品的总数量
        totalNum(){
            return this.list
                .filter(item => item.goods_state)
                .reduce((t, item) => (t += item.goods_count), 0)
        }
    },
    created(){
        //调用请求列表数据的方法
        this.initCartList()

        //通过EventBus接收孙子组件Counter传来的封装对象
        bus.$on('share', val=>{
            this.list.some(item => {
                if(item.id === val.id){
                    item.goods_count = val.value
                    return true
                }
            })
        })
    },
    methods: {
        //封装请求列表数据的方法
        async initCartList(){
            // 调用 axios 的 get 方法,请求列表数据
            const {data:res} = await axios.get('https://www.escook.cn/api/cart')
            console.log(res)
            // 只要请求回来的数据,在页面渲染期间要用到,则必须转存到 data 中
            if(res.status === 200){ //状态码200表示请求成功
                this.list = res.list
            }
        },
        //接收子组件传递过来的数据并修改父组件中对应商品的勾选状态值
        getNewState(e){ //e 的格式为 { id, value }
            // console.log(e) //商品1默认被勾选,点击复选框取消勾选--->打印{id: 1, value: false}
            this.list.some(item => {
                if(item.id === e.id){
                    item.goods_state = e.value
                    return true //查找到对应商品后,终止后续的循环
                }
            })
        },
        // 接收 Footer 子组件传递过来的全选按钮的状态
        getFullState(e){
            this.list.forEach(item => item.goods_state=e)
        }
    },
    //2、注册组件
    components: {
        Header, Goods, Footer
    }
}
</script>



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

 

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>
View Code

 

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" />  
                <!--建议此处checked不要用v-model,因为props只读; @change事件是vue中input自带的事件-->
            <label class="custom-control-label" :for="'cb'+id">
                <!-- 商品的缩略图 -->
                <img :src="img" alt="" /> <!--属性不能用{{}}插值,得用v-bind:绑定-->
            </label>
        </div>
    </div>

    <!-- 右侧信息区域 -->
    <div class="goods-info">
        <!-- 商品标题 -->
        <h6 class="goods-title">{{title}}</h6>
        <div class="goods-info-bottom">
            <!-- 商品价格 -->
            <span class="goods-price">¥{{price}}</span>
            <!-- 商品的数量:组件Counter -->
            <Counter :id="id" :num="count"></Counter>
        </div>
    </div>

</div>
</template>



<script>
import Counter from '@/components/Counter/Counter.vue'

export default {
    props: {
        //商品ID
        /*为啥在这里要封装一个 id 属性呢?
        原因:期望子组件中商品的勾选状态变化之后, 需要通过子 -> 父的形式,通知父组件根据 id 修改对应商品的勾选状态。
        */
        id: {
            required: true, //因为ID是修改商品状态必须的,所以此处设为必需,即不传ID会报错
            type: Number
        },
        //商品的标题
        title: {
            dafault: '',
            type: String
        },
        //商品的图片
        img: {
            dafault: '',
            type: String
        },
        //商品的单价
        price: {
            dafault: 0,
            type: Number
        },
        //商品的复选框勾选状态
        state: {
            default: true, //默认值要设置为true勾选状态,因为期望顾客多多购买
            type: Boolean
        },
        //商品的数量
        count: {
            default: 1,
            type: Number
        }
    },
    methods: {
        // 只要复选框的选中状态发生了变化,就会调用这个处理函数
        stateChange(e){
            /*
            console.log('ok')
            console.log(e)*/
            const newState = e.target.checked
            /*
            console.log(newState)
            console.log(this) //this.id就是Goods组件当前商品的id,需要传递给父组件App*/
            //触发自定义事件state-change
            this.$emit('state-change', {id:this.id, value:newState})
        }
    },
    components: {
        Counter
    }
}
</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>
View Code

 

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> <!--.toFied(n):n位小数-->
        </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){
            this.$emit('full-change', e.target.checked) //自定义事件名称:full-change
        }
    }
}
</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>
View Code

 

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" @click="sub">-</button>
    <!-- 购买的数量 -->
    <span class="number-box">{{num}}</span>
    <!-- 加 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="add">+</button>

</div>
</template>



<script>
import bus from '@/components/eventBus.js'

export default {
    props: {
        //接收商品的 id 值
        //期望使用 EventBus 方案,把数量传递到 App.vue 的时候,需要通知 App 组件,更新哪个商品的数量
        id: {
            required: true,
            type: Number
        },
        // 接收到的商品数量
        num: {
            default: 1,
            type: Number
        }
    },
    methods: {
        //要发送给 App 的数据格式为 { id, value };其中id是商品的 id,value 是商品最新的购买数量
        add(){
            const obj = {id:this.id, value:this.num+1}  //num是props,this.num+1并没有修改num的原值,所以没问题
            bus.$emit('share', obj)
        },
        sub(){
            if(this.num-1 === 0) return
            const obj = {id:this.id, value:this.num-1}
            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>
View Code

 

posted @ 2021-11-22 21:06  yub4by  阅读(57)  评论(0)    收藏  举报