Web前端-Vue2+Vue3基础入门到实战项目-Day2
- 指令补充
- computed计算属性
- watch侦听器
-
- [简写 - 语法](#简写 - 语法)
- [简写 - 业务实现](#简写 - 业务实现)
- 完整写法
- [综合案例 - 水果购物车](#综合案例 - 水果购物车)
- 来源
指令补充
指令修饰符
@keyup.enter
: 键盘回车监听v-model.trim
: 去除首尾空格v-model.number
: 转数字@事件名.stop
: 阻止冒泡@事件名.prevent
: 阻止默认行为
html
<head>
...
<style>
.father {
width: 200px;
height: 200px;
background-color: pink;
margin-top: 20px;
}
.son {
width: 100px;
height: 100px;
background-color: skyblue;
}
</style>
</head>
<body>
<div id="app">
<h3>@keyup.enter -> 监听键盘回车事件</h3>
<input type="text" v-model="username" @keyup.enter="fn">
<h3>v-model修饰符 .trim .number</h3>
姓名: <input type="text" v-model.trim="username2"><br>
年纪: <input type="text" v-model.number="age">
<h3>@事件名.stop → 阻止冒泡</h3>
<div @click="fatherFn" class="father">
<div @click.stop="sonFn" class="son">儿子</div>
</div>
<h3>@事件名.prevent → 阻止默认行为</h3>
<a @click.prevent href="http://www.baidu.com">阻止默认行为</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
username2: '',
age: ''
},
methods: {
fn(){
console.log(this.username)
},
fatherFn () {
alert('老父亲被点击了')
},
sonFn () {
alert('儿子被点击了')
}
}
})
</script>
</body>
v-bind 对样式控制的增强
控制class
- 对象使用场景: 一个类名, 来回切换
- 数组使用场景: 批量添加或删除类
html
<head>
...
<style>
.box {
width: 200px;
height: 200px;
border: 3px solid #000;
font-size: 30px;
margin-top: 10px;
}
.pink {
background-color: pink;
}
.big {
width: 300px;
height: 300px;
}
</style>
</head>
<body>
<div id="app">
<div class="box" :class="{pink: true, big: true}">黑马程序员</div>
<div class="box" :class="['pink', 'big']">黑马程序员</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>
</body>
案例 - 京东秒杀tab导航高亮
html
<head>
...
<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;
}
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: activeIndex === index}" href="#"> {{item.name}} </a></li>
</li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/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>
控制style
json对象的键不能有"-", 可以单引号引起来或者驼峰命名
html
<head>
...
<style>
.box {
width: 200px;
height: 200px;
background-color: rgb(187, 150, 156);
}
</style>
</head>
<body>
<div id="app">
<div class="box" :style="{width: '400px', height: '400px', 'background-color': 'green'}"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>
</body>
案例 - 控制进度条
html
<head>
...
<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/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
percent: 0
}
})
</script>
</body>
v-model 应用于其他表单元素
v-model
会根据控件类型自动选取正确的方法来更新元素
html
<head>
...
<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 type="radio" name="gender" value="1" v-model="gender">男
<input type="radio" name="gender" value="2" v-model="gender">女
<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/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
isSingle: true,
gender: '1',
cityId: '101',
desc: ''
}
})
</script>
</body>
computed计算属性
基本使用
语法:
- 声明在computed配置项中, 一个计算属性对应一个函数
- 使用和普通属性一样使用
html
<head>
...
<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/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: 求和函数
let total = this.list.reduce((sum, item) => sum+item.num, 0)
return total
}
}
})
</script>
</body>
computed计算属性 vs methods方法
- computed作用: 封装了一段对于数据的处理, 获得一个结果
- methods作用: 给实例提供一个方法, 调用以处理业务逻辑
- computed缓存特性(提升性能): 计算属性会对计算出来的结果缓存, 再次使用直接读取缓存, 依赖项变化了, 会自动重新计算, 并再次缓存
计算属性完整写法
- 当计算属性被修改赋值时, 执行set方法, 修改的值, 传递给set方法的形参
html
<body>
<div id="app">
姓:<input type="text" v-model="firstName"> +
名:<input type="text" v-model="lastName"> =
<span> {{fullName}} </span><br><br>
<button @click="changeName">改名卡</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
firstName: '',
lastName: ''
},
computed: {
fullName: {
get(){
return this.firstName+this.lastName
},
set(value){
this.firstName = value.slice(0, 1)
this.lastName = value.slice(1)
}
}
},
methods: {
changeName(){
this.fullName = '阿巴巴'
}
}
})
</script>
</body>
案例 - 成绩
- 渲染功能(不及格高亮)
- v-if v-else
- v-for
- v-bind:class
- 删除功能
- 点击传参
- filter过滤覆盖原数组
- .prevent阻止默认行为
- 添加功能
- v-model v-model修饰符(.trim .number)
- unshift修改数组更新视图
- 统计总分, 求平均分
- 计算属性
- reduce秋娥和
html
<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 href="#" @click.prevent="del(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>总分: {{scoreCount}} </span>
<span style="margin-left: 50px">平均分 {{scoreAvg}} </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/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: {
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.score = ''
this.subject = ''
}
},
computed: {
scoreCount(){
return this.list.reduce((sum, item) => sum+item.score, 0)
},
scoreAvg(){
if(this.list.length === 0){
return 0
}
return (this.scoreCount/this.list.length).toFixed(2)
}
}
})
</script>
</body>
watch侦听器
简写 - 语法
js
const app = new Vue({
el: '#app',
data: {
// words: '',
obj: {
words: ''
}
},
watch: {
// words(newValue, oldValue){
// console.log(newValue, oldValue)
// }
'obj.words'(newValue, oldValue){
console.log(newValue, oldValue)
}
}
})
简写 - 业务实现
html
<head>
...
<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>
<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/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: '',
obj: {
words: ''
},
result: '', // 翻译结果
// 1. 这个timer可以不写, 提高性能,
// 像this.timer这种写法可以挂载timer属性到当前实例上
// 2. 非响应式的数据, 不渲染在页面上的数据都可以不写
// timer: '' // 延迟期id
},
watch: {
'obj.words' (newValue, oldValue){
// console.log(newValue, oldValue)
// 防抖: 延迟执行 -> 一段时间内没有再次触发再执行
clearTimeout(this.timer)
this.timer = setTimeout(async () => {
const res = await axios({
url: 'https://applet-base-api-t.itheima.net/api/translate',
params: {
words: newValue
}
})
this.result = res.data.data
console.log(this.result)
}, 300)
}
}
})
</script>
</body>
完整写法
deep: true
: 对复杂类型深度剪视immediate: true
: 初始化执行一次handler方法
html
<head>
...
<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">
<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/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: 'italy'
},
result: '', // 翻译结果
},
watch: {
obj: {
deep: true, // 深度监视
immediate: true, // 初始化执行
handler(newValue, oldValue){
clearTimeout(this.timer)
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(this.result)
}, 300)
}
}
}
})
</script>
</body>
综合案例 - 水果购物车
- 渲染功能
- v-if/v-else
- v-for
- :class
- 删除功能
- 点击传参
- filter过滤覆盖原数组
- 修改个数
- 点击传参
- find找对象
- 全选反选
- 计算属性computed
- 计算属性完整写法 get/set
- 统计选中的总价和总数量
- 计算属性computed
- reduce条件求和
- 持久化到本地
- watch监视
- localStorage
- JSON.stringify/JSON.parse
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">
<button class="decrease" @click="--item.num" :disabled="item.num <= 1"> - </button>
<span class="my-input__inner"> {{item.num}} </span>
<button class="increase" @click="add(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">
<input type="checkbox" v-model="isAll"/>
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥
<span class="price"> {{totalPrice}} </span>
</span>
<!-- 结算按钮 -->
<button class="pay">结算( {{totalCount}} )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/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: {
// 水果列表
fruitList: JSON.parse(localStorage.getItem('list')) || defaultArr
},
methods: {
del(id){
this.fruitList = this.fruitList.filter(item => item.id !== id)
},
add(id){
// 1. 根据id找到数组中的对应项 -> find
const fruit = this.fruitList.find(item => item.id === id)
// 2. 操作num数量
++fruit.num
}
},
computed: {
isAll: {
get(){
// 所有小选框都选中, 全选按钮才选中 -> every
return this.fruitList.every(item => item.isChecked)
},
set(value){
// 基于拿到的布尔值, 让所有的小选框同步状态
this.fruitList.forEach(item => item.isChecked = value)
}
},
totalCount(){
return this.fruitList.reduce((sum, item) => {
if(item.isChecked){
return sum + item.num
}else{
return sum
}
}, 0)
},
totalPrice(){
return this.fruitList.reduce((sum, item) =>
item.isChecked ? sum+item.num*item.price:sum
, 0)
}
},
watch: {
fruitList: {
deep: true,
handler(newValue){
// 将变化后的newValue存入本地 (转JSON)
localStorage.setItem('list', JSON.stringify(newValue))
}
}
}
})
</script>
</body>
</html>
来源
黑马程序员. Vue2+Vue3基础入门到实战项目