@keyup.enter:当回车时才触发事件,如果不写【.enter】按下任意键都会触发事件
不写【.enter】代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<div id = "app">
<input type="text" v-model="enter" @keyup="fn">
</div>
<script>
const app = new Vue({
el: "#app",
data: {
enter:""
},
methods: {
fn() {
console.log(this.enter)
}
},
})
</script>
</body>
</html>
结果:按下任意键,都会将文本框内容输出到控制台
加上【.enter】代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<div id = "app">
<input type="text" v-model="enter" @keyup.enter="fn">
</div>
<script>
const app = new Vue({
el: "#app",
data: {
enter:""
},
methods: {
fn() {
console.log(this.enter)
}
},
})
</script>
</body>
</html>
结果:当回车时,才输出文本框内容到控制台
v-model.trim:去掉表单数据前后空格
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<div id = "app">
<span>.enter</span> <input type="text" v-model="enter" @keyup.enter="fn"><br><br>
<span>.trim</span> <input type="text" v-model.trim="trim1">
</div>
<script>
const app = new Vue({
el: "#app",
data: {
enter:"",
trim1:""
},
methods: {
fn() {
console.log(this.enter)
}
},
})
</script>
</body>
</html>
结果:
v-model.number:将表单数据转换类型为数字
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<div id = "app">
<span>.enter</span> <input type="text" v-model="enter" @keyup.enter="fn"><br><br>
<span>.trim</span> <input type="text" v-model.trim="trim1"><br><br>
<span>.number</span> <input type="text" v-model.number="number">
</div>
<script>
const app = new Vue({
el: "#app",
data: {
enter:"",
trim1:"",
number:""
},
methods: {
fn() {
console.log(this.enter)
}
},
})
</script>
</body>
</html>
结果:
@click.stop:阻止冒泡
子盒子的事件触发时,默认父盒子的事件也会触发
要达到只触发子盒子的效果,加上【.stop】就可以了
@click.prevent:阻止默认行为
默认点击a标签的网址会跳转,如果不想跳转,加上【@click.prevent】就可以了
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.father{
width: 200px;
height: 200px;
background: greenyellow;
text-align: center;
}
.son{
width: 100px;
height: 100px;
background: pink;
text-align: center;
}
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<div id = "app">
<span>.enter</span> <input type="text" v-model="enter" @keyup.enter="fn"><br><br>
<span>.trim</span> <input type="text" v-model.trim="trim1"><br><br>
<span>.number</span> <input type="text" v-model.number="number"><br><br>
<span>.stop 阻止冒泡</span><div @click="ffn" class="father">
<div @click.stop="sfn" class="son">sBox</div>
fBox
</div>
<span>.prevent</span>
<a @click.prevent href="https://www.baidu.com">阻止默认行为</a>
</div>
<script>
const app = new Vue({
el: "#app",
data: {
enter:"",
trim1:"",
number:""
},
methods: {
fn() {
console.log(this.enter)
},
ffn() {
alert("父方法执行力")
},
sfn() {
alert("子方法执行力")
}
},
})
</script>
</body>
</html>
结果:子方法执行后,父方法不会执行;链接点击不会有反应
v-bind对样式class的控制:通过对象或者数组来控制生效的class样式
对象形式::class="{pink: true, big: true}",用键值对决定生效的类名
数组形式::class="['pink', 'big']",写在数组里的类都会生效
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
width: 200px;
height: 200px;
border: 3px solid;
margin-top: 10px;
font-size: 30px;
}
.pink {
background: pink;
}
.big {
width: 300px;
height: 300px;
}
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<div id = "app">
<!-- :class的作用:增强class -->
<div class="box" :class="{pink: true, big: true}">我爱前端</div>
<div class="box" :class="['pink', 'big']">我爱前端</div>
</div>
<script>
const app = new Vue({
el: "#app",
data: {
}
})
</script>
</body>
</html>
结果:
案例:被点击的导航栏显示高亮(利用:class)
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
ul {
display: flex;
border-bottom: 2px solid #e01222;
padding: 0 10px;
}
li {
width: 100px;
height: 50px;
line-height: 50px;
list-style: none;
text-align: center;
}
li a {
display: block;
text-decoration: none;
font-weight: bold;
color: #333333;
}
/* #id和.class中间有空格和无空格的区别
首先中间有空格的情况:.class1 .class2
是选择到.class1类下的.class2类子节点,即.class2类的节点要是.class1类子节点
无空格情况:.class1.class2
是选择到同时拥有.class1和.class2的节点
*/
li a.active {
background-color: #e01222;
color: #fff;
}
</style>
</head>
<body>
<div id="app">
<ul>
<li v-for="(item, index) in list" :key="item.id" @click="activeIndex = index">
<a :class="{active: index === activeIndex}" href="#">{{ item.name }}</a>
</li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
activeIndex: 0, // 记录高亮
list: [
{ id: 1, name: '京东秒杀' },
{ id: 2, name: '每日特价' },
{ id: 3, name: '品类秒杀' }
]
}
})
</script>
</body>
</html>
效果:
v-bind对样式style的控制
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.progress {
height: 25px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
width: 50%;
height: 20px;
border-radius: 10px;
text-align: right;
position: relative;
background-color: #409eff;
background-size: 20px 20px;
box-sizing: border-box;
transition: all 1s;
}
.inner span {
position: absolute;
right: -20px;
bottom: -25px;
}
</style>
</head>
<body>
<div id="app">
<!-- 外层盒子底色 (黑色) -->
<div class="progress">
<!-- 内层盒子 - 进度(蓝色) -->
<div class="inner" :style="{width : percent + '%'}">
<span>{{ percent }}%</span>
</div>
</div>
<button @click="percent = 25">设置25%</button>
<button @click="percent = 50">设置50%</button>
<button @click="percent = 75">设置75%</button>
<button @click="percent = 100">设置100%</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
percent: 30
}
})
</script>
</body>
</html>
效果:点击不同按钮时,改变的百分比percent变量会变成进度条的width值,实现进度条变化
v-model应用于表单元素
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
textarea {
display: block;
width: 240px;
height: 100px;
margin: 10px 0;
}
</style>
</head>
<body>
<div id="app">
<h3>前端学习网</h3>
姓名:
<input type="text" v-model="username">
<br><br>
是否单身:
<input type="checkbox" v-model="isSingle">
<br><br>
<!--
前置理解:
1. name: 给单选框加上 name 属性 可以分组 → 同一组互相会互斥
2. value: 给单选框加上 value 属性,用于提交给后台的数据
结合 Vue 使用 → v-model
-->
性别:
<input v-model="sex" type="radio" name="gender" value="1">男
<input v-model="sex" type="radio" name="gender" value="2">女
<br><br>
<!--
前置理解:
1. option 需要设置 value 值,提交给后台
2. select 的 value 值,关联了选中的 option 的 value 值
结合 Vue 使用 → v-model
-->
所在城市:
<select v-model="cityId">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">成都</option>
<option value="104">南京</option>
</select>
<br><br>
自我描述:
<textarea v-model="desc"></textarea>
<button>立即注册</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
isSingle: false,
sex: "2",
cityId: '102',
desc: ""
}
})
</script>
</body>
</html>
效果:对所有表单元素都进行了双向数据绑定
计算属性computed:根据现有数据计算出来结果会同步变化
代码:统计礼物总数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
table {
border: 1px solid #000;
text-align: center;
width: 240px;
}
th,td {
border: 1px solid #000;
}
h3 {
position: relative;
}
</style>
</head>
<body>
<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: 5 },
]
},
computed: {
totalCount () {
/* 使用reduce函数求和,sum叫做阶段性结果,item为list里的每一项
由于传入了初始值0,所以开始时sum的值为0,item的值为数组第一项,
相加之后返回值为1作为下一轮回调的sum值,然后再继续与下一个数组项相加,
以此类推,直至完成所有数组项的和并返回。 */
let total = this.list.reduce((sum, item) => sum + item.num, 0)
return total
}
}
})
</script>
</body>
</html>
结果:
computed和methods的区别
computed用于计算出结果,使用时不加括号,比如{{ totalCount }},totalCount是属性名
methods用于处理业务逻辑,比如修改向数组中添加元素,使用方法:
①@click="add"
②{{ totalCountFn() }},totalCountFn是方法名
computed完整写法:获取(get)+设置(set)
代码:当修改计算属性fullName的值时,要通过set方法更改
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName"><br>
<p>姓名:{{ fullName }}</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: {
firstName: "张",
lastName: "小飞"
},
methods: {
changeName() {
this.fullName = "吕小布"
}
},
computed: {
fullName: {
get() {
return this.firstName + this.lastName
},
set(value) {
// slice方法:截取字符串
this.firstName = value.slice(0, 1)
this.lastName = value.slice(1)
}
}
},
})
</script>
</body>
</html>
结果:点击改名卡按钮时,fullName的值变成“吕小布”
综合案例:成绩表
代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./styles/index.css" />
<title>Document</title>
</head>
<body>
<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 @click.prevent="del(item.id)" 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>总分:{{ totalScore }}</span>
<span style="margin-left: 50px">平均分:{{ averageScore }}</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="../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: {
del (id) {
this.list = this.list.filter((item) => item.id !== id)
},
add () {
if (!this.subject){
alert("请输入科目")
return
}
if (typeof this.score !== "number"){
alert("请输入合法的分数")
return
}
this.list.unshift({ id: +new Date(), subject: this.subject, score: this.score })
this.subject = '',
this.score = ''
}
},
computed: {
totalScore() {
let total = this.list.reduce((sum, item) => sum + item.score, 0)
return total
},
averageScore() {
if (this.list.length === 0){
return 0
}
return (this.totalScore / this.list.length).toFixed(2)
}
}
})
</script>
</body>
</html>
index.css
.score-case {
width: 1000px;
margin: 50px auto;
display: flex;
}
.score-case .table {
flex: 4;
}
.score-case .table table {
width: 100%;
border-spacing: 0;
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
.score-case .table table th {
background: #f5f5f5;
}
.score-case .table table tr:hover td {
background: #f5f5f5;
}
.score-case .table table td,
.score-case .table table th {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
text-align: center;
padding: 10px;
}
.score-case .table table td.red,
.score-case .table table th.red {
color: red;
}
.score-case .table .none {
height: 100px;
line-height: 100px;
color: #999;
}
.score-case .form {
flex: 1;
padding: 20px;
}
.score-case .form .form-item {
display: flex;
margin-bottom: 20px;
align-items: center;
}
.score-case .form .form-item .label {
width: 60px;
text-align: right;
font-size: 14px;
}
.score-case .form .form-item .input {
flex: 1;
}
.score-case .form .form-item input,
.score-case .form .form-item select {
appearance: none;
outline: none;
border: 1px solid #ccc;
width: 200px;
height: 40px;
box-sizing: border-box;
padding: 10px;
color: #666;
}
.score-case .form .form-item input::placeholder {
color: #666;
}
.score-case .form .form-item .cancel,
.score-case .form .form-item .submit {
appearance: none;
outline: none;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 10px;
margin-right: 10px;
font-size: 12px;
background: #ccc;
}
.score-case .form .form-item .submit {
border-color: #069;
background: #069;
color: #fff;
}
index.less
.score-case {
width: 1000px;
margin: 50px auto;
display: flex;
.table {
flex: 4;
table {
width: 100%;
border-spacing: 0;
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
th {
background: #f5f5f5;
}
tr:hover td {
background: #f5f5f5;
}
td,
th {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
text-align: center;
padding: 10px;
&.red {
color: red;
}
}
}
.none {
height: 100px;
line-height: 100px;
color: #999;
}
}
.form {
flex: 1;
padding: 20px;
.form-item {
display: flex;
margin-bottom: 20px;
align-items: center;
}
.form-item .label {
width: 60px;
text-align: right;
font-size: 14px;
}
.form-item .input {
flex: 1;
}
.form-item input,
.form-item select {
appearance: none;
outline: none;
border: 1px solid #ccc;
width: 200px;
height: 40px;
box-sizing: border-box;
padding: 10px;
color: #666;
}
.form-item input::placeholder {
color: #666;
}
.form-item .cancel,
.form-item .submit {
appearance: none;
outline: none;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 10px;
margin-right: 10px;
font-size: 12px;
background: #ccc;
}
.form-item .submit {
border-color: #069;
background: #069;
color: #fff;
}
}
}
动态渲染表格数据:
添加成绩:
计算总分、平均分:
效果:
watch侦听器:当侦听的数据发生变化时,执行相应操作
代码:翻译器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 18px;
}
#app {
padding: 10px 20px;
}
.query {
margin: 10px 0;
}
.box {
display: flex;
}
textarea {
width: 300px;
height: 160px;
font-size: 18px;
border: 1px solid #dedede;
outline: none;
resize: none;
padding: 10px;
}
textarea:hover {
border: 1px solid #1589f5;
}
.transbox {
width: 300px;
height: 160px;
background-color: #f0f0f0;
padding: 10px;
border: none;
}
.tip-box {
width: 300px;
height: 25px;
line-height: 25px;
display: flex;
}
.tip-box span {
flex: 1;
text-align: center;
}
.query span {
font-size: 18px;
}
.input-wrap {
position: relative;
}
.input-wrap span {
position: absolute;
right: 15px;
bottom: 15px;
font-size: 12px;
}
.input-wrap i {
font-size: 20px;
font-style: normal;
}
</style>
</head>
<body>
<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">
<!-- 双向数据绑定到obj对象的words属性 -->
<textarea v-model="obj.words"></textarea>
<span><i>⌨️</i>文档翻译</span>
</div>
<div class="output-wrap">
<!-- 将翻译结果result显示到右框 -->
<div class="transbox">{{ result }}</div>
</div>
</div>
</div>
<script src="../vue.js"></script>
<script src="../axios.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: '',
obj: {
words: 'popo',
lang: 'italy'
},
result: '',
//timer: null //延时器id
},
// 具体讲解:(1) watch语法 (2) 具体业务实现
watch: {
/* words (newValue, oldValue) {
console.log(newValue, oldValue)
} */
/* 侦听器用法:当obj.words变量发生变化时,会触发与之同名的侦听事件,
也就是每输入一个字母,都会触发下面的函数 */
// 默认传入了两个参数,newValue是变化后的值, oldValue是变化前的值,可以只写newValue
obj: {
deep: true, // 深度监听,words和lang都可以被监听到
immediate: true, // 立刻执行,页面刷新时,立即触发,将words的默认值翻译出来
handler(newValue) {
//console.log(newValue)
// 但是,如果连续输入,每输入一个字母,obj.words函数就会被执行一次,生成一个延时器
// 无法达到输入最后一个字母后,不再有任何输入时,才延时300ms发送请求
// (还是输入了n个字母,就发送了n次请求)
// 解决办法:防抖
// clearTimeout函数:清除延时器
// 当输入最后一个字母后,触发一次obj.words函数,倒数第二个字母的延时器被清除,不发送请求
// 最后一个字母的延时器在300ms后会发送请求,因为不再输入,所以obj.words函数不会再触发
clearTimeout(this.timer)
// 设置延时器setTimeout:输入一个字母后,延迟300ms才发送请求
this.timer = setTimeout(async () => {
const res = await axios({
url: "https://applet-base-api-t.itheima.net/api/translate",
params: newValue
})
this.result = res.data.data
console.log(res.data.data)
}, 300)
}
}
// 'obj.words' (newValue) {
// }
}
})
</script>
</body>
</html>
words是要翻译的内容,result是翻译的结果
当words输入完以后,发送翻译请求,更新result的值
如果选择的语言发生变化 ,也会触发监听事件
效果:
综合案例:水果购物车
渲染数据:
1.水果信息与空购物车互斥(v-if、v-else
效果:
有水果信息,没有空购物车
有空购物车,没有水果
2.展示水果信息(v-bind控制class样式、v-for循环遍历数组、v-model与复选框的checked双向数据绑定、v-bind绑定标签img的src属性、插值表达式展示单价、个数和小计
删除功能:
修改个数:
:disabled的作用:当水果的数量到1时,“-”的按钮不可用
find函数:找到与传入id对应的水果,才能修改它的个数
全选反选:
效果:
统计选中的总价和总数量:
持久化到本地:
持久化以后,再次刷新网页,会保留前面操作的结果
清空本地存储:
最终效果:
html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./css/inputnumber.css" />
<link rel="stylesheet" href="./css/index.css" />
<title>购物车</title>
</head>
<body>
<div class="app-container" id="app">
<!-- 顶部banner -->
<div class="banner-box"><img src="./img/fruit.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="tr" :class="{ active:item.isChecked }" v-for="(item, index) in fruitList" :key="item.id">
<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">
<!-- :disabled的作用:当水果的数量到1时,“-”的按钮不可用 -->
<button class="decrease" @click="decrease(item.id)" :disabled="item.num <= 1"> - </button>
<span class="my-input__inner">{{ item.num }}</span>
<button class="increase" @click="increase(item.id)"> + </button>
</div>
</div>
<div class="td">{{ item.price * item.num }}</div>
<div class="td"><button @click="del(item.id)">删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<!-- isAll是计算属性 -->
<input v-model="isAll" type="checkbox" />
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<!-- sumPrice是计算属性 -->
<span class="price-box">总价 : ¥ <span class="price">{{ sumPrice }}</span></span>
<!-- 结算按钮 -->
<!-- sumNum是计算属性 -->
<button class="pay">结算( {{ sumNum }} )</button>
</div>
</div>
</div>
<!-- 空车 -->
<!-- 水果信息与空购物车互斥 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="D:\vue_code\vue2-study\vue.js"></script>
<script>
//默认数组
const defaultArr = [
{
id: 1,
icon: './img/火龙果.png',
isChecked: true,
num: 2,
price: 6,
},
{
id: 2,
icon: './img/荔枝.png',
isChecked: false,
num: 7,
price: 20,
},
{
id: 3,
icon: './img/榴莲.png',
isChecked: false,
num: 3,
price: 40,
},
{
id: 4,
icon: './img/鸭梨.png',
isChecked: true,
num: 10,
price: 3,
},
{
id: 5,
icon: './img/樱桃.png',
isChecked: false,
num: 20,
price: 34,
},
]
const app = new Vue({
el: '#app',
data: {
// 解析JSON串,从本地读取水果列表,当删除所有水果时,列表为空,null无法读取,会导致网页整个丢失
//可以清空本地存储,让fruitList恢复为默认数组defaultArr
fruitList: JSON.parse(localStorage.getItem('list')) || defaultArr,
},
computed: {
// isAll(){
// return this.fruitList.every(item => item.isChecked)
// }
// 当计算属性要作修改时,写完整写法:get + set
isAll: {
// 获取isAll的值
get () {
// every函数:数组中的元素都满足是选中状态时,返回true,
// 也就是水果都被选中时,全选按钮亮起
return this.fruitList.every(item => item.isChecked)
},
// 设置isAll的值为value
set (value) {
// forEach函数:遍历数组,修改数组中的选择状态都为true,
// 也就是,当勾选全选按钮时,遍历数组,让每个水果都被选中
this.fruitList.forEach(item => item.isChecked = value)
}
},
sumPrice () {
// reduce函数:求和
let sum = this.fruitList.reduce((sum, item) => {
// 如果水果被选中,把这个水果的总价加到sum上
if (item.isChecked){
return sum + item.num * item.price
} else { //否则,只返回sum
return sum
}
}, 0)
return sum
},
sumNum () {
let sum = this.fruitList.reduce((sum, item) => {
if (item.isChecked){
return sum + item.num
} else {
return sum
}
}, 0)
return sum
},
},
methods: {
del (id) {
this.fruitList = this.fruitList.filter((item) => item.id !== id)
},
decrease (id) {
// find函数:找到与传入id对应的水果,才能修改它的个数
const fruit = this.fruitList.find(item => item.id === id)
fruit.num--
},
increase (id) {
const fruit = this.fruitList.find(item => item.id === id)
fruit.num++
}
},
watch: {
fruitList: {
deep: true, //深度监听,水果数组的任何值发生变化,都能监听到
handler(newValue) { //newValue是变化后的fruitList
console.log(newValue)
//localStorage:本地存储,把fruitList转成JSON格式,当作值,list是键,存储到浏览器本地
localStorage.setItem('list', JSON.stringify(newValue))
}
}
}
})
</script>
</body>
</html>
index.css
.app-container {
padding-bottom: 300px;
width: 800px;
margin: 0 auto;
}
@media screen and (max-width: 800px) {
.app-container {
width: 600px;
}
}
.app-container .banner-box {
border-radius: 20px;
overflow: hidden;
margin-bottom: 10px;
}
.app-container .banner-box img {
width: 100%;
}
.app-container .nav-box {
background: #ddedec;
height: 60px;
border-radius: 10px;
padding-left: 20px;
display: flex;
align-items: center;
}
.app-container .nav-box .my-nav {
display: inline-block;
background: #5fca71;
border-radius: 5px;
width: 90px;
height: 35px;
color: white;
text-align: center;
line-height: 35px;
margin-right: 10px;
}
.breadcrumb {
font-size: 16px;
color: gray;
}
.table {
width: 100%;
text-align: left;
border-radius: 2px 2px 0 0;
border-collapse: separate;
border-spacing: 0;
}
.th {
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
text-align: left;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
transition: background 0.3s ease;
}
.th.num-th {
flex: 1.5;
}
.th {
text-align: center;
}
.th:nth-child(4),
.th:nth-child(5),
.th:nth-child(6),
.th:nth-child(7) {
text-align: center;
}
.th.th-pic {
flex: 1.3;
}
.th:nth-child(6) {
flex: 1.3;
}
.th,
.td {
position: relative;
padding: 16px 16px;
overflow-wrap: break-word;
flex: 1;
}
.pick-td {
font-size: 14px;
}
.main,
.empty {
border: 1px solid #f0f0f0;
margin-top: 10px;
}
.tr {
display: flex;
cursor: pointer;
border-bottom: 1px solid #ebeef5;
}
.tr.active {
background-color: #f5f7fa;
}
.td {
display: flex;
justify-content: center;
align-items: center;
}
.table img {
width: 100px;
height: 100px;
}
button {
outline: 0;
box-shadow: none;
color: #fff;
background: #d9363e;
border-color: #d9363e;
color: #fff;
background: #d9363e;
border-color: #d9363e;
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
box-shadow: 0 2px 0 rgb(0 0 0 / 2%);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
touch-action: manipulation;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
}
button.pay {
background-color: #3f85ed;
margin-left: 20px;
}
.bottom {
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 20px;
border: 1px solid #f0f0f0;
border-top: none;
padding-left: 20px;
}
.right-box {
display: flex;
align-items: center;
}
.check-all {
cursor: pointer;
}
.price {
color: hotpink;
font-size: 30px;
font-weight: 700;
}
.price-box {
display: flex;
align-items: center;
}
.empty {
padding: 20px;
text-align: center;
font-size: 30px;
color: #909399;
}
.my-input-number {
display: flex;
}
.my-input-number button {
height: 40px;
color: #333;
border: 1px solid #dcdfe6;
background-color: #f5f7fa;
}
.my-input-number button:disabled {
cursor: not-allowed!important;
}
.my-input-number .my-input__inner {
height: 40px;
width: 50px;
padding: 0;
border: none;
border-top: 1px solid #dcdfe6;
border-bottom: 1px solid #dcdfe6;
}
inputnumber.css
.my-input-number {
position: relative;
display: inline-block;
width: 140px;
line-height: 38px;
}
.my-input-number span {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.my-input-number .my-input {
display: block;
position: relative;
font-size: 14px;
width: 100%;
}
.my-input-number .my-input__inner {
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
padding-left: 50px;
padding-right: 50px;
text-align: center;
}
.my-input-number .my-input-number__decrease,
.my-input-number .my-input-number__increase {
position: absolute;
z-index: 1;
top: 1px;
width: 40px;
height: auto;
text-align: center;
background: #f5f7fa;
color: #606266;
cursor: pointer;
font-size: 13px;
}
.my-input-number .my-input-number__decrease {
left: 1px;
border-radius: 4px 0 0 4px;
border-right: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__increase {
right: 1px;
border-radius: 0 4px 4px 0;
border-left: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__decrease.is-disabled,
.my-input-number .my-input-number__increase.is-disabled {
color: #c0c4cc;
cursor: not-allowed;
}