当前位置:   article > 正文

尚品汇---笔记与思考_没有定义requpdatecheckedbyid 在store.updatecheckedbyid

没有定义requpdatecheckedbyid 在store.updatecheckedbyid

day1

1.1 项目结构介绍

node_modules文件夹:项目依赖文件夹

public文件夹:一般放置一些静态资源(图片),需要注意的是:放在public文件夹中的静态资源,webpack进行打包的时候会原封不动的打包到dist文件夹中

src文件夹(程序员源代码文件夹):

        assets文件夹:一般也是放置静态资源(一般放置多个组件共用的静态资源),需要注意:放置在assets文件夹里面的静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包到JS文件中

        components文件夹:一般放置的是非路由组件(全局组件)

        App.vue:唯一的根组件,Vue当中的组件(.vue)

babel.config.js:配置文件(babel相关)

package.json文件:认为是项目的“身份证”,记录项目叫做什么、项目中有哪些依赖、项目怎么运行

package-lock.json:缓存性文件

README.md:说明性文件

1.2 项目配置

项目运行起来的时候,让浏览器自动打开:

找到package.json文件

修改scripts配置项的serve命令(添加--open)

  1. "scripts": {
  2. "serve": "vue-cli-service serve --open",
  3. "build": "vue-cli-service build",
  4. "lint": "vue-cli-service lint"
  5. }

关闭eslint校验:

        添加下列语句即可

  lintOnSave:false

src文件夹简写方法:配置别名 @(在jsconfig.json文件夹中)

webstorm里自带下列代码,有需要的朋友可以复制一下放到jsonconfig.json中

@代表的是src文件夹,这样将来即使文件过多,找的时候也很方便

  1. "compilerOptions": {
  2. "target": "es5",
  3. "module": "esnext",
  4. "baseUrl": "./",
  5. "moduleResolution": "node",
  6. "paths": {
  7. "@/*": [
  8. "src/*"
  9. ]
  10. }

1.3 路由分析

项目路由的分析:vue-router

前端所谓路由:KV键值对

key:URL(地址栏中的路径)

value:相应的路由组件

项目为上中下结构

路由组件:Home首页路由组件、Search路由组件、Login登录组件、Register注册组件

非路由组件:Header头部组件(首页、搜索页)、Footer尾部组件(在首页、搜索页有,但是在登录页面是没有的)

开发项目的顺序:

        1.书写静态页面(HTML+CSS)

        2.拆分组件

        3.获取服务器的数据动态展示

        4.完成相应的动态业务逻辑

路由组件的搭建

下载vue-router:注意vue2对应vue-router@3,vue3对应vue-router@4

由上述分析,路由组件应该有四个:Home、Search、Login、Register

components文件夹:经常放置的是非路由组件(共用全局组件)

pages|views文件夹:经常放置路由组件

        配置路由:项目中配置的路由一般放置在router文件夹中

总结:

路由组件与非路由组件的区别:

        1.路由组件一般放置在pages|views文件夹下,非路由组件一般放置在components文件夹中

        2.路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用

        3.注册完路由,不管路由组件、还是非路由组件,身上都有$route、$router属性

        $route:一般用于获取路由信息(路径、query、params等等)

        $router:一般进行编程式导航,进行路由跳转(push、replace)

路由的跳转:

        路由的跳转有两种形式:

                声明式导航router-link,可以进行路由的跳转

                编程式导航push|replace,可以进行路由跳转

        编程式导航:

                声明式导航能做的,编程式导航都能做

                但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务处理

  1. //搜索按钮的回调函数,需要向search路由进行跳转
  2. goSearch(){
  3. this.$router.push('/search')
  4. }

        声明式导航:

<router-link to="/login">登录</router-link>

1.4 Footer组件的显示与隐藏

显示或者隐藏组件:v-if|v-show

Footer组件:在Home、Search显示Footer组件

Footer组件:在登录、注册的时候隐藏

  <Footer v-show="$route.path=='/home'||$route.path=='/search'"/>

我们可以根据组件身上的$route获取当前路由信息,通过路由路径判断Footer的显示与隐藏

在配置路由的时候,可以给路由添加路由元信息[meta]

  1. routes:[
  2. {
  3. path:'/home',
  4. component:Home,
  5. meta:{show:true}
  6. },
  7. {
  8. path:"/search",
  9. component:Search,
  10. meta:{show:true}
  11. },
  12. {
  13. path:"/login",
  14. component:Login,
  15. meta:{show:false}
  16. },
  17. {
  18. path:"/register",
  19. component:Register,
  20. meta:{show:false}
  21. },
  22. //重定向:在项目跑起来的时候,访问"/",立马让它定向到首页
  23. {
  24. path:'*',
  25. redirect:"/home"
  26. }
  27. ]
  <Footer v-show="$route.meta.show"/>

1.5 路由传参

路由跳转有几种方式?

比如:A->B

声明式导航:router-link(务必要有to属性),可以实现路由的跳转

编程式导航:利用的是组件实例的$router.push|$router.replace方法,可以实现路由的跳转(可以书写一些自己的业务)

路由传参,参数有几种写法?

params参数:属于路径当中的一部分。需要注意,在配置路由的时候,需要占位

  1. {
  2. path:"/search/:keyword",
  3. component:Search,
  4. meta:{show:true}
  5. }

query参数:不属于路径当中的一部分,类似于ajax中的queryString   /home?k=v&k=v,不需要占位

路由传递参数:

字符串形式:

      this.$router.push('/search/'+this.keyword+"?k="+this.keyword.toUpperCase())

模板字符串形式:

      this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)

对象的形式:

注意:使用对象的形式时,需要给要跳转的路由命名,而不能使用路径来跳转

  1. this.$router.push({
  2. name:'search',
  3. params:{
  4. keyword:this.keyword
  5. },
  6. query:{
  7. k:this.keyword.toUpperCase()
  8. }
  9. })

1.6 面试题

面试题1:路由传递参数(对象写法)path是否可以结合params参数一起使用?

不能,路由跳转传参的时候,对象的写法可以是name、path形式,但需要注意的是,path这种写法不能和params参数一起写

面试题2:如何指定params参数可传可不传?

如果路由要求传递params参数,但你不传,那么url会出现问题

如果想要指定params参数可传可不传,需要在配置路由的时候在占位的后面加上一个问号

  1. {
  2. name:'search',
  3. path:"/search/:keyword?",
  4. component:Search,
  5. meta:{show:true}
  6. }

面试题3:params参数可以传递也可以不传递,但如果传递的是空串,如何解决?

使用undefined解决:params参数可以传递、不传递或传递空的字符串

  1. this.$router.push({
  2. name:'search',
  3. params:{keyword:""||undefined},
  4. query:{k:this.keyword.toUpperCase()}
  5. })

面试题4:路由组件能不能传递props数据?

布尔值写法:只能传递params参数

        props:true

对象写法:额外的给路由组件传递一些props

        props:{a:1,b:2}

函数写法:可以params参数、query参数,通过props传递给路由组件

        props:($route)=>{

                return {keyword:$route.params.keyword,k:$route.query.k}

        }

day2

2.1 重写push和replace方法

编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误

 首先,底层push方法会返回一个Promise对象

  1. function push(){
  2. return new Promise((resolve,reject)=>{
  3. })
  4. }

解决方法:

1.通过给push方法传递相应的成功、失败的回调函数,可以捕获到当前的错误并解决

这种写法治标不治本,将来在别的组件当中push|replace,编程式导航还有类似错误

  1. this.$router.push({
  2. name:'search',
  3. params:{keyword:this.keyword},
  4. query:{k:this.keyword.toUpperCase()},
  5. },()=>{},()=>{})

2.

this:当前组件实例(search)

this.$router属性:当前的这个属性,属性值VueRouter类的一个实例,当在入口文件注册路由的时候,给组件实例添加$router|$route属性

  1. function VueRouter(){
  2. }
  3. //原型对象的方法
  4. VueRouter.prototype.push=function(){
  5. //函数的上下文为VueRouter类的一个实例
  6. }
  7. let $router=new VueRouter()
  8. $router.push(xxx)

重写方法:

  1. //先把VueRouter原型对象的push保存一份
  2. let originPush=VueRouter.prototype.push
  3. //重写push|replace
  4. //第一个参数:告诉原来的push方法,你往哪里跳转(传递哪些参数
  5. VueRouter.prototype.push=function(location,resolve,reject){
  6. if(resolve&&reject){
  7. //call|apply区别
  8. //相同点:都可以调用函数依次,都可以篡改函数的上下文一次
  9. //不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行传递数组
  10. originPush.call(this,location,resolve,reject)
  11. }else{
  12. originPush.call(this,location,()=>{},()=>{})
  13. }
  14. }

2.2 三级联动组件

由于三级联动,在Home、Search、Detail中都有使用,所以把三级联动注册为全局组件

好处:只需要注册一次,就可以在项目的任何地方使用

全局组件的注册:

        第一个参数:全局组件的名字

        第二个参数:指定是哪一个组件

Vue.component(TypeNav.name,TypeNav)

2.3 axios的二次封装

为什么要对axios进行二次封装?

请求拦截器、响应拦截器:请求拦截器,可以在发请求之前处理一些业务

                                           响应拦截器,当服务器数据返回以后,可以处理一些事情

在项目中API文件夹常用来放置axios相应文件

接口当中,路径都带有/api

        ==>baseURL:"/api"

  1. import axios from "axios";
  2. const requests = axios.create({
  3. baseURL:"/api",
  4. timeout:5000
  5. })
  6. //请求拦截器
  7. requests.interceptors.request.use((config)=>{
  8. //config是headers的请求头
  9. return config
  10. })
  11. //响应拦截器
  12. requests.interceptors.response.use((res)=>{
  13. //成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,并做一些处理
  14. return res.data
  15. },
  16. (error)=>{
  17. //响应失败的回调函数
  18. return Promise.reject(new Error(error))
  19. })
  20. export default requests

2.4 跨域问题

什么是跨域?

        协议、域名、端口号不同的请求,称之为跨域

http://localhost:8080/#/home        ---前端项目本地服务器

http://39.98.123.211        ---后台服务器

  1. //配置代理跨域
  2. devServer:{
  3. proxy:{
  4. "/api":{
  5. target:"http://gmall-h5-api.atguigu.cn",
  6. }
  7. }
  8. }

2.5 nprogress进度条的使用

  1. //引入进度条
  2. import nprogress from 'nprogress'
  3. //引入进度条的样式
  4. import 'nprogress/nprogress.css'

进度条的开始:

    nprogress.start()

进度条的结束:

    nprogress.end()

2.6 vuex状态管理库

vuex是什么?

        vuex是官方提供的一个插件,状态管理库,集中式管理项目中共用的数据

2.7 mapState

mapState传入参数是一个对象,对象的键值对中,值是一个函数。当时用这个计算属性的时候,右侧的函数会立即执行一次

函数注入一个参数为state,即为大仓库中的数据

  1. ...mapState({
  2. categoryList:(state)=>{
  3. return state.typeNav.categoryList
  4. }
  5. })

2.8 三级联动动态背景颜色改变

动态添加类:

  1. <div class="item"
  2. v-for="(c1,index) in categoryList"
  3. :key="c1.categoryId"
  4. :class="{cur:currentIndex===index}"
  5. >

currentIndex的改变,初始值为-1:

  1. changeIndex(index){
  2. this.currentIndex=index
  3. },
  4. leaveIndex(){
  5. this.currentIndex=-1
  6. }

类:

  1. .cur{
  2. background-color: skyblue;
  3. }

2.9 通过JS控制二三级分类的显示与隐藏

  1. <div class="item-list clearfix"
  2. :style="{display:currentIndex===index?'block':'none'}">

2.10 演示卡顿现象并引入防抖和节流

卡顿现象:事情触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很有可能出现浏览器卡顿)

函数的节流与防抖:

节流:在规定的间隔时间范围内不会重复处罚回调,只有大于这个时间才会触发回调,把频繁触发变为少量触发

防抖:前面所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,则只会执行一次

插件lodash:封装了函数的防抖与节流业务

三级联动节流:

引入throttle函数:

import throttle from 'lodash/throttle'

使用节流函数throttle:

  1. changeIndex:throttle(function(index){
  2. this.currentIndex=index
  3. },50),

day3

3.1 三级联动组件的路由跳转与参数传递

三级联动用户可以点击:一级分类、二级分类、三级分类

当你点击的时候,Home模块跳转到Search模块,一级会把用户选中的产品(产品的名字、产品的ID)在路由跳转的时候,进行传递

router-link是一个组件,当服务器的数据返回值后,会循环出很多的router-link组件实例

创建组件实例的时候,一瞬间创建1000+组件实例是很耗用内存的,因此可能出现卡顿现象

最好的解决方案:编程式导航+事件委派 实现路由的跳转和参数的传递

利用事件委派存在的一些问题:

        1.点击的不一定是a标签

                事件委派是把所有的子节点(h3/dt/dl/em)的时间委派给父亲结点。而只有点击a标签的时候,才会进行路由跳转,但如何确定点击的一定是a标签呢?

                即使你能确定点击的是a标签,如何区分是一级、二级还是三级分类的a标签呢?

        2.如何获取参数【1/2/3级分类的产品名字、id】

解决方案:

        1.把子节点当中的a标签加上自定义属性data-categoryName,而其他的子节点是没有的

<a :data-categoryName="c1.categoryName">{{c1.categoryName}}</a>

        2.给每一个a标签添加上对应的data-category1Id/data-category2Id/data-category3Id,用来区分不同级别的分类

总的解决方案:

  1. goSearch(event){
  2. let {categoryname,category1id,category2id,category3id} = event.target.dataset
  3. if(categoryname){
  4. //整理路由跳转的参数
  5. let location={name:'search'}
  6. let query={categoryName:categoryname}
  7. if(category1id){
  8. query.categoryName=category1id
  9. }else if(category2id){
  10. query.categoryName=category2id
  11. }else{
  12. query.categoryName=category3id
  13. }
  14. //整理参数
  15. location.query=query
  16. //路由跳转
  17. this.$router.push(location)
  18. }
  19. }

day4

4.1 三级联动效果在不同路由组件中的显示情况

需求:

在home组件下,会默认显示二级菜单和三级菜单

但search组件下,只默认显示一级菜单

解决方案:

通过v-show动态控制二级菜单和三级菜单的显示与隐藏(v-show="show")

通过路由控制,决定show属性的true/false

        原理:每跳转到一个组件,如果有TypeNav,都会重新创建一个TypeNav的实例,也就是说mounted函数会再次执行。可以将路由判断的逻辑写在mounted钩子里

  1. mounted(){
  2. //通知Vuex发送请求,获取数据,存储于仓库之中
  3. this.$store.dispatch('categoryList')
  4. if(this.$route.path!='/home'){
  5. this.show=false
  6. }
  7. }

在鼠标滑过“全部商品分类”时,应该有对应的二级分类、三级分类的展示

        对于home组件,鼠标离开时二级分类不会消失

        对于search组件,鼠标离开时只展示一级分类

逻辑判断:

TypeNav模块:

      <div  @mouseleave="leaveShow" @mouseenter="enterShow">
  1. enterShow(){
  2. this.show=true
  3. },
  4. leaveShow(){
  5. this.currentIndex=-1
  6. if(this.$route.path!='/home'){
  7. this.show=false
  8. }
  9. }

4.2 search模块中三级路由组件的过渡效果

过渡动画:前提是组件/元素务必要有v-if/v-show指令,才可以进行过渡动画

  1. <!--过渡动画-->
  2. <transition name="sort">
  3. <div class="sort" v-show="show">
  4. ……中间内容省略
  5. </div>
  6. </transition>
  1. //过渡动画的样式
  2. .sort-enter{
  3. height:0;
  4. }
  5. .sort-enter-to{
  6. height:461px;
  7. }
  8. .sort-enter-active{
  9. transition:all .7s linear
  10. }

4.3 TypeNav商品分类列表的优化

由于每次home/search组件的销毁和挂载都会导致TypeNav的销毁和挂载,即都会执行mounted函数,从而频繁地向服务器发送请求。但是三级联动组件获取到的内容是不变的,所以可以做优化

可以将三级联动的数据请求放在App.vue的钩子函数中:

  1. mounted(){
  2. //通知Vuex发送请求,获取数据,存储于仓库之中
  3. this.$store.dispatch('categoryList')
  4. }

4.4 合并参数

捎带params参数:

  1. //判断:如果路由跳转的时候带有params参数,也要捎带传递过去
  2. if(this.$route.params){
  3. location.params=this.$route.params
  4. }

合并参数:

  1. //搜索按钮的回调函数,需要向search路由进行跳转
  2. goSearch(){
  3. let location={
  4. name:'search',
  5. params:{keyword:this.keyword||undefined}
  6. }
  7. if(this.$route.query){
  8. location.query=this.$route.query
  9. }
  10. this.$router.push(location)
  11. }

4.5 mockjs模拟数据

开发Home首页当中的ListContainer组件与Floor组件:

        因为服务器返回的数据只有商品分类和菜单分类的数据,对于ListContainer组件与Floor组件数据服务器是没有提供的。可以通过mockjs模拟

使用步骤:

1)在项目当中src文件夹中创建mock文件夹

2)第二步准备JSON数据(mock文件夹中创建相应的JSON文件)

3)把mock数据需要的图片放置到public文件夹中(public文件夹在打包的时候,会把相应资源原封不动地放在dist文件夹下)

4)创建mockServer.js,开始实现mock虚拟数据

5)把mockServer.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)

问:JSON数据格式文件根本没有对外暴露,为什么可以直接引入?

答:webpack默认对外暴露图片和JSON数据格式文件

Mock.mock()函数有两个参数

        第一个参数是请求的地址

        第二个参数是请求的数据

  1. //先引入mockjs模板
  2. import Mock from 'mockjs'
  3. //把JSON数据格式引入进来
  4. import banner from './banner.json'
  5. import floor from './floor.json'
  6. Mock.mock("/mock/banner",{
  7. code:200,
  8. data:banner
  9. })
  10. Mock.mock("/mock/floor",{
  11. code:200,
  12. data:floor
  13. })

封装mockServer.js

  1. import axios from 'axios'
  2. import nprogress from 'nprogress'
  3. import 'nprogress/nprogress.css'
  4. let requests=axios.create({
  5. baseURL:"/mock",
  6. timeout:5000
  7. })
  8. requests.interceptors.request.use((config)=>{
  9. nprogress.start()
  10. return config
  11. })
  12. requests.interceptors.response.use(
  13. (res)=>{
  14. nprogress.done()
  15. return res.data
  16. },
  17. (err)=>{
  18. return Promise.reject(new Error(err))
  19. }
  20. )
  21. export default requests

发送请求的函数:

  1. //获取banner(Home首页轮播图接口)
  2. export const reqGetBannerList=()=>mockRequest.get('/banner')
  3. //获取floor
  4. export const reqGetFloor=()=>mockRequest.get('/floor')

获取banner轮播图的数据:

vuex处理:

  1. import { reqGetBannerList} from "@/api";
  2. const state={
  3. bannerList:[]
  4. }
  5. const mutations={
  6. GETBANNERLIST(state,bannerList){
  7. state.bannerList=bannerList
  8. }
  9. }
  10. const actions={
  11. async getBannerList({commit}){
  12. let result=await reqGetBannerList()
  13. if(result.code===200){
  14. commit('GETBANNERLIST')
  15. }
  16. }
  17. }
  18. const getters={}
  19. export default{
  20. state,mutations,actions,getters
  21. }

获取数据:

  1. computed:{
  2. ...mapState({
  3. bannerList:state=>state.home.bannerList
  4. })
  5. }

4.6 swiper

第一步:引包(引入相应的JS/CSS)

在main.js引入一次即可:

  1. import 'swiper/css/swiper.css'
  2. import 'swiper/js/swiper'

第二步:页面中的结构

  1. <div class="swiper-slide"
  2. v-for="(carousel,index) in bannerList" :key="carousel.id"
  3. >
  4. <img :src="carousel.imgUrl"/>
  5. </div>

第三步:new Swiper实例,给轮播图添加动态效果

  1. new Swiper(document.querySelector('.swiper-container'),{
  2. loop:true,
  3. pagination:{
  4. el:'.swiper-pagination',
  5. //点击小球的时候也切换图片
  6. clickable:true
  7. },
  8. //前进后退的按钮
  9. navigation:{
  10. nextEl:".swiper-button-next",
  11. prevEl:".swiper-button-prev"
  12. }
  13. })

安装swiper5版本比较稳定

4.7 Banner轮播图的实现

首先需要明确,如果直接将swiper实例写在mounted函数中,尽管mounted是在页面结构加载完成后执行,但此时从服务器拿到的数据还没有放到仓库中,故swiper实例中拿不到bannerList的数据

解决方式一:

可以通过setTimeout等待一段时间,这段时间里从服务器拿到的数据就已经放到仓库中了

  1. setTimeout({
  2. //swiper实例的实现
  3. },2000)

解决方式二:

watch+nextTick

watch:数据监听,监听已有数据的变化

        监听bannerList数据的变化:从空数组变为数组里有四个元素

        如果执行handler方法,代表组件实例身上的这个属性的属性值已经有了

但是仅有watch是不够的,因为数据更新了,但是页面渲染可能还没结束

nextTick:

        Vue.nextTick([callback,context])

        用法:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立刻使用这个方法,获取更新后的DOM

        $nextTick可以保证页面中的结构一定是有的,经常和很多需要DOM已经存在才能实现功能的插件一起使用

watch+nextTick实现功能:

  1. watch:{
  2. bannerList:{
  3. handler(newValue,oldValue){
  4. this.$nextTick(()=>{
  5. var mySwiper=new Swiper(document.querySelector('.swiper-container'),{
  6. loop:true,
  7. pagination:{
  8. el:'.swiper-pagination',
  9. //点击小球的时候也切换图片
  10. clickable:true
  11. },
  12. //前进后退的按钮
  13. navigation:{
  14. nextEl:".swiper-button-next",
  15. prevEl:".swiper-button-prev"
  16. }
  17. })
  18. })
  19. }
  20. }
  21. }

4.8 通过ref获取要操作的DOM

        <div class="swiper-container" ref="mySwiper">
          new Swiper(this.$refs.mySwiper,{

day5

5.1 开发floor组件

由于Floor组件在Home中被调用两次,如果是在Floor组件中触发getFloorList,很难实现两次调用数据不同

        故应该在Home组件中触发

v-for可以在自定义组件上使用

组件间通信的方式有哪些?

props:用于父子组件通信

自定义事件:@on 与 @emit

全局事件总线:$bus 全能

pubsub-js:vue中几乎不用(react使用的较多)

插槽

vuex

List组件中的轮播图,是当前组件内部发送请求、动态渲染解构服务器返回的数据,因此必须使用watch+nextTick

Floor组件中的轮播图,请求由父组件Home发送,并且数据是父组件发送过来的,此时结构已经完全解析好了,所以可以在mounted里写轮播图实例

  1. mounted(){
  2. new Swiper(this.$refs.cur,{
  3. loop:true,
  4. pagination:{
  5. el:'.swiper-pagination',
  6. //点击小球的时候也切换图片
  7. clickable:true
  8. },
  9. //前进后退的按钮
  10. navigation:{
  11. nextEl:".swiper-button-next",
  12. prevEl:".swiper-button-prev"
  13. }
  14. })
  15. }

5.2 共用组件Carousel

切记:以后在开发项目的时候,如果看到某一个组件在很多地方都使用,可以把它变成全局组件

  1. <template>
  2. <div class="swiper-container" ref="cur">
  3. <div class="swiper-wrapper">
  4. <div class="swiper-slide"
  5. v-for="(carousel,index) in list" :key="carousel.id"
  6. >
  7. <img :src="carousel.imgUrl">
  8. </div>
  9. </div>
  10. <!-- 如果需要分页器 -->
  11. <div class="swiper-pagination"></div>
  12. <!-- 如果需要导航按钮 -->
  13. <div class="swiper-button-prev"></div>
  14. <div class="swiper-button-next"></div>
  15. </div>
  16. </template>
  17. <script>
  18. import Swiper from "swiper";
  19. export default {
  20. name: "Carousel",
  21. props:['list'],
  22. watch:{
  23. list:{
  24. immediate:true,
  25. handler(){
  26. this.$nextTick(()=>{
  27. new Swiper(this.$refs.cur,{
  28. loop:true,
  29. autoplay:true,
  30. pagination:{
  31. el:'.swiper-pagination',
  32. //点击小球的时候也切换图片
  33. clickable:true
  34. },
  35. //前进后退的按钮
  36. navigation:{
  37. nextEl:".swiper-button-next",
  38. prevEl:".swiper-button-prev"
  39. }
  40. })
  41. })
  42. }
  43. }
  44. }
  45. }
  46. </script>
  47. <style scoped>
  48. </style>

5.3 search仓库(主讲getters)

getters,相当于计算属性

项目中getters的主要作用:简化仓库中的数据(简化数据而生)

可以把我们将来在组件当中需要用的数据简化一下【将来组件获取数据就简单多了】

数据的返回:

以goodsList为例,如果服务器数据回来了,返回一个数组

如果网络不给力/没有网,state.searchList.goodsList返回的就是undefined

  1. const getters={
  2. goodsList(state){
  3. return state.searchList.goodsList||[]
  4. },
  5. trademarkList(state){
  6. return state.searchList.trademarkList||[]
  7. },
  8. attrsList(state){
  9. return state.searchList.attrsList||[]
  10. }
  11. }

与getters对应的是mapGetters,此函数接收一个数组,不像state那样划分模块

  1. computed:{
  2. ...mapGetters(['goodsList'])
  3. }

5.4 Search中的数据处理

使用Object.assign方法,快速合并数据

在beforeMount里整理数据,在mounted发送请求

  1. export default {
  2. name: 'Search',
  3. components: {
  4. SearchSelector
  5. },
  6. data(){
  7. return {
  8. //带给服务器的参数
  9. searchParams:{
  10. category1Id:"",
  11. category2Id:"",
  12. category3Id:"",
  13. categoryName:"",
  14. keyword:"",
  15. order:"",
  16. pageNo:1,
  17. pageSize:3,
  18. props:[],
  19. trademark:""
  20. }
  21. }
  22. },
  23. beforeMount(){
  24. Object.assign(this.searchParams,this.$route.query,this.$route.params)
  25. },
  26. mounted(){
  27. this.getData()
  28. },
  29. methods:{
  30. getData(){
  31. this.$store.dispatch('getSearchList',this.searchParams)
  32. }
  33. },
  34. computed:{
  35. ...mapGetters(['goodsList'])
  36. }
  37. }

5.5 监听路由变化再次发送请求数据

当点击三级联动组件或者搜索框时,路径中的query或params会发生改变。那么路由就会改变,监听路由即可。

每一次请求完毕,应该把相应的1/2/3级分类的id只看,让它接收下一次相应的1/2/3级分类

分类名字和关键字不用清理:因为每一次路由发生变化,都会赋予它新的数据

  1. <!-- 分类的面包屑-->
  2. <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}} <i @click="removeCategoryName">x</i></li>
  1. watch:{
  2. $route(newValue,oldValue){
  3. Object.assign(this.searchParams,this.$route.query,this.$route.params)
  4. this.getData()
  5. this.searchParams.category1Id=''
  6. this.searchParams.category2Id=''
  7. this.searchParams.category3Id=''
  8. }
  9. }

5.6 动态删除面包屑

即使带给服务器的参数是为空的字符串,仍然会把相应的字段带给服务器

但如果把相应的字段变为undefined,当前这个字段就不会带给服务器了(性能优化)

骚操作:

        对自己所在组件进行路由跳转,可以清除地址栏的指定参数(比如query参数)

  1. //删除分类的名字
  2. removeCategoryName(){
  3. //把带给服务器的参数置空,并给服务器发请求
  4. this.searchParams.categoryName=undefined
  5. this.searchParams.category1Id=undefined
  6. this.searchParams.category2Id=undefined
  7. this.searchParams.category3Id=undefined
  8. this.getData()
  9. this.$router.push({name:'search',params:this.$route.params})
  10. }

5.6 动态开发面包屑中的分类名

  1. <!-- 关键字的面包屑-->
  2. <li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}} <i @click="removeQueryName">x</i></li>
  1. removeQueryName(){
  2. this.searchParams.keyword=undefined
  3. this.getData()
  4. }

5.7 动态开发面包屑中的关键字

当面包屑中的关键字清除以后,需要让兄弟组件Header组件中的关键字清除

设计组件间通信:

props:父子

自定义事件:子父

vuex:仓库数据

插槽:父子

$bus:全局事件总线

注册全局事件总线:

  1. new Vue({
  2. render: h => h(App),
  3. beforeCreate(){
  4. Vue.prototype.$bus=this
  5. },
  6. router,
  7. store,
  8. }).$mount('#app')

在Search组件中触发事件:

      this.$bus.$emit('clear')

在Header组件中绑定事件:

  1. mounted(){
  2. this.$bus.$on("clear",()=>{
  3. this.keyword=""
  4. })
  5. }

更新params参数:(Search组件)

      this.$router.push({name:'search',query:this.$route.query})

5.8 面包屑处理品牌信息

在Search组件中展示品牌的面包屑:

注意trademark.tmName的展示方式

  1. <!-- 品牌的面包屑-->
  2. <li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(":")[1]}} <i @click="removeTrademark">x</i></li>

子父组件间通信:

父传子自定义事件:

  1. <!--selector-->
  2. <SearchSelector @trademarkInfo="trademarkInfo"/>

由于传递过来的trademark是个对象,而传递给服务器的trademark是个字符串,所以要进行字符串拼接

  1. trademarkInfo(trademark){
  2. console.log(trademark)
  3. this.searchParams.trademark=`${trademark.tmId}:${trademark.tmName}`
  4. this.getData()
  5. }

子触发父中的函数:

  1. trademarkHandler(trademark){
  2. this.$emit('trademarkInfo',trademark)
  3. }

5.9 平台售卖属性的操作

父组件:

  1. <!-- 平台售卖的属性值展示-->
  2. <li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{{attrValue.split(":")[1]}} <i @click="removeAttr(index)">x</i> </li>
  3. </ul>
  1. attrInfo(attr,attrValue){
  2. let props=`${attr.attrId}:${attrValue}:${attr.attrName}`
  3. //数组去重
  4. if(this.searchParams.props.indexOf(props)===-1)
  5. this.searchParams.props.push(props)
  6. },
  7. removeAttr(index){
  8. //再次整理参数
  9. this.searchParams.props.splice(index,1)
  10. //再次发送请求
  11. this.getData()
  12. }

父子组件间通信:

  1. <!--selector-->
  2. <SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/>
  1. attrInfo(attr,attrValue){
  2. this.$emit("attrInfo",attr,attrValue)
  3. }

day6

6.1 排序操作(上)

排序方式:

        1:综合

        2:价格

        asc:升序

        desc:降序

①order属性的属性值最多有4种写法:

1:asc

1:desc

2:asc

2:desc

  1. //排序的初始状态:综合:降序
  2. order:"1:desc",

②综合和价格谁应该具有类名active?

        通过order属性值当中是包含1还是包含2来判断

  1. <ul class="sui-nav">
  2. <li :class="{active:searchParams.order.indexOf('1')!==-1}">
  3. <a href="#">综合</a>
  4. </li>
  5. <li :class="{active:searchParams.order.indexOf('2')!==-1}">
  6. <a href="#">价格</a>
  7. </li>
  8. </ul>

由于模板中不建议使用太长的表达式语句,可以用计算属性书写:

  1. <ul class="sui-nav">
  2. <li :class="{active:isOne}">
  3. <a href="#">综合</a>
  4. </li>
  5. <li :class="{active:isTwo}">
  6. <a href="#">价格</a>
  7. </li>
  8. </ul>
  1. computed:{
  2. ...mapGetters(['goodsList']),
  3. isOne(){
  4. return this.searchParams.order.indexOf('1')!==-1
  5. },
  6. isTwo(){
  7. return this.searchParams.or.indexOf('2')!==-1
  8. }
  9. }

③综合和价格,谁应该有箭头?

        谁有类名active,谁就有箭头

④箭头用什么制作?

        阿里图标库iconfont

在复制的地址前加 https:

放入index.html静态页面即可使用

      <link rel="stylesheet" href="https://at.alicdn.com/t/c/font_3941403_5qqhi0nusus.css">

图标的基本使用:

  1. <ul class="sui-nav">
  2. <li :class="{active:isOne}">
  3. <a href="#">综合 <span v-show="isOne" class="iconfont icon-up-arrow"></span></a>
  4. </li>
  5. <li :class="{active:isTwo}">
  6. <a href="#">价格 <span v-show="isTwo" class="iconfont icon-arrow_down"></span></a>
  7. </li>
  8. </ul>

通过计算属性动态决定箭头上下:

  1. isAsc(){
  2. return this.searchParams.order.indexOf("asc")!==-1
  3. },
  4. isDesc(){
  5. return this.searchParams.order.indexOf("desc")!==-1
  6. }
  1. <ul class="sui-nav">
  2. <li :class="{active:isOne}">
  3. <a href="#">综合 <span v-show="isOne" class="iconfont" :class="{'icon-up-arrow':isAsc,'icon-arrow_down':isDesc}"></span></a>
  4. </li>
  5. <li :class="{active:isTwo}">
  6. <a href="#">价格 <span v-show="isTwo" class="iconfont" :class="{'icon-up-arrow':isAsc,'icon-arrow_down':isDesc}"></span></a>
  7. </li>
  8. </ul>

6.2 排序操作(下)

设定改变上下箭头的规则:

  1. changeOrder(flag){
  2. let originFlag=this.searchParams.order.split(":")[0]
  3. let originSort=this.searchParams.order.split(":")[1]
  4. let newOrder=''
  5. if(flag===originFlag){
  6. originSort=originSort==="desc"?"asc":"desc"
  7. newOrder=`${originFlag}:${originSort}`
  8. }else{
  9. newOrder=`${flag}:desc`
  10. }
  11. this.searchParams.order=newOrder
  12. this.getData()
  13. }

大家注意newOrder=`${flag}:desc`中的冒号一定要用英文字符,否则切换失败!

day7

7.1 分页功能分析

为什么很多项目采用分页功能?

        因为比如电商平台同时展示的数据有很多(1w+),需要采用分页功能一次加载少量数据,防止卡顿

分页器展示,需要哪些数据(条件)?

        需要知道当前是第几页:pageNo字段代表当前页数

        需要知道每一页需要展示多少条数据:pageSize字段进行代表

        需要知道整个分页器一共有多少条数据:total字段进行代表---【获取另外一条信息:总共有多少页】

        需要知道分页器连续页面个数:5|7(一般是奇数,对称比较好看)

7.2 分页器起始与结束数字计算

①总页数小于连续页数

        仅展示总页数即可

②总页数大于/等于连续页数

根据当前页算起始、结束页码:

        i.起始页码小于1,此时置起始页码为1,结束页码为连续页码数

        ii.结束页码大于总页码,此时置结束页码为总页码,起始页码为:总页码-连续页码+1

        iii.正常情况下,直接计算

  1. computed:{
  2. totalPage(){
  3. return Math.ceil(this.total/this.pageSize)
  4. },
  5. startNumAndEndNum(){
  6. let start=1,end=1;
  7. //不正常现象:总页数没有连续页码数多
  8. if(this.continues>this.totalPage){
  9. end=this.totalPage
  10. }else{
  11. start=this.pageNo-parseInt(this.continues)/2
  12. end=this.pageNo+parseInt(this.continues)/2
  13. if(start<1){
  14. start=1
  15. end=this.continues
  16. }
  17. if(end>this.totalPage){
  18. start=end-this.continues+1
  19. end=this.totalPage
  20. }
  21. }
  22. return {start,end}
  23. }

7.3 分页器的动态展示

v-for也可以用于循环数字。指定要循环的数字,比如说10

则v-for会遍历0~10

如果希望从指定的数字开始遍历,可以用v-if加以限制

  1. <template>
  2. <div class="fr page">
  3. <div class="sui-pagination clearfix">
  4. <ul>
  5. <li class="prev disabled">
  6. <a href="#">«上一页</a>
  7. </li>
  8. <li class="active" v-if="startNumAndEndNum.start>1">
  9. <a href="#">1</a>
  10. </li>
  11. <li>
  12. <a href="#" v-if="startNumAndEndNum.start>2">...</a>
  13. </li>
  14. <li v-for="(page,index) in startNumAndEndNum.end" :key="index" v-if="page>=startNumAndEndNum.start">
  15. <a href="#">{{page}}</a>
  16. </li>
  17. <li class="dotted" v-if="startNumAndEndNum.end<(totalPage-1)"><span>...</span></li>
  18. <li>
  19. <a href="#" v-if="startNumAndEndNum.end<totalPage">{{totalPage}}</a>
  20. </li>
  21. <li>
  22. <a href="#">{{totalPage}}</a>
  23. </li>
  24. <li class="next">
  25. <a href="#">下一页»</a>
  26. </li>
  27. </ul>
  28. <div><span>共{{total}}条&nbsp;</span></div>
  29. </div>
  30. </div>
  31. </template>

7.4 分页器的完成

  1. <template>
  2. <div class="fr page">
  3. <div class="sui-pagination clearfix">
  4. <ul>
  5. <li class="prev" :disabled="pageNo===1" @click="$emit('getPageNo',pageNo-1)">
  6. <a>«上一页</a>
  7. </li>
  8. <li class="active" v-if="startNumAndEndNum.start>1" @click="$emit('getPageNo',1)">
  9. <a>1</a>
  10. </li>
  11. <li>
  12. <a v-if="startNumAndEndNum.start>2">...</a>
  13. </li>
  14. <li
  15. v-for="(page,index) in startNumAndEndNum" :key="index"
  16. v-if="page>=startNumAndEndNum.start"
  17. @click="$emit('getPageNo',page)"
  18. >
  19. <a>{{page}}</a>
  20. </li>
  21. <li class="dotted" v-if="startNumAndEndNum.end<(totalPage-1)"><span>...</span></li>
  22. <li>
  23. <a
  24. v-if="startNumAndEndNum.end<totalPage"
  25. @click="$emit('getPageNo',totalPage)"
  26. >{{totalPage}}</a>
  27. </li>
  28. <li>
  29. <a>{{totalPage}}</a>
  30. </li>
  31. <li class="next" :disabled="pageNo===totalPage" @click="$emit('getPageNo',pageNo+1)">
  32. <a>下一页»</a>
  33. </li>
  34. </ul>
  35. <div><span>共{{total}}条&nbsp;</span></div>
  36. </div>
  37. </div>
  38. </template>

7.5 分页器类名的添加

            :class="{active:pageNo===page}"

7.6 滚动行为

开发某一个产品的详情页面?

1.静态组件

2.发请求

3.vuex

4.动态展示组件

当点击商品的图片的时候,跳转到详情页面,在路由跳转的时候需要带上产品的ID给详情页面

detail路由:

  1. {
  2. path:'/detail/:id',
  3. component:Detail,
  4. meta:{isShow:true}
  5. },

路由跳转:

  1. <router-link :to="`/detail/${good.id}`">
  2. <img :src="good.defaultImg" />
  3. </router-link>

滚动行为:

使用前端路由,当切换到新的路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router能做到,且做得更好,他可以让你自定义路由切换时页面如何滚动

注意:这个功能只在支持history.pushState的浏览器中可用

  1. export default new VueRouter({
  2. routes,
  3. scrollBehavior(to,from,savePosition){
  4. //返回的这个y=0,代表页面在最上面
  5. return {y:0}
  6. }
  7. })

7.7 产品详情数据获取

获取数据的接口:

  1. //获取产品详细信息的接口 URL:/api/item/{skuId} 请求方式:get
  2. export const reqGoodsInfo=(skuId)=>requests({
  3. url:`/item/${skuId}`,
  4. method:'get'
  5. })

vuex仓库捞数据:

  1. import {reqGoodsInfo} from "@/api";
  2. const state={
  3. goodsInfo:{}
  4. }
  5. const mutations={
  6. GETGOODSINFO(state,goodsInfo){
  7. state.goodsInfo=goodsInfo
  8. }
  9. }
  10. const actions={
  11. //获取产品信息的action
  12. async getGoodsInfo({commit},skuId){
  13. let result=await reqGoodsInfo(skuId)
  14. if(result.code===200){
  15. commit(GETGOODSINFO(result.data))
  16. }
  17. }
  18. }
  19. const getters={}
  20. export default {
  21. state,mutations,actions,getters
  22. }

7.8 产品详情展示动态数据

报错,但不影响程序运行

这是因为仓库中的getters方法:

        state.goodsInfo初始状态为空对象,空对象的categoryView属性值为undefined。undefined.category1Id肯定会报错。

  1. const getters={
  2. categoryView(state){
  3. return state.goodsInfo.categoryView
  4. }
  5. }

可以改为:

        那么此时计算出来的categoryView属性值至少是一个空对象,假的报错就不会有了

  1. const getters={
  2. categoryView(state){
  3. return state.goodsInfo.categoryView||{}
  4. }
  5. }

Detail组件和Zoom组件通信的时候,通用会报错,可以修改为:

  1. skuImageList(){
  2. return this.skuInfo.skuImageList||[{}]
  3. }

day8

8.1 产品售卖属性值排他操作

先通过遍历父数组将所有子元素的isChecked取消

再将点中的子元素isChecked点亮

  1. changeActive(spuSaleAttrValue,spuSaleAttrValueList){
  2. spuSaleAttrValueList.forEach(item=>{
  3. item.isChecked='0'
  4. })
  5. spuSaleAttrValue.isChecked='1'
  6. }

8.2 Zoom和ImgList兄弟组件间通信

通过ImgList传递给Zoom的index,使得轮播图中的图片改变导致Zoom中展示的大图一起改变(联动效应)

ImgList:

  1. changeCurrentIndex(index){
  2. this.currentIndex=index
  3. this.$bus.$emit('getIndex',this.currentIndex)
  4. }

Zoom:

  1. data(){
  2. return {
  3. currentIndex:0
  4. }
  5. },
  6. computed:{
  7. imgObj(){
  8. return this.skuImageList[this.currentIndex]||{}
  9. }
  10. },
  11. mounted(){
  12. this.$bus.$on('getIndex',(index)=>{
  13. this.currentIndex=index
  14. })
  15. }

8.3 轮播图实例的完成

注意没见过的新属性slidesPereView和slidesPerGroup

  1. watch:{
  2. //监听数据:虽然可以保证skuImageList数据已经传递过来,但v-for未必已经遍历完成
  3. skuImageList(newValue,oldValue){
  4. this.$nextTick(()=>{
  5. new Swiper(".swiper-container",{
  6. navigation:{
  7. nextEl:".swiper-button-next",
  8. prevEl:".swiper-button-prev"
  9. },
  10. //显示一次显示3个图片
  11. slidesPerView:3,
  12. //设置一次切换1张图片
  13. slidesPerGroup:1
  14. })
  15. })
  16. }
  17. }

8.4 放大镜的实现

遮罩层方法剖析:

big是用来显示mask滑过的区域,mask向右的时候,big对应的图片应该向左,大小为两倍差

  1. handler(event){
  2. let mask=this.$refs.mask
  3. let big=this.$refs.big
  4. let left=event.offsetX-mask.offsetWidth/2
  5. let top=event.offsetY-mask.offsetHeight/2
  6. //约束范围
  7. if(left<=0) left=0
  8. else if(left>=mask.offsetWidth) left=mask.offsetWidth
  9. if(top<=0) top=0
  10. else if(top>=mask.offsetHeight) top=mask.offsetHeight
  11. //修改元素的left|top
  12. mask.style.left=left+'px'
  13. mask.style.top=top+'px'
  14. big.style.left=-2*left+'px'
  15. big.style.top=-2*top+'px'
  16. }
  17. }

总代码:

  1. <template>
  2. <div class="spec-preview">
  3. <img :src="imgObj.imgUrl" />
  4. <div class="event" @mousemove="handler"></div>
  5. <div class="big">
  6. <img :src="imgObj.imgUrl" ref="big"/>
  7. </div>
  8. <!-- 遮罩层-->
  9. <div class="mask" ref="mask"></div>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. name: "Zoom",
  15. props:['skuImageList'],
  16. data(){
  17. return {
  18. currentIndex:0
  19. }
  20. },
  21. computed:{
  22. imgObj(){
  23. return this.skuImageList[this.currentIndex]||{}
  24. }
  25. },
  26. mounted(){
  27. this.$bus.$on('getIndex',(index)=>{
  28. this.currentIndex=index
  29. })
  30. },
  31. methods:{
  32. handler(event){
  33. let mask=this.$refs.mask
  34. let big=this.$refs.big
  35. let left=event.offsetX-mask.offsetWidth/2
  36. let top=event.offsetY-mask.offsetHeight/2
  37. //约束范围
  38. if(left<=0) left=0
  39. else if(left>=mask.offsetWidth) left=mask.offsetWidth
  40. if(top<=0) top=0
  41. else if(top>=mask.offsetHeight) top=mask.offsetHeight
  42. //修改元素的left|top
  43. mask.style.left=left+'px'
  44. mask.style.top=top+'px'
  45. big.style.left=-2*left+'px'
  46. big.style.top=-2*top+'px'
  47. }
  48. }
  49. }
  50. </script>
  51. <style lang="less">
  52. .spec-preview {
  53. position: relative;
  54. width: 400px;
  55. height: 400px;
  56. border: 1px solid #ccc;
  57. img {
  58. width: 100%;
  59. height: 100%;
  60. }
  61. .event {
  62. width: 100%;
  63. height: 100%;
  64. position: absolute;
  65. top: 0;
  66. left: 0;
  67. z-index: 998;
  68. }
  69. .mask {
  70. width: 50%;
  71. height: 50%;
  72. background-color: rgba(0, 255, 0, 0.3);
  73. position: absolute;
  74. left: 0;
  75. top: 0;
  76. display: none;
  77. }
  78. .big {
  79. width: 100%;
  80. height: 100%;
  81. position: absolute;
  82. top: -1px;
  83. left: 100%;
  84. border: 1px solid #aaa;
  85. overflow: hidden;
  86. z-index: 998;
  87. display: none;
  88. background: white;
  89. img {
  90. width: 200%;
  91. max-width: 200%;
  92. height: 200%;
  93. position: absolute;
  94. left: 0;
  95. top: 0;
  96. }
  97. }
  98. .event:hover~.mask,
  99. .event:hover~.big {
  100. display: block;
  101. }
  102. }
  103. </style>

8.5 购买产品个数的操作

  1. //表单元素修改产品个数
  2. changeSkuNum(event){
  3. //用户输入进来的文本*1
  4. let value=event.target.value*1
  5. //如果用户输入进来的非法
  6. if(isNaN(value)||value<1){
  7. this.skuNum=1
  8. }else{
  9. this.skuNum=parseInt(value)
  10. }
  11. }

8.6 加入购物车

接口api:

血的教训,千万不要把method写成methods!!!!

  1. export const reqAddOrUpdateShopCart=(skuId,skuNum)=>requests({
  2. url:`/cart/addToCart/${skuId}/${skuNum}`,
  3. method:"post"
  4. })

加入购物车成功的路由:

  1. {
  2. name:'addcartsuccess',
  3. path:'/addcartsuccess',
  4. component:AddCartSuccess,
  5. meta:{isShow:true}
  6. }

仓库中的action:

  1. //将产品添加到购物车中
  2. async addOrUpdateShopCart({commit},{skuId,skuNum}){
  3. //服务器写入数据成功以后,并没有返回其他的数据,只是返回了code=200,代表此处操作成功
  4. let result= await reqAddOrUpdateShopCart(skuId,skuNum)
  5. if(result.code==200){
  6. return 'ok'
  7. }else{
  8. return Promise.reject(new Error('fail'))
  9. }
  10. }

浏览器存储功能:

        本地存储:持久化的——5M

        会话存储:并非持久——会话结束就消失

路由传递参数结合会话存储:

进行路由跳转的时候需要将产品信息带给下一级的路由组件

一些简单的数据skuNum,通过query形式给路由组件传递过去

复杂的产品信息数据(比如skuInfo),通过会话存储即可(不持久化,会话结束数据消失)

  1. async addShopCart(){
  2. //1.发请求——将产品加入到数据库(通知服务器)
  3. try {
  4. await this.$store.dispatch('addOrUpdateShopCart', {
  5. skuId: this.$route.params.skuId,
  6. skuNum: this.skuNum})
  7. //2.服务器存储成功——进行路由跳转传递参数
  8. sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))
  9. this.$router.push({
  10. name: 'addcartsuccess',
  11. query:{
  12. skuNum:this.skuNum
  13. }
  14. })
  15. }catch(err) {
  16. //3.失败——给用户进行提示
  17. alert(err.message)
  18. }
  19. }

AddCartSuccess组件中使用到sessionStorage存储的item:

  1. computed:{
  2. skuInfo(){
  3. return JSON.parse(sessionStorage.getItem('SKUINFO'))
  4. }
  5. }

day9

9.1 购物车静态组件与修改

购物车静态组件——需要修改样式结构

1.调整css让各个项目对齐

2.向服务器发送ajax,获取购物车数据

3.UUID临时游客身份

4.动态展示购物车

9.2 uuid游客身份获取购物车数据

获取购物车数据的接口:

  1. //获取购物车列表数据接口
  2. export const reqCartList=()=>requests({
  3. url:"/cart/cartList",
  4. method:"get"
  5. })

向服务器发送ajax请求获取购物车数据:

        发请求的时候无法获取你购物车里面的数据,因为服务器不知道你是谁

        可以使用UUID临时游客身份

uuid函数逻辑:

随机生成一个字符串,且每次执行不能发生变化,游客身份持久存储

①从本地存储中获取uuid

②如果没有

        i.生成游客临时身份

        ii.本地存储

一定要有返回值!!!

  1. import {v4 as uuidv4} from 'uuid'
  2. export const getUUID=()=>{
  3. let uuid_token=localStorage.getItem("UUIDTOKEN")
  4. if(!uuid_token){
  5. uuid_token=uuidv4()
  6. localStorage.setItem('UUIDTOKEN',uuid_token)
  7. }
  8. return uuid_token
  9. }

在请求拦截器中将存储好的uuid发往后台:

  1. //请求拦截器
  2. requests.interceptors.request.use((config)=>{
  3. if(store.state.detail.uuid_token){
  4. config.headers.userTempId=store.state.detail.uuid_token
  5. }
  6. //config是headers的请求头
  7. nprogress.start()
  8. return config
  9. })

9.3 修改购物车产品的数量完成

  1. <a class="mins" @click="handler('minus',-1,cart)">-</a>
  2. <input
  3. autocomplete="off"
  4. type="text"
  5. :value="cart.skuNum"
  6. minnum="1"
  7. class="itxt"
  8. @change="handler('change',$event.target.value*1,cart)"
  9. >
  10. <a class="plus" @click="handler('add',1,cart)">+</a>
  1. //修改某一个产品的个数
  2. async handler(type,disNum,cart) {
  3. switch (type) {
  4. //加号
  5. case 'add':
  6. disNum = 1
  7. break
  8. case 'minus':
  9. disNum = cart.skuNum > 1 ? -1 : 0
  10. break
  11. case 'change':
  12. if(isNaN(disNum)||disNum<1){
  13. disNum=0
  14. }else{
  15. disNum=parseInt(disNum)-cart.skuNum
  16. }
  17. }
  18. try {
  19. await this.$store.dispatch('addOrUpdateShopCart', {
  20. skuId: cart.skuId,
  21. skuNum: disNum
  22. })
  23. this.getData()
  24. }catch(error){
  25. alert('修改失败')
  26. }
  27. }

特注:因为改变数量的时候如果点击过快可能会发生意想不到的结果,所以可以进行节流处理

  1. //修改某一个产品的个数
  2. handler:throttle(async function(type,disNum,cart){
  3. switch (type) {
  4. //加号
  5. case 'add':
  6. disNum = 1
  7. break
  8. case 'minus':
  9. disNum = cart.skuNum > 1 ? -1 : 0
  10. break
  11. case 'change':
  12. if(isNaN(disNum)||disNum<1){
  13. disNum=0
  14. }else{
  15. disNum=parseInt(disNum)-cart.skuNum
  16. }
  17. }
  18. try {
  19. await this.$store.dispatch('addOrUpdateShopCart', {
  20. skuId: cart.skuId,
  21. skuNum: disNum
  22. })
  23. this.getData()
  24. }catch(error){
  25. alert('修改失败')
  26. }
  27. })
  28. },

9.4 删除购物车产品的操作

接口:

  1. //删除购物产品的接口
  2. export const reqDeleteCartById=(skuId)=>requests({
  3. url:`/cart/deleteCart/${skuId}`,
  4. method:"delete"
  5. })

具体的删除操作:

  1. //删除某一个产品的操作
  2. async deleteCartById(cart){
  3. try{
  4. await this.$store.dispatch('deleteCartListBySkuId',cart.skuId)
  5. //如果删除成功,再次发请求获取新的数据展示
  6. this.getData()
  7. }catch(error){
  8. alert(error.message)
  9. }
  10. }

9.5 修改产品状态

接口:

  1. //修改商品的选中状态
  2. export const reqUpdateCheckedById=(skuId,isChecked)=>requests({
  3. url:`/cart/checkCart/${skuId}/${isChecked}`,
  4. method:"get"
  5. })

仓库:

  1. //修改购物车某一个产品的选中状态
  2. async updateCheckedById({commit},{skuId,isChecked}){
  3. let result = await reqUpdateCheckedById(skuId,isChecked)
  4. if(result.code===200){
  5. return 'ok'
  6. }else{
  7. return Promise.reject(new Error('fail'))
  8. }
  9. }

修改template模板内容:

  1. <input
  2. type="checkbox"
  3. name="chk_list"
  4. :checked="cart.isChecked==1"
  5. @change="updateChecked(cart,$event)"
  6. >

修改script脚本内容:

  1. async updateChecked(cart,event){
  2. try{
  3. let checked=event.target.checked?"1":"0"
  4. await this.$store.dispatch('updateCheckedById',{skuId:cart.skuId,isChecked:checked})
  5. this.getData()
  6. }catch(error){
  7. }
  8. }

9.6 复习

1.加入购物车

UUID:点击加入购物车的时候,通过请求头给服务器带临时身份给服务器,存储某一个用户购物车数据

会话存储:去存储产品的信息以及展示功能

2.购物车功能

        修改产品的数量

        删除某一个产品的接口

        某一个产品的勾选状态切换

day10

10.1 删除全部选中的商品

context:小仓库,包含commit(提交mutations修改state)、getters(计算属性)、dispatch(派发action)、state(当前仓库数据)

  1. //删除全部勾选的产品
  2. deleteAllCheckedCart(context) {
  3. }

Promise.all([p1,p2,p3])

p1|p2|p3:每一个都是Promise对象,如果有一个Promise失败,都失败;如果都成功,返回成功

删除全部选中的商品

        action逻辑:

  1. //删除全部勾选的产品
  2. deleteAllCheckedCart({dispatch,getters}) {
  3. let promiseAll=[]
  4. //获取购物车中的全部产品
  5. getters.cartList.cartInfoList.forEach(item=>{
  6. let promise=item.isChecked===1?dispatch("deleteCartListBySkuId",item.skuId):''
  7. promiseAll.push(promise)
  8. })
  9. return Promise.all(promiseAll)
  10. }

        组件里的逻辑:

  1. async deleteAllCheckedCart(){
  2. try{
  3. await this.$store.dispatch("deleteAllCheckedCart")
  4. this.getData()
  5. }catch(error){
  6. alert(error.message)
  7. }
  8. }

10.2 全部商品的勾选状态修改

actions里的逻辑:

  1. updateAllCartIsChecked({dispatch,state},isChecked){
  2. let promiseAll=[]
  3. state.cartList[0].cartInfoList.forEach(item=>{
  4. let promise=dispatch("updateCheckedById",{
  5. skuId:item.skuId,
  6. isChecked:isChecked
  7. })
  8. promiseAll.push(promise)
  9. return Promise.all(promiseAll)
  10. })
  11. }

组件里的逻辑:

  1. //修改全部产品选中的状态
  2. async updateAllCartChecked(event){
  3. try{
  4. let isChecked=event.target.checked?"1":"0"
  5. await this.$store.dispatch("updateAllCartIsChecked",isChecked)
  6. this.getData()
  7. }catch(error){
  8. alert(error.message)
  9. }
  10. }

组件的input全选框判断:

  1. <div class="select-all">
  2. <input class="chooseAll"
  3. type="checkbox"
  4. :checked="isAllChecked&&cartInfoList.length>0"
  5. @change="updateAllCartChecked($event)"
  6. >
  7. <span>全选</span>
  8. </div>

10.3 注册业务

assets文件夹打包以后,整个项目在dist目录下,assets文件夹会消失

        assets文件夹——放置全部组件共用的静态资源

在样式中也可以使用@符号:

        url中使用@代表src路径:

            background-image: url(~@/assets/images/icons.png);

接口api:

  1. //获取验证码
  2. export const reqGetCode=(phone)=>requests({
  3. url:`/user/passport/sendCode/${phone}`,
  4. method:"get"
  5. })

仓库:

  1. import {reqGetCode} from "@/api";
  2. const state={
  3. code:''
  4. }
  5. const mutations={
  6. GETCODE(state,code){
  7. state.code=code
  8. }
  9. }
  10. const actions={
  11. //获取验证码
  12. async getCode({commit},phone){
  13. let result=await reqGetCode(phone)
  14. if(result.code===200){
  15. commit("GETCODE",result.data)
  16. }else{
  17. return Promise.reject(new Error('fail'))
  18. }
  19. }
  20. }
  21. const getters={}
  22. export default {
  23. state,mutations,actions,getters
  24. }

发送请求到仓库:

        <button style="width:100px;height:38px;" @click="$store.dispatch('getCode',phone)">获取验证码</button>

或:

  1. async getCode(){
  2. try{
  3. this.phone&&(await this.$store.dispatch("getCode",this.phone))
  4. console.log(this.$store.state.user.code)
  5. }catch(error){
  6. alert(error.message)
  7. }
  8. }

注册成功后跳转到登录页面:

接口api:

  1. //注册
  2. export const reqUserRegister=(data)=>requests({
  3. url:`/user/passport/register`,
  4. data,
  5. method:"post"
  6. })

仓库里的actions逻辑:

  1. //用户注册
  2. async userRegister({commit},user){
  3. let result=await reqUserRegister(user)
  4. if(result.code===200){
  5. return 'ok'
  6. }else{
  7. return Promise.reject(new Error('fail'))
  8. }
  9. }

组件里的逻辑:

  1. async userRegister(){
  2. try{
  3. const {phone,code,password,rePassword}=this
  4. phone&&code&&password===rePassword&&(await this.$store.dispatch("userRegister",{phone,code,password}))
  5. this.$router.push("/login")
  6. }catch(error) {
  7. alert(error.message)
  8. }
  9. }

10.4 登录业务

登录业务:

注册:通过数据库存储用户信息(名字、密码)

登录:登录成功的时候,后台为了区分你这个用户是谁,会让服务器下发token(令牌:唯一标识符)

登陆接口:一般登陆成功后服务器会下发token,前台持久化存储token,然后前台带着token 去找服务器登录

接口api:

  1. //登录
  2. export const reqUserLogin=(data)=>requests({
  3. url:"/user/passport/login",
  4. data,
  5. method:"post"
  6. })

组件逻辑:

  1. async userLogin(){
  2. try{
  3. const {phone,password}=this
  4. (phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
  5. this.$router.push("/home")
  6. }catch(error){
  7. alert(error.message)
  8. }
  9. }

仓库逻辑:

actions:

  1. //登录业务
  2. async userLogin({commit},data){
  3. let result=await reqUserLogin(data)
  4. if(result.code===200){
  5. commit("USERLOGIN",result.data.token)
  6. return 'ok'
  7. }else{
  8. return Promise.reject(new Error('fail'))
  9. }
  10. }

mutations:

  1. USERLOGIN(state,token){
  2. state.token=token
  3. }

前台携带token获取用户信息:

export const reqUserInfo=()=>requests.get("/user/passport/auth/getUserInfo")

处理请求拦截器:

  1. //请求拦截器
  2. requests.interceptors.request.use((config)=>{
  3. if(store.state.detail.uuid_token){
  4. config.headers.userTempId=store.state.detail.uuid_token
  5. }
  6. if(store.state.user.token){
  7. config.headers.token=store.state.user.token
  8. }
  9. //config是headers的请求头
  10. nprogress.start()
  11. return config
  12. })

获取服务器返回的用户信息:

  1. async getUserInfo({commit}){
  2. let result=await reqUserInfo()
  3. if(result.code===200){
  4. commit("GETUSERINFO",result.data)
  5. return 'ok'
  6. }else{
  7. return Promise.reject(new Error('fail'))
  8. }
  9. }
  1. GETUSERINFO(state,userInfo){
  2. state.userInfo=userInfo
  3. }

处理Header组件:

  1. <div class="loginList">
  2. <p>尚品汇欢迎您!</p>
  3. <p v-if="!userName">
  4. <span></span>
  5. <router-link to="/login">登录</router-link>
  6. <router-link to="/register" class="register">免费注册</router-link>
  7. </p>
  8. <p v-else>
  9. <a>{{userName}}</a>
  10. <a class="register">退出登录</a>
  11. </p>
  12. </div>

vuex仓库存储的数据并不持久化,一刷新数据就没了。可以通过本地存储持久化token:

  1. //登录业务
  2. async userLogin({commit},data){
  3. let result=await reqUserLogin(data)
  4. if(result.code===200){
  5. commit("USERLOGIN",result.data.token)
  6. localStorage.setItem("TOKEN",result.data.token)
  7. return 'ok'
  8. }else{
  9. return Promise.reject(new Error('fail'))
  10. }
  11. }
  1. const state={
  2. code:'',
  3. token:localStorage.getItem("TOKEN"),
  4. userInfo:{}
  5. }

目前存在的bug:

①多个组件展示用户信息需要在每一个组件的mounted中触发this.$store.dispatch("getUserInfo")

②用户已经登陆,就不能再跳转到登录页

10.5 退出登录

退出登录需要做的事:

①需要发送请求,通知服务器退出登录(清除一些数据,比如token)

②清除项目当中的数据

接口api:

export const reqLogout=()=>requests.get("/user/passport/logout")

清除记录的数据:

  1. async userLogout({commit}){
  2. let result=await reqLogout()
  3. if(result.code===200){
  4. commit("CLEAR")
  5. return "ok"
  6. }else{
  7. return Promise.reject(new Error("fail"))
  8. }
  9. }
  1. CLEAR(state){
  2. state.token=""
  3. state.userInfo={}
  4. localStorage.clear()
  5. }

派发action:

  1. async logout(){
  2. try{
  3. await this.$store.dispatch("userLogout")
  4. this.$router.push("/home")
  5. }catch(error){
  6. alert(error.message)
  7. }
  8. }

10.6 导航守卫用户登录操作

导航:表示路由正在发生变化,进行路由跳转

导航守卫可以简单地分为全局守卫、路由独享守卫、组件内守卫

全局前置守卫:

        当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve完之前一直处于等待中

参数剖析:

        to:可以获取到你要跳转到的那个路由的信息

        from:可以获取到你从哪个路由而来的信息

        next:放行函数

                next()放行       

                next("/login")放行到指定的路由中

                next(false)中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址

  1. router.beforeEach((to, from, next)=>{
  2. })

全局路由守卫逻辑:

1.如果已经登录

        i.想去的是login路由,跳转到主页

        ii.想去的不是login路由

                ①如果有用户名,直接放行(不能用userInfo判断,因为空对象为真值)

                ②如果没有用户名,可能是刷新导致仓库的数据消失了,再去捞一下数据,然后放行

                        如果捞不到数据,就直接退出登录,回到登录界面

2.如果没有登陆

        直接放行(此时还没有做处理,后续可能会有追加)

  1. router.beforeEach(async (to, from, next)=>{
  2. let token=store.state.user.token
  3. let name=store.state.user.userInfo.name
  4. if(token) {
  5. //登陆了订单还想去login或register(不能去,停留在首页)
  6. if (to.path === '/login'||to.path === '/register') {
  7. next("/home")
  8. } else {
  9. //登陆了,去的不是login
  10. //如果用户信息
  11. if (name) {
  12. next()
  13. } else {
  14. try {
  15. //没有用户信息,派发action让仓库存储用户信息再跳转
  16. await store.dispatch("getUserInfo")
  17. next()
  18. } catch (error) {
  19. //token失效,获取不到用户信息,需要重新登陆
  20. //清除token
  21. await store.dispatch("userLogout")
  22. next("/login")
  23. }
  24. }
  25. }
  26. }else{
  27. next()
  28. }
  29. })

10.7 获取交易页数据及展示

api接口:

  1. //获取用户地址信息
  2. export const reqAddressInfo=()=>requests({
  3. url:"/user/userAddress/auth/findUserAddressList",
  4. method:"GET"
  5. })
  6. //获取商品清单
  7. export const reqOrderInfo=()=>requests({
  8. url:"/order/auth/trade",
  9. method:"GET"
  10. })

仓库:

  1. import {reqAddressInfo, reqOrderInfo} from "@/api";
  2. const state={
  3. address:[],
  4. orderInfo:{}
  5. }
  6. const mutations={
  7. GETUSERADDRESS(state,address){
  8. state.address=address
  9. },
  10. GETORDERINFO(state,orderInfo){
  11. state.orderInfo=orderInfo
  12. }
  13. }
  14. const actions={
  15. async getUserAddress({commit}){
  16. let result = await reqAddressInfo()
  17. if(result.code===200){
  18. commit("GETUSERADDRESS",result.data)
  19. }
  20. },
  21. async getOrderInfo({commit}){
  22. let result = await reqOrderInfo()
  23. if(result.code===200){
  24. commit("GETORDERINFO",result.data)
  25. }
  26. }
  27. }
  28. const getters={}
  29. export default {
  30. state,mutations,actions,getters
  31. }

组件逻辑:

注意mapState和mapGetters的区别

  1. <script>
  2. import {mapState} from "vuex";
  3. export default {
  4. name: 'Trade',
  5. data(){
  6. return {
  7. msg:""
  8. }
  9. },
  10. mounted(){
  11. this.$store.dispatch("getUserAddress"),
  12. this.$store.dispatch("getOrderInfo")
  13. },
  14. computed:{
  15. ...mapState({
  16. addressInfo:state=>state.trade.address,
  17. orderInfo:state=>state.trade.orderInfo
  18. }),
  19. //将来提交订单最终选中地址
  20. userDefaultAddress(){
  21. return this.addressInfo.find(item=>item.isDefault==1)||{}
  22. },
  23. detailArrayList(){
  24. return this.orderInfo.detailArrayList||[]
  25. }
  26. },
  27. methods:{
  28. changeDefault(address,addressInfo){
  29. //find:查找数组当中符合条件的元素作为返回值返回
  30. addressInfo.forEach(item=>item.isDefault=0)
  31. address.isDefault=1
  32. }
  33. }
  34. }
  35. </script>

day11

11.1 提交订单

①静态组件

②点击提交订单的按钮时,还需要向服务器发送一次请求,把一些支付的相关信息传递给服务器

接口:

  1. export const reqSubmitOrder=(tradeNo,data)=>requests({
  2. url:`/order/auth/submitOrder?tradeNo=${tradeNo}`,
  3. data,
  4. method:"post"
  5. })

统一接口api文件夹里面全部的请求函数:

  1. import * as API from "@/api"
  2. new Vue({
  3. render: h => h(App),
  4. beforeCreate(){
  5. Vue.prototype.$bus=this
  6. Vue.prototype.$API=API
  7. },
  8. router,
  9. store,
  10. }).$mount('#app')

发送请求到支付页面:

  1. async submitOrder(){
  2. let {tradeNo}=this;
  3. let data={
  4. consignee:this.userDefaultAddress.consignee,
  5. consigneeTel:this.userDefaultAddress.phoneNum,
  6. deliveryAddress:this.userDefaultAddress.fullAddress,
  7. paymentWay:"ONLINE",
  8. orderComment:this.msg,
  9. orderDetailList:this.detailArrayList
  10. }
  11. let result=await this.$API.reqSubmitOrder(tradeNo,data)
  12. console.log(result)
  13. if(result.code===200){
  14. this.orderId=result.data
  15. this.$router.push(`/pay?orderId=${this.orderId}`)
  16. }else{
  17. alert(result.data)
  18. }
  19. }

11.2 获取订单号与展示支付信息

接口:

  1. //获取支付的信息
  2. export const reqPayInfo=(orderId)=>requests({
  3. url:`/payment/weixin/createNative/${orderId}`,
  4. method:"get"
  5. })

生命周期函数中不能使用async

  1. <script>
  2. export default {
  3. name: 'Pay',
  4. data(){
  5. return {
  6. payInfo:{}
  7. }
  8. },
  9. computed:{
  10. orderId(){
  11. return this.$route.query.orderId
  12. }
  13. },
  14. mounted(){
  15. this.getPayInfo()
  16. },
  17. methods:{
  18. async getPayInfo(){
  19. let result=await this.$API.reqPayInfo(this.orderId)
  20. if(result.code===200){
  21. this.payInfo=result.data
  22. }
  23. }
  24. }
  25. }
  26. </script>

11.3 支付页面中使用ElementUI以及按需引入

  1. //注册全局组件
  2. import {Button,MessageBox} from 'element-ui'
  3. //ElementUI注册组件的时候还有一种写法:挂在原型上
  4. Vue.use(Button)
  5. Vue.prototype.$msgbox = MessageBox;
  6. Vue.prototype.$alert = MessageBox.alert;

ElementUI按需引入:

借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的

①安装 babel-plugin-component

yarn add babel-plugin-component -D

②将babel.config.js修改为:

  1. module.exports = {
  2. presets: [
  3. '@vue/cli-plugin-babel/preset',
  4. ],
  5. "plugins":[
  6. [
  7. "component",
  8. {
  9. libraryName:'element-ui',
  10. "StyleLibraryName":'theme-chalk'
  11. }
  12. ]
  13. ]
  14. }

直接使用:

          <el-button class="btn" @click="open">立即支付</el-button>
  1. //弹出框
  2. open(){
  3. this.$alert(`<strong>这是 <i>HTML</i> 片段</strong>`,'HTML片段',{
  4. dangerouslyUseHTMLString:true,
  5. //中间布局
  6. center:true,
  7. //是否显示取消按钮
  8. showCancelButton:true,
  9. //取消按钮的文本内容
  10. cancelButtonText:"支付遇见问题",
  11. //确定按钮的文本内容
  12. confirmButtonText:"已经支付成功",
  13. //右上角的叉叉是否显示
  14. showClose:false
  15. })
  16. }

11.4 微信支付业务

使用qrcode,一个用于生成二维码的JavaScript库

获取支付订单状态的接口:

  1. //获取支付订单状态
  2. export const reqPayStatus=(orderId)=>requests({
  3. url:`/payment/weixin/queryPayStatus/${orderId}`,
  4. method:"get"
  5. })

弹出框业务:

  1. //弹出框
  2. async open(){
  3. //生成二维码地址
  4. let url=await QRCode.toDataURL(this.payInfo.codeUrl)
  5. this.$alert(`<img src="${url}"/>`,'HTML片段',{
  6. dangerouslyUseHTMLString:true,
  7. //中间布局
  8. center:true,
  9. //是否显示取消按钮
  10. showCancelButton:true,
  11. //取消按钮的文本内容
  12. cancelButtonText:"支付遇见问题",
  13. //确定按钮的文本内容
  14. confirmButtonText:"已经支付成功",
  15. //右上角的叉叉是否显示
  16. showClose:false,
  17. //关闭弹出框的配置值
  18. beforeClose:(type,instance,done)=>{
  19. //type:区分取消|确定按钮
  20. //instance:当前组件实例
  21. //done:关闭弹出框的方法
  22. if(type==='cancel'){
  23. alert('请联系管理员')
  24. clearInterval(this.timer)
  25. this.timer=null
  26. //关闭弹出框
  27. done()
  28. }else{
  29. //判断是否真的支付了
  30. if(this.code===200){
  31. clearInterval(this.timer)
  32. this.timer=null
  33. done()
  34. this.$router.push('/paysuccess')
  35. }
  36. }
  37. }
  38. })
  39. if(!this.timer){
  40. this.timer=setInterval(async ()=>{
  41. //发请求获取用户支付状态
  42. let result=await this.$API.reqPayStatus(this.orderId)
  43. //如果code===200
  44. if(result.code===200){
  45. //第一步:清除定时器
  46. clearInterval(this.timer)
  47. this.timer=null
  48. //保存支付成功返回的code
  49. this.code=result.code
  50. //关闭弹出窗
  51. this.$msgbox.close()
  52. //跳转到下一路由
  53. this.$router.push('/paysuccess')
  54. }
  55. },1000)
  56. }
  57. }

day12

12.1 个人中心二级路由的搭建

如果只进入/center,可能会出现右侧没有内容的情况,所以需要重定向到/center/myorder

  1. {
  2. path:'/center',
  3. component:Center,
  4. meta:{isShow:true},
  5. children:[
  6. {
  7. path:'myorder',
  8. component:MyOrder
  9. },
  10. {
  11. path:'grouporder',
  12. component:GroupOrder
  13. },
  14. {
  15. path:'/center',
  16. redirect:"/center/myorder"
  17. }
  18. ]
  19. }

12.2 我的订单

接口api:

  1. //获取个人中心的数据
  2. export const reqMyOrderList=(page,limit)=>requests({
  3. url:`/order/auth/${page}/${limit}`,
  4. method:"get"
  5. })

12.3 未登录的导航守卫判断

全局前置守卫逻辑处理:

  1. //未登录:不能去交易相关、支付相关的页面(pay|paysuccess)、不能去个人中心
  2. //未登录若去上述页面---跳转至登录页面
  3. let toPath=to.path
  4. if(toPath.indexOf('/trade')!==-1||toPath.indexOf('/pay')!==-1||toPath.indexOf('/center')!==-1){
  5. next('/login?redirect='+toPath)
  6. }else {
  7. //去的不是上面这些路由(home|search|shopCart)---放行
  8. next()
  9. }

对login的处理:增加了对query参数的判断

  1. async userLogin(){
  2. try{
  3. const {phone,password}=this;
  4. (phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
  5. let toPath=this.$route.query.redirect||'/home'
  6. this.$router.push(toPath)
  7. }catch(error){
  8. alert(error.message)
  9. }
  10. }

12.4 用户登录(路由独享与组件内守卫)

路由独享守卫:

只有从购物车界面才能跳转到交易页面(创建订单)

  1. {
  2. path:"/trade",
  3. component: Trade,
  4. meta:{isShow:true},
  5. //路由独享守卫
  6. beforeEnter:(to,from,next)=>{
  7. if(from.path==='/shopcart'){
  8. next()
  9. }else{
  10. next(false)
  11. }
  12. }
  13. }

只有从交易页面(创建订单)页面才能跳转到支付页面

  1. {
  2. path:"/pay",
  3. component:Pay,
  4. meta:{isShow:true},
  5. beforeEnter:(to,from,next)=>{
  6. if(from.path==='/trade'){
  7. next()
  8. }else{
  9. next(false)
  10. }
  11. }
  12. }

只有从支付页面才能跳转到支付成功页面

组件内守卫:

beforeRouterEnter(to,from,next){        }:

        在渲染该组件的对应路由被confirm前调用

        不能获取组件的实例“this”

        因为当守卫执行前,组件实例还没有被创建

  1. export default {
  2. name: 'PaySuccess',
  3. beforeRouteEnter(to,next,from){
  4. if(from.path==='/pay'){
  5. next()
  6. }else{
  7. next(false)
  8. }
  9. }
  10. }

beforeRouteUpdate(to,from,next){        }:

        在当前路由改变、但是该组件被复用时调用

        举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候

        由于会渲染同样的Foo组件,因此组件实例会被复用,而这个钩子就是这种情况下被调用

        可以访问组件实例“this”

beforeRouteLeave(to,from,next){        }:

        导航离开该组件的对应路由时调用

        可以访问组件实例的“this”

12.5 图片懒加载

①下载插件vue-lazyload

注意vue2对应1.3.3版本的vue-lazyload,否则会报路径错误

yarn add vue-lazyload@1.3.3

②图片、json不需要对外暴露就可引入

  1. import VueLazyLoad from 'vue-lazyload'
  2. import winter from '@/assets/winter.jpg'
  3. Vue.use(VueLazyLoad,{
  4. //懒加载默认图片
  5. loading:winter
  6. })

在需要默认图片的模板中插入:

                      <img v-lazy="good.defaultImg" />

12.6 vee-validate表单验证的使用

①安装:注意vue2对应vee-validate2版本

yarn add vee-validate@2   

②在main.js中引入表单校验插件

  1. //引入表单校验插件
  2. import '@/plugins/validate'

③编写表单校验插件内容

  1. import Vue from 'vue'
  2. import VeeValidate from 'vee-validate'
  3. import zh_CN from 'vee-validate/dist/locale/zh_CN'
  4. Vue.use(VeeValidate)
  5. //表单验证
  6. VeeValidate.Validator.localize("zh_CN",{
  7. messages:{
  8. ...zh_CN.messages,
  9. is:(field)=>`${field}必须与密码相同`,
  10. },
  11. attributes:{
  12. phone:"手机号",
  13. code:"验证码",
  14. password:"密码",
  15. rePassword:"确认密码",
  16. agree:"协议"
  17. }
  18. })

④改写模板里的代码(以手机号校验为例)

  1. <div class="content">
  2. <label>手机号:</label>
  3. <input type="text"
  4. placeholder="请输入你的手机号"
  5. v-model="phone"
  6. name="phone" v-validate="{required:true,regex:/^1\d{10}$/}" :class="{invalid:errors.has('phone')}"
  7. >
  8. <span class="error-msg">{{errors.first("phone")}}</span>
  9. </div>

⑤效果图

不输入时:

 输入错误时:

 

格式正确时警告会消失:

 

验证码校验:

  1. <div class="content">
  2. <label>验证码:</label>
  3. <input type="text"
  4. placeholder="请输入验证码"
  5. v-model="code"
  6. name="code" v-validate="{required:true,regex:/^\d{6}$/}" :class="{invalid:errors.has('code')}"
  7. >
  8. <button style="width:100px;height:38px;" @click="getCode">获取验证码</button>
  9. <span class="error-msg">{{errors.first("code")}}</span>
  10. </div>

密码校验:

  1. <div class="content">
  2. <label>登录密码:</label>
  3. <input type="password"
  4. placeholder="请输入你的登录密码"
  5. v-model="password"
  6. name="password" v-validate="{required:true,regex:/^[0-9a-zA-Z]{8,20}$/}" :class="{invalid:errors.has('password')}"
  7. >
  8. <span class="error-msg">{{errors.first("password")}}</span>
  9. </div>

确认密码校验:

  1. <div class="content">
  2. <label>确认密码:</label>
  3. <input type="password"
  4. placeholder="请输入确认密码"
  5. v-model="rePassword"
  6. name="rePassword" v-validate="{required:true,is:password}" :class="{invalid:errors.has('rePassword')}"
  7. >
  8. <span class="error-msg">{{errors.first("rePassword")}}</span>
  9. </div>

效果图:

 

 

对于勾选同意协议的复选框,必须自定义规则

  1. //自定义校验规则
  2. VeeValidate.Validator.extend("agree",{
  3. validate:value=>{
  4. return value
  5. },
  6. getMessage:field=>field+"必须同意"
  7. })

修改模板里的内容:

  1. <div class="controls">
  2. <input
  3. type="checkbox"
  4. :checked="agree"
  5. name="agree" v-validate="{required:true,'agree':true}" :class="{invalid:errors.has('agree')}"
  6. >
  7. <span>同意协议并注册《尚品汇用户协议》</span>
  8. <span class="error-msg">{{errors.first("agree")}}</span>
  9. </div>

如果所有的表单验证都通过,再向服务器发送请求进行注册

  1. async userRegister(){
  2. const success=await this.$validator.validateAll()
  3. if(success){
  4. try{
  5. const {phone,code,password,rePassword}=this
  6. await this.$store.dispatch("userRegister",{phone,code,password})
  7. this.$router.push("/login")
  8. }catch(error) {
  9. alert(error.message)
  10. }
  11. }
  12. }

12.7 路由懒加载

当打包构建应用时,JavaScript包会变的非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,就会更加高效

以Search组件为例:

  1. {
  2. name:'search',
  3. path:"/search/:keyword?",
  4. component:()=>import("@/pages/Search.vue"),
  5. meta:{show:true}
  6. }

12.8 处理map文件

打包:yarn build

项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列又错了

文件如果项目不需要可以去掉

Vue.config.js配置中productionSourceMap:false可以不生成体积很大的map文件

  productionSourceMap:false

12.9 购买服务器

可以去阿里云或者腾讯云购买服务器,腾讯云更便宜


完结撒花❀❀~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/69902
推荐阅读
相关标签
  

闽ICP备14008679号