尚硅谷商品汇项目复习文档

项目资料

项目源码:https://gitee.com/HusePanghu/project-SP
项目地址:HusePanghu.SPH


前言

提示:本文档的编撰初衷是用于复习和回顾该项目,而非该项目的教程文档,弊处多多,敬请包涵。欢迎大家在评论区交流。


1、各种基础文件介绍     

node_modules:存储项目依赖文件
public:存放项目静态资源,注意:当项目打包时,webpack会把public文件夹原封不动的打包到dist文件中
src:程序员代码文件夹
assets:组件中的静态资源文件夹,一般存放组件公用的静态资源;在webpack打包的时候会将assets作为一个模块,打包到一个js文件夹中。
components:组件文件夹,存放vue中的各个组件。(一般是非路由组件)
App.vue:唯一的一个根组件
main.js:项目的入口文件,也是项目中最先执行的一个文件。

babel.config.js:与Babel相关的配置文件
package.js:相当于项目‘身份证’,记录着项目的名称,项目有哪些依赖,项目怎么运行。
package-lock.json:缓存性文件

2、项目的一些其他配置:

        1、项目启动 npm run serve 之后自动打开网页(localhost:8080)
        package.json

"scripts": {
        "serve": "vue-cli-service serve --open",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint"
        }

         2、 关闭eslint校验提示
                在vue.config.js文件夹中 设置 lintOnSave:false

//关闭lint校验提示
lintOnSave:false,

        3、src文件夹设置别名 @ ,src文件夹创建别名,src/...简化为@/...

 "paths": {
            "@/*": [
            "src/*"
            ]
            },

3、项目路由的分析:

        1、vue-router
                何为路由:kv键值对
                key:路由组件的路径
                value:路由组件

        2、项目中的路由组件:
                home组件、search组件、login组件、register组件
        3、项目中的非路由组件:
                header组件、footer组件(在home、search组件中显示,在login、regist中不展示)

4、项目流程

        项目以开发业务和逻辑为主,css和HTML开发为次
                1、编辑静态页面、确定页面样式
                2、拆分组件
                3、获取服务器的动态展示数据
                4、完成相应的动态业务逻辑
        注意事项:1、编辑静态页面时,组件的结构+组件的样式+图片资源,编辑不要有遗漏 2、本项目中样式使用的是less样式,在<style>中需标注lang='less',还需npm 安装less-loader 3、当样式编辑好之后,记得得清除默认样式,否则编辑的样式可能被覆盖而显示不出来。 public/index.html <link rel="stylesheet" href="./reset.css">

5、项目路由的设定 vue-router

        1、路由组件一般放置在pages||views文件夹中,components放置的为非路由组件
        2、部分路由组件:home、search、login、register
        3、设置路由器router,暴露并注册到main.js文件中;在路由器中注册路由组件
        4、路由配置文件一般放在router文件中
        5、当路由注册完成,不管是路由组件还是非路由组件,他们身上都有$router、$route属性
                $router:一般进行编程式路由导航跳转(push、replace)
                $route:一般获取路由信息(params、query、路径等信息)
        6、声明式导航<router-link>,编程式导航push\replace 声明式导航功能<编程式导航功能

6、Footer的显示与隐藏,mate路由元信息

 routes:[{
            path:''',
            components:,
            mate:{show:false}
        }]

        v-show="$route.meta.show"

7、路由传参

        传参的类型:params传参、query传参
                params参数:属于路径中的一部分,注意在路由中配置路径是需要占位,占位符 ‘:‘
                query参数:不属于路径的一部分,不需要占位,类似于ajax中的queryString,/home?k=v&kv=
        三种方式:字符串传参、模板字符串、对象传参

        //字符串传参
        this.$router.push('/search/'+this.keyword+'?k='+this.keyword.toUpperCase())
        //模板字符串传参
        this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
        //对象传参(最常用)
        this.$router.push({
        name:'search',
        params:{keyword:this.keyword},
        query:{k:this.keyword.toUpperCase()}
        })

8、拆分 home组件

        把预先准备好的静态页面拆分成七个路由组件

        css+HTML+图片资源,缺一不可,细心!

        分别注册引入到home组件中

<template>
  <div>
    <TypeNav></TypeNav>
    <ListContainer></ListContainer>
    <TodayRecomment></TodayRecomment>
    <Rank></Rank>
    <Like></Like>
    <Floor v-for="(floor,index) in floorList" :key="floor.id" :floor="floor"></Floor>
    <Brand></Brand>
  </div>
</template>


   
9、测试api接口(postman)

        使用postman工具测试尚硅谷提供的接口没有问题
        接口返回的code字段为200,则表示接口访问成功,状态正常
        整个项目接口前缀都有/api字样               

10、axios二次封装,二次封装的目的

        1、配置请求拦截器和响应拦截器
                请求拦截器:在发起请求访问之前,进行一些业务处理。比如添加请求头关键字、设置请求进度条
                响应拦截器:在获得返回数据之后,可以进行一些业务处理。
        2、api文件夹
                用来存放api接口相关的文件和配置文件
                接口文档中都有/api,baseURL:'/api'

                '

//利用axios里的create方法创建axios实例
const requests=axios.create({
    //设置基础路径
    baseURL:'/api',
    //设置访问超时时间
    timeout:5000
})

11、跨域问题

        1、什么是跨域?从本地访问协议、端口、域名不同的地址叫做跨域访问,区别于本地访问
                HTTP://localhost:8080/#/home 本地服务器
                http://gmall-h5-api.atguigu.cn 后台服务器,非本地服务器
        2、跨域的解决办法:JOSNP、CORS、代理proxy

12、引入进度条nprogress

        npm i nprogress@0.2.0
        在请求拦截器中开始,在响应拦截器中结束

        引入nprogress的样式,(/node_modules/nprogress/nprogress.css),样式可修改 蓝色 #29d

//设置请求拦截器
requests.interceptors.request.use((config)=>{
    //进度条开始
    nprogress.start();
    return config;
})
//设置响应拦截器
requests.interceptors.response.use((res)=>{
    //成功的回调函数:当访问数据返回后,可以响应拦截,做一些业务处理
    //进度条结束
    nprogress.done();
    return res.data;
},(error)=>{
    return Promise.reject(new Error('fail'))
})


13、vuex

        1、多个组件间状态共享的集中式状态管理工具,用于组件间通讯
                大项目可用来维护数据,使数据维护轻量化、便捷化,小项目没必要使用vuex
                属性:state,actions,mutations,getter,modules
        2、vuex模块化开发
                把所有的状态(数据)放到一个store中,当项目数据过多时,就会显得store过于臃肿,
                数据的易维护特点便也失去了意义,所以就有了vuex模块化开发
                modules:{}

14、完成TypeNav的三级联动数据展示

        1、全局组件TyepNav,使用vuex发起api接口访问,获得数据

mounted() {
    //vue一挂载就立马发起请求CategoryList,存储于仓库中
    this.$store.dispatch('categoryList')
  }


        2、mounted(){}挂载里编辑回调函数,vue一挂载便开始请求api获取数据
            store.home actions->mutations->state使用mapstate展示数据   

<div class="sort" v-show="show"  @mouseleave="showLeave">
              <div class="all-sort-list2" @click="goSearch">
                <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"
                     :class="{cur:index==currentIndex}">
                  <h3 >
                    <!-- 渲染后 元素的属性名会自动转为小写 -->
                    <a @mouseenter="indexChange(index)" :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{ c1.categoryName }}</a>
                  </h3>
                  <!--二三级展示列表 -->
                  <div class="item-list clearfix" :style="{display:index==currentIndex?'block':'none'}">
                    <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                      <dl class="fore">
                        <dt>
                          <a :data-categoryName="c2.categoryName"
                             :data-category2Id="c2.categoryId">{{ c2.categoryName }}</a>
                        </dt>
                        <dd>
                          <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                            <a :data-categoryName="c3.categoryName"
                               :data-category3Id="c3.categoryId">{{ c3.categoryName }}</a>
                          </em>
                        </dd>
                      </dl>
                    </div>
                  </div>
                </div>
              </div>
            </div>

15、向一级分类动态添加背景色

        1、第一种解决方案,采用样式
                .item:hover{
                background: skyblue;
                }
        2、第二种解决方法,TypeScript
                创建参数currentindex获取category列表的index值,当index==currentindex时 触发样式
                :class="{cur:index==currentindex}"
                样式cur .cur{blackground:skyblue}
                在methods中写触发事件函数,@mouseenter=“”,@mouseleave=“”

16、通过js控制二三级样式的隐藏与展示

        原生css中的style为:display:block|none

//js控制样式:
:style="{display:index==currentIndex?'block':'none'} 
//vue v-show: 
v-show="index==currentIndex"

17、卡顿现象 

        当事件触发非常频繁,每次事件触发都会进行一次回调,频繁执行回调则有可能导致浏览器卡顿。
        节流:在规定的时间间隔内不会触发事件回调,只有当时间间隔大于设定值回调才会被执行,使频繁触发变为少量触发
        防抖:前面的触发执行都被取消,只有最后一次在规定时间间隔里的触发才被执行。频繁的时间触发只执行一次。debounce

18、完成三级联动的节流操作

        //节流器写法,频繁触发->少量触发,节流触发次数

import throttle from "lodash/throttle" 
indexChange:throttle(function (index){ this.currentIndex=index; },50),

19、三级联动的路由跳转->search.html

        策略:a标签+编程式导航+事件委派 event.target定位标签+自定义属性
        缘由:页面一渲染,声明式导航<router-link>就会在循环体里会自动生成很多导航标签,内存使用率高,效率低。编程式导航同样存在内存占用高,页面卡顿的情况,所以均不采用

goSearch(event) {
      //获取event事件的点击位置
      let element = event.target
      // console.log(element)
      let {
        //前端标签里的属性名自动转为小写,取值的时候得注意
        categoryname,
        category1id,
        category2id,
        category3id
      } = element.dataset
      console.log(categoryname, category1id, category2id, category3id)
      //整理路由跳转的参数
      if (categoryname) {
        let location = {name: 'search'}
        let query = {categoryName: categoryname}
        if (category1id) {
          query.category1Id = category1id
        } else if (category2id) {
          query.category2Id = category2id
        } else {
          query.category3Id = category3id
        }
        if(this.$route.params){
          location.query = query
          location.params=this.$route.params
          // console.log(location);
          this.$router.push(location);
        }
      }
    },

20、对search页面进行categoryList显示控制

        1、鼠标悬停展示,鼠标移走关闭显示
        2、均在TypeNav.vue 编辑js操作,使用this.$route.path 获取当前页面路径
        3、添加过渡效果 <transition name="xxx"></transition>
    组件必须要有v-if/v-show属性才能使过渡效果生效
    样式: .xxx-enter{} .xxx-enter-to{} .xxx-enter-active{transition: all 0.2s linear;}

21、search页面的性能优化问题

        每跳转一次search页面就会发起一次categoryList的get请求,而在第一次获取到请求结果时,store中已经存储了请求数据 所以只需请求一次即可 将请求代码 this.$store.dispatch('categoryList')写在App.vue的mounted中,数据全局可使用。

22、合并参数query和params参数

            在search的input框中执行回调函数时,向push中加入query参数
            在三级联动的路由跳转回调中,加入params参数

    location.query=this.$route.query;
    location.params=this.$route.params;

23、swiper 轮播图插件

        import ‘/swiper/css/swiper.css’
        swiper轮播图生效的前提是必须先渲染前端结构
        mock 模仿 模拟发起访问请求,获取数据。数据是假数据,前提写好的。

mockServe.js   
        import Mock from 'mock.js'//导入的Mock为一个函数
        import banner from './banner.json'
        Mock.mock(url:'/mock/banner',{code:200,data:banner})

24、轮播图效果生效时间问题。

        由于swiper生效的前提是页面dom必须渲染完成,而swiper实例创建在哪里就成了问题
        1、swiper实例创建在mounted中,执行swiper实例创建时,数据也还没拿到,页面dom就没完成渲染,轮播效果不生成。
        2、在mounted中写一个计时器setTimeout(),实例创建放在计时器中,实例创建完成时,dom创建了,页面也有了,轮播效果也有,但有延时,不完美!
        nextTick:在下一次更新dom 循环结束之后,开始执行延时回调函数 异步操作, nextTick保证了执行延时回调时,dom已经更新。
        3、在mounted中使用nextTick,实例创建放在nextTick中,轮播有效果,但有延时,因为mounted执行完成就会开始执行nextTick,但此时bannerList获取数据这个流程却还没有完成,页面依旧没有渲染(异步操作的原因)。
        4、watch+nextTick解题,监视mounted中的bannerList是否有完成数据获取,当bannerList中获取了数据,再开始执行swiper实例创建
        此方法完美解决轮播生效问题。

watch:{
            bannerList:{
                    handler(newValue,oldValue){
                        this.$nextTick(()=>{
                            const mySwiper = new Swiper ('.swiper-container', {
                                    loop: true, // 循环模式选项
                                    // 如果需要分页器
                                    pagination: {
                                    el: '.swiper-pagination',
                                    clickable:true,
                                    },
        
                                    // 如果需要前进后退按钮
                                            navigation: {
                                              nextEl: '.swiper-button-next',
                                              prevEl: '.swiper-button-prev',
                                            },
                                
                                            // 如果需要滚动条
                                            scrollbar: {
                                              el: '.swiper-scrollbar',
                                            },
})})}}}

25、组件中通讯的方式

        props通讯 父传子
        自定义绑定事件:@on,@emit 子传父
        消息全局总线:$bus 全能
        消息订阅与发布:pubsub 全能
        插槽
        vuex 全能

26、Carsouel全局组件

        将swiper轮播图组件全局注册,实现全局复用

        并利用props传参。:list="list" 组件props接收

//注册 Carsouel轮播组件为全局组件
    import Carsouel from '@/components/Carsouel/index'
    Vue.use(Carsouel.name,Carsouel)
    <!--轮播图组件Carsouel-->
    <Carsouel :list="floor.carouselList"></Carsouel>

27、Search路由组件

        1、search路由静态页面拆分和资源部署
                css+html+images,并注册路由组件
        2、获取search页面resSearchInfo数据
            1、api/index.js const reqresSearchInfo=(parmas)=>{
                return requests({rul:'/list',method:'post',data:parmas})
                }
            2、在search mounted(){} this.$store.dispatch();
                store 三连载 actions->mutations->getters->mapGetters
            3、在search页面更新动态数据
        3、发起请求访问时,需要携带参数
            this.$store.dispatch("resSearchInfo",this.searchParmas);
            params=Object.assign(this.searchParmas,this.$route.parmas,this.$route.query)//合并相同的参数

28、search页面存在的一个问题

        在搜索框中输入keyword点击搜索之后页面没反应,只有刷新页面才会展示搜索内容
        是因为在点击搜索之后,并没有触发访问请求api,dispatch
        所以可以对keyword进行watch监视,监视页面路由route变化,一旦路由发生变化就发起访问请求   

watch: {
        $route(newValue, oldValue) {
          //再次更新请求参数,
          //Object.assign()合并具有相同参数的对象,并更新参数
          Object.assign(this.searchParams, this.$route.params, this.$route.query)
          this.getData();
          //请求结束后需要清空路由参数,防止下一请求参数叠加混乱
          this.searchParams.category1Id = "";
          this.searchParams.category2Id = "";
          this.searchParams.category3Id = "";
        }
      }

29、面包屑

        页面上细小的属性块,面包屑展示的是路由信息,可删除
        1、search页面的面包屑,展示了当前搜索的关键信息,categoryName、keyword、trademark、AttrValue


        2、每当添加、删除一个面包屑,搜索页的关键信息改变,也就是路由路径改变,都需要重新发起一次访问请求来更新search页面。绑定自定义点击事件

<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i
                @click="removeCategoryName">×</i></li>
removeCategoryName() {
      //点击面包屑‘×’标志,请求路径中的属性被清除,并且重新发起访问请求并刷新页面
      this.searchParams.categoryName = undefined;
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
      //路径请求属性清除之后,再次发起访问请求,刷新页面
      // this.getData();//由于路径改变,watch监视路径能监视到变化,所以会触发watch里的请求,而这里就不需要重复请求一次了。
      //但是清除面包屑属性并不会修改搜索框params属性,所以params属性在请求路径中需要保留
      if (this.$route.params) {
        // console.log(this.$route.params)
        this.$router.push({name: 'search', params: this.$route.params})
      }
    },

 30、售卖属性的操作 升序与降序

        1、属性:综合和价格
        点击”综合“展示综合属性的商品列表,默认降序展示,再点一次”综合”升序展示,同时样式改变↓->↑

changeSort(flag) {
          //获取之前的flag和sort信息
          let originFlag = this.searchParams.order.split(":")[0];
          let originSort = this.searchParams.order.split(":")[1];
          //定义一个新order
          let newOrder = "";
          if (flag == originFlag) {//当页面的flay没有发生改变,则改变排序方式
            newOrder = `${flag}:${originSort == 'desc' ? 'asc' : 'desc'}`;
          } else {//如果改变了,则为newOrder输入新flay和默认排序方式
            newOrder = `${flag}:desc`;//${"desc"}
          }
          //更新order属性,发起访问请求,刷新页面
          this.searchParams.order = newOrder;
          this.getData();
        },

        2、引入阿里巴巴图标库
                1、在public的index.html中添加引入的图标样式链接

    <!--    iconfont阿里图片链接-->
    <link rel="stylesheet" href="https://at.alicdn.com/t/c/font_3566864_wlpeakzjqio.css">

                2、引入样式 class="iconfont" 绑定样式 :class="{'icon-long-arrow-up'}"

<span v-show="isOne" class="iconfont"
         :class="{'icon-long-arrow-up':isUp,'icon-long-arrow-down':isDown}">
    </span>

31、手写分页器 重难点

        1、分页器的结构一般分为6部分
                 «上一页  第一页  ...  中  ...  最后一页  下一页»  共10条数据
                中间部分的连续展示页continues一般为奇数长度(5、7、9页),居中效果美观。
        2、难点:如何在不同情况下确定连续展示页的起始页和结束页(start,end)

startNumAndendNum() {
              let start = 0;
              let end = 0;
              const {pageNo, totalPage, continues} = this;
              if (totalPage < continues) {//当总页数小于连续页长度时
                start = 1;
                end = totalPage;
              } else {                    //正常情况下确定起始页和结束页
                start = pageNo - Math.floor(continues / 2);
                end = pageNo + Math.floor(continues / 2);
                if (start < 1) {           //起始页小于1时重新确定起始页、结束页
                  start = 1;
                  end = continues;
                }
                if (end > totalPage) {      //结束页大于总页数时
                  start = totalPage - continues + 1;
                  end = totalPage;
                }
              }
              return {start, end}
            }

        3、页面编辑分页器结构

<!-- 上-->
        <button :disabled="pageNo==1" @click="goPageNo(pageNo-1)">上一页</button>
        <!-- 第一页-->
        <button v-if="pageNo>Math.ceil(continues/2)" @click="goPageNo(1)" :class="{active:pageNo==1}">1</button>
        <button v-if="pageNo>1+Math.ceil(continues/2)">···</button>
        <!-- 中-->
        <button v-for="(n,index) in startNumAndendNum.end-startNumAndendNum.start+1"
                :key="index" @click="goPageNo(startNumAndendNum.start+n-1)"
                :class="{active:pageNo==startNumAndendNum.start+n-1 }">{{ startNumAndendNum.start + n - 1 }}
        </button>
        <!-- 下-->
        <button v-if="pageNo<totalPage-Math.ceil(continues/2)">···</button>
        <!-- 最后一页-->
        <button v-if="pageNo<totalPage-Math.ceil(continues/2)+1"
                @click="goPageNo(totalPage)"
                :class="{active:pageNo==totalPage}">{{ totalPage }}
        </button>
        <button :disabled="pageNo==totalPage" @click="goPageNo(pageNo+1)">下一页</button>
        <!--数据列表的数据条数-->
        <button style="margin-left: 30px">共 {{total }} 条</button>

        4、访问指定页 点击分页器中的某一页跳转到该页

                 绑定点击事件@click 获取当前页 pageNo 更新pageNo,发起请求访问

searchPage(page) {
                this.searchParams.pageNo = page;
                this.getData();
              }

        5、注册分页器为全局组件,全局复用

//注册Pagination分页器组件为全局组件
import Pagination from '@/components/Pagination/index'
Vue.use(Pagination.name,Pagination)
<Pagination :pageNo="searchParams.pageNo"
                      :pageSize="searchParams.pageSize"
                      :total="total"
                      :continues="5"
                      @goPageNo="searchPage"></Pagination>

32、商品详情页面

        1、编辑页面: HTML、css、静态资源,在router中注册路由组件
        2、动态展示商品详情页信息
                1、准备vuex,新建一个子库,detail.js
                2、detail.js state、actives、mutations、getters 在主库中注册
        3、向后端发起请求,获取数据

const reqGoodList=(skuId)=>{ return requests({url:`/detail/${skuId}`,method:"get"})}
            mounted(){ this.#store.dispatch("GoodInfo",this.$route.params.skuId)}
            vuex actives->mutations 
            mapState({
                goodInfo:(state)=>{
                    return state.detail.goodInfo.categoryView
                }   
            })

         注意:由于存在页面已经开始渲染了但绑定的属性却还是undefined的情况,控制台会报错,
                避免报错,采用mapGetters获取数据可以避免该问题,因为在detail的store中可
                可以getters预先处理数据,若还未获取到数据赋空{}|[]
                categoryView(){
                    return state.goodInfo.categoryView||{};
                },
            ...mapGetters(['categoryView'])
        4、在页面用插值语法{{}}展示数据

33、详情页面的放大镜

        根据鼠标事件获取展示图片的像素坐标(event.offsetX,event.offsetY),以像素坐标为中心点展示一个放大的图片mask
        难点:1、获取的像素坐标的约束范围(left,top)

//属性约束
        if(left<=0) left=0;
        if(left>=mask.offsetWidth) left=mask.offsetWidth;
        if(top<=0) top=0;
        if(top>=mask.offsetHeight) top=mask.offsetHeight;

                2、放大倍数的方向问题
                放大之后,放大的倍数好确定,但得注意,像素坐标得反方向放大,展示的才为像素坐标为中心的图片

 //放大展示 反方向
        big.style.left=-2*left+'px';
        big.style.top=-2*top+'px'

34、商品可选属性的排他操作

        商品的颜色、配置、内存容量等属性可供用户选择,但选择一个高亮展示,其他的同类型属性就得关闭高亮展示,高亮只能有一个,排他操作。
        v-for遍历每个属性,在属性中配置一个点击事件,并传递当前属性和全部属性

@click="isCheck(spuSaleAttr,spuSaleAttrValue)"
        spuSaleAttr.forEach((item)=>{
        //关闭高亮效果
            item.isChecked=0;
        )
        spuSaleAttrValue.isChecked=1;

        forEach(()={})遍历每一对象

35、将商品添加到购物车

        1、<a @click="addShopCart">加入购物车</a>
        2、派发actions,三连环:dispatch->actions->reqAddOrUpdataCart
        问题:商品添加到谁的购物车里?由于此时未登录,认定添加到了一个游客的购物车里。一般购物网站点击到了这一步会要求登录才能进行下一步
        问题:怎么添加到游客的购物车里?访问请求的接口reqAddOrUpdataCart会根据请求头headers里的关键字段uuid_token,将商品信息添加到该游客的购物车里。
    3、设置uuid_token

import {getUUID} from '@/utils/uuid_token.js'
        在store.detail中 uuid_token=getUUID();
        import {v4 as uuidv4} from 'uuid'
        export const getUUID=()={
            let uuid_token=localstoreg.getItem('UUIDTOKEN')  
            if(!uuid_token){
                uuid_token=uuidv4();
                //将uuid_token存储到本地存储中,持久化存储
                localStorage.setItem.setItem('UUIDTOKEN',uuid_token)
            }
            return uuid_token;
        }
        //将uuid_token添加到requests.js的请求拦截器的headers中
        //返回config配置对象,里边有一个属性很重要,header请求头
        //给请求头添加一个userTempId临时id,userTempId字段为后端接收的字段名,不可随意编辑,否则后端显示无该参数
        if(store.state.detail.uuid_token){
        config.headers.userTempId=store.state.detail.uuid_token;
        }

36、成功添加购物车

        若let result=await reqAddOrUpdataCart() result.code==200,则购物车添加成功

//添加或更新购物车,传递参数skuId,skuNum
    export const reqAddOrUpdataCart=(skuId,skuNum)=>{
    return requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:'post'})
    }

37、购物车页面

        1、HTML、style、css、静态资源,在router中注册路由组件
        2、创建shopCart的vuex
                1、/store shopCart.js state->actions->mutations->getters
        3、获取购物车商品列表 getCart();请求接口会根据headers 的userTempId获取该游客(未登录)的cartList
                mounted(){this.getCart()}
                getCart()->actions->reqShopCartList->mutations->getters return state.cartList[0]|| [];//当数据还未获取到时,返回一个空数据,避免报错的保险操作
                computed:{...getters(['cartList'])}//获取数据,页面展示 v-for
        4、删除购物车某一商品
                <a class="sindelet" @click="DeleteCartGood(good.skuId)">删除</a>
                DeleteCartGood(skuId)->dispatch->actions->reqDeleteCarListById->回到DeleteCartGood,再次调用getCart()重新获取购物车页面商品信息
        5、修改某一个商品的勾选状态
                <input @change="updateChecked(good,$event)">
                updateChecked->dispatch->actions->reqUpdateCheckedById(skuId,isChecked)->回到updateChecked,再次调用getCart()重新获取购物车页面商品信息

38、删除全部勾选的商品

        <a @click="deleteAllChecked">删除选中的商品</a>
        deleteAllChecked->dispatch->actions->在actions中派发dispatch,dispatch('deleteCartList',item.skuId)->getCart();
        代码实现:forEach逐一删除

deleteAllCheckedList({dispatch,getters}){
                let PromiseAll=[]
                getters.cartList.cartInfoList.forEach(item=>{
                    let promise=item.isChecked==1?dispatch('deleteCartList',item.skuId):''
                    PromiseAll.push(promise)
                })
                //只要全部的p1|p2....都成功,返回结果即为成功
                //如果有一个失败,返回即为失败结果
                return Promise.all(PromiseAll);
            }

39、勾选全选框控制所有商品的勾选

updateAllChecked({dispatch,getters},isChecked){
        let PromiseAll=[]
        getters.cartList.cartInfoList.forEach(item=>{
            let promise=dispatch('updateCheckedById',{skuId:item.skuId,isChecked})
            PromiseAll.push(promise)
        })
        return Promise.all(PromiseAll);
    }
    //computed:
    isAllCheck(){
    //every是forEach的break用法,
    //遍历数组里面原理,只要全部元素isChecked属性都为1===>真 true
    //只要有一个不是1======>假false
    let checked= this.cartInfoList.every((good)=>good.isChecked==1)
    return checked;
    },

40、登录注册

        1、HTML、css、style、静态资源,在router中注册路由组件
        2、创建user store库 vuex
                用于存储用户的相关消息
                user.js (state actions mutations getters)->store index.js->向主库注册子库

41、注册组件

        1、四个input框,手机号、验证码、密码、再次输入密码
                data(){return{phone:"",code:'',password:'',password1:''}}
                全部v-model=''绑定
        2、获取验证码
                由于向手机号发送验证码,有成本,所以就直接从后端获取验证码之后直接自动输入到input框中
                <button  @click="getCode" >获取验证码</button>
                getCode->dispatch->actions->reqGetCode->mutations->state.code
                this.code=this.$store.state.user.code,v-model双向绑定,input自动填入
        3、注册用户信息
                由于验证码为自动填入,则没有验证验证码是否正确的一步
                所以直接验证,phone、code是否填入,password==password1
                //注意!访问请求的参数一定不能写错,参数名保持一致,顺序保持一致
                this.$store.dispatch('userRegister',{phone,password,code})->actions->reqUserRegister
                code=200 后端成功注册用户信息->注册成功跳转到登录页面 this.$router.push('/login')

42、用户登录

        1、在用户注册之后,后台会为用户生成一个用户令牌token。
        2、用户登录时输入的邮箱/用户名/手机号、password仅用于验证登录,用户信息均已后台获取得到的为准,获得用户信息需要token
        3、用户登录成功之后,会返回一个token,将token存储到localStorage中,持久化存储,页面刷新用户也不会丢失。

//给请求头添加token
        if(store.state.user.token){
        config.headers.token=store.state.user.token;
        }
        export const=setToken(token){localStorage.setItem('TOKEN',token)}
        //引入setToken
        import {setToken} from '@/utils/token.js'
        this.$store.dispatch('userLogin',{password,phone})->actions setToken(result.data.token)->mutations->state.token或者 state.token:getToken();
        //获取token
        export const getToken=()=>{
        return localStorage.getItem('TOKEN')
        }

        4、获取用户信息
                1、home页面根据用户token,获取用户信息 this.$store.dispatch('getUserInfoByToken'),在home的header中展示个人信息
                2、获取到userInfo信息之后存储到user state.userInfo中,在header页面展示用户信息||退出登录,如果userInfo.name为空则展示登录||注册

43、退出登录

        1、发起退出登录的请求,并清空用户信息(userInfo,token)
                logout->actions->reqLogout->mutations

//mutations 操作state,退出登录,清除用户信息
     LOGOUT(state){
     state.token=''
     state.userInfo=''
     clearToken()
     }
    import {clearToken} from '@/utils/token.js'
    //清除本地存储数据
    export const clearToken=()=>{
    localStorage.removeItem('TOKEN');
    }

44、路由守卫

        1、全局守卫
                对项目中全局的路由跳转都进行监控和管制,可通过全局前置守卫(router.beforeEach())
        和后置守卫(router.afterEach())进行路由跳转管制。
        2、独享守卫
                某一个路由所独享的一个守卫,路由代码写在routes的组件中,独享守卫没有后置守卫,但可以和全局后置守卫配合使用。router.beforeEach((to,from,next)={})。
        3、组件内守卫
                路由规则写在路由组件中。beforeRouterEnter(){}进入守卫,进入组件时路由规则被调用
                        beforeRouterLeave(){}离开守卫,路由规则在离开该组件时被调用
                        beforeRouterUpdate(){}更新守卫,路由更新时路由规则被调用。
        4、vue的路由访问形式/模式:hash和history。有#的为hash路由,#值后边的内容称之为hash值。

45、使用路由导航控制用户的登录注册行为

        当用户未登录时,无法访问交易页面、订单页面、支付页面、个人主页并自动跳转到登录页面。
        当用户登录之后,home页面的登录/注册将会被替换为个人信息和退出登录。当用户在网址导航栏中跳转时,将会被自动跳转到主页。

//@/router/index.js
Router.beforeEach((to,from,next)=>{
            const toPath=to.path;
            const fromPath=from.path;
            const token=this.$store.state.user.token;
            if(!token){//用户未登录,无token
                if(toPath.indexOf('trade')!=-1||toPath.indexOf('pay')!=-1||toPath.indexOf('center')!=-1){
                    next('/login');//路由守卫管制,跳转到login页面               
            }else{next();//除了上述地址,其余可以访问,放行}
    }else{//用户已登录
    if(toPath=='/login'||toPath=='/register'){
        next('/home');//自动跳转到主页。
    }else{
    if(name){next();}else{
    //当用户登录之后,用户名获取不到,则重新获取
    try{
        //重新获取userInfo
        await store.dispatch('getUserInfoByToken')
        next();
    }catch (error) {
        //如果userInfo获取失败,则表明token失效,获取不到userInfo
        //token失效,重新登录
        await store.dispatch('logout')
        next('/login')
        }
    }   
    }
}
})

46、订单提交页面(trade)

        1、html、css、images、注册路由
        2、动态绑定数据
                //获取用户地址信息
                        this.$store.dispatch('getUserAddressInfo')
                //获取订单信息
                        this.$store.dispatch('getOrderInfo')
                store三连环:dispatch->actions->mutations->state
                ...mapState({userAddressInfo:(state)=>{return state.trade.userAddressInfo},
                orderInfo:(state)=>{return state.trade.orderInfo}})

        3、绑定路由
                在ShopCart页面声明式导航,跳转到'/trade'
                编辑组件内路由守卫路由规则,只能从ShopCart页面跳转到Trade页面
    在trade页面:

 //组件内守卫
        //只能从购物车跳转到交易页面
        beforeRouteEnter(to,from,next){
        if(from.path=='/shopcart'){
        // next('/trade');
        next();
        }else {
        next(false);
        }
        },

        4、提交订单,并跳转到支付页面
                1、编程式导航
        <a class="subBtn" @click="submitOider">提交订单</a>
        this.$router.push(`/pay?orderId=${result.data}`)
                2、向后端接口发起请求,提交订单信息

async submitOider(){
        //提交订单
        const {orderInfo}=this;
        this.orderId=orderInfo.tradeNo;
        this.orderDetailList=orderInfo.detailArrayList;
        let data={
            consignee: orderInfo.consignee,
            consigneeTel: orderInfo.phoneNum,
            deliveryAddress:orderInfo.fullAddress,
            paymentWay: "ONLINE",
            orderComment: this.msg,
            orderDetailList:this.orderDetailList
        }
        let result=await this.$API.reqSubmitOrder(this.orderId,data)
        console.log(result)
        //订单提交成功,跳转到pay页面
        if(result.code==200){
            this.$router.push(`/pay?orderId=${result.data}`)
        }else {
            return result.message;
        }
}

47、支付页面Pay

        1、html、css、images
        2、获取支付页面信息
                1、不再使用state三联环获取数据,使用全局API发起请求,返回结果直接存储在data()中。原来是在actions中发起请求,在mutations中处理数据,存储在state中,现在尝试一种新方式,直接在组件中操作api并存储返回结果。
                        1、将api接口js文件全局暴露,并全局引入。
                                在mian.js文件中 import * as API from '@/api/index'
                                beforeCreate中 Vue.prototype.$API=API 全局引入完成。
                        2、发起api请求    

data(){return{payMentInfo: {}}}
const result=this.$API.reqPaymentInfo(this.orderId);//注意异步请求
if(result.code==200){this.payMentInfo=result.data||{}}

        3、绑定动态数据,订单号、订单总金额

48、订单支付功能 使用element-ui messageBox支付二维码弹窗

        1、element-ui按需引入,全局引入会使得项目打包之后的文件非常庞大,占用空间。

//按需引入需要安装插件babel-plugin-component,详情查询官网
            //按需引入element-ui
            import {Button, MessageBox} from 'element-ui';
            Vue.use(Button);
            Vue.use(MessageBox)
            //或者组件方式引入
            // Vue.component(Button.name, Button);
            // Vue.component(Select.name, Select);
            //使用
            <Button/>

        2、引入element 消息盒子和消息弹窗

import MessageBox from 'element-ui';
        Vue.prototype.$msgbox = MessageBox;
        Vue.prototype.$alert = MessageBox.alert;

        3、QRcode 链接转二维码插件,npm i qrcode 下载
                import QRCode from 'qrcode'
                let url=await QRCode.toDataURL(this.payMentInfo.codeUrl)
        4、使用element组件 创建定时器setInterval,不断查看支付状态,当获取并保存支付状态,关闭定时器clearInterval(this.timer),this.timer=null;  

async open() {
      let url=await QRCode.toDataURL(this.payMentInfo.codeUrl)
      this.$alert(`<img src= ${url} />`, '微信支付', {
        dangerouslyUseHTMLString:true,
        center:true,
        confirmButtonText: '已完成支付',
        showCancelButton:true,
        cancelButtonText:'支付遇到问题',
        beforeClose:(action,instance,done)=>{
          if(action=='cancel'){
            alert('请稍后再试')
            //关闭定时器
            clearInterval(this.timer);
            this.timer=null;
            done();
          }else {
            if(this.payMentCode==205){
              // clearInterval(this.timer)
              // this.timer=null;
              done();
              this.$router.push('/paysuccess');
            }
          }
        }
      });
      //在支付框里发起请求,查看支付状态
      if(!this.timer){
        this.timer=setInterval(async ()=>{
          let result=await this.$API.reqPaymentStatus(this.payMentInfo.orderId);
          //保存订单状态码
          console.log(result)
          this.payMentCode=result.code;
          if(result.code==205){
              clearInterval(this.timer);
              this.timer=null;
              console.log('定时器已关闭')
              // this.$msgbox.close();
              // this.$router.push('/paysuccess')
          }
        },2000)
      }
    },

        5、支付完成跳转paysuccess页面
                 <router-link to="/paysuccess">支付完成</router-link>

49、我的订单页面Center(个人中心)

        1、HTML、css、images
        2、注册路由

{
            name:'center',
            component:Center,
            path:'/center',
            mate:{
            show:true           
            }
        },

        3、订单页面为二级路由页面
        注册二级路由

{
            name:'center',
            component:Center,
            path:'/center',
            mate:{
                show:true           
            },
            children:[
                {
                name:'myorder',
                component:MyOrder,
                path:"myorder",//二级路由下,路径直接写路径名,不用带'/'.
                },
                {
                name:'grouporder',
                component:GroupOrder,
                path:'grouporder',
                },
                {//重定向,自动展示二级路由下的myorder
                path:'/center',
                redirect:'/center/myorder'
                }
            ]      
        },
        <router-view />

50、编辑myorder页面

        1、HTML、css、images
        2、请求访问全局api获取数据
                const result=this.$API.reqAllPaymentInfo(this.page, this.limit);
                data(){return orders:{}}
                if(result.code==200){this.orders=result.data;}
        3、页面绑定动态数据
        4、注册到center路由下的子路由中,完成路由绑定

51、图片懒加载 lazyload

import VueLazyLoad from 'vue-lazyload'
    Vue.use(VueLazyLoad,{
        loading:'@/assets/images/img.gif'
    })

        在页面使用v-lazy='' 代替img=''就实现了图片懒加载的运用

52、路由懒加载

        component:()=>{return import('@/pages/search/index.vue')}
        只有当调用路由时才加载路由,而非项目一挂载就全部加载,提高了路由的高效性

const foo=()=>{ruturn import('@/pages/search/index.vue')}
    {
        name:'search',
        path:'/search',
        component:foo//调用路由
    }
    //简写形式:
    {
        name:'search',
        path:'/search',
        component:()=>import('@/pages/search/index.vue'),
    }

53、vee-validate表单验证

        1、plugins思维,当使用一款插件时,插件的引入、调用、属性编辑、编辑规则我们可以全部放在一个js文件里,在main.js文件里全局引入。这样就不用全部堆在main.js文件里显得臃肿。实现分块管理。比如,element-ui的引入和Vue.use()使用全部放在一个js文件里,在main.js文件里导入该文件。实现element-ui的按需引用。
        2、在@/plugins/validate.js文件里编辑相关信息

//vee-validate 插件导入
import Vue from 'vue'
import VeeValidate from 'vee-validate'
//引入中文验证
import zh_CN from 'vee-validate/dist/locale/zh_CN'

Vue.use(VeeValidate);

//编辑属性
VeeValidate.Validator.localize('zh_CN',{
    messages:{
        ...zh_CN.messages,
        is:(fail)=>{
            return `${fail}必须与密码相同`;
        }
    },
    attributes:{
        phone:'手机号',
        code:"验证码",
        password:"密码",
        password1:"确认密码",
        agree:"协议"
    },
})

//自定义校验规则
VeeValidate.Validator.extend('agree',{
    validate:(value)=>{
        return value;
    },
    getMessage(field) {
        return field+'必须同意'
    }
})

        3、在页面绑定校验信息

<input type="text" placeholder="请输入你的手机号"
    v-model="phone"
    name="phone" v-validate="{required:true,regex:/^1\d{10}$/}"//编辑输入框的输入规则
    :class="{invalid:errors.has('phone')}"
>
//绑定样式
<span class="error-msg">{{ errors.first('phone') }}</span>//显示提示信息

54、项目打包

        1、在项目文件夹下npm run build,生成dist文件

        2、项目上线参考:(2条消息) ubuntu20.04安装nginx并配置反向代理_HusePanghu的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值