1.案例效果
2.实现步骤
①初始化项目基本结构
②封装EsHeader组件
2.1创建并注册EsHeader组件
2.2封装es-header组件
③基于axios请求商品列表数据(GET请求,地址为https://www.escook.cn/api/cart)
④封装EsFooter组件
4.1创建并注册EsFooter组件
4.2封装es-footer组件
4.2.0封装需求
4.2.1渲染组件的基础布局
4.2.2封装自定义属性 amount
4.2.3封装自定义属性 total
4.2.4封装自定义属性 isfull
4.2.5封装自定义事件 fullChange
⑤封装EsGoods组件
5.1创建并注册EsGoods组件
5.2封装es-goods组件
5.2.0封装需求
5.2.1渲染组件的基础布局
5.2.2封装自定义属性 id
5.2.3封装其他属性
5.2.4封装自定义事件 stateChange
⑥封装EsCounter组件
6.1动态统计已勾选商品的总价格
6.2动态统计已勾选商品的总数量
6.3实现全选功能
⑦封装es-counter组件
main.js
import { createApp } from 'vue'
import App from './App.vue'
import './assets/bootstrap-3.4.1-dist/css/bootstrap.css'
import './index.css'
//导入axios
import axios from 'axios'
const app = createApp(App);
//配置请求的根路径
axios.defaults.baseURL = 'https://applet-base-api-t.itheima.net';
//将axios挂栽为全局的$http自定义属性
app.config.globalProperties.$http = axios
app.mount('#app')
App.vue
<template>
<div class="app-container">
<es-header title="购物车案例"></es-header>
<EsGoods
v-for="item in goodslist"
:key="item.goods_id"
:id="item.goods_id"
:thumb="item.goods_img"
:title="item.goods_name"
:price="item.goods_price"
:count="item.goods_count"
:checked="item.goods_state"
@stateChange="onGoodsStateChange"
@countChange="onGoodsCountChange"
></EsGoods>
<EsFooter
@fullChange="onFullStateChange"
:amount="amount"
:total="total"
></EsFooter>
</div>
</template>
<script>
import EsHeader from "./components/es-header/EsHeader.vue";
import EsFooter from "./components/es-footer/EsFoote.vue";
import EsGoods from "./components/es-goods/EsGoods.vue";
export default {
name: "MyApp",
components: { EsHeader, EsFooter, EsGoods },
data() {
return {
//商品列表数据
goodslist: [],
};
},
created() {
this.getGoodsList();
},
methods: {
//获取商品列表数据的方法
async getGoodsList() {
//1.通过组件实例this访问到全局挂栽的$http属性,并发起Ajax数据请求
const { data: res } = await this.$http.get("/api/cart");
if (res.status !== 200) return alert("数据请求失败!");
this.goodslist = res.list;
console.log(this.goodslist);
},
//监听选中状态变化的事件
onFullStateChange(isFull) {
this.goodslist.forEach((x) => (x.goods_state = isFull));
},
onGoodsStateChange(e) {
const findResult = this.goodslist.find((x) => x.goods_id === e.id);
if (findResult) {
findResult.goods_state = e.value;
}
},
//监听商品数量变化的事件
onGoodsCountChange(e) {
const findResult = this.goodslist.find((x) => x.goods_id === e.id);
if (findResult) {
findResult.goods_count = e.value;
}
},
},
computed: {
//已勾选商品的总价
amount() {
//1.定义商品总价格
let a = 0;
//2.循环累加商品总价格
this.goodslist
.filter((x) => x.goods_state)
.forEach((x) => {
a += x.goods_price * x.goods_count;
});
//3.返回累加的结果
return a;
},
//已勾选商品的总数量
total() {
let t = 0;
this.goodslist
.filter((x) => x.goods_state)
.forEach((x) => {
t += x.goods_count;
});
return t;
},
},
};
</script>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
}
</style>
EsHeader.vue
<template>
<div
:style="{ backgroundColor: bgcolor, color: color, fontSize: fsize + 'px' }"
class="header-container"
>
{{ title }}
</div>
</template>
<script>
export default {
name: "EsHeader",
props: {
title: {
type: String,
default: "es-header",
},
bgcolor: {
type: String,
default: "#007BFF",
},
color: {
type: String,
default: "#ffffff",
},
fsize: {
type: Number,
default: 12,
},
},
};
</script>
<style lang="less" scoped>
.header-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 45px;
text-align: center;
line-height: 45px;
z-index: 999;
}
</style>
EsFooter.vue
<template>
<div class="footer-container">
<!-- 全选区域 -->
<div class="custom-control custom-checkbox">
<input
type="checkbox"
class="custom-control-input aaa"
id="fullCheck"
:checked="isfull"
@change="onCheckBoxChange"
/>
<label class="custom-control-label" for="fullCheck">全选</label>
</div>
<!-- 合计区域 -->
<div>
<span>合计:</span>
<span class="amount">¥{{ amount.toFixed(2) }}</span>
</div>
<!-- 结算按钮 -->
<button
type="button"
class="btn btn-primary btn-settle"
:disabled="total === 0"
>
结算({{ total }})
</button>
</div>
</template>
<script>
export default {
name: "EsFooter",
emits: ["fullChange"],
props: {
//以勾选商品的总价格
amount: {
type: Number,
default: 0,
},
//已勾选商品的总数量
total: {
type: Number,
default: 0,
},
//全选按钮的选中状态
isfull: {
type: Boolean,
default: false,
},
},
methods: {
onCheckBoxChange(e) {
this.$emit("fullChange", e.target.checked);
},
},
};
</script>
<style lang="less" scoped>
.footer-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
align-items: center;
background-color: #fff;
border-top: 1px solid #efefef;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.amount {
font-weight: bold;
color: #f10404;
}
.btn-settle {
min-width: 90px;
height: 38px;
border-radius: 19px;
}
</style>
EsGoods.vue
<template>
<div>
<div class="goods-container">
<div class="left">
<div class="custom-control custom-checkbox">
<input
type="checkbox"
class="custom-control-input"
:id="id"
:checked="checked"
@change="onCheckBoxChange"
/>
<label class="custom-control-label" :for="id">
<img :src="thumb" alt="商品图片" class="thumb"
/></label>
</div>
</div>
<!-- 右侧信息区域 -->
<div class="right">
<!-- 商品名称 -->
<div class="top">{{ title }}</div>
<div class="bottom">
<!-- 商品价格 -->
<div class="price">¥{{ price.toFixed(2) }}</div>
<!-- 商品数量 -->
<div class="count">
<EsCounter :num="count" :min="1" @numChange="getNumbe"></EsCounter>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import EsCounter from "../es-counter/EsCounter.vue";
export default {
name: "EsGoods",
props: {
//商品的id
id: {
type: [String, Number],
required: true,
},
//商品的缩略图
thumb: {
type: String,
required: true,
},
//商品的名称
title: {
type: String,
required: true,
},
//单价
price: {
type: Number,
required: true,
},
//数量
count: {
type: Number,
required: true,
},
//勾选的状态
checked: {
type: Boolean,
required: true,
},
},
emits: ["stateChange", "countChange"],
methods: {
onCheckBoxChange(e) {
this.$emit("stateChange", {
id: this.id,
value: e.target.checked,
});
},
//监听数量值的变化,
getNumbe(num) {
this.$emit("countChange", {
id: this.id,
value: num,
});
},
},
components: {
EsCounter,
},
};
</script>
<style lang="less" scoped>
.goods-container {
border-bottom: 1px solid #efefef;
display: flex;
padding: 10px;
.left {
margin-right: 10px;
//商品图片
.thumb {
display: block;
width: 100px;
height: 100px;
background-color: #efefef;
}
}
//右侧商品名称、单价、数量的样式
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
.top {
font-weight: bold;
}
.bottom {
display: flex;
justify-content: space-between;
align-items: center;
.price {
color: red;
font-weight: bold;
}
}
}
}
</style>
EsCounter.vue
<template>
<div class="counter-container">
<button type="button" class="btn btn-light btn-sm" @click="onSubClick">
-
</button>
<input
type="number"
class="form-control form-control-sm ipt-num"
v-model.number.lazy="number"
/>
<button type="button" class="btn btn-light btn-sm" @click="onAddClick">
+
</button>
</div>
</template>
<script>
export default {
name: "EsCounter",
data() {
return {
number: this.num,
};
},
watch: {
number(newVal) {
const parseResult = parseInt(newVal);
if (isNaN(parseResult) || parseResult < 1) {
this.number = 1;
return;
}
if (String(newVal).indexOf(".") != -1) {
this.number = parseResult;
return;
}
//触发自定义事件,把最新的number数值传递给组件的使用者
this.$emit("numChange", this.number);
},
},
emits: ["numChange"],
props: {
num: {
type: Number,
default: 0,
},
min: {
type: Number,
//min属性的值默认为NaN,表示不限制最小值
default: NaN,
},
},
methods: {
onSubClick() {
if (!isNaN(this.min) && this.number - 1 < this.min) return;
this.number--;
},
onAddClick() {
this.number++;
},
},
};
</script>
<style lang="less" scoped>
.counter-container {
display: flex;
.btn {
width: 25px;
}
//输入框的样式
.ipt-num {
width: 34px;
text-align: center;
margin: 0 4px;
}
}
</style>