效果展示

创建项目
- 创建项目
| npm init vite-app shopping-cart
cd shopping-cart
npm i
npm i less -D
npm i axios -S
|
将Bootstrap.css复制到/static/css下面
在index.css中设置全局样式
1 2 3 4 5
| :root {
font-size: 12px;
}
|
- 封装axios
创建api/api.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import axios from 'axios';
axios.defaults.baseURL = "https://www.escook.cn/";
export const MyRequest = axios;
在main.js中注册全局函数:
import { MyRequest } from './api/api.js';
const app = createApp(App);
app.config.globalProperties.$http = MyRequest;
app.mount('#app');
|
- 获取数据
在App.vue中加入如下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <script>
export default {
data() {
return {
goodList: []
}
},
created() {
this.getGoodList();
},
methods: {
async getGoodList() {
const { data: res } = await this.$http.get('/api/cart');
if (res.status !== 200) return alert('请求商品列表数据失败!');
this.goodList = res.list;
}
}
}
</script>
|
- 实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <template>
<div class="header-container">
<div class="header">{{ title }}</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
default: ""
}
}
}
</script>
<style>
.header {
width: 100%;
height: 45px;
line-height: 45px;
text-align: center;
color: white;
background-color: #007BFF;
position: fixed;
top: 0;
left: 0;
z-index: 999;
}
</style>
|
- 增加一个占位框,让es-goods组件往下挪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template>
...
<div class="placeholder"></div>
...
</template>
<style>
.placeholder {
height: 45px;
width: 100%;
}
</style>
|
- 在App.vue中注册组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template>
<div>
<es-header title="购物车案例"></es-header>
</div>
</template>
<script>
import EsHeader from './components/EsHeader.vue';
export default {
components: {
EsHeader
}
}
</script>
|
- 实现基本界面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| <template>
<div class="footer-container">
<div class="footer">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="fullCheck" />
<div class="custom-control-label" for="fullCheck">全选</div>
</div>
<div class="fullPrice">
<span>合计:</span>
<span class="price">¥{{ amount.toFixed(2) }}</span>
</div>
<button class="btn btn-primary" :disabled="!total">结算<span v-show="total">({{ total }})</span></button>
</div>
</div>
</template>
<script>
export default {
props: {
total: {
type: Number,
default: 0
},
amount: {
type: Number,
default: 0
}
}
}
</script>
<style lang="less" scoped>
.footer {
width: 100%;
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #efefef;
position: fixed;
bottom: 0;
left: 0;
padding: 0 10px;
z-index: 999;
.fullPrice {
.price {
color: red;
font-size: 15px;
font-weight: bold;
}
}
.btn {
min-width: 90px;
height: 38px;
border-radius: 19px;
}
}
.custom-checkbox .custom-control-label::before {
border-radius: 1.25rem;
}
</style>
|
- 增加占位框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template>
...
<div class="placeholder"></div>
...
</template>
<style>
.placeholder {
width: 100%;
height: 50px;
}
</style>
|
- 实现全选功能
- 在EsFooter.vue中增加相关逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template>
...
<input :checked="isfull" @change="onCheckboxChange" />
...
</template>
<script>
export default {
props: {
isfull: {
type: Boolean,
default: false
}
},
methods: {
onCheckboxChange(e) {
this.$emit('fullChange', e.target.checked);
}
}
}
</script>
|
一方面给checked绑定属性isfull,让全选勾选框的选中状态跟随isfull变化
另一方面是在勾选框状态变化之后,向上一级发射事件,告知上级勾选框的状态。
- 在App.vue中渲染EsFooter组件并获取勾选状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template>
<es-footer @fullChange="onFullChange"></es-footer>
</template>
<script>
import EsFooter from './components/EsFooter.vue';
export default {
components: {
EsFooter
},
methods: {
onFullChange(isfull) {
this.goodList.forEach(item => item.goods_state = isfull);
}
}
}
</script>
|
- 根据商品勾选状态来更新全选状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template>
...
<es-footer :isfull="checkFull" />
...
</template>
<script>
export default {
computed: {
checkFull() {
let l = 0;
this.goodList.forEach(item => l += item.goods_state);
return l === this.goodList.length;
}
}
}
</script>
|
- 实现amount和total的计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <template>
...
<input :amount="amount" :total="total" />
...
</template>
<script>
export default {
computed: {
amount() {
let a = 0;
this.goodList.find(item => item.goods_state).forEach(item => a+= item.goods_count * item.goods_price);
return a;
},
total() {
let t = 0;
this.goodList.find(item => item.goods_state).forEach(item => t += item.goods_count);
return t;
}
}
}
</script>
|
实现es-goods组件
- 实现界面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
| <template>
<div class="goods-container">
<div class="left custom-control custom-checkbox">
<input class="custom-control-input" :checked="state" :id="id" />
<label class="custom-control-label" :for="id"></label>
</div>
<img class="thumb" :src="img" />
<div class="right">
<div class="tit">{{ title }}</div>
<div class="info">
<div class="price">¥{{ price.toFixed(2) }}</div>
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
id: {
type: [Number, String],
required: true
},
state: {
type: Boolean,
default: false
},
img: {
type: String,
default: ""
},
title: {
type: String,
default: ""
},
price: {
type: Number,
default: 0
}
}
}
</script>
<style>
.goods-container {
display: flex;
padding: 10px 5px;
border-top: 1px solid #ccc;
.left {
margin: auto 0;
}
.thumb {
width: 100px.
height: 100px;
}
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 10px;
.tit {
text-align: justify;
}
.info {
padding-bottom: 3px;
display: flex;
justify-content: space-between;
.price {
color: red;
font-weight: bold;
}
}
}
}
.custom-checkbox .custom-control-label::before {
border-radius: 1.25rem;
}
</style>
|
预留插槽slot,用于渲染es-counter组件
- 在App.vue中向es-goods组件传递数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template>
...
<es-goods v-for="item in goodList"
:key="item.id"
:id="item.id"
:state="item.goods_state"
:img="item.goods_img"
:title="item.goods_name"
:price="item.goods_price"
</es-goods>
...
</template>
<script>
import EsGoods from './components/EsGoods.vue';
export default {
components: {
EsGoods
}
}
</script>
|
- 向外通知勾选事件
在EsGoods.vue中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template>
...
<input @change="onCheckboxChange">
...
</template>
<script>
export default {
onCheckboxChange(e) {
this.$emit('checkboxChange', {
id: this.id,
state: e.target.checked
})
}
}
</script>
|
- App.vue接收通知事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <template>
<es-goods @checkboxChange="onCheckboxChange"></es-goods>
</template>
<script>
export default {
...
methods: {
onCheckboxChange(e) {
const findResult = this.goodList.find(item => item.id === e.id);
if (findResult) findResult.goods_state = e.state;
}
}
}
</script>
|
实现es-counter组件
- 实现组件界面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <template>
<div class="counter-container">
<button class="btn btn-light btn-sm">-</button>
<input type="number" class="form-control form-control-sm ipt-num" />
<button class="btn btn-light btn-sm">+</button>
</div>
</template>
<script>
export default {
props: {
id: {
type: Number,
required: true
}
}
}
</script>
<style lang="less" scoped>
.counter-container {
display: flex;
.btn {
width: 25px;
}
.ipt-num {
width: 34px;
text-align: center;
margin: 0 4px;
}
}
</style>
|
input标签的type属性定位number,保证无法输入除数字之外的任何内容
传入id是为了方便后续向外更新数据
- 在App.vue中渲染组件,并传入数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template>
<es-goods ...>
<es-counter :num="item.goods_count" :id="item.id"></es-counter>
</es-goods>
</template>
<script>
import EsCounter from './components/EsCounter.vue';
export default {
components: {
EsCounter
}
}
</script>
|
- 在EsCounter中渲染数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template>
...
<input ... v-model.number="count" />
...
</template>
<script>
export default {
props: {
num: {
type: Number,
default: 0
}
},
data() {
return {
count: this.num
}
}
}
</script>
|
由于props中的属性是只读的,不能随意修改,因此需要在data中定义一个count来保存数据
v-model增加number过滤,保证传入的值自动转化为数字。
另外过滤器lazy用于延迟出发刷新的时机,原本每次输入都会出发刷新,使用lazy后只会在失去焦点时出发刷新。(但实际使用中不用加就已经是失去焦点时出发刷新)
- 实现按钮功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <template>
...
<button @click="onSubClick">-</button>
...
<button @click="onAddClick">+</button>
...
</template>
<script>
export default {
methods: {
onSubClick() {
this.count --;
},
onAddClick() {
this.count ++;
}
}
}
</script>
|
- 最大最小值限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <template>
...
<button :disabled="this.count <= min">-</button>
...
<button :disabled="!isNaN(max) && this.count >= max">+</button>
...
</template>
<script>
export default {
props: {
min: {
type: Number,
default: 0
},
max: {
type:Number,
default: NaN
}
}
}
</script>
|
在外部传入数据
1 2 3 4 5 6 7 8 9
| <template>
...
<es-counter ... :min=1 :max=10></es-counter>
...
</template>
|
传入属性必须带绑定号,否则会被当作字符串来处理。
- 对输入框的值进行校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <script>
export default {
watch: {
count(newVal) {
const parseResult = parseInt(newVal);
if (isNaN(parseResult) || parseResult < this.min) {
this.count = this.min;
return;
}
if(!isNaN(this.max) && parseResult > this.max) {
this.count = this.max;
return;
}
if(String(newVal).indexOf('.') !== -1) {
this.count = parseResult;
return;
}
}
}
}
</script>
|
- 向外更新数据
在EsGoods.vue中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script>
export default {
watch(newVal) {
...
this.$emit('numChange', {
id: this.id,
num: this.count
})
}
}
</script>
|
在App.vue中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template>
...
<es-counter @numChange="onNumChange" />
...
</template>
<script>
export default {
methods: {
onNumChange(e) {
const findResult = this.goodList.find(item => item.id === e.id);
if (findResult) findResult.goods_count = e.num;
}
}
}
</script>
|
参考:
Vue3.0-24.购物车案例 -
初始化项目结构
Vue3.0-25.购物车案例 -
创建并注册es-header组件
Vue3.0-26.购物车案例 -
封装es-header组件
Vue3.0-27.购物车案例 -
基于axios请求商品列表的数据
Vue3.0-28.购物车案例 -
创建并注册es-footer的组件
Vue3.0-29.购物车案例 -
了解es-footer组件的封装要求
Vue3.0-30.购物车案例 -
渲染es-footer组件的DOM结构
Vue3.0-31.购物车案例 -
封装es-footer组件的amount和total属性
Vue3.0-32.购物车案例 -
封装es-footer组件的isfull属性和fullChange事件
Vue3.0-33.购物车案例 -
创建并注册es-goods组件
Vue3.0-34.购物车案例 -
渲染es-goods的DOM结构
Vue3.0-35.购物车案例 -
封装es-goods的id属性
Vue3.0-36.购物车案例 -
封装es-goods的其他属性
Vue3.0-37.购物车案例 -
修改单个商品的勾选状态
Vue3.0-38.购物车案例 -
实现合计、结算数量、全选功能
Vue3.0-39.购物车案例 -
创建并注册es-counter组件
Vue3.0-40.购物车案例 -
渲染es-counter组件的DOM结构
Vue3.0-41.购物车案例 -
实现数值的渲染及加减的操作
Vue3.0-42.购物车案例 -
实现min最小值的处理
Vue3.0-43.购物车案例 -
处理用户在输入框填写的内容
Vue3.0-44.购物车案例 -
在es-counter组件中把数量传递给es-goods组件
Vue3.0-45.购物车案例 -
更新购物车中商品的数量
Vue3.0-46.总结