① 尚品汇的前台开发笔记【尚硅谷】【Vue】

文章目录

  • 一、采用vue-cli去初始化项目
  • 二、项目的其他配置
  • 三、项目路由的分析
  • 四、完成非路由组件Header与Footer业务
    • 开发项目的步骤
    • 使用组件的步骤(非路由组件)
    • 完成路由组件的搭建
    • Footer组件显示与隐藏
    • 路由传参
    • 路由传参相关面试题
    • 解决问题
  • 五、完成Home首页模块业务
    • 开发项目的步骤
    • 完成Home首页拆分静态组件
      • 完成TypeNav 三级联动组件--全局组件
      • 完成其余静态组件--局部组件
    • POSTMAN测试接口是否正常
    • axios二次封装
    • 接口统一管理
    • nprogress进度条的使用
    • vuex状态管理库
    • 完成TypeNav三级联动
      • 完成TypeNav三级联动展示数据业务
      • 完成一级分类动态添加背景颜色
      • 通过JS控制二三级商品分类的显示与隐藏
      • 函数的防抖和节流
      • 完成三级联动节流的操作
      • 完成三级联动组件的路由跳转与传递参数
      • Search模块中商品分类的显示与隐藏
      • TypeNav商品分类列表的优化
      • 合并params与query参数
    • 开发Home首页当中的ListContainer组件与Floor组件
      • 获得ListContainer组件的mock模拟数据
      • listContainer轮播图
      • 开发floor组件
      • 组件通信的方式有哪些?
      • 将首页的轮播图封装为全局组件
  • 六、完成Search搜索模块业务
    • search模块vuex操作
      • 发起api请求
      • vuex 把数据放到store仓库
      • 让组件拿到仓库的数据
      • Search模块中动态展示产品列表
      • search模块根据不同的参数获取数据展示
    • search模块子组件的动态开发
    • Search面包屑开发
      • Search面包屑处理分类的操作
      • 面包屑处理关键字
      • 面包屑处理品牌信息
    • Search的子组件平台售卖属性的操作
    • Search模块的排序操作(重点)
    • 分页器功能分析(重点)
      • 分页器静态组件
      • 分页功能分析
      • 分页器起始与结束数字计算
      • 分页器动态展示
      • 替换数据
      • 分页器添加类名
  • 七、完成Detail某一个产品的详情页面
    • 配置详情 路由 src/router/index.js
      • 滚动行为
    • 产品详情数据获取
      • api--请求接口
      • vuex--获取产品详情信息给仓库
      • 发请求捞数据
    • 产品详情展示动态数据
    • Zoom放大镜操作
      • Zoom放大镜展示数据
      • 放大镜下的轮播图数据展示
      • 放大镜轮播图操作
      • 放大镜
    • detail路由组件展示商品售卖属性
    • 购买产品个数的操作
  • 八、加入购物车

一、采用vue-cli去初始化项目

  1. 准备这些: node √+ webpack + 淘宝镜像√

  2. 进入到项目文件夹 输入 cmd

    输入 vue create app 去初始化项目

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第1张图片

  3. 介绍目录project-SPH/app

    node_modules文件夹:放置项目的依赖

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

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

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

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

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

    ​ —main.js:程序的入口文件,也是整个程序当中最先执行的文件

    babel.config.js:配置文件,与babel相关

    package.json文件:记录着项目信息,叫什么…有哪些依赖…项目怎么运行…

    package-lock.json:缓存性文件

    README.md:说明性文件

二、项目的其他配置

  1. eslint校验功能关闭

    — 在根目录下,创建一个vue.config.js的文件,加入这行代码lintOnSave:false

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第2张图片

  2. src文件夹简写方式,配置别名

    jsconfig.json 配置别名 @ 提示 【@代表的时候src文件夹,这样将来文件过多,找的时候方便很多】
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第3张图片

三、项目路由的分析

vue-router

前端所谓路由:key-value键值对

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

value:相应的路由组件

注意:该项目是 上中下结构 ---- 跟品优购一样…

非路由组件:Header,Footer(首页,搜索页)

路由组件:Home首页路由组件,Search路由组件,login登录路由组件(无Footer),register路由组件(无Footer)

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第4张图片

四、完成非路由组件Header与Footer业务

开发项目的步骤

  1. 书写静态页面(HTML,CSS)
  2. 拆分组件
  3. 获取服务器的数据动态展示
  4. 完成相应的动态业务逻辑

注意1:创建组件的时候,找准 组件结构 + 组件的样式 + 图片资源

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第5张图片

注意2:咱们项目采用的是less样式,浏览器不识别less样式,需要通过less,less-loader【安装 六 版本的】进行处理less,把less样式变为css样式,浏览器才可以识别npm i less-loader@6

使用组件的步骤(非路由组件)

  • 创建或定义组件
  • 引入组件
  • 注册组件
  • 使用组件

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第6张图片

完成路由组件的搭建

  1. 安装vue-router插件 npm i vue-router@3

    路由组件具体学习–> Vue | 路由

    路由组件有四个:Home,Search,Login,Register(注册)

  • components文件夹:经常放置 非路由组件 (公用全局组件)
  • pages|views文件夹:经常放置 路由组件
  1. 编写router配置项:src/router/index.js
// 配置路由的地方
import Vue from 'vue';
import VueRouter from 'vue-router';
// 使用插件
Vue.use(VueRouter);

// 引入路由组件
import Home from '../pages/Home'
import Login from '../pages/Login'
import Register from '../pages/Register'
import Search from '../pages/Search'

// 配置路由
export default new VueRouter({
    // 配置路由
    routes:[
        {
            path:'/home',
            component:Home
        },
        {
            path:'/login',
            component:Login
        },{
            path:'/segister',
            component:Register
        },{
            path:'/search',
            component:Search
        },
    ]
})
  1. 引入和注册路由在 src/main.js
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第7张图片

  2. 使用路由组件 指定展示位置
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第8张图片

  3. 总结:

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

    1. 路由组件一般放置在pages|views文件夹,非路由组件一般放置在components文件夹中
    2. 路由组件一般需要在router文件夹中进行注册(使用的为组件名字),非路由组件在使用的时候,一般都是以标签的形式使用
    3. 注册完路由,不管是路由组件还是非路由组件,身上都有 $route,$router属性

    $route:一般获取路由信息【路径,query,params等等】

    $router:一般进行编程式导航进行路由跳转【push|replace】

  4. 重定向:在项目跑起来的时候,访问/,立马让他定向到首页 写在 src/router/index.js

        // 重定向:在项目跑起来的时候,访问/,立马让他定向到首页
        {
            path:'*',
            redirect:'/home'
        }

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第9张图片

  1. 路由的跳转?

    路由的跳转有两种形式:

    1. 声明式导航router-link
    2. 编程式导航 push| replace

    声明式路由导航可以做的事情 编程式导航都能做,除此之外,编程式导航还能做一些其他的业务

Footer组件显示与隐藏

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

Footer组件在 Home,Search中是显示的,在登录和注册页面中是隐藏的

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

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第10张图片
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第11张图片

  2. 路由元信息

    配置路由的时候,可以给路由添加路由元信息【meta】,路由需要的配置对象不能瞎写胡写
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第12张图片

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第13张图片
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第14张图片

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第15张图片
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第16张图片

路由传参

  1. 路由跳转有几种方式?

    声明式导航:router-link(务必要有to属性)

    编程式导航:利用的是组件实例的$router.push | replace 方法

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

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

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

    **特别注意:**路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置!params需要去占位!

      // 搜索按钮的回调函数,需要向Search路由进行跳转
      goSearch(){
        // 路由传递参数
        this.$router.push({
            
          // 第一种:字符串形式
          // path:'/search' + this.keyWord + "?k="+ this.keyWord.toUpperCase(),
            
          // 第二种:模板字符串
          // path:`/search/${this.keyWord}?k=${this.keyWord.toUpperCase()}`
            
          // 第三种:对象写法
          name:'sousuo',
          // params参数
          params:{
            keyWord:this.keyWord,
          },
          // query参数
          query:{
            k:this.keyWord.toUpperCase()
          }
        })
      }

路由传参相关面试题

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

    不可以。

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

  2. 如何指定params参数可传可不传?

    在配置路由的时候,给params占位 的后面加上?,代表可传递也可以不传递
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第17张图片

    比如:配置路由的时候,已经给params参数占位了,但是路由跳转的时候就不传递参数,路径会出现问题

    你跳转的本来是 http://localhost:8081/#/search/k=QWE 这个位置,结果你跳转的是 http://localhost:8081/#/k=QWE 这个位置

  3. params参数可传递也可以不传递,但是如果传递是空串,如何解决?

    传递的是空串的话,路径有问题(和上面路径问题一样)

    使用undefined解决:params参数可传递不可传递的时候,传递是空串路径 有问题的错误

              params:{
                keyWord:'' || undefined,
              },
    
  4. 路由组件能不能传递props数据?

    可以的。

    关于路由相关详细笔记---->路由的props配置

    配置props写在 src/router/index.js 里

    第一种写法props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第18张图片

    第二种写法:值为布尔值,若布尔值为真,就会把该路由组件收到的params参数,以props的形式传给Detail组件 -------缺点:query参数不能用这个

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第19张图片
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第20张图片

    写法三:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
    个人认为这个挺好

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第21张图片

解决问题

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

​ 路由跳转有两种形式:声明式导航,编程式导航

​ 声明式导航没有这类问题,因为vue-router底层已经处理好了

  1. 为什么编程式导航进行路由跳转的时候,就有这种警告错误?

    “vue-router”: “^3.5.4”:最新的vue-router引入promise,promise有两个形参,成功返回的函数和失败返回的函数

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

  3. 通过底部的代码可以实现解决错误

    this.$router.push({
              // 第三种:对象写法
              name:'sousuo',
              // params参数
              params:{
                keyWord:'' || undefined,
              },
              // query参数
              query:{
                k:this.keyWord.toUpperCase()
              }
            },()=>{},()=>{})
    

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

  4. 重写push 与 replace 方法

    // src/router/index.js
    
    // 先把VueRouter原型对象的push,先保存一份
    const originalPush = VueRouter.prototype.push
    const originalReplace = VueRouter.prototype.replace
    
    // 重写push | replace
    // 参数:告诉原来的push方法,你往哪里跳转(传递哪些参数)
    VueRouter.prototype.push = function push(location) {
      return originalPush.call(this, location).catch(err => err)
    }
    VueRouter.prototype.replace = function replace(location) {
      return originalReplace.call(this, location).catch(err => err)
    }
    

五、完成Home首页模块业务

开发项目的步骤

  1. 书写静态页面(HTML,CSS)
  2. 拆分组件
  3. 获取服务器的数据动态展示
  4. 完成相应的动态业务逻辑

Home拆分为7个组件

完成Home首页拆分静态组件

完成TypeNav 三级联动组件–全局组件

三级联动Home,Search,Detail组件都在使用,可以把它注册为全局组件

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

  • 创建组件
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第22张图片

  • 引入组件,注册组件
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第23张图片

  • 使用组件

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第24张图片

完成其余静态组件–局部组件

  • 创建组件

    导入结构 样式 查看图片位置有无错误进行修改

  • 引入组件

  • 注册组件

  • 使用组件
    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第25张图片

POSTMAN测试接口是否正常

我用的是Vscode插件【 Postcode】

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第26张图片

刚刚经过测试,接口没有问题

​ 如果服务器返回的数据code是200,代表服务器返回数据成功

​ 整个项目,接口前缀都有/api

最新接口地址:

http://gmall-h5-api.atguigu.cn

后台文档swagger地址:
http://39.98.123.211:8510/swagger-ui.html#/

首页三级联动的请求地址是: /api/product/getBaseCategoryList

请求方式是 GET

axios二次封装

向服务器发请求的方式有:XMLHttpRequest,fetch,JQ,axios

这里用axios

  1. :为什么需要二次封装axios

    因为要用到 请求和响应拦截器。

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

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

在终端里安装axiosnpm i axios

  1. 在项目当中经常会出现 api 文件夹,一般是放关于【axios】请求的

    baseURL:'/api',:基础路径,发请求的时候,路径当中会出现基础api

    timeout:5000,: 代表请求超时的时间5s,在5s之内没有响应就失败了

  2. axios基础不好,可以参考 git | axios 文档

// src/api/request.js

// 对于axios进行二次封装
import axios from 'axios';

// 1. 利用axios对象的方法create,去创建一个axios实例
// 2. request就是axios,只不过稍微配置一下
const requests = axios.create({
    // 配置对象
    // 基础路径,发请求的时候,路径当中会出现基础api
    baseURL:'/api',
    // 代表请求超时的时间5s,在5s之内没有响应就失败了
    timeout:5000,
});

// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
    // config:配置对象,对象立马有一个属性很重要,headers请求头
    return config;
})

// 响应拦截器
requests.interceptors.response.use((res)=>{
    // 成功的回调函数:服务器相应数据回来以后,相应拦截器可以检测到,可以做一些事情
    return res.data;
},(error)=>{
    // 服务器响应失败的回调函数
    return Promise.reject(new Error('failed'))
})

// 对外暴露
export default requests;

接口统一管理

项目很小:完全可以在组件的生命周期函数中发请求

项目大,有很多接口:axios.get(‘xxx’)

// src/api/index.js

// 当前这个模块:api进行统一管理
import requests from "./request";

// 三级联动接口
// /api/product/getBaseCategoryList   GET  无参数

export const reqCategoryList = ()=>{  // 要请求数据的时候直接调用reqCategoryList这个函数就可以了
    // 发请求:axios发请求返回结果是Promise对象
   return requests({url:'/product/getBaseCategoryList',method:'GET',})
}
  1. 跨域问题

    什么是跨域:协议,域名,端口号不同的请求,称之为跨域

    从这里http://localhost:8081/#/home ----前端项目本地服务器

    向这里发请求 http://gmall-h5-api.atguigu.cn ---- 后台服务器

  2. 跨域的解决方案:JSONP,CROS,数据代理

    // Vue.config.js 
    
    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
      transpileDependencies: true,
      // 关闭eslint
      lintOnSave: false,
      // 代理跨域
      devServer: {
        proxy: {
          '/api': {// 匹配所有以 '/api'开头的请求路径
            target: 'http://gmall-h5-api.atguigu.cn',// 获取数据的目标地址
            changeOrigin: true,
          },
        }
      }
    })
    

具体跨域看这里—>解决开发环境Ajax跨域问题

nprogress进度条的使用

安装nprogress插件 npm i nprogress

只要项目当中发请求,进度条就开始往前动,服务器数据返回之后,进度条就结束

用在 请求和响应拦截器 src/api/request.js

nprogress.start方法:进度条开始

nprogress.done方法:进度条结束
① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第27张图片

vuex状态管理库

  1. vuex是什么?

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

    切记,并不是全部项目都需要vuex

    如果项目很小,完全不需要vuex

    如果项目很大,组件很多,数据很多,数据维护很费劲,需要vuex

安装vuex npm i vuex@3

  1. vuex基本使用

    看这个 -->Vue | vuex【理解 vuex + vuex开发者工具的使用 + vuex的基本使用和API + 四个map方法的使用+多组件共享数据 +模块化+命名空间 】_不爱吃菜的蔡菜的博客-CSDN博客

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第28张图片

  1. vuex实现模块化开发

    如果项目过大,组件过多,接口也很多,数据也很多,可以让vuex实现模块化开发

    1. 创建模块化开发的小仓库,每一个组件都可以有一个小仓库

      ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第29张图片

    2. 引入小仓库

      ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第30张图片

    3. 在入口文件引入总仓库并注册

      ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第31张图片

      注册仓库,组件实例的身上会多一个$store属性

完成TypeNav三级联动

完成TypeNav三级联动展示数据业务

当TypeNav挂载完毕就开始请求数据然后展示数据

这里模块化记得给每个组件开启命名空间!!!namespace:true

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第32张图片

让TypeNav组件挂载,挂载的时候通知vuex发请求,将数据存储于 home仓库中,用dispatch,于是要书写actions

actions执行的时候,要通过api里面的接口函数调用,向服务器发请求,获取服务器的数据

​ 需要把之前的api引入进来,在这里发请求就是要调用这个reqCategoryList函数

发请求后返回的是一个promise函数,当里面的code是200,代表成功,我们要修改仓库里面的数据

我们要解构commit,提交给mutations

result.data是一个数组形式

mutations里面开始更改数据

传入的参数是state是数据,categoryList这个参数就是服务器返回的result.data

仓库当中应该有个起始值,于是让state中的数据类型为服务器返回的数据类型

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第33张图片

result 数据是
在这里插入图片描述

这个时候home仓库已经有了相应的数组,但是我们需要在TypeNav组件中拿到数据并进行展示

进入到TypeNav组件当中,用mapState去获取组件中的数据

  • TypeNav组件获取数据

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第34张图片

  • 获得数据,然后渲染到页面上

对页面的三级联动进行分析

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第35张图片

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第36张图片

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第37张图片

完成一级分类动态添加背景颜色

  1. 采用样式完成

    .item:hover{
    	background:skyblue;
    }
    
  2. 通过JS完成

    给一级分类添加鼠标经过和鼠标离开事件

              <div
                class="item"
                v-for="(c1, index) in categoryList"
                :key="c1.categoryId"
                :class="{ cur: currentIndex == index }"
              >
              
                <h3 @mouseenter="changeIndex(index)" @mouseleave="leaveIndex">
                  <a href="">{{ c1.categoryName }}a>
                h3>
                  ....
    		  div>
    
    

    先设置一个响应式属性,存储用户鼠标移上哪一个一级分类currentIndex = -1 ;代表鼠标谁都没有移上去

    然后鼠标进入和移出 修改响应式数据currentIndex属性

    当data中的currentIndex==该item中的index时添加cur属性

      data() {
        return {
          // 响应式属性,存储用户鼠标移上哪一个一级分类
          currentIndex: -1, // 代表鼠标谁都没有移上去
        };
      },
      methods: {
        // 鼠标进入修改响应式数据currentIndex属性
        changeIndex(index) {
          // index 鼠标移上某一个一级分类的元素的索引值
          this.currentIndex = index;
        },
        // 一级分类鼠标移出的事件回调
        leaveIndex(){
          // 鼠标移出currentIndex=-1
          this.currentIndex =-1;
        }
      },
    

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

  1. 通过CSS样式display:block|none;来控制二三级商品分类的显示与隐藏

  2. 通过JS控制二三级商品分类的显示与隐藏

                
           
    .....

函数的防抖和节流

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

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

防抖:前面的所有的触发都被取消,最后一次执行在规定的事件之后才会触发,也就是说如果连续的快速触发,只会执行一次 ----------------------当事件被触发后,延迟 n 秒后再执行回调

具体看这个笔记---->防抖和节流

lodash插件:里面封装函数的防抖与节流的业务【闭包+延迟期】

npm i lodash

  1. lodash函数库对外暴露的是 _函数-----------比如:JQ暴露的是$函数

  2. lodash.debounce | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)防抖函数

  3. lodash.throttle | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)节流函数

完成三级联动节流的操作

鼠标来回滑动的时候,把频繁触发变成少量触发,进行节流

// 这种引入的方式,是把lodash全部功能函数引入
import _ from 'lodash';
// 最好的引入方式:按需加载
import {throttle} from 'lodash/throttle'
...

    // 鼠标进入修改响应式数据currentIndex属性
    changeIndex:throttle(function(index){
      // index 鼠标移上某一个一级分类的元素的索引值
       this.currentIndex = index;
    },50),

这里的throttle回调函数别用箭头函数,可能出现上下文this问题

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

  • 当你点击分类的时候,会从home模块跳转到search模块,并且把分类的名字categoryName和ID传递给search模块

  • 然后search模块拿到这些参数向服务器发请求展示相应数据

路由跳转:

声明式导航 :router-llink

编程式导航 :push | replace

最好的解决方案:编程式导航+事件委派

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

  1. 你怎么知道点击的是a标签 
       1. 事件委派,是把全部的子节点【h3,dt,dl,em】的事件委派给父亲节点
       2. 点击a标签的时候,才会进行路由跳转
  1. 如何获取参数【1,2,3级分类的产品的名字和ID】

给a标签加上自定义属性:data-categoryName="c3.categoryName",如果有这个自定义属性就是a标签

利用event.target获取事件对象的子节点

    goSearch(event){
      // 最好的解决方案:编程式导航+事件委派
      // 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
      console.log(event.target);
    }

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第38张图片

注意:这里的自定义属性时全小写的,我加上的是驼峰写法!

节点有一个属性dataset属性,它可以获取节点的自定义属性与属性值

    goSearch(event){
      // 最好的解决方案:编程式导航+事件委派
      // 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
      let element = event.target;
      console.log(element.dataset);
    }

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第39张图片

    goSearch(event){
      // 最好的解决方案:编程式导航+事件委派
      // 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
      let element = event.target;
      console.log(element.dataset); // 返回的是一个对象
      // 解构对象
      let {categoryname} = element.dataset;
      // 如果标签身上有categoryname属性,一定是a标签
      if(categoryname){
        alert(123)
      }
    }
  • 上面已经知道了点击的是否是a标签

  • 下面解决怎么知道是一级分类还是二级分类的a标签

和上面一样,给a标签添加自定义属性:data-category1Id="c1.categoryId"

另外路由要进行跳转和传参,我们这里选择编程式导航携带query参数

  1. 首先location 是要跳转的路由是哪里,携带什么参数,这里携带参数是根据一级分类二级分类来看的,所以先不写携带的query参数,光写上跳转的路由组件name
  2. query参数另定义,根据解构 element.dataset对象来获得a标签的分类ID,根据if语句来判断他在哪一级分类,如果是第一级分类,就在query参数里面添加一级分类的ID…
  3. 最后整理参数,让location里面有query,然后实现路由的跳转
    goSearch(event) {
      // 最好的解决方案:编程式导航+事件委派
      // 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
      let element = event.target;
      console.log(element.dataset); // 返回的是一个对象
      // 解构对象
      let { categoryname, category1id, category2id, category3id } =
        element.dataset;
      // 如果标签身上有categoryname属性,一定是a标签
      if (categoryname) {
        // 整理路由跳转的参数
        let location = { name: "sousuo"};
        let query = {categoryName:categoryname}; 
        // 怎么知道一级分类还是二级分类的a标签
        if (category1id) {
          query.category1Id = category1id;
        } else if (category2id) {
          query.category2Id = category2id;
        } else if (category3id) {
          query.category3Id = category3id;
        }
        // 整理完参数,现在location和query是两个参数
        location.query = query;
        // 实现路由的跳转
        this.$router.push(location);
      }
    },

Search模块中商品分类的显示与隐藏

  1. 开发search模块中的TypeNav商品分类菜单(过渡动画)

    • 用一个响应式属性来控制 从Home进入到Search的时候 显示与隐藏
      ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第40张图片
      ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第41张图片

当从Home进入到Search的时候,TypeNav会再一次挂载,所以当进入的时候,让自定义属性show变成false
① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第42张图片

判断当前路由是不是search

  mounted() {
    // 通知Vuex发请求,获取数据,存储于仓库当中,当前三级联动在home组件当中!!!
    this.$store.dispatch("home/categoryList");
    // 当组件挂载完毕,让show的属性变为false
    // 如果不是home路由组件,将TypeNav进行隐藏
    if(this.$route.path !== '/home'){
      this.show = false;
    }
  },
  • 显示隐藏效果:当鼠标移进移出的时候

这里用到事件委派

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第43张图片

当鼠标移入的时候,让商品分类列表进行展示,让 this.show = true;

鼠标移出的时候,隐藏,让this.show = false;

注意!如果在首页的话就不用隐藏了
① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第44张图片

  • 过渡动画效果-------->http://t.csdn.cn/On1U6

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

加上过渡动画,需要用transition包裹,取名字为sort

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第45张图片

在CSS样式里书写过渡动画 -------注意:动画样式和.sort是同一级别的

    // 过渡动画的样式
    // 过渡动画开始状态(进入)
    .sort-enter,.sort-leave-to{
      height: 0;
    }
    .sort-enter-to,.sort-leave{
      height: 461px;
    }
    // 定义动画事件,速率
    .sort-enter-active,.sort-leave-active{
      transition: all 0.3s linear;
    }

TypeNav商品分类列表的优化

现在咱们的商品分类三级列表可以优化

​ 当路由跳转的时候—home和search来回切换的时候 会重新发起TypeNavgetBaseCategoryList的数据请求

​ 我们这里优化:只发起一次请求可以更好的优化性能

:哪个地方只执行一次?

最先执行的是入口文件 main.js,引入了App组件,App是唯一的根组件,它的mounted只会执行一次,且App组件早就先执行的,所以把TypeNav的 派发action 放到App根组件里

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第46张图片

不能放到入口文件上,只能放在组件上,因为只有组件上面才有$store

合并params与query参数

当你先点击三级列表然后再搜索的时候,传递的参数可以又有params参数,也有query参数

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第47张图片

  1. 判断:点击三级列表的时候,如果路由跳转携带query参数的时候,也要带有params参数,捎带着传递过去

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第48张图片

  2. 点击搜索的时候,携带params参数,但是也可以带有query参数,也给带进去

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第49张图片

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

注意!接口返回的数据只有商品菜单分类数据(三级联动),对于ListContainer组件与Floor组件数据服务器没有提供的

获得ListContainer组件的mock模拟数据

mock模拟数据:如果你想mock数据,需要用到一个插件mockjs

前端mock数据不会和你的服务器进行任何通信

mockjs文档

安装mockjsnpm i mockjs

使用步骤:

  1. 在项目当中src文件夹中创建mock文件夹--------提供假数据的

  2. 准备JSON数据(mock文件夹中创建相应的JSON文件夹)-----记得格式化一下

    1. 首页广告轮播数据: src/mock/banner.json
    2. 首页楼层数据: src/mock/floor.json
  3. 把mock数据需要的图片放置到public文件夹中【public文件夹在打包的时候,会把相应的资源原封不动打包到dist文件夹中】

  4. 在mock文件夹下创建mockServe.js开始mock 虚拟的数据了,通过mockjs模块实现

    // 引入mockjs模块
    import Mock from 'mockjs'
    // 把JSON数据格式引入进来[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});  // 模拟楼梯floor
    
  5. mockServe.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)

    // 引入MockServe.js-----mock数据
    import 'src/mock/mockServe';
    
  6. 获取到Banner轮播图的数据

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第50张图片

  7. 当ListCointainer挂载的时候开始 发请求

    1. 组件派发action:通过vuex发起ajax请求,将数据存储在仓库中

      //   src/pages/Home/ListContainer  
      this.$store.dispatch('home/getBannerList');  // 调用仓库中的这个函数
      
      //   src/store/home
      
      const actions = {
      // 获取首页Banner轮播图的数据
          async getBannerList({commit}) {
              let result = await reqGetBannerList();
              console.log(result);
              if (result.code === 200) {
                  commit('GETBANNERLIST', result.data)
              }
          }
      }
      
      const mutations = {
          GETBANNERLIST(state,bannerList){
              state.bannerList = bannerList;
          }
      };
      
    2. 现在仓库已经有数据了,但是listContainer没有数据----引入仓库中的数据

      //   src/pages/Home/ListContainer
      
      import {mapState} from 'vuex';
      export default {
        name: "ListContainer",
        mounted(){
          // 派发action:通过vuex发起ajax请求,将数据存储在仓库中
          this.$store.dispatch('home/getBannerList');  // 调用仓库中的这个函数
        },
        computed:{
          ...mapState('home',['bannerList'])
        }
      }
      

listContainer轮播图

swiper基本使用 —> Swiper中文网-轮播图幻灯片js插件,H5页面前端开发

  1. 安装swiper插件 npm i swiper@5

  2. 引入相应的包(js/css)

    样式在入口文件引入,因为下面也有轮播图样式和这个一样

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第51张图片

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第52张图片

  3. 引入你需要的页面中的结构

    carousel 轮播图的意思

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第53张图片

    v-for 在遍历服务器返回的数据,要等服务器返回完数据后才渲染遍历

  4. 给轮播图添加动态效果

    在new Swiper实例之前,页面中结构必须要有!

    当把new Swiper实例放在mounted这里发现不行

    – 因为dispatch当中涉及到异步语句,导致v-for遍历的时候结构还没有完全

    1. 弄成定时器(不完美)

      //   src/pages/Home/ListContainer
      
      mounted() {
          // 派发action:通过vuex发起ajax请求,将数据存储在仓库中
          this.$store.dispatch("home/getBannerList"); // 调用仓库中的这个函数
          setTimeout(() => {
            var mySwiper = new Swiper(".swiper-container", {
              loop:true,
              cssMode: true,
              navigation: {
                nextEl: ".swiper-button-next",
                prevEl: ".swiper-button-prev",
              },
              pagination: {
                el: ".swiper-pagination",
                clickable:true, // 点击小球的时候也切换
              },
              mousewheel: true,
              keyboard: true,
            });
          }, 800);
        },
      
    2. 完美写法

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

      1. 监听bannerList数据的变化,因为这条数据 —由空数组变为数组里面有四个元素
      2. 通过watch监听bannerList属性的属性值的变化
      3. 如果执行handler方法,代表组件实例身上这个属性的属性值数组已经有了四个元素
      4. 但是这里 结构没有渲染完成 ,现在只能保证数据已经有了,但是没法保证v-for已经执行完成了

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

      当你执行这个回调的时候,保证服务器的数据回来了,v-for执行完毕了【轮播图的结构已经完成了】

      $nextTick:可以保证页面中的结构一定是有的,经常和很多插件一起使用【都需要DOM存在】

      //   src/pages/Home/ListContainer
      
      watch: {
          // 监听bannerList数据的变化,因为这条数据 ---由空数组变为数组里面有四个元素
          bannerList: {
            handler() {
              // 通过watch监听bannerList属性的属性值的变化
              // 如果执行handler方法,代表组件实例身上这个属性的属性值数组已经有了四个元素
              // nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
              this.$nextTick(function () {
                var swiper = new Swiper(".swiper-container", {
                  loop:true,
                  cssMode: true,
                  navigation: {
                    nextEl: ".swiper-button-next",
                    prevEl: ".swiper-button-prev",
                  },
                  pagination: {
                    el: ".swiper-pagination",
                  },
                  mousewheel: true,
                  keyboard: true,
                });
              });
            },
          },
        },
      

开发floor组件

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第54张图片

将动态数据渲染到页面上的步骤:

​ ①静态组件完成以后写 api 发请求,

​ ②数据存储到仓库,

​ ③组件拿到仓库里的数据,

​ ④然后数据渲染到页面上

  1. 静态组件完成以后写 api 发请求

    //   src/api/index.js
    
    // 获取floor组件的数据
    export const reqGetFloorList = ()=>{
        // 发请求
        return mockRequests.get('/floor');// 获取mock里面floor的数据
    }
    
  2. 数据存储到仓库 src/store/home/index.js

    1. actions

          // 获取floor的数据
          async getFloorList({ commit }) {
              let result = await reqGetFloorList();
              console.log(result);
              if (result.code === 200) {
                  commit('GETFLOORLIST', result.data)
              }
          }
      
    2. state

      const state = {
          // state中数据默认初始值别瞎写类型【根据接口返回值初始化】
          categoryList: [], // 服务器返回的数据是数组
          // 轮播图的数据
          bannerList: [],
      	// 请求floor数据的格式是数组    
          floorList:[],
      };
      
    3. mutations

      const mutations = {
      	...
          // 开始更换数据
          GETFLOORLIST(state,floorList){
              state.floorList = floorList;
          }
      };
      
    4. dispatch

      :getFloorList在哪里触发?

      ​ 因为主页结构里面有两个floor,服务器发送的数据是一个数组里面有两个对象[{ },{ }],如果你请求数据在floor组件里面请求,那么你请求到的数据怎么给另一个floor,你没办法遍历出两个floor组件,所以发请求应该在Home路由组件里面触发

      //   src/pages/Home/index.vue
      
      	mounted(){
            // 派发action,获取floor组件的数据
            this.$store.dispatch("home/getFloorList")
          }
      

      ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第55张图片

  3. 让Home组件拿到数据

    //   src/pages/Home/index.vue
    import {mapState} from 'vuex';
    
    export default {
        name:'Home',
        // 注册组件
        components:{
          ListContainer,
          Recommend,
          Rank,
          Like,
          Floor,
          Brand
        },
        mounted(){
          // 派发action,获取floor组件的数据
          this.$store.dispatch("home/getFloorList")
        },
        computed:{
          ...mapState('home',['floorList']) // 获得state大仓库下的home里面的floorList
        }
    }
    

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第56张图片

  4. 数据渲染到页面

    v-for在自定义遍历使用

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第57张图片

    现在数据在home组件上面拿着,但是floor要有数据,第一个floor用数组里的第一个对象,第二个floor用数组里的第二个对象---->父子组件通信

        <Floor v-for="(floor,index) in floorList" :key = floor.id  :list="floor"></Floor>
    
    export default {
      name: "Floor",
      props:['list'],
    };
    

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第58张图片

  5. 动态展示数据

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第59张图片

    开始替换数据

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第60张图片

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第61张图片

    开始轮播图

                  <div class="swiper-container" id="floor1Swiper">
                    <div class="swiper-wrapper">
                      <div
                        class="swiper-slide"
                        v-for="carousel in list.carouselList"
                        :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>
    ...
    export default {
      name: "Floor",
      props: ["list"],
      mounted() {
        var swiper = new Swiper(".swiper-container", {
          cssMode: true,
          navigation: {
            nextEl: ".swiper-button-next",
            prevEl: ".swiper-button-prev",
          },
          pagination: {
            el: ".swiper-pagination",
          },
          mousewheel: true,
          keyboard: true,
        });
      },
    };
    

    :为什么这里可以挂载的时候可以new Swiper实例?

    ​ 第一次书写轮播图的时候,在当前组件的内部发请求以及动态的渲染结构【前提是只要服务器的数据要回来渲染完毕】,因此不行

    ​ 这里的时候,发请求是在父组件Home里面挂载完毕发的,父组件把数据给子组件,所以子组件早就把数据遍历完了,可以在mounted挂载的时候可以new Swiper实例

组件通信的方式有哪些?

  1. props:用于父给子组件通信的

  2. 自定义事件:$on $emit 可以实现子给父通信

  3. 全局事件总线:$bus 全能

  4. 消息订阅与发布 pubsub-js:vue中几乎不用 全能

  5. 插槽slot :适用于 父组件 ===> 子组件

  6. vuex:全能

将首页的轮播图封装为全局组件

以后在开发项目的时候,如果看到某一个组件在很多地方都使用,你把它变成全局组件,注册一次,可以在任意地方使用,公用的组件 | 非路由组件放到components文件夹中

结构,样式,行为要几乎一样才能封装成全局组件,大家一起公用

现在 ListContainer 和 Floor 的JS 代码有点不一样

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第62张图片

为什么watch监听不到list:是因为这个数据没有发生变化(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)

为了可以监听到list: immediate: true, // 立即监听,不管你数据有没有变化,立即上来监听一次

步骤:

  1. 先把JS结构改成相似的

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第63张图片

  2. 去components文件夹下创建Carousel轮播图的文件夹,把轮播图的结构和JS代码放进去(因为之前已经把轮播图样式在入口文件main.js中引入了,所以这里不需要再放置轮播图样式了)

    <template>
      <div class="swiper-container" ref="cur">
        <div class="swiper-wrapper">
          <div
            class="swiper-slide"
            v-for="carousel 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>
    // 引入swiper
    import Swiper from "swiper"
    export default {
      name: "Carousel",
      props:['list'],
        watch: {
        list: {
          // 监听list
          immediate: true, // 立即监听,不管你数据有没有变化,立即上来监听一次
          handler() {
            this.$nextTick(function(){
              var swiper = new Swiper(this.$refs.cur, {
                cssMode: true,
                navigation: {
                  nextEl: ".swiper-button-next",
                  prevEl: ".swiper-button-prev",
                },
                pagination: {
                  el: ".swiper-pagination",
                },
                mousewheel: true,
                keyboard: true,
              });
            })
          },
        },
      },
    };
    </script>
    
  3. 其中 v-for遍历的是 父组件 通信给子组件的数据,统称为list

  4. 然后 当 ListContainer 和 Floor组件需要用到轮播图的时候,在结构中引入

    ListContainer 中传入的数据是bannerList,Floor中传入的数据是 list.carouselList

六、完成Search搜索模块业务

开发项目的步骤

  1. 书写静态页面(HTML,CSS)
  2. 拆分组件
  3. 获取服务器的数据动态展示
    1. 发起api请求
    2. vuex 把数据放到store仓库
    3. 组件拿到仓库里的数据
    4. 动态渲染
  4. 完成相应的动态业务逻辑

search模块vuex操作

  1. 发起api请求
  2. vuex 把数据放到store仓库
  3. 组件拿到仓库里的数据
  4. 动态渲染

发起api请求

//    src/api/index.js

// 获取搜索模块的数据   /api/list     POST  需要带参数
export const reqGetSearchInfo = (params)=>{ // params是服务器传递的参数,至少是一个空对象
    // 发请求
    return requests({url:'/list',method:'POST',data:params}) // data 是携带的参数
}

当前这个【获取搜索模块的数据】 接口,会给服务器传递一个默认参数,必须得有个参数,至少是一个空对象

vuex 把数据放到store仓库

  1. actions

    const actions = {
        // 获取search模块的数据
        async getSearchList({commit }, params) {
            // context是一个迷你版的store,上面有vuex常用的一些方法,{commit}解构commit,这样下面就可以直接调用commit函数
            // dispatch的时候传递的第二个参数就是params
            let result = await reqGetSearchInfo(params);
            if (result.code === 200) {
                commit('GETSEARCHLIST', result.data)
            }
        }
    };
    
  2. mutations

    const mutations = { // 进行更换数据 
        GETSEARCHLIST(state, searchList) {
            state.searchList = searchList;
        }
    };
    
  3. state:

    const state = {
         searchList:{}, // 类型为对象,可以模拟一下请求数据
    };
    
  4. dispatch

        mounted(){
          this.$store.dispatch('getSearchList',{}); // {}传的参数
        }
    

让组件拿到仓库的数据

如果用...mapState拿数据过于繁琐了【套娃】

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第64张图片

所以我们使用getters

// 计算属性,在项目当中,为了简化数据而生
// 可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候方便了】
const getters = {
    goodsList(state){ // state是当前仓库中的state,并非大仓库中的state
        // 如果searchList是空对象,则return的是undefined
        return state.searchList.goodsList || []; // 假如没有网络,应该返回的是数组形式,数组可以遍历
    },
    trademarkList(state){
        return state.searchList.trademarkList;
    },
    attrsList(state){
        return state.searchList.attrsList;
    }
};

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第65张图片

注意:我开启了命名空间,所以从组件中读取getters数据是这样写的

...mapGetters('search',['goodsList']),

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第66张图片

Search模块中动态展示产品列表

  • 遍历goodsList

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第67张图片

search模块根据不同的参数获取数据展示

问题:当用户 搜索的时候 需要再发一次请求来获取相应的数据,而我们派发action请求数据的时候是放在mounted挂载函数里面的,请求只在挂载的时候发请求,不能多次发请求,所以我们不能放在这里

解决方法:我们把请求search模块的数据 封装成一个函数,当你需要调用的时候调用即可

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第68张图片

当我们点击三级联动或者搜索的时候,搜索页面会根据点击|搜索不同的数据进行展示

所以 search模块要根据不同的参数获取数据展示

  1. 我们要向服务器发请求的时候携带 参数【根据api接口文档 发现最多携带10个参数】

    我们先把 searchParams 【带给服务器的参数】有哪些写到data中,到时候发请求携带的就是data中的 searchParams

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第69张图片

  2. 在组件挂载完毕之前 再发一次请求,将咱们带的参数传到服务器获取到 有关参数的数据

    这里就是要将我们获取到的参数覆盖掉初始带给服务器的参数
    Object.assign:对象的合并,第二个参数覆盖掉第一个参数,返回值是覆盖后的对象

      beforeMount(){
        // console.log(this.$route.query); // 可以拿到传递过来的query参数
        // console.log(this.$route.params); //  可以拿到传递过来的params参数
          //在发请求之前,把接口需要传递的参数,进行整理
        Object.assign(this.searchParams,this.$route.query,this.$route.params)
      },
    

    注意:这里还是只发了一次请求,因为只在mounted里面调用了getData

    -----所以,当有参数的时候地址栏会发生变化,我们可以监听$route里面属性的属性值的变化,再一次整理参数-发送请求

      watch: {
        // 监听路由的信息是否发生变化,如果发生变化再次发请求
        deep:true,
        $route() {
          // 整理数据
          Object.assign(this.searchParams, this.$route.query, this.$route.params);
          //  发请求 ---发请求之前再整理一下数据
          this.getData();  // 调用getData函数,再次发请求
     	  // 这里assing只是替换数据,但是如果你第一次点一级分类,第二次点二级分类,id并没有干扰,所以没有替换,这里请求就有错误
          // 每一次请求完毕,应该把相应的1,2,3级分类的id置空,让他接收下一次的相应1,2,3id
          this.searchParams.category1Id = '';
          this.searchParams.category2Id = '';
          this.searchParams.category3Id = '';
        },
      },
    

search模块子组件的动态开发

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第70张图片

  1. 从仓库中捞到数据

    <script>
      import {mapGetters} from 'vuex'
      export default {
        name: 'SearchSelector',
        computed:{
          ...mapGetters('search',['trademarkList','attrsList']), // // 去search仓库里面 拿到这两个数据
        }
      }
    </script>
    
  2. 变为动态数据

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第71张图片

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第72张图片

Search面包屑开发

Search面包屑处理分类的操作

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第73张图片

  1. 这个面包屑 可能有 也可能没有,如果你携带的参数searchParams.categoryName为空,就是没有面包屑,有的话,面包屑就是这个参数的值

    所以用v-if来展示是否有面包屑

    <ul class="fl sui-tag">
      	 <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i>×</i></li>
    </ul>
    
  2. 当你搜索的时候或者点击的时候 ----携带的query参数和params参数都要在面包屑上显示,当你点X号的时候,面包屑和它所对应的参数也应该删掉,然后再次发请求渲染页面

    也就是说,把searchParams.categoryName和那些分级的id置空,然后再次发个请求

    带给服务器参数 是可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器

    但是你把相应的字段变为undefined,当前这个字段不会带给服务器-------性能更好

  3. 当你点X号的时候,地址栏也要改:进行路由跳转----自己跳自己

        removeCategoryName(){
          // 把searchParams.categoryName和哪些分级的id也置空
          // 带给服务器参数 是可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
          // 但是你把相应的字段变为undefined,当前这个字段不会带给服务器-------性能更好
          this.searchParams.categoryName = undefined;
          this.searchParams.category1Id = undefined;
          this.searchParams.category2Id = undefined;
          this.searchParams.category3Id = undefined;
          this.getData();
          // 自己跳转自己的路由
          // 严谨:本意是删除query参数,如果路径当中出现了params参数,路由跳转的时候应该带着
          if(this.$route.params){
            this.$router.push({
              name:'sousuo',
              params:this.$route.params,
            })
          }
        },
    

面包屑处理关键字

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第74张图片

需求:

  1. 当你搜索关键字的时候,面包屑会出现该关键字,路由的参数也会携带

  2. 当你点面包屑X号的时候,面包屑会删除该关键字,搜索框为空,路由携带的参数也为空


当面包屑中的关键字清除以后,需要让兄弟组件Header中的keyword关键字清除,这样搜索框的关键字也清除了 -------组件通信----------全局事件总线

//     pages/Search/index.vue    需求1

		<ul class="fl sui-tag">
            <!-- 分类的面包屑---query参数 -->
            <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">×</i></li>
            <!-- 关键字的面包屑---params参数 -->
            <li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">×</i></li>
          </ul>
//     pages/Search/index.vue     需求2

    // 删除关键字面包屑
    removeKeyword(){
      // 给服务器带的参数searchParams的keyword给置空
      this.searchParams.keyword = undefined;
      // 再次发请求
      this.getData();
      // 通知Header组件的搜索框为空----兄弟组件通信----全局事件总线
      this.$bus.$emit('clear')
      // 自己跳转自己的路由
      // 严谨:本意是删除params参数,如果路径当中出现了query参数,路由跳转的时候应该带着
      if(this.$route.query){
        this.$router.push({
          name:'sousuo',
          query:this.$route.query,
        })
      }
    },
//     Header/index.vue

  mounted(){
    // 通过全局事件总线清除搜索框里的关键字
    this.$bus.$on('clear',()=>{
      // 把keyword置空
      this.keyword='';
    })
  }

面包屑处理品牌信息

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第75张图片

当你点击VIVO的时候,面包屑会出现该词汇,然后页面展示该相关数据

  1. 点击以后,子组件需要整理参数,向服务器发请求 获取相应的数据在面包屑展示

  2. 子组件把要传递的信息(点击的品牌的信息)给父组件,然后父组件把searchParams参数带给服务器参数

  • 当点击品牌logo的时候,触发tradeMarkHandler(trademark)回调函数,并把品牌的信息当做参数传过去
        <ul class="logo-list">
          <li v-for="trademark in trademarkList" :key="trademark.tmId" @click="tradeMarkHandler(trademark)">{{trademark.tmName}}</li>
        </ul>
  • 执行回调函数,用自定义事件把(点击的品牌的信息)给父组件
    methods:{
      // 点击logo的时候触发----品牌logo的事件处理函数
      tradeMarkHandler(trademark){  // 接收trademark品牌的信息
        // 点击以后,子组件需要整理参数,向服务器发请求获取相应的数据在面包屑展示
        // 子组件把要传递的信息(点击的品牌的信息)给父组件,然后父组件发请求
        // console.log(trademark); // 是个对象 {tmId: 4,tmName: "尚硅谷"}
        // 用自定义事件来传参
        this.$emit('trademarkInfo',trademark); // 触发自定义事件trademarkInfo,把trademark的信息传给父组件
      }
    }
  • 在父组件中绑定自定义事件trademarkInfo
        <!--selector-->
        <SearchSelector @trademarkInfo="trademarkInfo"></SearchSelector>
  • 父组件拿到(点击的品牌的信息)后,整理品牌的参数,传过来的参数是对象形式{tmId:xxx,tmName:xx}
  • 我们用ES6语法模板字符串来输出变量tmId:xxx,tmName:xx,然后赋值给 searchParams.trademark
  • 再次发请求的时候会把searchParams传给服务器
    // 自定义事件的回调 --- 在子组件触发
    trademarkInfo(trademark){ // trademark从子组件接收到的信息
      // 1.父组件整理品牌字段参数  "ID:品牌名称"
      this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
      // 2.再次发请求并展示相关数据
      this.getData(); 
    },

  • 面包屑品牌信息进行展示

  • 如果 trademark 有内容,就进行展示面包屑,当点击X号的时候,就执行删除操作

              <ul class="fl sui-tag">
                <!-- 分类的面包屑---query参数 -->
                <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">×</i></li>
                <!-- 关键字的面包屑---params参数 -->
                <li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">×</i></li>
                <!-- 品牌logo信息展示 -->
                <li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(':')[1]}}<i @click="removeTrademark">×</i></li>
              </ul>
    
  • 让trademark为undefined,然后再次发请求

        // 删除品牌logo面包屑
        removeTrademark(){
          this.searchParams.trademark = undefined;
           // 再次发请求
          this.getData();
        },
    

Search的子组件平台售卖属性的操作

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第76张图片

  1. 点击以后,子组件把要传递的信息给父组件,然后父组件把searchParams参数带给服务器参数

  2. 需要的信息:(商品属性的数组: [“属性ID:属性值:属性名”]示例: [“2:6.0~6.24英寸:屏幕尺寸”])

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第77张图片

  3. 获取相应的数据在面包屑展示,里面的属性值可以多次点击展示不同的属性,所以不能用v-if,props是一个数组,需要遍历把里面的属性名展示

  4. 点击重复属性的不需要重复展示------数组去重

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第78张图片

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第79张图片

  1. 点击以后,子组件把要传递的信息给父组件,然后父组件把searchParams参数带给服务器参数

      // 子组件
    	
    	<div class="type-wrap" v-for="attr in attrsList" :key="attr.attrId">
          <!-- 属性 比如 颜色 -->
          <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 @click="attrInfo(attr,attrValue)">
                <a>{{attrValue}}</a>
              </li>
            </ul>
          </div>
    

    点击----------------------------触发点击事件的回调attrInfo

    通过自定义事件把 点击的属性相关信息给父组件

      //   子组件 
    
    	  // 平台售卖属性值的点击事件
          attrInfo(attr,attrValue){
            // console.log(attr);  attr里面有id和name
            // console.log(attrValue);  这个直接就是你点击的属性值
            this.$emit('attrInfo',attr,attrValue); // 自定义事件名字叫 attrInfo
          },
    

    父组件绑定自定义事件

            
            <SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo">SearchSelector>
    

    父组件通过自定义事件收集到传过来的属性信息,然后进行整理数据,再次发请求重新渲染页面

        // 自定义事件的回调---收集平台属性地方 回调函数
        attrInfo(attr,attrValue){ // 数组里面存储
         // 整理参数["属性ID:属性值:属性名"] 数组里面放字符串
          let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
          // 数组去重---判断数组里面是否有重复元素,如果有就不push追加了
          if(this.searchParams.props.indexOf(props)==-1){
              // 把元素放到数组里面
              this.searchParams.props.push(props);
          }
          // 再次发请求
          this.getData();
        },
    
  2. 将 点击的属性值 在面包屑展示,里面的属性值可以多次点击展示不同的属性,所以不能用v-if,props是一个数组,需要遍历把里面的 属性名 进行展示

    ​ props.attrValue:[“106:安卓手机:手机一级”],只需要展示属性名,也就是用:来分割数组,展示索引号为1的值

     <ul class="fl sui-tag">
         ...
      
      <li class="with-x" v-for="(attrValue,index) in searchParams.props" :key=index>{{attrValue.split(':')[1]}}<i @click="removeAttr(index)">×i>li>
    ul>
    
  3. 当点击面包屑X号的时候,删除掉 props数组里的 该属性的信息,用到删除数组splice

        // 删除售卖属性的面包屑
        removeAttr(index){
          // array.splice(从哪开始删除, 删除几个)
          this.searchParams.props.splice(index,1);
          this.getData();
        },
    

Search模块的排序操作(重点)

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第80张图片

参数名称 类型 是否必选 描述
order string N 排序方式 1: 综合,2: 价格 asc: 升序,desc: 降序 示例: “1:desc”

问题:

  1. order属性的属性值最多有多少种写法?

    1:asc

    2:asc

    1:desc (初始值)

    2:desc

  2. 谁应该有类名 active

    默认是 综合,当点了哪个,那个就有active类名

    通过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>
    

    :class="{active:searchParams.order.indexOf('1')!=-1}"

    动态添加类名 active :查看searchParams.order里面是否有1,有的话就是综合,没有的话就是价格,indexOf如果不包含返回-1,我们要包含就!=-1

    将动态添加类名用计算属性:

                  
           <ul class="sui-nav">
                <li :class="{active:isOne}">
                      <a>综合a>
                li>
                <li :class="{active:isTwo}">
                      <a>价格a>
                li>
           ul>
    
      computed: {
          ...mapGetters("search", ["goodsList"]),
          // 动态添加类名
          isOne(){
            return this.searchParams.order.indexOf('1')!=-1
          },
          isTwo(){
            return this.searchParams.order.indexOf('2')!=-1
          },
      },
    
  3. 谁身上应该有箭头↑ ↓

    谁有active类名,谁就有箭头

        <ul class="sui-nav">
              <li :class="{active:isOne}">
                      <a>综合<span v-show="isOne" ></span></a>
              </li>
              <li :class="{active:isTwo}">
                      <a>价格<span v-show="isTwo"></span></a>
              </li>
        </ul>
    
  4. 箭头应该如何展示

    用阿里图标库…iconfont-阿里巴巴矢量图标库

    把所要的图标添加到里,然后下载到本地,在index.html引入css样式,然后就直接添加类名就可以

    箭头的上下取向 取决于asc升序还是desc降序

    :class="{'icon-up':isAsc,'icon-down':isDesc}"

    给箭头添加动态类名,如果要显示↑箭头,则isAsc返回值true

     
     <ul class="sui-nav">
           <li :class="{active:isOne}">
                 <a>综合<span v-show="isOne" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
           li>
           <li :class="{active:isTwo}">
                 <a>价格<span v-show="isTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
           li>
     ul>
    

    用计算属性来 看是升序还是降序:

    asc升序还是desc降序

    如果searchParams.order里面包含asc,则显示升序,添加icon-up类名

        // 是升序还是降序
        isAsc(){ // 升序
             return this.searchParams.order.indexOf('asc') !=-1;
        },
        isDesc(){ // 降序
             return this.searchParams.order.indexOf('desc') !=-1;
        }
    
  5. 综合价格 绑定点击事件

    默认上来是 1:desc 综合降序

    当我们再次点综合的时候,降序变为升序;

    如果我们点 价格,就会变成 2:desc

    绑定点击事件

      
      <ul class="sui-nav">
            <li :class="{active:isOne}" @click="changeOrder('1')">
                  <a>综合<span v-show="isOne" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
            li>
            <li :class="{active:isTwo}" @click="changeOrder('2')">
                  <a>价格<span v-show="isTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
            li>
      ul>
    

    给 综合 和 价格 绑定点击事件并传参 ,flag他是一个标记,代表用户点击的时候综合(1)还是价格(2)【用户点击的时候传进来的】


    ​ orginFlag 是初始值,flag是我们点击哪个传过来的参数,如果相同,说明我们一直在点【综合】或者【价格】,这时候要切换升序和降序

    ​ orginFlag如果和flag不相同,说明初始值是【综合】我们却点了【价格】,就让传过来的flag为我们现在的searchParams.order里面的参数,排序方式默认为降序

        // 排序操作
        changeOrder(flag){
          // flag是一个形参,他是一个标记,代表用户点击的时候综合(1)还是价格(2)【用户点击的时候传进来的】
          let orginOrder = this.searchParams.order; // 获得起始的状态 1:desc默认综合
          let orginFlag = this.searchParams.order.split(":")[0]; // 拿到现在状态是 综合还是 价格
          let orginSort = this.searchParams.order.split(":")[1]; // 拿到现在是升序还是降序
          // 准备一个新的order属性值
          let newOrder = '';
          // 这个语句可以确定默认的和你点击的orginFlag是一样的
          if(orginFlag == flag) {
            // 如果默认值和咱们点击的是一样的,就切换升序和降序
            newOrder = `${flag}:${orginSort=='desc'?'asc':'desc'}`
          }else {
            // 假如默认是综合,我们却点击了价格,让点击传入的参数flag=2 就是价格,然后默认是降序
            newOrder = `${flag}:${orginSort='desc'}`
          }
          this.searchParams.order = newOrder;
          // 再次发请求
          this.getData();
        }
    

    服务器会自动帮我们排好序的…

分页器功能分析(重点)

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第81张图片

分页功能很多地方都要用到,我们可以把分页功能 设成一个 全局组件

分页器静态组件

  1. 创建组件
  2. 引入组件
  3. 注册组件
  4. 使用组件

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第82张图片
① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第83张图片

// src/components/Pagination/index.vue






分页功能分析

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

​ 比如电商平台同时展示的数据很多(1w+),需要采用分页功能

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

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

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

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

  4. 需要知道分页器连续页码个数:continues字段进行代表 5 | 7 【为什么是奇数?因为对称】

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

自定义分页器,在开发的时候先自己传递假的数据进行调试,调试成功后再用服务器数据

  1. 先让父组件给子组件传递假的数据props
//    src/pages/Search/index.vue


<Pagination :pageNo="1" :pageSize="3" :total="91" :continues="5">Pagination>
  1. 对于分页器而言,很重要的一个地方即为【算出:连续页面起始数字和结束数字】

    比如,当前是第8页,【6 7 8 9 10 】6是起始数字,10是结束数字

    获得的数据有:

      props: ["pageNo", "pageSize", "total", "continues"],
    
    1. 计算出总共有多少页数

      Math.ceil()向上取整

        computed: {
          // 计算出总共有多少页数
          totalPage() {
            // 向上取整
            return Math.ceil(this.total / this.pageSize);
          },
        }
      
    2. 计算出连续的页码的起始数字结束数字【连续页码的数字:至少是5,才能有连续页码】

      parseInt()取整数

      前提:分页器数字没有0,也没有负数

      假如当前是第1页,应该是1 2 3 4 5

      假如当前是第2页,应该是 1 2 3 4 5

      所以:start应该是1,end应该是你分页器的连续页码的数字


      假如现在总共是31页

      当前是31页,应该是 27 28 29 30 31

      当前是30页,应该是 27 28 29 30 31

          // 计算出连续的页码的起始数字和结束数字【连续页码的数字:至少是5,才能有连续页码】
          startNumAndEndNum() {
            // 先定义两个变量
            let start = 0,
              end = 0;
            // 连续页码的数字:至少是5,才能有连续页码。所以至少有5页,也有可能是7
            // 1. 如果数据不够5页【不正常现象:总页数没有连续页码多】
            if (this.continues > this.totalPage) {
              start = 1;
              end = this.totalPage;
            } else {
              // 正常现象【总页数>连续页码】
              start = this.pageNo - parseInt(this.continues / 2);
              end = this.pageNo + parseInt(this.continues / 2);
              // 2. 把不正常的现象纠正【start数字出现了 0 | 负数】
              if (start < 1) {
                start = 1;
                end = continues;
              }
              // 3. 把不正常的现象纠正【end数字大于总页码】
              if (end > this.totalPage) {
                start = this.totalPage - (this.continues - 1);
                end = this.totalPage;
              }
            }
            return {start,end};
          },
      

      注意!计算属性必须要有return返回值!!!

分页器动态展示

分为 上中下 三部分
① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第84张图片

v-for 可以遍历数字
① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第85张图片

  1. 遍历连续页码的结束数字,如果页数 大于等于 起始数字就进行展示【为了防止出现这种情况】
    在这里插入图片描述

     
    
    <button
      v-for="(page, index) in startNumAndEndNum.end"
      :key="index"
      v-if="page >= startNumAndEndNum.start"
    >
      {{ page }}
    button>
    
  2. 上面部分

    
    <button>上一页button>
    <button v-if="startNumAndEndNum.start > 1">1button>
    <button v-if="startNumAndEndNum.start > 2">···button>
    

    中间部分进行展示的时候会出现 和上面部分相同的情况,所以用v-if来判断上面部分的展示
    当连续页的起始值 大于 1 的时候,中间部分和上面部分没用重复的,这里要上面部分的 1

    当连续页的起始值 大于 2 的时候,起始值是3,和1中间有个2,可以要 …

在这里插入图片描述

当前页是第1页,应该把 1… 给去掉
在这里插入图片描述

当前页是第2页,应该把 1… 给去掉
在这里插入图片描述

当前页是第3页,应该把 1… 给去掉

在这里插入图片描述

当前页是第4页,要1,不要…

所以,当start > 1 就要 1,当start>2就要…


  1. 下面部分

        
        <button v-if="startNumAndEndNum.end < totalPage - 1">···button>
        <button v-if="startNumAndEndNum.end < totalPage">{{ totalPage }}button>
        <button>下一页button>
    

    当连续页的结束值 小于 总页数,中间部分和下面部分没重复,才需要 总页数,否则不显示

    当连续页的结束值 小于 总页数-1,他们俩之间还隔着1个数,可以用…

在这里插入图片描述

当前页是第31页,应该把 … 31 给去掉

在这里插入图片描述

当前页是第30页,应该把 … 31 给去掉

在这里插入图片描述

当前页是第29页,应该把 … 31 给去掉

在这里插入图片描述

当前页是第28页,要31,不要…

在这里插入图片描述

当前页是第27页,要 … 31

所以,当end 小于 总页数 才要 31,当end小于 总页数-1,才要

替换数据

仓库里面有数据

在这里插入图片描述

    // 获取 search模块中展示产品一共有多少数据
    ...mapState('search',['searchList']),

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第86张图片

替换好的真实数据:

          
          <Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="searchList.total" :continues="5">Pagination>

当点击第 ? 页,子组件告诉父组件 你点了第 ? 页,父组件拿第 ? 页的数据

子给父通信----------自定义事件

给父组件绑定自定义事件@getPageNo="getPageNo"获取你点击的是第几页

⚠注意 需要传参的话父组件自定义事件不需要带()

          
          <Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="searchList.total" :continues="5" @getPageNo="getPageNo">Pagination>

子组件触发自定义事件:

  1. 当你当前页是第1页,‘上一页’不能点击了,所以用:disabled="pageNo==1",当可以点击的时候,传的参数是当前这页-1
  2. 当点击1的时候,传参为1
  3. 当点击中级连续页码的时候,传的参数就是page,连续页码的页数
    
    
    
    <button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页button>
    <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)">1button>
    <button v-if="startNumAndEndNum.start > 2">···button>

    
    
    <button
      v-for="(page, index) in startNumAndEndNum.end"
      :key="index"
      v-if="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>
  1. 当你点击最后一页的话,传的参数就是最后一页
  2. 当你点击下一页的话,如果你在最后一页,就不能点击下一页,否则的话你传的参数就是当前页数+1

触发自定义函数后 父组件的回调函数

    // 自定义事件----获取当前第几页
    getPageNo(pageNo){
      // 整理带给服务器的参数
      this.searchParams.pageNo = pageNo;
      // 再次发请求
      this.getData();
    },

分页器添加类名

当点击哪个页码的时候,会有类名,背景色变色

动态添加类名-----:class=“{ xxxxxx }”

   // 点击第1页的时候
 <button
      v-if="startNumAndEndNum.start > 1"
      @click="$emit('getPageNo', 1)"
      :class="{ active: pageNo == 1 }"
    >
      1
 button>

	// 点击中间部分连续页码的时候
<button
      v-for="(page, index) in startNumAndEndNum.end"
      :key="index"
      v-if="page >= startNumAndEndNum.start"
      @click="$emit('getPageNo', page)"
      :class="{ active: pageNo == page }"
    >
      {{ page }}
button>

	// 点击最后一页的时候
<button
      v-if="startNumAndEndNum.end < totalPage"
      @click="$emit('getPageNo', totalPage)"
      :class="{ active: pageNo == totalPage }"
    >
      {{ totalPage }}
button>

七、完成Detail某一个产品的详情页面

  1. 静态组件 ----- Detail 详情页组件

  2. 发请求api

  3. vuex

  4. 动态展示组件

配置详情 路由 src/router/index.js

当点击search下的产品图片的时候,会跳转到该产品详情页Detail,同时携带params参数

// 配置路由的地方
import Vue from 'vue';
import VueRouter from 'vue-router';
// 使用插件
Vue.use(VueRouter);

// 引入路由组件
import Detail from '../pages/Detail'

// 先把VueRouter原型对象的push,先保存一份
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace

// 重写push | replace
// 参数:告诉原来的push方法,你往哪里跳转(传递哪些参数)
VueRouter.prototype.push = function push(location) {
    return originalPush.call(this, location).catch(err => err)
}
VueRouter.prototype.replace = function replace(location) {
    return originalReplace.call(this, location).catch(err => err)
}

// 配置路由
export default new VueRouter({
    // 配置路由
    routes: [
        .....
        {
            name: 'xiangqing',
            path: '/detail/:skuId', // 需要携带params参数,所以要占位
            component: Detail,          
            meta: { showFooter: true },
        },
        // 重定向:在项目跑起来的时候,访问/,立马让他定向到首页!!!
        {
            path: '*',
            redirect: '/home',
        }
    ]
})

对search里面的点击图片进行路由跳转

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

当跳转的时候发现滚动条不动,和上一页面的滚动条在相同位置,咱们要让控制滚动条在顶部

滚动行为

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第87张图片

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    // 始终滚动到顶部
    return { y: 0 }
  },
})

产品详情数据获取

api–请求接口

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第88张图片

// 获取产品详情页模块的数据   /api/item/{ skuId }  GET   需要带参数
export const reqGoodsInfo = (skuId) =>{
   return requests({url:`/item/${skuId}`,method:'GET'})
}

vuex–获取产品详情信息给仓库

vuex仓库中还需要新增一个模块detail,需要回到大仓库中进行合并

//     src/store/detail/index.js

// detail 产品详情页的仓库
import { reqGoodsInfo } from '@/api'
const state = {};
const mutations = {};
const actions = {};
const getters = {};

// 对外暴露
export default {
    namespaced: true,
    state,
    mutations,
    actions,
    getters,
}

引入到大仓库中

//       src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
// 需要使用插件
Vue.use(Vuex);

// 引入小仓库
import home from './home'
import search from './search'
import detail from './detail'
// 对外暴露Store类的一个实例
export default new Vuex.Store({
    // 实现vuex仓库模块式开发存储数据
    modules:{
        home,
        search,
        detail,
    }
})

发请求捞数据

  1. 派发action

    什么时候派发action?

    当你在搜索页点击某个产品的时候发生跳转,跳转到detail页面,当页面挂载完毕开始派发action

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第89张图片

        mounted(){
          // 派发action获取产品详情的信息,后面要携带参数即为产品的id
          this.$store.dispatch('detail/getGoodsInfo',this.$route.params.skuId);
        },
    
  2. vuex三件套 actions-mutations-state

    //        src/store/detail/index.js
    
    // detail 产品详情页的仓库
    import { reqGoodsInfo } from '@/api'
    
    const state = {
        goodsInfo: {}, // 看api文档,result.data返回的是一个对象
    };
    
    const mutations = {
        GETGOODSINFO(state, goodsInfo) { // goodsInfo 是commit传过来的参数
            state.goodsInfo = goodsInfo;
        }
    };
    
    const actions = {
        // 获取产品信息的action
        async getGoodsInfo({ commit }, skuId) { // 需要带参数
            let result = await reqGoodsInfo(skuId)
            console.log(result.data);
            if (result.code == 200) {
                // 提交mutations,修改state
                commit('GETGOODSINFO', result.data)
            }
        }
    };
    
  3. 现在detail仓库里的state已经拿到数据了

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第90张图片

产品详情展示动态数据

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第91张图片

数据存储到detail仓库里面,拿数据是这样的->state.detail.goodsInfo.xxx,比较麻烦,可以利用计算属性

//        src/store/detail/index.js

// 简化数据
const getters = {
    // 获取数据的时候直接getters获取然后categoryView来获取数据
    categoryView(state){
        // 比如:state.goodsInfo初始状态为空对象,空对象的categoryView属性为undefined,读属性undefined的xxx会报错
        return state.goodsInfo.categoryView || {};
    },
    skuInfo(state){
        return state.goodsInfo.skuInfo || {};
    },
};

在组件中引入mapGetters

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第92张图片

渲染数据

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第93张图片

Zoom放大镜操作

Zoom放大镜展示数据

skuInfo.skuImageList里面存储着放大镜图片

现在detail仓库里面有数据,detail组件有数据,我们要让zoom组件拿到数据,就是父组件detail 向 子组件zoom传递数据,用props

   <div class="previewWrap">
          
          
          <Zoom :skuImageList="skuImageList">Zoom>
          
          <ImageList />
   div>

给子组件的数据

  1. 如果服务器数据没有回来,skuInfo是个空对象,skuInfo.skuImageList就是undefined,子组件出现skuImageList[0]就会报错,因为undefined没有第0项,所以skuImageList应该至少是一个空数组
//    src/pages/detail/index.vue

computed:{
      ...mapGetters('detail',['categoryView','skuInfo']),
      // 给子组件的数据
      skuImageList(){
       	 return this.skuInfo.skuImageList || [];
      }
},
  1. 同上,如果服务器数据没有回来,skuImageList应该至少是一个空数组,空数组的第0项还是undefined,skuImageList[0].imgUrl就是undefined的imgUrl还是不存在,所以skuImageList[0]至少是个空对象
//      src/pages/detail/zoom/index.vue 

<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() {
        //  至少 skuImageList[0] 是一个空对象
      return this.skuImageList[0] || {};
    },
  },
};
script>

放大镜下的轮播图数据展示

  1. 从父组件detail拿数据到ImageList组件

              
              <ImageList :skuImageList="skuImageList">ImageList>
    
  2. 接收数据props

        props:["skuImageList"],
    
  3. 展示数据

     <div class="swiper-wrapper">
        <div class="swiper-slide" v-for="slide in skuImageList" :key="slide.id">
            <img :src="slide.imgUrl">
        div>
     div>
    

放大镜轮播图操作

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第94张图片

同样,我们轮播图采用 监听watch+$next Tick

  watch: {
    // 监听数据可以保证数据一定有,但是不能保证v-for遍历结构是否完事
    skuImageList() {
      // 延迟执行回调,当下次DOM更新循环结束后
      this.$nextTick(() => {
        new Swiper(".swiper-container", {
          navigation: {
            nextEl: ".swiper-button-next",
            prevEl: ".swiper-button-prev",
          },
          slidesPerView: 3, // 显示几个图片设置
          slidesPerGroup:1, // 每一次切换图片的个数
        });
      });
    },
  },

当鼠标点击的时候,img添加动态classactive

//  src/pages/detail/ImageList

  <div class="swiper-slide" v-for="(slide,index) in skuImageList" :key="slide.id">
        <img :src="slide.imgUrl" :class="{active:currentIndex==index}" @click="changeCurrentIndex(index)"/>
   </div>

....

  data(){
    return {
      // 响应式数据
      currentIndex:0,
    }
  },
  methods:{
    // 修改响应式数据
    changeCurrentIndex(index){
      this.currentIndex = index;
    }
  },

下面的轮播图点击后 上面的轮播图就要展示相应图片-------兄弟间通信

  1. 当点击图片的时候,把图片当前的索引值传过去,然后进行兄弟间通信

      methods:{
        // 修改响应式数据
        changeCurrentIndex(index){
          this.currentIndex = index;
          // 通知兄弟组件当前的索引值
          this.$bus.$emit('getIndex',this.currentIndex);
        }
      },
    
  2. 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"],
      data(){
        return{
          // 响应式数据
          currentIndex:0,
        }
      },
      computed: {
        imgObj() {
          return this.skuImageList[this.currentIndex] || {};
        },
      },
      mounted(){
        // 全局事件总线获取兄弟组件传过来的索引值
        this.$bus.$on('getIndex',(index)=>{
          // 修改当前响应式数据
          this.currentIndex = index;
        })
      },
    };
    script>
    

放大镜

pageX: 页面X坐标位置
pageY: 页面Y坐标位置

offsetX:鼠标坐标到元素的左侧的距离
offsetY:鼠标坐标到元素的顶部的距离

  1. 要让mask遮罩跟着鼠标移动

  2. 约束范围

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第95张图片

  methods:{
    handler(event){ // event由用户触发的事件,这里可以不用传参,默认有的
      let mask = this.$refs.mask; // 获得mask的DOM节点 
// 要让mask的left = 鼠标的距离 - 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 值 !!!一定要加'px'!! mask.style.left = left + 'px'; mask.style.top = top + 'px'; } },
  1. 要让上面右面的大图跟着动

    ① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第96张图片

    遮挡层移动距离 left | top

    遮挡层最大移动距离 mask.offsetWidth | mask.offsetHeight

    大图片最大移动距离 imgWidth - bigWidth = 800-400=400px // 这里去看CSS样式就知道他们的宽度

    求大图片移动距离 = 遮挡层移动距离*大图片最大移动距离/遮挡层最大移动距离

          /* ----------要让上面右边的大图跟着动-------------------- */
          let bigImg = this.$refs.bigImg;
          // 大图片的宽度和高度
          let bigImgWidth = bigImg.offsetWidth
          let bigImgHeight = bigImg.offsetHeight;
          // big盒子的高度和宽度
          let big = this.$refs.big;
          let bigWidth = big.offsetWidth
          let bigHeight = big.offsetHeight
          // 求大图片移动距离 = 遮挡层移动距离*大图片最大移动距离(bigImgWidth-bigWidth)/遮挡层最大移动距离
          let imgLeft = left*(bigImgWidth-bigWidth)/mask.offsetWidth;
          let imgTop = top*(bigImgHeight-bigHeight)/mask.offsetHeight;
          // 修改大图片的left和top值!记得大图片是向左移动的,和mask是相反的
          bigImg.style.left = -imgLeft + 'px';
          bigImg.style.top = -imgTop + 'px';
    

    完整代码:

    <template>
      <div class="spec-preview">
        <!-- 上面左侧的大图 -->
        <img :src="imgObj.imgUrl" />
        <!-- 写事件的event,要放事件的地方 -->
        <div class="event" @mousemove="handler"></div>
        <!-- 上面右侧的大图 -->
        <!-- big Width 400px       bigImgWidth 800px -->
        <div class="big" ref="big">
          <img :src="imgObj.imgUrl" ref="bigImg" />
        </div>
        <!-- 遮罩层 -->
        <div class="mask" ref="mask"></div>
      </div>
    </template>
    ...
      methods:{
        handler(event){ // event由用户触发的事件,这里可以不用传参,默认有的
          let mask = this.$refs.mask; // 获得mask的DOM节点 
    // 要让mask的left = 鼠标的距离 - 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 值 !!!一定要加'px'!! mask.style.left = left + 'px'; mask.style.top = top + 'px'; /* ----------要让上面右边的大图跟着动-------------------- */ let bigImg = this.$refs.bigImg; // 大图片的宽度和高度 let bigImgWidth = bigImg.offsetWidth let bigImgHeight = bigImg.offsetHeight; // big盒子的高度和宽度 let big = this.$refs.big; let bigWidth = big.offsetWidth let bigHeight = big.offsetHeight // 求大图片移动距离 = 遮挡层移动距离*大图片最大移动距离(bigImgWidth-bigWidth)/遮挡层最大移动距离 let imgLeft = left*(bigImgWidth-bigWidth)/mask.offsetWidth; let imgTop = top*(bigImgHeight-bigHeight)/mask.offsetHeight; // 修改大图片的left和top值!记得大图片是向左移动的,和mask是相反的 bigImg.style.left = -imgLeft + 'px'; bigImg.style.top = -imgTop + 'px'; } },

detail路由组件展示商品售卖属性

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第97张图片

detail组件从仓库里拿到商品售卖属性信息spuSaleAttrList

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第98张图片

  1. 简化数据

    //    src/store/detail/index.js
    
    // 简化数据
    const getters = {
        // 产品售卖属性的简化
        spuSaleAttrList(){
            // 至少是一个数组
            return state.goodsInfo.spuSaleAttrList || [];
        },
    };
    
  2. 子组件拿到数据

    import { mapGetters } from "vuex";
    ....
      computed: {
        ...mapGetters("detail", ["categoryView", "skuInfo","spuSaleAttrList"]),
      },
    
  3. 展示数据

                
     <div class="chooseArea">
         <div class="choosed">div>
              <dl v-for="spuSale in spuSaleAttrList" :key="spuSale.id">
                  <dt class="title">{{spuSale.saleAttrName}}dt>   
                  <dd changepirce="0" :class="{active:spuSaleAttrValue.isChecked == 1}" v-for="spuSaleAttrValue in spuSale.spuSaleAttrValueList" :key="spuSaleAttrValue.id">{{spuSaleAttrValue.saleAttrValueName}}dd>
              dl>
     div>
    

    :class动态加高亮

  4. 点击事件--------排他思想

    点哪个售卖属性值,谁高亮,其余属性值不亮---------排他思想

    给属性值dd 绑定点击事件changeActive

                
     <div class="chooseArea">
          <div class="choosed">div>
              <dl v-for="spuSale in spuSaleAttrList" :key="spuSale.id">
                    <dt class="title">{{ spuSale.saleAttrName }}dt>
                    <dd
                      changepirce="0"
                      :class="{ active: spuSaleAttrValue.isChecked == 1 }"
                      v-for="spuSaleAttrValue in spuSale.spuSaleAttrValueList"
                      :key="spuSaleAttrValue.id"
                     @click="changeActive(spuSaleAttrValue,spuSale.spuSaleAttrValueList)"
                    >
                      {{ spuSaleAttrValue.saleAttrValueName }}
                    dd>
              dl>
     div>
    
      methods:{
        // 产品的售卖属性值高亮
        changeActive(spuSaleAttrValue,arr){
          // spuSaleAttrValue 是你当前点击的那个属性值相关信息
          // arr 是你这个属性所有的属性值的信息
          // 遍历全部的属性值isChecked为0,这样就没有高亮了
          arr.forEach(item => {
            item.isChecked = 0;
          });
          // 你当前点击的属性值添加高亮
          spuSaleAttrValue.isChecked=1;
        },
      },
    

购买产品个数的操作

① 尚品汇的前台开发笔记【尚硅谷】【Vue】_第99张图片

将input框里的数据保存起来------v-model双向绑定skuNum

用户点击 + 或者 - 输入框的数字进行变化 skuNum++ 注意:点 - 的时候 不能小于1

当用户在input框里面输入的时候注意规范性:不能为字符,不能是负数,不能是小数

  1. 不能为字符---------任何字符 * 1 就是NaN
  2. 不能是负数---------if判断,如果是NaN或者小于1,让skuNum = 1;
  3. 不能是小数---------最后让input输入框里的value赋值给skuNum的时候,对value取整数parseInt()
//       src/pages/detail/index.vue

 
<div class="cartWrap">
     <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>
     <div class="add">
            <a href="javascript:">加入购物车a>
     div>
div>
    // 表单元素修改产品个数 --当表单元素发生变化就触发该回调
    // 目的:让用户输入的文本赋值给data里的skuNum
 changeSkuNum(event){
      // 拿到表单元素文本  event.target.value
      // 让表单元素文本为正数

      // 1. 用户输进来的文本 * 1 就是NaN
      // 2. 用户输入的小于1的
      let value = event.target.value * 1;
      if(isNaN(value) || value < 1) {
        this.skuNum = 1;
      }else {
        // 3. 如果用户输入小数,进行取整数
        this.skuNum = parseInt(value);
      }
 },

八、加入购物车

② 尚品汇的前台开发笔记【尚硅谷】【Vue】

你可能感兴趣的:(Vue,vue.js,javascript,前端)