vue2+vue3
vue官网
1 2 <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script >
或者:
1 2 <script src ="https://cdn.jsdelivr.net/npm/vue@2" > </script >
day01
一、为什么要学习Vue
1.前端必备技能
2.岗位多,绝大互联网公司都在使用Vue
3.提高开发效率
4.高薪必备技能(Vue2+Vue3)
二、什么是Vue
概念:Vue (读音 /vjuː/,类似于 view) 是一套 **构建用户界面 ** 的 渐进式 框架
Vue2官网:https://v2.cn.vuejs.org/
1.什么是构建用户界面
基于数据 渲染出用户可以看到的界面
2.什么是渐进式
所谓渐进式就是循序渐进,不一定非得把Vue中的所有API都学完才能开发Vue,可以学一点开发一点
Vue的两种开发方式:
Vue核心包开发
场景:局部模块改造
Vue核心包&Vue插件&工程化
场景:整站开发
3.什么是框架
所谓框架:就是一套完整的解决方案
举个栗子
如果把一个完整的项目比喻为一个装修好的房子,那么框架就是一个毛坯房。
我们只需要在“毛坯房”的基础上,增加功能代码即可。
提到框架,不得不提一下库。
库,类似工具箱,是一堆方法的集合,比如 axios、lodash、echarts等
框架,是一套完整的解决方案,实现了大部分功能,我们只需要按照一定的规则去编码即可。
下图是 库 和 框架的对比。
框架的特点:有一套必须让开发者遵守的规则 或者约束
咱们学框架就是学习的这些规则 官网
总结:什么是Vue?
Vue是什么:
什么是构建用户界面:
什么是渐进式:
什么是框架:
三、创建Vue实例
我们已经知道了Vue框架可以 基于数据帮助我们渲染出用户界面,那应该怎么做呢?
比如就上面这个数据,基于提供好的msg 怎么渲染后右侧可展示的数据呢?
核心步骤(4步):
准备容器
引包(官网) — 开发版本/生产版本
创建Vue实例 new Vue()
指定配置项,渲染数据
el:指定挂载点
data提供数据
总结:创建Vue实例需要执行哪4步
四、插值表达式
插值表达式是一种Vue的模板语法
我们可以用插值表达式渲染出Vue提供的数据
1.作用:利用表达式进行插值,渲染到页面中
表达式:是可以被求值的代码,JS引擎会讲其计算出一个结果
以下的情况都是表达式:
1 2 3 4 5 6 7 8 9 money + 100 money - 100 money * 10 money / 10 price >= 100 ? '真贵' :'还行' obj.name arr[0 ] fn ()obj.fn ()
2.语法
插值表达式语法:
1 2 3 4 5 6 7 8 9 <h3>{{title}}<h3> <p > {{nickName.toUpperCase()}}</p > <p > {{age >= 18 ? '成年':'未成年'}}</p > <p > {{obj.name}}</p > <p > {{fn()}}</p >
3.错误用法
1 2 3 4 5 6 7 8 1. 在插值表达式中使用的数据 必须在data中进行了提供<p>{{hobby}}</p> / /如果在data中不存在 则会报错 2.支持的是表达式,而非语句,比如:if for ... <p>{{if}}</ p>3. 不能在标签属性中使用 {{ }} 插值 (插值表达式只能标签中间使用)<p title="{{username}}" >我是P标签</p>
4.总结
1.插值表达式的作用是什么
2.语法是什么
3.插值表达式的注意事项
五、响应式特性
1.什么是响应式?
简单理解就是数据变,视图对应变。
2.如何访问 和 修改 data中的数据(响应式演示)
data中的数据, 最终会被添加到实例上
① 访问数据: “实例.属性名”
② 修改数据: “实例.属性名”= “值”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { msg : '你好,黑马' , count : 100 } }) app.data app.data ='hello' </script >
3.总结
什么是响应式
如何访问和修改data中的数据呢
六、Vue开发者工具安装
通过谷歌应用商店安装(国外网站)
极简插件下载(推荐) https://chrome.zzzmh.cn/index
安装步骤:
安装之后可以F12后看到多一个Vue的调试面板
七、Vue中的常用指令
概念:指令(Directives)是 Vue 提供的带有 v- 前缀 的 特殊 标签 属性 。
为啥要学:提高程序员操作 DOM 的效率。
vue 中的指令按照不同的用途可以分为如下 6 大类:
内容渲染指令(v-html、v-text)
条件渲染指令(v-show、v-if、v-else、v-else-if)
事件绑定指令(v-on)
属性绑定指令 (v-bind)
双向绑定指令(v-model)
列表渲染指令(v-for)
指令是 vue 开发中最基础、最常用、最简单的知识点。
八、内容渲染指令
内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下2 个:
v-text(类似innerText)
使用语法:<p v-text="uname">hello</p>
,意思是将 uame 值渲染到 p 标签中
类似 innerText,使用该语法,会覆盖 p 标签原有内容
v-html(类似 innerHTML)
使用语法:<p v-html="intro">hello</p>
,意思是将 intro 值渲染到 p 标签中
类似 innerHTML,使用该语法,会覆盖 p 标签原有内容
类似 innerHTML,使用该语法,能够将HTML标签的样式呈现出来。
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id="app" > <h2 > 个人信息</h2 > <p v-text ="uname" > 姓名:</p > <p v-html ="intro" > 简介:</p > </div> <script> const app = new Vue({ el:'#app', data:{ uname:'张三', intro:'<h2>这是一个<strong>非常优秀</strong>的boy<h2>' } }) </script>
九、条件渲染指令
条件判断指令,用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是:
v-show
作用: 控制元素显示隐藏
语法: v-show = “表达式” 表达式值为 true 显示, false 隐藏
原理: 切换 display:none 控制显示隐藏
场景:频繁切换显示隐藏的场景
v-if
作用: 控制元素显示隐藏(条件渲染)
语法: v-if= “表达式” 表达式值 true显示, false 隐藏
原理: 基于条件判断,是否创建 或 移除元素节点
场景: 要么显示,要么隐藏,不频繁切换的场景
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id="app" > <div class ="box" > 我是v-show控制的盒子</div > <div class ="box" > 我是v-if控制的盒子</div > </div> <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { flag : false } }) </script >
v-else 和 v-else-if
作用:辅助v-if进行判断渲染
语法:v-else v-else-if=“表达式”
需要紧接着v-if使用
示例代码:
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 <body> <div id ="app" > <p v-if ="gender === 1" > 性别:♂ 男</p > <p v-else > 性别:♀ 女</p > <hr > <p v-if ="score >= 90" > 成绩评定A:奖励电脑一台</p > <p v-else-if ="score >= 70" > 成绩评定B:奖励周末郊游</p > <p v-else-if ="score >= 60" > 成绩评定C:奖励零食礼包</p > <p v-else > 成绩评定D:惩罚一周不能玩手机</p > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { gender : 2 , score : 95 } }) </script > </body > </html>
十、事件绑定指令
使用Vue时,如需为DOM注册事件,及其的简单,语法如下:
<button v-on:事件名=“内联语句”>按钮
<button v-on:事件名=“处理函数”>按钮
<button v-on:事件名=“处理函数(实参)”>按钮
v-on:
简写为 @
内联语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id="app" > <button @click ="count--" > -</button > <span > {{ count }}</span > <button v-on:click ="count++" > +</button > </div> <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { count : 100 } }) </script >
事件处理函数
注意:
事件处理函数应该写到一个跟data同级的配置项(methods)中
methods中的函数内部的this都指向Vue实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div id="app" > <button > 切换显示隐藏</button > <h1 @click ="fn" v-show ="isShow" > 黑马程序员</h1 > </div> <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { isShow : true }, methods :{ fn ( ){ isshow=!this .isshow } } }) </script >
3.给事件处理函数传参
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 <style> .box { border : 3px solid #000000 ; border-radius : 10px; padding : 20px; margin : 20px; width : 200px; } h3 { margin : 10px 0 20px 0 ; } p { margin : 20px; } </style> <div id ="app" > <div class ="box" > <h3 > 小黑自动售货机</h3 > <button > 可乐5元</button > <button > 咖啡10元</button > <button > 牛奶8元</button > </div > <p > 银行卡余额:{{ money }}元</p > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { money : 100 } }) </script >
十一、属性绑定指令
**作用:**动态设置html的标签属性 比如:src、url、title
语法 :**v-bind:**属性名=“表达式”
**v-bind:**可以简写成 => :
比如,有一个图片,它的 src
属性值,是一个图片地址。这个地址在数据 data 中存储。
则可以这样设置属性值:
<img v-bind:src="url" />
<img :src="url" />
(v-bind可以省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id="app" > <img v-bind:src ="imgUrl" v-bind:title ="msg" alt ="" > <img :src ="imgUrl" :title ="msg" alt ="" > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { imgUrl : './imgs/10-02.png' , msg : 'hello 波仔' } }) </script >
十二、小案例-波仔的学习之旅
需求:默认展示数组中的第一张图片,点击上一页下一页来回切换数组中的图片
实现思路:
1.数组存储图片路径 [‘url1’,‘url2’,‘url3’,…]
2.可以准备个下标index 去数组中取图片地址。
3.通过v-bind给src绑定当前的图片地址
4.点击上一页下一页只需要修改下标的值即可
5.当展示第一张的时候,上一页按钮应该隐藏。展示最后一张的时候,下一页按钮应该隐藏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <div id="app" > <button v-show ="index > 0" @click ="index--" > 上一页</button > <div > <img :src ="list[index]" alt ="" > </div > <button v-show ="index<list.length-1" @click ="index++" > 下一页</button > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { list : [ './imgs/11-00.gif' , './imgs/11-01.gif' , './imgs/11-02.gif' , './imgs/11-03.gif' , './imgs/11-04.png' , './imgs/11-05.png' , ] } }) </script >
十三、列表渲染指令v-for
Vue 提供了 v-for 列表渲染指令,用来辅助开发者基于一个数组来循环渲染一个列表结构。
v-for 指令需要使用 (item, index) in arr
形式的特殊语法,其中:
item 是数组中的每一项
index 是每一项的索引,不需要可以省略
arr 是被遍历的数组
此语法也可以遍历对象和数字
1 2 3 4 5 6 7 8 9 <div v-for ="(value, key, index) in object" >{{value}}</div> value :对象中的值key :对象中的键index :遍历索引从0 开始<p v-for ="item in 10" >{{item}}</p> item从1 开始
十四、小案例-小黑的书架
需求:
1.根据左侧数据渲染出右侧列表(v-for)
2.点击删除按钮时,应该把当前行从列表中删除(获取当前行的id,利用filter进行过滤)
准备代码:
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 <!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"> <h3>小黑的书架</h3> <ul> <li v-for="(item, index) in booksList" :key="item.id"> <span>{{ item.name }}</span> <span>{{ item.author }}</span> <!-- 注册点击事件 → 通过 id 进行删除数组中的 对应项 --> <button @click="del(item.id)">删除</button> </li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script> const app = new Vue({ el: '#app', data: { booksList: [ { id: 1, name: '《红楼梦》', author: '曹雪芹' }, { id: 2, name: '《西游记》', author: '吴承恩' }, { id: 3, name: '《水浒传》', author: '施耐庵' }, { id: 4, name: '《三国演义》', author: '罗贯中' } ] }, methods: { del (id) { // console.log('删除', id) // 通过 id 进行删除数组中的 对应项 → filter(不会改变原数组) // filter: 根据条件,保留满足条件的对应项,得到一个新数组。 // console.log(this.booksList.filter(item => item.id !== id)) this.booksList = this.booksList.filter(item => item.id !== id) } } }) </script> </body> </html>
十五、v-for中的key
语法: key=“唯一值”
作用:给列表项添加的 唯一标识 。便于Vue进行列表项的正确排序复用 。
**为什么加key:**Vue 的默认行为会尝试原地修改元素(就地复用 )
实例代码:
1 2 3 4 5 6 7 <ul> <li v-for ="(item, index) in booksList" :key ="item.id" > <span > {{ item.name }}</span > <span > {{ item.author }}</span > <button @click ="del(item.id)" > 删除</button > </li > </ul>
注意:
key 的值只能是字符串 或 数字类型
key 的值必须具有唯一性
推荐使用 id 作为 key(唯一),不推荐使用 index 作为 key(会变化,不对应)
十六、双向绑定指令
所谓双向绑定就是:
数据改变后,呈现的页面结果会更新
页面结果更新后,数据也会随之而变
作用: 给表单元素 (input、radio、select)使用,双向绑定数据,可以快速 获取 或 设置 表单元素内容
**语法:**v-model=“变量”
**需求:**使用双向绑定实现以下需求
点击登录按钮获取表单中的内容
点击重置按钮清空表单中的内容
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 <!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"> <!-- v-model 可以让数据和视图,形成双向数据绑定 (1) 数据变化,视图自动更新 (2) 视图变化,数据自动更新 可以快速[获取]或[设置]表单元素的内容 --> 账户:<input type="text" v-model="username"> <br><br> 密码:<input type="password" v-model="password"> <br><br> <button @click="login">登录</button> <button @click="reset">重置</button> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script> const app = new Vue({ el: '#app', data: { username: '', password: '' }, methods: { login () { console.log(this.username, this.password) }, reset () { this.username = '' this.password = '' } } }) </script> </body> </html
十七、综合案例-小黑记事本
功能需求:
列表渲染
删除功能
添加功能
底部统计 和 清空
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 <!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/index.css" /> <title > 记事本</title > </head > <body > <section id ="app" > <header class ="header" > <h1 > 小黑记事本</h1 > <input v-model ="todoName" placeholder ="请输入任务" class ="new-todo" /> <button @click ="add" class ="add" > 添加任务</button > </header > <section class ="main" > <ul class ="todo-list" > <li class ="todo" v-for ="(item, index) in list" :key ="item.id" > <div class ="view" > <span class ="index" > {{ index + 1 }} .</span > <label > {{ item.name }} </label > <button @click ="del(item.id)" class ="destroy" > </button > </div > </li > </ul > </section > <footer class ="footer" v-show ="list.length > 0" > <span class ="todo-count" > 合 计:<strong > {{ list.length }} </strong > </span > <button @click ="clear" class ="clear-completed" > 清空任务 </button > </footer > </section > <script src ="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { todoName : '' , list : [ { id : 1 , name : '跑步一公里' }, { id : 2 , name : '跳绳200个' }, { id : 3 , name : '游泳100米' }, ] }, methods : { del (id) { this .list = this .list .filter (item => item.id !== id) }, add () { if (this .todoName .trim () === '' ) { alert ('请输入任务名称' ) return } this .list .unshift ({ id : +new Date (), name : this .todoName }) this .todoName = '' }, clear () { this .list = [] } } }) </script > </body > </html >
day02
一、今日学习目标
1.指令补充
指令修饰符
v-bind对样式增强的操作
v-model应用于其他表单元素
2.computed计算属性
基础语法
计算属性vs方法
计算属性的完整写法
成绩案例
3.watch侦听器
基础写法
完整写法
4.综合案例 (演示)
渲染 / 删除 / 修改数量 / 全选 / 反选 / 统计总价 / 持久化
二、指令修饰符
1.什么是指令修饰符?
所谓指令修饰符就是通过“.”指明一些指令后缀 不同的后缀 封装了不同的处理操作 —> 简化代码
2.按键修饰符
@keyup.enter —>当点击enter键的时候才触发
代码演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div id="app" > <h3 > @keyup.enter → 监听键盘回车事件</h3 > <input v-model ="username" type ="text" > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { username : '' }, methods : { } }) </script >
3.v-model修饰符
v-model.trim —>去除首位空格
v-model.number —>转数字
4.事件修饰符
@事件名.stop —> 阻止冒泡
@事件名.prevent —>阻止默认行为
@事件名.stop.prevent —>可以连用 即阻止事件冒泡也阻止默认行为
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 <style> .father { width : 200px; height : 200px; background-color : pink; margin-top : 20px; } .son { width : 100px; height : 100px; background-color : skyblue; } </style> <div id="app"> <h3>v-model修饰符 .trim .number</h3> 姓名:<input v-model.trim="username" type="text"><br> 年纪:<input v-model.number="age" type="text"><br> <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@2/dist/vue.js"></script> <script> const app = new Vue({ el: '#app', data: { username: '', age: '', }, methods: { fatherFn () { alert('老父亲被点击了') }, sonFn (e) { // e.stopPropagation() alert('儿子被点击了') } } }) </script>
三、v-bind对样式控制的增强-操作class
为了方便开发者进行样式控制, Vue 扩展了 v-bind 的语法,可以针对 class 类名 和 style 行内样式 进行控制 。
1.语法:
1 <div > :class = "对象/数组">这是一个div</div >
2.对象语法
当class动态绑定的是对象 时,键就是类名,值就是布尔值 ,如果值是true ,就有这个类,否则没有这个类
1 <div class ="box" :class ="{ 类名1: 布尔值, 类名2: 布尔值 }" > </div >
适用场景:一个类名,来回切换
3.数组语法
当class动态绑定的是数组 时 → 数组中所有的类,都会添加到盒子上,本质就是一个 class 列表
1 <div class ="box" :class ="[ 类名1, 类名2, 类名3 ]" > </div >
使用场景:批量添加或删除类
4.代码练习
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 <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 > <div id ="app" > <div class ="box" > 黑马程序员</div > <div class ="box" > 黑马程序员</div > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { } }) </script >
四、京东秒杀-tab栏切换导航高亮
1.需求:
当我们点击哪个tab页签时,哪个tab页签就高亮
2.准备代码:
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 <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 > <div id ="app" > <ul > <li v-for ="(item,index) in list" :key ="item.id" @click ="currentindex=index" > <a :class ="{active:index===currentindex}" 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 : { currentindex : 0 , list : [ { id : 1 , name : '京东秒杀' }, { id : 2 , name : '每日特价' }, { id : 3 , name : '品类秒杀' } ] } }) </script >
3.思路:
1.基于数据,动态渲染tab(v-for)
2.准备一个下标 记录高亮的是哪一个 tab
3.基于下标动态切换class的类名
五、v-bind对有样式控制的增强-操作style
1.语法
1 <div class ="box" :style ="{ CSS属性名1: CSS属性值, CSS属性名2: CSS属性值 }" > </div >
2.代码练习
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 <!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 >
3.进度条案例
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 <!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 >
六、v-model在其他表单元素的使用
1.讲解内容:
常见的表单元素都可以用 v-model 绑定关联 → 快速 获取 或 设置 表单元素的值
它会根据 控件类型 自动选取 正确的方法 来更新元素
1 2 3 4 5 6 输入框 input :text ——> value 文本域 textarea ——> value 复选框 input :checkbox ——> checked 单选框 input :radio ——> checked 下拉菜单 select ——> value ...
2.代码准备
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 <!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 > 性别: <input v-model ="gender" type ="radio" name ="gender" value ="1" > 男 <input v-model ="gender" type ="radio" name ="gender" value ="2" > 女 <br > <br > 所在城市: <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 , gender : "2" , cityId : '102' , desc : "" } }) </script > </body > </html >
七、computed计算属性
1.概念
基于现有的数据 ,计算出来的新属性 。 依赖 的数据变化,自动 重新计算。
2.语法
声明在 computed 配置项 中,一个计算属性对应一个函数
使用起来和普通属性一样使用
3.注意
computed配置项和data配置项是同级 的
computed中的计算属性虽然是函数的写法 ,但他依然是个属性
computed中的计算属性不能 和data中的属性同名
使用computed中的计算属性和使用data中的属性是一样的用法
computed中计算属性内部的this 依然指向的是Vue实例
4.案例
比如我们可以使用计算属性实现下面这个业务场景
5.代码准备
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 <!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 () { let total = this .list .reduce ((sum, item ) => sum + item.num , 0 ) return total } } }) </script > </body > </html >
八、computed计算属性 VS methods方法
1.computed计算属性
作用:封装了一段对于数据 的处理,求得一个结果
语法:
写在computed配置项中
作为属性,直接使用
js中使用计算属性: this.计算属性
模板中使用计算属性:
2.methods计算属性
作用:给Vue实例提供一个方法 ,调用以处理业务逻辑 。
语法:
写在methods配置项中
作为方法调用
1 2 3 - js中调用:this.方法名() - 模板中调用 {{方法名 ()}} 或者 @事件名=“方法名” -
3.计算属性的优势
缓存特性(提升性能)
计算属性会对计算出来的结果缓存,再次使用直接读取缓存,
依赖项变化了,会自动重新计算 → 并再次缓存
methods没有缓存特性
通过代码比较
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 <style > table { border : 1px solid #000 ; text-align : center; width : 300px ; } th ,td { border : 1px solid #000 ; } h3 { position : relative; } span { position : absolute; left : 145px ; top : -4px ; width : 16px ; height : 16px ; color : white; font-size : 12px ; text-align : center; border-radius : 50% ; background-color : #e63f32 ; } </style > <div id ="app" > <h3 > 小黑的礼物清单🛒<span > ?</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 () { let total = this .list .reduce ((sum, item ) => sum + item.num , 0 ) return total } } }) </script >
4.总结
1.computed有缓存特性 ,methods没有缓存
2.当一个结果依赖其他多个值时,推荐使用计算属性
3.当处理业务逻辑时,推荐使用methods方法,比如事件的处理函数
九、计算属性的完整写法
既然计算属性也是属性,能访问,应该也能修改了?
计算属性默认的简写,只能读取访问,不能 “修改”
如果要 “修改” → 需要写计算属性的完整写法
完整写法代码演示
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 <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@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) { this .firstName = value.slice (0 , 1 ) this .lastName = value.slice (1 ) } } } }) </script >
十、综合案例-成绩案例
功能描述:
1.渲染功能
2.删除功能
3.添加功能
4.统计总分,求平均分
思路分析:
1.渲染功能 v-for :key v-bind:动态绑定class的样式
2.删除功能 v-on绑定事件, 阻止a标签的默认行为
3.v-model的修饰符 .trim、 .number、 判断数据是否为空后 再添加、添加后清空文本框的数据
4.使用计算属性computed 计算总分和平均分的值
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 <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 ="http://www.baidu.com" > 删除</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 @click ="add" 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 : 62 }, { id : 7 , subject : '数学' , score : 89 }, { id : 12 , subject : '英语' , score : 70 }, ], subject : '' , score : '' }, computed : { totalScore ( ) { return this .list .reduce ((sum, item ) => sum + item.score , 0 ) }, averageScore () { if (this .list .length === 0 ) { return 0 } return (this .totalScore / this .list .length ).toFixed (2 ) } }, 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 = '' } } }) </script > </body > </html >
十一、watch侦听器(监视器)
1.作用:
监视数据变化 ,执行一些业务逻辑或异步操作
2.语法:
watch同样声明在跟data同级的配置项中
简单写法: 简单类型数据直接监视
完整写法:添加额外配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 data : { words : '苹果' , obj : { words : '苹果' } }, watch : { 数据属性名 (newValue, oldValue) { 一些业务逻辑 或 异步操作。 }, '对象.属性名' (newValue, oldValue) { 一些业务逻辑 或 异步操作。 } }
3.侦听器代码准备
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 <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 > <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 > const app = new Vue ({ el : '#app' , data : { words : '' }, }) </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 <script> const app = new Vue ({ el : '#app' , data : { obj : { words : '' }, result : '' , }, watch : { 'obj.words' (newValue) { 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 (res.data .data ) }, 300 ) } } }) </script>
十三、watch侦听器
1.语法
完整写法 —>添加额外的配置项
deep:true 对复杂类型进行深度监听
immdiate:true 初始化 立刻执行一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 data : { obj : { words : '苹果' , lang : 'italy' }, }, watch : { 对象: { deep : true , immdiate :true , handler (newValue) { console .log (newValue) } } }
2.需求
当文本框输入的时候 右侧翻译内容要时时变化
当下拉框中的语言发生变化的时候 右侧翻译的内容依然要时时变化
如果文本框中有默认值的话要立即翻译
3.代码实现
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 <script> const app = new Vue ({ el : '#app' , data : { obj : { words : '小黑' , lang : 'italy' }, result : '' , }, watch : { obj : { deep : true , immediate : true , handler (newValue) { 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 (res.data .data ) }, 300 ) } } } }) </script>
4.总结
watch侦听器的写法有几种?
1.简单写法
1 2 3 4 5 6 7 8 watch : { 数据属性名 (newValue, oldValue) { 一些业务逻辑 或 异步操作。 }, '对象.属性名' (newValue, oldValue) { 一些业务逻辑 或 异步操作。 } }
2.完整写法
1 2 3 4 5 6 7 8 9 watch : { 数据属性名: { deep : true , immediate : true , handler (newValue) { console .log (newValue) } } }
网页在线翻译器
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 <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@2/dist/vue.js" > </script > <script src ="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { obj : { words : '小黑' , lang : 'italy' }, result : '' , }, watch : { obj : { deep : true , immediate : true , handler (newValue) { 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 (res.data .data ) }, 300 ) } } } }) </script >
十四、综合案例
购物车案例
需求说明:
渲染功能
删除功能
修改个数
全选反选
统计 选中的 总价 和 总数量
持久化到本地
实现思路:
1.基本渲染: v-for遍历、:class动态绑定样式
2.删除功能 : v-on 绑定事件,获取当前行的id
3.修改个数 : v-on绑定事件,获取当前行的id,进行筛选出对应的项然后增加或减少
4.全选反选
必须所有的小选框都选中,全选按钮才选中 → every
如果全选按钮选中,则所有小选框都选中
如果全选取消,则所有小选框都取消选中
声明计算属性,判断数组中的每一个checked属性的值,看是否需要全部选
5.统计 选中的 总价 和 总数量 :通过计算属性来计算选中的 总价和总数量
6.持久化到本地: 在数据变化时都要更新下本地存储 watch
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 <!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" > <div class ="banner-box" > <img src ="http://autumnfish.cn/static/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 v-for ="(item, index) in fruitList" :key ="item.id" class ="tr" :class ="{ active: item.isChecked }" > <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 :disabled ="item.num <= 1" class ="decrease" @click ="sub(item.id)" > - </button > <span class ="my-input__inner" > {{ item.num }} </span > <button class ="increase" @click ="add(item.id)" > + </button > </div > </div > <div class ="td" > {{ item.num * item.price }} </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@2/dist/vue.js" > </script > <script > const defaultArr = [ { id : 1 , icon : 'http://autumnfish.cn/static/火龙果.png' , isChecked : true , num : 2 , price : 6 , }, { id : 2 , icon : 'http://autumnfish.cn/static/荔枝.png' , isChecked : false , num : 7 , price : 20 , }, { id : 3 , icon : 'http://autumnfish.cn/static/榴莲.png' , isChecked : false , num : 3 , price : 40 , }, { id : 4 , icon : 'http://autumnfish.cn/static/鸭梨.png' , isChecked : true , num : 10 , price : 3 , }, { id : 5 , icon : 'http://autumnfish.cn/static/樱桃.png' , isChecked : false , num : 20 , price : 34 , }, ] const app = new Vue ({ el : '#app' , data : { fruitList : JSON .parse (localStorage .getItem ('list' )) || defaultArr, }, computed : { isAll : { get () { 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 ) => { if (item.isChecked ) { return sum + item.num * item.price } else { return sum } }, 0 ) } }, methods : { del (id) { this .fruitList = this .fruitList .filter (item => item.id !== id) }, add (id) { const fruit = this .fruitList .find (item => item.id === id) fruit.num ++ }, sub (id) { const fruit = this .fruitList .find (item => item.id === id) fruit.num -- } }, watch : { fruitList : { deep : true , handler (newValue) { localStorage .setItem ('list' , JSON .stringify (newValue)) } } } }) </script > </body > </html >
day03
一、今日目标
1.生命周期
生命周期介绍
生命周期的四个阶段
生命周期钩子
声明周期案例
2.综合案例-小黑记账清单
列表渲染
添加/删除
饼图渲染
3.工程化开发入门
工程化开发和脚手架
项目运行流程
组件化
组件注册
4.综合案例-小兔仙首页
拆分模块-局部注册
结构样式完善
拆分组件 – 全局注册
二、Vue生命周期
思考:什么时候可以发送初始化渲染请求?(越早越好)什么时候可以开始操作dom?(至少dom得渲染出来)
Vue生命周期:就是一个Vue实例从创建 到 销毁 的整个过程。
生命周期四个阶段:① 创建 ② 挂载 ③ 更新 ④ 销毁
1.创建阶段:创建响应式数据
2.挂载阶段:渲染模板
3.更新阶段:修改数据,更新视图
4.销毁阶段:销毁Vue实例
三、Vue生命周期钩子
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 40 41 42 43 44 <script > const app = new Vue ({ el : '#app' , data : { count : 100 , title : '计数器' }, beforeCreate () { console .log ('beforeCreate 响应式数据准备好之前' , this .count ) }, created () { console .log ('created 响应式数据准备好之后' , this .count ) }, beforeMount () { console .log ('beforeMount 模板渲染之前' , document .querySelector ('h3' ).innerHTML ) }, mounted () { console .log ('mounted 模板渲染之后' , document .querySelector ('h3' ).innerHTML ) }, beforeUpdate () { console .log ('beforeUpdate 数据修改了,视图还没更新' , document .querySelector ('span' ).innerHTML ) }, updated () { console .log ('updated 数据修改了,视图已经更新' , document .querySelector ('span' ).innerHTML ) }, beforeDestroy () { console .log ('beforeDestroy, 卸载前' ) console .log ('清除掉一些Vue以外的资源占用,定时器,延时器...' ) }, destroyed () { console .log ('destroyed,卸载后' ) } }) </script >
四、生命周期钩子小案例
1.在created中发送数据
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 <div id ="app" > <ul > <li v-for ="(item, index) in list" :key ="item.id" class ="news" > <div class ="left" > <div class ="title" > {{ item.title }}</div > <div class ="info" > <span > {{ item.source }}</span > <span > {{ item.time }}</span > </div > </div > <div class ="right" > <img :src ="item.img" alt ="" > </div > </li > </ul > </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 > const app = new Vue ({ el : '#app' , data : { list : [] }, async created () { const res = await axios.get ('http://hmajax.itheima.net/api/news' ) this .list = res.data .data } }) </script >
2.在mounted中获取焦点
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 <body > <div class ="container" id ="app" > <div class ="search-container" > <img src ="https://www.itheima.com/images/logo.png" alt ="" > <div class ="search-box" > <input type ="text" v-model ="words" id ="inp" > <button > 搜索一下</button > </div > </div > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { words : '' }, mounted () { document .querySelector ('#inp' ).focus () } }) </script > </body >
五、案例-小黑记账清单
1.需求图示:
2.需求分析
1.基本渲染
2.添加功能
3.删除功能
4.饼图渲染
3.思路分析
1.基本渲染
立刻发送请求获取数据 created
拿到数据,存到data的响应式数据中
结合数据,进行渲染 v-for
消费统计 —> 计算属性
2.添加功能
收集表单数据 v-model,使用指令修饰符处理数据
给添加按钮注册点击事件,对输入的内容做非空判断,发送请求
请求成功后,对文本框内容进行清空
重新渲染列表
3.删除功能
注册点击事件,获取当前行的id
根据id发送删除请求
需要重新渲染
4.饼图渲染
初始化一个饼图 echarts.init(dom) mounted钩子中渲染
根据数据试试更新饼图 echarts.setOptions({…})
4.代码准备
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 <div id ="app" > <div class ="contain" > <div class ="list-box" > <form class ="my-form" > <input v-model.trim ="name" type ="text" class ="form-control" placeholder ="消费名称" /> <input v-model.number ="price" type ="text" class ="form-control" placeholder ="消费价格" /> <button @click ="add" type ="button" class ="btn btn-primary" > 添加账单</button > </form > <table class ="table table-hover" > <thead > <tr > <th > 编号</th > <th > 消费名称</th > <th > 消费价格</th > <th > 操作</th > </tr > </thead > <tbody > <tr v-for ="(item, index) in list" :key ="item.id" > <td > {{ index + 1 }}</td > <td > {{ item.name }}</td > <td :class ="{ red: item.price > 500 }" > {{ item.price.toFixed(2) }}</td > <td > <a @click ="del(item.id)" href ="javascript:;" > 删除</a > </td > </tr > </tbody > <tfoot > <tr > <td colspan ="4" > 消费总计: {{ totalPrice.toFixed(2) }}</td > </tr > </tfoot > </table > </div > <div class ="echarts-box" id ="main" > </div > </div > </div > <script src ="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js" > </script > <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 > const app = new Vue ({ el : '#app' , data : { list : [], name : '' , price : '' }, computed : { totalPrice () { return this .list .reduce ((sum, item ) => sum + item.price , 0 ) } }, created () { this .getList () }, mounted () { this .myChart = echarts.init (document .querySelector ('#main' )) this .myChart .setOption ({ title : { text : '消费账单列表' , left : 'center' }, tooltip : { trigger : 'item' }, legend : { orient : 'vertical' , left : 'left' }, series : [ { name : '消费账单' , type : 'pie' , radius : '50%' , data : [ ], emphasis : { itemStyle : { shadowBlur : 10 , shadowOffsetX : 0 , shadowColor : 'rgba(0, 0, 0, 0.5)' } } } ] }) }, methods : { async getList () { const res = await axios.get ('https://applet-base-api-t.itheima.net/bill' , { params : { creator : '小黑' } }) this .list = res.data .data this .myChart .setOption ({ series : [ { data : this .list .map (item => ({ value : item.price , name : item.name })) } ] }) }, async add () { if (!this .name ) { alert ('请输入消费名称' ) return } if (typeof this .price !== 'number' ) { alert ('请输入正确的消费价格' ) return } const res = await axios.post ('https://applet-base-api-t.itheima.net/bill' , { creator : '小黑' , name : this .name , price : this .price }) this .getList () this .name = '' this .price = '' }, async del (id) { const res = await axios.delete (`https://applet-base-api-t.itheima.net/bill/${id} ` ) this .getList () } } }) </script >
六、工程化开发和脚手架
1.开发Vue的两种方式
核心包传统开发模式:基于html / css / js 文件,直接引入核心包,开发 Vue。
工程化开发模式:基于构建工具(例如:webpack)的环境中开发Vue。
工程化开发模式优点:
提高编码效率,比如使用JS新语法、Less/Sass、Typescript等通过webpack都可以编译成浏览器识别的ES3/ES5/CSS等
工程化开发模式问题:
webpack配置不简单
雷同 的基础配置
缺乏统一的标准
为了解决以上问题,所以我们需要一个工具,生成标准化的配置
2.脚手架Vue CLI
基本介绍:
Vue CLI 是Vue官方提供的一个全局命令工具
可以帮助我们快速创建 一个开发Vue项目的标准化基础架子 。【集成了webpack配置】
好处:
开箱即用,零配置
内置babel等工具
标准化的webpack配置
使用步骤:
全局安装(只需安装一次即可) yarn global add @vue/cli 或者 npm i @vue/cli -g
查看vue/cli版本: vue --version
创建项目架子:vue create vue-name (项目名不能使用中文)
启动项目:yarn serve 或者 npm run serve (命令不固定,找package.json)
3.安装报错
windows上使用vue命令时,
报错:vue无法加载文件C:\Users\Administrator\AppData\Roaming\npm\vue.ps1因为在此系统上禁止运行脚本…
已经是使用管理员身份运行。
原因:windows上设置了策略是禁止未受信任的脚本运行,所以修改策略。
解决:
(前提:管理员身份运行终端了powershell)
执行命令:set-ExecutionPolicy RemoteSigned
选择Y
七、项目目录介绍和运行流程
1.项目目录介绍
虽然脚手架中的文件有很多,目前咱们只需人事三个文件即可
main.js 入口文件
App.vue App根组件
index.html 模板文件
2.运行流程
八、组件化开发
组件化:一个页面可以拆分成一个个组件,每个组件有着自己独立的结构、样式、行为。
好处:便于维护,利于复用 → 提升开发效率。
组件分类:普通组件、根组件。
比如:下面这个页面,可以把所有的代码都写在一个页面中,但是这样显得代码比较混乱,难易维护。咱们可以按模块进行组件划分
总结:
组件化的好处是什么?
组件的分类?
九、根组件 App.vue
1.根组件介绍
整个应用最上层的组件,包裹所有普通小组件
2.组件是由三部分构成
3.总结
App组件包含哪三部分?
十、普通组件的注册使用-局部注册
1.特点:
只能在注册的组件内使用
2.步骤:
创建.vue文件(三个组成部分)
在使用的组件内先导入再注册,最后使用
3.使用方式:
当成html标签使用即可 <组件名></组件名>
4.注意:
组件名规范 —> 大驼峰命名法, 如 HmHeader
5.语法:
1 2 3 4 5 6 7 8 9 10 11 import 组件对象 from '.vue文件路径' import HmHeader from './components/HmHeader' export default { components : { '组件名' : 组件对象, HmHeader :HmHeaer , HmHeader } }
6.练习
在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 <template> <div class="hm-header"> 我是hm-header </div> </template> <script> export default { } </script> <style> .hm-header { height: 100px; line-height: 100px; text-align: center; font-size: 30px; background-color: #8064a2; color: white; } </style>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div class="hm-main"> 我是hm-main </div> </template> <script> export default { } </script> <style> .hm-main { height: 400px; line-height: 400px; text-align: center; font-size: 30px; background-color: #f79646; color: white; margin: 20px 0; } </style>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <div class="hm-footer"> 我是hm-footer </div> </template> <script> export default { } </script> <style> .hm-footer { height: 100px; line-height: 100px; text-align: center; font-size: 30px; background-color: #4f81bd; color: white; } </style>
7.总结
A组件内部注册的局部组件能在B组件使用吗
局部注册组件的步骤是什么
使用组件时 应该按照什么命名法
十一、普通组件的注册使用-全局注册
1.特点:
全局注册的组件,在项目的任何组件 中都能使用
2.步骤
创建.vue组件(三个组成部分)
main.js 中进行全局注册
3.使用方式
当成HTML标签直接使用
<组件名></组件名>
4.注意
组件名规范 —> 大驼峰命名法, 如 HmHeader
5.语法
Vue.component(‘组件名’, 组件对象)
例:
1 2 3 import HmButton from './components/HmButton' Vue .component ('HmButton' , HmButton )
6.练习
在以下3个局部组件中是展示一个通用按钮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <button class="hm-button">通用按钮</button> </template> <script> export default { } </script> <style> .hm-button { height: 50px; line-height: 50px; padding: 0 20px; background-color: #3bae56; border-radius: 5px; color: white; border: none; vertical-align: middle; cursor: pointer; } </style>
7.总结
1.全局注册组件应该在哪个文件中注册以及语法是什么?
2.全局组件在项目中的任何一个组件中可不可以使用?
十二、综合案例
1.小兔仙首页启动项目演示
2.小兔仙组件拆分示意图
3.开发思路
分析页面,按模块拆分组件,搭架子 (局部或全局注册)
根据设计图,编写组件 html 结构 css 样式 (已准备好)
拆分封装通用小组件 (局部或全局注册)
将来 → 通过 js 动态渲染,实现功能
4.快捷键
alt+shift+鼠标左键光标,再向下点,可以全部选中
先按crtl+K,再按ctrl+0,可以全部折叠
先按crtl+K,再按ctrl+j,可以全部展开
day04
一、学习目标
1.组件的三大组成部分(结构/样式/逻辑)
scoped解决样式冲突/data是一个函数
2.组件通信
组件通信语法
父传子
子传父
非父子通信(扩展)
3.综合案例:小黑记事本(组件版)
拆分组件
列表渲染
数据添加
数据删除
列表统计
清空
持久化
4.进阶语法
v-model原理
v-model应用于组件
sync修饰符
ref和$refs
$nextTick
二、scoped解决样式冲突
1.默认情况 :
写在组件中的样式会 全局生效 → 因此很容易造成多个组件之间的样式冲突问题。
全局样式 : 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响
局部样式 : 可以给组件加上scoped 属性,可以让样式只作用于当前组件
2.代码演示
BaseOne.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div class="base-one"> BaseOne </div> </template> <script> export default { } </script> <style scoped> </style>
BaseTwo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <div class="base-one"> BaseTwo </div> </template> <script> export default { } </script> <style scoped> </style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div id="app"> <BaseOne></BaseOne> <BaseTwo></BaseTwo> </div> </template> <script> import BaseOne from './components/BaseOne' import BaseTwo from './components/BaseTwo' export default { name: 'App', components: { BaseOne, BaseTwo } } </script>
3.scoped原理
当前组件内标签都被添加data-v-hash值 的属性
css选择器都被添加 [data-v-hash值 ] 的属性选择器
最终效果: 必须是当前组件的元素 , 才会有这个自定义属性, 才会被这个样式作用到
4.总结
style的默认样式是作用到哪里的?
scoped的作用是什么?
style中推不推荐加scoped?
三、data必须是一个函数
1、data为什么要写成函数
一个组件的 data 选项必须是一个函数 。目的是为了:保证每个组件实例,维护独立 的一份数据 对象。
每次创建新的组件实例,都会新执行一次data 函数 ,得到一个新对象。
2.代码演示
BaseCount.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div class="base-count"> <button @click="count--">-</button> <span>{{ count }}</span> <button @click="count++">+</button> </div> </template> <script> export default { data: function () { return { count: 100, } }, } </script> <style> .base-count { margin: 20px; } </style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="app"> <BaseCount></BaseCount> </div> </template> <script> import BaseCount from './components/BaseCount' export default { components: { BaseCount, }, } </script> <style> </style>
3.总结
data写成函数的目的是什么?
四、组件通信
1.什么是组件通信?
组件通信,就是指组件与组件 之间的数据传递
组件的数据是独立的,无法直接访问其他组件的数据。
想使用其他组件的数据,就需要组件通信
2.组件之间如何通信
思考:
组件之间有哪些关系?
对应的组件通信方案有哪几类?
3.组件关系分类
父子关系
非父子关系
4.通信解决方案
5.父子通信流程
父组件通过 props 将数据传递给子组件
子组件利用 $emit 通知父组件修改更新
6.父向子通信代码示例
父组件通过props 将数据传递给子组件
父组件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 class="app" style="border: 3px solid #000; margin: 10px"> 我是APP组件 <!-- 1.给组件标签,添加属性方式 赋值 --> <Son :title="myTitle"></Son> </div> </template> <script> import Son from './components/Son.vue' export default { name: 'App', data() { return { myTitle: '学前端,就来黑马程序员', } }, components: { Son, }, } </script> <style> </style>
子组件Son.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div class="son" style="border:3px solid #000;margin:10px"> <!-- 3.直接使用props的值 --> 我是Son组件 {{title}} </div> </template> <script> export default { name: 'Son-Child', // 2.通过props来接受 props:['title'] } </script> <style> </style>
父向子传值步骤
给子组件以添加属性的方式传值
子组件内部通过props接收
模板中直接使用 props接收的值
7.子向父通信代码示例
子组件利用 $emit 通知父组件,进行修改更新
子向父传值步骤
$emit触发事件,给父组件发送消息通知
父组件监听$emit触发的事件
提供处理函数,在函数的性参中获取传过来的参数
父组件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 <template > <div class ="app" style ="border: 3px solid #000; margin: 10px" > 我是APP组件 <Son :title ="myTitle" @changTitle ="handleChange" > </Son > </div > </template > <script > import Son from './components/Son.vue' export default { name : 'App' , data ( ) { return { myTitle : '学前端,就来黑马程序员' , } }, components : { Son , }, methods : { handleChange (newTitle ) { this .myTitle = newTitle }, }, } </script > <style > </style >
子组件.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template > <div class ="son" style ="border: 3px solid #000; margin: 10px" > 我是Son组件 {{ title }} <button @click ="changeFn" > 修改title</button > </div > </template > <script > export default { name : 'Son-Child' , props : ['title' ], methods : { changeFn ( ) { this .$emit('changTitle' ,'传智教育' ) }, }, } </script > <style > </style >
8.总结
组件关系分类有哪两种
父子组件通信的流程是什么?
父向子
子向父
五、什么是props
1.Props 定义
组件上 注册的一些 自定义属性
2.Props 作用
向子组件传递数据
3.特点
可以 传递 任意数量 的prop
可以 传递 任意类型 的prop
4.代码演示
父组件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 <template> <div class="app"> <UserInfo :username="username" :age="age" :isSingle="isSingle" :car="car" :hobby="hobby" ></UserInfo> </div> </template> <script> import UserInfo from './components/UserInfo.vue' export default { data() { return { username: '小帅', age: 28, isSingle: true, car: { brand: '宝马', }, hobby: ['篮球', '足球', '羽毛球'], } }, components: { UserInfo, }, } </script> <style> </style>
子组件UserInfo.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> <div class="userinfo"> <h3>我是个人信息组件</h3> <div>姓名:</div> <div>年龄:</div> <div>是否单身:</div> <div>座驾:</div> <div>兴趣爱好:</div> </div> </template> <script> export default { } </script> <style> .userinfo { width: 300px; border: 3px solid #000; padding: 20px; } .userinfo > div { margin: 20px 10px; } </style>
六、props校验
1.思考
组件的props可以乱传吗
2.作用
为组件的 prop 指定验证要求 ,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误
3.语法
4.代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <div class="app"> <BaseProgress :w="width"></BaseProgress> </div> </template> <script> import BaseProgress from './components/BaseProgress.vue' export default { data() { return { width: 30, } }, components: { BaseProgress, }, } </script> <style> </style>
BaseProgress.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 40 41 <template> <div class="base-progress"> <div class="inner" :style="{ width: w + '%' }"> <span>{{ w }}%</span> </div> </div> </template> <script> export default { props: {w:Number} , } </script> <style scoped> .base-progress { height: 26px; width: 400px; border-radius: 15px; background-color: #272425; border: 3px solid #272425; box-sizing: border-box; margin-bottom: 30px; } .inner { position: relative; background: #379bff; border-radius: 15px; height: 25px; box-sizing: border-box; left: -3px; top: -2px; } .inner span { position: absolute; right: 0; top: 26px; } </style>
七、props校验完整写法
1.语法
1 2 3 4 5 6 7 8 9 10 11 props: { 校验的属性名: { type: 类型, // Number String Boolean ... required: true, // 是否必填 default: 默认值, // 默认值 validator (value) { // 自定义校验逻辑 return 是否通过校验 } } },
2.代码实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script> export default { // 完整写法(类型、默认值、非空、自定义校验) props: { w: { type: Number, //required: true, default: 0, validator(val) { // console.log(val) if (val >= 100 || val <= 0) { console.error('传入的范围必须是0-100之间') return false } else { return true } }, }, }, } </script>
3.注意
1.default和required一般不同时写(因为当时必填项时,肯定是有值的)
2.default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
八、props&data、单向数据流
1.共同点
都可以给组件提供数据
2.区别
data 的数据是自己 的 → 随便改
prop 的数据是外部 的 → 不能直接改,要遵循 单向数据流
3.单向数据流:
父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
4.代码演示
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> <div class="app"> <BaseCount :count="count" @changeCount="handleChange"></BaseCount> </div> </template> <script> import BaseCount from './components/BaseCount.vue' export default { components:{ BaseCount }, data(){ return { count:100 } }, methods:{ handleChange(newVal){ // console.log(newVal); this.count = newVal } } } </script> <style> </style>
BaseCount.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 <template> <div class="base-count"> <button @click="handleSub">-</button> <span>{{ count }}</span> <button @click="handleAdd">+</button> </div> </template> <script> export default { // 1.自己的数据随便修改 (谁的数据 谁负责) // data () { // return { // count: 100, // } // }, // 2.外部传过来的数据 不能随便修改 props: { count: { type: Number, }, }, methods: { handleSub() { this.$emit('changeCount', this.count - 1) }, handleAdd() { this.$emit('changeCount', this.count + 1) }, }, } </script> <style> .base-count { margin: 20px; } </style>
5.口诀
谁的数据谁负责
九、综合案例-组件拆分
1.需求说明
拆分基础组件
渲染待办任务
添加任务
删除任务
底部合计 和 清空功能
持久化存储
2.拆分基础组件
咱们可以把小黑记事本原有的结构拆成三部分内容:头部(TodoHeader)、列表(TodoMain)、底部(TodoFooter)
十、综合案例-列表渲染
思路分析:
提供数据:提供在公共的父组件 App.vue
通过父传子,将数据传递给TodoMain
利用v-for进行渲染
十一、综合案例-添加功能
思路分析:
收集表单数据 v-model
监听时间 (回车+点击 都要进行添加)
子传父,将任务名称传递给父组件App.vue
父组件接受到数据后 进行添加 unshift (自己的数据自己负责)
十二、综合案例-删除功能
思路分析:
监听时间(监听删除的点击)携带id
子传父,将删除的id传递给父组件App.vue
进行删除 filter (自己的数据自己负责)
十三、综合案例-底部功能及持久化存储
思路分析:
底部合计:父组件传递list到底部组件 —>展示合计
清空功能:监听事件 —> 子组件 通知父组件 —>父组件清空
持久化存储:watch监听数据变化,持久化到本地
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 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 <template > <section id ="app" > <TodoHeader @add ="handleAdd" > </TodoHeader > <TodoMain :list ="list" @del ="handelDel" > </TodoMain > <TodoFooter :list ="list" @clear ="clear" > </TodoFooter > </section > </template > <script > import TodoHeader from './components/TodoHeader.vue' import TodoMain from './components/TodoMain.vue' import TodoFooter from './components/TodoFooter.vue' export default { data ( ) { return { list : JSON .parse (localStorage .getItem ('list' )) || [ { id : 1 , name : '打篮球' }, { id : 2 , name : '看电影' }, { id : 3 , name : '逛街' }, ], } }, components : { TodoHeader , TodoMain , TodoFooter , }, watch : { list : { deep : true , handler (newVal ) { localStorage .setItem ('list' , JSON .stringify (newVal)) }, }, }, methods : { handleAdd (todoName ) { this .list .unshift ({ id : +new Date (), name : todoName, }) }, handelDel (id ) { this .list = this .list .filter ((item ) => item.id !== id) }, clear ( ) { this .list = [] }, }, } </script > <style > </style >
十四、非父子通信-event bus 事件总线
1.作用
非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)
2.步骤
创建一个都能访问的事件总线 (空Vue实例)
1 2 3 import Vue from 'vue' const Bus = new Vue ()export default Bus
A组件(接受方),监听Bus的 $on事件
1 2 3 4 5 created () { Bus.$on('sendMsg', (msg) => { this.msg = msg }) }
B组件(发送方),触发Bus的$emit事件
1 Bus.$emit('sendMsg', '这是一个消息')
3.代码示例
EventBus.js
1 2 3 import Vue from 'vue' const Bus = new Vue ()export default Bus
BaseA.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 <template> <div class="base-a"> 我是A组件(接受方) <p>{{msg}}</p> </div> </template> <script> import Bus from '../utils/EventBus' export default { data() { return { msg: '', } }, created() { Bus.$on('sendMsg', (msg) => { // console.log(msg) this.msg = msg }) }, } </script> <style scoped> .base-a { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
BaseB.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> <div class="base-b"> <div>我是B组件(发布方)</div> <button @click="sendMsgFn">发送消息</button> </div> </template> <script> import Bus from '../utils/EventBus' export default { methods: { sendMsgFn() { Bus.$emit('sendMsg', '今天天气不错,适合旅游') }, }, } </script> <style scoped> .base-b { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
Basec.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 <template > <div class ="base-c" > 我是C组件(接受方) <p > {{msg }} </p > </div > </template > <script > import Bus from '../utils/EventBus' export default { data ( ) { return { msg : '' , } }, created ( ) { Bus .$on('sendMsg' , (msg ) => { this .msg = msg }) }, } </script > <style scoped > .base-c { width : 200px ; height : 200px ; border : 3px solid #000 ; border-radius : 3px ; margin : 10px ; } </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 <template> <div class="app"> <BaseA></BaseA> <BaseB></BaseB> <BaseC></BaseC> </div> </template> <script> import BaseA from './components/BaseA.vue' import BaseB from './components/BaseB.vue' import BaseC from './components/BaseC.vue' export default { components:{ BaseA, BaseB, BaseC } } </script> <style> </style>
4.总结
1.非父子组件传值借助什么?
2.什么是事件总线
3.发送方应该调用事件总线的哪个方法
4.接收方应该调用事件总线的哪个方法
5.一个组件发送数据,可不可以被多个组件接收
十五、非父子通信-provide&inject
1.作用
跨层级共享数据
2.场景
3.语法
父组件 provide提供数据
1 2 3 4 5 6 7 8 9 10 export default { provide () { return { color : this .color , userInfo : this .userInfo , } } }
2.子/孙组件 inject获取数据
1 2 3 4 5 6 export default { inject : ['color' ,'userInfo' ], created () { console .log (this .color , this .userInfo ) } }
4.注意
provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)
子/孙组件通过inject获取的数据,不能在自身组件内修改
十六、v-model原理
1.原理:
v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写
1 2 3 4 5 6 7 8 <template> <div id="app" > <input v-model="msg" type="text"> <input :value="msg" @input="msg = $event.target.value" type="text"> </div> </template>
2.作用:
提供数据的双向绑定
数据变,视图跟着变 :value
视图变,数据跟着变 @input
3.注意
$event 用于在模板中,获取事件的形参
4.代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div class="app"> <input type="text" v-model="msg1" /> <br /> <!-- v-model的底层其实就是:value和 @input的简写 --> <input type="text" :value="msg2" @input="msg2 = $event.target.value" /> </div> </template> <script> export default { data() { return { msg1: '', msg2: '', } }, } </script> <style> </style>
5.v-model使用在其他表单元素上的原理
不同的表单元素, v-model在底层的处理机制是不一样的。比如给checkbox使用v-model
底层处理的是 checked属性和change事件。
不过咱们只需要掌握应用在文本框上的原理即可
十七、表单类组件封装
1.需求目标
实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)
2.代码演示
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 class="app"> <BaseSelect :selectId="selectId" @changeCity="selectId = $event" ></BaseSelect> </div> </template> <script> import BaseSelect from './components/BaseSelect.vue' export default { data() { return { selectId: '102', } }, components: { BaseSelect, }, } </script> <style> </style>
BaseSelect.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> <div> <select :value="selectId" @change="selectCity"> <option value="101">北京</option> <option value="102">上海</option> <option value="103">武汉</option> <option value="104">广州</option> <option value="105">深圳</option> </select> </div> </template> <script> export default { props: { selectId: String, }, methods: { selectCity(e) { this.$emit('changeCity', e.target.value) }, }, } </script> <style> </style>
十八、v-model简化代码
1.目标:
父组件通过v-model 简化代码 ,实现子组件和父组件数据 双向绑定
2.如何简化:
v-model其实就是 :value和@input事件的简写
子组件:props通过value接收数据 ,事件触发 input
父组件:v-model直接绑定数据
3.代码示例
子组件
1 2 3 4 5 6 7 8 9 <select :value="value" @change="handleChange">...</select> props: { value: String }, methods: { handleChange (e) { this.$emit('input', e.target.value) } }
父组件
1 <BaseSelect v-model="selectId"></BaseSelect>
十九、.sync修饰符
1.作用
可以实现 子组件 与 父组件数据 的 双向绑定 ,简化代码
简单理解:子组件可以修改父组件传过来的props值
2.场景
封装弹框类的基础组件, visible属性 true显示 false隐藏
3.本质
.sync修饰符 就是 :属性名 和 @update:属性名 合写
4.语法
父组件
1 2 3 4 5 6 7 8 //.sync写法 <BaseDialog :visible.sync="isShow" /> -------------------------------------- //完整写法 <BaseDialog :visible="isShow" @update:visible="isShow = $event" />
子组件
1 2 3 4 5 props: { visible: Boolean }, this.$emit('update:visible', false)
5.代码示例
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 <template> <div class="app"> <button @click="openDialog">退出按钮</button> <!-- isShow.sync => :isShow="isShow" @update:isShow="isShow=$event" --> <BaseDialog :isShow.sync="isShow"></BaseDialog> </div> </template> <script> import BaseDialog from './components/BaseDialog.vue' export default { data() { return { isShow: false, } }, methods: { openDialog() { this.isShow = true // console.log(document.querySelectorAll('.box')); }, }, components: { BaseDialog, }, } </script> <style> </style>
BaseDialog.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 <template> <div class="base-dialog-wrap" v-show="isShow"> <div class="base-dialog"> <div class="title"> <h3>温馨提示:</h3> <button class="close" @click="closeDialog">x</button> </div> <div class="content"> <p>你确认要退出本系统么?</p> </div> <div class="footer"> <button>确认</button> <button>取消</button> </div> </div> </div> </template> <script> export default { props: { isShow: Boolean, }, methods:{ closeDialog(){ this.$emit('update:isShow',false) } } } </script>
6.总结
1.父组件如果想让子组件修改传过去的值 必须加什么修饰符?
2.子组件要修改父组件的props值 必须使用什么语法?
二十、ref和$refs
1.作用
利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
2.特点:
查找范围 → 当前组件内(更精确稳定)
3.语法
1.给要获取的盒子添加ref属性
1 <div ref ="chartRef" > 我是渲染图表的容器</div >
2.获取时通过 $refs获取 this.$refs.chartRef 获取
1 2 3 mounted () { console.log(this.$refs.chartRef) }
4.注意
之前只用document.querySelect(‘.box’) 获取的是整个页面中的盒子
5.代码示例
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 <template> <div class="app"> <div class="base-chart-box"> 这是一个捣乱的盒子 </div> <BaseChart></BaseChart> </div> </template> <script> import BaseChart from './components/BaseChart.vue' export default { components:{ BaseChart } } </script> <style> .base-chart-box { width: 300px; height: 200px; } </style>
BaseChart.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 40 41 42 43 44 <template> <div class="base-chart-box" ref="baseChartBox">子组件</div> </template> <script> import * as echarts from 'echarts' export default { mounted() { // 基于准备好的dom,初始化echarts实例 // document.querySelector 会查找项目中所有的元素 // $refs只会在当前组件查找盒子 // var myChart = echarts.init(document.querySelector('.base-chart-box')) var myChart = echarts.init(this.$refs.baseChartBox) // 绘制图表 myChart.setOption({ title: { text: 'ECharts 入门示例', }, tooltip: {}, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'], }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20], }, ], }) }, } </script> <style scoped> .base-chart-box { width: 400px; height: 300px; border: 3px solid #000; border-radius: 6px; } </style>
二十一、异步更新 & $nextTick
1.需求
编辑标题, 编辑框自动聚焦
点击编辑,显示编辑框
让编辑框,立刻获取焦点
2.代码实现
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> <div class="app"> <div v-if="isShowEdit"> <input type="text" v-model="editValue" ref="inp" /> <button>确认</button> </div> <div v-else> <span>{{ title }}</span> <button @click="editFn">编辑</button> </div> </div> </template> <script> export default { data() { return { title: '大标题', isShowEdit: false, editValue: '', } }, methods: { editFn() { // 显示输入框 this.isShowEdit = true // 获取焦点 this.$refs.inp.focus() } }, } </script>
3.问题
“显示之后”,立刻获取焦点是不能成功的!
原因:Vue 是异步更新DOM (提升性能)
4.解决方案
$nextTick:等 DOM更新后 ,才会触发执行此方法里的函数体
语法: this.$nextTick(函数体)
1 2 3 this .$nextTick(() => { this .$refs .inp .focus () })
注意: $nextTick 内的函数体 一定是箭头函数 ,这样才能让函数内部的this指向Vue实例
5.深度解析
前言
最初更新是 vue核心 实现之一,在整体流程中预先着手观看者更新的调度者这一角色。大部分观察者更新都会通过它的处理,在适当时机让更新有序的执行。而nextTick 作为替代更新的核心,也是需要学习的重点。
本文你能学习到:
js 运行机制
在理解初步更新前,需要对js运行机制进行了解,如果你已经知道这些知识,可以选择跳过这部分内容。
js执行是单线程的,它是基于事件循环 的。事件循环大致分为以下几个步骤:
所有同步任务都在主线程上执行,形成一个执行栈(执行上下文堆栈)。
主线程之外,还存在一个“任务队列”(task queue)。只要初始化任务有了运行结果,就在“任务变量”之中放置一个事件。
一旦“执行栈”中的所有同步任务执行完毕,系统就会重新“任务类别”,看看里面有什么事件。那些对应的初始化任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
“任务类别”中的任务(任务)被分为两个类,分别是宏任务(宏任务)和微任务(micro task)
宏任务 :在一次新的事件循环的过程中,遇到宏任务时,宏任务将被加入任务类别,但需要等到下一次事件循环才会执行。常见的宏任务有setTimeout,setImmediate,requestAnimationFrame
微任务 :当前事件循环的任务队列为空时,微任务队列中的任务就会被依次执行在执行过程中,如果遇到微任务,微任务被加入到当前事件循环的微任务队列中。简单来说,只要有微任务就会继续执行,而不是放到下一个事件循环才执行。常见的微任务有MutationObserver,Promise.then
总的来说,在事件循环中,微任务会先于宏任务执行。而在微任务执行完后会进入浏览器更新渲染阶段,所以在更新渲染前使用微任务会比宏任务快一些。
为什么需要初步更新
既然异步更新是核心之一,首先要知道它的作用是什么,解决了什么问题。
先来看一个很常见的场景:
1 created ( ){ this .id = 10 this .list = [] this .info = {}}
总所周知,vue 基于数据驱动视图,数据更改会触发setter 函数,通知观察者进行更新。如果像上面的情况,是不是代表需要更新3次,而且在实际开发中的更新可不止那么少。
更新过程是需要经过繁杂的操作,例如模板编译,dom diff,不断进行更新的性能当然很差。
VUE 作为一个优秀的框架,当然不会那么“直男”,来多少就照单全收。VUE 内部实际是将观看者加入到一个队列数组中,最后再触发队列中所有观察家的运行方法来更新。
并且加入队列的过程中将会对watcher进行去重操作,因为在一个组件中数据内定义的数据都是存储同一个“渲染watcher”,所以以上场景中数据甚至更新了3次,最终也只会执行一次更新页面的逻辑。
为了达到这种效果,vue 使用异步更新,等待所有数据同步修改完成后,再去执行更新逻辑。
nextTick原理
异步更新内部是最重要的就是nextTick方法,它负责将异步任务加入队列和执行异步任务。VUE 也将它暴露出来提供给用户使用。在数据修改完成后,立即获取相关DOM还没那么快更新,使用nextTick便可以解决这一问题。
认识nextTick
官方文档对它的描述:
在下一DOM更新循环结束之后执行连续的替代。在修改数据之后立即使用此方法,获取更新后的DOM。
1 2 // 修改数据vm.msg = 'Hello'// DOM 还没有更新vue.nextTick(function () { // DOM 更新了}) // 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)vue.nextTick() .then(function () { // DOM 更新了 })
nextTick使用方法有一种和Promise两种,以上是通过构造函数调用的形式,更常见的是在实例调用this。$ nextTick。它们都是同一个方法。
内部实现
在 vue 源码2.5+后,nextTick的实现单独有一个js文件来维护它,它的内核并不复杂,代码实现不过100行,稍微花点时间可以啃下来。
比特位置在src / core / util /下一步 js,接下来我们来看一下它的实现,先从入口函数开始:
1 export function nextTick (cb?: Function , ctx?: Object ) catch (e) } else if (_resolve) })
cb即预期的最大值,它被push进一个回调回调,等待调用。
等待的作用就是一个锁,防止后续的nextTick重复执行timerFunc。timerFunc内部创建会一个微任务或宏任务,等待所有的nextTick同步执行完成后,再去执行回调内部的替代。
如果没有预先设定的,用户可能使用的是Promise形式,返回一个Promise,_resolve被调用时进入到。
继续往下走看看timerFunc的实现:
1 2 // Here we have async deferring wrappers using microtasks.// In 2.5 we used (macro) tasks (in combination with microtasks).// However, it has subtle problems when state is changed right before repaint// (e.g. #6813, out-in transitions).// Also, using (macro) tasks in event handler would cause some weird behaviors// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).// So we now use microtasks everywhere, again.// A major drawback of this tradeoff is that there are some scenarios// where microtasks have too high a priority and fire in between supposedly// sequential events (e.g. #4521, #6690, which have workarounds)// or even between bubbling of the same event (#6566).let timerFunc // The nextTick behavior leverages the microtask queue, which can be accessed// via either native Promise.then or MutationObserver.// MutationObserver has wider support, however it is seriously bugged in// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It// completely stops working after triggering a few times... so, if native// Promise is available, we will use it:/* istanbul ignore next, $flow-disable-line */if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop) } isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // Phantomjs and iOS 7.x MutationObserver.toString() === '[object MutationObserverconstructor]')) { // Use MutationObserver where native Promise is not available, // e.g. Phantomjs, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Technically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = () => { setImmediate(flushCallbacks) }} else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) }}
顶层的代码并不复杂,主要通过一些兼容的判断来创建合适的timerFunc,最优先肯定是微任务,其次再到宏任务。
优先级为promise.then> MutationObserver> setImmediate> setTimeout。也很重要,它们能帮助我们理解设计的意义)
我们会发现在某种情况下创建的timerFunc,最终都会执行一个flushCallbacks的函数。
1 2 const callbacks = []let pending = false function flushCallbacks () { pending = falseconst copies = callbacks.slice(0 ) callbacks.length = 0 for (let i = 0 ; i < copies.length; i++) { copies[i]() }}
flushCallbacks里做的事情是如此简单,它负责执行回调里的事情。
好了,nextTick的原始码那么那么多,现在已经知道它的实现,下面再结合转化更新流程,让我们对它更充分的理解吧。
初步更新流程
数据被改变时,触发watcher.update
1 // 源码位置:src/core/observer/watcher.jsupdate () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) // this 为当前的实例 watcher }}
调用queueWatcher,将watcher加入
1 2 // 源码位置:src/core/observer/scheduler.jsconst queue = []let has = {}let waiting = falselet flushing = falselet index = 0 export function queueWatcher (watcher: Watcher) {const id = watcher.id // 1 if (has[id] == null) { has[id] = true // 2 if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush // 3 if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } }}
每个监视者都有他们自己的id,当没有记录到对应的监视者,即第一次进入逻辑,否则是重复的监视者,则不会进入。这一步就是实现监视者去重的点。
将watcher加入到体重中,等待执行
等待的作用是防止nextTick重复执行
flushSchedulerQueue作为替代预期nextTick初始化执行。
1 2 3 4 5 6 function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, idresetSchedulerState()
flushSchedulerQueue内将刚刚加入队列的观察者逐个运行更新。resetSchedulerState重置状态,等待下一轮的异步更新。
1 function resetSchedulerState () { index = queue.length = activatedChildren.length = 0 has = {} if (process.env.NODE_ENV !== 'production' ) { circular = {} } waiting = flushing = false }
要注意此时flushSchedulerQueue仍未执行,它只是作为一个预期的插入而已。因为用户可能会调用nextTick方法。
这种情况下,回调里的内容为[“ flushSchedulerQueue”,“用户的nextTick选择”],当所有同步任务执行完成,才开始执行回调里面的一部分。
由此可见,最先执行的是页面更新的逻辑,其次再到用户的nextTick将会执行。这也是为什么我们能在nextTick中获取到更新后DOM的原因。
总结
初始更新机制使用微任务或宏任务,基于事件循环运行,在 vue 中对性能起着至关重要的作用,它对重复重复的watcher进行过滤。而nextTick根据不同的环境,使用优先级最高的初始任务。
此类的好处是等待所有的状态同步更新完成后,再一次性渲染页面。用户创建的nextTick运行页面更新之后,因此能够获取更新后的DOM。
day05
一、学习目标
1.自定义指令
基本语法(全局、局部注册)
指令的值
v-loading的指令封装
2.插槽
3.综合案例:商品列表
4.路由入门
二、自定义指令
1.指令介绍
2.自定义指令
概念:自己定义的指令,可以封装一些DOM操作 ,扩展额外的功能
3.自定义指令语法
原来的写法
1 2 3 4 5 操作dom:dom元素。focus() mounted ( this .$ref s.inp.focus()}麻烦
4.指令中的配置项介绍
inserted:被绑定元素插入父节点时调用的钩子函数
el:使用指令的那个DOM元素
5.代码示例
需求:当页面加载时,让元素获取焦点(autofocus在safari浏览器有兼容性 )
App.vue
1 2 3 4 <div> <h1>自定义指令</h1> <input v-focus ref="inp" type="text"> </div>
6.总结
1.自定义指令的作用是什么?
2.使用自定义指令的步骤是哪两步?
三、自定义指令-指令的值
1.需求
实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色
2.语法
1.在绑定指令时,可以通过“等号”的形式为指令 绑定 具体的参数值
1 <div v-color ="color" > 我是内容</div >
2.通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数
1 2 3 4 5 6 7 8 9 10 directives : { color : { inserted (el, binding) { el.style .color = binding.value }, update (el, binding) { el.style .color = binding.value } } }
3.代码示例
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 <template> <div> <!--显示红色--> <h2 v-color="color1">指令的值1测试</h2> <!--显示蓝色--> <h2 v-color="color2">指令的值2测试</h2> <button> 改变第一个h1的颜色 </button> </div> </template> <script> export default { data () { return { color1: 'red', color2: 'blue' } } directives: { color: { inserted (el, binding) { el.style.color = binding.value }, update (el, binding) { el.style.color = binding.value } } } } </script> <style> </style>
四、自定义指令-v-loading指令的封装
1.场景
实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好
2.需求
封装一个 v-loading 指令,实现加载中的效果
3.分析
1.本质 loading效果就是一个蒙层,盖在了盒子上
2.数据请求中,开启loading状态,添加蒙层
3.数据请求完毕,关闭loading状态,移除蒙层
4.实现
1.准备一个 loading类,通过伪元素定位,设置宽高,实现蒙层
2.开启关闭 loading状态(添加移除蒙层),本质只需要添加移除类即可
3.结合自定义指令的语法进行封装复用
1 2 3 4 5 6 7 8 9 .loading :before { content : "" ; position : absolute; left : 0 ; top : 0 ; width : 100% ; height : 100% ; background : #fff url ("./loading.gif" ) no-repeat center; }
5.准备代码
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 <template > <div class ="main" > <div class ="box" v-loading ="isLoading" > <ul > <li v-for ="item in list" :key ="item.id" class ="news" > <div class ="left" > <div class ="title" > {{ item.title }}</div > <div class ="info" > <span > {{ item.source }}</span > <span > {{ item.time }}</span > </div > </div > <div class ="right" > <img :src ="item.img" alt ="" > </div > </li > </ul > </div > <div class ="box2" v-loading ="isLoading2" > </div > </div > </template > <script > import axios from 'axios' export default { data () { return { list : [], isLoading : true , isLoading2 : true } }, async created () { const res = await axios.get ('http://hmajax.itheima.net/api/news' ) setTimeout (() => { this .list = res.data .data this .isLoading = false }, 2000 ) }, directives : { loading : { inserted (el, binding) { binding.value ? el.classList .add ('loading' ) : el.classList .remove ('loading' ) }, update (el, binding) { binding.value ? el.classList .add ('loading' ) : el.classList .remove ('loading' ) } } } } </script > <style > .loading :before { content : '' ; position : absolute; left : 0 ; top : 0 ; width : 100% ; height : 100% ; background : #fff url ('./loading.gif' ) no-repeat center; } .box2 { width : 400px ; height : 400px ; border : 2px solid #000 ; position : relative; } .box { width : 800px ; min-height : 500px ; border : 3px solid orange; border-radius : 5px ; position : relative; } .news { display : flex; height : 120px ; width : 600px ; margin : 0 auto; padding : 20px 0 ; cursor : pointer; } .news .left { flex : 1 ; display : flex; flex-direction : column; justify-content : space-between; padding-right : 10px ; } .news .left .title { font-size : 20px ; } .news .left .info { color : #999999 ; } .news .left .info span { margin-right : 20px ; } .news .right { width : 160px ; height : 120px ; } .news .right img { width : 100% ; height : 100% ; object-fit : cover; } </style >
五、插槽-默认插槽
1.作用
让组件内部的一些 结构 支持 自定义
2.需求
将需要多次显示的对话框,封装成一个组件
3.问题
组件的内容部分,不希望写死 ,希望能使用的时候自定义 。怎么办
4.插槽的基本语法
组件内需要定制的结构部分,改用** **占位
使用组件时, ** **标签内部, 传入结构替换slot
给插槽传入内容时,可以传入纯文本、html标签、组件
5.代码示例
MyDialog.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> <div class="dialog"> <div class="dialog-header"> <h3>友情提示</h3> <span class="close">✖️</span> </div> <div class="dialog-content"> <!-- 1. 在需要定制的位置,使用slot占位 --> <slot></slot> </div> <div class="dialog-footer"> <button>取消</button> <button>确认</button> </div> </div> </template> <script> export default { data () { return { } } } </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 30 31 32 <template> <div> <!-- 2. 在使用组件时,组件标签内填入内容 --> <MyDialog> <div>你确认要删除么</div> </MyDialog> <MyDialog> <p>你确认要退出么</p> </MyDialog> </div> </template> <script> import MyDialog from './components/MyDialog.vue' export default { data () { return { } }, components: { MyDialog } } </script> <style> body { background-color: #b3b3b3; } </style>
6.总结
场景:组件内某一部分结构不确定,想要自定义怎么办
使用:插槽的步骤分为哪几步?
六、插槽-后备内容(默认值)
1.问题
通过插槽完成了内容的定制,传什么显示什么, 但是如果不传,则是空白
能否给插槽设置 默认显示内容 呢?
2.插槽的后备内容
封装组件时,可以为预留的 <slot>
插槽提供后备内容(默认内容)。
3.语法
在 标签内,放置内容, 作为默认显示内容
4.效果
5.代码示例
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 <template> <div> <MyDialog></MyDialog> <MyDialog> 你确认要退出么 </MyDialog> </div> </template> <script> import MyDialog from './components/MyDialog.vue' export default { data () { return { } }, components: { MyDialog } } </script> <style> body { background-color: #b3b3b3; } </style>
七、插槽-具名插槽
1.需求
一个组件内有多处结构,需要外部传入标签,进行定制
上面的弹框中有三处不同 ,但是默认插槽 只能定制一个位置 ,这时候怎么办呢?
2.具名插槽语法
3.v-slot的简写
v-slot写起来太长,vue给我们提供一个简单写法 v-slot —> #
4.总结
组件内 有多处不确定的结构 怎么办?
具名插槽的语法是什么?
v-slot:插槽名可以简化成什么?
八、作用域插槽(获取值)
1.插槽分类
默认插槽
具名插槽
插槽只有两种,作用域插槽不属于插槽的一种分类
2.作用
定义slot 插槽的同时, 是可以传值 的。给 插槽 上可以 绑定数据 ,将来 使用组件时可以用
3.场景
封装表格组件
4.使用步骤
给 slot 标签, 以 添加属性的方式传值
1 <slot name="text" :id="item.id" msg="测试文本"></slot>
所有添加的属性, 都会被收集到一个对象中
在template中, 通过 #插槽名= "obj"
接收,默认插槽名为 default
1 2 3 4 5 <MyTable :list="list"> <template #name="obj"> <button @click="del(obj.id)">删除</button> </template> </MyTable>
5.代码示例
MyTable.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> <table class="my-table"> <thead> <tr> <th>序号</th> <th>姓名</th> <th>年纪</th> <th>操作</th> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td>{{ item.age }}</td> <td> <!-- 1. 给slot标签,添加属性的方式传值 --> <slot :row="item" msg="测试文本"></slot> <!-- 2. 将所有的属性,添加到一个对象中 --> <!-- { row: { id: 2, name: '孙大明', age: 19 }, msg: '测试文本' } --> </td> </tr> </tbody> </table> </template> <script> export default { props: { data: Array } } </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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <template> <div> <MyTable :data="list"> <!-- 3. 通过template #插槽名="变量名" 接收 --> <template #default="obj"> <button @click="del(obj.row.id)"> 删除 </button> </template> </MyTable> <MyTable :data="list2"> <template #default="{ row }"> <button @click="show(row)">查看</button> </template> </MyTable> </div> </template> <script> import MyTable from './components/MyTable.vue' export default { data () { return { list: [ { id: 1, name: '张小花', age: 18 }, { id: 2, name: '孙大明', age: 19 }, { id: 3, name: '刘德忠', age: 17 }, ], list2: [ { id: 1, name: '赵小云', age: 18 }, { id: 2, name: '刘蓓蓓', age: 19 }, { id: 3, name: '姜肖泰', age: 17 }, ] } }, methods: { del (id) { this.list = this.list.filter(item => item.id !== id) }, show (row) { // console.log(row); alert(`姓名:${row.name}; 年纪:${row.age}`) } }, components: { MyTable } } </script>
6.总结
1.作用域插槽的作用是什么?
2.作用域插槽的使用步骤是什么?
九、综合案例 - 商品列表-MyTag组件抽离
1.需求说明
my-tag 标签组件封装
(1) 双击显示输入框,输入框获取焦点
(2) 失去焦点,隐藏输入框
(3) 回显标签信息
(4) 内容修改,回车 → 修改标签信息
my-table 表格组件封装
(1) 动态传递表格数据渲染
(2) 表头支持用户自定义
(3) 主体支持用户自定义
2.代码准备
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 154 155 156 157 <template> <div class="table-case"> <table class="my-table"> <thead> <tr> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td> <td> <img src="https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg" /> </td> <td> <div class="my-tag"> <!-- <input class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 茶具 </div> </div> </td> </tr> <tr> <td>1</td> <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td> <td> <img src="https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg" /> </td> <td> <div class="my-tag"> <!-- <input ref="inp" class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 男靴 </div> </div> </td> </tr> </tbody> </table> </div> </template> <script> export default { name: 'TableCase', components: {}, data() { return { goods: [ { id: 101, picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg', name: '梨皮朱泥三绝清代小品壶经典款紫砂壶', tag: '茶具', }, { id: 102, picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg', name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌', tag: '男鞋', }, { id: 103, picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png', name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm', tag: '儿童服饰', }, { id: 104, picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg', name: '基础百搭,儿童套头针织毛衣1-9岁', tag: '儿童服饰', }, ], } }, } </script> <style lang="less" scoped> .table-case { width: 1000px; margin: 50px auto; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } .my-table { width: 100%; border-spacing: 0; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } th { background: #f5f5f5; border-bottom: 2px solid #069; } td { border-bottom: 1px dashed #ccc; } td, th { text-align: center; padding: 10px; transition: all 0.5s; &.red { color: red; } } .none { height: 100px; line-height: 100px; color: #999; } } .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } } </style>
3.my-tag组件封装-创建组件
MyTag.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 <template> <div class="my-tag"> <!-- <input class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 茶具 </div> </div> </template> <script> export default { } </script> <style lang="less" scoped> .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } </style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> ... <tbody> <tr> .... <td> <MyTag></MyTag> </td> </tr> </tbody> ... </template> <script> import MyTag from './components/MyTag.vue' export default { name: 'TableCase', components: { MyTag, }, .... </script>
十、综合案例-MyTag组件控制显示隐藏
MyTag.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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <template> <div class="my-tag"> <input v-if="isEdit" v-focus ref="inp" class="input" type="text" placeholder="输入标签" :value="value" @blur="isEdit = false" @keyup.enter="handleEnter" /> <div v-else @dblclick="handleClick" class="text"> {{ value }} </div> </div> </template> <script> export default { props: { value: String }, data () { return { isEdit: false } }, methods: { handleClick () { // 双击后,切换到显示状态 (Vue是异步dom更新) this.isEdit = true // // 等dom更新完了,再获取焦点 // this.$nextTick(() => { // // 立刻获取焦点 // this.$refs.inp.focus() // }) }, handleEnter (e) { // 非空处理 if (e.target.value.trim() === '') return alert('标签内容不能为空') // 子传父,将回车时,[输入框的内容] 提交给父组件更新 // 由于父组件是v-model,触发事件,需要触发 input 事件 this.$emit('input', e.target.value) // 提交完成,关闭输入状态 this.isEdit = false } } } </script>
main.js
1 2 3 4 5 6 7 Vue .directive ('focus' , { inserted (el) { el.focus () } })
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 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 <template > <div class ="table-case" > <MyTable :data ="goods" > <template #head > <th > 编号</th > <th > 名称</th > <th > 图片</th > <th width ="100px" > 标签</th > </template > <template #body ="{ item, index }" > <td > {{ index + 1 }} </td > <td > {{ item.name }} </td > <td > <img :src ="item.picture" /> </td > <td > <MyTag v-model ="item.tag" > </MyTag > </td > </template > </MyTable > </div > </template > <script > import MyTag from './components/MyTag.vue' import MyTable from './components/MyTable.vue' export default { name : 'TableCase' , components : { MyTag , MyTable }, data () { return { tempText : '水杯' , tempText2 : '钢笔' , goods : [ { id : 101 , picture : 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg' , name : '梨皮朱泥三绝清代小品壶经典款紫砂壶' , tag : '茶具' }, { id : 102 , picture : 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg' , name : '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌' , tag : '男鞋' }, { id : 103 , picture : 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png' , name : '毛茸茸小熊出没,儿童羊羔绒背心73-90cm' , tag : '儿童服饰' }, { id : 104 , picture : 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg' , name : '基础百搭,儿童套头针织毛衣1-9岁' , tag : '儿童服饰' }, ] } } } </script > <style lang ="less" scoped > .table-case { width : 1000px ; margin : 50px auto; img { width : 100px ; height : 100px ; object-fit : contain; vertical-align : middle; } } </style >
十一、综合案例-MyTag组件进行v-model绑定
App.vue
1 2 3 4 5 6 7 8 <MyTag v-model="tempText"></MyTag> <script> export default { data(){ tempText:'水杯' } } </script>
MyTag.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 40 41 42 43 44 45 46 <template > <div class ="my-tag" > <input v-if ="isEdit" v-focus ref ="inp" class ="input" type ="text" placeholder ="输入标签" :value ="value" @blur ="isEdit = false" @keyup.enter ="handleEnter" /> <div v-else @dblclick ="handleClick" class ="text" > {{ value }} </div > </div > </template > <script > export default { props : { value : String }, data () { return { isEdit : false } }, methods : { handleClick () { this .isEdit = true }, handleEnter (e) { if (e.target .value .trim () === '' ) return alert ('标签内容不能为空' ) this .$emit('input' , e.target .value ) this .isEdit = false } } } </script >
十二、综合案例-封装MyTable组件-动态渲染数据
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div class="table-case"> <MyTable :data="goods"></MyTable> </div> </template> <script> import MyTable from './components/MyTable.vue' export default { name: 'TableCase', components: { MyTable }, data(){ return { .... } }, } </script>
MyTable.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 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 <template> <table class="my-table"> <thead> <tr> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td> <img :src="item.picture" /> </td> <td> 标签内容 <!-- <MyTag v-model="item.tag"></MyTag> --> </td> </tr> </tbody> </table> </template> <script> export default { props: { data: { type: Array, required: true } } }; </script> <style lang="less" scoped> .my-table { width: 100%; border-spacing: 0; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } th { background: #f5f5f5; border-bottom: 2px solid #069; } td { border-bottom: 1px dashed #ccc; } td, th { text-align: center; padding: 10px; transition: all .5s; &.red { color: red; } } .none { height: 100px; line-height: 100px; color: #999; } } </style>
十三、综合案例-封装MyTable组件-自定义结构
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 38 39 40 41 42 <template> <div class="table-case"> <MyTable :data="goods"> <template #head> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </template> <template #body="{ item, index }"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td> <img :src="item.picture" /> </td> <td> <MyTag v-model="item.tag"></MyTag> </td> </template> </MyTable> </div> </template> <script> import MyTag from './components/MyTag.vue' import MyTable from './components/MyTable.vue' export default { name: 'TableCase', components: { MyTag, MyTable }, data () { return { .... } } </script>
MyTable.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> <table class="my-table"> <thead> <tr> <slot name="head"></slot> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id"> <slot name="body" :item="item" :index="index" ></slot> </tr> </tbody> </table> </template> <script> export default { props: { data: { type: Array, required: true } } }; </script>
十四、单页应用程序介绍
1.概念
单页应用程序:SPA【Single Page Application】是指所有的功能都在一个html页面 上实现
2.具体示例
单页应用网站: 网易云音乐 https://music.163.com/
多页应用网站:京东 https://jd.com/
3.单页应用 VS 多页面应用
单页应用类网站:系统类网站 / 内部网站 / 文档类网站 / 移动端站点
多页应用类网站:公司官网 / 电商类网站
4.总结
1.什么是单页面应用程序?
2.单页面应用优缺点?
3.单页应用场景?
十五、路由介绍
1.思考
单页面应用程序,之所以开发效率高,性能好,用户体验好
最大的原因就是:页面按需更新
比如当点击【发现音乐】和【关注】时,只是更新下面部分内容 ,对于头部是不更新的
要按需更新,首先就需要明确:访问路径 和 组件 的对应关系!
访问路径 和 组件的对应关系如何确定呢? 路由
2.路由的介绍
生活中的路由:设备和ip的映射关系
Vue中的路由:路径和组件 的映射 关系
3.总结
十六、路由的基本使用
1.目标
认识插件 VueRouter,掌握 VueRouter 的基本使用步骤
2.作用
修改 地址栏路径时,切换显示 匹配的组件
3.说明
Vue 官方的一个路由插件,是一个第三方包
4.官网
https://v3.router.vuejs.org/zh/
5.VueRouter的使用(5+2)
固定5个固定的步骤(不用死背,熟能生巧)
下载 VueRouter 模块到当前工程,版本3.6.5
1 yarn add vue-router@3.6.5
main.js中引入VueRouter
1 import VueRouter from 'vue-router'
安装注册
创建路由对象
1 2 const router = new VueRouter() //报错的话就是因为粘贴后有空格,删了自己敲就好
注入,将路由对象注入到new Vue实例中,建立关联
1 2 3 4 5 new Vue({ render: h => h(App), router:router }).$mount('#app')
当我们配置完以上5步之后 就可以看到浏览器地址栏中的路由 变成了 /#/的形式。表示项目的路由已经被Vue-Router管理了
6.代码示例
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 路由的使用步骤 5 + 2 // 5个基础步骤 // 1. 下载 v3.6.5 // yarn add vue-router@3.6.5 // 2. 引入 // 3. 安装注册 Vue.use(Vue插件) // 4. 创建路由对象 // 5. 注入到new Vue中,建立关联 import VueRouter from 'vue-router' Vue.use(VueRouter) // VueRouter插件初始化 const router = new VueRouter() new Vue({ render: h => h(App), router }).$mount('#app')
7.两个核心步骤
创建需要的组件 (views目录),在app.vue配置路由规则

配置导航,配置路由出口(路径匹配的组件显示的位置)
App.vue
1 2 3 4 5 6 7 8 <div class="footer_wrap"> <a href="#/find">发现音乐</a> <a href="#/my">我的音乐</a> <a href="#/friend">朋友</a> </div> <div class="top"> <router-view></router-view> </div>
8.总结
如何实现 路径改变,对应组件 切换,应该使用哪个插件?
Vue-Router的使用步骤是什么(5+2)?
十七、组件的存放目录问题
注意: .vue文件 本质无区别
1.组件分类
.vue文件分为2类,都是 .vue文件(本质无区别)
页面组件 (配置路由规则时使用的组件)
复用组件(多个组件中都使用到的组件)
2.存放目录
分类开来的目的就是为了 更易维护
src/views文件夹
页面组件 - 页面展示 - 配合路由用
src/components文件夹
复用组件 - 展示数据 - 常用于复用
3.总结
组件分类有哪两类?分类的目的?
不同分类的组件应该放在什么文件夹?作用分别是什么?
十八、路由的封装抽离
问题:所有的路由配置都在main.js中合适吗?
目标:将路由模块抽离出来。 好处:拆分模块,利于维护
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 import Find from '@/views/Find' import My from '@/views/My' import Friend from '@/views/Friend' import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const router = new VueRouter({ routes: [ { path: '/find' , component: Find }, { path: '/my' , component: My }, { path: '/friend' , component: Friend }, ] }) export default router
路径简写:
脚手架环境下 @指代src目录,可以用于快速引入组件
总结:
路由模块的封装抽离的好处是什么?
以后如何快速引入组件?
day06
一、声明式导航-导航链接
1.需求
实现导航高亮效果
如果使用a标签进行跳转的话,需要给当前跳转的导航加样式,同时要移除上一个a标签的样式,太麻烦!!!
2.解决方案
vue-router 提供了一个全局组件 router-link (取代 a 标签)
能跳转 ,配置 to 属性指定路径(必须 ) 。本质还是 a 标签 ,to 无需 #
能高亮 ,默认就会提供高亮类名 ,可以直接设置高亮样式
语法: 发现音乐
1 2 3 4 5 6 7 8 9 10 11 <div> <div class="footer_wrap"> <router-link to="/find">发现音乐</router-link> <router-link to="/my">我的音乐</router-link> <router-link to="/friend">朋友</router-link> </div> <div class="top"> <!-- 路由出口 → 匹配的组件所展示的位置 --> <router-view></router-view> </div> </div>
3.通过router-link自带的两个样式进行高亮
使用router-link跳转后,我们发现。当前点击的链接默认加了两个class的值 router-link-exact-active
和router-link-active
我们可以给任意一个class属性添加高亮样式即可实现功能
4.总结
router-link是什么?
router-link怎么用?
router-link的好处是什么?
二、声明式导航-两个类名
当我们使用 跳转时,自动给当前导航加了两个类名
1.router-link-active
模糊匹配(用的多)
to=“/my” 可以匹配 /my /my/a /my/b …
只要是以/my开头的路径 都可以和 to="/my"匹配到
2.router-link-exact-active
精确匹配
to=“/my” 仅可以匹配 /my
3.在地址栏中输入二级路由查看类名的添加
4.总结
router-link 会自动给当前导航添加两个类名,有什么区别呢?
router-link-active模糊匹配(用的多)
router-link-exact-active精确匹配
三、声明式导航-自定义类名(了解)
1.问题
router-link的两个高亮类名 太长了 ,我们希望能定制怎么办
2.解决方案
我们可以在创建路由对象时,额外配置两个配置项即可。 linkActiveClass
和linkExactActiveClass
1 2 3 4 5 const router = new VueRouter ({ routes : [...], linkActiveClass : "类名1" , linkExactActiveClass : "类名2" })
3.代码演示
1 2 3 4 5 6 7 8 const router = new VueRouter ({ routes : [ ... ], linkActiveClass : 'active' , linkExactActiveClass : 'exact-active' })
4.总结
如何自定义router-link的两个高亮类名
四、声明式导航-查询参数传参
1.目标
在跳转路由时,进行传参
比如:现在我们在搜索页点击了热门搜索链接,跳转到详情页,需要把点击的内容带到详情页 ,改怎么办呢?
2.跳转传参
我们可以通过两种方式,在跳转的时候把所需要的参数传到其他页面中
3.查询参数传参
如何传参?
如何接受参数
固定用法:$router.query.参数名
4.代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div id="app"> <div class="link"> <router-link to="/home">首页</router-link> <router-link to="/search">搜索页</router-link> </div> <router-view></router-view> </div> </template> <script> export default {}; </script>
Home.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div class="home"> <div class="logo-box"></div> <div class="search-box"> <input type="text"> <button>搜索一下</button> </div> <div class="hot-link"> 热门搜索: <router-link to="/search?key=黑马程序员">黑马程序员</router-link> <router-link to="/search?key=前端培训">前端培训</router-link> <router-link to="/search?key=如何成为前端大牛">如何成为前端大牛</router-link> </div> </div> </template> <script> export default { name: 'FindMusic' } </script>
Search.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 <template> <div class="search"> <p>搜索关键字: {{ $route.query.key }} </p> <p>搜索结果: </p> <ul> <li>.............</li> <li>.............</li> <li>.............</li> <li>.............</li> </ul> </div> </template> <script> export default { name: 'MyFriend', created () { // 在created中,获取路由参数 // this.$route.query.参数名 获取 console.log(this.$route.query.key); } } </script>
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import Home from '@/views/Home' import Search from '@/views/Search' import Vue from 'vue' import VueRouter from 'vue-router' Vue .use (VueRouter ) const router = new VueRouter ({ routes : [ { path : '/home' , component : Home }, { path : '/search' , component : Search } ] }) export default router
main.js
1 2 3 4 5 6 7 ... import router from './router/index' ... new Vue({ render: h => h(App), router }).$mount('#app' )
五、声明式导航-动态路由传参
1.动态路由传参方式
配置动态路由
动态路由后面的参数可以随便起名,但要有语义
1 2 3 4 5 6 7 8 9 const router = new VueRouter ({ routes : [ ..., { path : '/search/:words' , component : Search } ] })
配置导航链接
to=“/path/参数值”
对应页面组件接受参数
$route.params .参数名
params后面的参数名要和动态路由配置的参数保持一致
2.查询参数传参 VS 动态路由传参
查询参数传参 (比较适合传多个参数 )
跳转:to=“/path?参数名=值&参数名2=值”
获取:$route.query.参数名
动态路由传参 (优雅简洁 ,传单个参数比较方便)
配置动态路由:path: “/path/:参数名”
跳转:to=“/path/参数值”
获取:$route.params.参数名
注意:动态路由也可以传多个参数,但一般只传一个
3.总结
声明式导航跳转时, 有几种方式传值给路由页面?
查询参数传参(多个参数)
动态路由传参(一个参数,优雅简洁)
六、动态路由参数的可选符(了解)
1.问题
配了路由 path:“/search/:words” 为什么按下面步骤操作,会未匹配到组件,显示空白?
2.原因
/search/:words 表示,必须要传参数 。如果不传参数,也希望匹配,可以加个可选符"?"
1 2 3 4 5 6 const router = new VueRouter ({ routes : [ ... { path : '/search/:words?' , component : Search } ] })
七、Vue路由-重定向
1.问题
网页打开时, url 默认是 / 路径,未匹配到组件时,会出现空白
2.解决方案
重定向 → 匹配 / 后, 强制跳转 /home 路径
3.语法
1 2 3 { path : 匹配路径, redirect : 重定向到的路径 }, 比如: { path :'/' ,redirect :'/home' }
4.代码演示
1 2 3 4 5 6 7 const router = new VueRouter({ routes: [ { path: '/' , redirect: '/home' }, { path: '/home' , component: Home } ... ] })
八、Vue路由-404
1.作用
当路径找不到匹配时,给个提示页面
2.位置
404的路由,虽然配置在任何一个位置都可以,但一般都配置在其他路由规则的最后面
3.语法
path: “*” (任意路径) – 前面不匹配就命中最后这个
1 2 3 4 5 6 7 8 import NotFind from '@/views/NotFind' const router = new VueRouter ({ routes : [ ... { path : '*' , component : NotFind } ] })
4.代码示例
NotFound.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div> <h1>404 Not Found</h1> </div> </template> <script> export default { } </script> <style> </style>
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 ... import NotFound from '@/views/NotFound' ... const router = new VueRouter ({ routes : [ ... { path : '*' , component : NotFound } ] }) export default router
九、Vue路由-模式设置
1.问题
路由的路径看起来不自然, 有#,能否切成真正路径形式?
2.语法
1 2 3 4 const router = new VueRouter ({ mode :'histroy' , routes :[] })
十、编程式导航-两种路由跳转方式
1.问题
点击按钮跳转如何实现?
2.方案
编程式导航:用JS代码来进行跳转
3.语法
两种语法:
path 路径跳转 (简易方便)
name 命名路由跳转 (适合 path 路径长的场景)
4.path路径跳转语法
特点:简易方便
1 2 3 4 5 6 7 this .$router .push ('路由路径' )this .$router .push ({ path : '路由路径' })
5.代码演示 path跳转方式
6.name命名路由跳转
特点:适合 path 路径长的场景
语法:
路由规则,必须配置name配置项
1 { name : '路由名' , path : '/path/xxx' , component : XXX },
通过name来进行跳转
1 2 3 this .$router .push ({ name : '路由名' })
7.代码演示通过name命名路由跳转
8.总结
编程式导航有几种跳转方式?
十一、编程式导航-path路径跳转传参
1.问题
点击搜索按钮,跳转需要把文本框中输入的内容传到下一个页面如何实现?

2.两种传参方式
1.查询参数
2.动态路由传参
3.传参
两种跳转方式,对于两种传参方式都支持:
① path 路径跳转传参
② name 命名路由跳转传参
4.path路径跳转传参(query传参)
1 2 3 4 5 6 7 8 9 10 this .$router .push ('/路径?参数名1=参数值1&参数2=参数值2' )this .$router .push ({ path : '/路径' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
接受参数的方式依然是:$route.query.参数名
5.path路径跳转传参(动态路由传参)
1 2 3 4 5 6 this.$router .push ('/路径/参数值' ) this.$router .push ({ path : '/路径/参数值' })
接受参数的方式依然是:$route.params.参数值
**注意:**path不能配合params使用
十二、编程式导航-name命名路由传参
1.name 命名路由跳转传参 (query传参)
1 2 3 4 5 6 7 this .$router .push ({ name : '路由名字' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
2.name 命名路由跳转传参 (动态路由传参)
1 2 3 4 5 6 this .$router .push ({ name : '路由名字' , params : { 参数名: '参数值' , } })
3.总结
编程式导航,如何跳转传参?
1.path路径跳转
query传参
1 2 3 4 5 6 7 8 this .$router .push ('/路径?参数名1=参数值1&参数2=参数值2' )this .$router .push ({ path : '/路径' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
动态路由传参
1 2 3 4 this .$router .push ('/路径/参数值' )this .$router .push ({ path : '/路径/参数值' })
2.name命名路由跳转
query传参
1 2 3 4 5 6 7 this .$router .push ({ name : '路由名字' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
动态路由传参 (需要配动态路由)
1 2 3 4 5 6 this .$router .push ({ name : '路由名字' , params : { 参数名: '参数值' , } })
十三、面经基础版-案例效果分析
1.面经效果演示
2.功能分析
通过演示效果发现,主要的功能页面有两个,一个是列表页 ,一个是详情页 ,并且在列表页点击时可以跳转到详情页
底部导航可以来回切换,并且切换时,只有上面的主题内容在动态渲染
3.实现思路分析:配置路由+功能实现
1.配置路由
首页和面经详情页,两个一级路由
首页内嵌套4个可切换的页面(嵌套二级路由)
2.实现功能
首页请求渲染
跳转传参 到 详情页,详情页动态渲染
组件缓存,性能优化
十四、面经基础版-一级路由配置
1.把文档中准备的素材拷贝到项目中
2.针对router/index.js文件 进行一级路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ... import Layout from '@/views/Layout.vue' import ArticleDetail from '@/views/ArticleDetail.vue' ... const router = new VueRouter ({ routes : [ { path : '/' , component : Layout }, { path : '/detail' , component : ArticleDetail } ] })
十五、面经基础版-二级路由配置
二级路由也叫嵌套路由,当然也可以嵌套三级、四级…
1.使用场景
当在页面中点击链接跳转,只是部分内容切换时,我们可以使用嵌套路由
2.语法
在一级路由下,配置children属性即可
配置二级路由的出口
1.在一级路由下,配置children属性
注意 :一级的路由path 需要加 /
二级路由的path不需要加 /
1 2 3 4 5 6 7 8 9 10 11 12 13 const router = new VueRouter ({ routes : [ { path : '/' , component : Layout , children :[ {path :'xxxx' ,component :xxxx.vue }, {path :'xxxx' ,component :xxxx.vue }, ] } ] })
技巧:二级路由应该配置到哪个一级路由下呢?
这些二级路由对应的组件渲染到哪个一级路由下,children就配置到哪个路由下边
2.配置二级路由的出口
注意: 配置了嵌套路由,一定配置对应的路由出口,否则不会渲染出对应的组件
Layout.vue
1 2 3 4 5 6 7 8 <template> <div class="h5-wrapper"> <div class="content"> <router-view></router-view> </div> .... </div> </template>
3.代码实现
router/index.js
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 ... import Article from '@/views/Article.vue' import Collect from '@/views/Collect.vue' import Like from '@/views/Like.vue' import User from '@/views/User.vue' ... const router = new VueRouter ({ routes : [ { path : '/' , component : Layout , redirect : '/article' , children :[ { path :'/article' , component :Article }, { path :'/collect' , component :Collect }, { path :'/like' , component :Like }, { path :'/user' , component :User } ] }, .... ] })
Layout.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <div class="h5-wrapper"> <div class="content"> <!-- 内容部分 --> <router-view></router-view> </div> <nav class="tabbar"> <a href="#/article">面经</a> <a href="#/collect">收藏</a> <a href="#/like">喜欢</a> <a href="#/user">我的</a> </nav> </div> </template>
十六、面经基础版-二级导航高亮
1.实现思路
将a标签替换成 组件,配置to属性,不用加 #
结合高亮类名实现高亮效果 (推荐模糊匹配:router-link-active)
2.代码实现
Layout.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 .... <nav class="tabbar"> <router-link to="/article">面经</router-link> <router-link to="/collect">收藏</router-link> <router-link to="/like">喜欢</router-link> <router-link to="/user">我的</router-link> </nav> <style> a.router-link-active { color: orange; } </style>
十七、面经基础版-首页请求渲染
1.步骤分析
1.安装axios
2.看接口文档,确认请求方式,请求地址,请求参数
3.created中发送请求,获取数据,存储到data中
4.页面动态渲染
2.代码实现
1.安装axios
yarn add axios
npm i axios
2.接口文档
1 2 请求地址: https://mock.boxuegu.com/mock/3083/articles 请求方式: get
3.created中发送请求,获取数据,存储到data中
1 2 3 4 5 6 7 8 9 data() { return { articelList: [], } }, async created() { const { data: { result: { rows } }} = await axios.get('https://mock.boxuegu.com/mock/3083/articles') this.articelList = rows },
4.页面动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="article-page"> <div class="article-item" v-for="item in articelList" :key="item.id"> <div class="head"> <img :src="item.creatorAvatar" alt="" /> <div class="con"> <p class="title">{{ item.stem }}</p> <p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p> </div> </div> <div class="body"> {{item.content}} </div> <div class="foot">点赞 {{item.likeCount}} | 浏览 {{item.views}}</div> </div> </div> </template>
十八、面经基础版-查询参数传参
1.说明
跳转详情页需要把当前点击的文章id传给详情页,获取数据
查询参数传参 this.$router.push(‘/detail?参数1=参数值&参数2=参数值’)
动态路由传参 先改造路由 在传参 this.$router.push(‘/detail/参数值’)
2.查询参数传参实现
Article.vue
1 2 3 4 5 6 7 8 9 <template> <div class="article-page"> <div class="article-item" v-for="item in articelList" :key="item.id" @click="$router.push(`/detail?id=${item.id}`)"> ... </div> </div> </template>
ArticleDetail.vue
1 2 3 created(){ console.log(this.$route.query.id) }
十九、面经基础版-动态路由传参
1.实现步骤
2.代码实现
改造路由
router/index.js
1 2 3 4 5 ... { path : '/detail/:id' , component : ArticleDetail }
Article.vue
1 2 3 4 5 <div class="article-item" v-for="item in articelList" :key="item.id" @click="$router.push(`/detail/${item.id}`)"> .... </div>
ArticleDetail.vue
1 2 3 created(){ console.log(this.$route.params.id) }
3.额外优化功能点-点击回退跳转到上一页
ArticleDetail.vue
1 2 3 4 5 6 <template> <div class="article-detail-page"> <nav class="nav"><span class="back" @click="$router.back()"><</span> 面经详情</nav> .... </div> </template>
二十、面经基础版-详情页渲染
1.实现步骤分析
导入axios
查看接口文档
在created中发送请求
页面动态渲染
2.代码实现
接口文档
1 2 请求地址: https://mock.boxuegu.com/mock/3083/articles/:id 请求方式: get
在created中发送请求
1 2 3 4 5 6 7 8 9 10 11 12 data() { return { articleDetail:{} } }, async created() { const id = this.$route.params.id const {data:{result}} = await axios.get( `https://mock.boxuegu.com/mock/3083/articles/${id}` ) this.articleDetail = result },
页面动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <div class="article-detail-page"> <nav class="nav"> <span class="back" @click="$router.back()"><</span> 面经详情 </nav> <header class="header"> <h1>{{articleDetail.stem}}</h1> <p>{{articleDetail.createAt}} | {{articleDetail.views}} 浏览量 | {{articleDetail.likeCount}} 点赞数</p> <p> <img :src="articleDetail.creatorAvatar" alt="" /> <span>{{articleDetail.creatorName}}</span> </p> </header> <main class="body"> {{articleDetail.content}} </main> </div> </template>
二十一、面经基础版-缓存组件
1.问题
从面经列表 点到 详情页,又点返回,数据重新加载了 → 希望回到原来的位置
2.原因
当路由被跳转 后,原来所看到的组件就被销毁 了(会执行组件内的beforeDestroy和destroyed生命周期钩子),重新返回 后组件又被重新创建 了(会执行组件内的beforeCreate,created,beforeMount,Mounted生命周期钩子),所以数据被加载了
3.解决方案
利用keep-alive把原来的组件给缓存下来
4.什么是keep-alive
keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁 它们。
keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件中。
优点:
在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染DOM,
减少加载时间及性能消耗,提高用户体验性。
App.vue
1 2 3 4 5 6 7 <template> <div class="h5-wrapper"> <keep-alive> <router-view></router-view> </keep-alive> </div> </template>
问题:
缓存了所有被切换的组件
5.keep-alive的三个属性
① include : 组件名数组,只有匹配的组件会被缓存
② exclude : 组件名数组,任何匹配的组件都不会被缓存
③ max : 最多可以缓存多少 组件实例
App.vue
1 2 3 4 5 6 7 <template> <div class="h5-wrapper"> <keep-alive :include="['LayoutPage']"> <router-view></router-view> </keep-alive> </div> </template>
6.额外的两个生命周期钩子
keep-alive的使用会触发两个生命周期函数
activated 当组件被激活(使用)的时候触发 → 进入这个页面的时候触发
deactivated 当组件不被使用的时候触发 → 离开这个页面的时候触发
组件缓存后 就不会执行 组件的created, mounted, destroyed 等钩子了
所以其提供了actived 和deactived 钩子,帮我们实现业务需求。
7.总结
1.keep-alive是什么
2.keep-alive的优点
3.keep-alive的三个属性 (了解)
4.keep-alive的使用会触发两个生命周期函数(了解)
二十二、VueCli 自定义创建项目
1.安装脚手架 (已安装)
2.创建项目
1 vue create hm-exp -mobile
1 2 3 4 5 Vue CLI v5.0 .8 ? Please pick a preset : Default ([Vue 3 ] babel, eslint) Default ([Vue 2 ] babel, eslint) > Manually select features 选自定义
选择eslint的风格 (eslint 代码规范的检验工具,检验代码是否符合规范)
比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子
是否保存预设,下次直接使用? => 不保存,输入 N
二十三、ESlint代码规范及手动修复
代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加;?…
没有规矩不成方圆
ESLint:是一个代码检查工具,用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。在创建项目时,我们使用的是 JavaScript Standard Style 代码风格的规则。
1.JavaScript Standard Style 规范说明
建议把:https://standardjs.com/rules-zhcn.html 看一遍,然后在写的时候, 遇到错误就查询解决。
下面是这份规则中的一小部分:
字符串使用单引号 – 需要转义的地方除外
无分号 – 这 没什么不好。 不骗你!
关键字后加空格 if (condition) { ... }
函数名后加空格 function name (arg) { ... }
坚持使用全等 ===
摒弃 ==
一但在需要检查 null || undefined
时可以使用 obj == null
…
2.代码规范错误
如果你的代码不符合standard的要求,eslint会跳出来刀子嘴,豆腐心地提示你。
下面我们在main.js中随意做一些改动:添加一些空行,空格。
1 2 3 4 5 6 7 8 9 10 11 12 13 import Vue from 'vue' import App from './App.vue' import './styles/index.less' import router from './router' Vue .config .productionTip = false new Vue ( { render : h => h (App ), router }).$mount('#app' )
按下保存代码之后:
你将会看在控制台中输出如下错误:
eslint 是来帮助你的。心态要好,有错,就改。
3.手动修正
根据错误提示来一项一项手动修正。
如果你不认识命令行中的语法报错是什么意思,你可以根据错误代码(func-call-spacing, space-in-parens,…)去 ESLint 规则列表中查找其具体含义。
打开 ESLint 规则表 ,使用页面搜索(Ctrl + F)这个代码,查找对该规则的一个释义。
二十四、通过eslint插件来实现自动修正
eslint会自动高亮错误显示
通过配置,eslint会自动帮助我们修复错误
1 2 3 4 5 6 "editor.codeActionsOnSave" : { "source.fixAll" : true }, "editor.formatOnSave" : false
注意:eslint的配置文件必须在根目录下,这个插件才能才能生效。打开项目必须以根目录打开,一次打开一个项目
注意:使用了eslint校验之后,把vscode带的那些格式化工具全禁用了 Beatify
settings.json 参考
1 2 3 4 5 6 7 8 9 10 11 12 { "window.zoomLevel" : 2 , "workbench.iconTheme" : "vscode-icons" , "editor.tabSize" : 2 , "emmet.triggerExpansionOnTab" : true , "editor.codeActionsOnSave" : { "source.fixAll" : true }, "editor.formatOnSave" : false }
day07
目标:明确Vuex 是什么,应用场景以及优势
1.是什么
Vuex 是一个 Vue 的 状态管理工具,状态就是数据。
大白话:Vuex 是一个插件,可以帮我们管理 Vue 通用的数据 (多组件共享的数据)。例如:购物车数据 个人信息数
2.使用场景
某个状态 在 很多个组件 来使用 (个人信息)
多个组件 共同维护 一份数据 (购物车)
3.优势
共同维护一份数据,数据集中化管理
响应式变化
操作简洁 (vuex提供了一些辅助函数)
4.注意:
官方原文:
不是所有的场景都适用于vuex,只有在必要的时候才使用vuex
使用了vuex之后,会附加更多的框架中的概念进来,增加了项目的复杂度 (数据的操作更便捷,数据的流动更清晰)
Vuex就像《近视眼镜》, 你自然会知道什么时候需要用它~
二、需求: 多组件共享数据
目标:基于脚手架创建项目,构建 vuex 多组件数据共享环境
效果是三个组件共享一份数据:
任意一个组件都可以修改数据
三个组件的数据是同步的
1.创建项目
2.创建三个组件, 目录如下
1 2 3 4 |-components |--Son1.vue |--Son2.vue |-App.vue
3.源代码如下
App.vue
在入口组件中引入 Son1 和 Son2 这两个子组件
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 <template > <div id ="app" > <h1 > 根组件</h1 > <input type ="text" > <Son1 > </Son1 > <hr > <Son2 > </Son2 > </div > </template > <script > import Son1 from './components/Son1.vue' import Son2 from './components/Son2.vue' export default { name : 'app' , data : function ( ) { return { } }, components : { Son1 , Son2 } } </script > <style > #app { width : 600px ; margin : 20px auto; border : 3px solid #ccc ; border-radius : 3px ; padding : 10px ; } </style >
main.js
1 2 3 4 5 6 7 8 import Vue from 'vue' import App from './App.vue' Vue .config .productionTip = false new Vue ({ render : h => h (App ) }).$mount('#app' )
Son1.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 > <div class ="box" > <h2 > Son1 子组件</h2 > 从vuex中获取的值: <label > </label > <br > <button > 值 + 1</button > </div > </template > <script > export default { name : 'Son1Com' } </script > <style lang ="css" scoped > .box { border : 3px solid #ccc ; width : 400px ; padding : 10px ; margin : 20px ; } h2 { margin-top : 10px ; } </style >
Son2.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 <template > <div class ="box" > <h2 > Son2 子组件</h2 > 从vuex中获取的值:<label > </label > <br /> <button > 值 - 1</button > </div > </template > <script > export default { name : 'Son2Com' } </script > <style lang ="css" scoped > .box { border : 3px solid #ccc ; width : 400px ; padding : 10px ; margin : 20px ; } h2 { margin-top : 10px ; } </style >
三、vuex 的使用 - 创建仓库
1.安装 vuex
安装vuex与vue-router类似,vuex是一个独立存在的插件,如果脚手架初始化没有选 vuex,就需要额外安装。
1 yarn add vuex@3 或者 npm i vuex@3
2.新建 store/index.js
专门存放 vuex
为了维护项目目录的整洁,在src目录下新建一个store目录其下放置一个index.js文件。 (和 router/index.js
类似)
3.创建仓库 store/index.js
1 2 3 4 5 6 7 8 9 10 11 12 import Vue from 'vue' import Vuex from 'vuex' Vue .use (Vuex )const store = new Vuex .Store ()export default store
4 在 main.js 中导入挂载到 Vue 实例上
1 2 3 4 5 6 7 8 9 10 import Vue from 'vue' import App from './App.vue' import store from './store' Vue .config .productionTip = false new Vue ({ render : h => h (App ), store }).$mount('#app' )
此刻起, 就成功创建了一个 空仓库!!
5.测试打印Vuex
App.vue
1 2 3 created ( ){ console .log (this .$store ) }
四、核心概念 - state 状态
1.目标
明确如何给仓库 提供 数据,如何 使用 仓库的数据
2.提供数据
State提供唯一的公共数据源,所有共享的数据都要统一放到Store中的State中存储。
打开项目中的store.js文件,在state对象中可以添加我们要共享的数据。
1 2 3 4 5 6 7 8 9 10 const store = new Vuex .Store ({ state : { count : 101 } })
3.访问Vuex中的数据
问题: 如何在组件中获取count?
通过$store直接访问 —>
通过辅助函数mapState 映射计算属性 —>
4.通过$store访问的语法
1 2 3 4 5 6 7 8 获取 store: 1. Vue 模板中获取 this .$store 2. js文件中获取 import 导入 store 模板中: {{ $store.state .xxx }} 组件逻辑中: this .$store .state .xxx JS 模块中: store.state .xxx
5.代码实现
5.1模板中使用
组件中可以使用 $store 获取到vuex中的store对象实例,可通过state 属性属性获取count , 如下
1 <h1>state的数据 - {{ $store.state.count }}</h1>
5.2组件逻辑中使用
将state属性定义在计算属性中 https://vuex.vuejs.org/zh/guide/state.html
1 2 3 4 5 6 7 8 <h1>state的数据 - {{ count }}</h1> computed : { count () { return this .$store .state .count } }
5.3 js文件中使用
1 2 3 4 5 //main.js import store from "@/store" console.log(store.state.count)
每次都像这样一个个的提供计算属性, 太麻烦了,我们有没有简单的语法帮我们获取state中的值呢?
五、通过辅助函数 - mapState获取 state中的数据
mapState是辅助函数,帮助我们把store中的数据映射到 组件的计算属性中, 它属于一种方便的用法
用法 :
1.第一步:导入mapState (mapState是vuex中的一个函数)
1 import { mapState } from 'vuex'
2.第二步:采用数组形式引入state属性
上面代码的最终得到的是 类似于
1 2 3 count () { return this .$store .state .count }
3.第三步:利用展开运算符 将导出的状态映射给计算属性
1 2 3 computed : { ...mapState (['count' ]) }
1 <div> state的数据:{{ count }}</div>
六、开启严格模式及Vuex的单项数据流
1.目标
明确 vuex 同样遵循单向数据流,组件中不能直接修改仓库的数据
2.直接在组件中修改Vuex中state的值
Son1.vue
1 2 3 4 5 6 7 8 9 10 button @click="handleAdd">值 + 1</button> methods:{ handleAdd (n) { // 错误代码(vue默认不会监测,监测需要成本) this.$store.state.count++ // console.log(this.$store.state.count) }, }
3.开启严格模式
通过 strict: true
可以开启严格模式,开启严格模式后,直接修改state中的值会报错
state数据的修改只能通过mutations,并且mutations必须是同步的
七、核心概念-mutations
1.定义mutations
1 2 3 4 5 6 7 8 9 const store = new Vuex .Store ({ state : { count : 0 }, mutations : { } })
2.格式说明
mutations是一个对象,对象中存放修改state的方法
1 2 3 4 5 6 7 mutations : { addCount (state) { state.count += 1 } },
3.组件中提交 mutations
1 this .$store .commit ('addCount' )
4.练习
1.在mutations中定义个点击按钮进行 +5 的方法
2.在mutations中定义个点击按钮进行 改变title 的方法
3.在组件中调用mutations修改state中的值
5.总结
通过mutations修改state的步骤
1.定义 mutations 对象,对象中存放修改 state 的方法
2.组件中提交调用 mutations(通过$store.commit(‘mutations的方法名’))
八、带参数的 mutations
1.目标:
掌握 mutations 传参语法
2.语法
看下面这个案例,每次点击不同的按钮,加的值都不同,每次都要定义不同的mutations处理吗?
提交 mutation 是可以传递参数的 this.$store.commit('xxx', 参数)
2.1 提供mutation函数(带参数)
1 2 3 4 5 6 mutations : { ... addCount (state, count) { state.count = count } },
2.2 提交mutation
1 2 3 handle ( ) { this .$store .commit ('addCount' , 10 ) }
小tips: 提交的参数只能是一个, 如果有多个参数要传, 可以传递一个对象
1 2 3 this .$store .commit ('addCount' , { count : 10 })
九、练习-mutations的减法功能
1.步骤
2.代码实现
Son2.vue
1 2 3 4 5 6 7 8 9 10 11 <button @click="subCount(1)">值 - 1</button> <button @click="subCount(5)">值 - 5</button> <button @click="subCount(10)">值 - 10</button> export default { methods:{ subCount (n) { this.$store.commit('addCount', n) }, } }
store/index.js
1 2 3 4 5 mutations :{ subCount (state, n) { state.count -= n }, }
1.目标
实时输入,实时更新,巩固 mutations 传参语法
不要用v-model !!!
2.实现步骤
3.代码实现
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 <input :value="count" @input="handleInput" type="text"> export default { methods: { handleInput (e) { // 1. 实时获取输入框的值 const num = +e.target.value // 2. 提交mutation,调用mutation函数 this.$store.commit('changeCount', num) } } }
store/index.js
1 2 3 4 5 mutations : { changeCount (state, newCount) { state.count = newCount } },
十一、辅助函数- mapMutations
mapMutations和mapState很像,它把位于mutations中的方法提取了出来,我们可以将它导入
1 2 3 4 import { mapMutations } from 'vuex' methods : { ...mapMutations (['addCount' ]) }
上面代码的含义是将mutations的方法导入了methods中,等价于
1 2 3 4 5 6 methods : { addCount () { this .$store .commit ('addCount' ) } }
此时,就可以直接通过this.addCount调用了
1 <button @click="addCount" >值+1 </button>
但是请注意: Vuex中mutations中要求不能写异步代码,如果有异步的ajax请求,应该放置在actions中
十二、核心概念 - actions
state是存放数据的,mutations是同步更新数据 (便于监测数据的变化, 更新视图等, 方便于调试工具查看变化),
actions则负责进行异步操作
说明:mutations必须是同步的
需求: 一秒钟之后, 要给一个数 去修改state
1.定义actions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mutations : { changeCount (state, newCount) { state.count = newCount } } actions : { setAsyncCount (context, num) { setTimeout (() => { context.commit ('changeCount' , num) }, 1000 ) } },
2.组件中通过dispatch调用
1 2 3 setAsyncCount () { this .$store .dispatch ('setAsyncCount' , 666 ) }
十三、辅助函数 -mapActions
1.目标:掌握辅助函数 mapActions,映射方法
mapActions 是把位于 actions中的方法提取了出来,映射到组件methods中
Son2.vue
1 2 3 4 5 6 7 8 9 10 11 import { mapActions } from 'vuex' methods : { ...mapActions (['changeCountAction' ]) }
直接通过 this.方法 就可以调用
1 <button @click="changeCountAction(200)">+异步</button>
十四、核心概念 - getters
除了state之外,有时我们还需要从state中筛选出符合条件的一些数据 ,这些数据是依赖state的,此时会用到getters
例如,state中定义了list,为1-10的数组,
1 2 3 state : { list : [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ] }
组件中,需要显示所有大于5的数据,正常的方式,是需要list在组件中进行再一步的处理,但是getters可以帮助我们实现它
1.定义getters
1 2 3 4 5 getters : { filterList : state => state.list .filter (item => item > 5 ) }
2.使用getters
2.1原始方式-$store
1 <div>{{ $store.getters.filterList }}</div>
2.2辅助函数 - mapGetters
1 2 3 computed : { ...mapGetters (['filterList' ]) }
1 <div>{{ filterList }}</div>
十五、使用小结
十六、核心概念 - module
1.目标
掌握核心概念 module 模块的创建
2.问题
由于使用单一状态树 ,应用的所有状态会集中到一个比较大的对象 。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
这句话的意思是,如果把所有的状态都放在state中,当项目变得越来越大的时候,Vuex会变得越来越难以维护
由此,又有了Vuex的模块化
3.模块定义 - 准备 state
定义两个模块 user 和 setting
user中管理用户的信息状态 userInfo modules/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const state = { userInfo : { name : 'zs' , age : 18 } } const mutations = {}const actions = {}const getters = {}export default { state, mutations, actions, getters }
setting中管理项目应用的 主题色 theme,描述 desc, modules/setting.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const state = { theme : 'dark' desc : '描述真呀真不错' } const mutations = {}const actions = {}const getters = {}export default { state, mutations, actions, getters }
在store/index.js
文件中的modules配置项中,注册这两个模块
1 2 3 4 5 6 7 8 9 import user from './modules/user' import setting from './modules/setting' const store = new Vuex .Store ({ modules :{ user, setting } })
使用模块中的数据, 可以直接通过模块名访问 $store.state.模块名.xxx
=> $store.state.setting.desc
也可以通过 mapState 映射
十七、获取模块内的state数据
1.目标:
掌握模块中 state 的访问语法
尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的 state 中,属性名就是模块名
2.使用模块中的数据
直接通过模块名访问 $store.state.模块名.xxx
通过 mapState 映射:
默认根级别的映射 mapState([ ‘xxx’ ])
子模块的映射 :mapState(‘模块名’, [‘xxx’]) - 需要开启命名空间 namespaced:true
modules/user.js
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 const state = { userInfo : { name : 'zs' , age : 18 }, myMsg : '我的数据' } const mutations = { updateMsg (state, msg) { state.myMsg = msg } } const actions = {}const getters = {}export default { namespaced : true , state, mutations, actions, getters }
3.代码示例
$store直接访问
1 $store.state .user .userInfo .name
mapState辅助函数访问
1 2 ...mapState ('user' , ['userInfo' ]), ...mapState ('setting' , ['theme' , 'desc' ]),
十八、获取模块内的getters数据
1.目标:
掌握模块中 getters 的访问语
2.语法:
使用模块中 getters 中的数据:
直接通过模块名访问 $store.getters['模块名/xxx ']
通过 mapGetters 映射
默认根级别的映射 mapGetters([ 'xxx' ])
子模块的映射 mapGetters('模块名', ['xxx'])
- 需要开启命名空间
3.代码演示
modules/user.js
1 2 3 4 5 6 const getters = { UpperCaseName (state) { return state.userInfo .name .toUpperCase () } }
Son1.vue 直接访问getters
1 2 <div > {{ $store.getters['user/UpperCaseName'] }}</div >
Son2.vue 通过命名空间访问
1 2 3 computed :{ ...mapGetters ('user' , ['UpperCaseName' ]) }
十九、获取模块内的mutations方法
1.目标:
掌握模块中 mutation 的调用语法
2.注意:
默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间 ,才会挂载到子模块。
3.调用方式:
直接通过 store 调用 $store.commit('模块名/xxx ', 额外参数)
通过 mapMutations 映射
默认根级别的映射 mapMutations([ ‘xxx’ ])
子模块的映射 mapMutations(‘模块名’, [‘xxx’]) - 需要开启命名空间
4.代码实现
modules/user.js
1 2 3 4 5 const mutations = { setUser (state, newUserInfo) { state.userInfo = newUserInfo } }
modules/setting.js
1 2 3 4 5 const mutations = { setTheme (state, newTheme) { state.theme = newTheme } }
Son1.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <button @click="updateUser">更新个人信息</button> <button @click="updateTheme">更新主题色</button> export default { methods: { updateUser () { // $store.commit('模块名/mutation名', 额外传参) this.$store.commit('user/setUser', { name: 'xiaowang', age: 25 }) }, updateTheme () { this.$store.commit('setting/setTheme', 'pink') } } }
Son2.vue
1 2 3 4 5 6 7 8 <button @click="setUser({ name: 'xiaoli', age: 80 })">更新个人信息</button> <button @click="setTheme('skyblue')">更新主题</button> methods:{ // 分模块的映射 ...mapMutations('setting', ['setTheme']), ...mapMutations('user', ['setUser']), }
二十、获取模块内的actions方法
1.目标:
掌握模块中 action 的调用语法 (同理 - 直接类比 mutation 即可)
2.注意:
默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间 ,才会挂载到子模块。
3.调用语法:
直接通过 store 调用 $store.dispatch('模块名/xxx ', 额外参数)
通过 mapActions 映射
默认根级别的映射 mapActions([ ‘xxx’ ])
子模块的映射 mapActions(‘模块名’, [‘xxx’]) - 需要开启命名空间
4.代码实现
需求:
modules/user.js
1 2 3 4 5 6 7 8 9 const actions = { setUserSecond (context, newUserInfo) { setTimeout (() => { context.commit ('setUser' , newUserInfo) }, 1000 ) } }
Son1.vue 直接通过store调用
1 2 3 4 5 6 7 8 9 10 11 <button @click="updateUser2">一秒后更新信息</button> methods:{ updateUser2 () { // 调用action dispatch this.$store.dispatch('user/setUserSecond', { name: 'xiaohong', age: 28 }) }, }
Son2.vue mapActions映射
1 2 3 4 5 <button @click="setUserSecond({ name: 'xiaoli', age: 80 })" >一秒后更新信息</button> methods :{ ...mapActions ('user' , ['setUserSecond' ]) }
二十一、Vuex模块化的使用小结
1.直接使用
state --> $store.state.模块名 .数据项名
getters --> $store.getters[‘模块名 /属性名’]
mutations --> $store.commit(‘模块名 /方法名’, 其他参数)
actions --> $store.dispatch(‘模块名 /方法名’, 其他参数)
2.借助辅助方法使用
1.import { mapXxxx, mapXxx } from ‘vuex’
computed、methods: {
// …mapState、…mapGetters放computed中;
// …mapMutations、…mapActions放methods中;
…mapXxxx(‘模块名’ , [‘数据项|方法’]),
…mapXxxx(‘模块名’ , { 新的名字: 原来的名字 }),
}
2.组件中直接使用 属性 {{ age }}
或 方法 @click="updateAge(2)"
二十二、综合案例 - 创建项目
脚手架新建项目 (注意:勾选vuex )
版本说明:
vue2 vue-router3 vuex3
vue3 vue-router4 vuex4/pinia
1 vue create vue-cart-demo
将原本src内容清空,替换成教学资料的《vuex-cart-准备代码》
需求:
发请求动态渲染购物车,数据存vuex (存cart模块, 将来还会有user模块,article模块…)
数字框可以修改数据
动态计算总价和总数量
二十三、综合案例-构建vuex-cart模块
新建 store/modules/cart.js
1 2 3 4 5 6 7 8 export default { namespaced : true , state () { return { list : [] } }, }
挂载到 vuex 仓库上 store/cart.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import Vuex from 'vuex' import Vue from 'vue' import cart from './modules/cart' Vue .use (Vuex )const store = new Vuex .Store ({ modules : { cart } }) export default store
二十四、综合案例-准备后端接口服务环境(了解)
安装全局工具 json-server (全局工具仅需要安装一次)
1 yarn global add json -server 或 npm i json -server -g
代码根目录新建一个 db 目录
将资料 index.json 移入 db 目录
进入 db 目录,执行命令,启动后端接口服务 (使用–watch 参数 可以实时监听 json 文件的修改)
二十五、综合案例-请求动态渲染数据
1.目标
请求获取数据存入 vuex, 映射渲染
安装 axios
准备actions 和 mutations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import axios from 'axios' export default { namespaced : true , state () { return { list : [] } }, mutations : { updateList (state, payload) { state.list = payload } }, actions : { async getList (ctx) { const res = await axios.get ('http://localhost:3000/cart' ) ctx.commit ('updateList' , res.data ) } } }
App.vue
页面中调用 action, 获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { mapState } from 'vuex' export default { name : 'App' , components : { CartHeader , CartFooter , CartItem }, created () { this .$store .dispatch ('cart/getList' ) }, computed : { ...mapState ('cart' , ['list' ]) } }
动态渲染
1 2 <!-- 商品 Item 项组件 --> <cart-item v-for ="item in list" :key ="item.id" :item ="item" > </cart-item >
cart-item.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 <template> <div class ="goods-container" > <div class ="left" > <img :src ="item.thumb" class ="avatar" alt ="" > </div > <div class ="right" > <div class ="title" > {{item.name}}</div > <div class ="info" > <span class ="price" > ¥{{item.price}}</span > <div class ="btns" > <button class ="btn btn-light" > -</button > <span class ="count" > {{item.count}}</span > <button class ="btn btn-light" > +</button > </div > </div > </div > </div > </template > <script > export default { name : 'CartItem' , props : { item : Object }, methods : { } } </script >
二十六、综合案例-修改数量
注册点击事件
1 2 3 4 <!-- 按钮区域 --> <button class ="btn btn-light" @click ="onBtnClick(-1)" > -</button > <span class ="count" > {{item.count}}</span > <button class ="btn btn-light" @click ="onBtnClick(1)" > +</button >
页面中dispatch action
1 2 3 4 5 6 7 8 9 10 onBtnClick (step) { const newCount = this .item .count + step if (newCount < 1 ) return this .$store .dispatch ('cart/updateCount' , { id : this .item .id , count : newCount }) }
提供action函数
1 2 3 4 5 6 async updateCount (ctx, payload) { await axios.patch ('http://localhost:3000/cart/' + payload.id , { count : payload.count }) ctx.commit ('updateCount' , payload) }
提供mutation处理函数
1 2 3 4 5 6 7 mutations : { ..., updateCount (state, payload) { const goods = state.list .find ((item ) => item.id === payload.id ) goods.count = payload.count } },
二十七、综合案例-底部总价展示
提供getters
1 2 3 4 5 6 7 8 getters : { total (state ) { return state.list .reduce ((p, c ) => p + c.count , 0 ); }, totalPrice (state) { return state.list .reduce ((p, c ) => p + c.count * c.price , 0 ); }, },
动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div class ="footer-container" > <div > <span > 共 {{total}} 件商品,合计:</span > <span class ="price" > ¥{{totalPrice}}</span > </div > <button class ="btn btn-success btn-settle" > 结算</button > </div > </template> <script > import { mapGetters } from 'vuex' export default { name : 'CartFooter' , computed : { ...mapGetters ('cart' , ['total' , 'totalPrice' ]) } } </script >
智慧商城 - 授课大纲
接口文档:https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080
演示地址:http://cba.itlike.com/public/mweb/#/
01. 项目功能演示
1.明确功能模块
启动准备好的代码,演示移动端面经内容,明确功能模块
2.项目收获
02. 项目创建目录初始化
vue-cli 建项目
1.安装脚手架 (已安装)
2.创建项目
1 2 3 4 5 Vue CLI v5.0 .8 ? Please pick a preset : Default ([Vue 3 ] babel, eslint) Default ([Vue 2 ] babel, eslint) > Manually select features 选自定义
选择eslint的风格 (eslint 代码规范的检验工具,检验代码是否符合规范)
比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子
是否保存预设,下次直接使用? => 不保存,输入 N
03. 调整初始化目录结构
强烈建议大家严格按照老师的步骤进行调整,为了符合企业规范
为了更好的实现后面的操作,我们把整体的目录结构做一些调整。
目标:
删除初始化的一些默认文件
修改没删除的文件
新增我们需要的目录结构
1.删除文件
src/assets/logo.png
src/components/HelloWorld.vue
src/views/AboutView.vue
src/views/HomeView.vue
2.修改文件
main.js
不需要修改
router/index.js
删除默认的路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import Vue from 'vue' import VueRouter from 'vue-router' Vue .use (VueRouter )const routes = [] const router = new VueRouter ({ routes }) export default router
App.vue
1 2 3 4 5 <template > <div id ="app" > <router-view /> </div > </template >
3.新增目录
目录效果如下:
04. vant组件库及Vue周边的其他组件库
组件库:第三方封装好了很多很多的组件,整合到一起就是一个组件库。
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/
比如日历组件、键盘组件、打分组件、下拉筛选组件等
组件库并不是唯一的,常用的组件库还有以下几种:
pc: element-ui element-plus iview ant-design
移动:vant-ui Mint UI (饿了么) Cube UI (滴滴)
05. 全部导入和按需导入的区别
目标:明确 全部导入 和 按需导入 的区别
区别:
1.全部导入会引起项目打包后的体积变大,进而影响用户访问网站的性能
2.按需导入只会导入你使用的组件,进而节约了资源
06. 全部导入
1 2 3 4 import Vant from 'vant' ;import 'vant/lib/index.css' ;Vue .use (Vant )
1 2 <van-button type="primary" >主要按钮</van-button> <van-button type ="info" > 信息按钮</van-button >
vant-ui提供了很多的组件,全部导入,会导致项目打包变得很大。
07. 按需导入
1 yarn add babel-plugin-import -D
1 2 3 4 5 6 7 8 9 10 11 12 module .exports = { presets : [ '@vue/cli-plugin-babel/preset' ], plugins : [ ['import' , { libraryName : 'vant' , libraryDirectory : 'es' , style : true }, 'vant' ] ] }
1 2 3 4 import { Button , Icon } from 'vant' Vue .use (Button )Vue .use (Icon )
1 2 3 4 5 <van-button type="primary" >主要按钮</van-button> <van-button type ="info" > 信息按钮</van-button > <van-button type ="default" > 默认按钮</van-button > <van-button type ="warning" > 警告按钮</van-button > <van-button type ="danger" > 危险按钮</van-button >
把引入组件的步骤抽离到单独的js文件中比如 utils/vant-ui.js
1 2 3 4 import { Button , Icon } from 'vant' Vue .use (Button )Vue .use (Icon )
main.js中进行导入
1 2 import '@/utils/vant-ui'
如果没有样式的话重启一下就行了
08. 项目中的vw适配
官方说明:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage
1 yarn add postcss-px-to-viewport@1.1 .1 -D
项目根目录, 新建postcss的配置文件postcss.config.js
1 2 3 4 5 6 7 8 module .exports = { plugins : { 'postcss-px-to-viewport' : { viewportWidth : 375 , }, }, };
viewportWidth:设计稿的视口宽度
vant-ui中的组件就是按照375的视口宽度设计的
恰好面经项目中的设计稿也是按照375的视口宽度设计的,所以此时 我们只需要配置375就可以了
如果设计稿不是按照375而是按照750的宽度设计,那此时这个值该怎么填呢?
09. 路由配置 - 一级路由
但凡是单个页面,独立展示的,都是一级路由
每个文件夹下的index界面如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <div > 我是layout</div > </template > <script > export default { name : 'LayoutIndex' } </script > <style > </style >
路由设计:
登录页
首页架子
首页 - 二级
分类页 - 二级
购物车 - 二级
我的 - 二级
搜索页
搜索列表页
商品详情页
结算支付页
我的订单页
router/index.js
配置一级路由,新建对应的页面文件
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 import Vue from 'vue' import VueRouter from 'vue-router' import Layout from '@/views/layout' import Search from '@/views/search' import SearchList from '@/views/search/list' import ProDetail from '@/views/prodetail' import Login from '@/views/login' import Pay from '@/views/pay' import MyOrder from '@/views/myorder' Vue .use (VueRouter )const router = new VueRouter ({ routes : [ { path : '/login' , component : Login }, { path : '/' , component : Layout }, { path : '/search' , component : Search }, { path : '/searchlist' , component : SearchList }, { path : '/prodetail/:id' , component : ProDetail }, { path : '/pay' , component : Pay }, { path : '/myorder' , component : MyOrder } ] }) export default router
10. 路由配置-tabbar标签页
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar
vant-ui.js
引入组件
1 2 3 import { Tabbar , TabbarItem } from 'vant' Vue .use (Tabbar )Vue .use (TabbarItem )
layout.vue
复制官方代码
修改显示文本及显示的图标
配置高亮颜色
1 2 3 4 5 6 7 8 9 10 11 <template> <div > <van-tabbar active-color ="#ee0a24" inactive-color ="#000" > <van-tabbar-item icon ="wap-home-o" > 首页</van-tabbar-item > <van-tabbar-item icon ="apps-o" > 分类页</van-tabbar-item > <van-tabbar-item icon ="shopping-cart-o" > 购物车</van-tabbar-item > <van-tabbar-item icon ="user-o" > 我的</van-tabbar-item > </van-tabbar > </div > </template>
11. 路由配置 - 二级路由
router/index.js
配置二级路由
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 import Vue from 'vue' import VueRouter from 'vue-router' import Layout from '@/views/layout' import Search from '@/views/search' import SearchList from '@/views/search/list' import ProDetail from '@/views/prodetail' import Login from '@/views/login' import Pay from '@/views/pay' import MyOrder from '@/views/myorder' import Home from '@/views/layout/home' import Category from '@/views/layout/category' import Cart from '@/views/layout/cart' import User from '@/views/layout/user' Vue .use (VueRouter )const router = new VueRouter ({ routes : [ { path : '/login' , component : Login }, { path : '/' , component : Layout , redirect : '/home' , children : [ { path : 'home' , component : Home }, { path : 'category' , component : Category }, { path : 'cart' , component : Cart }, { path : 'user' , component : User } ] }, { path : '/search' , component : Search }, { path : '/searchlist' , component : SearchList }, { path : '/prodetail/:id' , component : ProDetail }, { path : '/pay' , component : Pay }, { path : '/myorder' , component : MyOrder } ] }) export default router
准备对应的组件文件
layout/home.vue
layout/category.vue
layout/cart.vue
layout/user.vue
layout.vue
配置路由出口, 配置 tabbar
1 2 3 4 5 6 7 8 9 10 11 <template> <div > <router-view > </router-view > <van-tabbar route active-color ="#ee0a24" inactive-color ="#000" > <van-tabbar-item to ="/home" icon ="wap-home-o" > 首页</van-tabbar-item > <van-tabbar-item to ="/category" icon ="apps-o" > 分类页</van-tabbar-item > <van-tabbar-item to ="/cart" icon ="shopping-cart-o" > 购物车</van-tabbar-item > <van-tabbar-item to ="/user" icon ="user-o" > 我的</van-tabbar-item > </van-tabbar > </div > </template>
12. 登录页静态布局
(1) 准备工作
新建 styles/common.less
重置默认样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 * { margin : 0 ; padding : 0 ; box-sizing : border-box; } .text -ellipsis-2 { overflow : hidden; -webkit-line-clamp : 2 ; text-overflow : ellipsis; display : -webkit-box; -webkit-box-orient : vertical; }
main.js 中导入应用
1 import '@/styles/common.less'
将准备好的一些图片素材拷贝到 assets 目录【备用】
(2) 登录静态布局
使用组件
vant-ui.js
注册
1 2 import { NavBar } from 'vant' Vue .use (NavBar )
Login.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 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 <template> <div class="login"> <van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" /> <div class="container"> <div class="title"> <h3>手机号登录</h3> <p>未注册的手机号登录后将自动注册</p> </div> <div class="form"> <div class="form-item"> <input class="inp" maxlength="11" placeholder="请输入手机号码" type="text"> </div> <div class="form-item"> <input class="inp" maxlength="5" placeholder="请输入图形验证码" type="text"> <img src="@/assets/code.png" alt=""> </div> <div class="form-item"> <input class="inp" placeholder="请输入短信验证码" type="text"> <button>获取验证码</button> </div> </div> <div class="login-btn">登录</div> </div> </div> </template> <script> export default { name: 'LoginPage' } </script> <style lang="less" scoped> .container { padding: 49px 29px; .title { margin-bottom: 20px; h3 { font-size: 26px; font-weight: normal; } p { line-height: 40px; font-size: 14px; color: #b8b8b8; } } .form-item { border-bottom: 1px solid #f3f1f2; padding: 8px; margin-bottom: 14px; display: flex; align-items: center; .inp { display: block; border: none; outline: none; height: 32px; font-size: 14px; flex: 1; } img { width: 94px; height: 31px; } button { height: 31px; border: none; font-size: 13px; color: #cea26a; background-color: transparent; padding-right: 9px; } } .login-btn { width: 100%; height: 42px; margin-top: 39px; background: linear-gradient(90deg,#ecb53c,#ff9211); color: #fff; border-radius: 39px; box-shadow: 0 10px 20px 0 rgba(0,0,0,.1); letter-spacing: 2px; display: flex; justify-content: center; align-items: center; } } </style>
添加通用样式
styles/common.less
设置导航条,返回箭头颜色
1 2 3 4 5 6 .van -nav-bar { .van -icon-arrow-left { color : #333 ; } }
13. request模块 - axios封装
接口文档:https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080
演示地址:http://cba.itlike.com/public/mweb/#/
基地址:http://cba.itlike.com/public/index.php?s=/api/
我们会使用 axios 来请求后端接口 , 一般都会对 axios 进行一些配置 (比如: 配置基础地址,请求响应拦截器等等)
一般项目开发中, 都会对 axios 进行基本的二次封装 , 单独封装到一个模块中, 便于使用
目标:将 axios 请求方法,封装到 request 模块
安装 axios
新建 utils/request.js
封装 axios 模块
利用 axios.create 创建一个自定义的 axios 来使用
http://www.axios-js.com/zh-cn/docs/#axios-create-config
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 import axios from 'axios' const request = axios.create ({ baseURL : 'http://cba.itlike.com/public/index.php?s=/api/' , timeout : 5000 }) request.interceptors .request .use (function (config ) { return config }, function (error ) { return Promise .reject (error) }) request.interceptors .response .use (function (response ) { return response.data }, function (error ) { return Promise .reject (error) }) export default request
login中获取图形验证码,请求测试
1 2 3 4 5 6 7 8 import request from '@/utils/request' export default { name : 'LoginPage' , async created () { const res = await request.get ('/captcha/image' ) console .log (res) } }
14. 图形验证码功能完成
准备数据,获取图形验证码后存储图片路径,存储图片唯一标识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async created () { this .getPicCode () }, data () { return { picUrl : '' , picKey : '' } }, methods : { async getPicCode () { const { data : { base64, key } } = await request.get ('/captcha/image' ) this .picUrl = base64 this .picKey = key } }
动态渲染图形验证码,并且点击时要重新刷新验证码
1 <img v-if ="picUrl" :src="picUrl" @click="getPicCode" >
15. 封装api接口 - 图片验证码接口
**1.目标:**将请求封装成方法,统一存放到 api 模块,与页面分离
2.原因:以前的模式
页面中充斥着请求代码
可阅读性不高
相同的请求没有复用请求没有统一管理
3.期望:
请求与页面逻辑分离
相同的请求可以直接复用请求
进行了统一管理
4.具体实现
新建 api/login.js
提供获取图形验证码 Api 函数
1 2 3 4 5 6 import request from '@/utils/request' export const getPicCode = ( ) => { return request.get ('/captcha/image' ) }
login/index.vue
页面中调用测试
1 2 3 4 5 6 7 import { getPicCode } from '@/api/login' async getPicCode () { const { data : { base64, key } } = await getPicCode () this .picUrl = base64 this .picKey = key },
16. toast 轻提示
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/toast
两种使用方式
导入调用 ( 组件内 或 非组件中均可 )
1 2 import { Toast } from 'vant' ;Toast ('提示内容' );
通过this直接调用 ( **组件内 **)
main.js 注册绑定到原型
1 2 import { Toast } from 'vant' ;Vue .use (Toast )
17. 短信验证倒计时功能
(1) 倒计时基础效果
准备 data 数据
1 2 3 4 5 6 7 data () { return { totalSecond : 60 , second : 60 , timer : null } },
给按钮注册点击事件
1 2 3 <button @click="getCode" > {{ second === totalSecond ? '获取验证码' : second + `秒后重新发送` }} </button>
开启倒计时时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async getCode () { if (!this .timer && this .second === this .totalSecond ) { this .timer = setInterval (() => { this .second -- if (this .second < 1 ) { clearInterval (this .timer ) this .timer = null this .second = this .totalSecond } }, 1000 ) this .$toast('发送成功,请注意查收' ) } }
离开页面销毁定时器
1 2 3 destroyed () { clearInterval (this .timer ) }
(2) 验证码请求校验处理
输入框 v-model 绑定变量
1 2 3 4 5 6 7 8 9 data () { return { mobile : '' , picCode : '' } }, <input v-model="mobile" class="inp" maxlength="11" placeholder="请输入手机号码" type="text"> <input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
methods中封装校验方法
1 2 3 4 5 6 7 8 9 10 11 12 validFn () { if (!/^1[3-9]\d{9}$/ .test (this .mobile )) { this .$toast('请输入正确的手机号' ) return false } if (!/^\w{4}$/ .test (this .picCode )) { this .$toast('请输入正确的图形验证码' ) return false } return true },
请求倒计时前进行校验
1 2 3 4 5 6 7 async getCode () { if (!this .validFn ()) { return } ... }
(3) 封装接口,请求获取验证码
封装接口 api/login.js
1 2 3 4 5 6 7 8 9 10 export const getMsgCode = (captchaCode, captchaKey, mobile ) => { return request.post ('/captcha/sendSmsCaptcha' , { form : { captchaCode, captchaKey, mobile } }) }
调用接口,添加提示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async getCode () { if (!this .validFn ()) { return } if (!this .timer && this .second === this .totalSecond ) { await getMsgCode (this .picCode , this .picKey , this .mobile ) this .$toast('发送成功,请注意查收' ) ... } }
18. 封装api接口 - 登录功能
api/login.js
提供登录 Api 函数
1 2 3 4 5 6 7 8 9 10 11 export const codeLogin = (mobile, smsCode ) => { return request.post ('/passport/login' , { form : { isParty : false , mobile, partyData : {}, smsCode } }) }
login/index.vue
登录功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <input class ="inp" v-model="msgCode" maxlength="6" placeholder="请输入短信验证码" type="text" > <div class ="login-btn" @click ="login" > 登录</div > data () { return { msgCode : '' , } }, methods : { async login () { if (!this .validFn ()) { return } if (!/^\d{6}$/ .test (this .msgCode )) { this .$toast('请输入正确的手机验证码' ) return } await codeLogin (this .mobile , this .msgCode ) this .$router .push ('/' ) this .$toast('登录成功' ) } }
19. 响应拦截器统一处理错误提示
响应拦截器是咱们拿到数据的 第一个 “数据流转站”,可以在里面统一处理错误,只要不是 200 默认给提示,抛出错误
utils/request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { Toast } from 'vant' ... request.interceptors .response .use (function (response ) { const res = response.data if (res.status !== 200 ) { Toast (res.message ) return Promise .reject (res.message ) } return res }, function (error ) { return Promise .reject (error) })
20. 将登录权证信息存入 vuex
新建 vuex user 模块 store/modules/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 export default { namespaced : true , state () { return { userInfo : { token : '' , userId : '' }, } }, mutations : {}, actions : {} }
挂载到 vuex 上
1 2 3 4 5 6 7 8 9 10 11 import Vue from 'vue' import Vuex from 'vuex' import user from './modules/user' Vue .use (Vuex )export default new Vuex .Store ({ modules : { user, } })
提供 mutations
1 2 3 4 5 mutations : { setUserInfo (state, obj) { state.userInfo = obj }, },
页面中 commit 调用
1 2 3 4 5 6 7 8 9 10 11 async login () { if (!this .validFn ()) { return } ... const res = await codeLogin (this .mobile , this .msgCode ) this .$store .commit ('user/setUserInfo' , res.data ) this .$router .push ('/' ) this .$toast('登录成功' ) }
21. vuex持久化处理
新建 utils/storage.js
封装方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const INFO_KEY = 'hm_shopping_info' export const getInfo = ( ) => { const result = localStorage .getItem (INFO_KEY ) return result ? JSON .parse (result) : { token : '' , userId : '' } } export const setInfo = (info ) => { localStorage .setItem (INFO_KEY , JSON .stringify (info)) } export const removeInfo = ( ) => { localStorage .removeItem (INFO_KEY ) }
vuex user 模块持久化处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { getInfo, setInfo } from '@/utils/storage' export default { namespaced : true , state () { return { userInfo : getInfo () } }, mutations : { setUserInfo (state, obj) { state.userInfo = obj setInfo (obj) } }, actions : {} }
22. 优化:添加请求 loading 效果
请求时,打开 loading
1 2 3 4 5 6 7 8 9 10 11 12 13 14 request.interceptors .request .use (function (config ) { Toast .loading ({ message : '请求中...' , forbidClick : true , loadingType : 'spinner' , duration : 0 }) return config }, function (error ) { return Promise .reject (error) })
响应时,关闭 loading
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 request.interceptors .response .use (function (response ) { const res = response.data if (res.status !== 200 ) { Toast (res.message ) return Promise .reject (res.message ) } else { Toast .clear () } return res }, function (error ) { return Promise .reject (error) })
23. 登录访问拦截 - 路由前置守卫
目标:基于全局前置守卫,进行页面访问拦截处理
说明:智慧商城项目,大部分页面,游客都可以直接访问, 如遇到需要登录才能进行的操作,提示并跳转到登录
但是:对于支付页,订单页等,必须是登录的用户才能访问的,游客不能进入该页面,需要做拦截处理
路由导航守卫 - 全局前置守卫
1.所有的路由一旦被匹配到,都会先经过全局前置守卫
2.只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
route/index.js
1 2 3 4 5 6 7 router.beforeEach ((to, from , next ) => { })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const authUrl = ['/pay' , '/myorder' ]router.beforeEach ((to, from , next ) => { const token = store.getters .token if (!authUrl.includes (to.path )) { next () return } const token=store.getters .token if (token) { next () } else { next ('/login' ) } })
在store/index.js中添加
1 2 3 getters: { token (state ) { return state .user .user Info.token } }
24. 首页 - 静态结构准备
静态结构和样式 layout/home.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 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 <template> <div class="home"> <!-- 导航条 --> <van-nav-bar title="智慧商城" fixed /> <!-- 搜索框 --> <van-search readonly shape="round" background="#f1f1f2" placeholder="请在此输入搜索关键词" @click="$router.push('/search')" /> <!-- 轮播图 --> <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white"> <van-swipe-item> <img src="@/assets/banner1.jpg" alt=""> </van-swipe-item> <van-swipe-item> <img src="@/assets/banner2.jpg" alt=""> </van-swipe-item> <van-swipe-item> <img src="@/assets/banner3.jpg" alt=""> </van-swipe-item> </van-swipe> <!-- 导航 --> <van-grid column-num="5" icon-size="40"> <van-grid-item v-for="item in 10" :key="item" icon="http://cba.itlike.com/public/uploads/10001/20230320/58a7c1f62df4cb1eb47fe83ff0e566e6.png" text="新品首发" @click="$router.push('/category')" /> </van-grid> <!-- 主会场 --> <div class="main"> <img src="@/assets/main.png" alt=""> </div> <!-- 猜你喜欢 --> <div class="guess"> <p class="guess-title">—— 猜你喜欢 ——</p> <div class="goods-list"> <GoodsItem v-for="item in 10" :key="item"></GoodsItem> </div> </div> </div> </template> <script> import GoodsItem from '@/components/GoodsItem.vue' export default { name: 'HomePage', components: { GoodsItem } } </script> <style lang="less" scoped> // 主题 padding .home { padding-top: 100px; padding-bottom: 50px; } // 导航条样式定制 .van-nav-bar { z-index: 999; background-color: #c21401; ::v-deep .van-nav-bar__title { color: #fff; } } // 搜索框样式定制 .van-search { position: fixed; width: 100%; top: 46px; z-index: 999; } // 分类导航部分 .my-swipe .van-swipe-item { height: 185px; color: #fff; font-size: 20px; text-align: center; background-color: #39a9ed; } .my-swipe .van-swipe-item img { width: 100%; height: 185px; } // 主会场 .main img { display: block; width: 100%; } // 猜你喜欢 .guess .guess-title { height: 40px; line-height: 40px; text-align: center; } // 商品样式 .goods-list { background-color: #f6f6f6; } </style>
新建components/GoodsItem.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 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 <template> <div class ="goods-item" @click ="$router.push('/prodetail')" > <div class ="left" > <img src ="@/assets/product.jpg" alt ="" /> </div > <div class ="right" > <p class ="tit text-ellipsis-2" > 三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23 </p > <p class ="count" > 已售104件</p > <p class ="price" > <span class ="new" > ¥3999.00</span > <span class ="old" > ¥6699.00</span > </p > </div > </div > </template> <script > export default {}</script > <style lang ="less" scoped > .goods-item { height : 148px ; margin-bottom : 6px ; padding : 10px ; background-color : #fff ; display : flex; .left { width : 127px ; img { display : block; width : 100% ; } } .right { flex : 1 ; font-size : 14px ; line-height : 1.3 ; padding : 10px ; display : flex; flex-direction : column; justify-content : space-evenly; .count { color : #999 ; font-size : 12px ; } .price { color : #999 ; font-size : 16px ; .new { color : #f03c3c ; margin-right : 10px ; } .old { text-decoration : line-through; font-size : 12px ; } } } } </style >
组件按需引入
1 2 3 4 5 6 7 import { Search , Swipe , SwipeItem , Grid , GridItem } from 'vant' Vue .use (GridItem )Vue .use (Search )Vue .use (Swipe )Vue .use (SwipeItem )Vue .use (Grid )
25. 首页 - 动态渲染
封装准备接口 api/home.js
1 2 3 4 5 6 7 8 9 10 import request from '@/utils/request' export const getHomeData = ( ) => { return request.get ('/page/detail' , { params : { pageId : 0 } }) }
页面中请求调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import GoodsItem from '@/components/GoodsItem.vue' import { getHomeData } from '@/api/home' export default { name : 'HomePage' , components : { GoodsItem }, data () { return { bannerList : [], navList : [], proList : [] } }, async created () { const { data : { pageData } } = await getHomeData () this .bannerList = pageData.items [1 ].data this .navList = pageData.items [3 ].data this .proList = pageData.items [6 ].data } }
轮播图、导航、猜你喜欢渲染
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 <!-- 轮播图 --> <van-swipe class ="my-swipe" :autoplay ="3000" indicator-color ="white" > <van-swipe-item v-for ="item in bannerList" :key ="item.imgUrl" > <img :src ="item.imgUrl" alt ="" > </van-swipe-item > </van-swipe > <van-grid column-num ="5" icon-size ="40" > <van-grid-item v-for ="item in navList" :key ="item.imgUrl" :icon ="item.imgUrl" :text ="item.text" @click ="$router.push('/category')" /> </van-grid > <div class ="guess" > <p class ="guess-title" > —— 猜你喜欢 ——</p > <div class ="goods-list" > <GoodsItem v-for ="item in proList" :item ="item" :key ="item.goods_id" > </GoodsItem > </div > </div >
商品组件内,动态渲染
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 <template> <div v-if ="item.goods_name" class ="goods-item" @click ="$router.push(`/prodetail/${item.goods_id}`)" > <div class ="left" > <img :src ="item.goods_image" alt ="" /> </div > <div class ="right" > <p class ="tit text-ellipsis-2" > {{ item.goods_name }} </p > <p class ="count" > 已售 {{ item.goods_sales }}件</p > <p class ="price" > <span class ="new" > ¥{{ item.goods_price_min }}</span > <span class ="old" > ¥{{ item.goods_price_max }}</span > </p > </div > </div > </template> <script > export default { props : { item : { type : Object , default : () => { return {} } } } } </script >
26. 搜索 - 静态布局准备
静态结构和代码
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 <template> <div class ="search" > <van-nav-bar title ="商品搜索" left-arrow @click-left ="$router.go(-1)" /> <van-search show-action placeholder ="请输入搜索关键词" clearable > <template #action > <div > 搜索</div > </template > </van-search > <div class ="search-history" > <div class ="title" > <span > 最近搜索</span > <van-icon name ="delete-o" size ="16" /> </div > <div class ="list" > <div class ="list-item" @click ="$router.push('/searchlist')" > 炒锅</div > <div class ="list-item" @click ="$router.push('/searchlist')" > 电视</div > <div class ="list-item" @click ="$router.push('/searchlist')" > 冰箱</div > <div class ="list-item" @click ="$router.push('/searchlist')" > 手机</div > </div > </div > </div > </template> <script > export default { name : 'SearchIndex' } </script > <style lang ="less" scoped > .search { .searchBtn { background-color : #fa2209 ; color : #fff ; } ::v-deep .van-search__action { background-color : #c21401 ; color : #fff ; padding : 0 20px ; border-radius : 0 5px 5px 0 ; margin-right : 10px ; } ::v-deep .van-icon-arrow-left { color : #333 ; } .title { height : 40px ; line-height : 40px ; font-size : 14px ; display : flex; justify-content : space-between; align-items : center; padding : 0 15px ; } .list { display : flex; justify-content : flex-start; flex-wrap : wrap; padding : 0 10px ; gap : 5% ; } .list-item { width : 30% ; text-align : center; padding : 7px ; line-height : 15px ; border-radius : 50px ; background : #fff ; font-size : 13px ; border : 1px solid #efefef ; overflow : hidden; white-space : nowrap; text-overflow : ellipsis; margin-bottom : 10px ; } } </style >
组件按需导入
1 2 import { Icon } from 'vant' Vue .use (Icon )
27. 搜索 - 历史记录 - 基本管理
data 中提供数据,和搜索框双向绑定 (实时获取用户内容)
1 2 3 4 5 6 7 8 9 10 11 data () { return { search : '' } } <van-search v-model="search" show-action placeholder="请输入搜索关键词" clearable> <template #action > <div > 搜索</div > </template > </van-search>
准备假数据,进行基本的历史纪录渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 data () { return { ... history : ['手机' , '空调' , '白酒' , '电视' ] } }, <div class ="search-history" v-if ="history.length > 0" > ... <div class ="list" > <div v-for ="item in history" :key ="item" @click ="goSearch(item)" class ="list-item" > {{ item }} </div > </div > </div >
点击搜索,或者下面搜索历史按钮,都要进行搜索历史记录更新 (去重,新搜索的内容置顶)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div @click="goSearch(search)" >搜索</div> <div class ="list" > <div v-for ="item in history" :key ="item" @click ="goSearch(item)" class ="list-item" > {{ item }} </div > </div > goSearch (key) { const index = this .history .indexOf (key) if (index !== -1 ) { this .history .splice (index, 1 ) } this .history .unshift (key) this .$router .push (`/searchlist?search=${key} ` ) }
清空历史
1 2 3 4 5 <van-icon @click="clear" name="delete-o" size="16" /> clear () { this .history = [] }
28. 搜索 - 历史记录 - 持久化
持久化到本地 - 封装方法
1 2 3 4 5 6 7 8 9 10 11 12 const HISTORY_KEY = 'hm_history_list' export const getHistoryList = ( ) => { const result = localStorage .getItem (HISTORY_KEY ) return result ? JSON .parse (result) : [] } export const setHistoryList = (arr ) => { localStorage .setItem (HISTORY_KEY , JSON .stringify (arr)) }
页面中调用 - 实现持久化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 data () { return { search : '' , history : getHistoryList () } }, methods : { goSearch (key) { ... setHistoryList (this .history ) this .$router .push (`/searchlist?search=${key} ` ) }, clear () { this .history = [] setHistoryList ([]) this .$toast .success ('清空历史成功' ) } }
29. 搜索列表 - 静态布局
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 <template> <div class ="search" > <van-nav-bar fixed title ="商品列表" left-arrow @click-left ="$router.go(-1)" /> <van-search readonly shape ="round" background ="#ffffff" value ="手机" show-action @click ="$router.push('/search')" > <template #action > <van-icon class ="tool" name ="apps-o" /> </template > </van-search > <div class ="sort-btns" > <div class ="sort-item" > 综合</div > <div class ="sort-item" > 销量</div > <div class ="sort-item" > 价格 </div > </div > <div class ="goods-list" > <GoodsItem v-for ="item in 10" :key ="item" > </GoodsItem > </div > </div > </template> <script > import GoodsItem from '@/components/GoodsItem.vue' export default { name : 'SearchIndex' , components : { GoodsItem } } </script > <style lang ="less" scoped > .search { padding-top : 46px ; ::v-deep .van-icon-arrow-left { color : #333 ; } .tool { font-size : 24px ; height : 40px ; line-height : 40px ; } .sort-btns { display : flex; height : 36px ; line-height : 36px ; .sort-item { text-align : center; flex : 1 ; font-size : 16px ; } } } // 商品样式 .goods-list { background-color : #f6f6f6 ; } </style >
30. 搜索列表 - 动态渲染
(1) 搜索关键字搜索
计算属性,基于query 解析路由参数
1 2 3 4 5 computed : { querySearch () { return this .$route .query .search } }
根据不同的情况,设置输入框的值
1 2 3 4 <van-search ... :value="querySearch || '搜索商品'" ></van-search>
api/product.js
封装接口,获取搜索商品
1 2 3 4 5 6 7 8 9 10 11 12 13 import request from '@/utils/request' export const getProList = (paramsObj ) => { const { categoryId, goodsName, page } = paramsObj return request.get ('/goods/list' , { params : { categoryId, goodsName, page } }) }
页面中基于 goodsName 发送请求,动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 data () { return { page : 1 , proList : [] } }, async created () { const { data : { list } } = await getProList ({ goodsName : this .querySearch , page : this .page }) this .proList = list.data } <div class ="goods-list" > <GoodsItem v-for ="item in proList" :key ="item.goods_id" :item ="item" > </GoodsItem > </div>
(2) 分类id搜索
1 封装接口 api/category.js
1 2 3 4 5 6 import request from '@/utils/request' export const getCategoryData = ( ) => { return request.get ('/category/list' ) }
2 分类页静态结构
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 <template> <div class ="category" > <van-nav-bar title ="全部分类" fixed /> <van-search readonly shape ="round" background ="#f1f1f2" placeholder ="请输入搜索关键词" @click ="$router.push('/search')" /> <div class ="list-box" > <div class ="left" > <ul > <li v-for ="(item, index) in list" :key ="item.category_id" > <a :class ="{ active: index === activeIndex }" @click ="activeIndex = index" href ="javascript:;" > {{ item.name }}</a > </li > </ul > </div > <div class ="right" > <div @click ="$router.push(`/searchlist?categoryId=${item.category_id}`)" v-for ="item in list[activeIndex]?.children" :key ="item.category_id" class ="cate-goods" > <img :src ="item.image?.external_url" alt ="" > <p > {{ item.name }}</p > </div > </div > </div > </div > </template > <script > import { getCategoryData } from '@/api/category' export default { name : 'CategoryPage' , created () { this .getCategoryList () }, data () { return { list : [], activeIndex : 0 } }, methods : { async getCategoryList () { const { data : { list } } = await getCategoryData () this .list = list } } } </script > <style lang ="less" scoped > // 主题 padding .category { padding-top : 100px ; padding-bottom : 50px ; height : 100vh ; .list-box { height : 100% ; display : flex; .left { width : 85px ; height : 100% ; background-color : #f3f3f3 ; overflow : auto; a { display : block; height : 45px ; line-height : 45px ; text-align : center; color : #444444 ; font-size : 12px ; &.active { color : #fb442f ; background-color : #fff ; } } } .right { flex : 1 ; height : 100% ; background-color : #ffffff ; display : flex; flex-wrap : wrap; justify-content : flex-start; align-content : flex-start; padding : 10px 0 ; overflow : auto; .cate-goods { width : 33.3% ; margin-bottom : 10px ; img { width : 70px ; height : 70px ; display : block; margin : 5px auto; } p { text-align : center; font-size : 12px ; } } } } } // 导航条样式定制 .van-nav-bar { z-index : 999 ; } // 搜索框样式定制 .van-search { position : fixed; width : 100% ; top : 46px ; z-index : 999 ; } </style >
3 搜索页,基于分类 ID 请求
1 2 3 4 5 6 7 8 async created () { const { data : { list } } = await getProList ({ categoryId : this .$route .query .categoryId , goodsName : this .querySearch , page : this .page }) this .proList = list.data }
31. 商品详情 - 静态布局
静态结构 和 样式
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 <template> <div class="prodetail"> <van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" /> <van-swipe :autoplay="3000" @change="onChange"> <van-swipe-item v-for="(image, index) in images" :key="index"> <img :src="image" /> </van-swipe-item> <template #indicator> <div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div> </template> </van-swipe> <!-- 商品说明 --> <div class="info"> <div class="title"> <div class="price"> <span class="now">¥0.01</span> <span class="oldprice">¥6699.00</span> </div> <div class="sellcount">已售1001件</div> </div> <div class="msg text-ellipsis-2"> 三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23 </div> <div class="service"> <div class="left-words"> <span><van-icon name="passed" />七天无理由退货</span> <span><van-icon name="passed" />48小时发货</span> </div> <div class="right-icon"> <van-icon name="arrow" /> </div> </div> </div> <!-- 商品评价 --> <div class="comment"> <div class="comment-title"> <div class="left">商品评价 (5条)</div> <div class="right">查看更多 <van-icon name="arrow" /> </div> </div> <div class="comment-list"> <div class="comment-item" v-for="item in 3" :key="item"> <div class="top"> <img src="http://cba.itlike.com/public/uploads/10001/20230321/a0db9adb2e666a65bc8dd133fbed7834.png" alt=""> <div class="name">神雕大侠</div> <van-rate :size="16" :value="5" color="#ffd21e" void-icon="star" void-color="#eee"/> </div> <div class="content"> 质量很不错 挺喜欢的 </div> <div class="time"> 2023-03-21 15:01:35 </div> </div> </div> </div> <!-- 商品描述 --> <div class="desc"> <img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/kHgx21fZMWwqirkMhawkAw.jpg" alt=""> <img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/0rRMmncfF0kGjuK5cvLolg.jpg" alt=""> <img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/2P04A4Jn0HKxbKYSHc17kw.jpg" alt=""> <img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/MT4k-mPd0veQXWPPO5yTIw.jpg" alt=""> </div> <!-- 底部 --> <div class="footer"> <div class="icon-home"> <van-icon name="wap-home-o" /> <span>首页</span> </div> <div class="icon-cart"> <van-icon name="shopping-cart-o" /> <span>购物车</span> </div> <div class="btn-add">加入购物车</div> <div class="btn-buy">立刻购买</div> </div> </div> </template> <script> export default { name: 'ProDetail', data () { return { images: [ 'https://img01.yzcdn.cn/vant/apple-1.jpg', 'https://img01.yzcdn.cn/vant/apple-2.jpg' ], current: 0 } }, methods: { onChange (index) { this.current = index } } } </script> <style lang="less" scoped> .prodetail { padding-top: 46px; ::v-deep .van-icon-arrow-left { color: #333; } img { display: block; width: 100%; } .custom-indicator { position: absolute; right: 10px; bottom: 10px; padding: 5px 10px; font-size: 12px; background: rgba(0, 0, 0, 0.1); border-radius: 15px; } .desc { width: 100%; overflow: scroll; ::v-deep img { display: block; width: 100%!important; } } .info { padding: 10px; } .title { display: flex; justify-content: space-between; .now { color: #fa2209; font-size: 20px; } .oldprice { color: #959595; font-size: 16px; text-decoration: line-through; margin-left: 5px; } .sellcount { color: #959595; font-size: 16px; position: relative; top: 4px; } } .msg { font-size: 16px; line-height: 24px; margin-top: 5px; } .service { display: flex; justify-content: space-between; line-height: 40px; margin-top: 10px; font-size: 16px; background-color: #fafafa; .left-words { span { margin-right: 10px; } .van-icon { margin-right: 4px; color: #fa2209; } } } .comment { padding: 10px; } .comment-title { display: flex; justify-content: space-between; .right { color: #959595; } } .comment-item { font-size: 16px; line-height: 30px; .top { height: 30px; display: flex; align-items: center; margin-top: 20px; img { width: 20px; height: 20px; } .name { margin: 0 10px; } } .time { color: #999; } } .footer { position: fixed; left: 0; bottom: 0; width: 100%; height: 55px; background-color: #fff; border-top: 1px solid #ccc; display: flex; justify-content: space-evenly; align-items: center; .icon-home, .icon-cart { display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 14px; .van-icon { font-size: 24px; } } .btn-add, .btn-buy { height: 36px; line-height: 36px; width: 120px; border-radius: 18px; background-color: #ffa900; text-align: center; color: #fff; font-size: 14px; } .btn-buy { background-color: #fe5630; } } } .tips { padding: 10px; } </style>
Lazyload
是 Vue
指令,使用前需要对指令进行注册。
1 2 import { Lazyload } from 'vant' Vue .use (Lazyload )
32. 商品详情 - 动态渲染介绍
动态路由参数,获取商品 id
1 2 3 4 5 computed : { goodsId () { return this .$route .params .id } },
封装 api 接口 api/product.js
1 2 3 4 5 6 7 8 export const getProDetail = (goodsId ) => { return request.get ('/goods/detail' , { params : { goodsId } }) }
一进入页面发送请求,获取商品详情数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 data () { return { images : [ 'https://img01.yzcdn.cn/vant/apple-1.jpg' , 'https://img01.yzcdn.cn/vant/apple-2.jpg' ], current : 0 , detail : {}, } }, async created () { this .getDetail () }, methods : { ... async getDetail () { const { data : { detail } } = await getProDetail (this .goodsId ) this .detail = detail this .images = detail.goods_images } }
动态渲染
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 <div class ="prodetail" v-if ="detail.goods_name" > <van-swipe :autoplay ="3000" @change ="onChange" > <van-swipe-item v-for ="(image, index) in images" :key ="index" > <img v-lazy ="image.external_url" /> </van-swipe-item > <template #indicator > <div class ="custom-indicator" > {{ current + 1 }} / {{ images.length }}</div > </template > </van-swipe > <!-- 商品说明 --> <div class ="info" > <div class ="title" > <div class ="price" > <span class ="now" > ¥{{ detail.goods_price_min }}</span > <span class ="oldprice" > ¥{{ detail.goods_price_max }}</span > </div > <div class ="sellcount" > 已售{{ detail.goods_sales }}件</div > </div > <div class ="msg text-ellipsis-2" > {{ detail.goods_name }} </div > <div class ="service" > <div class ="left-words" > <span > <van-icon name ="passed" /> 七天无理由退货</span > <span > <van-icon name ="passed" /> 48小时发货</span > </div > <div class ="right-icon" > <van-icon name ="arrow" /> </div > </div > </div > <!-- 商品描述 --> <div class ="tips" > 商品描述</div > <div class ="desc" v-html ="detail.content" > </div >
33. 商品详情 - 动态渲染评价
封装接口 api/product.js
1 2 3 4 5 6 7 8 9 export const getProComments = (goodsId, limit ) => { return request.get ('/comment/listRows' , { params : { goodsId, limit } }) }
页面调用获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import defaultImg from '@/assets/default-avatar.png' data () { return { ... total : 0 , commentList : [], defaultImg }, async created () { ... this .getComments () }, async getComments () { const { data : { list, total } } = await getProComments (this .goodsId , 3 ) this .commentList = list this .total = total },
动态渲染评价
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <!-- 商品评价 --> <div class ="comment" v-if ="total > 0" > <div class ="comment-title" > <div class ="left" > 商品评价 ({{ total }}条)</div > <div class ="right" > 查看更多 <van-icon name ="arrow" /> </div > </div > <div class ="comment-list" > <div class ="comment-item" v-for ="item in commentList" :key ="item.comment_id" > <div class ="top" > <img :src ="item.user.avatar_url || defaultImg" alt ="" > <div class ="name" > {{ item.user.nick_name }}</div > <van-rate :size ="16" :value ="item.score / 2" color ="#ffd21e" void-icon ="star" void-color ="#eee" /> </div > <div class ="content" > {{ item.content }} </div > <div class ="time" > {{ item.create_time }} </div > </div > </div > </div >
34. 加入购物车 - 唤起弹窗
按需导入 van-action-sheet
1 2 import { ActionSheet } from 'vant' Vue .use (ActionSheet )
准备 van-action-sheet 基本结构
1 2 3 4 5 6 7 8 9 10 11 <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'" > </van-action-sheet> data () { return { ... mode : 'cart' showPannel : false } },
注册点击事件,点击时唤起弹窗
1 2 3 4 5 6 7 8 9 10 11 <div class ="btn-add" @click="addFn" >加入购物车</div> <div class ="btn-buy" @click ="buyFn" > 立刻购买</div > addFn () { this .mode = 'cart' this .showPannel = true }, buyFn () { this .mode = 'buyNow' this .showPannel = true }
完善结构
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 <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'" > <div class ="product" > <div class ="product-title" > <div class ="left" > <img src ="http://cba.itlike.com/public/uploads/10001/20230321/8f505c6c437fc3d4b4310b57b1567544.jpg" alt ="" > </div > <div class ="right" > <div class ="price" > <span > ¥</span > <span class ="nowprice" > 9.99</span > </div > <div class ="count" > <span > 库存</span > <span > 55</span > </div > </div > </div > <div class ="num-box" > <span > 数量</span > 数字框占位 </div > <div class ="showbtn" v-if ="true" > <div class ="btn" v-if ="true" > 加入购物车</div > <div class ="btn now" v-else > 立刻购买</div > </div > <div class ="btn-none" v-else > 该商品已抢完</div > </div > </van-action-sheet >
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 .product { .product-title { display : flex; .left { img { width : 90px ; height : 90px ; } margin : 10px ; } .right { flex : 1 ; padding : 10px ; .price { font-size : 14px ; color : #fe560a ; .nowprice { font-size : 24px ; margin : 0 5px ; } } } } .num-box { display : flex; justify-content : space-between; padding : 10px ; align-items : center; } .btn , .btn-none { height : 40px ; line-height : 40px ; margin : 20px ; border-radius : 20px ; text-align : center; color : rgb (255 , 255 , 255 ); background-color : rgb (255 , 148 , 2 ); } .btn .now { background-color : #fe5630 ; } .btn-none { background-color : #cccccc ; } }
动态渲染
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 <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'" > <div class ="product" > <div class ="product-title" > <div class ="left" > <img :src ="detail.goods_image" alt ="" > </div > <div class ="right" > <div class ="price" > <span > ¥</span > <span class ="nowprice" > {{ detail.goods_price_min }}</span > </div > <div class ="count" > <span > 库存</span > <span > {{ detail.stock_total }}</span > </div > </div > </div > <div class ="num-box" > <span > 数量</span > 数字框组件 </div > <div class ="showbtn" v-if ="detail.stock_total > 0" > <div class ="btn" v-if ="mode === 'cart'" > 加入购物车</div > <div class ="btn now" v-if ="mode === 'buyNow'" > 立刻购买</div > </div > <div class ="btn-none" v-else > 该商品已抢完</div > </div > </van-action-sheet >
35. 加入购物车 - 封装数字框组件
封装组件 components/CountBox.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 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 <template> <div class ="count-box" > <button @click ="handleSub" class ="minus" > -</button > <input :value ="value" @change ="handleChange" class ="inp" type ="text" > <button @click ="handleAdd" class ="add" > +</button > </div > </template > <script > export default { props : { value : { type : Number , default : 1 } }, methods : { handleSub () { if (this .value <= 1 ) { return } this .$emit('input' , this .value - 1 ) }, handleAdd () { this .$emit('input' , this .value + 1 ) }, handleChange (e) { const num = +e.target .value if (isNaN (num) || num < 1 ) { e.target .value = this .value return } this .$emit('input' , num) } } } </script > <style lang ="less" scoped > .count-box { width : 110px ; display : flex; .add , .minus { width : 30px ; height : 30px ; outline : none; border : none; background-color : #efefef ; } .inp { width : 40px ; height : 30px ; outline : none; border : none; margin : 0 5px ; background-color : #efefef ; text-align : center; } } </style >
使用组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import CountBox from '@/components/CountBox.vue' export default { name : 'ProDetail' , components : { CountBox }, data () { return { addCount : 1 ... } }, } <div class ="num-box" > <span > 数量</span > <CountBox v-model ="addCount" > </CountBox > </div>
36. 加入购物车 - 判断 token 登录提示
说明:加入购物车,是一个登录后的用户才能进行的操作,所以需要进行鉴权判断,判断用户 token 是否存在
若存在:继续加入购物车操作
不存在:提示用户未登录,引导到登录页
按需注册 dialog 组件
1 2 import { Dialog } from 'vant' Vue .use (Dialog )
按钮注册点击事件
1 <div class ="btn" v-if ="mode === 'cart'" @click="addCart" >加入购物车</div>
添加 token 鉴权判断,跳转携带回跳地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 async addCart () { if (!this .$store .getters .token ) { this .$dialog .confirm ({ title : '温馨提示' , message : '此时需要先登录才能继续操作哦' , confirmButtonText : '去登录' , cancelButtonText : '再逛逛' }) .then (() => { this .$router .replace ({ path : '/login' , query : { backUrl : this .$route .fullPath } }) }) .catch (() => {}) return } console .log ('进行加入购物车操作' ) }
登录后,若有回跳地址,则回跳页面
1 2 3 const url = this .$route .query .backUrl || '/' this .$router .replace (url)
37. 加入购物车 - 封装接口进行请求
封装接口 api/cart.js
1 2 3 4 5 6 7 8 export const addCart = (goodsId, goodsNum, goodsSkuId ) => { return request.post ('/cart/add' , { goodsId, goodsNum, goodsSkuId }) }
页面中调用请求
1 2 3 4 5 6 7 8 9 10 11 12 13 data () { return { cartTotal : 0 } }, async addCart () { ... const { data } = await addCart (this .goodsId , this .addCount , this .detail .skuList [0 ].goods_sku_id ) this .cartTotal = data.cartTotal this .$toast('加入购物车成功' ) this .showPannel = false },
请求拦截器中,统一携带 token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 instance.interceptors .request .use (function (config ) { ... const token = store.getters .token if (token) { config.headers ['Access-Token' ] = token config.headers .platform = 'H5' } return config }, function (error ) { return Promise .reject (error) })
准备小图标
1 2 3 4 5 <div class ="icon-cart" > <span v-if ="cartTotal > 0" class ="num" > {{ cartTotal }}</span > <van-icon name ="shopping-cart-o" /> <span > 购物车</span > </div>
定制样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .footer .icon-cart { position : relative; padding : 0 6px ; .num { z-index : 999 ; position : absolute; top : -2px ; right : 0 ; min-width : 16px ; padding : 0 4px ; color : #fff ; text-align : center; background-color : #ee0a24 ; border-radius : 50% ; } }
38. 购物车 - 静态布局
基本结构
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 <template> <div class ="cart" > <van-nav-bar title ="购物车" fixed /> <div class ="cart-title" > <span class ="all" > 共<i > 4</i > 件商品</span > <span class ="edit" > <van-icon name ="edit" /> 编辑 </span > </div > <div class ="cart-list" > <div class ="cart-item" v-for ="item in 10" :key ="item" > <van-checkbox > </van-checkbox > <div class ="show" > <img src ="http://cba.itlike.com/public/uploads/10001/20230321/a072ef0eef1648a5c4eae81fad1b7583.jpg" alt ="" > </div > <div class ="info" > <span class ="tit text-ellipsis-2" > 新Pad 14英寸 12+128 远峰蓝 M6平板电脑 智能安卓娱乐十核游戏学习二合一 低蓝光护眼超清4K全面三星屏5GWIFI全网通 蓝魔快本平板</span > <span class ="bottom" > <div class ="price" > ¥ <span > 1247.04</span > </div > <div class ="count-box" > <button class ="minus" > -</button > <input class ="inp" :value ="4" type ="text" readonly > <button class ="add" > +</button > </div > </span > </div > </div > </div > <div class ="footer-fixed" > <div class ="all-check" > <van-checkbox icon-size ="18" > </van-checkbox > 全选 </div > <div class ="all-total" > <div class ="price" > <span > 合计:</span > <span > ¥ <i class ="totalPrice" > 99.99</i > </span > </div > <div v-if ="true" class ="goPay" > 结算(5)</div > <div v-else class ="delete" > 删除</div > </div > </div > </div > </template > <script > export default { name : 'CartPage' } </script > <style lang ="less" scoped > // 主题 padding .cart { padding-top : 46px ; padding-bottom : 100px ; background-color : #f5f5f5 ; min-height : 100vh ; .cart-title { height : 40px ; display : flex; justify-content : space-between; align-items : center; padding : 0 10px ; font-size : 14px ; .all { i { font-style : normal; margin : 0 2px ; color : #fa2209 ; font-size : 16px ; } } .edit { .van-icon { font-size : 18px ; } } } .cart-item { margin : 0 10px 10px 10px ; padding : 10px ; display : flex; justify-content : space-between; background-color : #ffffff ; border-radius : 5px ; .show img { width : 100px ; height : 100px ; } .info { width : 210px ; padding : 10px 5px ; font-size : 14px ; display : flex; flex-direction : column; justify-content : space-between; .bottom { display : flex; justify-content : space-between; .price { display : flex; align-items : flex-end; color : #fa2209 ; font-size : 12px ; span { font-size : 16px ; } } .count-box { display : flex; width : 110px ; .add , .minus { width : 30px ; height : 30px ; outline : none; border : none; } .inp { width : 40px ; height : 30px ; outline : none; border : none; background-color : #efefef ; text-align : center; margin : 0 5px ; } } } } } } .footer-fixed { position : fixed; left : 0 ; bottom : 50px ; height : 50px ; width : 100% ; border-bottom : 1px solid #ccc ; background-color : #fff ; display : flex; justify-content : space-between; align-items : center; padding : 0 10px ; .all-check { display : flex; align-items : center; .van-checkbox { margin-right : 5px ; } } .all-total { display : flex; line-height : 36px ; .price { font-size : 14px ; margin-right : 10px ; .totalPrice { color : #fa2209 ; font-size : 18px ; font-style : normal; } } .goPay , .delete { min-width : 100px ; height : 36px ; line-height : 36px ; text-align : center; background-color : #fa2f21 ; color : #fff ; border-radius : 18px ; &.disabled { background-color : #ff9779 ; } } } } </style >
按需导入组件
1 2 import { Checkbox } from 'vant' Vue .use (Checkbox )
39. 购物车 - 构建 vuex 模块 - 获取数据存储
新建 modules/cart.js
模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export default { namespaced : true , state () { return { cartList : [] } }, mutations : { }, actions : { }, getters : { } }
挂载到 store 上面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import Vue from 'vue' import Vuex from 'vuex' import user from './modules/user' import cart from './modules/cart' Vue .use (Vuex )export default new Vuex .Store ({ getters : { token : state => state.user .userInfo .token }, modules : { user, cart } })
封装 API 接口 api/cart.js
1 2 3 4 export const getCartList = ( ) => { return request.get ('/cart/list' ) }
封装 action 和 mutation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 mutations : { setCartList (state, newList) { state.cartList = newList }, }, actions : { async getCartAction (context) { const { data } = await getCartList () data.list .forEach (item => { item.isChecked = true }) context.commit ('setCartList' , data.list ) } },
页面中 dispatch 调用
1 2 3 4 5 6 7 8 9 10 computed : { isLogin () { return this .$store .getters .token } }, created () { if (this .isLogin ) { this .$store .dispatch ('cart/getCartAction' ) } },
40. 购物车 - mapState - 渲染购物车列表
将数据映射到页面
1 2 3 4 5 import { mapState } from 'vuex' computed : { ...mapState ('cart' , ['cartList' ]) }
动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!-- 购物车列表 --> <div class ="cart-list" > <div class ="cart-item" v-for ="item in cartList" :key ="item.goods_id" > <van-checkbox icon-size ="18" :value ="item.isChecked" > </van-checkbox > <div class ="show" @click ="$router.push(`/prodetail/${item.goods_id}`)" > <img :src ="item.goods.goods_image" alt ="" > </div > <div class ="info" > <span class ="tit text-ellipsis-2" > {{ item.goods.goods_name }}</span > <span class ="bottom" > <div class ="price" > ¥ <span > {{ item.goods.goods_price_min }}</span > </div > <CountBox :value ="item.goods_num" > </CountBox > </span > </div > </div > </div >
41. 购物车 - 封装 getters - 动态计算展示
封装 getters:商品总数 / 选中的商品列表 / 选中的商品总数 / 选中的商品总价
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 getters : { cartTotal (state) { return state.cartList .reduce ((sum, item, index ) => sum + item.goods_num , 0 ) }, selCartList (state) { return state.cartList .filter (item => item.isChecked ) }, selCount (state, getters) { return getters.selCartList .reduce ((sum, item, index ) => sum + item.goods_num , 0 ) }, selPrice (state, getters) { return getters.selCartList .reduce ((sum, item, index ) => { return sum + item.goods_num * item.goods .goods_price_min }, 0 ).toFixed (2 ) } }
页面中 mapGetters 映射使用
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 computed : { ...mapGetters ('cart' , ['cartTotal' , 'selCount' , 'selPrice' ]), }, <!-- 购物车开头 --> <div class ="cart-title" > <span class ="all" > 共<i > {{ cartTotal || 0 }}</i > 件商品</span > <span class ="edit" > <van-icon name ="edit" /> 编辑 </span > </div > <div class ="footer-fixed" > <div class ="all-check" > <van-checkbox icon-size ="18" > </van-checkbox > 全选 </div > <div class ="all-total" > <div class ="price" > <span > 合计:</span > <span > ¥ <i class ="totalPrice" > {{ selPrice }}</i > </span > </div > <div v-if ="true" :class ="{ disabled: selCount === 0 }" class ="goPay" > 结算({{ selCount }}) </div > <div v-else :class ="{ disabled: selCount === 0 }" class ="delete" > 删除({{ selCount }}) </div > </div > </div >
42. 购物车 - 全选反选功能
全选 getters
1 2 3 4 5 6 7 8 9 10 11 12 getters : { isAllChecked (state) { return state.cartList .every (item => item.isChecked ) } } ...mapGetters ('cart' , ['isAllChecked' ]), <div class ="all-check" > <van-checkbox :value ="isAllChecked" icon-size ="18" > </van-checkbox > 全选 </div >
点击小选,修改状态
1 2 3 4 5 6 7 8 9 10 11 12 <van-checkbox @click="toggleCheck(item.goods_id)" ...></van-checkbox> toggleCheck (goodsId) { this .$store .commit ('cart/toggleCheck' , goodsId) }, mutations : { toggleCheck (state, goodsId) { const goods = state.cartList .find (item => item.goods_id === goodsId) goods.isChecked = !goods.isChecked }, }
点击全选,重置状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div @click="toggleAllCheck" class ="all-check" > <van-checkbox :value ="isAllChecked" icon-size ="18" > </van-checkbox > 全选 </div> toggleAllCheck () { this .$store .commit ('cart/toggleAllCheck' , !this .isAllChecked ) }, mutations : { toggleAllCheck (state, flag) { state.cartList .forEach (item => { item.isChecked = flag }) }, }
43. 购物车 - 数字框修改数量
封装 api 接口
1 2 3 4 5 6 7 8 export const changeCount = (goodsId, goodsNum, goodsSkuId ) => { return request.post ('/cart/update' , { goodsId, goodsNum, goodsSkuId }) }
页面中注册点击事件,传递数据
1 2 3 4 5 6 7 8 9 <CountBox :value="item.goods_num" @input="value => changeCount(value, item.goods_id, item.goods_sku_id)" ></CountBox > changeCount (value, goodsId, skuId) { this .$store .dispatch ('cart/changeCountAction' , { value, goodsId, skuId }) },
提供 action 发送请求, commit mutation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mutations : { changeCount (state, { goodsId, value }) { const obj = state.cartList .find (item => item.goods_id === goodsId) obj.goods_num = value } }, actions : { async changeCountAction (context, obj) { const { goodsId, value, skuId } = obj context.commit ('changeCount' , { goodsId, value }) await changeCount (goodsId, value, skuId) }, }
44. 购物车 - 编辑切换状态
data 提供数据, 定义是否在编辑删除的状态
1 2 3 4 5 data () { return { isEdit : false } },
注册点击事件,修改状态
1 2 3 4 <span class ="edit" @click="isEdit = !isEdit" > <van-icon name ="edit" /> 编辑 </span>
底下按钮根据状态变化
1 2 3 4 <div v-if ="!isEdit" :class ="{ disabled: selCount === 0 }" class ="goPay" > 去结算({{ selCount }}) </div> <div v-else :class ="{ disabled: selCount === 0 }" class ="delete" > 删除</div >
监视编辑状态,动态控制复选框状态
1 2 3 4 5 6 7 8 9 watch : { isEdit (value) { if (value) { this .$store .commit ('cart/toggleAllCheck' , false ) } else { this .$store .commit ('cart/toggleAllCheck' , true ) } } }
45. 购物车 - 删除功能完成
查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )
1 2 3 4 5 6 export const delSelect = (cartIds ) => { return request.post ('/cart/clear' , { cartIds }) }
注册删除点击事件
1 2 3 4 5 6 7 8 9 <div v-else :class ="{ disabled: selCount === 0 }" @click="handleDel" class ="delete" > 删除({{ selCount }}) </div> async handleDel () { if (this .selCount === 0 ) return await this .$store .dispatch ('cart/delSelect' ) this .isEdit = false },
提供 actions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 actions : { actions : { async delSelect (context) { const selCartList = context.getters .selCartList const cartIds = selCartList.map (item => item.id ) await delSelect (cartIds) Toast ('删除成功' ) context.dispatch ('getCartAction' ) } }, },
46. 购物车 - 空购物车处理
外面包个大盒子,添加 v-if 判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div class ="cart-box" v-if ="isLogin && cartList.length > 0" > <!-- 购物车开头 --> <div class ="cart-title" > ... </div > <!-- 购物车列表 --> <div class ="cart-list" > ... </div > <div class ="footer-fixed" > ... </div > </div> <div class ="empty-cart" v-else > <img src ="@/assets/empty.png" alt ="" > <div class ="tips" > 您的购物车是空的, 快去逛逛吧 </div > <div class ="btn" @click ="$router.push('/')" > 去逛逛</div > </div >
相关样式
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 .empty-cart { padding : 80px 30px ; img { width : 140px ; height : 92px ; display : block; margin : 0 auto; } .tips { text-align : center; color : #666 ; margin : 30px ; } .btn { width : 110px ; height : 32px ; line-height : 32px ; text-align : center; background-color : #fa2c20 ; border-radius : 16px ; color : #fff ; display : block; margin : 0 auto; } }
47. 订单结算台
所谓的 “立即结算”,本质就是跳转到订单结算台,并且跳转的同时,需要携带上对应的订单参数。
而具体需要哪些参数,就需要基于 【订单结算台】 的需求来定。
(1) 静态布局
准备静态页面
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 <template> <div class ="pay" > <van-nav-bar fixed title ="订单结算台" left-arrow @click-left ="$router.go(-1)" /> <div class ="address" > <div class ="left-icon" > <van-icon name ="logistics" /> </div > <div class ="info" v-if ="true" > <div class ="info-content" > <span class ="name" > 小红</span > <span class ="mobile" > 13811112222</span > </div > <div class ="info-address" > 江苏省 无锡市 南长街 110号 504 </div > </div > <div class ="info" v-else > 请选择配送地址 </div > <div class ="right-icon" > <van-icon name ="arrow" /> </div > </div > <div class ="pay-list" > <div class ="list" > <div class ="goods-item" > <div class ="left" > <img src ="http://cba.itlike.com/public/uploads/10001/20230321/8f505c6c437fc3d4b4310b57b1567544.jpg" alt ="" /> </div > <div class ="right" > <p class ="tit text-ellipsis-2" > 三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23 </p > <p class ="info" > <span class ="count" > x3</span > <span class ="price" > ¥9.99</span > </p > </div > </div > </div > <div class ="flow-num-box" > <span > 共 12 件商品,合计:</span > <span class ="money" > ¥1219.00</span > </div > <div class ="pay-detail" > <div class ="pay-cell" > <span > 订单总金额:</span > <span class ="red" > ¥1219.00</span > </div > <div class ="pay-cell" > <span > 优惠券:</span > <span > 无优惠券可用</span > </div > <div class ="pay-cell" > <span > 配送费用:</span > <span v-if ="false" > 请先选择配送地址</span > <span v-else class ="red" > +¥0.00</span > </div > </div > <div class ="pay-way" > <span class ="tit" > 支付方式</span > <div class ="pay-cell" > <span > <van-icon name ="balance-o" /> 余额支付(可用 ¥ 999919.00 元)</span > <span class ="red" > <van-icon name ="passed" /> </span > </div > </div > <div class ="buytips" > <textarea placeholder ="选填:买家留言(50字内)" name ="" id ="" cols ="30" rows ="10" > </textarea > </div > </div > <div class ="footer-fixed" > <div class ="left" > 实付款:<span > ¥999919</span > </div > <div class ="tipsbtn" > 提交订单</div > </div > </div > </template> <script > export default { name : 'PayIndex' , data () { return { } }, methods : { } } </script > <style lang ="less" scoped > .pay { padding-top : 46px ; padding-bottom : 46px ; ::v-deep { .van-nav-bar__arrow { color : #333 ; } } } .address { display : flex; align-items : center; justify-content : flex-start; padding : 20px ; font-size : 14px ; color : #666 ; position : relative; background : url (@/assets/border-line.png ) bottom repeat-x; background-size : 60px auto; .left-icon { margin-right : 20px ; } .right-icon { position : absolute; right : 20px ; top : 50% ; transform : translateY (-7px ); } } .goods-item { height : 100px ; margin-bottom : 6px ; padding : 10px ; background-color : #fff ; display : flex; .left { width : 100px ; img { display : block; width : 80px ; margin : 10px auto; } } .right { flex : 1 ; font-size : 14px ; line-height : 1.3 ; padding : 10px ; padding-right : 0px ; display : flex; flex-direction : column; justify-content : space-evenly; color : #333 ; .info { margin-top : 5px ; display : flex; justify-content : space-between; .price { color : #fa2209 ; } } } } .flow-num-box { display : flex; justify-content : flex-end; padding : 10px 10px ; font-size : 14px ; border-bottom : 1px solid #efefef ; .money { color : #fa2209 ; } } .pay-cell { font-size : 14px ; padding : 10px 12px ; color : #333 ; display : flex; justify-content : space-between; .red { color : #fa2209 ; } } .pay-detail { border-bottom : 1px solid #efefef ; } .pay-way { font-size : 14px ; padding : 10px 12px ; border-bottom : 1px solid #efefef ; color : #333 ; .tit { line-height : 30px ; } .pay-cell { padding : 10px 0 ; } .van-icon { font-size : 20px ; margin-right : 5px ; } } .buytips { display : block; textarea { display : block; width : 100% ; border : none; font-size : 14px ; padding : 12px ; height : 100px ; } } .footer-fixed { position : fixed; background-color : #fff ; left : 0 ; bottom : 0 ; width : 100% ; height : 46px ; line-height : 46px ; border-top : 1px solid #efefef ; font-size : 14px ; display : flex; .left { flex : 1 ; padding-left : 12px ; color : #666 ; span { color :#fa2209 ; } } .tipsbtn { width : 121px ; background : linear-gradient (90deg ,#f9211c ,#ff6335 ); color : #fff ; text-align : center; line-height : 46px ; display : block; font-size : 14px ; } } </style >
(2) 获取收货地址列表
1 封装获取地址的接口
1 2 3 4 5 6 import request from '@/utils/request' export const getAddressList = ( ) => { return request.get ('/address/list' ) }
2 页面中 - 调用获取地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 data () { return { addressList : [] } }, computed : { selectAddress () { return this .addressList [0 ] } }, async created () { this .getAddressList () }, methods : { async getAddressList () { const { data : { list } } = await getAddressList () this .addressList = list } }
3 页面中 - 进行渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 computed : { longAddress () { const region = this .selectAddress .region return region.province + region.city + region.region + this .selectAddress .detail } }, <div class ="info" v-if ="selectAddress?.address_id" > <div class ="info-content" > <span class ="name" > {{ selectAddress.name }}</span > <span class ="mobile" > {{ selectAddress.phone }}</span > </div > <div class ="info-address" > {{ longAddress }} </div > </div >
(3) 订单结算 - 封装通用接口
**思路分析:**这里的订单结算,有两种情况:
购物车结算,需要两个参数
① mode=“cart”
② cartIds=“cartId, cartId”
立即购买结算,需要三个参数
① mode=“buyNow”
② goodsId=“商品id”
③ goodsSkuId=“商品skuId”
都需要跳转时将参数传递过来
封装通用 API 接口 api/order
1 2 3 4 5 6 7 8 9 10 11 12 13 import request from '@/utils/request' export const checkOrder = (mode, obj ) => { return request.get ('/checkout/order' , { params : { mode, delivery : 0 , couponId : 0 , isUsePoints : 0 , ...obj } }) }
(4) 订单结算 - 购物车结算
1 跳转时,传递查询参数
layout/cart.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 <div @click="goPay" >结算({{ selCount }})</div> goPay () { if (this .selCount > 0 ) { this .$router .push ({ path : '/pay' , query : { mode : 'cart' , cartIds : this .selCartList .map (item => item.id ).join (',' ) } }) } }
2 页面中接收参数, 调用接口,获取数据
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 data () { return { order : {}, personal : {} } }, computed : { mode () { return this .$route .query .mode }, cartIds () { return this .$route .query .cartIds } } async created () { this .getOrderList () }, async getOrderList () { if (this .mode === 'cart' ) { const { data : { order, personal } } = await checkOrder (this .mode , { cartIds : this .cartIds }) this .order = order this .personal = personal } }
3 基于数据进行渲染
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 <!-- 订单明细 --> <div class ="pay-list" v-if ="order.goodsList" > <div class ="list" > <div class ="goods-item" v-for ="item in order.goodsList" :key ="item.goods_id" > <div class ="left" > <img :src ="item.goods_image" alt ="" /> </div > <div class ="right" > <p class ="tit text-ellipsis-2" > {{ item.goods_name }} </p > <p class ="info" > <span class ="count" > x{{ item.total_num }}</span > <span class ="price" > ¥{{ item.total_pay_price }}</span > </p > </div > </div > </div > <div class ="flow-num-box" > <span > 共 {{ order.orderTotalNum }} 件商品,合计:</span > <span class ="money" > ¥{{ order.orderTotalPrice }}</span > </div > <div class ="pay-detail" > <div class ="pay-cell" > <span > 订单总金额:</span > <span class ="red" > ¥{{ order.orderTotalPrice }}</span > </div > <div class ="pay-cell" > <span > 优惠券:</span > <span > 无优惠券可用</span > </div > <div class ="pay-cell" > <span > 配送费用:</span > <span v-if ="!selectAddress" > 请先选择配送地址</span > <span v-else class ="red" > +¥0.00</span > </div > </div > <div class ="pay-way" > <span class ="tit" > 支付方式</span > <div class ="pay-cell" > <span > <van-icon name ="balance-o" /> 余额支付(可用 ¥ {{ personal.balance }} 元)</span > <span class ="red" > <van-icon name ="passed" /> </span > </div > </div > <div class ="buytips" > <textarea placeholder ="选填:买家留言(50字内)" name ="" id ="" cols ="30" rows ="10" > </textarea > </div > </div > <!-- 底部提交 --> <div class ="footer-fixed" > <div class ="left" > 实付款:<span > ¥{{ order.orderTotalPrice }}</span > </div > <div class ="tipsbtn" > 提交订单</div > </div >
(5) 订单结算 - 立即购买结算
1 点击跳转传参
prodetail/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 <div class ="btn" v-if ="mode === 'buyNow'" @click="goBuyNow" >立刻购买</div> goBuyNow () { this .$router .push ({ path : '/pay' , query : { mode : 'buyNow' , goodsId : this .goodsId , goodsSkuId : this .detail .skuList [0 ].goods_sku_id , goodsNum : this .addCount } }) }
2 计算属性处理参数
1 2 3 4 5 6 7 8 9 10 11 12 computed : { ... goodsId () { return this .$route .query .goodsId }, goodsSkuId () { return this .$route .query .goodsSkuId }, goodsNum () { return this .$route .query .goodsNum } }
3 基于请求时携带参数发请求渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 async getOrderList () { ... if (this .mode === 'buyNow' ) { const { data : { order, personal } } = await checkOrder (this .mode , { goodsId : this .goodsId , goodsSkuId : this .goodsSkuId , goodsNum : this .goodsNum }) this .order = order this .personal = personal } }
(6) mixins 复用 - 处理登录确认框的弹出
1 新建一个 mixin 文件 mixins/loginConfirm.js
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 export default { methods : { loginConfirm () { if (!this .$store .getters .token ) { this .$dialog .confirm ({ title : '温馨提示' , message : '此时需要先登录才能继续操作哦' , confirmButtonText : '去登陆' , cancelButtonText : '再逛逛' }) .then (() => { this .$router .replace ({ path : '/login' , query : { backUrl : this .$route .fullPath } }) }) .catch (() => {}) return true } return false } } }
2 页面中导入,混入方法
1 2 3 4 5 6 7 import loginConfirm from '@/mixins/loginConfirm' export default { name : 'ProDetail' , mixins : [loginConfirm], ... }
3 页面中调用 混入的方法
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 async addCart () { if (this .loginConfirm ()) { return } const { data } = await addCart (this .goodsId , this .addCount , this .detail .skuList [0 ].goods_sku_id ) this .cartTotal = data.cartTotal this .$toast('加入购物车成功' ) this .showPannel = false console .log (this .cartTotal ) }, goBuyNow () { if (this .loginConfirm ()) { return } this .$router .push ({ path : '/pay' , query : { mode : 'buyNow' , goodsId : this .goodsId , goodsSkuId : this .detail .skuList [0 ].goods_sku_id , goodsNum : this .addCount } }) }
48. 提交订单并支付
1 封装 API 通用方法(统一余额支付)
1 2 3 4 5 6 7 8 9 10 11 export const submitOrder = (mode, params ) => { return request.post ('/checkout/submit' , { mode, delivery : 10 , couponId : 0 , payType : 10 , isUsePoints : 0 , ...params }) }
2 买家留言绑定
1 2 3 4 5 6 7 8 9 data () { return { remark : '' } }, <div class ="buytips" > <textarea v-model ="remark" placeholder ="选填:买家留言(50字内)" name ="" id ="" cols ="30" rows ="10" > </textarea > </div >
3 注册点击事件,提交订单并支付
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div class ="tipsbtn" @click="submitOrder" >提交订单</div> async submitOrder () { if (this .mode === 'cart' ) { await submitOrder (this .mode , { remark : this .remark , cartIds : this .cartIds }) } if (this .mode === 'buyNow' ) { await submitOrder (this .mode , { remark : this .remark , goodsId : this .goodsId , goodsSkuId : this .goodsSkuId , goodsNum : this .goodsNum }) } this .$toast .success ('支付成功' ) this .$router .replace ('/myorder' ) }
49. 订单管理
(1) 静态布局
1 基础静态结构
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> <div class ="order" > <van-nav-bar title ="我的订单" left-arrow @click-left ="$router.go(-1)" /> <van-tabs v-model ="active" > <van-tab title ="全部" > </van-tab > <van-tab title ="待支付" > </van-tab > <van-tab title ="待发货" > </van-tab > <van-tab title ="待收货" > </van-tab > <van-tab title ="待评价" > </van-tab > </van-tabs > <OrderListItem > </OrderListItem > </div > </template> <script > import OrderListItem from '@/components/OrderListItem.vue' export default { name : 'OrderPage' , components : { OrderListItem }, data () { return { active : 0 } } } </script > <style lang ="less" scoped > .order { background-color : #fafafa ; } .van-tabs { position : sticky; top : 0 ; } </style >
2 components/OrderListItem
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 <template> <div class="order-list-item"> <div class="tit"> <div class="time">2023-07-01 12:02:13</div> <div class="status"> <span>待支付</span> </div> </div> <div class="list"> <div class="list-item"> <div class="goods-img"> <img src="http://cba.itlike.com/public/uploads/10001/20230321/c4b5c61e46489bb9b9c0630002fbd69e.jpg" alt=""> </div> <div class="goods-content text-ellipsis-2"> Apple iPhone 14 Pro Max 256G 银色 移动联通电信5G双卡双待手机 </div> <div class="goods-trade"> <p>¥ 1299.00</p> <p>x 3</p> </div> </div> <div class="list-item"> <div class="goods-img"> <img src="http://cba.itlike.com/public/uploads/10001/20230321/c4b5c61e46489bb9b9c0630002fbd69e.jpg" alt=""> </div> <div class="goods-content text-ellipsis-2"> Apple iPhone 14 Pro Max 256G 银色 移动联通电信5G双卡双待手机 </div> <div class="goods-trade"> <p>¥ 1299.00</p> <p>x 3</p> </div> </div> <div class="list-item"> <div class="goods-img"> <img src="http://cba.itlike.com/public/uploads/10001/20230321/c4b5c61e46489bb9b9c0630002fbd69e.jpg" alt=""> </div> <div class="goods-content text-ellipsis-2"> Apple iPhone 14 Pro Max 256G 银色 移动联通电信5G双卡双待手机 </div> <div class="goods-trade"> <p>¥ 1299.00</p> <p>x 3</p> </div> </div> </div> <div class="total"> 共12件商品,总金额 ¥29888.00 </div> <div class="actions"> <span v-if="false">立刻付款</span> <span v-if="true">申请取消</span> <span v-if="false">确认收货</span> <span v-if="false">评价</span> </div> </div> </template> <script> export default { } </script> <style lang="less" scoped> .order-list-item { margin: 10px auto; width: 94%; padding: 15px; background-color: #ffffff; box-shadow: 0 0.5px 2px 0 rgba(0,0,0,.05); border-radius: 8px; color: #333; font-size: 13px; .tit { height: 24px; line-height: 24px; display: flex; justify-content: space-between; margin-bottom: 20px; .status { color: #fa2209; } } .list-item { display: flex; .goods-img { width: 90px; height: 90px; margin: 0px 10px 10px 0; img { width: 100%; height: 100%; } } .goods-content { flex: 2; line-height: 18px; max-height: 36px; margin-top: 8px; } .goods-trade { flex: 1; line-height: 18px; text-align: right; color: #b39999; margin-top: 8px; } } .total { text-align: right; } .actions { text-align: right; span { display: inline-block; height: 28px; line-height: 28px; color: #383838; border: 0.5px solid #a8a8a8; font-size: 14px; padding: 0 15px; border-radius: 5px; margin: 10px 0; } } } </style>
3 导入注册
1 2 3 import { Tab , Tabs } from 'vant' Vue .use (Tab )Vue .use (Tabs )
(2) 点击 tab 切换渲染
1 封装获取订单列表的 API 接口
1 2 3 4 5 6 7 8 9 export const getMyOrderList = (dataType, page ) => { return request.get ('/order/list' , { params : { dataType, page } }) }
2 给 tab 绑定 name 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <van-tabs v-model="active" sticky> <van-tab name ="all" title ="全部" > </van-tab > <van-tab name ="payment" title ="待支付" > </van-tab > <van-tab name ="delivery" title ="待发货" > </van-tab > <van-tab name ="received" title ="待收货" > </van-tab > <van-tab name ="comment" title ="待评价" > </van-tab > </van-tabs> data () { return { active : this .$route .query .dataType || 'all' , page : 1 , list : [] } },
3 封装调用接口获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 methods : { async getOrderList () { const { data : { list } } = await getMyOrderList (this .active , this .page ) list.data .forEach ((item ) => { item.total_num = 0 item.goods .forEach (goods => { item.total_num += goods.total_num }) }) this .list = list.data } }, watch : { active : { immediate : true , handler () { this .getOrderList () } } }
4 动态渲染
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 <OrderListItem v-for ="item in list" :key="item.order_id" :item="item" ></OrderListItem > <template > <div class ="order-list-item" v-if ="item.order_id" > <div class ="tit" > <div class ="time" > {{ item.create_time }}</div > <div class ="status" > <span > {{ item.state_text }}</span > </div > </div > <div class ="list" > <div class ="list-item" v-for ="(goods, index) in item.goods" :key ="index" > <div class ="goods-img" > <img :src ="goods.goods_image" alt ="" > </div > <div class ="goods-content text-ellipsis-2" > {{ goods.goods_name }} </div > <div class ="goods-trade" > <p > ¥ {{ goods.total_pay_price }}</p > <p > x {{ goods.total_num }}</p > </div > </div > </div > <div class ="total" > 共 {{ item.total_num }} 件商品,总金额 ¥{{ item.total_price }} </div > <div class ="actions" > <div v-if ="item.order_status === 10" > <span v-if ="item.pay_status === 10" > 立刻付款</span > <span v-else-if ="item.delivery_status === 10" > 申请取消</span > <span v-else-if ="item.delivery_status === 20 || item.delivery_status === 30" > 确认收货</span > </div > <div v-if ="item.order_status === 30" > <span > 评价</span > </div > </div > </div > </template > <script > export default { props : { item : { type : Object , default : () => { return {} } } } } </script >
50. 个人中心 - 基本渲染
1 封装获取个人信息 - API接口
1 2 3 4 5 6 import request from '@/utils/request' export const getUserInfoDetail = ( ) => { return request.get ('/user/info' ) }
2 调用接口,获取数据进行渲染
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 <template> <div class ="user" > <div class ="head-page" v-if ="isLogin" > <div class ="head-img" > <img src ="@/assets/default-avatar.png" alt ="" /> </div > <div class ="info" > <div class ="mobile" > {{ detail.mobile }}</div > <div class ="vip" > <van-icon name ="diamond-o" /> 普通会员 </div > </div > </div > <div v-else class ="head-page" @click ="$router.push('/login')" > <div class ="head-img" > <img src ="@/assets/default-avatar.png" alt ="" /> </div > <div class ="info" > <div class ="mobile" > 未登录</div > <div class ="words" > 点击登录账号</div > </div > </div > <div class ="my-asset" > <div class ="asset-left" > <div class ="asset-left-item" > <span > {{ detail.pay_money || 0 }}</span > <span > 账户余额</span > </div > <div class ="asset-left-item" > <span > 0</span > <span > 积分</span > </div > <div class ="asset-left-item" > <span > 0</span > <span > 优惠券</span > </div > </div > <div class ="asset-right" > <div class ="asset-right-item" > <van-icon name ="balance-pay" /> <span > 我的钱包</span > </div > </div > </div > <div class ="order-navbar" > <div class ="order-navbar-item" @click ="$router.push('/myorder?dataType=all')" > <van-icon name ="balance-list-o" /> <span > 全部订单</span > </div > <div class ="order-navbar-item" @click ="$router.push('/myorder?dataType=payment')" > <van-icon name ="clock-o" /> <span > 待支付</span > </div > <div class ="order-navbar-item" @click ="$router.push('/myorder?dataType=delivery')" > <van-icon name ="logistics" /> <span > 待发货</span > </div > <div class ="order-navbar-item" @click ="$router.push('/myorder?dataType=received')" > <van-icon name ="send-gift-o" /> <span > 待收货</span > </div > </div > <div class ="service" > <div class ="title" > 我的服务</div > <div class ="content" > <div class ="content-item" > <van-icon name ="records" /> <span > 收货地址</span > </div > <div class ="content-item" > <van-icon name ="gift-o" /> <span > 领券中心</span > </div > <div class ="content-item" > <van-icon name ="gift-card-o" /> <span > 优惠券</span > </div > <div class ="content-item" > <van-icon name ="question-o" /> <span > 我的帮助</span > </div > <div class ="content-item" > <van-icon name ="balance-o" /> <span > 我的积分</span > </div > <div class ="content-item" > <van-icon name ="refund-o" /> <span > 退换/售后</span > </div > </div > </div > <div class ="logout-btn" > <button > 退出登录</button > </div > </div > </template> <script > import { getUserInfoDetail } from '@/api/user.js' export default { name : 'UserPage' , data () { return { detail : {} } }, created () { if (this .isLogin ) { this .getUserInfoDetail () } }, computed : { isLogin () { return this .$store .getters .token } }, methods : { async getUserInfoDetail () { const { data : { userInfo } } = await getUserInfoDetail () this .detail = userInfo console .log (this .detail ) } } } </script > <style lang ="less" scoped > .user { min-height : 100vh ; background-color : #f7f7f7 ; padding-bottom : 50px ; } .head-page { height : 130px ; background : url ("http://cba.itlike.com/public/mweb/static/background/user-header2.png" ); background-size : cover; display : flex; align-items : center; .head-img { width : 50px ; height : 50px ; border-radius : 50% ; overflow : hidden; margin : 0 10px ; img { width : 100% ; height : 100% ; object-fit : cover; } } } .info { .mobile { margin-bottom : 5px ; color : #c59a46 ; font-size : 18px ; font-weight : bold; } .vip { display : inline-block; background-color : #3c3c3c ; padding : 3px 5px ; border-radius : 5px ; color : #e0d3b6 ; font-size : 14px ; .van-icon { font-weight : bold; color : #ffb632 ; } } } .my-asset { display : flex; padding : 20px 0 ; font-size : 14px ; background-color : #fff ; .asset-left { display : flex; justify-content : space-evenly; flex : 3 ; .asset-left-item { display : flex; flex-direction : column; justify-content : center; align-items : center; span :first -child { margin-bottom : 5px ; color : #ff0000 ; font-size : 16px ; } } } .asset-right { flex : 1 ; .asset-right-item { display : flex; flex-direction : column; justify-content : center; align-items : center; .van-icon { font-size : 24px ; margin-bottom : 5px ; } } } } .order-navbar { display : flex; padding : 15px 0 ; margin : 10px ; font-size : 14px ; background-color : #fff ; border-radius : 5px ; .order-navbar-item { display : flex; flex-direction : column; justify-content : center; align-items : center; width : 25% ; .van-icon { font-size : 24px ; margin-bottom : 5px ; } } } .service { font-size : 14px ; background-color : #fff ; border-radius : 5px ; margin : 10px ; .title { height : 50px ; line-height : 50px ; padding : 0 15px ; font-size : 16px ; } .content { display : flex; justify-content : flex-start; flex-wrap : wrap; font-size : 14px ; background-color : #fff ; border-radius : 5px ; .content-item { display : flex; flex-direction : column; justify-content : center; align-items : center; width : 25% ; margin-bottom : 20px ; .van-icon { font-size : 24px ; margin-bottom : 5px ; color : #ff3800 ; } } } } .logout-btn { button { width : 60% ; margin : 10px auto; display : block; font-size : 13px ; color : #616161 ; border-radius : 9px ; border : 1px solid #dcdcdc ; padding : 7px 0 ; text-align : center; background-color : #fafafa ; } } </style >
51. 个人中心 - 退出功能
1 注册点击事件
1 <button @click="logout" >退出登录</button>
2 提供方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 methods : { logout () { this .$dialog .confirm ({ title : '温馨提示' , message : '你确认要退出么?' }) .then (() => { this .$store .dispatch ('user/logout' ) }) .catch (() => { }) } } actions : { logout (context) { context.commit ('setUserInfo' , {}) context.commit ('cart/setCartList' , [], { root : true }) } },
52. 项目打包优化
vue脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线
参与上线的是 => 打包后的源代码
打包:
将多个文件压缩合并成一个文件
语法降级
less sass ts 语法解析, 解析成css
…
打包后,可以生成,浏览器能够直接运行的网页 => 就是需要上线的源码!
(1) 打包命令
vue脚手架工具已经提供了打包命令,直接使用即可。
1 2 yarn build npm run build
在项目的根目录会自动创建一个文件夹dist
,dist中的文件就是打包后的文件,只需要放到服务器中即可。
(2) vue.config.js配置publicPath
1 2 3 4 5 6 module .exports = { ... publicPath : './' }
(3) 路由懒加载
路由懒加载 & 异步组件, 不会一上来就将所有的组件都加载,而是访问到对应的路由了,才加载解析这个路由对应的所有组件
官网链接:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#使用-webpack
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
1 2 3 4 5 6 7 8 9 10 原来 import ProDetail from '@/views/prodetail' import Pay from '@/views/pay' import MyOrder from '@/views/myorder' 改之后,要放下面,因为为定义的变量,要放在import 之后 const ProDetail = ( ) => import ('@/views/prodetail' )const Pay = ( ) => import ('@/views/pay' )const MyOrder = ( ) => import ('@/views/myorder' )
认识vue3
1. Vue2 选项式 API vs Vue3 组合式API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script> export default { data(){ return { count:0 } }, methods:{ addCount(){ this.count++ } } } </script>
1 2 3 4 5 <script setup> import { ref } from 'vue' const count = ref(0) const addCount = ()=> count.value++ </script>
特点:
代码量变少
分散式维护变成集中式维护
2. Vue3的优势
使用create-vue搭建Vue3项目
1. 认识create-vue
create-vue是Vue官方新的脚手架工具,底层切换到了 vite (下一代前端工具链),为开发提供极速响应
2. 使用create-vue创建项目
前置条件 - 已安装16.0或更高版本的Node.js
执行如下命令,这一指令将会安装并执行 create-vue
运行项目
熟悉项目和关键文件
组合式API - setup选项
1. setup选项的写法和执行时机
写法
1 2 3 4 5 6 7 8 9 10 <script> export default { setup(){ }, beforeCreate(){ } } </script>
执行时机
在beforeCreate钩子之前执行
2. setup中写代码的特点
在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script> export default { setup(){ const message = 'this is message' const logMessage = ()=>{ console.log(message) } // 必须return才可以 return { message, logMessage } } } </script>
3. 语法糖
script标签添加 setup标记,不需要再写导出语句,默认会添加导出语句
1 2 3 4 5 6 <script setup> const message = 'this is message' const logMessage = ()=>{ console.log(message) } </script>
组合式API - reactive和ref函数
1. reactive
接受对象类型数据的参数传入并返回一个响应式的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script setup> // 导入 import { reactive } from 'vue' // 执行函数 传入参数 变量接收 const state = reactive({ msg:'this is msg' }) const setSate = ()=>{ // 修改数据更新视图 state.msg = 'this is new msg' } </script> <template> {{ state.msg }} <button @click="setState">change msg</button> </template>
2. ref
接收简单类型或者对象类型的数据传入并返回一个响应式的对象
注意点:
1.脚本中访问数据,需要通过.value
2.在template中,.value不需要加(帮我们扒了一层)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script setup> // 导入 import { ref } from 'vue' // 执行函数 传入参数 变量接收 const count = ref(0) const setCount = ()=>{ // 修改数据更新视图必须加上.value count.value++ } </script> <template> <button @click="setCount">{{count}}</button> </template>
3. reactive 对比 ref
都是用来生成响应式数据
不同点
reactive不能处理简单类型的数据
ref参数类型支持更好,但是必须通过.value做访问修改
ref函数内部的实现依赖于reactive函数
在实际工作中的推荐
推荐使用ref函数,减少记忆负担,小兔鲜项目都使用ref
组合式API - computed
计算属性基本思想和Vue2保持一致,组合式API下的计算属性只是修改了API写法
1 2 3 4 5 6 7 8 9 10 11 12 13 <script setup> // 导入 import {ref, computed } from 'vue' // 原始数据 const count = ref(0) // 计算属性 const doubleCount = computed(()=>count.value * 2) // 原始数据 const list = ref([1,2,3,4,5,6,7,8]) // 计算属性list const filterList = computed(item=>item > 2) </script>
最佳实践
1.计算属性中不应该有“副作用”
比如异步请求/修改dom
2.避免直接修改计算属性的值
计算属性应该是只读的,特殊情况可以配置get set
组合式API - watch
侦听一个或者多个数据的变化,数据变化时执行回调函数,俩个额外参数 immediate控制立刻执行,deep开启深度侦听
1. 侦听单个数据
1 2 3 4 5 6 7 8 9 <script setup> // 1. 导入watch import { ref, watch } from 'vue' const count = ref(0) // 2. 调用watch 侦听变化 watch(count, (newValue, oldValue)=>{ console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`) }) </script>
2. 侦听多个数据
侦听多个数据,第一个参数可以改写成数组的写法
1 2 3 4 5 6 7 8 9 10 <script setup> // 1. 导入watch import { ref, watch } from 'vue' const count = ref(0) const name = ref('cp') // 2. 调用watch 侦听变化 watch([count, name], ([newCount, newName],[oldCount,oldName])=>{ console.log(`count或者name变化了,[newCount, newName],[oldCount,oldName]) }) </script>
在侦听器创建时立即出发回调,响应式数据变化之后继续执行回调
1 2 3 4 5 6 7 8 9 10 11 <script setup> // 1. 导入watch import { ref, watch } from 'vue' const count = ref(0) // 2. 调用watch 侦听变化 watch(count, (newValue, oldValue)=>{ console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`) },{ immediate: true }) </script>
4. deep
通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执行,需要开启deep
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 <script setup> // 1. 导入watch import { ref, watch } from 'vue' const state = ref({ count: 0 }) // 2. 监听对象state watch(state, ()=>{ console.log('数据变化了') }) const changeStateByCount = ()=>{ // 直接修改不会引发回调执行 state.value.count++ } </script> <script setup> // 1. 导入watch import { ref, watch } from 'vue' const state = ref({ count: 0 }) // 2. 监听对象state 并开启deep watch(state, ()=>{ console.log('数据变化了') },{deep:true}) const changeStateByCount = ()=>{ // 此时修改可以触发回调 state.value.count++ } </script>
5.对于对象中的某个属性,进行监视
1 2 3 watch(()=>userInfo.value.age,(new value ,oldValue)=>{ console.log(new Value ,oldValue)})
1.作为watch函数的第一个参数,ref对象需要添加。value吗?
不需要,第一个参数就是传ref对象
2.watch只能侦听单个数据吗?
单个或者多个
3.不开启deep,直接监视复杂类型,修改属性能触发回调吗?
不能,默认是浅层侦听
4.不开启deep,精确侦听对象的某个属性?
可以把第一个参数写成函数的写法,返回要监听的具体属性
组合式API - 生命周期函数
1. 选项式对比组合式
2. 生命周期函数基本使用
导入生命周期函数
执行生命周期函数,传入回调
1 2 3 4 5 6 <scirpt setup> import { onMounted } from 'vue' onMounted(()=>{ // 自定义逻辑 }) </script>
3. 执行多次
生命周期函数执行多次的时候,会按照顺序依次执行
1 2 3 4 5 6 7 8 9 10 <scirpt setup> import { onMounted } from 'vue' onMounted(()=>{ // 自定义逻辑 }) onMounted(()=>{ // 自定义逻辑 }) </script>
组合式API - 父子通信
1. 父传子
基本思想
父组件中给子组件绑定属性
子组件内部通过props选项接收数据
2. 子传父
基本思想
父组件中给子组件标签通过@绑定事件
子组件内部通过 emit 方法触发事件
父传子
1.父传子的过程中通过什么方式接收props?
defineProps({属性名:类型})
2.setup语法糖中如何使用父组件传过来的数据?
const props=defineProps({属性名:类型})
props.XXX
子传父
1.子传父的过程中通过什么方式得到emit方法?
defineEmits([‘事件名称’])
2.怎么触发事件
emit(‘自定义事件名’,参数)
组合式API - 模版引用
概念:通过 ref标识 获取真实的 dom对象或者组件实例对象
1. 基本使用
实现步骤:
调用ref函数生成一个ref对象
通过ref标识绑定ref对象到标签
2. defineExpose
默认情况下在语法糖下组件内部的属性和方法是不开放给父组件访问的,可以通过defineExpose编译宏指定哪些属性和方法容许访问
说明:指定testMessage属性可以被访问到
组合式API - provide和inject
1. 作用和场景
顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信
2. 跨层传递普通数据
实现步骤
顶层组件通过 provide
函数提供数据
底层组件通过 inject
函数提供数据
3. 跨层传递响应式数据
在调用provide函数时,第二个参数设置为ref对象
4. 跨层传递方法
顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件的数据
Vue3.3 新特性-defineOptions
1 2 3 4 5 6 7 8 9 10 11 12 13 背景说明: 有 <script setup> 之前,如果要定义 props, emits 可以轻而易举地添加一个与 setup 平级的属性。 但是用了 <script setup> 后,就没法这么干了 setup 属性已经没有了,自然无法添加与其平级的属性。 --- 为了解决这一问题,引入了 defineProps 与 defineEmits 这两个宏。但这只解决了 props 与 emits 这两个属性。 如果我们要定义组件的 name 或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的 <script> 标签。 这样就会存在两个 <script> 标签。让人无法接受。
所以在 Vue 3.3 中新引入了 defineOptions 宏。顾名思义,主要是用来定义 Options API 的选项。可以用 defineOptions 定义任意的选项, props, emits, expose, slots 除外(因为这些可以使用 defineXXX 来做到)
Vue3.3新特性-defineModel
在Vue3中,自定义组件上使用v-model, 相当于传递一个modelValue属性,同时触发 update:modelValue 事件
我们需要先定义 props,再定义 emits 。其中有许多重复的代码。如果需要修改此值,还需要手动调用 emit 函数。
于是乎 defineModel 诞生了。
生效需要配置 vite.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig ({ plugins : [ vue ({ script : { defineModel : true } }), ], resolve : { alias : { '@' : fileURLToPath (new URL ('./src' , import .meta .url )) } } })
Vue3 状态管理 - Pinia
1. 什么是Pinia
Pinia 是 Vue 的专属的最新状态管理库 ,是 Vuex 状态管理工具的替代品
2. 手动添加Pinia到Vue项目
后面在实际开发项目的时候,Pinia可以在项目创建时自动添加,现在我们初次学习,从零开始:
使用 Vite 创建一个空的 Vue3项目
按照官方文档安装 pinia 到项目中
1 2 3 4 5 6 7 8 import { createApp}from ‘vue' import {createPinia}from' pinia”import App from './App.vue' const pinia = createPinia() const app = createApp(App) app.use(pinia) app.mount("#app')
3. Pinia基础使用
定义store
组件使用store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //原来 export const useCounterstore = definestore('counter', { state : () => ({ count: }),getters: { double:(state )=> state .count * 2 , }, actions: { increment() { this.count++ }, }, }) //改进之后 export const usecounterstore definestore('counter',()=> const count ref(0 )function increment(){ count.value++ return count,increment })
4. getters实现
Pinia中的 getters 直接使用 computed函数 进行模拟, 组件中需要使用需要把 getters return出去
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 export const useCounterStore = defineStore('counter' ,()=>{ const count =ref(100 )/声明操作数据的方法action(普通函数) const addCount =()=>count .value++const subCount =()=>count .value--const double = computed(()=>count .value* 2 )const msg =ref('hello pinia' )return {count ,double ,addCount, subCount, msg, ) })
5. action异步实现
方式:异步action函数的写法和组件中获取异步数据的写法完全一致
需求:在Pinia中获取频道列表数据并把数据渲染App组件的模板中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export const useChannelstore= definestore ('channel ',()=>{ const channelList ref ([])声明操作数据的方法 const getList =async ()=>{const {data:{data }}=await axios.get ('http :channelList.value =data.channels } /声明getters相关 return {channelList, getList} })
6. storeToRefs工具函数
使用storeToRefs函数可以辅助保持数据(state + getter)的响应式解构
1 2 3 4 5 6 7 8 9 10 <script setup > import {storeToRefs} from 'pinia' const store useCounterstore()const {name,doublecount } =storeToRefs (store) const {increment} =store </script >
7. Pinia的调试
Vue官方的 dev-tools 调试工具 对 Pinia直接支持,可以直接进行调试
8. Pinia持久化插件
官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
安装插件 pinia-plugin-persistedstate
1 npm i pinia-plugin-persistedstate
使用 main.js
1 2 3 import persist from 'pinia-plugin-persistedstate' ... app.use (createPinia ().use (persist))
配置 store/counter.js
1 2 3 4 5 6 7 8 9 10 11 12 13 import { defineStore } from 'pinia' import { computed, ref } from 'vue' export const useCounterStore = defineStore ('counter' , () => { ... return { count, doubleCount, increment } }, { persist : true })
其他配置,看官网文档即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import defineStore from 'pinia' export const useStore defineStore ('store' ,{ state :()=>({ save :{me :'saved' ,notMe :'not-saved' ,saveMeToo :'saved' ,}})}), persist:{ key: 'hm-couter' paths:['save.me' ,'saveMeToo' ], }, }) 该store中,只有save,me和saveMeToo被持久化,而save.notMe不会被持久化。
1.Pinia是用来做什么的?
新一代的状态管理工具,替代vueX
2.Pinia中还需要mutation吗?
不需要,action既支持同步也支持异步
3.Pinia如何实现getter?
computed计算属性函数
4.Pinia产生的Store如何解构赋值数据保持响应式?
storeToRefs
5.Pinia如何快速实现持久化?
后台数据管理系统 - 项目架构设计
在线演示:https://fe-bigevent-web.itheima.net/login
接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
接口根路径: http://big-event-vue-api-t.itheima.net
本项目的技术栈 本项目技术栈基于 ES6 、vue3 、pinia 、vue-router 、vite 、axios 和 element-plus
项目页面介绍
pnpm 包管理器 - 创建项目
一些优势:比同类工具快 2倍 左右、节省磁盘空间… https://www.pnpm.cn/
安装方式:
创建项目:
ESLint & prettier 配置代码风格
环境同步:
安装了插件 ESlint,开启保存自动修复
禁用了插件 Prettier,并关闭保存自动格式化
1 2 3 4 5 "editor.codeActionsOnSave" : { "source.fixAll" : true }, "editor.formatOnSave" : false ,
配置文件 .eslintrc.cjs
prettier 风格配置 https://prettier.io
单引号
不使用分号
每行宽度至多80字符
不加对象|数组最后逗号
换行符号不限制(win mac 不一致)
vue组件名称多单词组成(忽略index.vue)
props解构(关闭)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 rules : { 'prettier/prettier' : [ 'warn' , { singleQuote : true , semi : false , printWidth : 80 , trailingComma : 'none' , endOfLine : 'auto' } ], 'vue/multi-word-component-names' : [ 'warn' , { ignores : ['index' ] } ], 'vue/no-setup-props-destructure' : ['off' ], 'no-undef' : 'error' }
基于 husky 的代码检查工作流
husky 是一个 git hooks 工具 ( git的钩子工具,可以在特定时机执行特定的命令 )
husky 配置
git初始化 git init
初始化 husky 工具配置 https://typicode.github.io/husky/
1 pnpm dlx husky-init && pnpm install
修改 .husky/pre-commit 文件
**问题:**默认进行的是全量检查,耗时问题,历史问题。
lint-staged 配置
安装
配置 package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "lint-staged" : { "*.{js,ts,vue}" : [ "eslint --fix" ] } } { "scripts" : { "lint-staged" : "lint-staged" } }
修改 .husky/pre-commit 文件
调整项目目录
默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。主要是两个工作:
删除初始化的默认文件
修改剩余代码内容
新增调整我们需要的目录结构
拷贝初始化资源文件,安装预处理器插件
删除文件
修改内容
src/router/index.js
1 2 3 4 5 6 7 8 import { createRouter, createWebHistory } from 'vue-router' const router = createRouter ({ history : createWebHistory (import .meta .env .BASE_URL ), routes : [] }) export default router
src/App.vue
1 2 3 4 5 6 7 8 9 <script setup></script> <template > <div > <router-view > </router-view > </div > </template > <style scoped > </style >
src/main.js
1 2 3 4 5 6 7 8 9 10 11 import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' const app = createApp (App )app.use (createPinia ()) app.use (router) app.mount ('#app' )
新增需要目录 api utils
将项目需要的全局样式 和 图片文件,复制到 assets 文件夹中, 并将全局样式在main.js中引入
1 import '@/assets/main.scss'
VueRouter4 路由代码解析
基础代码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { createRouter, createWebHistory } from 'vue-router' const router = createRouter ({ history : createWebHistory (import .meta .env .BASE_URL ), routes : [] }) export default router
import.meta.env.BASE_URL 是Vite 环境变量:https://cn.vitejs.dev/guide/env-and-mode.html
引入 element-ui 组件库
官方文档: https://element-plus.org/zh-CN/
自动按需:
安装插件
1 pnpm add -D unplugin-vue-components unplugin-auto-import
然后把下列代码插入到你的 Vite
或 Webpack
的配置文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ... import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig ({ plugins : [ ... AutoImport ({ resolvers : [ElementPlusResolver ()] }), Components ({ resolvers : [ElementPlusResolver ()] }) ] })
直接使用
1 2 3 4 5 6 7 8 9 10 <template> <div > <el-button type ="primary" > Primary</el-button > <el-button type ="success" > Success</el-button > <el-button type ="info" > Info</el-button > <el-button type ="warning" > Warning</el-button > <el-button type ="danger" > Danger</el-button > ... </div > </template>
**彩蛋:**默认 components 下的文件也会被自动注册~
Pinia - 构建用户仓库 和 持久化
官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
安装插件 pinia-plugin-persistedstate
1 pnpm add pinia-plugin-persistedstate -D
使用 main.js
1 2 3 import persist from 'pinia-plugin-persistedstate' ... app.use (createPinia ().use (persist))
配置 stores/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { defineStore } from 'pinia' import { ref } from 'vue' export const useUserStore = defineStore ( 'big-user' , () => { const token = ref ('' ) const setToken = (t ) => (token.value = t) return { token, setToken } }, { persist : true } )
Pinia - 配置仓库统一管理
pinia 独立维护
- 现在:初始化代码在 main.js 中,仓库代码在 stores 中,代码分散职能不单一
- 优化:由 stores 统一维护,在 stores/index.js 中完成 pinia 初始化,交付 main.js 使用
仓库 统一导出
- 现在:使用一个仓库 import { useUserStore } from ./stores/user.js
不同仓库路径不一致
- 优化:由 stores/index.js 统一导出,导入路径统一 ./stores
,而且仓库维护在 stores/modules 中
数据交互 - 请求工具设计
1. 创建 axios 实例
们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址等)
一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用
安装 axios
新建 utils/request.js
封装 axios 模块
利用 axios.create 创建一个自定义的 axios 来使用
http://www.axios-js.com/zh-cn/docs/#axios-create-config
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 import axios from 'axios' const baseURL = 'http://big-event-vue-api-t.itheima.net' const instance = axios.create ({ }) instance.interceptors .request .use ( (config ) => { return config }, (err ) => Promise .reject (err) ) instance.interceptors .response .use ( (res ) => { return res }, (err ) => { return Promise .reject (err) } ) export default instance
2. 完成 axios 基本配置
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 import { useUserStore } from '@/stores/user' import axios from 'axios' import router from '@/router' import { ElMessage } from 'element-plus' const baseURL = 'http://big-event-vue-api-t.itheima.net' const instance = axios.create ({ baseURL, timeout : 100000 }) instance.interceptors .request .use ( (config ) => { const userStore = useUserStore () if (userStore.token ) { config.headers .Authorization = userStore.token } return config }, (err ) => Promise .reject (err) ) instance.interceptors .response .use ( (res ) => { if (res.data .code === 0 ) { return res } ElMessage ({ message : res.data .message || '服务异常' , type : 'error' }) return Promise .reject (res.data ) }, (err ) => { ElMessage ({ message : err.response .data .message || '服务异常' , type : 'error' }) console .log (err) if (err.response ?.status === 401 ) { router.push ('/login' ) } return Promise .reject (err) } ) export default instanceexport { baseURL }
首页整体路由设计
实现目标:
完成整体路由规划【搞清楚要做几个页面,它们分别在哪个路由下面,怎么跳转的…】
通过观察, 点击左侧导航, 右侧区域在切换, 那右侧区域内容一直在变, 那这个地方就是一个路由的出口
我们需要搭建嵌套路由
目标:
约定路由规则
path
文件
功能
组件名
路由级别
/login
views/login/LoginPage.vue
登录&注册
LoginPage
一级路由
/
views/layout/LayoutContainer.vue
布局架子
LayoutContainer
一级路由
├─ /article/manage
views/article/ArticleManage.vue
文章管理
ArticleManage
二级路由
├─ /article/channel
views/article/ArticleChannel.vue
频道管理
ArticleChannel
二级路由
├─ /user/profile
views/user/UserProfile.vue
个人详情
UserProfile
二级路由
├─ /user/avatar
views/user/UserAvatar.vue
更换头像
UserAvatar
二级路由
├─ /user/password
views/user/UserPassword.vue
重置密码
UserPassword
二级路由
明确了路由规则,可以全部配完,也可以边写边配。
登录注册页面 [element-plus 表单 & 表单校验]
注册登录 静态结构 & 基本切换
安装 element-plus 图标库
1 pnpm i @element-plus/icons-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 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 <script setup> import { User , Lock } from '@element-plus/icons-vue' import { ref } from 'vue' const isRegister = ref (true )</script> <template > <el-row class ="login-page" > <el-col :span ="12" class ="bg" > </el-col > <el-col :span ="6" :offset ="3" class ="form" > <el-form ref ="form" size ="large" autocomplete ="off" v-if ="isRegister" > <el-form-item > <h1 > 注册</h1 > </el-form-item > <el-form-item > <el-input :prefix-icon ="User" placeholder ="请输入用户名" > </el-input > </el-form-item > <el-form-item > <el-input :prefix-icon ="Lock" type ="password" placeholder ="请输入密码" > </el-input > </el-form-item > <el-form-item > <el-input :prefix-icon ="Lock" type ="password" placeholder ="请输入再次密码" > </el-input > </el-form-item > <el-form-item > <el-button class ="button" type ="primary" auto-insert-space > 注册 </el-button > </el-form-item > <el-form-item class ="flex" > <el-link type ="info" :underline ="false" @click ="isRegister = false" > ← 返回 </el-link > </el-form-item > </el-form > <el-form ref ="form" size ="large" autocomplete ="off" v-else > <el-form-item > <h1 > 登录</h1 > </el-form-item > <el-form-item > <el-input :prefix-icon ="User" placeholder ="请输入用户名" > </el-input > </el-form-item > <el-form-item > <el-input name ="password" :prefix-icon ="Lock" type ="password" placeholder ="请输入密码" > </el-input > </el-form-item > <el-form-item class ="flex" > <div class ="flex" > <el-checkbox > 记住我</el-checkbox > <el-link type ="primary" :underline ="false" > 忘记密码?</el-link > </div > </el-form-item > <el-form-item > <el-button class ="button" type ="primary" auto-insert-space > 登录</el-button > </el-form-item > <el-form-item class ="flex" > <el-link type ="info" :underline ="false" @click ="isRegister = true" > 注册 → </el-link > </el-form-item > </el-form > </el-col > </el-row > </template > <style lang ="scss" scoped > .login-page { height : 100vh ; background-color : #fff ; .bg { background : url ('@/assets/logo2.png' ) no-repeat 60% center / 240px auto, url ('@/assets/login_bg.jpg' ) no-repeat center / cover; border-radius : 0 20px 20px 0 ; } .form { display : flex; flex-direction : column; justify-content : center; user-select: none; .title { margin : 0 auto; } .button { width : 100% ; } .flex { width : 100% ; display : flex; justify-content : space-between; } } } </style >
注册功能
实现注册校验
【需求】注册页面基本校验
用户名非空,长度校验5-10位
密码非空,长度校验6-15位
再次输入密码,非空,长度校验6-15位
【进阶】再次输入密码需要自定义校验规则,和密码框值一致(可选)
注意:
model 属性绑定 form 数据对象
1 2 3 4 5 6 7 const formModel = ref ({ username : '' , password : '' , repassword : '' }) <el-form :model="formModel" >
v-model 绑定 form 数据对象的子属性
1 2 3 4 5 6 7 <el-input v-model="formModel.username" :prefix-icon="User" placeholder="请输入用户名" ></el-input> ... (其他两个也要绑定)
rules 配置校验规则
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 <el-form :rules="rules" > const rules = { username : [ { required : true , message : '请输入用户名' , trigger : 'blur' }, { min : 5 , max : 10 , message : '用户名必须是5-10位的字符' , trigger : 'blur' } ], password : [ { required : true , message : '请输入密码' , trigger : 'blur' }, { pattern : /^\S{6,15}$/ , message : '密码必须是6-15位的非空字符' , trigger : 'blur' } ], repassword : [ { required : true , message : '请再次输入密码' , trigger : 'blur' }, { pattern : /^\S{6,15}$/ , message : '密码必须是6-15的非空字符' , trigger : 'blur' }, { validator : (rule, value, callback ) => { if (value !== formModel.value .password ) { callback (new Error ('两次输入密码不一致!' )) } else { callback () } }, trigger : 'blur' } ] }
prop 绑定校验规则
1 2 3 4 5 6 7 8 9 <el-form-item prop="username" > <el-input v-model ="formModel.username" :prefix-icon ="User" placeholder ="请输入用户名" > </el-input > </el-form-item> ... (其他两个也要绑定prop)
注册前的预校验
需求:点击注册按钮,注册之前,需要先校验
通过 ref 获取到 表单组件
1 2 3 const form = ref ()<el-form ref="form" >
注册之前进行校验
1 2 3 4 5 6 7 8 9 10 11 12 13 <el-button @click="register" class ="button" type="primary" auto-insert-space > 注册 </el-button> const register = async ( ) => { await form.value .validate () console .log ('开始注册请求' ) }
封装 api 实现注册功能
需求:封装注册api,进行注册,注册成功切换到登录
新建 api/user.js 封装
1 2 3 4 import request from '@/utils/request' export const userRegisterService = ({ username, password, repassword } ) => request.post ('/api/reg' , { username, password, repassword })
页面中调用
1 2 3 4 5 6 7 const register = async ( ) => { await form.value .validate () await userRegisterService (formModel.value ) ElMessage .success ('注册成功' ) isRegister.value = false }
eslintrc 中声明全局变量名, 解决 ElMessage 报错问题
1 2 3 4 5 6 7 8 module .exports = { ... globals : { ElMessage : 'readonly' , ElMessageBox : 'readonly' , ElLoading : 'readonly' } }
登录功能
实现登录校验
【需求说明】给输入框添加表单校验
用户名不能为空,用户名必须是5-10位的字符,失去焦点 和 修改内容时触发校验
密码不能为空,密码必须是6-15位的字符,失去焦点 和 修改内容时触发校验
操作步骤:
model 属性绑定 form 数据对象,直接绑定之前提供好的数据对象即可
1 <el-form :model="formModel" >
rules 配置校验规则,共用注册的规则即可
1 <el-form :rules="rules" >
v-model 绑定 form 数据对象的子属性
1 2 3 4 5 6 7 8 9 10 11 12 13 <el-input v-model="formModel.username" :prefix-icon="User" placeholder="请输入用户名" ></el-input> <el-input v-model ="formModel.password" name ="password" :prefix-icon ="Lock" type ="password" placeholder ="请输入密码" > </el-input >
prop 绑定校验规则
1 2 3 4 5 6 7 8 <el-form-item prop="username" > <el-input v-model ="formModel.username" :prefix-icon ="User" placeholder ="请输入用户名" > </el-input > </el-form-item> ...
切换的时候重置
1 2 3 4 5 6 7 watch (isRegister, () => { formModel.value = { username : '' , password : '' , repassword : '' } })
登录前的预校验 & 登录成功
【需求说明1】登录之前的预校验
登录请求之前,需要对用户的输入内容,进行校验
校验通过才发送请求
【需求说明2】登录功能
封装登录API,点击按钮发送登录请求
登录成功存储token,存入pinia 和 持久化本地storage
跳转到首页,给提示
【测试账号】
登录的测试账号: shuaipeng
登录测试密码: 123456
PS: 每天账号会重置,如果被重置了,可以去注册页,注册一个新号
实现步骤:
注册事件,进行登录前的预校验 (获取到组件调用方法)
1 2 3 4 5 6 <el-form ref="form" > const login = async ( ) => { await form.value .validate () console .log ('开始登录' ) }
封装接口 API
1 2 export const userLoginService = ({ username, password } ) => request.post ('api/login' , { username, password })
调用方法将 token 存入 pinia 并 自动持久化本地
1 2 3 4 5 6 7 8 9 const userStore = useUserStore ()const router = useRouter ()const login = async ( ) => { await form.value .validate () const res = await userLoginService (formModel.value ) userStore.setToken (res.data .token ) ElMessage .success ('登录成功' ) router.push ('/' ) }
首页 layout 架子 [element-plus 菜单]
基本架子拆解
架子组件列表:
el-container
el-aside 左侧
el-container 右侧
el-header 右侧头部
el-main 右侧主体
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 <script setup> import { Management , Promotion , UserFilled , User , Crop , EditPen , SwitchButton , CaretBottom } from '@element-plus/icons-vue' import avatar from '@/assets/default.png' </script> <template> <el-container class="layout-container"> <el-aside width="200px"> <div class="el-aside__logo"></div> <el-menu active-text-color="#ffd04b" background-color="#232323" :default-active="$route.path" text-color="#fff" router > <el-menu-item index="/article/channel"> <el-icon><Management /></el-icon> <span>文章分类</span> </el-menu-item> <el-menu-item index="/article/manage"> <el-icon><Promotion /></el-icon> <span>文章管理</span> </el-menu-item> <el-sub-menu index="/user"> <template #title> <el-icon><UserFilled /></el-icon> <span>个人中心</span> </template> <el-menu-item index="/user/profile"> <el-icon><User /></el-icon> <span>基本资料</span> </el-menu-item> <el-menu-item index="/user/avatar"> <el-icon><Crop /></el-icon> <span>更换头像</span> </el-menu-item> <el-menu-item index="/user/password"> <el-icon><EditPen /></el-icon> <span>重置密码</span> </el-menu-item> </el-sub-menu> </el-menu> </el-aside> <el-container> <el-header> <div>黑马程序员:<strong>小帅鹏</strong></div> <el-dropdown placement="bottom-end"> <span class="el-dropdown__box"> <el-avatar :src="avatar" /> <el-icon><CaretBottom /></el-icon> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="profile" :icon="User" >基本资料</el-dropdown-item > <el-dropdown-item command="avatar" :icon="Crop" >更换头像</el-dropdown-item > <el-dropdown-item command="password" :icon="EditPen" >重置密码</el-dropdown-item > <el-dropdown-item command="logout" :icon="SwitchButton" >退出登录</el-dropdown-item > </el-dropdown-menu> </template> </el-dropdown> </el-header> <el-main> <router-view></router-view> </el-main> <el-footer>大事件 ©2023 Created by 黑马程序员</el-footer> </el-container> </el-container> </template> <style lang="scss" scoped> .layout-container { height: 100vh; .el-aside { background-color: #232323; &__logo { height: 120px; background: url('@/assets/logo.png') no-repeat center / 120px auto; } .el-menu { border-right: none; } } .el-header { background-color: #fff; display: flex; align-items: center; justify-content: space-between; .el-dropdown__box { display: flex; align-items: center; .el-icon { color: #999; margin-left: 10px; } &:active, &:focus { outline: none; } } } .el-footer { display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666; } } </style>
登录访问拦截
需求:只有登录页,可以未授权的时候访问,其他所有页面,都需要先登录再访问
1 2 3 4 5 router.beforeEach ((to ) => { const userStore = useUserStore () if (!userStore.token && to.path !== '/login' ) return '/login' })
用户基本信息获取&渲染
api/user.js
封装接口
1 export const userGetInfoService = ( ) => request.get ('/my/userinfo' )
stores/modules/user.js 定义数据
1 2 3 4 5 const user = ref ({})const getUser = async ( ) => { const res = await userGetInfoService () user.value = res.data .data }
layout/LayoutContainer
页面中调用
1 2 3 4 5 import { useUserStore } from '@/stores' const userStore = useUserStore ()onMounted (() => { userStore.getUser () })
动态渲染
1 2 3 4 5 <div> 黑马程序员:<strong>{{ userStore.user .nickname || userStore.user .username }}</strong> </div> <el-avatar :src ="userStore.user.user_pic || avatar" />
退出功能 [element-plus 确认框]
注册点击事件
1 2 3 4 5 6 7 8 <el-dropdown placement="bottom-end" @command="onCommand" > <el-dropdown-menu > <el-dropdown-item command ="profile" :icon ="User" > 基本资料</el-dropdown-item > <el-dropdown-item command ="avatar" :icon ="Crop" > 更换头像</el-dropdown-item > <el-dropdown-item command ="password" :icon ="EditPen" > 重置密码</el-dropdown-item > <el-dropdown-item command ="logout" :icon ="SwitchButton" > 退出登录</el-dropdown-item > </el-dropdown-menu >
添加退出功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const onCommand = async (command ) => { if (command === 'logout' ) { await ElMessageBox .confirm ('你确认退出大事件吗?' , '温馨提示' , { type : 'warning' , confirmButtonText : '确认' , cancelButtonText : '取消' }) userStore.removeToken () userStore.setUser ({}) router.push (`/login` ) } else { router.push (`/user/${command} ` ) } }
pinia user.js 模块 提供 setUser 方法
1 const setUser = (obj ) => (user.value = obj)
文章分类页面 - [element-plus 表格]
基本架子 - PageContainer
基本结构样式,用到了 el-card 组件
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> <el-card class ="page-container" > <template #header > <div class ="header" > <span > 文章分类</span > <div class ="extra" > <el-button type ="primary" > 添加分类</el-button > </div > </div > </template > ... </el-card > </template> <style lang ="scss" scoped > .page-container { min-height : 100% ; box-sizing : border-box; .header { display : flex; align-items : center; justify-content : space-between; } } </style >
考虑到多个页面复用,封装成组件
props 定制标题
默认插槽 default 定制内容主体
具名插槽 extra 定制头部右侧额外的按钮
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 <script setup> defineProps ({ title : { required : true , type : String } }) </script> <template > <el-card class ="page-container" > <template #header > <div class ="header" > <span > {{ title }}</span > <div class ="extra" > <slot name ="extra" > </slot > </div > </div > </template > <slot > </slot > </el-card > </template > <style lang ="scss" scoped > .page-container { min-height : 100% ; box-sizing : border-box; .header { display : flex; align-items : center; justify-content : space-between; } } </style >
页面中直接使用测试 ( unplugin-vue-components 会自动注册)
1 2 3 4 5 6 7 8 9 <template> <page-container title ="文章分类" > <template #extra > <el-button type ="primary" > 添加分类 </el-button > </template > 主体部分 </page-container > </template>
1 2 3 4 5 6 7 8 9 <template> <page-container title ="文章管理" > <template #extra > <el-button type ="primary" > 发布文章</el-button > </template > 主体部分 </page-container > </template>
文章分类渲染
封装API - 请求获取表格数据
新建 api/article.js
封装获取频道列表的接口
1 2 import request from '@/utils/request' export const artGetChannelsService = ( ) => request.get ('/my/cate/list' )
页面中调用接口,获取数据存储
1 2 3 4 5 6 const channelList = ref ([])const getChannelList = async ( ) => { const res = await artGetChannelsService () channelList.value = res.data .data }
el-table 表格动态渲染
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 <el-table :data="channelList" style="width: 100%" > <el-table-column type ='index' label ="序号" width ="100" type ="index" > </el-table-column > <el-table-column label ="分类名称" prop ="cate_name" > </el-table-column > <el-table-column label ="分类别名" prop ="cate_alias" > </el-table-column > <el-table-column label ="操作" width ="100" > <template #default ="{ row }" > <el-button :icon ="Edit" circle plain type ="primary" @click ="onEditChannel(row)" > </el-button > <el-button :icon ="Delete" circle plain type ="danger" @click ="onDelChannel(row)" > </el-button > </template > </el-table-column > <template #empty > <el-empty description ="没有数据" /> </template > </el-table> const onEditChannel (row,$index)=>{console .log (row,$index)} const onDelChannel = (row ) => { console .log (row) }
el-table 表格 loading 效果
定义变量,v-loading绑定
1 2 3 const loading = ref (false )<el-table v-loading="loading" >
发送请求前开启,请求结束关闭
1 2 3 4 5 6 const getChannelList = async ( ) => { loading.value = true const res = await artGetChannelsService () channelList.value = res.data .data loading.value = false }
文章分类添加编辑 [element-plus 弹层]
点击显示弹层
准备弹层
1 2 3 4 5 6 7 8 9 10 11 const dialogVisible = ref (false )<el-dialog v-model="dialogVisible" title="添加弹层" width="30%" > <div > 我是内容部分</div > <template #footer > <span class ="dialog-footer" > <el-button @click ="dialogVisible = false" > 取消</el-button > <el-button type ="primary" > 确认 </el-button > </span > </template > </el-dialog>
点击事件
1 2 3 4 5 <template #extra><el-button type ="primary" @click ="onAddChannel" > 添加分类</el-button > </template> const onAddChannel = ( ) => { dialogVisible.value = true }
封装弹层组件 ChannelEdit
添加 和 编辑,可以共用一个弹层,所以可以将弹层封装成一个组件
组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑
open({ }) => 添加操作,添加表单初始化无数据
open({ id: xx, … }) => 编辑操作,编辑表单初始化需回显
具体实现:
封装组件 article/components/ChannelEdit.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 <script setup> import { ref } from 'vue' const dialogVisible = ref (false )const open = async (row ) => { dialogVisible.value = true console .log (row) } defineExpose ({ open }) </script> <template > <el-dialog v-model ="dialogVisible" title ="添加弹层" width ="30%" > <div > 我是内容部分</div > <template #footer > <span class ="dialog-footer" > <el-button @click ="dialogVisible = false" > 取消</el-button > <el-button type ="primary" > 确认 </el-button > </span > </template > </el-dialog > </template >
通过 ref 绑定
1 2 3 4 const dialog = ref ()<!-- 弹窗 --> <channel-edit ref ="dialog" > </channel-edit >
点击调用方法显示弹窗
1 2 3 4 5 6 const onAddChannel = ( ) => { dialog.value .open ({}) } const onEditChannel = (row ) => { dialog.value .open (row) }
准备弹层表单
准备数据 和 校验规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const formModel = ref ({ cate_name : '' , cate_alias : '' }) const rules = { cate_name : [ { required : true , message : '请输入分类名称' , trigger : 'blur' }, { pattern : /^\S{1,10}$/ , message : '分类名必须是1-10位的非空字符' , trigger : 'blur' } ], cate_alias : [ { required : true , message : '请输入分类别名' , trigger : 'blur' }, { pattern : /^[a-zA-Z0-9]{1,15}$/ , message : '分类别名必须是1-15位的字母数字' , trigger : 'blur' } ] }
准备表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <el-form :model="formModel" :rules="rules" label-width="100px" style="padding-right: 30px" > <el-form-item label ="分类名称" prop ="cate_name" > <el-input v-model ="formModel.cate_name" minlength ="1" maxlength ="10" > </el-input > </el-form-item > <el-form-item label ="分类别名" prop ="cate_alias" > <el-input v-model ="formModel.cate_alias" minlength ="1" maxlength ="15" > </el-input > </el-form-item > </el-form>
编辑需要回显,表单数据需要初始化
1 2 3 4 const open = async (row ) => { dialogVisible.value = true formModel.value = { ...row } }
基于传过来的表单数据,进行标题控制,有 id 的是编辑
1 :title="formModel.id ? '编辑分类' : '添加分类'"
确认提交
api/article.js
封装请求 API
1 2 3 4 5 export const artAddChannelService = (data ) => request.post ('/my/cate/add' , data)export const artEditChannelService = (data ) => request.put ('/my/cate/info' , data)
页面中校验,判断,提交请求
1 2 3 4 5 6 7 8 9 10 11 12 const formRef = ref ()const onSubmit = async ( ) => { await formRef.value .validate () formModel.value .id ? await artEditChannelService (formModel.value ) : await artAddChannelService (formModel.value ) ElMessage ({ type : 'success' , message : formModel.value .id ? '编辑成功' : '添加成功' }) dialogVisible.value = false }
通知父组件进行回显
1 2 3 4 5 6 const emit = defineEmits (['success' ])const onSubmit = async ( ) => { ... emit ('success' ) }
父组件监听 success 事件,进行调用回显
1 2 3 4 5 <channel-edit ref="dialog" @success="onSuccess" ></channel-edit> const onSuccess = ( ) => { getChannelList () }
文章分类删除
api/article.js
封装接口 api
1 2 3 4 5 export const artDelChannelService = (id ) => request.delete ('/my/cate/del' , { params : { id } })
页面中添加确认框,调用接口进行提示
1 2 3 4 5 6 7 8 9 10 const onDelChannel = async (row ) => { await ElMessageBox .confirm ('你确认删除该分类信息吗?' , '温馨提示' , { type : 'warning' , confirmButtonText : '确认' , cancelButtonText : '取消' }) await artDelChannelService (row.id ) ElMessage ({ type : 'success' , message : '删除成功' }) getChannelList () }
文章管理页面 - [element-plus 强化]
文章列表渲染
基本架子搭建
搜索表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <el-form inline> <el-form-item label ="文章分类:" > <el-select > <el-option label ="新闻" value ="111" > </el-option > <el-option label ="体育" value ="222" > </el-option > </el-select > </el-form-item > <el-form-item label ="发布状态:" > <el-select > <el-option label ="已发布" value ="已发布" > </el-option > <el-option label ="草稿" value ="草稿" > </el-option > </el-select > </el-form-item > <el-form-item > <el-button type ="primary" > 搜索</el-button > <el-button > 重置</el-button > </el-form-item > </el-form>
表格准备,模拟假数据渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Delete , Edit } from '@element-plus/icons-vue' import { ref } from 'vue' const articleList = ref ([ { id : 5961 , title : '新的文章啊' , pub_date : '2022-07-10 14:53:52.604' , state : '已发布' , cate_name : '体育' }, { id : 5962 , title : '新的文章啊' , pub_date : '2022-07-10 14:54:30.904' , state : null , cate_name : '体育' } ])
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 <el-table :data="articleList" style="width: 100%" > <el-table-column label ="文章标题" width ="400" > <template #default ="{ row }" > <el-link type ="primary" :underline ="false" > {{ row.title }}</el-link > </template > </el-table-column > <el-table-column label ="分类" prop ="cate_name" > </el-table-column > <el-table-column label ="发表时间" prop ="pub_date" > </el-table-column > <el-table-column label ="状态" prop ="state" > </el-table-column > <el-table-column label ="操作" width ="100" > <template #default ="{ row }" > <el-button :icon ="Edit" circle plain type ="primary" @click ="onEditArticle(row)" > </el-button > <el-button :icon ="Delete" circle plain type ="danger" @click ="onDeleteArticle(row)" > </el-button > </template > </el-table-column > <template #empty > <el-empty description ="没有数据" /> </template > </el-table> const onEditArticle = (row ) => { console .log (row) } const onDeleteArticle = (row ) => { console .log (row) }
中英国际化处理
默认是英文的,由于这里不涉及切换, 所以在 App.vue 中直接导入设置成中文即可,
1 2 3 4 5 6 7 8 9 10 <script setup> import zh from 'element-plus/es/locale/lang/zh-cn.mjs' </script> <template > <el-config-provider :locale ="zh" > <router-view /> </el-config-provider > </template >
文章分类选择
为了便于维护,直接拆分成一个小组件 ChannelSelect.vue
新建 article/components/ChannelSelect.vue
1 2 3 4 5 6 <template> <el-select > <el-option label ="新闻" value ="新闻" > </el-option > <el-option label ="体育" value ="体育" > </el-option > </el-select > </template>
页面中导入渲染
1 2 3 4 5 import ChannelSelect from './components/ChannelSelect.vue' <el-form-item label="文章分类:"> <channel-select></channel-select> </el-form-item>
调用接口,动态渲染下拉分类,设计成 v-model 的使用方式
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 <script setup> import { artGetChannelsService } from '@/api/article' import { ref } from 'vue' defineProps ({ modelValue : { type : [Number , String ] } }) const emit = defineEmits (['update:modelValue' ])const channelList = ref ([])const getChannelList = async ( ) => { const res = await artGetChannelsService () channelList.value = res.data .data } getChannelList ()</script> <template > <el-select :modelValue ="modelValue" @update:modelValue ="emit('update:modelValue', $event)" > <el-option v-for ="channel in channelList" :key ="channel.id" :label ="channel.cate_name" :value ="channel.id" > </el-option > </el-select > </template >
父组件定义参数绑定
1 2 3 4 5 6 7 8 const params = ref ({ pagenum : 1 , pagesize : 5 , cate_id : '' , state : '' }) <channel-select v-model="params.cate_id" ></channel-select>
发布状态,也绑定一下,便于将来提交表单
1 2 3 4 <el-select v-model="params.state" > <el-option label ="已发布" value ="已发布" > </el-option > <el-option label ="草稿" value ="草稿" > </el-option > </el-select>
封装 API 接口,请求渲染
没有数据,可以登录已完成的系统,添加几条数据
api/article.js
封装接口
1 2 export const artGetListService = (params ) => request.get ('/my/article/list' , { params })
页面中调用保存数据
1 2 3 4 5 6 7 8 9 const articleList = ref ([])const total = ref (0 )const getArticleList = async ( ) => { const res = await artGetListService (params.value ) articleList.value = res.data .data total.value = res.data .total } getArticleList ()
新建 utils/format.js
封装格式化日期函数
1 2 3 import { dayjs } from 'element-plus' export const formatTime = (time ) => dayjs (time).format ('YYYY年MM月DD日' )
导入使用
1 2 3 4 5 6 7 import { formatTime } from '@/utils/format' <el-table-column label="发表时间"> <template #default="{ row }"> {{ formatTime(row.pub_date) }} </template> </el-table-column>
分页渲染 [element-plus 分页]
分页组件
1 2 3 4 5 6 7 8 9 10 11 <el-pagination v-model :current-page="params.pagenum" v-model :page-size="params.pagesize" :page-sizes="[2, 3, 4, 5, 10]" layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange" @current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
提供分页修改逻辑
1 2 3 4 5 6 7 8 9 const onSizeChange = (size ) => { params.value .pagenum = 1 params.value .pagesize = size getArticleList () } const onCurrentChange = (page ) => { params.value .pagenum = page getArticleList () }
添加 loading 处理
准备数据
1 const loading = ref (false )
el-table上面绑定
1 <el-table v-loading="loading" > ... </el-table>
发送请求时添加 loading
1 2 3 4 5 6 7 8 const getArticleList = async ( ) => { loading.value = true ... loading.value = false } getArticleList ()
搜索 和 重置功能
注册事件
1 2 3 4 <el-form-item> <el-button @click ="onSearch" type ="primary" > 搜索</el-button > <el-button @click ="onReset" > 重置</el-button > </el-form-item>
绑定处理
1 2 3 4 5 6 7 8 9 10 11 const onSearch = ( ) => { params.value .pagenum = 1 getArticleList () } const onReset = ( ) => { params.value .pagenum = 1 params.value .cate_id = '' params.value .state = '' getArticleList () }
文章发布&修改 [element-plus - 抽屉]
点击显示抽屉
准备数据
1 2 import { ref } from 'vue' const visibleDrawer = ref (false )
准备抽屉容器
1 2 3 4 5 6 7 8 <el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%" > <span > Hi there!</span > </el-drawer>
点击修改布尔值显示抽屉
1 2 3 4 5 6 <el-button type="primary" @click="onAddArticle" >发布文章</el-button> const visibleDrawer = ref (false )const onAddArticle = ( ) => { visibleDrawer.value = true }
封装抽屉组件 ArticleEdit
添加 和 编辑,可以共用一个抽屉,所以可以将抽屉封装成一个组件
组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑
open({ }) => 添加操作,添加表单初始化无数据
open({ id: xx, … }) => 编辑操作,编辑表单初始化需回显
具体实现:
封装组件 article/components/ArticleEdit.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script setup> import { ref } from 'vue' const visibleDrawer = ref (false )const open = (row ) => { visibleDrawer.value = true console .log (row) } defineExpose ({ open }) </script> <template > <el-drawer v-model ="visibleDrawer" title ="大标题" direction ="rtl" size ="50%" > <span > Hi there!</span > </el-drawer > </template >
通过 ref 绑定
1 2 3 4 const articleEditRef = ref ()<!-- 弹窗 --> <article-edit ref ="articleEditRef" > </article-edit >
点击调用方法显示弹窗
1 2 3 4 5 6 7 const onAddArticle = ( ) => { articleEditRef.value .open ({}) } const onEditArticle = (row ) => { articleEditRef.value .open (row) }
完善抽屉表单结构
准备数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const formModel = ref ({ title : '' , cate_id : '' , cover_img : '' , content : '' , state : '' }) const open = async (row ) => { visibleDrawer.value = true if (row.id ) { console .log ('编辑回显' ) } else { console .log ('添加功能' ) } }
准备 form 表单结构
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 import ChannelSelect from './ChannelSelect.vue' <template> <el-drawer v-model ="visibleDrawer" :title ="formModel.id ? '编辑文章' : '添加文章'" direction ="rtl" size ="50%" > <el-form :model ="formModel" ref ="formRef" label-width ="100px" > <el-form-item label ="文章标题" prop ="title" > <el-input v-model ="formModel.title" placeholder ="请输入标题" > </el-input > </el-form-item > <el-form-item label ="文章分类" prop ="cate_id" > <channel-select v-model ="formModel.cate_id" width ="100%" > </channel-select > </el-form-item > <el-form-item label ="文章封面" prop ="cover_img" > 文件上传 </el-form-item > <el-form-item label ="文章内容" prop ="content" > <div class ="editor" > 富文本编辑器</div > </el-form-item > <el-form-item > <el-button type ="primary" > 发布</el-button > <el-button type ="info" > 草稿</el-button > </el-form-item > </el-form > </el-drawer > </template>
一打开默认重置添加的 form 表单数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const defaultForm = { title : '' , cate_id : '' , cover_img : '' , content : '' , state : '' } const formModel = ref ({ ...defaultForm })const open = async (row ) => { visibleDrawer.value = true if (row.id ) { console .log ('编辑回显' ) } else { console .log ('添加功能' ) formModel.value = { ...defaultForm } } }
扩展 下拉菜单 width props
1 2 3 4 5 6 7 8 9 10 11 12 13 defineProps ({ modelValue : { type : [Number , String ] }, width : { type : String } }) <el-select ... :style="{ width }" >
上传文件 [element-plus - 文件预览]
关闭自动上传,准备结构
1 2 3 4 5 6 7 8 9 10 11 import { Plus } from '@element-plus/icons-vue' <el-upload class ="avatar-uploader" :auto-upload="false" :show-file-list="false" :on-change="onUploadFile" > <img v-if ="imgUrl" :src ="imgUrl" class ="avatar" /> <el-icon v-else class ="avatar-uploader-icon" > <Plus /> </el-icon > </el-upload>
准备数据 和 选择图片的处理逻辑
1 2 3 4 5 const imgUrl = ref ('' )const onUploadFile = (uploadFile ) => { imgUrl.value = URL .createObjectURL (uploadFile.raw ) formModel.value .cover_img = uploadFile.raw }
样式美化
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 .avatar-uploader { :deep () { .avatar { width : 178px ; height : 178px ; display : block; } .el-upload { border : 1px dashed var (--el-border-color); border-radius : 6px ; cursor : pointer; position : relative; overflow : hidden; transition : var (--el-transition-duration-fast); } .el-upload :hover { border-color : var (--el-color-primary); } .el-icon .avatar-uploader-icon { font-size : 28px ; color : #8c939d ; width : 178px ; height : 178px ; text-align : center; } } }
富文本编辑器 [ vue-quill ]
官网地址:https://vueup.github.io/vue-quill/
安装包
1 pnpm add @vueup/vue-quill@latest
注册成局部组件
1 2 import { QuillEditor } from '@vueup/vue-quill' import '@vueup/vue-quill/dist/vue-quill.snow.css'
页面中使用绑定
1 2 3 4 5 6 7 8 <div class ="editor" > <quill-editor theme ="snow" v-model:content ="formModel.content" contentType ="html" > </quill-editor > </div>
样式美化
1 2 3 4 5 6 .editor { width : 100 %; :deep (.ql-editor ) { min-height : 200px; } }
添加文章功能
封装添加接口
1 2 export const artPublishService = (data ) => request.post ('/my/article/add' , data)
注册点击事件调用
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 <el-form-item> <el-button @click ="onPublish('已发布')" type ="primary" > 发布</el-button > <el-button @click ="onPublish('草稿')" type ="info" > 草稿</el-button > </el-form-item> const emit = defineEmits (['success' ])const onPublish = async (state ) => { formModel.value .state = state const fd = new FormData () for (let key in formModel.value ) { fd.append (key, formModel.value [key]) } if (formModel.value .id ) { console .log ('编辑操作' ) } else { await artPublishService (fd) ElMessage .success ('添加成功' ) visibleDrawer.value = false emit ('success' , 'add' ) } }
父组件监听事件,重新渲染
1 2 3 4 5 6 7 8 9 10 11 <article-edit ref="articleEditRef" @success="onSuccess" ></article-edit> const onSuccess = (type ) => { if (type === 'add' ) { const lastPage = Math .ceil ((total.value + 1 ) / params.value .pagesize ) params.value .pagenum = lastPage } getArticleList () }
添加完成后的内容重置
1 2 3 4 5 6 7 8 9 10 11 12 const formRef = ref ()const editorRef = ref ()const open = async (row ) => { visibleDrawer.value = true if (row.id ) { console .log ('编辑回显' ) } else { formModel.value = { ...defaultForm } imgUrl.value = '' editorRef.value .setHTML ('' ) } }
编辑文章回显
如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显
封装接口,根据 id 获取详情数据
1 2 export const artGetDetailService = (id ) => request.get ('my/article/info' , { params : { id } })
页面中调用渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const open = async (row ) => { visibleDrawer.value = true if (row.id ) { console .log ('编辑回显' ) const res = await artGetDetailService (row.id ) formModel.value = res.data .data imgUrl.value = baseURL + formModel.value .cover_img formModel.value .cover_img = await imageUrlToFile (imgUrl.value , formModel.value .cover_img ) } else { console .log ('添加功能' ) ... } }
chatGPT prompt:封装一个函数,基于 axios, 网络图片地址,转 file 对象, 请注意:写中文注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 async function imageUrlToFile (url, fileName ) { try { const response = await axios.get (url, { responseType : 'arraybuffer' }); const imageData = response.data ; const blob = new Blob ([imageData], { type : response.headers ['content-type' ] }); const file = new File ([blob], fileName, { type : blob.type }); return file; } catch (error) { console .error ('将图片转换为File对象时发生错误:' , error); throw error; } }
编辑文章功能
封装编辑接口
1 export const artEditService = (data ) => request.put ('my/article/info' , data)
提交时调用
1 2 3 4 5 6 7 8 9 10 11 12 const onPublish = async (state ) => { ... if (formModel.value .id ) { await artEditService (fd) ElMessage .success ('编辑成功' ) visibleDrawer.value = false emit ('success' , 'edit' ) } else { ... } }
文章删除
封装删除接口
1 export const artDelService = (id ) => request.delete ('my/article/info' , { params : { id } })
页面中添加确认框调用
1 2 3 4 5 6 7 8 9 10 const onDeleteArticle = async (row ) => { await ElMessageBox .confirm ('你确认删除该文章信息吗?' , '温馨提示' , { type : 'warning' , confirmButtonText : '确认' , cancelButtonText : '取消' }) await artDelService (row.id ) ElMessage ({ type : 'success' , message : '删除成功' }) getArticleList () }
ChatGPT & Copilot
AI 的认知 & 讲解内容说明
认知同步:
AI 早已不是新事物 (接受) => 语音识别,人脸识别,无人驾驶,智能机器人… (包括 ChatGPT 也是研发了多年的产物)
AI 本质是智能工具 (认识) => 人工智能辅助,可以提升效率,但不具备思想意识,无法从零到一取代人类工作
AI 一定会淘汰掉一部分人 => 逆水行舟,不进则退;学会拥抱变化,尽早上车
两个工具:
ChatGPT 3.5 的使用 (4.0 使用方式一致,回答准确度更高,但付费,且每3小时,有次数限制)
正常注册流程 (IP限制,手机号限制)
三方整合产品
工具 Github Copilot 智能生成代码
ChatGPT 的基本使用 - Prompt 优化
AI 互动的过程中,容易出现的问题:
AI未能理解问题的核心要点
AI的回答过于宽泛 或 过于具体
AI提供了错误的信息或观点
AI未能提供有价值的建议或解决方案
在识别了问题所在之后,我们可以尝试以下策略来优化我们的Prompt:
明确提问 :
确保问题表述清晰明确,关键字的准确度,决定了AI 对于需求的理解。
细化需求:
将问题拆分成多个小问题,可以帮助AI更具针对性地回答,也利于即时纠错。
添加背景信息:
提供有关问题背景的详细信息,也可以给 AI 预设一个角色,将有助于AI生成更具深度和价值的回答。
适当引导:
比如:“例如”、“请注意”、“请使用”等,来告诉模型你期望它做什么 或者 不做什么
限制范围:
通过限定回答的范围和长度,可以引导AI生成更精炼的回答
…
案例 - 前端简历
Prompt 优化前:
Prompt1:
Prompt 优化后:
Prompt1:
1 2 3 4 5 背景:你是一名【具有三年开发经验】的前端开发工程师,这三年期间,前两年,你做的【金融】相关四个项目,最后一年做的是【医疗】相关领域的两个项目,且有一定的管理 10 人+ 团队的经验。主要的技术栈:【Vue】 和 【小程序】。由于你是计算机软件工程专业,所以你具备一些Java后台、Mysql数据库的知识,也掌握一些基础的算法。 问题:你会如何编写你的简历个人技能介绍 要求:8 条技能介绍,请注意:你不会 angular。
Prompt2:
Prompt3:
1 2 你刚才说的方向完全没有问题,但是我想看到更多的项目技术亮点,项目业务解决方案。 请注意:每个项目3 个技术亮点,3 个业务解决方案。
工具 Github Copilot 智能生成代码的使用
安装步骤
登录 github,试用 Copilot
打开 vscode, 搜索并安装插件 Copilot
使用说明
删除键:不接受
Tab键:接收
Ctrl + enter: 查看更多方案
个人中心项目实战 - 基本资料
静态结构 + 校验处理
chatgpt prompt 提示词参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 请基于 elementPlus 和 Vue3 的语法,生成组件代码 要求: 一、表单结构要求 1. 组件中包含一个el-form表单,有四行内容,前三行是输入框,第四行是按钮2. 第一行 label 登录名称,输入框禁用不可输入状态3. 第二行 label 用户昵称,输入框可输入4. 第三行 label 用户邮箱,输入框可输入5. 第四行按钮,提交修改二、校验需求 给昵称 和 邮箱添加校验 1. 昵称 nickname 必须是2-10位的非空字符串2. 邮箱 email 符合邮箱格式即可,且不能为空
参考目标代码:
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 <script setup> import { useUserStore } from '@/stores' import { ref } from 'vue' const { user : { username, nickname, email, id } } = useUserStore () const userInfo = ref ({ username, nickname, email, id })const rules = { nickname : [ { required : true , message : '请输入用户昵称' , trigger : 'blur' }, { pattern : /^\S{2,10}$/ , message : '昵称必须是2-10位的非空字符串' , trigger : 'blur' } ], email : [ { required : true , message : '请输入用户邮箱' , trigger : 'blur' }, { type : 'email' , message : '邮箱格式不正确' , trigger : 'blur' } ] } </script> <template > <page-container title ="基本资料" > <el-row > <el-col :span ="12" > <el-form :model ="userInfo" :rules ="rules" ref ="formRef" label-width ="100px" size ="large" > <el-form-item label ="登录名称" > <el-input v-model ="userInfo.username" disabled > </el-input > </el-form-item > <el-form-item label ="用户昵称" prop ="nickname" > <el-input v-model ="userInfo.nickname" > </el-input > </el-form-item > <el-form-item label ="用户邮箱" prop ="email" > <el-input v-model ="userInfo.email" > </el-input > </el-form-item > <el-form-item > <el-button type ="primary" > 提交修改</el-button > </el-form-item > </el-form > </el-col > </el-row > </page-container > </template >
封装接口,更新个人信息
封装接口
1 2 export const userUpdateInfoService = ({ id, nickname, email } ) => request.put ('/my/userinfo' , { id, nickname, email })
页面中校验后,封装调用
1 2 3 4 5 6 7 8 9 const formRef = ref ()const onSubmit = async ( ) => { const valid = await formRef.value .validate () if (valid) { await userUpdateInfoService (userInfo.value ) await getUser () ElMessage .success ('修改成功' ) } }
个人中心项目实战 - 更换头像
静态结构
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 <script setup> import { ref } from 'vue' import { Plus , Upload } from '@element-plus/icons-vue' import { useUserStore } from '@/stores' const userStore = useUserStore ()const imgUrl = ref (userStore.user .user_pic )const onUploadFile = (file ) => { console .log (file) } </script> <template > <page-container title ="更换头像" > <el-row > <el-col :span ="12" > <el-upload ref ="uploadRef" class ="avatar-uploader" :auto-upload ="false" :show-file-list ="false" :on-change ="onUploadFile" > <img v-if ="imgUrl" :src ="imgUrl" class ="avatar" /> <img v-else src ="@/assets/avatar.jpg" width ="278" /> </el-upload > <br /> <el-button type ="primary" :icon ="Plus" size ="large" > 选择图片 </el-button > <el-button type ="success" :icon ="Upload" size ="large" > 上传头像 </el-button > </el-col > </el-row > </page-container > </template > <style lang ="scss" scoped > .avatar-uploader { :deep () { .avatar { width : 278px ; height : 278px ; display : block; } .el-upload { border : 1px dashed var (--el-border-color); border-radius : 6px ; cursor : pointer; position : relative; overflow : hidden; transition : var (--el-transition-duration-fast); } .el-upload :hover { border-color : var (--el-color-primary); } .el-icon .avatar-uploader-icon { font-size : 28px ; color : #8c939d ; width : 278px ; height : 278px ; text-align : center; } } } </style >
选择预览图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const uploadRef = ref ()const imgUrl = ref (userStore.user .user_pic )const onUploadFile = (file ) => { const reader = new FileReader () reader.readAsDataURL (file.raw ) reader.onload = () => { imgUrl.value = reader.result } } <el-upload ref="uploadRef" ></el-upload> <el-button @click ="uploadRef.$el.querySelector('input').click()" type ="primary" :icon ="Plus" size ="large" > 选择图片</el-button>
上传头像
封装接口
1 export const userUploadAvatarService = (avatar ) => request.patch ('/my/update/avatar' , { avatar })
调用接口
1 2 3 4 5 const onUpdateAvatar = async ( ) => { await userUploadAvatarService (imgUrl.value ) await userStore.getUser () ElMessage ({ type : 'success' , message : '更换头像成功' }) }
个人中心项目实战 - 重置密码
chatgpt prompt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 请基于 elementPlus 和 Vue3 的语法,生成组件代码 要求: 一、表单结构要求 1. 组件中包含一个el-form表单,有四行内容,前三行是表单输入框,第四行是两个按钮2. 第一行 label 原密码3. 第二行 label 新密码4. 第三行 label 确认密码5. 第四行两个按钮,修改密码 和 重置二、form绑定字段如下: const pwdForm = ref ({ old_pwd : '' , new_pwd : '' , re_pwd : '' }) 三、校验需求 所有字段,都是 6 -15 位 非空 自定义校验1 :原密码 和 新密码不能一样 自定义校验2 :新密码 和 确认密码必须一样
静态结构 + 校验处理
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 <script setup> import { ref } from 'vue' const pwdForm = ref ({ old_pwd : '' , new_pwd : '' , re_pwd : '' }) const checkOldSame = (rule, value, cb ) => { if (value === pwdForm.value .old_pwd ) { cb (new Error ('原密码和新密码不能一样!' )) } else { cb () } } const checkNewSame = (rule, value, cb ) => { if (value !== pwdForm.value .new_pwd ) { cb (new Error ('新密码和确认再次输入的新密码不一样!' )) } else { cb () } } const rules = { old_pwd : [ { required : true , message : '请输入密码' , trigger : 'blur' }, { pattern : /^\S{6,15}$/ , message : '密码长度必须是6-15位的非空字符串' , trigger : 'blur' } ], new_pwd : [ { required : true , message : '请输入新密码' , trigger : 'blur' }, { pattern : /^\S{6,15}$/ , message : '密码长度必须是6-15位的非空字符串' , trigger : 'blur' }, { validator : checkOldSame, trigger : 'blur' } ], re_pwd : [ { required : true , message : '请再次确认新密码' , trigger : 'blur' }, { pattern : /^\S{6,15}$/ , message : '密码长度必须是6-15位的非空字符串' , trigger : 'blur' }, { validator : checkNewSame, trigger : 'blur' } ] } </script> <template > <page-container title ="重置密码" > <el-row > <el-col :span ="12" > <el-form :model ="pwdForm" :rules ="rules" ref ="formRef" label-width ="100px" size ="large" > <el-form-item label ="原密码" prop ="old_pwd" > <el-input v-model ="pwdForm.old_pwd" type ="password" > </el-input > </el-form-item > <el-form-item label ="新密码" prop ="new_pwd" > <el-input v-model ="pwdForm.new_pwd" type ="password" > </el-input > </el-form-item > <el-form-item label ="确认新密码" prop ="re_pwd" > <el-input v-model ="pwdForm.re_pwd" type ="password" > </el-input > </el-form-item > <el-form-item > <el-button @click ="onSubmit" type ="primary" > 修改密码</el-button > <el-button @click ="onReset" > 重置</el-button > </el-form-item > </el-form > </el-col > </el-row > </page-container > </template >
封装接口,更新密码信息
封装接口
1 2 export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd } ) => request.patch ('/my/updatepwd' , { old_pwd, new_pwd, re_pwd })
页面中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const formRef = ref ()const router = useRouter ()const userStore = useUserStore ()const onSubmit = async ( ) => { const valid = await formRef.value .validate () if (valid) { await userUpdatePassService (pwdForm.value ) ElMessage ({ type : 'success' , message : '更换密码成功' }) userStore.setToken ('' ) userStore.setUser ({}) router.push ('/login' ) } } const onReset = ( ) => { formRef.value .resetFields () }