尚硅谷大型项目尚品汇笔记(已完结)

828 阅读27分钟

尚品汇项目笔记

文章已经全部完结,由于文章内容长度限制,最后一部分笔记放到另外一篇文章中。请点击个人主页查看!

Vue前台项目

vue-cli脚手架初始化项目

  1. 新建文件夹,输入cmd指令:vue create app,app是项目名称。选择vue2版本初始化项目。初始化完成后项目如下:

    image.png

  2. 项目组成

    1. node_modules:放置项目依赖的地方

    2. public:一般放置一些共用的静态资源,打包上线的时候,public文件夹里面资源原封不动打包到dist文件夹里

    3. src:程序员源代码文件夹

      1. assets文件夹:经常放置一些静态资源(图片),assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
      2. components文件夹:一般放置非路由组件(或者项目共用的组件)
      3. App.vue: 唯一的根组件
      4. main.js 入口文件【程序最先执行的文件】
    4. babel.config.js: babel配置文件

    5. package.json:看到项目描述、项目依赖、项目运行指令

    6. package-lock.json:缓存性文件

    7. README.md:项目说明文件

  3. 配置脚手架下载下来的项目

    • 浏览器自动打开:在package.json文件中

      "scripts": {
       "serve": "vue-cli-service serve --open",
       "build": "vue-cli-service build",
       "lint": "vue-cli-service lint"
       }
      
    • 关闭eslint校验工具

      在vue.config.js文件中:lintOnSave:false,

    • src文件夹的别名的设置:因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些

      在jsconfig.json中:

      {
       "compilerOptions": {
       "baseUrl": "./",
       "paths": {
       "@/*": [
       "src/*"
       ]
       }
       },
       "exclude": [
       "node_modules",
       "dist"
       ]
      }
      

分析路由

vue-router:前端的路由就是所谓的键值对

key:URL

value:相应的路由组件

注意:项目的上中下结构

  • 路由组件:

    1. Home首页路由组件

    2. Search路由组件

    3. login登录路由

    4. register注册路由

  • 非路由组件

    1. Header

    2. Footer【首页、搜索页】,在登录和注册界面没有

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

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

  2. 拆分组件

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

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

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

注意2:咱们的项目采用的less样式,浏览器不识别less样式,需要通过less、less-loader(使用 npm i --save less less-loader@6,5版本的安装不了了)进行处理less,把less样式变为css样式,浏览器才可以识别。

注意3:如果想让组件识别less样式,则在组件中设置lang="less"

使用组件的步骤(非路由组件)(以Header为例)

  1. 创建或定义

    1. 在components文件夹下创建Header文件夹,并且创建index.vueimages文件夹(此组件用到的图片)

    2. 将编写好的Html文件中的结构部分放入template模板中。

    3. 将编写好的less文件放入到css中,lang="less"

    4. 将reset.css(全局都要用的清除基本样式的文件)放入到public/index.html文件中,通过link的形式

  2. 引入(App.vue)

  3. 注册(App.vue)

  4. 使用(App.vue)

路由组件的搭建

vue-router

在上面的分析的时候,路由组件有:Home\Search\login\Register

  1. 安装Vue-router

    npm i --save vue-router@3

  2. 创建pages文件夹并添加4个非路由组件的文件夹,并添加index.vue文件

配置路由

项目当中的配置的路由一般放在router文件夹中,并创建index.js

1.index.js:

//配置路由的地方
import Vue from "vue";
import VueRouter from "vue-router";
//使用插件
Vue.use(VueRouter)
//引入路由组件
import AppHome from "../pages/Home";
import AppLogin from '../pages/Login'
import AppRegister from '../pages/Register'
import AppSearch from '../pages/Search'
//配置路由,默认暴露
export default new VueRouter({
    //配置路由
    routes:[
        {
            path:"/home",
            component:AppHome
        },
        {
            path:"/login",
            component:AppLogin
        },
        {
            path:"/register",
            component:AppRegister
        },
        {
            path:"/search",
            component:AppSearch
        },     
        //重定向,在项目跑起来的时候,访问/,立马让他定向到首页
        {
            path:'*',
            redirect:'/home'

        }
    ]
})

2.在main.js中引入和注册路由

3.在App.vue中:路由组件出口的地方

总结

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

1:路由组件一般放置在pages文件夹中,非路由组件放置在components文件夹中

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

3:注册完路由,不管是路由组件、还是非路由组件,它们身上都有routeroute和 router属性

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

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

路由的跳转

路由的跳转有2种形式:

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

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

编程式导航:声明式导航能做的,编程式导航都能做,但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务逻辑

例子:

声明式导航(在header里声明,变更的是App里面router-view所在的地方):

编程式导航:

Footer组件显示与隐藏

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

Footer组件:在Home、Search显示,在登录、注册的时候隐藏

根据$route判断

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

路由元信息(meta)

路由传参

路由的跳转有几种方式?

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

编程式导航:利用的是组件实例的$router.push|replace方法,可以实现路由的跳转。

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

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

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

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

  1. 字符串形式(同时使用了2种形式)

    router中占位:path:"/search/:keyword"

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

  2. 模板字符串(结果同上,同时使用了2种形式)

    this.$router.push(`/search/${this.keyword}?k=${this.keyword.toLocaleUpperCase()}`)
    
  3. 对象(重要,使用的多)

    1:使用对象写法传递参数时,若传递params参数,则需要在路由处对接受参数的路由命名,传递时需添加name属性指定路由,同样的在配置路由的时候,需要占位

    2:传递query参数时则不需要,直接写对象。

    {
                path:"/search/:keyword",
                component:AppSearch,
                meta:{show:true},
                name:'search'
            },
    
    this.$router.push({
    name:'search',
    params:{keyword:this.keyword},
    query:{k:this.keyword.toLocaleUpperCase()}
    })
    

路由传参相关的面试题

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

 不可以:不能这样书写,程序会崩掉。对象写法params必须搭配name来使用

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

注意:配置路由的时候占位了,但是跳转的时候不传递params参数,此时路径会出现问题

在占位的后面加上一个问号【指定params参数可传可不传】

{
            path:"/search/:keyword?",
            component:AppSearch,
            meta:{show:true},
            name:'search'
        },

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

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

this.$router.push({
name:'search',
params:{keyword:this.keyword||undefined},
query:{k:this.keyword.toLocaleUpperCase()}})

4: 路由组件能不能传递props数据(可以)?

第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给组件 props:{a:900} 第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给组件 props:true

第三种写法:params参数、query参数,通过props传递给路由组件

src\components\Header\index.vue

this.$router.push({
name:'search',
params:{keyword:this.keyword},
query:{k:this.keyword.toLocaleUpperCase()}})

router\index.js

{
            path:"/search/:keyword?",
            component:AppSearch,
            meta:{show:true},
            name:'search',
            props:($route)=>{
                return{
                    keyword:$route.params.keyword,
                    k:$route.query.k
                }
            }
        },

src\pages\Search\index.vue

<template>
    <div>
        <h1>{{ keyword }}</h1>
        <hr>
        <h1>{{ k }}</h1>
    </div>
</template>     
........
props:['keyword','k'],
..........

过程:header => ($route接收) router\index.js(props传出) => Search\index.vue

NavigationDuplicated的警告错误

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

注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。

原因:由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,因此出现上面现象,

解决:

第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调

第二种:重写push|replace方法

src\router\index.js

//先把VueRouter原型对象的push保存一份
let originPush = VueRouter.prototype.push
let originReplace = VueRouter.prototype.replace
//重写push|replace
//locationg:告诉原来的push方法,往哪跳
VueRouter.prototype.push = function(location,resolve,reject){
    if(resolve && reject){
        originPush.call(this,location,resolve,reject)
    }else{
        originPush.call(this,location,()=>{},()=>{})
    }
}
VueRouter.prototype.replace = function(location,resolve,reject){
    if(resolve && reject){
        originReplace.call(this,location,resolve,reject)
    }else{
        originReplace.call(this,location,()=>{},()=>{})
    }
}

Home模块组件拆分

  1. 完成静态页面(已完成)

  2. 拆分静态组件

  3. 发请求获取服务器数据进行展示

  4. 开发动态业务

三级联动组件

由于三级联动,在Home、Search、Detail都出现了,把县级联动注册为全局组件。优点:只需要要注册一次,就可以在项目任意地方使用

完成其余静态组件

HTML+CSS+图片资源(以recommend为例)

1》html资源

  1. 创建文件夹:images(存放组件所需图片)、index.vue

  2. HTML:将写好的静态资源复制到模板中,格式化一下(美观)

2》CSS资源

  1. CSS:将写好的静态资源复制到模板中,格式化一下(美观)

  2. lang="less" Css文件用less写的

3》图片资源

  1. HTML和CSS用的图片复制到images文件夹中

  2. 修改HTML和CSS图片资源中的路径(有时候静态资源写的时候路径对不上)

4》放置组件

  1. 修改一下属性名:name: "HomeRecommend".(语法要求大驼峰命名)

  2. 三步走放到Home中

    • 引入(@代表SRC文件夹,俩种路径都可以)

      import ListContainer from './ListContainer/index.vue'
      import HomeRecommend from './Recommend/index.vue'
      import HomeRank from './Rank/index.vue'
      import HomeLike from './Like/index.vue'
      import HomeFloor from './Floor/index.vue'
      import HomeBrand from '@/pages/Home/Brand'
      
    • 添加到组件中

      components:{
              ListContainer,
              HomeRecommend,
              HomeRank,
              HomeLike,
              HomeFloor,
              HomeBrand
          }
      
    • 放置到模板中

      <template>
          <div>
              <!-- 三级联动全局组件:全局组件,不需要再引入了 -->
              <TypeNav></TypeNav>
              <ListContainer></ListContainer>
              <HomeRecommend></HomeRecommend>
              <HomeRank></HomeRank>
              <HomeLike></HomeLike>
              <HomeFloor></HomeFloor>
              <HomeFloor></HomeFloor>
              <HomeBrand></HomeBrand>
          </div>
      </template>
      

POSTMAN测试接口

新接口:gmall-h5-api.atguigu.cn

--服务器返回的code字段200,代表成功

--整个项目接口的前缀都有/api

axios的二次封装

1.为什么要二次封装axios

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

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

安装axios:

npm i --save axios

2.项目中经常API文件夹【axios】

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

baseURL:'/api'

3.代码

src\api\request.js

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

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

//请求拦截器:在请求发出去之前可以做一些事情

requests.interceptors.request.use((config)=>{
    //config:配置对象,对象里面有一个属性很重要,headers请求头
    return config
});

//响应拦截器
requests.interceptors.response.use((res)=>{
    //成功的回调函数:服务器响应数据,数据在res中
    return res.data
},(error)=>{
    //响应失败的回调函数
    error
    return Promise.reject(new Error('faile'))
})
//对外暴露
export default requests;

接口统一管理

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

项目大:统一管理

跨域问题

什么是跨域:协议、域名、端口号不同请求。

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

gmall-h5-api.atguigu.cn --后台服务器

Jsonp、cors、配置代理

使用配置代理:

vue.config.js

module.exports = defineConfig({
  transpileDependencies: true,
  lintOnSave:false,
  //代理跨域
  devServer: {
    proxy: {
      '/api': {
        target:'http://gmall-h5-api.atguigu.cn',
      }
    },
  },
})

nprogress进度条插件

1:安装nprogress

npm i --save nprogress

2:检查安装package.json

"nprogress": "^0.2.0",

3:使用进度条(在和服务器发送和接受数据的时候有进度条),也就是在响应和请求拦截器里面操作

src\api\request.js

.....
//引入进度条
import nprogress from "nprogress";
//引入进度条样式
import  'nprogress/nprogress.css'
//start:进度开始 done:进度条结束
......
//请求拦截器:在请求发出去之前可以做一些事情
requests.interceptors.request.use((config)=>{
    ...
    //进度条开始动
    nprogress.start()
    ...
});

//响应拦截器
requests.interceptors.response.use((res)=>{
    ...
    //进度条结束
    nprogress.done()
    ...
},(error)=>{
    ...
})
......

vuex状态管理库

vuex是官方提供的一个插件,状态管理库。集中式管理项目中组件共用的数据。也是一种组件间通信的方式,且适用于任意组件间通信。

注意:不是所有项目都需要vuex。小项目不需要,大项目需要

安装(我们的项目安装3版本的):

npm i --save vuex@3

vuex的基本使用

  1. 创建文件夹

    src\store\index.js

  2. 默认配置

    import Vue from 'vue'
    import Vuex from 'vuex'
    //需要使用插件一次
    Vue.use(Vuex)
    
    //state:仓库存储数据的地方
    const state = {}
    //mutations:修改state的唯一手段
    const mutations = {}
    //actions:书写业务逻辑,也可以处理异步
    const actions = {}
    //getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
    const getters = {}
    

    //对外暴露Store类的一个实例 export default new Vuex.Store({

    state,
    mutations,
    actions,
    getters
    

    })

3. 引入和注册

`src\main.js`

```js
......
//引入仓库
import store from '@/store'
......
new Vue({
  ...
  store
}).$mount('#app')

    4. 图解

    

vuex实现模块化开发

如果项目过大,组件过多,接口也很多,数据也很多。需要模块化开发

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

此部分有部分模块化+命名空间的代码和尚品汇老师的不一样,格式使用的是张天禹老师的代码思路

  1. 创建模块化的文件夹并且初始化

    src\store\home\index.js

    const state ={ 
    }
    const mutations ={
    }
    const actions ={
    }
    const getters ={}
    export default {
        namespaced:true,
        state,
        mutations,
        actions,
        getters
    }
    
  2. TypeNav组件通知Vuex发请求,获取数据,存储于仓库中

    src\components\TypeNav\index.vue

    ......
    <script>
    ...
    export default {
      name: "TypeNav",
      //组件挂载完毕:可以向服务器发请求
      mounted() {
        //通知Vuex发请求,获取数据,存储于仓库中
        this.$store.dispatch("home/categoryList")
      },
      ...
    };
    </script>
    ......
    
  3. vuex接受信号

    action:接受组件的请求,并发出axios请求,并将得到的数据传递给mutations

    mutations:收到actions传递的数据,并且修改state的数据

    src\store\home\index.js

    //要使用reqCategoryList函数发出axios请求
    import {reqCategoryList} from '@/api/index'
    const state ={
        //state中的数据默认初始值不能乱写,服务器返回对象,服务器返回数组
        //根据接口的返回值去初始化
        categoryList:[]
    }
    const mutations ={
        CATEGORYLIST(state,categoryList){
            state.categoryList = categoryList
        }
    }
    const actions ={
        //通过API里面的接口函数调用,向服务器发请求,获取服务器的数据
        async categoryList({commit}){ 
            let result =await reqCategoryList()
            if(result.code==200){
                commit("CATEGORYLIST",result.data)
            }
        }
    }
    ......
    
  4. 组件使用修改后的state的数据

    src\components\TypeNav\index.vue

    ......
    <script>
    import {mapState} from 'vuex'
    export default {
      ...
      computed:{
        ...mapState('home',['categoryList'])
      }
    };
     </script>
    ......
    

    5. 将属性放到模板中:         得到的数据分为三级的,要清楚数据的结构,使用v-for放到模板中

        

完成三级联动动态背景颜色

  1. 采用样式完成(直接在组件的样式里添加)

  2. 通过js完成

    1. 在数据中定义当前指定的元素索引值(初始为-1,当前没有元素被指定)

        data() {
          return {
            //存储用户鼠标移上哪一个一级分类
            currentIndex: -1,
          };
        },
      
    2. 使用@mouseenter函数,以及index值来指定元素

      给被指定的元素添加回调,传入参数index

      回调函数:

        methods: {
          //鼠标进入修改响应式数据currentIndex属性
          changeIndex(index) {
            //index:鼠标移上某一个一级分类元素的索引值
            this.currentIndex = index;
          },
      
    3. 确定元素后给相应的元素添加样式

      通过类名(添加类名)

      修改样式

              .cur {
                background-color: skyblue;
              }
      
    4. 鼠标移出,样式消失并且当鼠标放到全部商品分类 位置上时保持样式

      1. sortdiv和前部商品分类放到一个大的DIV中

      2. 给大的DIV添加样式(事件委派|事件委托)

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

    最开始的时候是通过CSS样式display:block|nono显示与隐藏二三级商品分类

    通过js完成(在二三级分类的div增加一个判断语句):

```

卡顿现象

正常情况:鼠标进入每一个一级分类,都会触发鼠标进入事件    

非正常情况(用户操作很快):只有部分触发(浏览器反应不过来),有可能卡顿。

防抖:

前面所有的触发都被取消,最后一次执行在规定时间的之后才会触发,也就是说连续快速触发,只会执行一次(理解为LOL中的回城)

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

lodash官网:lodash官网

lodash暴露了一个_函数

使用(会在你的输入操作执行完1S后执行):

节流

在规定的间隔事件范围内不会触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发(理解为LOL中的技能CD)

使用:

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

由于声明式导航会出现卡顿问题。我们使用编程式导航+事件委派(把点击的事件添加给父元素)的处理方式

新问题 :使用事件委派会出现以下俩个重要问题

  1. 如何能确定用户点击的是a标签

  2. 如何获取子节点标签的商品名称和商品id

解决方式

对于问题一:对于问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)

对于问题二:对于问题2:为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。

我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。

  • 给a标签添加属性(三级都要)

      <a :data-categoryName="c1.categoryName" 
                    :data-category1Id="c1.categoryId">
                    {{ c1.categoryName }}
                  </a>
    
  • 父组件添加函数goSearch(事件委派)

    div class="all-sort-list2" @click="goSearch"

  • goSearch函数

    goSearch(event){
          //最好的解决方案:编程式导航+事件委派
          //利用事件委派存在一些问题:1:点击的一定是a标签。2:参数如何获取
    
          //第一个问题:把子节点中a标签,加上自定义属性data-categoryName,其余的子节点没有
          let element = event.target
          //获取到当前触发事件的节点,此节点需要带有data-categoryName属性
          //节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
          let {categoryname,category1id,category2id,category3id} = element.dataset;
    
          //如果标签身上有categoryname属性一定是a标签
          if(categoryname){
            //整理路由跳转的参数
            let location = {name:'search'}
            let query = {categoryName:categoryname}
            //三级a标签
            if(category1id){
              query.category1Id = category1id
            }
            else if(category2id){
              query.category2Id = category2id
            }
            else{
              query.category3Id = category3id
            }
            //整理参数
            location.query =query
            //路由跳转
    
            this.$router.push(location)
          }
        }
    

复习:

1)商品分类的三级列表由静态变为动态形式【获取服务器数据:解决跨域问题】

2)函数防抖与节流

3)路由跳转:声明式导航(很耗内存)、编程式导航

    编程式导航解决此问题使用自定义属性

开发 Search模块中TypeNav商品分类菜单(过度动画)

Search模块中全部商品分类菜单隐藏,鼠标移入才显示

src\components\TypeNav\index.vue

  1. 添加属性:show 用于表明是否展示

    data() {
        return {
          //存储用户鼠标移上哪一个一级分类
          currentIndex: -1,
          show:true
        };
      },
    
  2. 在挂载钩子上,进行路径判断(如果是Search路径,则隐藏)

    mounted() {
        。。。。
        //当组件挂载完毕,让show属性变为false
        //如果不是home路由组件,将typeNav隐藏
        if(this.$route.path!='/home')
        {
          this.show = false
        }
    
      },
    
  3. 当鼠标移入展示,移出隐藏:在父元素上添加移入移出函数

    leaveShow() {
          if(this.$route.path!='/home'){
            this.show = false
          }
          ....
    
        },    
    //当鼠标移入的时候,让商品分类列表进行展示
        enterShow(){
          this.show = true
        }
    
  4. 添加过渡效果

    1. transition包裹元素

    2. css效果

      <style lang="less" scoped>
          ......
          //过渡动画的样式
          //过渡动画的开始状态(进入)
          .sort-enter{
            height: 0px;
          }
          //过渡动画的结束状态(进入)
          .sort-enter-to{
            height: 461px;
          }
          //定义动画的事件、速率
          .sort-enter-active{
            transition: all .5s linear;
          }
        }
      }
      </style>
      

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

三级分类列表的优化

问题:每一次刷新home或者serach页面的时候typeNav组件都会向服务器发送请求,得到数据。能不能让发送请求这个动作只执行一次?

解决方法:将发送请求的动作放到App.vue钩子上。App.vue的mouted钩子只执行一次

src\App.vue

  mounted(){
    //通知Vuex发请求,获取数据,存储于仓库中
    this.$store.dispatch("home/categoryList");
  }

问题:能不能把这行代码放到main.js函数中执行?

答:不能,main.js中没有$store属性。只有组件有这个属性

合并参数

点击搜索按钮会传递params参数,但是没有传递query参数。点击typeNav跳转时只有query参数,没有params参数。统一一下。

src\components\TypeNav\index.vue

goSearch(event){
          ......
        //判断:如果路由跳转的时候,带有params参数,也要传递过去
        if(this.$route.params){
          location.params= this.$route.params
        }
        //整理参数
        location.query =query
        //路由跳转

        this.$router.push(location)
      }
    },

src\components\Header\index.vue

    goSearch(){
      ......
      let location = {name:'search',params:{keyword:this.keyword || undefined}}
      if(this.$route.query){
        location.query = this.$route.query
      }
      this.$router.push(location)
    }

(mock)开发Home首页当中的ListContainer组件与floor组件

注意:服务器返回的数据只有商品分类的数据,对于ListContainer组件与floor组件数据服务器没给。

mock数据(模拟);如果你想mock数据,需要用到插件mock.js

使用步骤:

  1. 在src文件中创建mock文件夹

  2. 准备json数据(mock文件夹中创建相应的JSON文件)----格式化一下,别留有空格

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

  4. 创建mockSerer.js通过mockjs插件实现模拟数据

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

获取Banner轮播图的数据(Vuex三步走)

  1. 封装mock数据请求函数(同之前的三级联动ajax请求)

    src\api\mockAjax.js

    ......
    //1:利用axios对象的方法create,去创建一个axios实例
    //2:request就是axios,只不过稍微配置一下
    const requests = axios.create({
        //配置对象
        //基础路径,发请求的时候,路径当中会出现mock
        baseURL:'/mock',
        //代表请求超时的时间5s
        timeout:5000
    })
    ......
    
  2. ListContain组件Vuex发送action请求

    src\pages\Home\ListContainer\index.vue

      ......
      mounted() {
        //派发action:通过vuex发起ajax请求,将数据存储在仓库中
        this.$store.dispatch('home/getBannerList')
    
      },
      ......
    
  3. Vuex发送mock请求,将数据存储在state中

    src\store\home\index.js

    import {reqCategoryList,reqGetBannerList} from '@/api/index'
    const state ={
       ......
        //轮播图的数据
        bannerList:[]
    }
    const mutations ={
        ......
        GETBANNERLIST(state,bannerList){
            state.bannerList = bannerList
        }
    }
    const actions ={
       ......
        //获取首页轮播图的数据
        async getBannerList({commit}){
            let result =  await reqGetBannerList();
            if(result.code===200){
                commit('GETBANNERLIST',result.data)
            }
    
        }
    }
    ......
    
  4. ListContain组件接受state的数据

    src\pages\Home\ListContainer\index.vue

    <script>
    import { mapState } from 'vuex';
    export default {
      ......
    
      computed:{
        ...mapState('home',['bannerList'])
      },
    
     ......
    };
    </script>
    

swiper插件实现轮播图

安装swiper插件:安装5版本

npm i --save swiper@5

(1)安装swiper
(2)在需要使用轮播图的组件内导入swpier和它的css样式
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
(4)创建swiper实例

注意:在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。 解决方法:在轮播图最外层DOM中添加ref属性

div class="swiper-container" id="mySwiper" ref="cur"

通过ref属性值获取DOM

let mySwiper = new Swiper(this.$refs.cur,{...})

 <!--banner轮播-->
        <div class="swiper-container" id="mySwiper" ref="cur">

          <div class="swiper-wrapper">
            <div class="swiper-slide" v-for="(carouse,index) in bannerList" :key="carouse.id">
              <img :src="carouse.imgUrl" />
            </div>
          </div>

          <!-- 如果需要分页器 -->
          <div class="swiper-pagination"></div>

          <!-- 如果需要导航按钮 -->
          <div class="swiper-button-prev" ></div>
          <div class="swiper-button-next"></div>
        </div>
<script>
//引入Swiper
import Swiper from 'swiper'
//引入Swiper样式
import 'swiper/css/swiper.css'
</script>

接下来要考虑的是什么时候去加载这个swiper,我们第一时间想到的是在mounted中创建这个实例。
但是会出现无法加载轮播图片的问题。

我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。

解决方法一:等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:

mounted() {
    this.$store.dispatch("getBannerList")
    setTimeout(()=>{
      let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
        pagination:{
          el: '.swiper-pagination',
          clickable: true,
        },
        // 如果需要前进后退按钮
        navigation: {
          nextEl: '.swiper-button-next',
          prevEl: '.swiper-button-prev',
        },
        // 如果需要滚动条
        scrollbar: {
          el: '.swiper-scrollbar',
        },
      })
    },1000)
  },

解决方法二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象

watch:{
    bannerList(newValue,oldValue){
        let mySwiper = new Swiper(this.$refs.cur,{
          pagination:{
            el: '.swiper-pagination',
            clickable: true,
          },
          // 如果需要前进后退按钮
          navigation: {
            nextEl: '.swiper-button-next',
            prevEl: '.swiper-button-prev',
          },
          // 如果需要滚动条
          scrollbar: {
            el: '.swiper-scrollbar',
          },
        })
    }
  }

即使这样也还是无法实现轮播图,原因是,我们轮播图的html中有v-for的循环,我们是通过v-for遍历bannerList中的图片数据,然后展示。我们的watch只能保证在bannerList变化时创建swiper对象,但是并不能保证此时v-for已经执行完了。假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。

完美解决方案:使用watch+[this.nextTick()](https://cn.vuejs.org/v2/api/#vm-nextTick) 官方介绍:this. nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。
个人理解:无非是等我们页面中的结构都有了再去执行回调函数

  watch: {
    //监听bannerList数据的变化:因为这条数据发生过变化----由空数组变为数组里有4个元素
    bannerList: {
      handler(newValue, oldValue) {
        //通过watch监听bannerList属性的属性值变化
        //如果执行handler方法,代表组件实例身上的属性值有了【数组:四个元素】
        //当前只能保证数据已经有了,但是v-for有可能没有渲染完毕
        //v-for执行完毕,才有结构【现在在这里无法保证】

        //nextTick:将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
        this.$nextTick(() => {
          //当你执行这个回调的时候,v-for已经渲染完毕了
          var mySwiper = new Swiper(".swiper-container", {
            loop: true, // 循环模式选项

            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              clickable: true,
            },

            // 如果需要前进后退按钮
            navigation: {
              nextEl: ".swiper-button-next",
              prevEl: ".swiper-button-prev",
            },
          });
        });
      },
    },
  },

floor组件获取数据

api模块

mock请求

src\api\index.js


//获取floor数据
export const reqFloorList = ()=>mockRequests.get('/floor')

Vuex模块

src\store\home\index.js

  1. actions

    //获取floor数据
        async getFloorList({commit}){
            let result =  await reqFloorList();
            if(result.code===200){
                commit('GETFLOORLIST',result.data)
            }
            
        }
    
  2. mutations

    GETFLOORLIST(state,floorList){
            state.floorList = floorList
        }
    
  3. state

    const state ={
        ......
        floorList:[]
    }
    

问题:

floor组件home中使用了2次,如果我们在floor中得到state的数据反而不方便,因此我们在home组件中得到数据,然后通过props传参的方式传递给floor

home组件

src\pages\Home\index.vue

<template>
    <div>
       ......
       <!-- v-for生成相应数量的floor组件,props传参 -->
        <HomeFloor v-for="(floor) in floorList " :key="floor.id" 
        :list="floor">
        ......
    </div>
</template>

<script>
。。。。。。
import { mapState } from "vuex";

export default {
    。。。。。。
    mounted() {
        //派发action,获取floor组件的数据
        this.$store.dispatch('home/getFloorList')
    },
    computed:{
        ...mapState('home',['floorList'])
    }
    
    
};
</script>

<style lang="less" scoped>

</style>

floor组件

src\pages\Home\Floor\index.vue

  1. props接受参数

     props:['list'],
    
  2. 将对应的数据放到template上去

  3. 轮播图

    1. 引入swiper

      //引入Swiper
      import Swiper from "swiper";
      
    2. 轮播图结构

      使用ref

      <div class="swiper-container" ref="cur">
           <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>
      
    3. 创建swiper实例

        //第一次书写swiper的时候:在mouted当中书写是不可以的,但是为什么现在可以
        //第一次书写轮播图的时候,是在当前组件内部发请求、动态渲染结构【需要服务器的数据返回】,因此之前不行
        //这一次的数据是父组件通过props给的,也就是渲染floor组件的时候数据已经有了。
        mounted() {
          var mySwiper = new Swiper(
                this.$refs.cur, 
                {
                  loop: true, // 循环模式选项
      
                  // 如果需要分页器
                  pagination: {
                    el: ".swiper-pagination",
                    clickable: true,
                  },
      
                  // 如果需要前进后退按钮
                  navigation: {
                    nextEl: ".swiper-button-next",
                    prevEl: ".swiper-button-prev",
                  },
                });
        },
      

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

props:用于父子组件通信

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

全局事件总线:$bus 全能

pubsub-js:vue当中几乎不用 全能

插槽

vuex

轮播图拆分成共用(全局)组件

  1. 创建文件夹

    src\components\Carousel\index.vue

  2. 轮播图的结构

    <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: "AppCarousel",
      props:['list'],
      watch:{ 
        list:{
          //立即监听,因为父组件给的数据没变化,所以使用immediate来立即监听
          immediate:true,
          handler(){
            //只能监听到数据以及有了,但是v-for动态渲染结构还是没有办法确定,因此还是需要使用nextTick
            this.$nextTick(() => {
              //当你执行这个回调的时候,v-for已经渲染完毕了
              var mySwiper = new Swiper(
              this.$refs.cur, 
              {
                loop: true, // 循环模式选项
    
                // 如果需要分页器
                pagination: {
                  el: ".swiper-pagination",
                  clickable: true,
                },
    
                // 如果需要前进后退按钮
                navigation: {
                  nextEl: ".swiper-button-next",
                  prevEl: ".swiper-button-prev",
                },
              });
            });
          }
        }
      },
    };
    </script>
    
    <style lang="scss" scoped></style>
    

    注意:

    1:轮播图结构是固定的

    2:轮播图的数据来源于props中的List,在使用这个全局组件的时候,通过props向这个组件传参。

  3. 将轮播图组件注册为全局组件

    src\main.js

    ......
    //轮播图全局组件
    import AppCarousel from "./components/Carousel";
    //第一个参数:全局组件的名字,第二个参数:组件的名字
    Vue.component(AppCarousel.name,AppCarousel)
    //引入swiper样式
    import 'swiper/css/swiper.css'
    ......
    
  4. floor组件使用轮播图组件

    src\pages\Home\Floor\index.vue

     <!-- 轮播图 -->
     <AppCarousel :list="list.carouselList"></AppCarousel>
    

    注意:list.carouselList是floor组件的数据,通过props参数的形式传递给AppCarousel(轮播图)组件

  5. ListContainer组件使用轮播图组件

    src\pages\Home\ListContainer\index.vue

    <AppCarousel :list="bannerList"></AppCarousel>
    

search模块的开发

步骤:

  1. 静态页面+静态组件拆分

  2. 发请求(API)

  3. vuex(三步走)

  4. 组件获取仓库数据,动态展示数据

静态页面

所给的资料已经写好。

发请求(API)

src\api\index.js

......
//获取搜索模块的数据  地址:/api/list  请求方式:post 参数:需要
//当前的接口,给服务器传递参数params,至少是个空对象
export const reqGetSearchInfo = (params)=> requests({
    url:"/list",
    method:'post',
    data:param
s,
})
......

vuex

src\store\search\index.js

state\mutations\actions\getters

import { reqGetSearchInfo } from "@/api";
const state ={
    //仓库初始状态
    searchList:{}
}
const mutations ={
    GETSEARCHLIST(state,searchList){
        state.searchList = searchList
    }
}
const actions ={
    //获取search模块数据
    async getSearchList({commit},params={}){
        //reqGetSearchInfo函数调用获取服务器数据的时候,至少需要一个参数
        //params形参:是当用户派发actions的时候,第二个参数,至少是一个空对象
        let result =  await reqGetSearchInfo(params)
        if(result.code===200){
            commit('GETSEARCHLIST',result.data)
        }
    }
}
//计算属性,在项目当中,为了简化数据而生
//把将来在组件中需要用的数据简化
const getters ={
    //当前形参state,当前仓库的state
    goodsList(state){
        //如果因为其他原因返回的是undefined,那么就返回一个空数组
        return state.searchList.goodsList||[]
    },
    trademarkList(state){
        return state.searchList.trademarkList||[]
    },
    attrsList(state){
        return state.searchList.attrsList||[]
    },
}

export default {
    namespaced:true,
    state,
    mutations,
    actions,
    getters
}

注意:因为使用的数据searchList为多级结构,使用getters方便组件使用。

search组件使用数据

src\pages\Search\index.vue

把数据放到对应的模板中。

......
<script>
import { mapGetters } from "vuex";
  import SearchSelector from './SearchSelector/SearchSelector'
  export default {
    name: 'AppSearch',

    components: {
      SearchSelector
    },
    mounted(){
      this.$store.dispatch('search/getSearchList',{})
    },
    computed:{
      ...mapGetters('search',['goodsList','trademarkList','attrsList'])
    },
  }
</script>
......

search根据不同参数展示不同页面

从home跳转到search时,更具搜索的关键字或者点击三级联动组件的不同的关键字来展示不同的页面。(只能展示一次)

src\pages\Search\index.vue

......
<script>
import { mapGetters } from "vuex";
import SearchSelector from "./SearchSelector/SearchSelector";
export default {
  name: "AppSearch",
  data() {
    return {
      //带给服务器的参数
      searchParams: {
        //一级分类id
        category1Id: "",
        //二级分类id
        category2Id: "",
        //三级分类id
        category3Id: "",
        //分类名字
        categoryName: "",
        //搜索关键字
        keyword: "",
        //排序
        order: "",
        //分页器:代表当前是第几页,默认是1
        pageNo: 1,
        //代表每一页展示数据的个数,默认是3
        pageSize: 3,
        //平台售卖属性参数
        props: [],
        //品牌
        trademark: "",
      },
    };
  },
  components: {
    SearchSelector,
  },
  //当组件挂载完毕之前执行一次【先于mounted之前】
  //在此处修改数据
  beforeMount(){
    //复杂的写法:如:
    //this.searchParams.category1Id = this.$route.query.category1Id
    
    //Object.assign:es6新增的语法
    Object.assign(this.searchParams,this.$route.query,this.$route.params)
  },
  mounted() {
    //在发请求之前带给服务器的参数【searchParams参数发生变化有数值带给服务器】
    this.getData()
  },
  computed: {
    ...mapGetters("search", ["goodsList", "trademarkList", "attrsList"]),
  },
  methods: {
    //向服务器发请求获取search模块数据(根据不同的数据进行展示)
    //把请求封装成函数
    getData() {
      this.$store.dispatch("search/getSearchList", this.searchParams);
    },
  },
};
</script>
......

步骤

  1. 把向vuex的actions封装成函数

      methods: {
        //向服务器发请求获取search模块数据(根据不同的数据进行展示)
        //把请求封装成函数
        getData() {
          this.$store.dispatch("search/getSearchList", this.searchParams);
        },
      },
    
  2. 在组件挂载(mounted)时,执行上述函数:

      mounted() {
        //在发请求之前带给服务器的参数【searchParams参数发生变化有数值带给服务器】
        this.getData()
      },
    
  3. 初始化data:

    data() {
        return {
          //带给服务器的参数
          searchParams: {
            //一级分类id
            category1Id: "",
            //二级分类id
            category2Id: "",
            //三级分类id
            category3Id: "",
            //分类名字
            categoryName: "",
            //搜索关键字
            keyword: "",
            //排序
            order: "",
            //分页器:代表当前是第几页,默认是1
            pageNo: 1,
            //代表每一页展示数据的个数,默认是3
            pageSize: 3,
            //平台售卖属性参数
            props: [],
            //品牌
            trademark: "",
          },
        };
      },
    
  4. 在组件挂在之前(beforeMount),更新数据data

      beforeMount(){
        //复杂的写法:如:
        //this.searchParams.category1Id = this.$route.query.category1Id
        
        //Object.assign:es6新增的语法
        Object.assign(this.searchParams,this.$route.query,this.$route.params)
      },
    
  5. 流程:

    1. home组件 =》search组件 (此时数据通过route传递给search)

    2. search组件beforeMount 钩子更新data

    3. search组件mouted钩子 调用getData()函数向vuex发送actions

    4. vuex 返回数据给search组件

Object.assign 函数

es6新增的语法:例子:将query和params的属性复制给searchParams对应的属性

<script>
    let searchParams = {
        //一级分类id
        category1Id: "",
        //二级分类id
        category2Id: "",
        //三级分类id
        category3Id: "",
        //分类名字
        categoryName: "",
        //搜索关键字
        keyword: "",
        //排序
        order: "",
        //分页器:代表当前是第几页,默认是1
        pageNo: 1,
        //代表每一页展示数据的个数,默认是3
        pageSize: 3,
        //平台售卖属性参数
        props: [],
        //品牌
        trademark: "",
    }
    let query = {
        category1Id: "110",
        categoryName: "手机",
    }
    let params = {keyword: "华为",}
    console.log(Object.assign(searchParams,query,params));
</script>

结果:

Search子组件动态开发

src\pages\Search\SearchSelector\SearchSelector.vue

  1. 接收数据(mapGetters)

    <script>
      import { mapGetters } from "vuex";
      export default {
        name: 'SearchSelector',
        computed:{
          ...mapGetters("search", ["trademarkList", "attrsList"])
        }
      }
    </script>
    
  2. 将数据放置到对于的模板中

Search组件监听路由变化再次发起请求

之前通过beforeMount更新数据以及mounted 发起请求。但是只能是从home组件跳转到search组件时更新一次。

解决方法:watch监听路由

    ......
  watch:{
    //监听属性,监听路由信息是否发生变化,如果发生变化,就再次发起请求
    $route(){
      //再次发请求之前整理带给服务器的参数
      Object.assign(this.searchParams,this.$route.query,this.$route.params)
      //再次发起ajax请求
      this.getData()
      //每一次请求完毕,应该把相应的1、2、3级分类的id置空,让它接收下一次的相应的id
      this.searchParams.category1Id=''
      this.searchParams.category2Id=''
      this.searchParams.category3Id=''
    }
  }
    ......

注意:当你之前点击了category1Id,第二次点击category2Id时,如果没有清空操作,此时参数中仍然有category1Id,因此,在每一次请求完毕后,把相应的1、2、3级分类的id置空。

Search组件更新界面的原理图

原理就是:每一次使用函数改变Data数据后,再把参数发送给Vuex,再从Vuex得到数据后更新页面。

Search组件更新面包屑部分

面包屑处理分类操作

src\pages\Search\index.vue

当你点击面包屑的”x“图标的时候,置空数据然后发起请求,修改地址栏(改变路由)。

    //删除分类的名字
    removeCategoryName(){
      //把带给服务器的请求置空了,但是还的更新页面
      //带给服务器的参数说明可有可无的,如果属性值为空的字符串还是会把相应的字段带给服务器
      //但是你把相应的字段设置为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:'search',params:this.$route.params})
      }

    },

面包屑处理关键字(搜索栏带来的关键字)

  1. 在模板上展示关键字(关键字是搜索栏模块通过params参数传递给路由的,search组件通过路由($route)拿到了query和params参数(关键字)。然后把这些数据放到了searchParams上面)

  2. 点击关键字的”x“图标.置空数据、发起请求、让Header组件清除关键字(全局事件总线)

        //删除关键字
        removeKeyword(){
          //服务器的参数searchParams的Keyword置空
          this.searchParams.keyword = ''
          //再次发请求
          this.getData() 
          //通知兄弟组件Header清除关键字
          this.$bus.$emit('clear')
          //进行路由的跳转
          if(this.$route.query){
            this.$router.push({name:'search',query:this.$route.query})
          }
    
        },
    

面包屑处理品牌

  1. 获取品牌数据(品牌数据在子组件中)子父传数据:自定义事件

    • 子组件中的品牌模板(数据在trademark中)

    • 父组件定义自定义事件:trademarkInfo函数

    • 子组件的品牌点击函数(其中触发自定义事件trademarkInfo,传递参数):

            tradeMarkHandler(trademark){
              //点击了品牌(苹果),还是需要整理参数,向服务器发请求获取相应的数据进行展示
              //子组件发请求,还是父组件发请求?:父组件
              //因为父组件中的searchParams参数是带给服务器的参数,子组件应该给父组件传递信息(自定义事件)
              this.$emit('trademarkInfo',trademark)
            }
      
    • 父组件得到点击的品牌数据,并且发起请求

          trademarkInfo(trademark){
            //1:整理品牌字段的参数  ID:品牌名称
            this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
            //再次发请求获取search模块列表的数据进行展示
            this.getData()
          }
      
  2. 在模板上展示品牌

  3. 点击品牌的”x“图标,置空数据、发起请求

        //删除品牌的信息
        removeTrademark(){
          //将品牌信息置空
          this.searchParams.trademark = undefined
          this.getData()
        },
    

平台售卖属性操作

平台售卖属性为子组件,所以要将数据通过自定义事件传递给父组件

1:子组件中模板

2:子组件的售卖属性的点击事件(触发自定义事件)

 //平台售卖属性值的点击事件
      attrInfo(attr,attrValue){
        //['属性ID:属性值:属性名']
        this.$emit("attrInfo",attr,attrValue)
      }
    },

3:父组件传递自定义事件,以及接收参数.接收参数先进行整理参数格式然后数组去重,然后存入Data中,更新页面

  //收集平台属性
    attrInfo(attr,attrValue){
      //整理参数格式
      let props = `${attr.attrId}:${attrValue}:${attr.attrName}`
      //数组去重
      if(this.searchParams.props.indexOf(props)==-1)
      {
        this.searchParams.props.push(props)
      }
      //再次发请求
      this.getData()
    },

4:添加父组件中面包屑。需要对props参数进行一些处理,只需要展示其中间部分

5:父组件面包屑删除操作;整理参数、发起请求

    //删除售卖属性
    removeAttr(index){
      //再次整理参数
      this.searchParams.props.splice(index,1)
      //再次发起请求
      this.getData()
    }
  },

排序操作

1.1问题

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

1:asc (综合升序)

1:desc (综合降序)

2:asc    (价格升序)

2:desc (价格降序)

1.2:谁应该有类名(高亮)

通过order属性值中包含1(综合)|包含2(价格)

通过添加类active实现。是否包含类由isOne和isTwo决定,这俩个属性根据Data决定

  computed: {
   .......
    isOne(){
      return this.searchParams.order.indexOf('1')!=-1
    },
    isTwo(){
      return this.searchParams.order.indexOf('2')!=-1
    },
    ......
  },

1.3:谁应该有箭头

答:谁有类名谁有箭头,类名和箭头绑定。

怎么使用箭头:iconfont。添加类名。以及引入。

1.4:箭头应该是向上还是向下。

由当前的数据决定

  computed: {
    ......
    isAsc(){
      return this.searchParams.order.indexOf('asc')!=-1
    },
    isDesc(){
      return this.searchParams.order.indexOf('desc')!=-1
    }
  },

1.5.添加点击事件

绑定点击事件(传递参数,区别点击的是哪个)

点击事件方法

 changeOrder(flag){
      //flag形参:标记,代表用户点击的是综合还是价格【用户点击的时候传递进来的】
      //这里获取到的是每次点击起始状态
      let originFlag = this.searchParams.order.split(':')[0]
      let originSort = this.searchParams.order.split(':')[1]
      //这个语句确定点击的元素是否是上次点击的那个
      if(flag==originFlag){
         //如果是,就调换箭头方向
        this.searchParams.order = `${flag}:${originSort=='desc'?'asc':'desc'}`
      }
      else{
         //如果不是,就修改点击的元素
        this.searchParams.order = `${flag}:desc`
      }
      console.log(this.searchParams.order);
     //发送请求,更新页面
      this.getData()

    }

分页器

为什么要分页。每个页面加载少量数据,防止卡顿。

ElmentUI是有相应的分页组件,使用起来超级简单,但是前台不用。

分页器的展示,需要哪些数据?

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

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

3:需要知道整个分页器一共有多少条数据:total字段表示--{获取一共有多少页的数据}

4:需要知道分页器连续页面个数:5|7 【奇数,对称,好看】continues字段代表

总结:对于分页器而言,需要知道以上4个前提条件

注意:对于分页器而言,很重要的一个地方【算出连续页面的起始和结束数字

分页器中间部分的完成【重要】

  1. 参数

    参数通过父组件search通过props传递给子组件AppPagination

    参数解释如上

  2. 中间部分的结构

    初始结构:

  3. 计算属性totalPage,计算一共有多少页

     computed:{
        //计算总共有多少页
        totalPage(){
            //向上取整
            return Math.ceil(this.total/this.pageSize)
        },
    
  4. 定义中间部分的起始和结束部分(由当前页面pageNo计算而来)

    注意:要排除错误部分,比如当前页面是1的话,那么起始页面也要是1.不能溢出

        startNumAndEndNum(){
            //先定义俩个变量存储起始数字和结束数字
            let start = 0 ,end = 0
            //连续的页码是5【至少5页,如果不够5页】
            if(this.continues>this.totalPage){
                start = 1;
                end = this.totalPage
            }else{
                //正常现象:总页数大于5
                start = this.pageNo - Math.floor(this.continues/2)
                end =  this.pageNo + Math.floor(this.continues/2)
    
                //把出现不正常的现象纠正
                //start小于最小页数
                if(start <1){
                    start = 1
                    end = this.continues
                }
                //end大于总页码
                if(end>this.totalPage){
                    end = this.totalPage
                    start = this.totalPage - this.continues + 1
                }
            }
            return {start,end}
        }
    
  5. 中间部分的应用(v-for在数字上的应用)

        <!-- 中间部分 -->
        <button 
        v-for="(page,index) in startNumAndEndNum.end"
        :key="index"
        v-if="page>=startNumAndEndNum.start"
        >{{page}}</button>
    

动态展示分页器(结构)

根据父组件传递的数据动态展示结构

分为上 中[已完成] 下 三组

v-for:数组|数字|对象|字符串

上面部分

  1. 最前面的1要不要展示

    如果中间部分的start是大于1(至少2起步)那么就展示

    <button v-if="startNumAndEndNum.start>1">1</button>
    
  2. 三点的省略要不要?

    如果中间部分的start是大于2的(至少3起步),那么就展示

    <button v-if="startNumAndEndNum.start>2">···</button>
    

下面部分

同样的关于后面的省略号以及最后页码展示问题。如果中间部分的end小于最后页码,那么就展示。如果中间页码的end小于最后页码减一,那么就展示省略号。

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

总体结构:

<template>
  <div class="pagination">
    <button>上一页</button>
    <!-- 上 -->
    <button v-if="startNumAndEndNum.start>1">1</button>
    <button v-if="startNumAndEndNum.start>2">···</button>

    <!-- 中间部分 -->
    <button 
    v-for="(page,index) in startNumAndEndNum.end"
    :key="index"
    v-if="page>=startNumAndEndNum.start"
    >{{page}}</button>

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

    <button style="margin-left: 30px">共 {{total}} 条</button>

  </div>
</template>

分页器按钮的点击效果添加

点击不同的按钮。会跳转到不同的页面

  1. 父组件参数的修改

    参数要使用服务器传递过来的参数

    total参数使用mapState获得

        ...mapState({
          total:state =>state.search.searchList.total
        })
    
  2. 父组件获得子组件参数

    要给各个按钮添加点击事件只需修改pageNo ,因此父组件只需要得到子组件传递过来的pageNo 参数即可(子给父传数据使用自定义事件)

    1:添加自定义事件

    2:得到参数,修改页面

        //自定义事件的回调函数:获取当前第几页
        getPageNo(pageNo){
          //整理带给服务器的参数
          this.searchParams.pageNo = pageNo
          //发请求
          this.getData()
        }
    
  3. 子组件触发自定义事件传递参数

    src\components\Pagination\index.vue

    上一页按钮(传递pageNo-1):

    当前是第一页时失去点击效果::disabled="pageNo==1"

    第一页按钮(传递1)

    中间部分

    下面部分(同理)

  4. 添加当前页高亮效果

    给当前页的按钮添加高亮,只需要中间部分的按钮添加高亮

    类(css)

        &.active {
          cursor: not-allowed;
          background-color: #409eff;
          color: #fff;
        }
    

开发详情页面

1:静态组件(详情页的组件:注册为路由组件)

静态组件由写好的detail文件给出

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

  1. 注册路由组件

    文件目录:src\pages\Detail\index.vue

    路由:src\router\routes.js

    {        
            ///:skui:params参数
            path:"/detail/:skuid",
            component:AppDetail,
            meta:{show:true}
        }
    
  2. search组件图片点击后跳转(包括传参)

    src\pages\Search\index.vue

  3. 整理路由

    路由配置信息太长,将其提取出来成一个单独的js文件

    src\router\index.js

    //配置路由,默认暴露
    export default new VueRouter({
        //配置路由
        routes,
    })
    

    整理后的js文件:src\router\routes.js

  4. 滚动行为

    跳转到detail页面后,默认滚动条在最下方。我们希望滚动条一开始就在最上方。这一点vue封装好了,叫做滚动行为。

    src\router\index.js

    //配置路由,默认暴露
    export default new VueRouter({
        。。。。。。
        //滚动行为
        scrollBehavior () {
            // y:0,代表滚动条在最上方
            return {y:0}
        }
    })
    

2:发请求(api)

使用params参数

//获取产品信息详情的接口 url:/api/item{skuId}  请求方式:get 参数:必须
export const reqGoodsInfo = (skuId)=> requests({
    url:`/item/${skuId}`,
    method:'get',
})

3:vuex(三步)

  src\store\detail\index.js

  1. actions

    const actions ={
        //获取产品信息的action
        async getGoodInfo({commit},skuId){
            let result = await reqGoodsInfo(skuId)
            if(result.code==200){
                commit('GETGOODINFO',result.data)
            }
        }
    }
    
  2. mutations

    const mutations ={
        GETGOODINFO(state,goodInfo){
            state.goodInfo = goodInfo
        }
    }
    
  3. dispach

    src\pages\Detail\index.vue

        mounted() {
          //派发actions,获取产品详情信息
          this.$store.dispatch('detail/getGoodInfo',this.$route.params.skuId)
        },
    

4:动态展示组件

  1. 得到数据、参数(重要)

    参数是search组件点击跳转时,通过params参数放到route里面

    search组件:

    detail组件(将参数传递给vuex):

        mounted() {
          //派发actions,获取产品详情信息
          this.$store.dispatch('detail/getGoodInfo',this.$route.params.skuId)
        },
    

    得到state数据,getters方便使用:

        computed:{
          ...mapGetters('detail',['categoryView','skuInfo']),
          //给子组件的数据
          skuImageList(){
            //如果服务器数据没有回来,skuInfo至少是一个空对象
            return this.skuInfo.skuImageList||[]
          }
        }
    
  2. 将得到的数据放入模板中

zoom组件(展示的大图部分以及放大镜)

  1. search组件将数据通过props参数传递给zoom。(数据是search组件通过vuex得到的)

  2. 将数据放到zoom模板中

  3. 问题一:父组件直接使用props传递参数会报错(警告)

    search组件中在传递参数时,vuex还没有从服务器取得数据,此时传递的参数没有得到。

    解决(计算属性,赋予初值空对象):

          skuImageList(){
            //如果服务器数据没有回来,skuInfo至少是一个空对象
            return this.skuInfo.skuImageList||[]
          }
    
  4. 问题二:子组件使用props传递参数会报错(警告)

    子组件模板使用的时props参数的属性的属性,同样的会出现使用的时候参数还没得到的情况。此时相当于从undefined上取属性。(其实就是,从空对象身上取不存在的属性不会报错,从undefined身上取属性会报错)

    解决:(计算属性,赋予初值空对象):

          imgObj(){
            return this.skuImageList[0]||{}
          }
        }
    

放大镜功能

  1. 接收兄弟组件传递过来的数据

    当兄弟组件(底下的轮播图)点击图片时,要在大图上展示对应的图片。由于使用的是同一组数据便利的,因此传递和接受的是index值)

    全局事件总线(在挂载的时候获取):

        mounted() {
          //全局事件总线获取兄弟组件传递过来的索引值
          this.$bus.$on('getIndex',(index)=>{
            //修改当前响应式数据
            this.currentIndex = index
          })
        },
    
  2. 放大镜功能

    放大镜功能主要分为2部分:第一部分是当鼠标移动的时候,蒙版跟着鼠标移动。第二是右侧的放大镜展示对应的部分

    1:蒙版部分

    分为四部分,得到蒙版、计算蒙版的top和left、进行范围约束、修改蒙版的top和left

          handler(event){
            //使用ref属性得到蒙版
            let mask = this.$refs.mask
    
            //计算蒙版的top和left
            let left = event.offsetX-mask.offsetWidth/2
            let top = event.offsetY - mask.offsetHeight/2
    
            //约束范围
            if(left<0){
              left = 0
            }
            //基于大图的宽度刚好等于2个蒙版的宽度
            if(left>mask.offsetWidth) left = mask.offsetWidth
            if(top<0) top =0
            //基于大图的高度刚好等于2个蒙版的高度
            if(top>mask.offsetHeight) top = mask.offsetHeight
    
            //修改元素的left和top
            mask.style.left = left+'px'
            mask.style.top = top+'px'
          }
        },
    

        2:放大镜部分

        放大镜部分是蒙版图像的2倍,因此在移动的时候只要移动2倍的top和left就可以,不过需要往相反的方向

  handler(event){

        let big = this.$refs.big
        let left = event.offsetX-mask.offsetWidth/2
        let top = event.offsetY - mask.offsetHeight/2
        //约束范围
        if(left<0){
          left = 0
        }
        //基于大图的宽度刚好等于2个蒙版的宽度
        if(left>mask.offsetWidth) left = mask.offsetWidth
        if(top<0) top =0
        //基于大图的高度刚好等于2个蒙版的高度
        if(top>mask.offsetHeight) top = mask.offsetHeight
        //修改元素的left和top

        //右边放大镜的图片是原版图片的2倍
        big.style.left = -2*left+'px'
        big.style.top = -2*top+'px'
      }
    },

detail组件中的商品售卖属性部分

src\pages\Detail\index.vue

  1. 将数据放入模板中

    数据是search从vuex得到的。类似于三级联动,这里我们使用了2个v-for循环

  2. 点击属性高亮效果

    注意:@click绑定的方法,传递的都是实参,可以直接修改组件中的数据。索尼在方法中修改arr,实际修改了spuSaleAttr.spuSaleAttrValueList

    //产品售卖属性值高亮(排他)
          changeActive(SaleAttrValue,arr){
            //遍历全部售卖属性值isChecked='0'没有高亮
            arr.forEach(item=>{
              item.isChecked=0
            })
            //点击的那个售卖属性值
            SaleAttrValue.isChecked=1;
          }
    

ImageList 组件

商品页下的轮播图

  1. 接收父组件通过props传递的参数,并放到模板中

  2. Swiper使用轮播图

    1. 轮播图结构(模板)

    2. 引入Swiper

      import Swiper from "swiper";
      
    3. Swiper实例

      实例放到watch中并且使用$nextTick

        watch: {
          //监听数据:可以保证数据一定ok,但是不能保证v-for结构是否完事
          skuImageList() {
            this.$nextTick(() => {
              new Swiper(this.$refs.cur, {
                // 如果需要前进后退按钮
                navigation: {
                  nextEl: ".swiper-button-next",
                  prevEl: ".swiper-button-prev",
                },
                //显示几个图片的设置
                slidesPerView : 3,
                //每一次切换图片的个数
                slidesPerGroup : 1
              });
            });
          },
        }
      
  3. 高亮

    添加currentIndex,初始化为0

  4. 点击切换高亮并且通知兄弟组件zoom

    全局事件总线$bus,因为使用的是同一份数据,所以可以传递index

      methods: {
        changeCurrentIndex(index){
          //修改响应式数据
          this.currentIndex = index
          //通知兄弟组件当前的索引值
          this.$bus.$emit('getIndex',index)
        }
      },
    };
    

购买产品的部分

这部分购买的产品结构比较简单,但是购买产品的数量是一个重要的参数。因此在数据添加参数来表示

1:添加参数

    data() {
      return {
        //购买产品的个数
        skuNum: 1,
      };
    },

2:相应结构

加和减的点击函数直接写在结构中,只有input输入的点击函数写在method中

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

3:点击函数

注意:对于非法输入的处理方式比较重要

       //表单元素修改产品个数
      changeSkuNum(event){
        //用户输入进来的文本*1
        let value = event.target.value * 1
        //如果用户输入进来非法(字符串输入*1=NaN或者小于1)
        if (isNaN(value)||value<1) {
            this.skuNum=1
        }else{
          //正常,给他取整
          this.skuNum = parseInt(value)
        }
      },

加入购物车操作

加入购物车操作主要有俩个点:一个是点击时需要将客户选购的数量(skuNum)传递给服务器(vuex)。一个是点击后就进行跳转操作,这时需要将skuNum传递给购物车组件。

  • 给服务器传递skuNum

    1. api

      //将产品添加到购物车中(获取更新某一个产品的个数)
      // /api/cart/addToCart/{ skuId }/{ skuNum }  POST
      export const reqAddOrUpdateShopCart = (skuId,skuNum) =>requests({
          url:`/cart/addToCart/${ skuId }/${ skuNum }`,
          method:'post'
      })
      
      
    2. vuex

      actions: 注意:由于只需要将{skuId,skuNum}传递给服务器,而服务器不需要返回数据,因此只需进行action。其次:由于要确认添加数据的成功与否,使用promise。

          //将产品添加到购物车中
          async addOrUpdateShopCart({commit},{skuId,skuNum}){
              //加入购物车返回的解构
              //加入购物车以后(发请求),前台将参数带给服务器
              //服务器不返回其他数据,只是返回code200,代表操作成功
              //因此不需要三连环
              let result = await reqAddOrUpdateShopCart(skuId,skuNum)
              //当前的这个函数如果执行返回Promise
              if(result.code==200){
                  return 'ok'
              }else{
                  //代表加入购物车失败
                  return Promise.reject(new Error('fail'))
              }
      
      
          }
      
    3. 组件发请求以及确认购物车添加成功与否

      src\pages\Detail\index.vue

            //加入购物车的回调
            async addShopCar(){
              //1:发请求---将产品加入到数据库(通知服务器)
              //怎么判断加入购物车是成功还是失败
              //以下语句其实就是调用仓库中的函数,这个函数有async,返回的一定是一个promise
              //要么成功|要么失败
              try {
                await this.$store.dispatch('detail/addOrUpdateShopCart',{skuId:this.$route.params.skuId,skuNum:this.skuNum})
                //2:服务器存储成功,路由跳转、传递参数
                //3:在路由跳转的时候还需将产品信息带给组件
                //下面这种通过路由传参是可以的。但是地址栏不好看
                // this.$router.push({name:'addCartSuccess',query:{skuInfo:this.skuInfo}})
                //一些简单的数据skuNum,通过query形式带过去
                //产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据消失)
                //不管是本地存储|会话存储,一般都是字符串
                sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))
                this.$router.push({name:'addCartSuccess',query:{skuNum:this.skuNum}})
              } catch (error) {
                //3.服务器存储失败,提示用户
                alert(error.message)
              }
              
            }
      
  • 给购物车组件传递参数

    点击“加入购物车”后,需要跳转页面到购物车组件。需要传递的参数有skuInfo(是个对象),skuNum(数字)。因为skuInfo是个对象而且比较大,如果使用路由传参的话,路由地址会很难看。因此我们使用会话存储来传递参数。

          //加入购物车的回调
          async addShopCar(){
            //1:发请求---将产品加入到数据库(通知服务器)
            //怎么判断加入购物车是成功还是失败
            //以下语句其实就是调用仓库中的函数,这个函数有async,返回的一定是一个promise
            //要么成功|要么失败
            try {
              await this.$store.dispatch('detail/addOrUpdateShopCart',{skuId:this.$route.params.skuId,skuNum:this.skuNum})
              //2:服务器存储成功,路由跳转、传递参数
              //3:在路由跳转的时候还需将产品信息带给组件
              //下面这种通过路由传参是可以的。但是地址栏不好看
              // this.$router.push({name:'addCartSuccess',query:{skuInfo:this.skuInfo}})
              //一些简单的数据skuNum,通过query形式带过去
              //产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据消失)
              //不管是本地存储|会话存储,一般都是字符串
              sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))
              this.$router.push({name:'addCartSuccess',query:{skuNum:this.skuNum}})
            } catch (error) {
              //3.服务器存储失败,提示用户
              alert(error.message)
            }
          }
    

添加购物车成功组件

添加购物车成功组件没有很多操作,只有俩个跳转的标签

  1. 注册组件

    src\router\routes.js

        {
            path:"/addcartsuccess",
            name:'addCartSuccess',
            component:AddCartSuccess,
            meta:{show:true}
        },
    
  2. 从会话存储中取出数据并且放到模板中

    skuInfo(){
            //重新转换成对象
            return JSON.parse(sessionStorage.getItem('SKUINFO'))
          }
    

  3. 查看商品详情

  4. 去购物车结算

    <router-link to="/shopcart">去购物车结算</router-link>

购物车组件

  1. 购物车的静态组件已经给出。

  2. 注册路由。

    {
            path:"/shopcart",
            component:ShopCart,
            meta:{show:true}
        },
    

  3. 兄弟组件(AddCartSuccess)跳转

    <router-link to="/shopcart">去购物车结算</router-link>

  4. api

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

    • actions

      const actions ={
          //获取购物车列表数据
          async getCartList({commit}){
              let result = await reqCartList()
              
              if(result.code==200){
                  commit("GETCARTLIST",result.data)
              }
          }
      }
      
    • mutations

      const mutations ={
          GETCARTLIST(state,cartList){
              state.cartList = cartList
          }
      }
      
    • state

      const state ={
          //仓库初始状态
          cartList:[]
      }
      
    • getters

      //计算属性,在项目当中,为了简化数据而生
      //把将来在组件中需要用的数据简化
      const getters ={
          cartList(state){
              return state.cartList[0]||{}
          },
          
      }
      
  6. 购物车组件发起请求

    mounted() {
          this.getData()
        },
        methods:{
          getData(){
            this.$store.dispatch('shopcart/getCartList')
            },
        },
    

UUid游客身份

经过上述流程后,会面临一个问题:后台返回的数据为空。

原因:没有用户识别,后台不能返回对应的数据

解决:使用UUid,得到对应的唯一用户识别码,然后再向后台发送数据

注意:由于其他模块依赖,已经有了UUID包

生成UUID

src\utils\uuid_token.js

import { v4 as uuidv4 } from 'uuid';
//要生成一个随机字符串,且每次执行不能发生变化,游客身份持久存储
export const getUUID = ()=>{
    //先从本地存储获取uuid(看一下本地存储是否有)
    let uuid_token = localStorage.getItem("UUIDTOKEN")
    //如果没有,就生成
    if(!uuid_token){
        //生成
        uuid_token = uuidv4()
        //存储
        localStorage.setItem('UUIDTOKEN',uuid_token)
    }
    return uuid_token
}

src\store\detail\index.js

const state ={
    goodInfo : {},
    //游客临时身份
    uuid_token:getUUID()
}

使用UUID

使用UUID的时候不把UUID放到参数里面传递,而是使用请求头发送过去.字段由后台指定

src\api\request.js

//请求拦截器:在请求发出去之前可以做一些事情
requests.interceptors.request.use((config)=>{
    //config:配置对象,对象里面有一个属性很重要,headers请求头
    //进度条开始动
    nprogress.start()
    if(store.state.detail.uuid_token){
        //请求头添加一个字段(userTempId):后台老师指定的
        config.headers.userTempId = store.state.detail.uuid_token
    }
    return config
});

此后发请求(加入购物车、购物车页面发送请求)就会带有userTempId字段,

修改购物车产品数量&&全选框

当我们使用UUID添加在请求头之后,我们的请求就能够得到数据。

  1. 组件得到vuex的数据

    src\pages\ShopCart\index.vue

        computed:{
          ...mapGetters('shopcart',['cartList']),
          //计算出来的购物车数据
          cartInfoList(){
            return this.cartList.cartInfoList||[]
          },
          //计算总价
          totalPrice(){
            let sum = 0
            this.cartInfoList.forEach(item => {
              sum+=item.skuNum*item.skuPrice
            });
            return sum
          },
        }
    
  2. 将数据放入到模板中

    看数据自己操作

  3. 全选框操作

    计算属性:isAllChecked

          //判断底部的复选框是否勾选【全部产品都选中,才勾选】
          isAllChecked(){
            //遍历数组元素
            return this.cartInfoList.every(item=>item.isChecked==1)
          }
    

    全选框绑定操作

  4. 修改购物车数量

    修改购物车数量需要对+、-以及输入框都要编写函数。在这里使用同一个点击函数

    点击函数:

    根据后台的要求,我们修改的函数就是之前detail组件加入购物车的addOrUpdateShopCart函数,传递俩个参数。一个是产品id,一个是变化的值(不是修改之后的值,例如加一传递1,从4-->10的话传递6)。由于添加购物车时我们的产品对应的数量为0,所以传递多少后台就加多少。

    步骤:计算出disNum-->传递给后台(派发action)-->更新页面

          async handler(type,disNum,cart){
            //type:为了区分三个元素
            //disNum:+和-传递的是变化量,input传的是最终量
            //哪一个产品【身上有id】
            //向服务器发起请求
            switch (type) {
              case 'add':
                disNum=1
                break;
              case 'minus':
                //判断产品的个数大于1,才可以传递给服务器-1
                //如果出现的产品个数小于等于1,传递给服务器个数0(不动)
                disNum = cart.skuNum>1?-1:0
                break;
              case 'change':
                //用户输入进来的最终量,非法的(带有汉字|出现负数),带给服务器数字0
                if (isNaN(disNum)||disNum<1) {
                  disNum = 0
                }else{
                  //正常情况:(小数:取整),带给服务器变化的量: 用户输入进来-产品起始个数
                  disNum = parseInt(disNum)-cart.skuNum
                }
                break
            }
            //派发actions
            try{      
              //代表修改成功   
              await this.$store.dispatch('detail/addOrUpdateShopCart',{
                skuId:cart.skuId,
                skuNum:disNum})
              //更新页面
              this.getData()
            }catch(e){
              console.log(e.message);
            }
          }  
    
  5. 点击‘-’的节流操作

    当你点击-(减)过快时,会将数量减为负数。个人理解:当你疯狂点击时,执行到判断数字的时候,服务器还没返回数据,因此判断条件disNum = cart.skuNum>1?-1:0 总是能通过,因为disNum没有刷新。通过之后会发起请求,因此服务器会受到多个减一请求。由于服务器不对数据进行验证,因此将负数的数据发送给了组件,并且放到了模板上

    解决方式:使用lodash,添加节流操作,保证你点击的时候数据已经返回和更新了。

    步骤:

          1:引入lodash

                import throttle from 'lodash/throttle'

           2:使用

      handler:throttle(async function(type,disNum,cart){
        ......
      },1000),

删除购物车操作

其实购物车详情部分都是通过后台给的数据来刷新页面(主要是要记住用户),所以基本上每一个操作的流程都是类似的

流程:绑定点击函数---->API(发送给服务器,携带相应的参数)----> vuex(action发请求,一般返回的数据都是空的,因为你只需要告诉服务器进行了说明操作就可以) ---->刷新页面(获取服务器更新好的数据)

  1. 绑定点击函数

  2. API

    //删除购物车产品的接口
    // URL : /api/cart/deleteCart/{skuId} method:DELETE
    export const reqDeleteCartById = (skuId) =>requests({
        url:`/cart/deleteCart/${skuId}`,
        method:'delete'
    })
    
  3. vuex

        //删除购物车某一个产品
        async deleteCartListBySkuId({commit},skuId){
            let result = await reqDeleteCartById(skuId)
            if(result.code==200){
                return 'ok'
            }
            else{
                return Promise.reject(new Error('faile'))
            }
        },
    
  4. 更新页面

          //删除某一个产品的操作
          async deleteCartById(cart){
            try {
            //如果删除成功再次发请求获取新的数据进行展示
                await this.$store.dispatch('shopcart/deleteCartListBySkuId',cart.skuId)
                this.getData()
            } catch (error) {
                alert(error.message)
            }
          },
    

修改产品状态

和上述删除操作的步骤差不多(重点和难点在于怎样拿到你要传递的参数)

  1. 绑定点击函数

    这里为什么要传$event呢?是因为这是单向绑定,你修改了input的状态后必须要通过 event才能拿到。

    是否可以通过每次点击对cart.isChecked取反得到?个人认为可以。

  2. API

    //修改商品选中的状态
    // URL: /api/cart/checkCart/{skuId}/{isChecked}  GET
    export const reqUpdateCheckedById = (skuId,isChecked) =>requests(
        {
            url:`/cart/checkCart/${skuId}/${isChecked}`,
            method:'get'
        }
    )
    
  3. vuex

        //修改购物车某一个产品的选中状态
        async updateCheckedById({commit},{skuId,isChecked}){
            let result = await reqUpdateCheckedById(skuId,isChecked)
            if(result.code==200){
                return 'ok'
            }
            else{
                return Promise.reject(new Error('faile'))
            }
        }
    
  4. 更新页面

          //修改某一产品的勾选状态
          async updateChecked(cart,$event){
            //带给服务器的参数isChecked,不是布尔值,应该是0|1
            try {
              let checked = $event.target.checked?'1':'0'
              await this.$store.dispatch('shopcart/updateCheckedById',{skuId:cart.skuId,isChecked:checked})
              //更新数据
              this.getData()
            } catch (error) {
              alert(error.message)
            }
          }
    

删除选中的全部商品

后台没有删除全部商品的接口,但是之前有做过删除某一个产品。

注意:一般像这几个功能回台没有返回数据,我们使用if-else结构来返回’ok‘,或者错误的promise,因此我们需要在组件中使用try-catch来捕获结果

  1. 绑定点击函数

    发请求

  2. vuex

    为什么不写API(这个函数的功能没有对应的api),调用的是别人的api

        //删除全部勾选的产品
        deleteAllCheckedCart({dispatch,getters}){
            //context:小仓库,conmmit,getters,dispatch,state
            //获取购物车中全部的产品(数组)
            let PromiseAll = []
            getters.cartList.cartInfoList.forEach(item => {
                let promise  = item.isChecked==1?dispatch('deleteCartListBySkuId',item.skuId):''
                //将每一次返回的promise添加到数组当中
                PromiseAll.push(promise)
            });
            //只有全部的promise都成功,则返回成功
            return Promise.all(PromiseAll)
        }
    

    注意:

    1:{dispatch,getters}是解构赋值

    2:PromiseAll将每一次调用返回的promise保存,确保每一次都是成功的

  3. 组件捕获结果、更新页面

           //删除所有选中的产品
          async deleteAllCheckedCart(){
              //派发一个action
              try {
                await this.$store.dispatch('shopcart/deleteAllCheckedCart')
                this.getData()
              } catch (error) {
                console.log(error.message);
              }
              
          }
    
    
    

修改全部商品的状态

这个功能也没有单独的接口,因此还是通过遍历的方式来修改每个元素的状态。

这个按钮一开始的状态是根据产品的状态来的,之后更改的的时候需要让产品跟着按钮的状态来改变自己的状态。

  1. 绑定函数、传递参数、发起请求

    因为修改全部产品需要对所有产品遍历,因此不需要传递多余的参数。不过需要event(不需要传递,直接拿)。

          //修改全部产品的选中状态
          async updateAllCartChecked(event){
            let isChecked = event.target.checked?"1":"0"
            //派发action
           this.$store.dispatch('shopcart/updateAllCartIsChecked',isChecked)
           ......
          }
    
  2. VUEX

    在这里我使用了2种方式发起请求(第一种自己写的)。第一种:遍历前对元素的状态判断:如果和全选按钮更改后的状态不一致,则修改。第二种:直接对所有元素的状态进行修改

        //修改全部产品的状态
        updateAllCartIsChecked({dispatch,state},isChecked){
            let promiseAll = []
            console.log(state);
            state.cartList[0].cartInfoList.forEach(item=>{
                let promise = item.isChecked!==isChecked?dispatch('updateCheckedById',
                {skuId:item.skuId,isChecked:isChecked}):''
                /* let promise = dispatch('updateCheckedById',{skuId:item.skuId,isChecked}) */
                promiseAll.push(promise)
            })
            //最终返回的结果
            return Promise.all(promiseAll)
        }
    
    
  3. 组件捕获结果、更新页面

      //修改全部产品的选中状态
      async updateAllCartChecked(event){
        let isChecked = event.target.checked?"1":"0"
        //派发action
        try {
          await this.$store.dispatch('shopcart/updateAllCartIsChecked',isChecked)
          this.getData()
        } catch (error) {
          console.log(error.message);
        }
      }