vue使用组件化思想实现一个简单的购物车页面
页面预览
项目结构
组件介绍
主页面ShopCar
- 主要负责获取商品数据、渲染数据到相应组件
- 将所有商品的选中状态传递给子组件Bottom
- 计算出已经勾选商品的总价格,将其传递给子组件Bottom
- 计算出已经勾选商品的总数量,将其传递给子组件Bottom
<template>
<div class="wrap">
<!-- 头部 -->
<Header></Header>
<!-- 商品栏 -->
<Goods v-for="item in list"
:key="item.id"
:index="item.id"
:title="item.shopDesc"
:pic="item.imgSrc"
:price="item.price"
:status="item.status"
:count="item.num"
@status-change="getNewStatus">
</Goods>
<div class="bottom">
<Bottom :checked="allState" :allPrice="amount" :total="total" @all-change="getFullStatus" ></Bottom>
</div>
</div>
</template>
<script>
import Header from '../components/Header.vue';
import Goods from '../components/Goods.vue'
import Bottom from '../components/Bottom.vue';
import bus from '../components/eventBus'
export default {
components:{
Header,Bottom,Goods
},
data(){
return{
status:false,
list:[
{id:"01", status:true ,imgSrc:'https://img.alicdn.com/imgextra/i4/14219353/O1CN0145jVEE2Ixj4WfGyOx_!!0-saturn_solar.jpg_468x468q75.jpg_.webp',shopDesc:'焕伊美宽松休闲F晒服外套女2022年新款夏季洋气百',price:'388.6',num:1},
{id:"02", status:true,imgSrc:'https://img.alicdn.com/imgextra/i1/30603673/O1CN01AjV6XY1d0HKGpaEJt_!!0-saturn_solar.jpg_468x468q75.jpg_.webp',shopDesc:'七小铺潮牌设计感拼接条纹韩版卫衣女2022年春秋',price:'379.8',num:1},
{id:"03", status:true,imgSrc:'https://img.alicdn.com/imgextra/i3/14219353/O1CN01ZKUntQ2Ixj4fJ4U1r_!!0-saturn_solar.jpg_468x468q75.jpg_.webp',shopDesc:'姿派女孩夏季镂空钩花遮晒衣女2022年新款夏装薄',price:'458.65',num:1},
{id:"04", status:true,imgSrc:'https://img.alicdn.com/imgextra/i4/30301515/O1CN01qME9In1N3ultkxD54_!!0-saturn_solar.jpg_468x468q75.jpg_.webp',shopDesc:'汐颜洋气衬衣女时尚短袖雪纺衬衫2022春季新款女',price:'418.69',num:2},
{id:"05", status:true,imgSrc:'https://img.alicdn.com/imgextra/i3/25431444/O1CN01NMvc931MXOZtzfxjN_!!0-saturn_solar.jpg_468x468q75.jpg_.webp',shopDesc:'NENW复古港味上衣韩版宽松白色衬衫女2022新款',price:'398.12',num:1}
]
}
},
computed:{
total(){
return this.list.filter(item=> item.status).reduce((total,item)=>(total += item.num),0)
},
allState(){
return this.list.every(item =>item.status);
},
amount(){
return this.list.filter(item =>item.status).reduce((total,item)=>{
return total += parseFloat(item.price) * parseInt(item.num);
},0)
}
},
methods:{
getFullStatus(e){
this.list.forEach(item=>{
item.status = e;
})
},
getNewStatus(e){
this.list.some(item =>{
if(item.id == e.id){
item.status = e.value;
}
})
}
},
created(){
bus.$on('share',(val)=>{
this.list.some(item =>{
if(item.id === val.id){
item.num = val.value;
return true
}
})
})
}
}
</script>
<style lang="scss" scoped>
.wrap{
text-align: left;
}
</style>
Header组件
<template>
<div class="wrap">
<div class="text">
<span>购物车</span>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
.wrap{
.text{
width: 750px;
height: 48px;
background-color: rgb(12, 144, 239);
text-align: center;
margin: 0 auto;
span{
color: #fff;
font-size: 18px;
line-height: 48px;
font-weight: bold;
}
}
}
</style>
Goods组件
- 主要负责获取父组件ShopCar的数据,并动态渲染出来
- 勾选状态发生变化时通过自定义事件向父组件传值
<template>
<div class="wrap">
<div class="goods-wrap">
<!-- 左侧多选框 -->
<div class="check">
<input type="checkbox" :id="index" :checked="status" @change="stastusChange">
<label for="id"></label>
</div>
<!-- 右侧商品信息 -->
<div class="goods-contain">
<!-- 图片容器 -->
<div class="img-wrap">
<a href=""><img :src="pic" alt=""></a>
</div>
<!-- 商品描述 -->
<div class="shop-desc-wrap">
<!-- 商品描述 -->
<p class="desc">{{title}}</p>
<!-- 价格 -->
<p class="price">¥{{price}}</p>
<div class="button">
<Count :shopnum="count" :id="index"></Count>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Count from './Count.vue';
export default {
components:{
Count,
},
props:{
count:{
type:Number,
default:1,
},
index:{
require:true,
type:String
},
title:{
default:'',
type:String,
},
pic:{
default:'',
type:String,
},
price:{
default:'0',
type:String,
},
status:{
default:true,
type:Boolean
}
},
data(){
return{}
},
methods:{
stastusChange(e){
this.$emit('status-change',{id:e.target.id,value:e.target.checked})
}
}
}
</script>
<style lang="scss" scoped>
.wrap{
.goods-wrap{
border-radius: 8px;
display: flex;
margin: 0 auto;
width: 750px;
height: 200px;
margin: 8px auto;
background-color: rgb(236, 230, 230);
.check{
width: calc(750px - 95%);
line-height: 200px;
text-align: center;
}
.goods-contain{
width: 95%;
height: 100%;
display: flex;
.img-wrap{
width: 200px;
height: 100%;
display: flex;
align-items: center;
a{
width: 200px;
height: 200px;
overflow: hidden;
}
img{
width: 200px;
height: 200px;
}
}
.shop-desc-wrap{
margin: 18px 0 0 12px;
flex-grow: 1;
position: relative;
.desc{
font-size: 16px;
color: #333;
}
.price{
margin-top: 12px;
color: #f00;
font-size: 36px;
font-weight: bold;
}
.button{
position: absolute;
right: 22px;
bottom: 22px;
}
}
}
}
}
</style>
Count组件
<template>
<div class="button">
<button @click="sub">-</button><span v-text="shopnum"></span><button @click="add">+</button>
</div>
</template>
<script>
import bus from './eventBus';
export default {
props:{
shopnum:{
type:Number,
default:1,
},
id:{
require:true,
type:String
}
},
data(){
return{
}
},
methods:{
add(){
const obj = {id:this.id,value:this.shopnum +1}
bus.$emit('share',obj);
},
sub(){
if(this.shopnum -1 == 0){
return
}
const obj = {id:this.id,value:this.shopnum -1}
bus.$emit('share',obj);
}
}
}
</script>
<style lang="scss" scoped>
.button{
display: flex;
button{
cursor: pointer;
width: 34px;
height: 30px;
margin: 0 6px;
font-size: 16px;
background-color: transparent;
border: none;
}
span{
display: inline-block;
height: 30px;
width: 32px;
line-height: 30px;
text-align: center;
background-color: #fff;
}
button:hover{
background-color: rgb(227, 105, 105);
}
button:active{
border: 1px solid #333;
background-color: rgb(227, 105, 105);
}
}
</style>
Bottom组件
- 将选中商品的价格与数量动态计算后展示出来
- 将全选的checked值传递给父元素
<template>
<div class="wrap">
<!-- 全选 -->
<div class="checkAll">
<input type="checkbox" id="cheakAll" :checked="checked" @change="allchange">
<label for="cheakAll">全选</label>
</div>
<!-- 价格 -->
<div class="price">
<span>合计:</span>
<span>¥{{allPrice.toFixed(2)}}</span>
</div>
<!-- 结算按钮 -->
<div class="submit">
<span>结算({{total}})</span>
</div>
</div>
</template>
<script>
export default {
props:{
total:{
type:Number,
default:0,
},
allPrice:{
type:Number,
default:0
},
checked:{
type:Boolean,
default:true
}
},
data(){
return{
}
},
methods:{
allchange(e){
this.$emit('all-change',e.target.checked)
}
}
}
</script>
<style lang="scss" scoped>
.wrap{
width: 750px;
height: 58px;
margin: 0 auto;
background-color: rgb(206, 208, 210);
display: flex;
justify-content: space-around;
align-items: center;
font-size: 18px;
font-weight: bold;
.checkAll{
input{
width: 16px;
height: 16px;
border-radius: 50%;
}
label{
text-align: middle;
}
}
.price{
span:last-child{
color: red;
font-size: 26px;
}
}
.submit{
width:120px;
height: 48px;
border-radius: 12px;
text-align: center;
line-height: 48px;
background-color: rgb(28, 142, 187);
cursor: pointer;
}
.submit:hover{
background-color: rgb(11, 186, 255);
}
.submit:active{
border: 1px solid rgb(0, 0, 0);
}
}
</style>
eventBus.js
- 里面就是一个vue示例,用于兄弟组件或者祖孙组件之间的通信
- 数据发送方通过调用$emit()方法想接收方发送数据
- 数据接收方通过调用$on()方法接收数据