本文主要是介绍笔记整理——Vue2项目尚品汇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
一、vue-cli脚手架初始化项目各文件夹
二、 项目的其他配置
2.1 项目运行,让浏览器自动打开
2.2 eslint校验功能(各种规范的报错)的关闭
2.3 src文件夹简写方法,配置别名
2.4 样式的引入
三. 本次项目路由的分析
五. 路由组件的搭建
5.1 配置路由
5.2 路由组件与非路由组件的区别?
5.3 路由的跳转的两种形式
5.4 重定向
6.1 v-if和v-show的区别
七. 路由传参
7.1 路由跳转的两种形式
7.2 路由传参(params、query)
7.3 路由传参相关问题
八. 重写push和replace
8.1 为什么编程式导航进行路由跳转时,会有这种警告错误?
九. Home首页组件拆分业务分析
9.2. 完成其余静态组件
十. POSTMAN测试接口
十一. axios二次封装
11.1 为什么需要进行二次封装axios?
11.2 在项目中,经常会有API文件夹【里面存放的就是axios请求】
十二. API接口统一管理
12.1 跨域问题
十三. nprogress进度条的使用
13.1 打开页面时,页面上方显示的进度条。
十四. Vuex状态管理库
14.1 vuex是什么?
14.2 Vuex的四个大核心概念
14.3 this.$state.dispatch与this.$state.commit的主要区别
14.4 vuex实现模块式开发的大致概念
十六. 完成三级联动动态背景颜色
十七. 函数的防抖与节流【lodash插件】
17.1 卡顿现象引入函数的防抖与节流
17.2 函数防抖的理解
17.3 函数节流的理解
十八. 三级联动
18.1 三级联动中的节流
18.2 三级联动中的路由跳转分析
18.3 用编程式导航 + 事件的委派
十九. Search模块
19.1 Search模块商品模块分类与过渡动画
19.3 合并query和params参数
19.4 mockjs模拟数据
二十. swiper的基本使用
20.1 Banner实现轮播图的第一种方法
二十一. 轮播图通过watch+nectTick解决问题
21.2 用watch + this.$neckTick(较为完美的解决方案)
二十二. 获取Floor组件mock数据
22.1 组件通信的方式有哪些?
22.2 动态展示Floor组件
22.3 共用组件Carsouel(轮播图)
二十三. Search模块的静态组件
23.1 写一个模块的步骤(套路):
23.2 箭头函数
二十四. search模块中动态展示产品列表
24.1 Search模块中子组件SearchSelector.vue的动态开发
二十五. Object.assign的用法
二十六. 面包屑
26.1 面包屑处理分类的操作
26.2 面包屑处理关键字的操作
26.3 面包屑处理品牌信息
二十七.平台售卖属性的操作
27.1 子组件传给父组件(用自定义事件)
27.2 将平台售卖属性放置面包屑(数组去重)
二十八. 排序操作(上)
二十九. 操作排序——对升序降序的操作(下)
三十. 分页器静态组件(分页器原理)
三十一. 分页器起始与结束数字计算(分页器逻辑)
31.1 分页器起始与结束(分页器显示的逻辑)
31.2 分页器的动态展示(分页器按钮显示的逻辑)
31.3 分页器的完成
31.4 分页器添加类名
三十二. 产品详情页面——滚动行为
三十三. 产品详情页面数据获取
33.1 产品详情页面数据获取
33.2 产品详情页面动态展示数据
33.3 产品详情页面动态展示数据——右侧
三十四. Zoom放大镜展示数据—裁剪
三十五. detail路由组件展示商品售卖属性
三十六. 产品售卖属性值——排他操作—裁剪
三十七. 放大镜操作(上)
37.1 放大镜操作——ImageList组件和Zoom组件之间的小操作
37.2 放大镜操作——大图片随着小图片的点击也进行改变
37.3 放大镜操作——遮罩层
三十八. 购买产品个数的操作
三十九. “加入购物车”路由
39.1 ”加入购物车“按钮的步骤
39.2 路由传递参数结合会话存储(成功路由跳转与参数传递)
39.3 购物车静态组件与修改
39.4 UUID游客身份获取购物车数据
39.5 购物车动态展示数据
39.6 处理产品数量、修改购物车产品的数量完成
39.7 修改产品个数【对函数进行节流】
39.8 删除购物车产品的操作
39.9 修改产品状态
四十. 删除全部选中的商品
四十一. "全部"产品的勾选状态修改
四十二. 登录注册静态组件【重要】
42.1 注册的静态组件
42.2 登录的静态组件
42.3 携带token获取用户信息
42.4 上一节登录业务存在的问题讲解(不完美的解决办法)
42.5 退出登录
四十三. 导航守卫理解
43.1 导航守卫的判断与操作
四十四. trade交易静态组件
44.1. trade交易静态组件
44.2 用户地址信息的展示(排他思想)
44.3 交易页面完成
四十五. 提交订单(不用vuex)
45.1 提交订单静态组件(不用vuex)
45.2 提交订单(没有vuex时,可以用的方法)
四十六. 获取订单号与展示信息
四十七. 支付页面中使用Element UI以及按需引入
四十八. 微信支付业务(上)
48.1 二维码生成(插件QRCODE)
48.2 获取支付订单状态
四十九. 个人中心——二级路由搭建
五十. 我的订单
五十一. 未登录的导航守卫判断(前置守卫)
五十二. 用户登录(路由独享与组件内守卫)
52.1 路由独享守卫
52.2 组件内守卫(用得不多)
五十三. 图片懒加载(插件:vue-lazyload)
五十四. 表单验证(插件:vee-validate)【了解即可,看懂就行】
五十五. 路由的懒加载
五十六. 处理map文件、打包上线
五十七. 购买服务器等操作(先了解)
57.1 利用xshell工具登录服务器
57.2 nginx反向代理
VUE2项目尚品汇笔记
将Typora记录的笔记、以及手写的笔记进行二次整理。
整理完本次笔记,也会继续将之前学习HTML、CSS、JS、Vue的笔记进行二次整理。
方便以后查找,同时有需要改正或补充的,还请大家指教。
一、vue-cli脚手架初始化项目各文件夹
node_modules文件夹:项目依赖文件夹
public文件夹:一般放置一些静态资源(图片等)。注:放在public文件夹中的静态资源,当webpack进行打包时候,会原封不动打包到dist文件夹中。
src文件夹(程序员源代码文件夹):
assets文件夹:一般放置静态资源(一般放置多个组件共用的静态资源)。注:放置在assets文件夹里的静态资源,在webpack打包时,会把它当做一个模块,打包到JS文件里。
components文件夹:一般放置的是非路由组件(全局组件)。
App.vue:唯一的根组件,Vue当中的组件(.vue)。
main.js:程序入口文件,也是整个程序当中最先执行的文件。
babel.config.js:配置文件(babel相关)。
package.json文件:类似于项目的‘身份证’,记录项目叫什么,当中有哪些依赖,如何运行。
package.lock.json文件:缓存性文件。
README.md文件:说明性文件。
二、 项目的其他配置
2.1 项目运行,让浏览器自动打开
在package.json文件中配置
{
......"scripts": {"serve": "vue-cli-service serve --open","build": "vue-cli-service build","lint": "vue-cli-service lint"},
......
2.2 eslint校验功能(各种规范的报错)的关闭
eg:声明了变量,但未进行使用,则eslint校验工具会报错
- 在根目录下创建文件vue.config.js
module.exports = {//关闭eslintlintOnSave: false }
2.3 src文件夹简写方法,配置别名
①.jsconfig.json配置别名,可用@提示
【@代表的是src文件夹,常在js文件中使用。"exclude"表示“除了......”,意为配置的别名不能在"node_modules"和"dist"使用】
{"compilerOptions":{"baseUrl":"./","paths":{"@/*":["src/*"]}//表示不能在"node_modules"和"dist"中使用这种配置的别名。"exclude":["node_modules","dist"]},
②.也可在.css中配置别名@提示 【在@前面加上~这个符号】
<style>
......
background-image: url(~@/assets/images/icons.png);
......
</style>
2.4 样式的引入
在public文件夹下的index.html中引入同级reset.css样式
<link rel="stylesheet" href="/reset.css">
三. 本次项目路由的分析
vue-router 前端的路由:KV键值对。
key:URL(地址栏中的路径) value:相应的路由组件 注:本次项目是上中下结构
- 本次项目的路由组件可分为:
Home首页路由组件、Search搜索路由组件、login登录路由组件、Refister注册路由组件
- 本次项目的非路由组件可分为:
Header组件:【存在于首页、搜索页】 Footer组件:【存在于首页、搜索页,但登录和注册页面没有】
四. 完成非路由组件Header与Footer业务
本次项目主要关注业务、逻辑。
- 开发项目步骤:
- 书写静态页面(HTML+CSS)(本次项目已提前准备好了)
- 拆分组件(静态组件、路由组件等)
- 获取服务器的数据动态展示
- 完成相应的动态业务逻辑(eg:Js动态业务等)
- 注意:
- 创建组件=组件结构 + 组件的样式 + 图片资源。
- 本次采用less样式。
需通过less、less-loader【安装版本五npm install --save less less-loader@5
】进行处理less,将其变为css样式。
若想让组件识别less样式,需在style标签上加上lang=less。<style lang="less" scoped>
五. 路由组件的搭建
经过上面的分析,本次项目的路由组件有四个:
Home首页、Search搜索、Login登录、Register注册。
components文件夹:经常放置非路由组件(共用的全局组件).
pages或views文件夹:经常放置路由组件
5.1 配置路由
项目当中配置的路由一般放置在router文件夹中的index.js里。路由配置好之后在入口文件main.js中引入。
5.2 路由组件与非路由组件的区别?
- 共同点:
注册完路由,不管是路由组件、还是非路由组件,它们身上都会有$route、$router属性。
$route:一般获取路由信息【路径、query、params等】
$router:一般进行编程式导航,进行路由跳转【push或replace(其中一个是有缓存记忆的)】
- 不同点:
路由组件 | 非路由组件(普通组件) |
一般放置在pages或views文件夹中 | 一般放置在components文件夹中。 |
一般需要在router文件夹中进行注册 (使用的名字即为组件的名字,例如router文件中的index.js的写法) 在App.vue中展示时, 用的是<router-view></router-view> | 一般以标签的形式使用 (例如App.vue里的<Header></Header>标签)。 |
5.3 路由的跳转的两种形式
① 声明式导航:router-link
② 编程式导航:push或replace
编程式导航:声明式导航能做的,编程式导航都能做;而且编程式导航不仅可以进行路由跳转,还做一些其他业务逻辑。
5.4 重定向
redirect重定向:在项目跑起来时就访问该页面(该方法可以立马让其定向到首页)
在router的index.js里书写重定向代码:
//redirect重定向:在项目跑起来时就访问该页面(该方法可以立马让其定向到首页)//配置路由,路由的单词都是小写的
export default [
......
{path: '*',redirect: './home'}
......
]
六. Footer组件显示与隐藏
6.1 v-if和v-show的区别
显示or隐藏,可用v-if或者v-show。两者都是动态显示DOM元素。
v-if初始化较快,但代价较高;v-show初始化较慢,但切换成本低。
v-if | v-show |
---|---|
动态向Dom内添加或删除DOM元素 | 通过设置DOM元素的display样式属性控制显示或隐藏 |
切换有一个局部编译/卸载的过程,过程中可能会销毁和重建内部的事件监听和子组件。 | 单纯地基于css切换 |
惰性的; 初始值为假时,不作任何操作;只有在条件第一次为真时才开始局部编译。被缓存后,再次切换时进行局部卸载。 | 无论首次条件是否为真,都可被编译;被缓存且DOM元素保留。 |
有更高的切换消耗 | 有更高的初始化渲染消耗 |
适合运营条件不大可能改变的场景 | 适合频繁切换的场景 |
6.2 Footer组件编写
本次项目在Home首页、Search搜索显示Footer组件,但是在Login登录、Register注册时隐藏Footer组件。
可根据组件身上的$route获取当前路由的信息,通过路由路径判断Home和Footer这俩非路由组件的显示与隐藏。
<!-- 写法一(不推荐): --><Footer v-show="$route.path=='/home'||$route.path=='/search'"></Footer>
配置路由时,可给路由添加路由元信息【meta】,注意路由配置对象的key不能乱写。
在APP.vue文件中:
<!-- 写法二: --><Footer v-show="$route.meta.show"></Footer>
在router文件的index.js中配置【meta】:
//配置路由
export default new VueRouter({routes: [{path: "/home",component: Home,meta: { show: true }},{path: "/search",component: Search,meta: { show: true }},{path: "/login",component: Login,meta: { show: false }},{path: "/register",component: Register,meta: { show: false }},//重定向:在项目跑起来时就访问该页面(该方法可以立马让其定向到首页){path: '*',redirect: './home'}]
})
七. 路由传参
了解URL语法格式
https://blog.csdn.net/Thaley/article/details/122286201
格式:protocol :// hostname[:port] / path / [;parameters][?query]#fragment
[ ]中的内容可有可无
即:协议 :// 主机名[:端口号] / 路径 / [;参数][?查询]#信息片断
7.1 路由跳转的两种形式
eg:A -> B
①声明式导航:router-link(务必要有to属性),<router-link to="......"></router-link>可实现路由跳转。
②编程式导航:利用组件实例的$router.push或replace方法,可实现路由的跳转,也可书写一些自己的业务逻辑。
7.2 路由传参(params、query)
①params参数:属于路径的一部分,需注意,在配置路由的时候,需要占位。
如路径的path:"/search/:keyword",/:keyword即为params参数的占位符。
②query参数:不属于路径的一部分,类似于ajax中的queryString /home?k=v&kv=, 不需占位。
params参数 | query参数 |
属于路径的一部分,需要占位 | 不属于路径的一部分, 不需占位 |
用name来引入路由 | 用path来引入路由 |
取值用法:this.$route.params.name | 取值用法this.$route.query.name |
有些类似于post,在浏览器地址栏中不显示参数,所以params传值相对安全一些。 | 类似于我们ajax中get传参,在浏览器地址栏中显示参数。 |
传值一刷新就没。 | 传值刷新还存在。 |
7.3 路由传参相关问题
问1. 路由传参(对象的写法)path是否可以结合params参数一起使用?
不能。路由跳转传参时,对象写法可为name、path形式,但是path这种写法不能与params参数一起使用。而且,路径参数缺失则无法匹配path里的占位符。
//错误写法【错误写法!】
this.$router.push({path:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}
})//应改写成:
this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}
})
问2. 如何指定params参数可传可不传?
若路由要求传params参数,但未传,则URL会产生问题。
- 未传递params参数时,得到的地址情况:
......
methods:{this.$router.push({name:"search",query:{k:this.keyword.toUpperCase()//由其他程序可得,调用toUpperCase()返回ABC}})
},
......
// 上面的代码,得到的地址为:http://localhost:8080/#/?k=ABC 此地址缺少了/search。
- 传递了params参数时,得到的地址情况:
......
methods:{this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()//由其他程序可得,调用toUpperCase()返回ABC}})
},
......
// 则得到的地址为:http://localhost:8080/#/search/abc?k=ABC 此地址有/search
如何指定params参数可传或可不传?
在配置路由(router文件中的index.js时),改变path,在占位的后面加上一个问号?
即:path: "/search/:keyword?"
【这里的?表示params可传递或可不传递(和正则表达式雷同,?代表出现次数为0次或1次,即可有可无)】
//配置路由
export default new VueRouter({//配置路由routes: [{path: "/search/:keyword?",component: Search,meta: { show: true },name: "search"},]
})
问3. params参数可传递也可不传递,但若传递空字符串,如何解决?
params传递空字符串也会产生URL问题。可通过在params传递的空字符串后面加上||undefined
进行解决。
......
methods:{this.$router.push({name:"search",params:{keyword:''||undefined},query:{k:this.keyword.toUpperCase()}})
},
......
问4. 路由组件能否传递props数据?若有,有几种?
可以。有三种写法:
this.$router.push({path:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}
})
- 方式一:props用布尔值,但只能传递params
props: true
- 方式二:对象写法。额外给路由组件传递一些props
props: { a: 1, b: 2 }
- 方式三:函数写法。可通过props传递给路由组件params参数、query参数。
props: ($route) => {
return { keyword: $route.params.keyword, k: $route.query.k }
}
引用路由传参的三种方法 - 醉温柔 - 博客园
点击当前页的某个按钮跳转到另外一个页面去,并将某个值带过去
<div class="examine" @click="insurance(2)">查看详情</div>
- 第一种方法:页面刷新数据不会丢失。直接调用$router.push 实现携带参数的跳转。
methods:{insurance(id) {//直接调用$router.push 实现携带参数的跳转this.$router.push({path: `/particulars/${id}`,}) }
需要对应路由配置如下:
{path: '/particulars/:id',name: 'particulars',component: particulars}
可以看出需要在path中添加/:id来对应 $router.push 中path携带的参数。在子组件中可以使用来获取传递的参数值
另外页面获取参数如下this.$route.params.id
- 第二种方法:页面刷新数据会丢失。通过路由属性中的name来匹配路由,通过params来传递参数。
methods:{insurance(id) {this.$router.push({name: 'particulars',params: {id: id}})}
对应路由配置: 注意这里不能使用:/id来传递参数了,因为组件中,已经使用params来携带参数了。
{path: '/particulars',name: 'particulars',component: particulars}
子组件中: 这样来获取参数
this.$route.params.id
- 第三种方法:使用path来匹配路由,然后通过query来传递参数。
这种情况下 query传递的参数会显示在url后面?id=?
methods:{insurance(id) {this.$router.push({path: '/particulars',query: {id: id}})}
对应路由配置:
{path: '/particulars',name: 'particulars',component: particulars}
对应子组件: 这样来获取参数
this.$route.query.id
八. 重写push和replace
--路由跳转的两种形式:①.声明式导航、②.编程式导航
为什么编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误?
--声明式导航没有这类问题,因为vue-router底层已经处理好了。
8.1 为什么编程式导航进行路由跳转时,会有这种警告错误?
因为最新版本的vue-router引入了promise,promise需要传递成功和失败两个参数。
1.而push的返回是一个promise,所以通过给push方法传递相应的成功、失败的回调函数,可捕获到当前错误,可以解决。
- 方法一(治标不治本,在将来的组件当中push或replace,编程式导航还是会有类似警告错误):
this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}
},()=>{},()=>{})
// ()=>{},()=>{}表示执行成功和执行失败的回调函数
2.通过底部的代码,也可实现解决错误。
- 方法二(在router文件夹中的index.js中):
//若有成功和失败的回调,有则返回这俩值,无则自己手写() => {}
//先把VueRouter原型对象的push先保存一份
let originPush = VueRouter.prototype.push;
//先把VueRouter原型对象的replace先保存一份
let originReplace = VueRouter.prototype.replace;
//重写push|replace
//第一个参数location:告诉原来的push方法,你往哪里跳转(传递哪些参数)
//第二个参数resolve:成功回调。
//第三个参数reject:失败回调。
//call|apply区别
//相同点:都可调用函数一次,都可篡改函数的上下文一次。
//不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行,传递数组。
VueRouter.prototype.push = function(location, resolve, reject) {if (resolve && reject) {originPush.call(this, location, resolve, reject);} else {originPush.call(this, location, () => {}, () => {})}
}
VueRouter.prototype.replace = function(location, resolve, reject) {if (resolve && reject) {originReplace.call(this, location, resolve, reject);} else {originReplace.call(this, location, () => {}, () => {})}
}
九. Home首页组件拆分业务分析
本次项目的Home首页被拆分为了七个部分。
<template><div><!-- 三级联动全局组件 --><!-- 不需要再用import引入三级联动了,因为已经是全局组件了,可以直接使用 --><TypeNav /><!-- 轮播图列表 --><ListContainer /><!-- 今日推荐 --><TodayRecommend /><!-- 商品排行 --><Rank /><!-- 猜你喜欢 --><Like /><!-- 楼层 --><Floor /><!-- 商标 --><Brand /></div>
</template><script>
//引入其余的组件
import ListContainer from "@/pages/Home/ListContainer";
import TodayRecommend from "@/pages/Home/TodayRecommend";
import Rank from "@/pages/Home/Rank";
import Like from "@/pages/Home/Like";
import Floor from "@/pages/Home/Floor";
import Brand from "@/pages/Home/Brand";export default {name: "Home",components: {ListContainer,TodayRecommend,Rank,Like,Floor,Brand,},
};
</script>
完成步骤:
-- 先把静态(css、html)页面完成。
-- 拆分出静态组件。
-- 获取服务器的数据进行展示。
-- 若有js的动态业务,则将其完成即可。
9.1. TypeNav的三级联动全局组件
——由于三级联动,在Home、Search、Detai都有使用,故把三级联动注册为全局组件。优点:只需注册一次,即可在项目任意地方使用。
——本次讲解的组件是属于Home模块下的组件,所以暂时在pages的Home文件夹下新建个文件夹(这里取名为TypeNav)
9.2. 完成其余静态组件
需要耐心、需要注意:
HTML + CSS + 图片静态资源 —————信息的【结构、样式、图片资源】,
其文件的名字、文件引用路径需要一一对应
十. POSTMAN测试接口
利用postman工具测试接口是否正常
--通过postman工具测试,接口无问题。
--若服务器返回的数据code字段为200(此项目设置的),代表服务器返回数据成功。
--整个项目,接口前缀会有/api字样
十一. axios二次封装
npm官网中的axios文档:axios - npm
axios文档网:axios中文文档|axios中文网 | axios
封装的方法有:XMLHttpRequest、fetch、JQ、axios
本次项目在api文件夹下的request.js文件进行axios的二次封装。
11.1 为什么需要进行二次封装axios?
为了请求拦截器、响应拦截器。
**请求拦截器**:可以在发请求之前,可处理一些业务。
**响应拦截器**:当服务器数据返回后,可处理一些事情。
安装axios : npm install axios
11.2 在项目中,经常会有API文件夹【里面存放的就是axios请求】
使接口中,路径都带有/api:*baseURL:"/api"
//利用axios对axios进行二次封装
//故先引入axios
import axios from 'axios'
//1.利用axios对象的方法create,创建一个axios实例
//2.request就是axios,但需要稍微进行配置。
const requests = axios.create({//配置对象//baseURL基础路径,发请求时,路径当中会出现apibaseURL: "/api",//timeout代表请求超时的时间timeout: 5000,
});
//请求拦截器:可以在发请求之前,可处理一些业务。
requests.interceptors.request.use((config) => {//config:配置对象,对象中有一个属性很重要(即:headers请求头)return config;
})
//响应拦截器
requests.interceptors.request.use((res) => {//响应成功的回调函数:服务器响应数据回来之后,响应拦截器可检测到,可做一些事情return res.data;
}, (error) => {//响应失败的回调函数return Promise.reject(new Error('faile'));
});
//对外暴露
export default requests;
十二. API接口统一管理
当项目很小时:完全可以在组件的生命周期函数中发请求。
当项目很大时:axios.get('xxx')
接口一般写在API文件夹的index.js文件中。
//当前模块的功能:API接口统一管理
//用到了二次封装的requests,需要将其引入
import requests from "./request";
//三级联动接口
//WORD接口文档中已经说明三级联动的接口为
// /api/product/getBaseCategoryList methods为get 无参数
//发请求:axios发请求返回的结果是Promise对象
export const reqCategoryList = () => {//二次封装时,已经写了/api,所以这里不用再写/api了return requests({ url: '/product/getBaseCategoryList', method: 'get' })
}
//上面代码=>箭头函数的简写形式
/* export const reqCategoryList = () => requests({ url: '/product/getBaseCategoryList', method: 'get'
}) */
export const reqCategoryList = () => requests.get('/product/getBaseCategoryList')
写好接口,即可再书写仓库中的内容
12.1 跨域问题
(服务器与服务器之间是没有跨域问题的,只有浏览器与浏览器之间才有)
问1:什么是跨域?
协议、域名、端口号不同请求,称之为跨域。
`webpack.config.js`文件实质就是`vue.config.js`
`target`是要获取的那台服务器的IP地址
问2:跨域的解决方案是什么?
JSONP方法、CROS方法、代理(Proxy)方法【较为常用】
十三. nprogress进度条的使用
安装nprogress:npm install --save nprogress
13.1 打开页面时,页面上方显示的进度条。
start:进度条开始
done:进度条结束
在API文件夹里的request.js文件中写:
//利用axios对axios进行二次封装
//故先引入axios
import axios from 'axios'
//①.引入进度条
import nprogress from 'nprogress';
// console.log(nprogress),输出的内容其中start:表示进度条开始;done:表示进度条结束
//②.引入进度条样式
import "nprogress/nprogress.css";//1.利用axios对象的方法create,创建一个axios实例
//2.request就是axios,但需要稍微进行配置。
const requests = axios.create({baseURL: "/api",timeout: 5000,
});requests.interceptors.request.use((config) => {nprogress.start() //③.进度条开始动return config;
})//响应拦截器
requests.interceptors.response.use((res) => {nprogress.done() //③.进度条结束return res.data;
}, (error) => {return Promise.reject(new Error('faile'));
});//对外暴露
export default requests;
进度条的颜色可修改,在nprogress.css中修改即可。
十四. Vuex状态管理库
14.1 vuex是什么?
是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据。
但并不是全部项目都需要Vuex,若项目很小,完全不需要;若项目很大、组件很多、数据很多,数据维护很费劲,则需要使用Vuex。Vuex可以模块化开发。
安装Vuex:npm install --save vuex
- 配置仓库Vuex:在src文件夹下新建一个store的文件夹,再里面再建一个index.js文件。
- 在入口文件main.js中引入建好的仓库
- 在使用到该仓库的组件中,引入{mapState} from 'vuex'映射到组件身上,成为组件的数组
- 三连环:在actions里面提交mutations,让mutations进来修改state。
14.2 Vuex的四个大核心概念
- state:仓库存储数据的地方。
const state = {};
- mutation:修改state的唯一手段。
const mutations = {};
- action:处理action,可书写自己的的业务逻辑,也可处理异步。
const actions = {};
- getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便。
const getters = {};
//写完接口,接着就写小仓库
import { reqCategoryList } from '@/api';
const state = {};
const mutations = {};
const actions = {};
const getters = {};
//默认暴露
export default {state,mutations,actions,getters
}
14.3 this.$state.dispatch与this.$state.commit的主要区别
dispatch:含有异步操作,数据提交至actions,可用于向后台提交数据。
this.$store.dispatch('action', payload);
commit:同步操作,数据提交至 mutations ,可用于读取用户信息写到缓存里。
this.$store.commit('add',1);
14.4 vuex实现模块式开发的大致概念
项目很大、组件很多、数据很多,数据维护很费劲,则需要使用Vuex,Vuex可以模块化开发(把大仓库变成小仓库,按模块式进行存储)。
1.可以先给每一个组件模块来一个小仓库。
2.再将每个小仓库引入到store文件夹里的index.js(大仓库)中,用modules对外暴露,实现Vuex仓库模块式开发存储数据。
import Vue from 'vue';
import Vuex from 'vuex'
//需要使用插件一次
Vue.use(Vuex);
//引入小仓库
import home from '@/store/home';//对外暴露Store类的一个实例
export default new Vuex.Store({//实现Vuex仓库模块式开发存储数据modules: {home,}
})
十五. TypeNav三级联动展示数据业务
前提已经将axios二次封装好了,vuex也准备好了,vuex中的模块化也准备好了。
注:项目中所有的全局组件的文件最好都放在components文件夹中
多练练一眼识别几层分类的思想。
state 相当于 data(){return{}} 区域定义属性;
mutations 相当于 created() 或 mounted() 调用方法;
actions 相当于 methods 定义方法;
getters 相当于 computed 是为了简化数据而生的。
十六. 完成三级联动动态背景颜色
- 方法一:写鼠标事件的css样式
.item:hover{background:skyblue;}
- 方法二:写js
<template><div @mouseleave="leaveIndex"><h2 class="all">全部商品分类</h2>......<h3 @mouseenter="changeIndex(index)">......</div></template><script>......methods: {//鼠标进入修改响应式数据currentIndex属性changeIndex(index) {//index:鼠标移上某一个一级分类的元素的索引值this.currentIndex = index;},//一级分类鼠标移除的事件回调leaveIndex(){//鼠标移除currentIndex,变为-1this.currentIndex = -1;}},......</script>
- 通过js动态去控制二、三级分类的隐藏和显示业务
二、三级分类应有一个动态的样式,动态地显示和隐藏。
判断条件:只要谁有类名,谁就应该显示;currentIndex是否等于当前的索引值index。
:style="{display:currentIndex==index?'block':'none'}"
代码如下:
<template>......<!-- 事件委派|事件代理 --><div @mouseleave="leaveIndex"><h2 class="all">全部商品分类</h2><!-- 三级联动 --><div class="sort">......<!-- 二级、三级分类 --><div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">......
</template>
十七. 函数的防抖与节流【lodash插件】
17.1 卡顿现象引入函数的防抖与节流
● 卡顿:由于用户的行为过快,导致浏览器反应不过来。若当前回调函数中有一些大量业务,则可能出现卡顿现象。
○ 正常情况下(用户慢慢操作):鼠标一进入每一个一级分类的h3,触发鼠标进入事件,可正常执行。
○ 非正常情况下(用户操作过快):本身全部的一级分类都应该触发鼠标进入事件,但经测试,只有部分h3触发了。
● 防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,即若连续快速地触发,也只会执行最后一次。
● 节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个事件间隔才会触发回调,把频繁触发变为少量触发,使浏览器有充分时间解析代码 。
17.2 函数防抖的理解
● 防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,即若连续快速地触发,也只会执行最后一次。
安装lodash.js:npm install --save lodash
防抖事例代码_.debounce:
input.oninput = _.debounce(function() {console.log('ajax发请求')}, 1000)//放个延时器
防抖事例进行使用:
<!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>防抖</title><!-- 引入lodash:lodash全部的功能引入 --><script src="js/lodash.js"></script>
</head>
<body><p>请输入搜索内容:<input type="text"></p>
</body>
</html><script>//防抖:前面的所有触发都被取消,最后一次执行在规定时间之后才会触发,即若连续快速的触发,只会执行一次。let input = document.querySelector('input');//文本发生变化立即执行/* input.oninput = function() {console.log('ajax发请求')} */input.oninput = _.debounce(function() {console.log('ajax发请求')}, 1000)//lodash插件:里面封装函数的防抖与节流的业务【闭包+延迟器】//1.lodash函数库对外暴露的是_函数
</script>
17.3 函数节流的理解
● 节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个事件间隔才会触发回调,把频繁触发变为少量触发,使浏览器有充分时间解析代码 。
安装lodash.js:npm install --save lodash
节流事例用代码_.throttle:
button.onclick = _.throttle(function() {//节流:目前这个回调函数5s执行一次//加入这里有很多的业务代码,是可以给浏览器充裕的时间进行解析的。count++;span.innerHTML = count;console.log('执行');}, 5000);
节流事例进行使用:
<!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>节流</title><!-- 引入lodash:lodash全部的功能引入 --><script src="js/lodash.js"></script>
</head>
<body><div><h1>我是计数器<span>0</span></h1><button>点我加一</button></div>
</body>
</html><script>//节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个事件间隔才会触发回调,把频繁触发变为少量触发。//获取节点let span = document.querySelector('span');let button = document.querySelector('button');let count = 0;//计数器:在一秒以内,数字只能加上1button.onclick = _.throttle(function() {//节流:目前这个回调函数5s执行一次//加入这里有很多的业务代码,是可以给浏览器充裕的时间进行解析的。count++;span.innerHTML = count;console.log('执行');}, 5000);
</script>
十八. 三级联动
18.1 三级联动中的节流
throttle回调函数别用箭头函数,否则可能出现上下文this的问题。
......
methods: {
..........changeIndex:throttle(function(index) {//index:鼠标移上某一个一级分类的元素的索引值 this.currentIndex = index;},50),.......
}
......
18.2 三级联动中的路由跳转分析
三级联动用户的分类:一级分类、二级分类、三级分类
Home模块跳转到Search模块,一级会把用户选中的产品(产品名字、产品ID)在路由跳转的时候进行传递。
路由跳转的两种方式:
①.声明式导航:router-link
在本次项目中,router-link容易出现卡顿问题。
②.编程式导航:push|replace
本次较好的解决方案:编程式导航 + 事件委派
18.3 用编程式导航 + 事件的委派
解决方案:用编程式导航 + 事件委派 完成三级联动的路由跳转与传递参数的业务
利用事件委派存在一些问题以及解决方法:
①.点击的不一定都是a标签
利用自定义属性data-categoryname解决,区分是否为a标签,带有data-categoryname这样的节点【一定是a标签】。
②.如何获取子节点参数【产品名、产品id】
节点有一个属性dataset属性,可获取节点的自定义属性与属性值。
通过categoryname区分是否为a标签,通过category1id、category2id、category3id区分是否为一级、二级、三级标签。再通过函数中传入的event参数,获取当前点击事件,通过event.target属性获取当前节点,通过dataset属性获取节点属性信息。
<template>
......<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">......</div>
......
</template><script>
......
methods{
......//进行路由跳转的方法goSearch(event){//最好的解决方案:编程式导航 + 事件委派//利用事件委派存在一些问题:①.点击的不一定都是a标签 ②.如何获取参数【产品名、产品id】console.log(event.target)}
......
}
</script>
通过categoryname区分是否为a标签,通过category1id、category2id、category3id区分是否为一级、二级、三级标签。再通过函数中传入的event参数,获取当前点击事件,通过event.target属性获取当前节点,通过dataset属性获取节点属性信息。
<template>........<h3 @mouseenter="changeIndex(index)"><a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{ c1.categoryName }}--{{ index }}</a></h3><!-- 二级、三级分类 --><div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}"><!-- 因为:Key里已经有categoryId了,所以不用写index --><divclass="subitem"v-for="(c2, index) in c1.categoryChild":key="c2.categoryId"><dl class="fore"><dt><a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{ c2.categoryName }}--{{ index }}</a></dt><dd><em v-for="c3 in c2.categoryChild" :key="c3.categoryId"><a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{ c3.categoryName }}</a></em></dd></dl></div></div>........
</template>
<script>........goSearch(event){//最好的解决方案:编程式导航 + 事件委派//利用事件委派存在一些问题:①.点击的不一定都是a标签 ②.如何获取参数【产品名、产品id】// this.$router.push('/search')let element = event.target;console.log(element)//获取到当前触发这个事件的节点【h3、a、dt、dl】,需带有data-categoryname这样的节点【一定是a标签】//节点有一个属性dataset属性,可获取节点的自定义属性与属性值。// 通过categoryname区分是否为a标签,通过category1id,category2id,category3id区分是否为一级、二级、三级标签let {categoryname,category1id,category2id,category3id} = element.dataset;//如果标签身上拥有categoryname,则一定是a标签if(categoryname){//整理路由跳转的参数let location = {name:"search"}let query = {categoryname:categoryname}//一级分类、二级分类、三级分类if(category1id){query.category1Id =category1id;}else if(category2id){query.category2Id =category2id;}else if(category3id){query.category3Id =category3id;}//整理完参数// console.log(location,query)location.query=query;//将其加入路由跳转this.$router.push(location);} }........
</script>
十九. Search模块
19.1 Search模块商品模块分类与过渡动画
全局组件可直接使用,不需引用
Search模块中 typeNav三级联动商品分类菜单 的过渡动画效果
过渡动画:前提组件、元素务必要有v-if或v-show指令才可以进行过渡动画。
1).判断是否为某个路由组件(可用路径判断)
<templayte>
......<div @mouseleave="leaveShow" @mouseenter="enterShow">......</div>
......
</templayte>
......
<script>
......//当鼠标移入时,让商品分类列表进行展示enterShow(){if(this.$route.path !="/home"){this.show = true}},//当鼠标离开时,让商品分类列表进行隐藏leaveShow(){this.currentIndex = -1if(this.$route.path !="/home"){this.show = false}}
......
</script>
2).过渡动画:
<template>
......<!-- 过渡动画 --><transition name="sort">..........</transition>
......
</template><script>//过渡动画的的样式//过渡动画开始状态(进入).sort-enter{height: 0px;}//过渡动画结束状态(进入).sort-enter-to{height: 461px;} //定义动画事件、速率.sort-enter-active{overflow: hidden;//溢出文字的隐藏transition: all .5s linear;}
</script>
19.2 typeNav商品分类列表的优化
在App.vue文件中
mounted(){
//这里的mounted只会执行一次,可把重复执行的,且只需执行一次即可的内容提前到这里写。
//通知Vuex发请求,获取数据,存储于仓库当中。
//派发一个action,获取商品分类的三级列表数据this.$store.dispatch("categoryList");}
19.3 合并query和params参数
- 如果有query没有params:
if(this.$route.params){location.params = this.$route.params;//动态给location配置对象添加query属性location.query = query//将其加入路由跳转this.$router.push(location);}
- 如果有params没有query:
if(this.$route.query){let location ={name:"search",params:{keyword:this.keyword || undefined}};location.query = this.$route.query;this.$router.push(location);}
19.4 mockjs模拟数据
mockjs只在前端页面生成数据,不会向后端发送请求。
安装mockjs:npm install --save mockjs
使用步骤:
1). 在项目中,src文件夹中创建mock文件夹。API文件夹中利用axios对axios进行二次封装创建mockAjax.js文件,mockAjax.js文件中的路径baseURL的值记得要更改,这里按需改成了/mock。
//API文件夹下的mockAjax.js文件
//1.利用axios对象的方法create,创建一个axios实例
//2.request就是axios,但需要稍微进行配置。
const requests = axios.create({//配置对象//baseURL基础路径,发请求时,路径当中会出现apibaseURL: "/mock",//timeout代表请求超时的时间timeout: 5000,
});
2). 准备JSON数据(在mock文件夹仲创建相应的JSON文件,数据从word文档中获取即可)————格式化一下,别留空格。
3).把mock数据需要的图片放置到public文件夹中【public文件夹在打包时,会把相应的资源原封不动打包到dist文件夹中】
4).在mock文件夹中创建的mockServe.js通过mockjs插件实现模拟数据。
//先引入mockjs模块
import Mock from 'mockjs'
//把JSON数据格式引进来
// webpack默认对外暴露的有:图片、JSON数据格式
import banner from './banner.json'
import floor from './floor.json'//mock数据:第一个参数:请求地址 第二个参数:请求数据
Mock.mock("/mock/banner", { code: 200, data: banner }); //模拟首页大的轮播图的数据
Mock.mock("/mock/floor", { code: 200, data: floor });
5).mockServe.js文件需要在入口文件main.js中引入(至少需要执行一次,才能模拟数据)。
webpack默认对外暴露的有:图片、JSON数据格式
19.5 获取banner轮播图的数据
用上了vuex中的三连环
- 三连环:在actions里面提交mutations,让mutations进来修改state。
①.在仓库store文件夹
里的home文件
中使用vuex三连环
//引入API
import {reqGetBannerList } from '@/api/index'
//home模块的小仓库
//vuex的四大核心概念必须要有
const state = {//state中数据的默认初始值是【根据接口返回值初始化的】,服务器返回对象则state默认值为空对象,服务器返回数组则state默认值返回空数组。getBannerList: []
};
const mutations = {GETBANNERLIST(state, getBannerList) {state.getBannerList = getBannerList}
};
const actions = {//三级联动,通过API里面的接口函数调用,向服务器发请求,获取服务器的数据。//获取首页轮播图的数据async getBannerList({ commit }) {const result2 = await reqGetBannerList();if (result2.code == 200) {commit('GETBANNERLIST', result2.data)}}
};
const getters = {}export default {state,mutations,actions,getters
}
②.在ListContainer文件
中获取轮播图数据,通过计算属性computed获取
<script>
import { mapState } from "vuex";
export default {name: "ListContainer", //挂载完毕,请求轮播图mounted() {//派发action:通过Vuex发起ajax请求,将数据存储在仓库中this.$store.dispatch("getBannerList");computed:{...mapState({bannerList:state=>state.home.getBannerList})}
}
</script>
二十. swiper的基本使用
swiper文档,阅读使用:new Swiper(swiperContainer, parameters)_Swiper参数选项
使用步骤:
第一步:加载插件,引入js和css包(swiper.js和swiper.css)。
第二步:在newSwiper实例之前,页面中的结构必须要先提前准备好。
第三步:(页面中务必要有结构) new Swiper(swiperContainer, parameters);第一个参数可以是字符串,也可以是真实的DOM节点
举例说明:
<!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><!-- 第一步:加载插件,引入js和css包 --><script src="js/swiper.min.js"></script><!-- <script src="css/swiper.min.css"></script> --><link rel="stylesheet" href="css/swiper.min.css">
</head>
<!--第二步:在newSwiper实例之前,页面中的结构必须要先提前准备好。-->
<body><div class="swiper-container"><div class="swiper-wrapper"><div class="swiper-slide">Slide 1</div><div class="swiper-slide">Slide 2</div><div class="swiper-slide">Slide 3</div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div><!-- 如果需要滚动条 --><div class="swiper-scrollbar"></div></div>
</body>
</html><script>//第三步:(页面中务必要有结构) new Swiper(swiperContainer, parameters);第一个参数可以是字符串,也可以是真实的DOM节点//有dom了才能new// 写法一:// var mySwiper = new Swiper('.swiper-container', {// 写法二: new Swiper(swiperContainer, parameters);第一个参数可以是字符串,也可以是真实的DOM节点var mySwiper = new Swiper(document.querySelector('.swiper-scrollbar'), {// direction: 'vertical', // 垂直切换选项loop: true, // 是否为循环模式选项// 如果需要分页器pagination: {el: '.swiper-pagination',},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',},// 如果需要滚动条scrollbar: {el: '.swiper-scrollbar',},})
</script>
<style>.swiper-container {width: 400px;height: 200px;}
</style>
20.1 Banner实现轮播图的第一种方法
安装swiper
:npm install --save swiper
在ListContainer文件夹
下的index.vue
中的周期函数mouted(){}里写一个定时器函数。
(不是最好的办法,但能解决问题)。
此时也不推荐用updata,因为若有其他数据刷新时,会重新new Swiper。
<script>import Swiper from "swiper";
......setTimeout(() => {var mySwiper = new Swiper(document.querySelector(".swiper-container"), {loop: true, // 循环模式选项// 如果需要分页器pagination: {el: ".swiper-pagination",clickable :true,//分液器小球是否能够翻页},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 如果需要滚动条scrollbar: {el: ".swiper-scrollbar",},});},1000);
......
</script>
二十一. 轮播图通过watch+nectTick解决问题
21.1 若只用watch监听bannerList轮播图列表的属性
<template><!-- 列表 --><div class="list-container"><div class="sortList clearfix"><div class="center"><!--banner轮播--><div class="swiper-container" ref="mySwiper"><div class="swiper-wrapper"><divclass="swiper-slide"v-for="(carousel, index) in list":key="carousel.id"><img :src="carousel.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div></div></div></div>
</template><script>
//引入Swiper
import Swiper from "swiper";import { mapState } from "vuex";
export default {name: "ListContainer",mounted() {//派发action:通过Vuex发起ajax请求,将数据存储在仓库中this.$store.dispatch("getBannerList");}computed: {...mapState({bannerList: (state) => state.home.getBannerList,}),},watch:{handler(newValue,oldValue){//通过watch监听bannerList属性的属性值变化。若执行handler方法,代表组件实例身上这个属性的属性已经有了【数组:四个元素(仓库两个,ListContainer有两个)】//当这个函数执行,只能保证bannerList数据已经有了,但是没法办证v-for是否执行结束。//当v-for执行完毕,才会有结构【但是单纯在watch中使没法保证的】var mySwiper = new Swiper(document.querySelector(".swiper-container"), {loop: true, // 循环模式选项// 如果需要分页器pagination: {el: ".swiper-pagination",clickable :true,//分液器小球是否能够翻页},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},});}
}
</script>
通过watch监听bannerList属性的属性值变化。若执行handler方法,代表组件实例身上这个属性的属性已经有了【数组:四个元素(仓库两个,ListContainer有两个)】
当这个函数执行,只能保证bannerList数据已经有了,但是没法办证v-for是否执行结束。
当v-for执行完毕,才会有结构【但是单纯在watch中使没法保证的】
21.2 用watch + this.$neckTick(较为完美的解决方案)
neckTick:
1.可在下次DOM更新,循环结束之后【这里就可以指的是v-for结束后,页面中有了结构之后】,执行延迟回调。或者说,在修改数据之后,立即使用这个方法,获取更新后的DOM。
2.可保证页面中的结构一定是存在了的,neckTick经常和很多插件一起使用【因为大多插件DOM都是存在的】
<script>
......watch: {bannerList: {handler(newValue, oldValue) {//通过watch箭筒bannerList属性的属性值变化。若执行handler方法,代表组件实例身上这个属性的属性已经有了【数组:四个元素(仓库两个,ListContainer有两个)】//当这个函数执行,只能保证bannerList数据已经有了,但是没法办证v-for是否执行结束。//当v-for执行完毕,才会有结构【但是单纯在watch中使没法保证的】this.$nextTick(() => {// 当执行这个回调时,保证服务器数据回来了,v-for执行完毕了【所以轮播图的结构一定有了】var mySwiper = new Swiper(document.querySelector(".swiper-container"),{loop: true, // 循环模式选项// 如果需要分页器pagination: {el: ".swiper-pagination",clickable: true, //分液器小球是否能够翻页},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},});});},},},
......
</script>
二十二. 获取Floor组件mock数据
getFloorList这个action是要在Home路由组件发,而不能在Floor组件中发,因为需要v-for遍历组件。v-for也可以在自定义标签中使用。
22.1 组件通信的方式有哪些?
- props:用于父给子组件通信
- 插槽:(父给子)
- 自定义事件:@on和 @emit 可实现子给父通信
- 全局事件总线:$bus (全能)
- vuex:(万能的方法,该方法可以用兄弟之间,但是本次项目用来当仓库数据)
- pubsub-js:全能,但是vue中几乎不用
22.2 动态展示Floor组件
由九.Home首页的结构分析,可知Home首页被拆分为了七个部分。其中Floor组件是Home的子组件,所以这里用props进行父给子组件通信。
动态传递数据。
运用swiper进行轮播图的修改。
在ListContainer组件中的轮播图,因为当时组件是由内部发请求、动态渲染结构【前台服务器数据需要传回来】,所以ListContainer组件中的轮播图,要把mounted写在外面的(写在了App.vue里),所以这里在ListContainer组件mounted之前结构还没有。
Floor这个组件,是在Home组件里发的请求,数据是父组件给的,子组件已经渲染了,所以说在Floor组件mounted之前结构就已经有了。
Home文件index.vue
<template><div>
......// 父组件Home通过自定义属性list给子组件Floor传递数据<Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/></div>
</template>
Home文件下的Floor子组件
<template><!--楼层--><div class="floor">
......</div>
</template><script>
import Swiper from "swiper";
export default {name: "Floor",// 子组件Floor通过props属性接收到父组件Home自定义属性list传来的数据props: ["list"],
......
};
</script>
22.3 共用组件Carsouel(轮播图)
将首页Home组件里的轮播图(在Floor组件和ListContainer组件中)功能拆出来,新建成一个共用全局组件。
注:若看到某个组件在很多地方使用,则可把它变成全局组件。注册一次,即可在任意地方使用,共用的组件或非路由组件应放在components文件夹中。
这里在components文件夹下新建文件夹Carsouel(轮播图专用),
在Carsouel文件夹中index.vue,代码如下:
<template><div class="swiper-container" ref="floor1Swiper"><div class="swiper-wrapper"><divclass="swiper-slide"v-for="(carousel, index) in list":key="carousel.id"><img :src="carousel.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div>
</template><script>
import Swiper from "swiper";
export default {name: "Carousel",props: ['list'],watch: {//监听父传过来的子组件listlist: {//使用立即监听//为什么不加immediate立即监听就watch监听不到list?immediate: true,handler() {//只能监听到数据已经有了,但是v-for动态渲染结构还是无法确定的,所以还是需要用nextTickthis.$nextTick(() => {var mySwiper = new Swiper(this.$refs.floor1Swiper,// document.querySelector(".swiper-container"),{loop: true, // 循环模式选项// 如果需要分页器pagination: {el: ".swiper-pagination",clickable: true, //分液器小球是否能够翻页},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},});});},},},
};
</script><style scoped>
</style>
二十三. Search模块的静态组件
23.1 写一个模块的步骤(套路):
- 1).先写静态页面 + 将静态组件拆分出来
- 2).写接口,发请求(API)
- 3).vuex(三连环)
- 4).组件获取仓库数据,动态展示数据
23.2 箭头函数
- ()中定义参数,若只有一个参数,可以不写括号;
- {}中写函数体,若函数体找那个只有返回值,可不写return。
箭头函数 vs 普通函数
箭头函数 | 普通函数 | |
this的指向不同 | 在哪里定义函数,this就指向谁。 | 谁调用这个函数,this就指向谁 |
二十四. search模块中动态展示产品列表
写一个模块的步骤(套路):
- 1).先写静态页面 + 将静态组件拆分出来
- 2).写接口,发请求(API)
- 3).vuex(三连环)
- 4).组件获取仓库数据,动态展示数据
一). API文件夹下的index.js
import requests from "./request";//获取search模块数据 地址:/api/list 请求方式为:post 这里需要带参数
//当前这个函数也需要接收外部传递参数
//当给这个接口获取搜索模块的数据,给服务器传递一个默认参数【传递的参数至少是一个空对象】export const reqGetSearchInfo = (params) => requests({ url: "/list", method: "post", data: params })
二). 在仓库store文件夹下的index.js
//引入API
import { reqGetSearchInfo } from '@/api'
//search模块的小仓库
//vuex的四大核心概念必须要有
const state = {searchList: {}
};
const mutations = {GETSEARCHLIST(state, searchList) {state.searchList = searchList}
};
const actions = {//获取search模块数据async getSearchList({ commit }, params = {}) {//当前reqGetSearchInfo这个函数在调用获取服务器数据时,至少传递一个参数(空对象)// `this.$store.dispatch('action', payload);`//params形参:当用户派发action时,当第二个参数传递过来时,至少是一个空对象。let result4 = await reqGetSearchInfo(params);if (result4.code == 200) {commit("GETSEARCHLIST", result4.data);}}
};//计算属性
//项目当中getters主要的作用:简化仓库中的数据。
const getters = {//当前的state是小仓库search里的state,并非大仓库中的那个state。goodsList(state) {//这样书写是会有问题的。//若网络不给力或没有网络,则state.searchList.goodsList应返回的是undefined;所以此时计算新的属性的属性值至少给一个数组。return state.searchList.goodsList || [];},trademarkList(state) {//这样书写是会有问题的return state.searchList.trademarkList;},attrsList(state) {//这样书写是会有问题的return state.searchList.attrsList;},
}export default {state,mutations,actions,getters
}
三). 在路由pages文件夹下的Search文件中的index.js
<template>
......
</template>
<script>
......computed:{...mapGetters(['goodsList'])}
......
</script>
24.1 Search模块中子组件SearchSelector.vue的动态开发
将重复的、可使用v-for的部分使用起来。
本次所使用的数据在store仓库文件夹下的search文件夹里。Search模块的子组件SearchSelector.vue进行引入使用。
<template><div class="clearfix selector"><div class="type-wrap logo"><div class="fl key brand">品牌</div><div class="value logos"><ul class="logo-list"><li v-for="(trademark,index) in trademarkList" :key="trademark.tmId">{{trademark.tmName}}</li></ul></div><div class="ext"><a href="javascript:void(0);" class="sui-btn">多选</a><a href="javascript:void(0);">更多</a></div></div><div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.ttrId"><div class="fl key">{{attr.attrName}}</div><div class="fl value"><ul class="type-list"><li v-for="(attrvalue,index) in attr.attrValueList" :key ="index"><a>{{attrvalue}}</a></li></ul></div><div class="fl ext"></div></div></div>
</template><script>import {mapGetters} from 'vuex'; export default {name: 'SearchSelector',computed:{...mapGetters(['trademarkList','attrsList'])}}
</script>
二十五. Object.assign的用法
为了再次发请求得到不同数据,可以用watch数据监听。
- Object.assign的用法: Object.assign(target, …sources)
target: 目标对象,sources: 源对象(可有多个)
用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,并将返回目标对象。
举例:
const o1 {a:1};
const o2 {b:2};
const o3 {c:3};const obj = Object.assign(o1,o2,o3);
console.log(obj);//{a:1,b:2,c:3}
console.log(o1);//{a:1,b:2,c:3},目标对象自身也会改变
二十六. 面包屑
26.1 面包屑处理分类的操作
用的是编程式路由导航【在自己的路由组件下跳转到自己】
本次项目的面包屑:
- ①.query删除(本次项目为分类属性的删除,修改路由信息)
给点击事件一个删除种类名称removeCategoryname的方法
<template>
......
<!-- 分类的面包屑 --><ul class="fl sui-tag"><li class="with-x" v-show="searchParams.categoryname">{{ searchParams.categoryname}}<i @click="removeCategoryname">×</i></li></ul>
......
</template><script>
......removeCategoryname() {//把带给服务器是参数置空了,还要向服务器发请求。//带给服务器参数说明可有可无的:若属性值为空的字符串还是会把相应的字段带给服务器。//但是把相应的字段变为undefined,则当前这个字段不会带给服务器。this.searchParams.categoryname = undefined;this.searchParams.category1Id = undefined;this.searchParams.category2Id = undefined;this.searchParams.category3Id = undefined;this.getData();//地址栏也需要修改,进行路由跳转(在自己的路由组件下跳转到自己)this.$router.push({name:"search"});},
......
</script>
把带给服务器是参数置空了,还要向服务器发请求。
带给服务器参数说明可有可无的:若属性值为空的字符串还是会把相应的字段带给服务器。但是把相应的字段变为undefined,则当前这个字段不会带给服务器。所以这里赋值undefined。地址栏也需要进行路由跳转。在自己的路由组件下跳转到自己:this.$router.push({name:"search"});
严谨一点儿的写法:
removeCategoryname() {this.searchParams.categoryname = undefined;this.searchParams.category1Id = undefined;this.searchParams.category2Id = undefined;this.searchParams.category3Id = undefined;this.getData();// 可要可不要//地址栏也需要修改,进行路由跳转(在自己的路由组件下跳转到自己)if(this.$route.params){this.$router.push({name:"search",params:this.$route.params});}
26.2 面包屑处理关键字的操作
当Search组件中的面包屑关键字清除后,需要让兄弟组件Header组件中的关键字清除。
- ②.params删除
需要多一步操作:删除输入框内的关键字(因为params参数是从Header组件搜索框内获取的,所以需要让Header组件传递信息给Search组件,然后再进行删除。这俩组件为兄弟组件)
组件通信方法(加深印象):
props:(父给子)
插槽:(父给子)
自定义事件:(子给父)
vuex:(万能的方法,该方法可以用兄弟之间,但是本次项目用来当仓库数据)
$bus:(万能的方法,全局事件总线)
pubsub-js:(万能的方法,但是在vue中用得少)
这里Header组件传递信息给Search组件运用了全局事件总线$bus
,
- 第一步:应在
main.js
入口文件中进行补写全局事件总线$bus配置:
new Vue({//全局事件总线$bus配置beforeCreate() {Vue.prototype.$bus = this;},
}
......)
- 第二步:在Search组件使用$bus通信 this.$bus.$emit("......")
<template>
......<ul><!-- 关键字的面包屑 --><li class="with-x" v-show="searchParams.keyword">{{ searchParams.keyword }}<i @click="removeKeyword">×</i></li></ul>
......
</template><script>//删除关键字的面包屑的名字removeKeyword() {this.searchParams.keyword = undefined;//再次发请求this.getData();//通知兄弟组件Header清除关键字,发送清除请求this.$bus.$emit("clear");if (this.$route.query) {this.$router.push({ name: "search",query:this.$route.query});}},
</script>
- 第三步:在
Header的组件index.vue
中补mounted的全局事件总线$bus this.$bus.$on
mounted(){//通过全局事件总线$bus清除关键字//一挂载就监听clear事件this.$bus.$on("clear",()=>{this.keyword = '';});},
26.3 面包屑处理品牌信息
此时用的是自定义事件
- 首先:在Search中的子组件SearchSelector展示标签里,用@定义自定义事件(这里定义了trademarkInfo):
<template><SearchSelector @trademarkInfo="trademarkInfo"/>
</template>
......
<methods>//自定义事件回调trademarkInfo(){},
</methods>
......
- 其次:在SearchSelector.vue中用this.$emit('trademarkInfo',trademark);发送请求给Search组件:
<template>......<li v-for="(trademark,index) in trademarkList" :key="trademark.tmId" @click="tradeMarkHandler(trademark)">{{trademark.tmName}}</li>......
</template>
<script>......methods:{//品牌的事件处理函数tradeMarkHandler(trademark){//点击了品牌(苹果),还需要整理参数,向服务器发请求获取相应的数据进行展示。// alert('123')//因为是由父组件中的searchParams参数带给服务器参数的,所以子组件把被点击的品牌的信息传给父组件,父组件再与服务器联系。this.$emit('trademarkInfo',trademark);}}......
</script>
- 然后:修改、补充Search模块里的内容
<template>......<ul><!-- 品牌点击的面包屑 --><li class="with-x" v-if="searchParams.trademark">{{ searchParams.trademark.split(":")[1]}}<i @click="removeTradeMark">×</i></li></ul>......
</template>
<Script>......//自定义事件回调trademarkInfo(trademark) {// console.log("信息",trademark)// 1.整理品牌字段的参数 “ID:品牌名称”this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;// 再次发请求获取search模块最新列表数据进行展示this.getData();},//删除品牌的信息removeTradeMark() {this.searchParams.trademark = undefined;//再次发请求this.getData();//老师使用的是v-if,所以当trademark为undefined时,不会解析if内部结构,故不会报错。而实测v-show会报错,Cannot read property 'split' of undefined"},......
</Script>
二十七.平台售卖属性的操作
27.1 子组件传给父组件(用自定义事件)
- 首先:在
Search
的子组件SearchSelector组件
中补充自定义事件@click="attrInfo(attr,attrvalue)"
和attrInfo(attr,attrValue)
的方法:
<template>
......<li v-for="(attrvalue,index) in attr.attrValueList" :key ="attrvalue" @click="attrInfo(attr,attrvalue)"><a>{{attrvalue}}</a></li>
......
</template>
<script>methods:{......attrInfo(attr,attrValue){//["属性ID:属性值:属性名"];this.$emit("attrInfo",attr,attrValue);}......}
</script>
- 其次:在
Search组件
中补充:点击事件@attrInfo="attrInfo"
和方法attrInfo(attr,attrValue)
,即可。
<template>......
<!--selector Search的子组件-->
<SearchSelector @attrInfo="attrInfo"/>......
</template>
......
<script>methods:{......//收集平台属性地方回调函数(自定义事件)attrInfo(attr,attrValue){// ["属性ID:属性值:属性名"]console.log(attr,attrValue);}......}
</script>
27.2 将平台售卖属性放置面包屑(数组去重)
需要遍历,所以用v-for,而不用v-if。
- 在Search组件中整理好数据、增加
平台的售卖属性值的面包屑:
<template>......<!-- 平台的售卖属性值的面包屑 --><li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{{ attrValue.split(":")[1]}}<i @click="removeTradeMark">×</i></li>......
</template>
<script>
......//收集平台属性地方回调函数(自定义事件)attrInfo(attr, attrValue) {// ["属性ID:属性值:属性名"]//补充:参数格式整理好let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;this.searchParams.props.push(props);//再次发请求this.getData();},
......
</script>
- 需要进行数组去重!!:
<script>//收集平台属性地方回调函数(自定义事件)attrInfo(attr, attrValue) {// ["属性ID:属性值:属性名"]//参数格式整理好let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;// 数组去重,这里采用了if方法进行数组去重。// if语句中只有一行代码时,可省略大花括号.if(this.searchParams.props.indexOf(props)==-1){this.searchParams.props.push(props);当if执行后,可再次发请求this.getData();}},
</script>
- 删除平台的售卖属性值的面包屑。
<template>
......<!-- 平台的售卖属性值的面包屑 补写参数index,即:@click="removeAttr(index)" --><li class="with-x" v-for="(attrValue, index) in searchParams.props" :key="index">{{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">×</i></li>
......
</template>
<script>
//删除平台的售卖属性值的面包屑。这里是删除数组里的某个数据,所以需要一个参数(索引值)removeAttr(index){//再次整理参数this.searchParams.props.splice(index,1);//再次发请求this.getData()}
</script>
二十八. 排序操作(上)
在接口文档中,
- 问题一:order属性的属性值最多有多少种写法?
四种。1:asc、1.desc、2:asc、2:desc
- 问题二:应先考虑:谁应该有类名?通过order属性值当中是包含1(综合)或者包含2(价格)
......<ul class="sui-nav"><li :class="{active: searchParams.order.indexOf('1') != -1}"><a>综合</a></li><li :class="{active: searchParams.order.indexOf('2') != -1}"><a>价格</a></li></ul>
......
<li :class="{active: searchParams.order.indexOf('1') != -1}">
写得比较复杂,可以放在计算属性computed里书写。
<template>
......<ul class="sui-nav"><li :class="{ active: isOne}"><a>综合</a></li><li :class="{ active: !isOne }"><a>价格</a></li></ul>
......
</template>
<script>......
computed:{isOne(){return this.searchParams.order.indexOf("1") != -1;}
}......
</script>
- 问题三:谁应该有箭头矢量图?
①在阿里巴巴网页上搜索需要的矢量图内容:iconfont-阿里巴巴矢量图标库
然后在public文件夹中的index.html中引用
<link rel="stylesheet" href="https://at.alicdn.com/t/font_3063860_l1hq82jazlm.css">
②然后在SearchSelector文件夹中:【iconfont一定要写】
<li :class="{ active: !isOne }"><a>价格 <span v-show="!isOne" class="iconfont icon-sort-down"></span></a></li>
③设置业务逻辑
<ul class="sui-nav"><li :class="{ active: isOne }"><a>综合<span v-if="isOne" class="iconfont" :class="{'icon-sort-up':isAsc,'icon-sort-down':isDesc}"></span></a></li><li :class="{ active: isTwo }"><a>价格<span v-show="isTwo" class="iconfont" :class="{'icon-sort-up':isAsc,'icon-sort-down':isDesc}"></span></a></li>
</ul>
二十九. 操作排序——对升序降序的操作(下)
对升序降序的操作
<template>......<li :class="{ active: isOne }" @click="changeOrder('1')"><a>综合<span v-if="isOne" class="iconfont" :class="{'icon-sort-up':isAsc,'icon-sort-down':isDesc}"></span></a></li><li :class="{ active: isTwo }" @click="changeOrder('2')"><a>价格<span v-show="isTwo" class="iconfont" :class="{'icon-sort-up':isAsc,'icon-sort-down':isDesc}"></span></a></li>......
</template>
<script>......//排序操作changeOrder(flag){//flag形参:它是原始数据,是一个标记,代表用户点击的是 综合(1) 还是 价格(2) 【用户点击时,@click="changeOrder('1')"传参进来的】//这里获取起始状态let originOrder = this.searchParams.order;let originFlag = this.searchParams.order.split(":")[0];let originSort = this.searchParams.order.split(":")[1];console.log("起始flag为"+originFlag,"用户点击的flag"+flag);//准备一个新的order属性值let newOrder = ''if(flag == originFlag){// 三元表达式(expr1)?(expr2):(expr3)。expr1 求值为 TRUE 时的值为 expr2,在 expr1 求值为 FALSE 时的值为 expr3newOrder = `${originFlag}:${originSort == "desc"?"asc":"desc"}`}else{newOrder = `${flag}:${"desc"}`}//将新的order赋予个searchParamsthis.searchParams.order = newOrder;//再次重新发请求this.getData();}......
</script>
前端三个重要的原理:轮播图(Carousel)、分页器(Pagination)、日历
三十. 分页器静态组件(分页器原理)
注册全局组件 分页器(Pagination)。
【本次项目需掌握自定义分页功能】
- 问题一:分页器展示需要哪些数据(条件)?
1.一共有多少页?用pageNo字段代表页数。
2.当前是第几页?
3.每一页有多少数据展示?用pageSize字段进行代表。
4.整个分页器一共有多少条数据?用total字段进行代表,(因此可额外获取另一条信息:一共多少页?)。
5.需要知道分页器连续页码的个数是多少?用continues进行代表,一般是五页或者七页【奇数】,因为奇数对称(好看)。
总结:对于分页器而言,自定义的前提需要知道四个条件。
(1). pageNo:当前第几个
(2).pageSize:代表每一页展示多少条数据
(3).total:代表整个分页一共要展示多少条数据【可得到总共多少页totalpage】
(4).continues:代表分页连续页码的个数【重要部分】
三十一. 分页器起始与结束数字计算(分页器逻辑)
再次复习:
组件通信方法:
props:(父给子)、插槽:(父给子)、自定义事件:(子给父)、vuex:(万能的方法,该方法可以用兄弟之间,但是本次项目用来当仓库数据)、$bus:(万能的方法,全局事件总线)、pubsub-js:(万能的方法,但是在vue中用得少
31.1 分页器起始与结束(分页器显示的逻辑)
- 一).这里使用的是父传子:
第一步:在父组件Search模块中传数据。
<!-- 分页器 测试分页器阶段,这里数据将来需要替换的--><Pagination :pageNo="1" :pageSize="3" :total="91" :continues="5"/>
第二步:在子组件分页器Pagination的index.js中用props接收父传来的数据
export default {name: "Pagination",props:['pageNo','pageSize','total','continues']
};
- 二).分页器(分页器逻辑)
自定义分页器,在开发时先用自己传递的假数据进行调试,调试成功后再用服务器数据。
对于分页器而言,需要算出连续页面起始数字和结束数字。
分页器上面的数字的业务逻辑(用computed):
pageNo为当前页;
continues为连续的页数;
totalPage为总页数;
start为起始页数;
end为结束页数。
<template>
......<h2>{{startNumAndEndNum}}------{{pageNo}}</h2>
......
</template>
<script> ......computed:{//知道条数之后可以算出总共多少页totalPage(){//向上取整return Math.ceil(this.total/this.pageSize);},//计算出连续的页码起始数字与结束数字【连续页码的数字,至少有5页】startNumAndEndNum(){//进行解构:const {continues,pageNo,totalPage} = this;。解构的原因是:因为接下来是直接用的变量名,若不解构则就作为实际属性使用。const {continues,pageNo,totalPage} = this;//先定义两个变量存储起始数字与结束数字let start = 0,end = 0;//连续页码数字为5【至少为五页】,若出现不正常现象【不够五页】//【不正常现象】:【总页数没有连续页码多】if(continues > totalPage){start = 1;end = totalPage;}else{//【正常现象】:【连续页码5,但是总页数一定是大于5的】//起始数字start = pageNo - Math.floor(continues/2);//结束数字end = pageNo + Math.floor(continues/2);//【不正常现象】:【start数字出现0或者负数】,需纠正if(start < 1){start = 1;end = continues;}//【不正常现象】:【end数字超过总页码数】,需纠正if(end > totalPage){start = totalPage - continues+1;end = totalPage;}}return {start,end};}......
</script>
31.2 分页器的动态展示(分页器按钮显示的逻辑)
分页器动态展示,可分为上中下
【中间部分】
v-for可以遍历:数组|数字|字符串|对象
分页器页码按钮显示逻辑:
<template><div class="pagination"><!-- 上 --><button>上一页</button><button v-if="startNumAndEndNum.start > 1">1</button><button v-if="startNumAndEndNum.start > 2">···</button><!-- 中 --><button v-for="(page,index) in startNumAndEndNum.end" :key="index" v-show="page >= startNumAndEndNum.start">{{page}}</button><!-- 下 --><button v-if="startNumAndEndNum.end < totalPage-1">···</button><button v-if="startNumAndEndNum.end < totalPage">{{totalPage}}</button><button>下一页</button>
<h2>{{startNumAndEndNum}}------{{pageNo}}</h2><button style="margin-left: 30px">共 {{total}} 条</button></div>
</template>
31.3 分页器的完成
将子组件Pagination中的业务传给父组件Search。
子传父:
- 第一步:在父组件Search中进行注册自定义事件
@getPageNo="getPageNo"
<!-- 分页器 测试分页器阶段,这里数据将来需要替换的--><Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" @getPageNo="getPageNo"/>
- 第二步:在父组件中Search中定义自定义事件
@getPageNo="getPageNo"
的回调事件
<script>
......
methods:{//自定义事件的回调函数---获取当前第几页(子组件Pagination给父组件Search传)getPageNo(pageNo){//子组件pagination已经整理好了数据,可带给服务器参数this.searchParams.pageNo = pageNo;//再次发请求this.getData();}
}
......
</script>
- 第三步:在子组件Pagination中用
@click="$emit('getPageNo',????)"
编写事件的触发
<template><div class="pagination"><!-- 上 --><button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页</button><button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)">1</button><button v-if="startNumAndEndNum.start > 2">···</button><!-- 中 --><button v-for="(page,index) in startNumAndEndNum.end" :key="index" v-show="page >= startNumAndEndNum.start" @click="$emit('getPageNo',page)">{{page}}</button><!-- 下 --><button v-if="startNumAndEndNum.end < totalPage-1">···</button><button v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo',totalPage)">{{totalPage}}</button><button :disabled="pageNo==totalPage" @click="$emit('getPageNo',pageNo+1)">下一页</button>
<h2>{{startNumAndEndNum}}------{{pageNo}}</h2><button style="margin-left: 30px">共 {{total}} 条</button></div>
</template>
31.4 分页器添加类名
在子组件Pagination中写上动态绑定的类名,使其在被点击时,按钮呈现颜色
<template>......<div class="pagination"><!-- 上 --><button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">上一页</button><buttonv-if="startNumAndEndNum.start > 1"@click="$emit('getPageNo', 1)":class="{ active: pageNo == 1 }">1</button><button v-if="startNumAndEndNum.start > 2">···</button><!-- 中 --><buttonv-for="(page, index) in startNumAndEndNum.end":key="index"v-show="page >= startNumAndEndNum.start"@click="$emit('getPageNo', page)":class="{active:pageNo==page}">{{ page }}</button><!-- 下 --><button v-if="startNumAndEndNum.end < totalPage - 1">···</button><buttonv-if="startNumAndEndNum.end < totalPage"@click="$emit('getPageNo', totalPage)":class="{active:pageNo==totalPage}">{{ totalPage }}</button><button:disabled="pageNo == totalPage"@click="$emit('getPageNo', pageNo + 1)">下一页</button><button style="margin-left: 30px">共 {{ total }} 条</button></div>......
</template>
............
<style>.......active{background: skyblue;
}
</style>
三十二. 产品详情页面——滚动行为
滚动行为 | Vue Router
- 步骤:
1.静态组件 (详情页的组件,暂未注册为路由组件)
当点击商品图片时,跳转到详情页面,在路由跳转的时候需要带上产品的ID给详情页面。用params传参,需占位
2.再搞请求
3.再再搞vuex
4.动态展示组件
- 操作:
先把Detail.vue在路由文件中引用
import Detail from '@/pages/Detail'
......
//配置路由
export default new VueRouter({//配置路由routes: [{// 这个的路由跳转需要带参数,所以需要占位,多写一下/:skuidpath: "/detail/:skuid",component: Detail,meta: { show: true }}, ......]
在Search组件中
<div class="p-img"><!-- 因为跳转的属性值中出现了变量,这是动态的跳转,所以是:to --><router-link :to="`/detail/${good.id}`"><img :src="good.defaultImg"/></router-link>
</div>
- 滚动行为
查看vue官方文档Vue Router里的进阶滚动行为:
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router
能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
注意: 这个功能只在支持 history.pushState
的浏览器中可用。
当创建一个 Router 实例,你可以提供一个 scrollBehavior
方法:
const router = new VueRouter({routes: [...],scrollBehavior (to, from, savedPosition) {// return 期望滚动到哪个的位置}
})
举例:
scrollBehavior (to, from, savedPosition) {return { x: 0, y: 0 }
}
三十三. 产品详情页面数据获取
33.1 产品详情页面数据获取
- 第一步:在API文件中写获取产品详情信息的接口
//获取detail模块数据 地址:/api/item/{ skuId } 请求方式是:get
export const reqGoodsInfo = (skuId) => requests({ url: `/item/${ skuId }`, method: "get" })
- 第二步:在vuex文件夹中新增小仓库detail.js获取产品详情信息
const state = {}
const mutations = {};
const actions = {};
const getters = {};export default {state,mutations,actions,getters
}
然后将小仓库引入大仓库中
......
import detail from '@/store//detail';
......
export default new Vuex.Store({//实现Vuex仓库模块式开发存储数据modules: {......detail......}
})
- 第三步:发请求,捞数据,存到仓库。修改完善一下仓库
detail.js
:
//引入api
import { reqGoodsInfo } from "@/api";
const state = {//这里data返回的是一个对象,所以应该写成空对象goodInfo: {}
}
const mutations = {GETGOODINFO(state, goodInfo) {state.goodInfo = goodInfo;}
};
const actions = {//获取产品信息的actionasync getGoodInfo({ commit }, skuId) {//当用户dispatch的时候派发得到的ID要带过去let result5 = await reqGoodsInfo(skuId);if (result5.code == "200") { //表示请求成功commit("GETGOODINFO", result5.data);}}
};
const getters = {};export default {state,mutations,actions,getters
}
- 第四步:在detail组件中派发action,写在
mounted
中,记得要把产品的ID带上(ID在路由的params参数中)。
mounted(){//派发action获取产品详情的信息// console.log(this.$route.params)//{skuid: "7"},其中skuid为占位符,可先 console.log(this.$route.params)看看占位符的命名this.$store.dispatch('getGoodInfo',this.$route.params.skuid)}
33.2 产品详情页面动态展示数据
先在detail的小仓库
中,用getters
简化detail.js
中的数据
//简化数据而生
const getters = {categoryView(state) {//例:state.goodInfo初始状态下是空对象,空对象的categoryView属性值是undefined,所以应该提前多声明一个空对象。//当前计算出的categoryView属性值至少是一个空对象,加上一个{},这样假的报错不会有了。return state.goodInfo.categoryView || {};}
};
再在detail.vue
中引入{mapGetters},把仓库
中的数据映射到detail组件
上。
<script>import { mapGetters } from "vuex";export default{computed: {...mapGetters(['categoryView']),},}
</script>
33.3 产品详情页面动态展示数据——右侧
方法同上一个小节33.2,上一节的跟进。
......
const state = {//这里data返回的是一个对象,所以应该写成空对象goodInfo: {},skuInfo: {}
}
......
//简化数据而生
const getters = {categoryView(state) {//例:state.goodInfo初始状态下是空对象,空对象的categoryView属性值是undefined,所以应该提前多声明一个空对象。//当前计算出的categoryView属性值至少是一个空对象,加上一个{},这样假的报错不会有了。return state.goodInfo.categoryView || {};},skuInfo(state) {return state.goodInfo.skuInfo || {};}
};
......
在Detail组件
中展示使用即可
......<!-- 导航路径区域 --><div class="conPoin"><span v-show="categoryView.category1Name">{{categoryView.category1Name}}</span><span v-show="categoryView.category2Name">{{categoryView.category2Name}}</span><span v-show="categoryView.category3Name">{{categoryView.category3Name}}</span></div>
......<!-- 右侧选择区域布局 --><div class="InfoWrap"><div class="goodsDetail"><h3 class="InfoName">{{skuInfo.skuName}}</h3><p class="news">{{skuInfo.skuDesc}}</p>
......
三十四. Zoom放大镜展示数据—裁剪
- 父组件
Detail
传
<template>......
<!--放大镜效果-->
<Zoom :skuImageList="skuImageList" />......
</template>
<script>
import Zoom from "./Zoom/Zoom";
......
computed: {...mapGetters(["categoryView", "skuInfo"]),skuImageList() {//给子组件的数据,若服务器的数据未回来,则skuInfo这个对象应赋值空对象return this.skuInfo.skuImageList || [];},
......
</script>
- 子组件
Zoom
接收
<template><div class="spec-preview"><img :src="imgObj.imgUrl" /><div class="event"></div><div class="big"><img :src="imgObj.imgUrl" /></div><div class="mask"></div></div>
</template><script>export default {name: "Zoom",props:["skuImageList"],computed:{imgObj(){//以防从空对象中找数据未找到而返回undefined,从而自己先定义个空对象return this.skuImageList[0]||{}}}}
</script>
三十五. detail路由组件展示商品售卖属性
<template><div class="choose"><div class="chooseArea"><div class="choosed"></div><dl v-for="(spuSaleAttr,index) in spuSaleAttrList" :key="spuSaleAttr.id"><dt class="title">{{spuSaleAttr.saleAttrName}}</dt><dd changepirce="0" v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList" :key="spuSaleAttrValue.id" :class="{active:spuSaleAttrValue.isChecked==1}" >{{spuSaleAttrValue.saleAttrValueName}}</dd></dl></div>
......
</template>
三十六. 产品售卖属性值——排他操作—裁剪
<template>
......<ddchangepirce="0"v-for="(spuSaleAttrValue, index) in spuSaleAttr.spuSaleAttrValueList":key="spuSaleAttrValue.id":class="{ active: spuSaleAttrValue.isChecked == 1 }"@click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)">{{ spuSaleAttrValue.saleAttrValueName }}</dd>
......
</template>
<script>
......
//排他方法methods:{//产品的售卖属性值切换高亮changeActive(saleAttrValue,arr){//遍历全部的售卖属性值isChecked为零(无高亮)arr.forEach(item=>{item.isChecked = 0;});//点击的那个售卖属性值isChecked为壹(有高亮)saleAttrValue.isChecked = 1;}}
......
</script>
三十七. 放大镜操作(上)
37.1 放大镜操作——ImageList组件和Zoom组件之间的小操作
点击小图片,周围边框变颜色。在ImageList.vue
中
<template>......<img :src="slide.imgUrl":class="{active:currentIndex == index}"@click="changeCurrentIndex(index)" />......
</template>
<script>
......data(){return{//设置响应式数据默认值为0currentIndex:0}},......methods:{//点击小图片,小图片的周围会产生光亮边框changeCurrentIndex(index){//修改响应式数据this.currentIndex = index;//同时通知兄弟组件当前的索引值,改变大图this.$bus.$emit('getIndex',this.currentIndex);}}
......
</script>
传递全局事件,兄弟组件Zoom.vue
接收
<script>
......mounted(){//全局事件总线,获取兄弟组件传来的索引值this.$bus.$on('getIndex',(index)=>{console.log(index)})}
......
</script>
37.2 放大镜操作——大图片随着小图片的点击也进行改变
在Zoom组件
中
<script>
......data(){return{currentIndex:0}},computed:{imgObj(){//以防从空对象中找数据未找到而返回undefined,从而自己先定义个空对象//this.currentIndex是可变的;this.currentIndex = index;return this.skuImageList[this.currentIndex]||{}}},mounted(){//全局事件总线,获取兄弟组件传来的索引值this.$bus.$on('getIndex',(index)=>{//修改当前响应式数据,使大图也进行改变this.currentIndex = index;})}
......
</script>
37.3 放大镜操作——遮罩层
- 设置遮罩层随着鼠标的移动一起移动
<template>
......<div class="event" @mousemove="handler"></div><!-- 遮罩层 --><div class="mask" ref="mask"></div>
......
</template>
<script>......methods:{handler(event){let mask = this.$refs.mask;let left = event.offsetX - mask.offsetWidth/2;let top = event.offsetY - mask.offsetHeight/2;//约束范围//左右的约束范围if(left <= 0 ){left = 0;}if(left >= mask.offsetWidth){left = mask.offsetWidth;}//上下的约束范围if(top <=0 ){top =0;}if(top >= mask.offsetHeight){top = mask.offsetHeight;}//修改元素的left|top属性值mask.style.left = left + 'px';mask.style.top = top + 'px';}}......
</script>
- 设置右侧的大图移动情况(补充)
<template>
......<!-- 右侧大图 --><div class="big"><img :src="imgObj.imgUrl" ref="big"/></div><!-- 遮罩层 --><div class="mask" ref="mask"></div>
......
</template>
<script>
......methods:{handler(event){let mask = this.$refs.mask;let left = event.offsetX - mask.offsetWidth/2;let top = event.offsetY - mask.offsetHeight/2;if(left <= 0 ){left = 0;}if(left >= mask.offsetWidth){left = mask.offsetWidth;}if(top <=0 ){top =0;}if(top >= mask.offsetHeight){top = mask.offsetHeight;}mask.style.left = left + 'px';mask.style.top = top + 'px';//设置右侧大图的情况let big = this.$refs.big;big.style.left = -2 * left +'px';big.style.top = -2 * top +'px';}}
......
</script>
三十八. 购买产品个数的操作
<template>
......<div class="controls"><input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum"/><a href="javascript:" class="plus" @click="skuNum++">+</a><a href="javascript:" class="mins" @click="skuNum > 1 ? skuNum--:skuNum=1">-</a></div>
......
</template><script>
......data(){return{//产品的个数skuNum:1}},methods:{//表单元素修改产品个数changeSkuNum(event){//用户输入的文本 * 1let value = event.target.value * 1;//如果用户输入的文本非法【字符串、负数】if(isNaN(value)|| value < -1){console.log("非法")this.skuNum = 1;}else{//向上取整【带有小数的情况】this.skuNum = Math.ceil(value);}}}
......
</script>
三十九. “加入购物车”路由
(这个路由和其他的路由不太一样)
"加入购物车”的按钮,需要用到的操作步骤:
1.发送请求,将产品的数据信息存储于服务器。
2.进行路由的跳转,跳转到购物车界面。
39.1 ”加入购物车“按钮的步骤
- 一、写好
API
接口
//获取更新某一个产品的个数
///api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId, skuNum) => requests({ url: `/cart/addToCart/${skuId}/${skuNum}`, method: "post" })
- 二、完善仓库
detail
数据
import { reqGoodsInfo, reqAddOrUpdateShopCart } from "@/api";
const actions = {async AddOrUpdateShopCart({ commit }, { skuId, skuNum }) {//加入购物车返回的解构数据skuId、skuNum。//加入购物车后(发请求后),前台将参数带给服务器,服务器写入数据成功,并未返回其他的数据,只返回了code=200,代表这次操作成功。//因服务器未返回其余数据,所以不需要三连环存储数据。let result = await reqAddOrUpdateShopCart(skuId, skuNum);}
}
- 三、在
Detail
组件中
<template>
......<div class="add"><!-- 以往的路由跳转:从A跳到B。本次的路由跳转之前,发请求将购买的产品信息通过请求的形式通知服务器,服务器进行相应的存储 --><a @click="addShopcar">加入购物车</a></div>
......
</template>
<script>
......methods:{addShopcar(){//1.发请求---将产品信息加入到数据库中(通知服务器)// this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})调用的是仓库中的addOrUpdateShopCart函数,addOrUpdateShopCart函数加上了async语法题,所以返回得到的一定是promise。this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})//2.若服务器存储成功---进行路由跳转传递参数//3.若失败,则给用户提示}}
......
</script>
- 四、修改仓库
detail
中的内容,加入返回成功和失败的情况内容。
//将产品添加到购物车中async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {//加入购物车返回的解构数据skuId、skuNum。//加入购物车后(发请求后),前台将参数带给服务器,服务器写入数据成功,并未返回其他的数据,只返回了code=200,代表这次操作成功//因服务器未返回其余数据,所以不需要三连环存储数据。//注:async函数执行返回的结果一定是一个promise【要么成功,要么失败】let result6 = await reqAddOrUpdateShopCart(skuId, skuNum);//当前这个函数执行返回Promise//当code返回200时为成功if (result6.code == 200) {return "ok"} else {//返回失败时return Promise.reject(new Error('faile'));}// console.log(result6);}
- 五、在四完成之后可以进行路由跳转,则再次改写
Detail组件
中的内容,用上了try{}catch{}语法。
......async addShopcar(){//1.发请求---将产品信息加入到数据库中(通知服务器)// this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})调用的是仓库中的addOrUpdateShopCart函数,返回得到的是promise。try{await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum});//路由跳转}catch(error){alert(error.message)}
......
- 六、在路由
router
文件夹中引入AddCartSuccess组件
的路由
import AddCartSuccess from '@/pages/AddCartSuccess'
export default [
......{path: "/addcartsuccess",component: AddCartSuccess,//meta是显示底下的floor组件的meta: { show: true }},......
]
39.2 路由传递参数结合会话存储(成功路由跳转与参数传递)
addCartSuccess查看详情
- HTML5新增的功能:本地存储和会话存储。
本地存储和会话存储不可以存对象。
所以这里用sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo));
先把对象设置转为字符串,存到本地存储中。
再在所需的组件AddCartSuccess组件
里写computed
<script>
......computed:{skuInfo(){return JSON.parse(sessionStorage.getItem('SKUINFO'))}
......
</script>
在Detail组件
跳转的地方补写一下跳转路径的代码
<script>async addShopcar(){//1:发请求---将产品信息加入到数据库中(通知服务器)// this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})调用的是仓库中的addOrUpdateShopCart函数,返回得到的是promise。//2:需要知道请求是成功还是失败,若成功则路由跳转,若失败则给用户提示try{//成功await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum});//3:进行路由跳转//4:在路由跳转时将产品信息带给下一级路由组件。//一些简单的数据skuNum,通过query形式给路由组件传递;而产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据再消失)// 跳转传参方法一:用query直接存储,但是网址会显得比较乱。 this.$router.push({name:"addcartsuccess",query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})// 跳转传参方法二:用本地存储(以下便是):sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo));this.$router.push({name:"addcartsuccess",query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})}catch(error){//失败alert(error.message)}//2.若服务器存储成功---进行路由跳转传递参数//3.若失败,则给用户提示}
</script>
39.3 购物车静态组件与修改
查看购物车ShopCart
添加路由
import ShopCart from '@/pages/ShopCart'
export default [{path: "/shopcart",component: ShopCart,//meta是显示底下的floor组件的meta: { show: true }},
]
在AddCartSuccess组件中
添加路由跳转
<router-link to="/shopcart">去购物车结算</router-link>
39.4 UUID游客身份获取购物车数据
静态组件写完了,就发请求,获取购物车数据,操作vuex三连环、组件获取数据展示数据
写接口——>写仓库——>写组件
- 一、补写数据仓库
detail
import { reqCartList } from "@/api";
const state = {};
const mutations = {};
const actions = {//获取购物车列表数据async getCartList({ commit }) {let result7 = await reqCartList();//测试是否能获取个人购物车数据console.log(result7);}
};
const getters = {};export default {state,mutations,actions,getters
}
- 二、捞购物车的数据。在ShopCart组件中补:
......mounted(){this.getData();},methods:{//获取个人购物车数据getData(){this.$store.dispatch('getCartList')}}
......
- 三、现在仓库detail中写
//封装游客身份模块UUID——>生成一个随机字符串(生成之后不能再变化)
import { getUUID } from '@/utils/uuid_token'
const state = {//游客临时身份uuid_token: getUUID()
}
- 四、在
uuid_token.js文件
中用uuid随机生成
import { v4 as uuidv4 } from 'uuid';
//生成一个随机字符串,且每次执行不能发生变化,游客的身份持久保持
export const getUUID = () => {//先从本地存储获取uuid(看看本地存储中是否有)let uuid_token = localStorage.getItem('UUIDTOKEN');//如果没有if (!uuid_token) {//调用UUID的函数uuidv4()生成游客临时身份uuid_token = uuidv4();//生成之后本地存储一次localStorage.setItem('UUIDTOKEN', uuid_token)}//切记务必要有返回值,没有返回值则是undefinedreturn uuid_token;
}
- 五、在API的
request.js
中引入仓库store、并使用请求头【Request Headers】添加uuid_token
//在当前模块中引入store
import store from '@/store';//请求拦截器:可以在发请求之前,可处理一些业务。
requests.interceptors.request.use((config) => {//config:配置对象,对象中有一个属性很重要(即:headers请求头)if (store.state.detail.uuid_token) {//请求头添加一个字段(userTempId),命名是与后台商量好的config.headers.userTempId = store.state.detail.uuid_token;}nprogress.start() //③.进度条开始动return config;
})
39.5 购物车动态展示数据
后端给的购物车的动态数据格式有些不完美
- 在补写
仓库shopcart
,简化其数据
import { reqCartList } from "@/api";
const state = {cartList: []
};
const mutations = {GETCARTLIST(state, cartList) {state.cartList = cartList;}
};
const actions = {//获取购物车列表数据async getCartList({ commit }) {let result7 = await reqCartList();//测试是否能获取个人购物车数据console.log(result7);if (result7.code == 200) {commit("GETCARTLIST", result7.data)}}
};
const getters = {cartList(state) {return state.cartList[0] || {}}
};export default {state,mutations,actions,getters
}
- 修改
ShopCart组件
中
<template>
......<ul class="cart-list" v-for="(cart,index) in cartInfoList" :key="cart.id"><li class="cart-list-con1"><input type="checkbox" name="chk_list" :checked="cart.isChecked==1"></li><li class="cart-list-con2"><img :src="cart.imgUrl"><div class="item-msg">{{cart.skuName}}</div></li><li class="cart-list-con4"><span class="price">{{cart.skuPrice}}.00</span></li><li class="cart-list-con5"><a href="javascript:void(0)" class="mins">-</a><input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt"><a href="javascript:void(0)" class="plus">+</a></li><li class="cart-list-con6"><span class="sum">{{cart.skuNum * cart.skuPrice}}</span></li><li class="cart-list-con7"><a href="#none" class="sindelet">删除</a><br><a href="#none">移到收藏</a></li></ul>
......<input class="chooseAll" type="checkbox" :checked="isAllCheck">
......<i class="summoney">{{totalPrice}}</i></template>
<script>import { mapGetters } from 'vuex';
......computed:{...mapGetters(['cartList']),//购物车数据cartInfoList(){return this.cartList.cartInfoList||[];},//计算购买产品的总价totalPrice(){let sum = 0;this.cartInfoList.forEach(item=>{sum+=item.skuNum * item.skuPrice;});return sum},//判断底部复选框是否需要勾选【全部产品都勾选则勾选】isAllCheck(){//every的使用方法要记得【遍历数组里原理,只要全部元素的isChecked属性都为1,则为真,true】return this.cartInfoList.every(item=>item.isChecked==1)}}
......
</script>
39.6 处理产品数量、修改购物车产品的数量完成
处理按钮
<template>
......<li class="cart-list-con5"><a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cart)">-</a><input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt" @change="handler('change',$event.target.value * 1,cart)"><a href="javascript:void(0)" class="plus" @click="handler('add',1,cart)">+</a></li>
......
</template>
<script>
......methods:{//获取个人购物车数据getData(){this.$store.dispatch('getCartList')},async handler(type,disNum,cart){//type:为了区分这三个元素//disNum形参,是变化量: +号为变化量(1),-号为变化量(-1),input是最终的个数(不是变化量)//cart:表示是哪个产品,是身上的idconsole.log("触发了handler",type,disNum,cart)switch(type){case "add":disNum = 1;break;case "minus"://判断产品的个数大于1,才可以传递给服务器-1;若个数小于等于1,则传递给服务器个数0disNum = cart.skuNum > 1 ? -1 : 0;break;case "change"://用户输入的是最终量,若为非法的(带有汉字等),则带给服务器的变化量应该为0if(isNaN(disNum)||disNum < 1){disNum = 0;}else{//正常情况下,但用户输入的值有小数,则带给服务器的变化量应该为:用户输入进来的 - 产品的起始个数disNum = parseInt(disNum) - cart.skuNum;}//简写形式(这里不建议这样写,不好阅读): disNum = (isNaN(disNum)||disNum < 1 )? 0 : (parseInt(disNum) - cart.skuNum);break;}//派发actiontry{//代表修改成功await this.$store.dispatch("addOrUpdateShopCart",{skuId:cart.skuId,skuNum:disNum});//再一次获取服务器最新数据进行展示this.getData();}catch(error){}}},
......
</script>
39.7 修改产品个数【对函数进行节流】
这里可以用节流的方式,防止发生卡顿现象。
<script>
//第一步:
//引入方式:把lodash中的文件,按需引入功能throttle函数
//这里的throttle.js文件是默认暴露的,所以不需要加花括号
import throttle from "lodash/throttle";
......
methods:{......//第二步://修改某一个产品的个数[节流]handler: throttle(async function (type, disNum, cart) {//type:为了区分这三个元素//disNum形参,是变化量: +号为变化量(1),-号为变化量(-1),input是最终的个数(不是变化量)//cart:表示是哪个产品,是身上的id// console.log("触发了handler",type,disNum,cart)switch (type) {case "add":disNum = 1;break;case "minus"://判断产品的个数大于1,才可以传递给服务器-1;若个数小于等于1,则传递给服务器个数0disNum = cart.skuNum > 1 ? -1 : 0;break;case "change"://用户输入的是最终量,若为非法的(带有汉字等),则带给服务器的变化量应该为0if (isNaN(disNum) || disNum < 1) {disNum = 0;} else {//正常情况下,但用户输入的值有小数,则带给服务器的变化量应该为:用户输入进来的 - 产品的起始个数disNum = parseInt(disNum) - cart.skuNum;}//简写形式(这里不建议这样写,不好阅读): disNum = (isNaN(disNum)||disNum <script 1 )? 0 : (parseInt(disNum) - cart.skuNum);break;}//派发actiontry {//代表修改成功await this.$store.dispatch("addOrUpdateShopCart", {skuId: cart.skuId,skuNum: disNum,});//再一次获取服务器最新数据进行展示this.getData();} catch (error) {}}, 800),......
}
</script>
39.8 删除购物车产品的操作
写接口——>写仓库——>写组件
- 在API
的index.js中
写接口
//获取删除购物车产品的接口
// URL:/api/cart/deleteCart/{skuId} method:delete
export const reqDeleteCartById = (skuId) => requests({ url: `/cart/deleteCart/${skuId}`, method: 'delete' });
- 在
store仓库中
写仓库
const actions = {......//删除购物车某一个产品(没有返回的信息,所以三连环不需要写完整)async deleteCartListBySkuId({ commit }, skuId) {let result8 = await reqDeleteCartById(skuId);if (result8.code == 200) {return "ok";} else {return Promise.reject(new Error("faile"));}}......
}
- 在
ShopCart组件中
写组件
<template>
......<li class="cart-list-con7"><a href="#none" class="sindelet" @click="deleteCartListById(cart)">删除</a><br><a href="#none">移到收藏</a></li>
......
</template>
<script>
......methods:{async deleteCartListById(cart){try{//若删除成功则再发请求获取新的数据进行展示await this.$store.dispatch('deleteCartListBySkuId',cart.skuId);this.getData();}catch(error){alert(error.message);}}}
......
</script>
39.9 修改产品状态
写接口——>写仓库——>写组件
- 在
API的index.js中
写接口
//修改商品的选中状态
//URL: /api/cart/checkCart/{skuID}/{isChecked}
export const reqUpdateCheckedByid = (skuId, isChecked) => requests({ url: `/cart/checkCart/${skuID}/${isChecked}`, method: 'get' })
- 在
store仓库中
写仓库
//修改购物车某一个产品选中状态async updateCheckedById({ commit }, { skuId, isChecked }) {let result9 = await reqUpdateCheckedByid(skuId, isChecked)if (result9.code == 200) {return 'ok';} else {return Promise.reject(new Error('faile'))}}
- 在
ShopCart组件中
写组件
<template>
......<li class="cart-list-con1"><inputtype="checkbox"name="chk_list":checked="cart.isChecked == 1"@change = "updateChecked(cart,$event)"/></li>
......
</template>
<script>
......
methods:{async updateChecked(cart, event) {//派发actions//带给服务器的参数isChecked,不是布尔值,应该是0|1try {//若修改数据成功,则再次获取服务器数据(购物车)let isChecked = event.target.checked ? "1" : "0";await this.$store.dispatch("updateCheckedById", {skuId: cart.skuId,isChecked,});this.getData();} catch (error) {//获取失败alert(error.message);}},
}
......
</script>
四十. 删除全部选中的商品
context其实是个小仓库。
context的小仓库其中:
commit【提交mutations修改state】、
getters【计算属性】、
dispatch【派发action】、
state【当前仓库数据】
注:可通过ID删除产品的接口【一次删一个】
Promise.all([p1,p2,p3])
其中P1|P2|P3,每一个都是Promise对象。
若有一个Promise失败,则都失败,若都成功,则返回成功。
//删除全部勾选的产品deleteAllCheckedCart({ dispatch, getters }) {//context:小仓库,commit【提交mutations修改state】、getters【计算属性】、dispatch【派发action】、state【当前仓库数据】// console.log(getters.cartList.cartInfoList)//获取购物车中全部的产品(是一个数组)let PromiseAll = [];getters.cartList.cartInfoList.forEach(item => {let promise = item.isChecked == 1 ? dispatch('deleteCartListBySkuId', item.skuId) : '';//将每一次返回的Promise添加到数组中PromiseAll.push(promise);});//PromiseAll只要全部的p1|p2......都成功,则返回结果即为成功//若有一个失败,则返回即为失败结果。//返回的结果传回给ShopCart组件return Promise.all(PromiseAll)}
传回给ShopCart组件
//删除全部选中的产品async deleteAllCheckedCart(){try{//派发一个actionawait this.$store.dispatch("deleteAllCheckedCart");//再发请求获取购物车列表this.getData();}catch(error){alert(error.message);}}
四十一. "全部"产品的勾选状态修改
- 在
shopcart仓库中
//修改全部产品的状态updateAllCartIsChecked({ dispatch, state }, isChecked) {//数组let promiseAll = [];state.cartList[0].cartInfoList.forEach((item) => {let promise = dispatch('updateCheckedById', { skuId: item.skuId, isChecked });promiseAll.push(promise);});//最终返回结果return Promise.all(promiseAll);}
- 在
ShopCart组件中
<template>
......<div class="select-all"><input class="chooseAll" type="checkbox" :checked="isAllCheck&&cartInfoList.length > 0" @change="updateAllCartChecked"/><span>全选</span></div>
......
</template>
<script>
......//修改全部产品的选中状态async updateAllCartChecked(event){try{let isChecked = event.target.checked ? "1" : "0";//派发actionawait this.$store.dispatch("updateAllCartIsChecked",isChecked);this.getData();}catch(error){alert(error.message);}}
......
</script>
四十二. 登录注册静态组件【重要】
也可在css中配置别名@提示
【background-image: url(~@/assets/images/icons.png);】
42.1 注册的静态组件
- 在
API文件中
写个接口
export const reqGetCode = (phone) => requests({ url: `/api/user/passport/sendCode/${phone}`, method: "get" })
- 新建仓库
user
import { reqGetCode, reqUserRegister } from "@/api"
//登录与注册的仓库
const state = {code: ""
}
const mutations = {GETCODE(state, code) {state.code = code;}
}
const actions = {//获取验证码async getCode({ commit }, phone) {let result9 = await reqGetCode(phone)if (result9.code == 200) {commit('GETCODE', result9.data);return 'ok'} else {return Promise.reject(new Error('faile'))}},//用户注册async userRegister({ commit }, user) {let result10 = await reqUserRegister(user);// console.log(result10)if (result10.code == 200) {return 'ok'} else {return Promise.reject(new Error('fail'))}}
}
const getters = {}export default {state,mutations,actions,getters
}
- 在
Register组件中
写入
<template>
......<input type="text" placeholder="请输入你的手机号" v-model="phone">
......<input type="text" placeholder="请输入验证码" v-model="code">
......<input type="password" placeholder="请输入你的登录密码" v-model="password">
.......<input type="password" placeholder="请输入确认密码" v-model="password1">
......<input name="m1" type="checkbox" :checked="agree">
......<button @click="userRegister">完成注册</button>
......
</template>
<script>
......},methods:{//获取验证码async getCode(){//简单判断try{//若获取到验证码const {phone} =this;phone && (await this.$store.dispatch('getCode',phone));//将组件的code属性值变为仓库中的验证码// console.log(this.$store)this.code = this.$store.state.user.code}catch(error){}},//用户注册async userRegister(){try{const {phone,code,password,password1} = this;(phone&&code&&password==password1) && await this.$store.dispatch('userRegister',{phone,code,password});this.$router.push('./login')}catch(error){alert('fail')}}}
......
</script>
42.2 登录的静态组件
token(令牌):是用户唯一标识,是服务器下发的。
vuex仓库存储数据不是持久化的
- 写
API文件
//登录
// URL:/api/user/passport/login methods:post
export const reqUserLogin = (data) => requests({ url: `/user/passport/login`, data, method: "post" })
- 写仓库
user
import { reqUserLogin } from "@/api"
const state = {token: ""......
}
const mutations = {USERLOGIN(state,token) {state.token = token}......
}
const actions = {//登录页面【token】async userLogin({ commit }, data) {let result11 = await reqUserLogin(data);if (result11.code == 200) {commit("USERLOGIN", result11.data.token);return 'ok'} else {return Promise.reject(new Error('faile'));}}}
......
}
const getters = {}export default {state,mutations,actions,getters
}
- 写组件
Login
<template>
......<input type="text" placeholder="邮箱/用户名/手机号" v-model="phone">
......<input type="text" placeholder="请输入密码" v-model="password">
......<form><button class="btn" @click.prevent="userLogin">登 录</button></form>
</template>
<script>
......data() {return {phone: "",password: "",};.......methods: {//登录的回调函数async userLogin() {try {// const phone = this phone;// const password = this password;// 简写形式:const { phone, password } = this;phone && password && (await this.$store.dispatch("userLogin", { phone, password }));//跳转到home首页this.$router.push("/home")} catch (error) {alert(error.message);}},
......
</script>
42.3 携带token获取用户信息
- 写
API文件
//获取用户信息【需要带token向服务器要用户信息】
//URL: /api/user/passport/auth/getUserInfo methods:get
export const reqUserInfo = () => requests({ url: `/user/passport/auth/getUserInfo`, method: "get" })
- 写仓库
user
import { reqUserInfo } from "@/api"const mutations = {
......GETUSERINFO(state, userInfo) {state.userInfo = userInfo}
}
const actions = {
......//token获取用户信息(需要存储用户的信息,所以需要三连环)async getUserInfo({commit}) {let result12 = await reqUserInfo();if (result12.code == 200) {//提交用户信息commit("GETUSERINFO", result12.data);return 'ok'} }
}
const getters = {}export default {state,mutations,actions,getters
}
- 需要在API
文件中的request.js
请求头中补入信息
//请求拦截器:可以在发请求之前,可处理一些业务。
requests.interceptors.request.use((config) => {......//config:配置对象,对象中有一个属性很重要(即:headers请求头)//携带token给服务器if (store.state.user.token) {config.headers.token = store.state.user.token}......
})
- 补写组件Home
<script>
......
mounted() {//派发action:通过Vuex发起ajax请求,将数据存储在仓库中this.$store.dispatch("getFloorList");//获取用户信息展示this.$store.dispatch('getUserInfo');},
......
</script>
对Header组件
的展示部分进行修改
这里用是否带有用户名来判断是否展示。
<template>
......<!-- 没有用户名,则未登录 --><p v-if="!userName"><span>请</span><router-link to="./login">登录</router-link><!-- <a href="###">登录</a> --><router-link to="./register" class="register">免费注册</router-link></p><!-- 登录后 --><p v-else><a href="">{{userName}}</a><a class="register">退出登录</a></p>
......
</template>
<script>
......computed:{//用户名登录信息userName(){return this.$store。state.user.userInfo.name;}}
......
</script>
注:vuex仓库存储数据不是持久化的,所以这里的程序写完后,登录之后再刷新,用户信息丢失。
42.4 上一节登录业务存在的问题讲解(不完美的解决办法)
vuex仓库存储数据不是持久化的,所以这里的程序写完后,登录之后再刷新,用户信息丢失。
将token转存到本地存储locationstorage中进行持久化存储。
- 写法一:
补写仓库user
......
const actions = {......//登录页面【token】async userLogin({ commit }, data) {let result11 = await reqUserLogin(data);if (result11.code == 200) {commit("USERLOGIN", result11.data.token);//持久化存储token//写法一:持久化存储tokenlocalStorage.setItem("TOKEN", result11.data.token)return 'ok'} else {return Promise.reject(new Error("faile"));}}......}
......
- 写法二:
进行封装,在utils文件夹下
新建token.js
,
//存储token
export const setToken = (token) => {localStorage.setItem('TOKEN', token)
}
再在仓库user
中引用即可
import { setToken } from "@/utils/token";
......
const actions = {......//登录页面【token】async userLogin({ commit }, data) {let result11 = await reqUserLogin(data);if (result11.code == 200) {commit("USERLOGIN", result11.data.token);//持久化存储token//写法二:持久化存储tokensetToken(result11.data.token);return 'ok'} else {return Promise.reject(new Error("faile"));}}......
}
......
再继续接着进行改写
将仓库user
改写token的内容
- 写法一:
补写仓库user
......
const state = {token: localStorage.getItem('TOKEN'),
}
......
......
const actions = {......//登录页面【token】async userLogin({ commit }, data) {let result11 = await reqUserLogin(data);if (result11.code == 200) {commit("USERLOGIN", result11.data.token);//持久化存储tokensetToken(result11.data.token);return 'ok'} else {return Promise.reject(new Error("faile"));}}......
}
......
- 写法二:
进行封装,在utils文件夹下
新建token.js
//存储token
export const setToken = (token) => {localStorage.setItem('TOKEN', token)
};
//获取token
export const getToken = () => {return localStorage.getItem("TOKEN");
}
再在仓库user
中引用即可
import { setToken, getToken } from "@/utils/token";
......
const state = {token: getToken(),
}
......
......
const actions = {......//登录页面【token】async userLogin({ commit }, data) {let result11 = await reqUserLogin(data);if (result11.code == 200) {commit("USERLOGIN", result11.data.token);//持久化存储tokensetToken(result11.data.token);return 'ok'} else {return Promise.reject(new Error('faile'));}},......
}
......
42.5 退出登录
在Header组件中
写退出登录的方法
- 写API
//退出登录
//URL:/api/user/passport/logout methods:get
export const reqLogout = () => requests({ url: `/user/passport/logout`, method: "get" })
- 写
仓库user
import { reqLogout } from "@/api"
import { removeToken } from "@/utils/token";
const mutations = {//清除本地数据CLEAR(state) {//帮仓库关掉、清空用户信息state.token = '';state.userInfo = {};//本地存储清空removeToken();}
}
const actions = {//退出登录async userLogout({ commit }) {//知识向服务器发起一次请求,通知服务器清除tokenlet result13 = await reqLogout();//actions将数据提交到mutations里修改stateif (result13.code == 200) {commit("CLEAR");return 'ok';} else {return Promise.reject(new Error("faile"))}}
}
- 再补写
Header组件
<template>
......<a class="register" @click="logout">退出登录</a>
......
</template>
<script>
......
methods:{async logout(){try{//退出登录需要:1.发请求【通知服务器退出登录清除数据(例如token)】;2.清除项目中的数据【userInfo、token】await this.$store.dispatch("userLogout")//退出成功,回到首页this.$router.push("/home")}catch{}}
}
......
</script>
然而到此为止,界面还不够完善,例如:未登录时,可以直接跳转到购物车信息
四十三. 导航守卫理解
导航守卫 | Vue Router
“导航”表示路由正在发生改变。
正如其名,
vue-router
提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用
beforeRouteUpdate
的组件内守卫。
在路由router文件
中定义
//配置路由
let router = new VueRouter({//配置路由routes: routes,scrollBehavior(to, from, savedPosition) {return { y: 0 }}
});//全局守卫:前置守卫(在路由跳转之间进行判断)
router.beforeEach((to, from, next) => {//to:可获取到要跳转的那个路由信息//from:可获取到从哪个路由而来的信息//next:放行函数// 写法一:next()直接放行 写法二:next(path)放行到指定路由 写法三:next(false)若URL改变,则中断跳回from路由next();
})export default router;
仓库state
中有token,所以可以直接在router文件
中引入仓库state
,在router文件中
向state
拿到token
43.1 导航守卫的判断与操作
- 补写
router文件index.js
//配置路由
let router = new VueRouter({//配置路由routes: routes,scrollBehavior(to, from, savedPosition) {return { y: 0 }}
});//全局守卫:前置守卫(在路由跳转之间进行判断)
router.beforeEach(async(to, from, next) => {//to:可获取到要跳转的那个路由信息//from:可获取到从哪个路由而来的信息//next:放行函数// 写法一:next()直接放行 写法二:next(path)放行到指定路由 写法三:next(false)若URL改变,则中断跳回from路由let token = store.state.user.token;//如果token是'',if(token)得到也是真,此时需要判断用户信息(用户名)let name = store.state.user.userInfo.name;//用户已经登录后if (token) {//用户已经登录了就不能去登录或注册页面,只允许停留在首页if (to.path == '/login' || to.path == '/register') {next('/home')} else {//登录但是不是去/login//若已存在用户名if (name) {next();} else {//没有用户信息,则派发action让仓库存储用户信息再跳转try {//获取用户信息展示成功await store.dispatch('getUserInfo');//放行next()} catch (error) {//服务器过期了,token失效的情况下//清除token(用了退出登录那里)await store.dispatch('userLogout')next('/login');}}}} else {//未登录next();}
})export default router;
四十四. trade交易静态组件
这里需要使用老师的账号密码才能点得进有数据的trade
将ShopCart组件
中的结算业务加上点击事件,进行页面跳转
<template>
......<div class="sumbtn"><a class="sum-btn" @click="$router.push('/trade')">结算</a></div>
......
</template>
44.1. trade交易静态组件
- 依旧按步骤,先写API
- 再写
trade仓库
- 最后再在写
Trade组件
写派- 数据展示
- 依旧按步骤,先写API
// 获取用户地址信息
// URL: /api/user/userAddress/auth/findUserAddressList methods:get
export const reqAddressInfo = () => requests({ url: `/user/userAddress/auth/findUserAddressList`, method: "get" })//获取商品清单
// URL:/api/order/auth/trade methods:get
export const reqOrderInfo = () => requests({ url: `/order/auth/trade`, method: "get" })
- 再写
trade仓库
import { reqAddressInfo, reqOrderInfo } from '@/api';
const state = {address: [],orderInfo: {}
};
const mutations = {GETUSERADDRESS(state, address) {state.address = address;},GETORDERINFO(state, orderInfo) {state.orderInfo = orderInfo;}
};
const actions = {//获取用户地址信息async getUserAddress({ commit }) {let result13 = await reqAddressInfo();if (result13.code == 200) {commit('GETUSERADDRESS', result13.data);}console.log(result13);},//获取商品清单数据async getOrderInfo({ commit }) {let result15 = await reqOrderInfo();if (result15.code == 200) {commit("GETORDERINFO", result15.data);}}
};
const getters = {};
export default {state,mutations,actions,getters
}
- 最后再在写
Trade组件
写派发
.....
<script>export default {name: 'Trade',//生命周期函数:挂载完毕mounted(){this.$store.dispatch("getUserAddress")this.$store.dispatch("getOrderInfo")}}
</script>
44.2 用户地址信息的展示(排他思想)
<template>
......<h5 class="receive">收件人信息</h5><div class="address clearFix" v-for="(address,index) in addressInfo" :key="address.id"><span class="username" :class="{selected:address.isDefault==1}">{{address.consignee}}</span><p @click="changeDefault(address,addressInfo)"><span class="s1">{{address.fullAddress}}</span><span class="s2">{{address.phoneNum}}</span><span class="s3" v-show="address.isDefault==1">默认地址</span></p></div>
......<div class="receiveInfo">寄送至:<span>{{userDefaultAddress.fullAddress}}</span>收货人:<span>{{userDefaultAddress.consignee}}</span><span>{{userDefaultAddress.phoneNum}}</span></div>
</template>
<script>import {mapState} from'vuex'
......computed:{...mapState({addressInfo:state=>state.trade.address}),//将来选中的地址,提交至右下角订单,成为终选中的地址userDefaultAddress(){//find:查找数组中符合条件的元素返回return this.addressInfo.find(item=>item.isDefault==1)}},//排他思想methods:{//修改默认地址changeDefault(address,addressInfo){//全部的isDefault赋值为零addressInfo.forEach(item=>item.isDefault=0);address.isDefault = 1;}}
......
</script>
44.3 交易页面完成
- 完善
Trade组件
<template>
......<div class="detail"><h5>商品清单</h5><ul class="list clearFix" v-for="(order,index) in orderInfo.detailArrayList" :key="order.skuId"><li><img :src="order.imgUrl" style="width:100px;height:100px"></li><li><p>{{order.skuName}}</p><h4>7天无理由退货</h4></li><li><h3>¥{{order.orderPrice}}.00</h3></li><li>X{{order.skuNum}}</li><li>有货</li></ul></div>......<li><b><i>{{orderInfo.totalNum}}</i>件商品,总商品金额</b><span>¥{{orderInfo.totalAmount}}.00</span></li><li><b>返现:</b><span>0.00</span></li><li><b>运费:</b><span>0.00</span></li>......<div class="price">应付金额: <span>¥{{orderInfo.totalAmount}}.00</span></div>
......
</template>
<script>
......computed:{...mapState({orderInfo:state=>state.trade.orderInfo,}),
......
</script>
- 收集买家留言
<template>
......<div class="bbs"><h5>买家留言:</h5><textarea placeholder="建议留言前先与商家沟通确认" class="remarks-cont" v-model="msg"></textarea></div>
......
</template>
<script>
......data(){return{//收集买家留言信息msg:''}},
......
</script>
四十五. 提交订单(不用vuex)
因操作时账号有冲突,订单一直无法成功提交,所以暂且将if(result.code ==200)改成了if(result.code !==200)
进行下一个页面的学习。暂且听了听将提交订单页面、生成二维码的内容。
45.1 提交订单静态组件(不用vuex)
在router文件
中引入路由
import Pay from '@/pages/Pay'
//配置路由,路由的单词都是小写的
export default [{path: "/pay",component: Pay,//meta是显示底下的floor组件的meta: { show: true }},
]
引入接口,写API
//提交订单的接口
// URL:/api/order/auth/submitOrder?tradeNo={tradeNo} method:POST
export const reqSubmitOrder = (tradeNo, data) => requests({ url: `/order/auth/submitOrder?tradeNo=${tradeNo}`, data, method: "post" })
45.2 提交订单(没有vuex时,可以用的方法)
在main.js中
写,在main.js
这里写接口,在$bus
里写,已经将$API
挂载在Vue.prototype
的原型对象上了,就不需要其他组件一个一个引入了。
//统一接口api文件夹里全部的请求函数
import * as API from '@/api';
new Vue({render: h => h(App),//引入路由之后要注册路由:以下的写法是KV键值对,一致省略V【router应小写】//全局事件总线$bus配置beforeCreate() {Vue.prototype.$API = API;},
直接在Trade组件
中使用
<template>
......<div class="sub clearFix"><a class="subBtn" @click="submitOrder">提交订单</a></div>
......
</template>
<script>
......
data(){return{//订单号orderId:"",}
}
methods:{//提交订单async submitOrder() {let { tradeNo } = this.orderInfo;//其余六个参数let data = {consignee: this.userDefaultAddress.consignee,//最终收件人的名字consigneeTel: this.userDefaultAddress.phoneNum,//最终收件人的手机号deliveryAddress: this.userDefaultAddress.fullAddress,//收件人的地址paymentWay: "ONLINE",//支付方式orderComment: this.msg,//买家留言信息orderDetailList: this.orderInfo.deliveryAddress,//商品清单};//带参数tradeNolet result = await this.$API.reqSubmitOrder(tradeNo,data);console.log(result)},
}//带参数tradeNolet result = await this.$API.reqSubmitOrder(tradeNo,data);//提交订单成功(这里暂时写不成功)if(result.code !==200 ){this.orderId = result.data;//路由跳转+路由传参this.$router.push('/pay?orderId='+this.orderId)//提交订单失败(暂时反着写)}else{alert(result.data);}
......
</script>
【支付跳转暂时完不成,因为有账号冲突】暂且将
if(result.code !==200 )
进行下一个页面的学习。
四十六. 获取订单号与展示信息
【支付跳转暂时完不成,因为有账号冲突】暂且将
if(result.code !==200 )
进行学习。参考借鉴其他笔记进行整理。
在pay组件
中
<template>
......<span class="fl">请您在提交订单<em class="orange time">4小时</em>之内完成支付,超时订单会自动取消。订单号:<em>{{orderId}}</em></span>
......
</template>
<script>export default {name: 'Pay',computed:{orderId(){return this.$route.query.orderId;}}}
</script>
- 写接口
api
//获取支付信息接口
// URL:/api/payment/weixin/createNative/{orderId} method:get
export const reqPayInfo = () => requests({ url: `/payment/weixin/createNative/${orderId}`, method: "get" })
- 再在
Pay组件
中补写:
<template>
......<span class="fr"><em class="lead">应付金额:</em><em class="orange money">¥{{payInfo.totalFee}}</em></span>
......
</template>
<script>
......export default {......data(){return{payInfo:{}} //尽量别在生命周期函数中写async或awaitmounted(){this.getPayInfo();},methods:{async getPayInfo(){let result16 = await this.$API.reqPayInfo(this.orderId);//如果成功:组件当中存储支付信息if(result.code == 200){this.payInfo = result16.data;}// console.log(result16)}}......}
......
</script>
四十七. 支付页面中使用Element UI以及按需引入
React(Vue)组件库大概有:antd[PC、 antd-mobile[移动端]
Vue组件库大概有:ElementUI[PC]、vant[移动端]
- 一、安装Element UI
npm install --save element-ui
- 二、本次用按需引入
npm install babel-plugin-component -D
然后,将 .babelrc的文件(或babel.config.js)
修改为:
{"presets": [["es2015", { "modules": false }]],"plugins": [["component",{"libraryName": "element-ui","styleLibraryName": "theme-chalk"}]]
}
- 三、ElementUI按需引入,配置文件发生变化后,项目需重启。
- 四、按需在
main.js入口文件
引入
//按需引入Element-UI
import { Button } from 'element-ui';
//import { MessageBox } from 'element-ui';
......
//使用方法一:
Vue.component(Button.name, Button);
//使用方法二:
//ElementUI注册全局组件时,可写在原型上
//Vue.prototype.$msgbox = MessageBox;
//Vue.prototype.$alert = MessageBox.alert;
......
- 五、在
pay组件
中的写法
<template>
......<a class="btn" @click="open">立即支付</a>
......
</template>
<script>
......
methods:{//弹出框open() {this.$alert('这是一段内容', '标题名称', {dangerouslyUseHTMLString:true,//中间布局center:true,//是否显示取消按钮showCancelButton:true,//取消按钮的文本内容cancelButtonText:"支付遇见问题",//确定按钮的文本confirmButtonText:'已支付成功',//关闭右上角的XshowClose:false});}
}
......
</script>
四十八. 微信支付业务(上)
48.1 二维码生成(插件QRCODE)
生成二维码用qrcode插件
- 一、先进行安装
qrcode
插件:npm install qrcode --save
- 二、生成二维码,补写
pay组件
<script>
......//弹出框async open() {//生成二维码(地址)let url = await QRCode.toDatatURL(this.payInfo.codeUrl);this.$alert(`<img src = ${url} /`, '请使用微信支付', {this.$alert(`放置二维码的地方`, '请使用微信支付', {dangerouslyUseHTMLString:true,//中间布局center:true,//是否显示取消按钮showCancelButton:true,//取消按钮的文本内容cancelButtonText:"支付遇见问题",//确定按钮的文本confirmButtonText:'已支付成功',//关闭右上角的XshowClose:false,//beforeClose关闭前的回调,会暂停实例的关闭beforeClose:(type,instance,done) =>{//type:区分取消||确定按钮//instance:当前组件实例//done:关闭弹出框方法if(type=='cancel'){alert('请联系管理员');//清除定时器clearInterval(this.timer);this.timer = null;//关闭弹出框done();}else{//判断是否真的支付了if(this.code !== 200){//由于实操较困难,故将if(this.code == 200)改为if(this.code !== 200)clearInterval(this.timer);this.timer = null;done();this.$router.push("/paysuccess");}}}});//支付成功,路由跳转;若失败则给客户提示}
......
</script>
48.2 获取支付订单状态
获取API
//获取支付订单状态
// URL:/api/payment/weixin/queryPayStatus/{orderId} mehods:Get
export const reqPayStatus = (orderId) => requests({ url: `/payment/weixin/queryPayStatus/${orderId}`, method: 'get' });
完善Pay组件
<script>
......
data(){return{payImg:'......'//这里可以放自己的图片链接timer:null,//支付定时器code:'',//支付状态码}......
methods:{//支付成功,路由跳转;若失败则给客户提示if(!this.timer){// 定时器间歇地发送请求获取订单支付的状态this.timer = setInterval(async()=>{//发送请求获取用户支付状态let result = await this.$API.reqPayStatus(this.orderId);//如果code==200(因为支付界面的原因这里先写成code!==200)if(result.code!==200){//一、清除定时器clearInterval(this.timer);this.timer = null;//二、保存支付成功返回的code,以防用户未完成支付而点击完成支付this.code = result.code;//关闭弹出框this.$msgbox.close();//跳转到下一个路由this.$router.push('/paysuccess')}},1000)}
}
......
</script>
四十九. 个人中心——二级路由搭建
在routes.js
中注册center的路由
import Center from '@/pages/Center'
//配置路由,路由的单词都是小写的
export default [{path: "/center",component: Center,//meta是显示底下的floor组件的meta: { show: true }}
......]
在routes.js
中引入它的二级路由
import Center from '@/pages/Center'
//引入二级路由组件
//引入组件
import myOrder from '@/pages/Center/myOrder'
import groupOrder from '@/pages/Center/groupOrder'
//配置路由,路由的单词都是小写的
export default [{path: "/center",component: Center,//meta是显示底下的floor组件的meta: { show: true },//children二级路由组件children: [{path: "myorder",component: myOrder},{path: "grouporder",component: groupOrder},{path: '/center',redirect: '/center/myorder'}],},
......]
在Center组件
中写入
<template>
......<!--左侧列表--><div class="order-left"><dl><dt><i>·</i> 订单中心</dt><dd><router-link to="/center/myorder">我的订单</router-link></dd><dd><router-link to="/center/grouporder">团购订单</router-link></dd></dl></div>..........<!-- 右侧内容 --><!-- 路由组件出口的位置 --><router-view></router-view>
......
</template>
五十. 我的订单
写接口/api/order/auth/{page}/{limit}
用到了路由导航、分页器、v-for循环等知识点。
由于依旧无法成功支付,故本节课无法展示具体内容点。代码在myOrder组件
中,可自行查看。
<template><div class="order-right"><div class="order-content"><div class="title"><h3>我的订单</h3>......<div class="orders"><tableclass="order-item"v-for="(order, index) in myOrder.records":key="order.id"><thead><tr><th colspan="5"><span class="ordertitle">2017-02-11 11:59 订单编号:7867473872181848<span class="pull-right delete"><img src="../images/delete.png" /></span></span></th></tr></thead><tbody><tr v-for="(cart, index) in order.orderDetailList" :key="card.id"><td width="60%"><div class="typographic"><img :src="cart.imgUrl" style="width: 100px; height: 100px" /><a href="#" class="block-text">{{ cart.skuName }}</a><span>x{{ cart.skuNum }}</span><a href="#" class="service">售后申请</a></div></td><td:rowspan="order.orderDetailList.length"v-if="index == 0"width="8%"class="center">{{ order.consignee }}</td><td:rowspan="order.orderDetailList.length"v-if="index == 0"width="13%"class="center"><ul class="unstyled"><li>总金额¥ {{ order.totalAmount }}.00</li><li>在线支付</li></ul></td><td:rowspan="order.orderDetailList.length"v-if="index == 0"width="8%"class="center"><a href="#" class="btn">{{ order.orderStatusName }} </a></td><td:rowspan="order.orderDetailList.length"v-if="index == 0"width="13%"class="center"><ul class="unstyled"><li><a href="mycomment.html" target="_blank">评价|晒单</a></li></ul></td></tr></tbody></table></div><div class="choose-order"><Pagination:pageNo="page":pageSize="limit":total="myOrder.total":continues="5"@getPageNo="getPageNo"/></div></div>
......</div></div>
</template><script>
export default {name: "myOrder",data() {return {//初始化参数//page:当前第几页?page: 1,//limit:每一页展示数据的数量limit: 6,//存储myOrder订单的数据myOrder: {},};},mounted() {//获取我的订单的数据方法this.getData();},methods: {//获取我的订单的方法async getData() {//解构参数const { page, limit } = this;let result = await this.$API.reqMyOrderList();if (result.code == 201) {//因无法进行实践,故将if(result.code==200)改为if(result.code==201)this.myOrder = result.data;}},//获取当前点击的那一页getPageNo(page){//修改组件响应式数据pagethis.page = page;this.getData();}},
};
</script>
五十一. 未登录的导航守卫判断(前置守卫)
未登录的访问,无法访问交易相关(trade)、支付相关(pay、paysuccess)、用户中心相关(center),点击后只能跳转到登录页面。
- 所以在
router文件夹
下的index.js
对之前的内容进行修改
//全局守卫:前置守卫(在路由跳转之间进行判断)
router.beforeEach(async(to, from, next) => {if (token) {......}else {//未登录的访问,无法访问交易相关(trade)、支付相关(pay、paysuccess)、用户中心相关(center),点击后只能跳转到登录页面。let toPath = to.path;//如果toPath中带有if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {//把未登录的时候去而未去成的信息,存储于地址栏【路由】中next('/login?redirect=' + toPath);} else {//对其他的组件访问均可放行next();}}
}
- 在
Login组件
中补写:
<script>
......methods: {//登录的回调函数async userLogin() {try {// const phone = this phone;// const password = this password;// 简写形式:const { phone, password } = this;phone && password && (await this.$store.dispatch("userLogin", { phone, password }));//跳转到home首页let toPath = this.$route.query.redirect||"/home"this.$router.push(toPath)} catch (error) {alert(error.message);}},},
......
</script>
五十二. 用户登录(路由独享与组件内守卫)
全局前置守卫(常用)、全局解析守卫、全局后置钩子、路由独享守卫(常用)、组件内守卫
52.1 路由独享守卫
在路由文件routes.js
文件中写:
export default [......{path: "/pay",component: Pay,//meta是显示底下的floor组件的meta: { show: true },//路由独享守卫beforeEnter: (to, from, next) => {if (from.path == "/trade") {next();} else {next(false);}}},{path: "/trade",component: Trade,//meta是显示底下的floor组件的meta: { show: true },//路由独享守卫beforeEnter: (to, from, next) => {if (from.path == "/shopcart") {next();} else {next(false);}}},......
]
52.2 组件内守卫(用得不多)
本节在paysuccess组件
中书写使用
<script>
......
export default {name: 'PaySuccess',//组件内守卫beforeRouteEnter(to, from, next) {// 在渲染该组件的对应路由被 confirm 前调用// 不!能!获取组件实例 `this`// 因为当守卫执行前,组件实例还没被创建// 如果是从pay路由来,那么就放行,否则就在原地if(from.path == "/pay"){next();}else{next(false)}},beforeRouteUpdate(to, from, next) {// 在当前路由改变,但是该组件被复用时调用// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。// 可以访问组件实例 `this`console.log("beforeRouteUpdate")},beforeRouteLeave(to, from, next) {// 导航离开该组件的对应路由时调用// 可以访问组件实例 `this`// 一般情况直接放行就行了next();}
}
......
</script>
五十三. 图片懒加载(插件:vue-lazyload)
图片懒加载的插件:
vue-lazyload - npm
- 第一步:安装插件
npm i vue-lazyload -S
- 第二步:在入口文件
main.js
中引入
//引入插件
import VueLazyload from 'vue-lazyload'
//引入需使用的图片
import green from '@/assets/green.gif'
//注册插件
Vue.use(VueLazyload, {//懒加载默认的图片loading: green})
- 第三步:使用该指令(找到需要使用图片懒加载的图片,进行替换)
本节由Search组件
使用,
将<img :src="good.defaultImg"/>
替换成<img v-lazy="good.defaultImg"/>
可以了解一下jquery的懒加载自定义插件
五十四. 表单验证(插件:vee-validate)【了解即可,看懂就行】
- 第一步:先安装2版本的:
npm i vee-validate@2 --save
- 第二步:引用表单校验插件
由于入口文件main.js
里面的内容太多了,新建一个文件写validata.js
插件:
//vee-validate插件:表单验证区域
import Vue from 'vue';
import VeeValidate from 'vee-validate';//使用插件
Vue.use(VeeValidate)//中文提示信息
import zh_CN from 'vee-validate/dist/locale/zh_CN'//表单验证
VeeValidate.Validator.localize('zh_CN', {messages: {...zh_CN.messages,//转成中文is: (field) => `${field}必须与密码相同` //修改内容规则的message,让确认密码和密码相同},attributes: {phone: '手机号',code: '验证码',password: '密码',password1: '确认密码',agree: '协议'},
});//自定义校验规则
VeeValidate.Validator.extend("tongyi", {validate: (value) => {return value;},getMessage: (field) => field + '必须同意'
})
接着在入口文件main.js
引入
//引入表单校验插件
import "@/plugins/validate"
- 第三步:基本使用
在Register组件
中使用:
<template>
......<div class="content"><label>手机号:</label><inputplaceholder="请输入手机号"v-model="phone"name="phone"v-validate="{ required: true, regex: /^1\d{10}$/ }":class="{ invalid: errors.has('phone') }"/><span class="error-msg">{{ errors.first("phone") }}</span></div><div class="content"><label>验证码:</label><inputplaceholder="请输入验证码"v-model="code"name="code"v-validate="{ required: true, regex: /^\d{6}$/ }":class="{ invalid: errors.has('code') }"/><button style="width: 100px; height: 38px" @click="getCode">获取验证码</button><span class="error-msg">{{ errors.first("code") }}</span></div><div class="content"><label>登录密码:</label><inputplaceholder="请输入密码"v-model="password"name="password"v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }":class="{ invalid: errors.has('password') }"/><span class="error-msg">{{ errors.first("password") }}</span></div><div class="content"><label>确认密码:</label><inputplaceholder="请再一次输入密码"v-model="password1"name="password1"v-validate="{ required: true, is: password }":class="{ invalid: errors.has('password1') }"/><span class="error-msg">{{ errors.first("password1") }}</span></div><div class="controls"><inputtype="checkbox"v-model="agree"name="agree"v-validate="{ required: true, 'tongyi':true }":class="{ invalid: errors.has('agree') }"/><span>同意协议并注册《尚品汇用户协议》</span><span class="error-msg">{{ errors.first("agree") }}</span> </div>
......
</template>
<script>
......//用户注册完善、修改async userRegister() {const success = await this.$validator.validateAll();//全部表单验证成功再像服务器发请求进行注册,若有一个表单没有成功则都不会发请求if (success) {try {const { phone, code, password, password1 } = this;await this.$store.dispatch("userRegister", {phone,code,password,});//注册成功进行路由跳转this.$router.push("./login");} catch (error) {alert("fail");}}},
......
</script>
五十五. 路由的懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。类似于按需引入。
路由懒加载优点:高效
路由懒加载 | Vue Router
官方写法:
const Foo = () => import('./Foo.vue')
const router = new VueRouter({routes: [{ path: '/foo', component: Foo }]
})
- 按照官方写法在
router.js
中完整写法:
......
const Foo = () => {return import ("@/pages/Home/index.vue")
}
......
export default [
{path: "/home",component: Foo,meta: { show: true }},
]
- 将上里面的代码简写的过程如下:
const Foo = () => {return import ("@/pages/Home/index.vue")
}
//可以简写为:
const Foo = () => return import ("@/pages/Home/index.vue")
//简化替换,可将下方的component组件简写为:
export default [
{path: "/home",component: () => return import ("@/pages/Home/index.vue")meta: { show: true }},
]
- 最终简写可直接简写得:
......
export default [
{path: "/home",component: () => return import("@/pages/Home/index.vue")//箭头函数返回Promise,只有在该组件被调用时才会使用。meta: { show: true }},
]
......
五十六. 处理map文件、打包上线
使用npm run build
将项目打包上线。打包上线后的代码都是经过压缩加密的,若运行报错则无法准确得知是哪里的代码报错。打包上线后之后,得到dist文件夹
,dist文件夹
里的js文件夹
里有.map结尾的文件。
.map文件可以像未加密的代码一样,准确输出是哪一行哪一列有错误。
所以如果项目不需要,.map文件
是可以手动删除的。
也可以在项目打包上线之前,在配置文件vue.config.js文件
里加上productionSourceMap:false
这一行代码,打包之后就不会出现.map文件,项目的体积就会缩小一些。
vue.config.js文件
:
module.exports = {productionSourceMap: false,//关闭eslintlintOnSave: false,//代理跨域devServer: {proxy: {'/api': {target: 'http://39.98.123.211',// pathRewrite: { '^/api': '' }, //因为本次项目的接口都已经带/api了,所以这里的路径可以不用写。},},},
}
五十七. 购买服务器等操作(先了解)
暂未购买,先进行了解
视频中的老师建议腾讯云(因为便宜),目前有活动——星星海 云服务器
57.1 利用xshell工具登录服务器
linux:/根目录
linux常用指令:cd 跳转目录、ls查看、mkdir创建目录、pwd:查看绝对路径。
57.2 nginx反向代理
nginx配置
1.xshell进入根目录/etc
2.进入etc目录,若这个目录下有一个nginx目录,进到这个目录【需提前安装好nginx】
在etc文件下配置nginx
3.安装nginx:yum install nginx
4.安装完nginx服务器后,在nginx目录下,多了一个nginx.conf文件
,在这个文件中进行配置。
5.vim nginx.conf进行编辑,主要添加如下俩:
- 解决第一个问题:为什么访问服务器IP地址即可访问到自己的项目?——在服务器上=>/root/jch/www/shangpinhui/dist;,需要进行一些配置。
location/{
root /root/jch/www/shangpinhui/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
- 解决第二个问题:项目的数据来自于http://39.98.123.211?
location /api{
proxy_pass http://39.98.123.211;
}
6,nginx服务器跑起来:service nginx start
这篇关于笔记整理——Vue2项目尚品汇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!