03 计算属性和侦听器
计算属性
- 基于 现有的数据,计算出来的 新属性。
- 依赖 的数据变化,自动 重新计算。
语法
- 声明在 computed 配置项中,一个计算属性对应一个函数
- 使用起来和普通属性一样使用
注意
computed配置项和data配置项是 同级 的computed中的计算属性 虽然是函数的写法,但他 依然是个属性computed中的计算属性 不能 和data中的属性 同名- 使用
computed中的计算属性和使用data中的属性是一样的用法 computed中计算属性内部的 this 依然 指向的是 Vue 实例
案例 小黑的礼物清单

html
<div id="app">
<h3>小黑的礼物清单</h3>
<table>
<tr>
<th>名字</th>
<th>数量</th>
</tr>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.num }}个</td>
</tr>
</table>
<!-- 目标:统计求和,求得礼物总数 -->
<p>礼物总数:? 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
// 现有的数据
list: [
{ id: 1, name: "篮球", num: 1 },
{ id: 2, name: "玩具", num: 2 },
{ id: 3, name: "铅笔", num: 5 },
],
},
});
</script>html
<div id="app">
<h3>小黑的礼物清单</h3>
<table>
<tr>
<th>名字</th>
<th>数量</th>
</tr>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.num }}个</td>
</tr>
</table>
<!-- 目标:统计求和,求得礼物总数 -->
<p>礼物总数:{{totalCount}} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
// 现有的数据
list: [
{ id: 1, name: "篮球", num: 1 },
{ id: 2, name: "玩具", num: 2 },
{ id: 3, name: "铅笔", num: 6 },
],
},
computed: {
totalCount() {
let sum = this.list.reduce((sum, item) => sum + item.num, 0)
return sum
}
}
});
</script>computed VS methods
computed计算属性 VSmethods方法
computed 计算属性
作用:封装了一段对于数据的处理,求得一个结果
语法:
- 写在
computed配置项中 - 作为属性,直接使用
- js 中使用计算属性:
this.计算属性 - 模板中使用计算属性:
- js 中使用计算属性:
- 写在
computed 计算属性
html
<div id="app">
<h3>小黑的礼物清单🛒<span>{{ totalCount }}</span></h3>
<table>
<tr>
<th>名字</th>
<th>数量</th>
</tr>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.num }} 个</td>
</tr>
</table>
<p>礼物总数:{{ totalCount }} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, name: "篮球", num: 3 },
{ id: 2, name: "玩具", num: 2 },
{ id: 3, name: "铅笔", num: 5 },
],
},
computed: {
// 计算属性:有缓存的,一旦计算出来结果,就会立刻缓存
// 下一次读取 → 直接读缓存就行 → 性能特别高
totalCount() {
console.log('计算属性执行了')
let total = this.list.reduce((sum, item) => sum + item.num, 0)
return total
}
}
});
</script>methods 方法
作用:给 Vue 实例提供一个方法,调用以处理业务逻辑。
语法:
写在
methods配置项中作为方法调用
- js 中调用:
this.方法名() - 模板中调用
{{方法名 ()}}或者@事件名="方法名"
- js 中调用:
methods 方法
html
<div id="app">
<h3>小黑的礼物清单🛒<span>{{ totalCount() }}</span></h3>
<table>
<tr>
<th>名字</th>
<th>数量</th>
</tr>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.num }} 个</td>
</tr>
</table>
<p>礼物总数:{{ totalCount() }} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, name: "篮球", num: 3 },
{ id: 2, name: "玩具", num: 2 },
{ id: 3, name: "铅笔", num: 5 },
],
},
methods: {
totalCount() {
console.log('methods 方法执行了')
let sum = this.list.reduce((sum, item) => sum + item.num, 0);
return sum;
},
},
});
</script>计算属性的优势
缓存特性(提升性能)
- 计算属性会对计算出来的结果缓存,再次使用直接读取缓存,
- 依赖项变化了,会自动重新计算 → 并再次缓存
methods没有缓存特性通过代码比较
计算属性和方法
html
<div id="app">
<h3>小黑的礼物清单🛒<span>{{ totalCountMethods() }}</span></h3>
<h3>小黑的礼物清单🛒<span>{{ totalCountMethods() }}</span></h3>
<h3>小黑的礼物清单🛒<span>{{ totalCountMethods() }}</span></h3>
<!-- totalCountMethods() 是方法,因为是方法,所以要加括号 -->
<!-- 方法没有缓存,所以每次执行都会重新计算 -->
<table>
<tr>
<th>名字</th>
<th>数量</th>
</tr>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.num }} 个</td>
</tr>
</table>
<p>礼物总数:{{ totalCountComputed }} 个</p>
<p>礼物总数:{{ totalCountComputed }} 个</p>
<p>礼物总数:{{ totalCountComputed }} 个</p>
<!-- totalCountComputed 是计算属性,因为是计算属性,所以不需要加括号 -->
<!-- 计算属性有缓存,所以只会执行一次 -->
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, name: "篮球", num: 3 },
{ id: 2, name: "玩具", num: 2 },
{ id: 3, name: "铅笔", num: 5 },
],
},
methods: {
totalCountMethods() {
console.log('methods 方法执行了')
let sum = this.list.reduce((sum, item) => sum + item.num, 0);
return sum;
},
},
computed: {
totalCountComputed() {
console.log('computed 计算属性执行了')
let sum = this.list.reduce((sum, item) => sum + item.num, 0);
return sum;
},
},
});
</script>总结
computed有缓存特性,methods没有缓存- 当一个结果依赖其他多个值时,推荐使用计算属性
- 当处理业务逻辑时,推荐使用
methods方法,比如事件的处理函数
计算属性的完整写法
既然计算属性也是属性,能访问,应该也能修改了?
- 计算属性默认的简写,只能读取访问,不能 "修改"
- 如果要 "修改" → 需要写计算属性的完整写法
js
computed: {
计算属性名 () {
一段代码逻辑 ()
return 结果
}
}js
computed: {
计算属性名: {
get() {
一段代码逻辑 (计算逻辑)
return 结果
},
set (修改的值) {
一段代码逻辑 (修改逻辑)
}
}
}案例 改名卡
改名卡
html
<div id="app">
姓:<input type="text" /><br />
名:<input type="text" /><br />
<p>姓名:</p>
<button>修改姓名</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {},
computed: {},
methods: {},
});
</script>html
<div id="app">
姓:<input type="text" placeholder="请输入姓" v-model="lastName" /><br />
名:<input type="text" placeholder="请输入名" v-model="firstName" /><br />
<p>读取姓名:<span>{{fullName}}</span></p>
<button @click="changeName">设置姓名</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
lastName: "",
firstName: "",
},
computed: {
// fullName() {
// return this.lastName + this.firstName;
// },
fullName: {
get() {
return this.lastName + this.firstName;
},
set(value) {
this.lastName = value.substr(0, 1);
this.firstName = value.substr(1);
},
},
},
methods: {
changeName() {
// this.lastName = "冯";
// this.firstName = "小刚";
this.fullName = '冯小刚'
},
},
});
</script>案例 成绩案例

功能描述
- 渲染功能
- 删除功能
- 添加功能
- 统计总分,求平均分
思路分析
渲染功能
v-for:keyv-bind:动态绑定 class 的样式v-ifv-else判断是否有数据,没有数据显示提示信息
删除功能
v-on绑定事件,阻止 a 标签的默认行为@click.prevent- 获取当前行的索引,使用
splice删除对应的项 (索引,1) - 或者使用
filter过滤出不等于当前行索引的项
添加功能
v-model绑定数据- 使用
.trim、.number判断数据是否为空后 再添加、添加后清空文本框的数据 - 使用
push修改数组(添加到数组尾部)更新视图 - 或者使用
unshift修改数组(添加到数组头部)更新视图
统计总分,求平均分
- 使用计算属性
computed计算总分和平均分 reduce累加求和toFixed保留两位小数
- 使用计算属性
html
<div id="app" class="score-case">
<div class="table">
<table>
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>语文</td>
<td class="red">46</td>
<td><a href="#">删除</a></td>
</tr>
<tr>
<td>2</td>
<td>英语</td>
<td>80</td>
<td><a href="#">删除</a></td>
</tr>
<tr>
<td>3</td>
<td>数学</td>
<td>100</td>
<td><a href="#">删除</a></td>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="5">
<span class="none">暂无数据</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<span>总分:246</span>
<span style="margin-left: 50px">平均分:79</span>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="form">
<div class="form-item">
<div class="label">科目:</div>
<div class="input">
<input type="text" placeholder="请输入科目" />
</div>
</div>
<div class="form-item">
<div class="label">分数:</div>
<div class="input">
<input type="text" placeholder="请输入分数" />
</div>
</div>
<div class="form-item">
<div class="label"></div>
<div class="input">
<button class="submit">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, subject: "语文", score: 20 },
{ id: 7, subject: "数学", score: 99 },
{ id: 12, subject: "英语", score: 70 },
],
subject: "",
score: "",
},
});
</script>html
<div id="app" class="score-case">
<div class="table">
<table>
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>操作</th>
</tr>
</thead>
<tbody v-if="list.length > 0">
<tr v-for="(item, index) in list" :key="item.id">
<td>{{index+1}}</td>
<td>{{item.subject}}</td>
<td :class="{red: item.score < 60}">{{item.score}}</td>
<td><a href="#">删除</a></td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="5">
<span class="none">暂无数据</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<span>总分:246</span>
<span style="margin-left: 50px">平均分:79</span>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="form">
<div class="form-item">
<div class="label">科目:</div>
<div class="input">
<input type="text" placeholder="请输入科目" />
</div>
</div>
<div class="form-item">
<div class="label">分数:</div>
<div class="input">
<input type="text" placeholder="请输入分数" />
</div>
</div>
<div class="form-item">
<div class="label"></div>
<div class="input">
<button class="submit">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, subject: "语文", score: 20 },
{ id: 7, subject: "数学", score: 99 },
{ id: 12, subject: "英语", score: 70 },
],
subject: "",
score: "",
},
});
</script>html
<div id="app" class="score-case">
<div class="table">
<table>
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>操作</th>
</tr>
</thead>
<tbody v-if="list.length > 0">
<tr v-for="(item, index) in list" :key="item.id">
<td>{{index+1}}</td>
<td>{{item.subject}}</td>
<td :class="{red: item.score < 60}">{{item.score}}</td>
<td><a href="#" @click.prevent="remove(item.id)">删除</a></td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="5">
<span class="none">暂无数据</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<span>总分:246</span>
<span style="margin-left: 50px">平均分:79</span>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="form">
<div class="form-item">
<div class="label">科目:</div>
<div class="input">
<input type="text" placeholder="请输入科目" />
</div>
</div>
<div class="form-item">
<div class="label">分数:</div>
<div class="input">
<input type="text" placeholder="请输入分数" />
</div>
</div>
<div class="form-item">
<div class="label"></div>
<div class="input">
<button class="submit">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, subject: "语文", score: 20 },
{ id: 7, subject: "数学", score: 99 },
{ id: 12, subject: "英语", score: 70 },
],
subject: "",
score: "",
},
methods: {
remove(id) {
this.list = this.list.filter((item) => item.id !== id);
},
},
});
</script>html
<div id="app" class="score-case">
<div class="table">
<table>
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>操作</th>
</tr>
</thead>
<tbody v-if="list.length > 0">
<tr v-for="(item, index) in list" :key="item.id">
<td>{{index+1}}</td>
<td>{{item.subject}}</td>
<td :class="{red: item.score < 60}">{{item.score}}</td>
<td><a href="#" @click.prevent="remove(item.id)">删除</a></td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="5">
<span class="none">暂无数据</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<span>总分:246</span>
<span style="margin-left: 50px">平均分:79</span>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="form">
<div class="form-item">
<div class="label">科目:</div>
<div class="input">
<input type="text" placeholder="请输入科目" v-model.trim="subject" />
</div>
</div>
<div class="form-item">
<div class="label">分数:</div>
<div class="input">
<input type="text" placeholder="请输入分数" v-model.number="score" />
</div>
</div>
<div class="form-item">
<div class="label"></div>
<div class="input">
<button class="submit" @click="add">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, subject: "语文", score: 20 },
{ id: 7, subject: "数学", score: 99 },
{ id: 12, subject: "英语", score: 70 },
],
subject: "",
score: "",
},
methods: {
remove(id) {
this.list = this.list.filter((item) => item.id !== id);
},
add() {
if (!this.subject || !this.score) return;
this.list.push({
id: +new Date(),
subject: this.subject,
score: this.score,
});
this.subject = "";
this.score = "";
},
},
});
</script>html
<div id="app" class="score-case">
<div class="table">
<table>
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>操作</th>
</tr>
</thead>
<tbody v-if="list.length > 0">
<tr v-for="(item, index) in list" :key="item.id">
<td>{{index+1}}</td>
<td>{{item.subject}}</td>
<td :class="{red: item.score < 60}">{{item.score}}</td>
<td><a href="#" @click.prevent="remove(item.id)">删除</a></td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="5">
<span class="none">暂无数据</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<span>总分:{{totalScore}}</span>
<span style="margin-left: 50px">平均分:{{avagerScore}}</span>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="form">
<div class="form-item">
<div class="label">科目:</div>
<div class="input">
<input type="text" placeholder="请输入科目" v-model.trim="subject" />
</div>
</div>
<div class="form-item">
<div class="label">分数:</div>
<div class="input">
<input type="text" placeholder="请输入分数" v-model.number="score" />
</div>
</div>
<div class="form-item">
<div class="label"></div>
<div class="input">
<button class="submit" @click="add">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
list: [
{ id: 1, subject: "语文", score: 20 },
{ id: 7, subject: "数学", score: 99 },
{ id: 12, subject: "英语", score: 70 },
],
subject: "",
score: "",
},
methods: {
remove(id) {
this.list = this.list.filter((item) => item.id !== id);
},
add() {
if (!this.subject || !this.score) return;
this.list.push({
id: +new Date(),
subject: this.subject,
score: this.score,
});
this.subject = "";
this.score = "";
},
},
computed: {
totalScore() {
return this.list.reduce((total, item) => total + item.score, 0);
},
avagerScore() {
if (this.list.length === 0) return 0;
// return Math.round(this.totalScore / this.list.length);
return (this.totalScore / this.list.length).toFixed(2);
}
}
});
</script>watch 侦听器
作用
监视数据变化,执行一些业务逻辑或异步操作
语法
watch同样声明在跟data同级的配置项中- 简单写法:简单类型数据直接监视
- 完整写法:添加额外配置项
js
data: {
words: '苹果',
obj: {
words: '苹果'
}
},
watch: {
// 该方法会在数据变化时,触发执行
数据属性名 (newValue, oldValue) {
一些业务逻辑 或 异步操作。
},
'对象.属性名' (newValue, oldValue) {
一些业务逻辑 或 异步操作。
}
}深度监听
完整写法 —>添加额外的配置项
deep:true对复杂类型进行深度监听immdiate: true初始化 立刻执行一次
js
watch: { // watch 完整写法
对象: {
deep: true, // 深度监视
immdiate:true,//立即执行 handler 函数
handler (newValue) {
console.log(newValue)
}
}
}案例 翻译
需求:
- 输入框输入内容,自动翻译
- 输入内容,修改语言,两种事件下都要翻译
html
<div id="app">
<!-- 条件选择框 -->
<div class="query">
<span>翻译成的语言:</span>
<select>
<option value="italy">意大利</option>
<option value="english">英语</option>
<option value="german">德语</option>
</select>
</div>
<!-- 翻译框 -->
<div class="box">
<div class="input-wrap">
<textarea v-model="words"></textarea>
<span><i>⌨️</i>文档翻译</span>
</div>
<div class="output-wrap">
<div class="transbox">mela</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 接口地址:https://applet-base-api-t.itheima.net/api/translate
// 请求方式:get
// 请求参数:
// (1)words:需要被翻译的文本(必传)
// (2)lang:需要被翻译成的语言(可选)默认值 - 意大利
// -----------------------------------------------
const app = new Vue({
el: "#app",
data: {
words: "",
},
// 具体讲解:(1) watch 语法 (2) 具体业务实现
});
</script>html
<div id="app">
<!-- 条件选择框 -->
<div class="query">
<span>翻译成的语言:</span>
<select>
<option value="italy">意大利</option>
<option value="english">英语</option>
<option value="german">德语</option>
</select>
</div>
<!-- 翻译框 -->
<div class="box">
<div class="input-wrap">
<textarea v-model="obj.words"></textarea>
<span><i>⌨️</i>文档翻译</span>
</div>
<div class="output-wrap">
<div class="transbox">{{result}}</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 接口地址:https://applet-base-api-t.itheima.net/api/translate
// 请求方式:get
// 请求参数:
// (1)words:需要被翻译的文本(必传)
// (2)lang:需要被翻译成的语言(可选)默认值 - 意大利
// -----------------------------------------------
const app = new Vue({
el: "#app",
data: {
obj: {
words: "",
},
result: "",
},
// 具体讲解:(1) watch 语法 (2) 具体业务实现
watch: {
'obj.words'(newVal) {
// 防抖:延迟执行 → 干啥事先等一等,延迟一会,一段时间内没有再次触发,才执行
clearTimeout(this.timer)
this.timer = setTimeout(async () => {
// 发送请求
const res = await axios.get('https://applet-base-api-t.itheima.net/api/translate', {
params: {
words: newVal
}
})
console.log(res);
// 获取翻译结果
this.result = res.data.data
}, 500);
}
},
});
</script>html
<div id="app">
<!-- 条件选择框 -->
<div class="query">
<span>翻译成的语言:</span>
<select v-model="obj.lang">
<option value="italy">意大利</option>
<option value="english">英语</option>
<option value="german">德语</option>
</select>
</div>
<!-- 翻译框 -->
<div class="box">
<div class="input-wrap">
<textarea v-model="obj.words"></textarea>
<span><i>⌨️</i>文档翻译</span>
</div>
<div class="output-wrap">
<div class="transbox">{{result}}</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 接口地址:https://applet-base-api-t.itheima.net/api/translate
// 请求方式:get
// 请求参数:
// (1)words:需要被翻译的文本(必传)
// (2)lang:需要被翻译成的语言(可选)默认值 - 意大利
// -----------------------------------------------
const app = new Vue({
el: "#app",
data: {
obj: {
words: "刁德一",
lang: "english"
},
result: "",
},
// 具体讲解:(1) watch 语法 (2) 具体业务实现
watch: {
obj: {
handler(newVal) {
// 防抖:延迟执行 → 干啥事先等一等,延迟一会,一段时间内没有再次触发,才执行
clearTimeout(this.timer) // 清除定时器
this.timer = setTimeout(async () => {
// 发送请求
const res = await axios.get("https://applet-base-api-t.itheima.net/api/translate", {
params: newVal
})
// console.log(res)
this.result = res.data.data
}, 500)
},
deep: true, // 深度监听
immediate: true, // 页面加载后立即执行
}
},
});
</script>总结
watch 侦听器的写法有几种?
简单写法
jswatch: { 数据属性名 (newValue, oldValue) { 一些业务逻辑 或 异步操作。 }, '对象。属性名' (newValue, oldValue) { 一些业务逻辑 或 异步操作。 } }完整写法
jswatch: {// watch 完整写法 数据属性名:{ deep: true, // 深度监视 (针对复杂类型) immediate: true, // 是否立刻执行一次 handler handler (newValue) { console.log(newValue) } } }
综合案例 购物车

需求说明
- 渲染功能
- 删除功能
- 修改个数
- 全选反选
- 统计 选中的 总价 和 总数量
- 持久化到本地
实现思路
基本渲染:
v-for遍历、:class动态绑定样式删除功能:
v-on绑定事件,获取当前行的 id修改个数:
v-on绑定事件,获取当前行的 id,进行筛选出对应的项然后增加或减少全选反选
- 必须所有的小选框都选中,全选按钮才选中 →
every - 如果全选按钮选中,则所有小选框都选中
- 如果全选取消,则所有小选框都取消选中
声明计算属性,判断数组中的每一个
checked属性的值,看是否需要全部选- 必须所有的小选框都选中,全选按钮才选中 →
统计 选中的 总价 和 总数量:通过计算属性来计算选中的总价和总数量
持久化到本地:在数据变化时都要更新下本地存储
watch
渲染功能
v-ifv-else判断是否有数据,有数据显示购物车本体,没有数据显示空购物车。v-for遍历购物车数组,渲染购物车列表。
html
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0"></div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
<div class="tbody">
<!-- 遍历购物车数组,渲染购物车列表 -->
<div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{item.price}}</div>
<div class="td">
<div class="my-input-number">
<button class="decrease">-</button>
<span class="my-input__inner">{{item.num}}</span>
<button class="increase">+</button>
</div>
</div>
<div class="td">{{item.num * item.price}}</div>
<div class="td"><button>删除</button></div>
</div>
</div>删除功能
v-onbutton 绑定点击事件,获取当前行的 id- 创建删除方法,使用
reduce遍历数组,筛选出不等于当前行 id 的项,重新赋值给购物车
jsx
<div class="td"><button @click="deleteItem(item.id)">删除</button></div>
methods: {
deleteItem(id) {
this.fruitList = this.fruitList.filter(item => item.id !== id)
}
}修改个数
v-onbutton 绑定点击事件,然后item.num++或item.num--- 当
item.num为 1 时,点击减少按钮,item.num会变成 0。所以当item.num <= 1要禁用减少按钮
html
<button class="decrease" @click="item.num--" :disabled="item.num <= 1">-</button>
<button class="increase" @click="item.num++">+</button>全选反选
要求
- 必须所有的小选框都选中,全选按钮才选中 →
every - 如果全选按钮选中,则所有小选框都选中
- 如果全选取消,则所有小选框都取消选中
- 必须所有的小选框都选中,全选按钮才选中 →
思路
- 声明计算属性 selectAll
- get 方法中使用
every判断数组中的每一个item.isChecked属性的值,看是否全部选择,如果全部选择则返回 true,否则返回 false - set 方法中,遍历数组,将每一个
item.isChecked属性的值都设置为selectAll的值(也就是全选按钮的值 true/false)
jsx
<label class="check-all">
<input type="checkbox" v-model="selectAll" />全选
</label>
computed: {
selectAll: {
get() {
return this.fruitList.every(item => item.isChecked)
},
set(val) {
this.fruitList.forEach(item => item.isChecked = val)
}
}
}统计
- 通过计算属性来计算选中的总价和总数量
jsx
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">{{totalPrice}}</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( {{totalNum}} )</button>
computed: {
// 统计选中的总数 reduce num
totalNum() {
return this.fruitList.reduce((total, item) => {
if (item.isChecked) {
return total + item.num;
} else {
return total;
}
}, 0);
},
// 总计选中的总价 reduce num * price
totalPrice() {
return this.fruitList.reduce((total, item) => {
if (item.isChecked) {
return total + (item.num * item.price);
} else {
return total;
}
}, 0);
},
}持久化到本地
- 使用
watch侦听器监听 fruitList 的变化,如果有变化,将变化的值存储到 localStorage 中(需要使用 JSON.stringify 将对象转换为字符串) - 默认数据从 localStorage 中获取(需要使用 JSON.parse 将字符串转换为对象),如果没有数据,就使用默认数据
jsx
// 监听 fruitList 的变化,如果有变化,将变化的值存储到 localStorage 中(需要使用 JSON.stringify 将对象转换为字符串)
watch: {
fruitList: {
handler(newVal) {
localStorage.setItem('fruitList', JSON.stringify(newVal))
},
deep: true
}
}
// 默认数据
const defaultFruitList = [
{
id: 1,
icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
isChecked: false,
num: 20,
price: 34,
},
];
data: {
// 水果列表
// 优先从 localStorage 中读取存储的 fruitList,如果不存在(用户清空本地存储),则从 defaultFruitList 中读取
fruitList: JSON.parse(localStorage.getItem('fruitList')) || defaultFruitList,
},相关代码
html
<div class="app-container" id="app">
<!-- 顶部 banner -->
<div class="banner-box">
<img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" />
</div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div class="tr active">
<div class="td"><input type="checkbox" checked /></div>
<div class="td">
<img src="https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png" alt="" />
</div>
<div class="td">6</div>
<div class="td">
<div class="my-input-number">
<button class="decrease">-</button>
<span class="my-input__inner">2</span>
<button class="increase">+</button>
</div>
</div>
<div class="td">12</div>
<div class="td"><button>删除</button></div>
</div>
<div class="tr">
<div class="td"><input type="checkbox" /></div>
<div class="td">
<img src="https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png" alt="" />
</div>
<div class="td">7</div>
<div class="td">
<div class="my-input-number">
<button disabled class="decrease">-</button>
<span class="my-input__inner">1</span>
<button class="increase">+</button>
</div>
</div>
<div class="td">14</div>
<div class="td"><button>删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">24</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( 6 )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty">🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
// 水果列表
fruitList: [
{
id: 1,
icon: "https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png",
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: "https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png",
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: "https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png",
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: "https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png",
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: "https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png",
isChecked: false,
num: 20,
price: 34,
},
],
},
});
</script>html
<div class="app-container" id="app">
<!-- 顶部 banner -->
<div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{item.price}}</div>
<div class="td">
<div class="my-input-number">
<button class="decrease"> - </button>
<span class="my-input__inner">{{item.num}}</span>
<button class="increase"> + </button>
</div>
</div>
<div class="td">{{item.num * item.price}}</div>
<div class="td"><button>删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">24</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( 6 )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 水果列表
fruitList: [
{
id: 1,
icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
isChecked: false,
num: 20,
price: 34,
},
],
},
})
</script>html
<div class="app-container" id="app">
<!-- 顶部 banner -->
<div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{item.price}}</div>
<div class="td">
<div class="my-input-number">
<button class="decrease"> - </button>
<span class="my-input__inner">{{item.num}}</span>
<button class="increase"> + </button>
</div>
</div>
<div class="td">{{item.num * item.price}}</div>
<div class="td"><button @click="deleteItem(item.id)">删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">24</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( 6 )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 水果列表
fruitList: [
{
id: 1,
icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
isChecked: false,
num: 20,
price: 34,
},
],
},
methods: {
deleteItem(id) {
this.fruitList = this.fruitList.filter(item => item.id !== id)
}
}
})
</script>html
<div class="app-container" id="app">
<!-- 顶部 banner -->
<div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{item.price}}</div>
<div class="td">
<div class="my-input-number">
<button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
<span class="my-input__inner">{{item.num}}</span>
<button class="increase" @click="item.num++"> + </button>
</div>
</div>
<div class="td">{{item.num * item.price}}</div>
<div class="td"><button @click="deleteItem(item.id)">删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">24</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( 6 )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 水果列表
fruitList: [
{
id: 1,
icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
isChecked: false,
num: 20,
price: 34,
},
],
},
methods: {
deleteItem(id) {
this.fruitList = this.fruitList.filter(item => item.id !== id)
}
}
})
</script>html
<div class="app-container" id="app">
<!-- 顶部 banner -->
<div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{item.price}}</div>
<div class="td">
<div class="my-input-number">
<button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
<span class="my-input__inner">{{item.num}}</span>
<button class="increase" @click="item.num++"> + </button>
</div>
</div>
<div class="td">{{item.num * item.price}}</div>
<div class="td"><button @click="deleteItem(item.id)">删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" v-model="selectAll" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">24</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( 6 )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 水果列表
fruitList: [
{
id: 1,
icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
isChecked: false,
num: 20,
price: 34,
},
],
},
methods: {
deleteItem(id) {
this.fruitList = this.fruitList.filter(item => item.id !== id)
}
},
computed: {
selectAll: {
get() {
return this.fruitList.every(item => item.isChecked)
},
set(val) {
this.fruitList.forEach(item => item.isChecked = val)
}
}
}
})
</script>html
<div class="app-container" id="app">
<!-- 顶部 banner -->
<div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{item.price}}</div>
<div class="td">
<div class="my-input-number">
<button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
<span class="my-input__inner">{{item.num}}</span>
<button class="increase" @click="item.num++"> + </button>
</div>
</div>
<div class="td">{{item.num * item.price}}</div>
<div class="td"><button @click="deleteItem(item.id)">删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" v-model="selectAll" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">{{totalPrice}}</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( {{totalNum}} )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 水果列表
fruitList: [
{
id: 1,
icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
isChecked: false,
num: 20,
price: 34,
},
],
},
methods: {
deleteItem(id) {
this.fruitList = this.fruitList.filter(item => item.id !== id)
}
},
computed: {
selectAll: {
get() {
return this.fruitList.every(item => item.isChecked)
},
set(val) {
this.fruitList.forEach(item => item.isChecked = val)
}
},
// 统计选中的总数 reduce
totalNum() {
return this.fruitList.reduce((total, item) => {
if (item.isChecked) {
return total + item.num;
} else {
return total;
}
}, 0);
},
// 总计选中的总价 num * price
totalPrice() {
return this.fruitList.reduce((total, item) => {
if (item.isChecked) {
return total + (item.num * item.price);
} else {
return total;
}
}, 0);
},
}
})
</script>html
<div class="app-container" id="app">
<!-- 顶部 banner -->
<div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span>
/
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{item.price}}</div>
<div class="td">
<div class="my-input-number">
<button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
<span class="my-input__inner">{{item.num}}</span>
<button class="increase" @click="item.num++"> + </button>
</div>
</div>
<div class="td">{{item.num * item.price}}</div>
<div class="td"><button @click="deleteItem(item.id)">删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" v-model="selectAll" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">{{totalPrice}}</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( {{totalNum}} )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const defaultFruitList = [
{
id: 1,
icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
isChecked: false,
num: 20,
price: 34,
},
];
const app = new Vue({
el: '#app',
data: {
// 水果列表
// 优先从 localStorage 中读取存储的 fruitList,如果不存在(用户清空本地存储),则从 defaultFruitList 中读取
fruitList: JSON.parse(localStorage.getItem('fruitList')) || defaultFruitList,
},
methods: {
deleteItem(id) {
this.fruitList = this.fruitList.filter(item => item.id !== id)
}
},
computed: {
selectAll: {
get() {
return this.fruitList.every(item => item.isChecked)
},
set(val) {
this.fruitList.forEach(item => item.isChecked = val)
}
},
// 统计选中的总数 reduce
totalNum() {
return this.fruitList.reduce((total, item) => {
if (item.isChecked) {
return total + item.num;
} else {
return total;
}
}, 0);
},
// 总计选中的总价 num * price
totalPrice() {
return this.fruitList.reduce((total, item) => {
if (item.isChecked) {
return total + (item.num * item.price);
} else {
return total;
}
}, 0);
},
},
watch: {
// 监听 fruitList 的变化,如果有变化,将变化的值存储到 localStorage 中(需要使用 JSON.stringify 将对象转换为字符串)
fruitList: {
handler(newVal) {
localStorage.setItem('fruitList', JSON.stringify(newVal))
},
deep: true
}
}
})
</script>