【Vue项目】尚品汇实战笔记

文章目录

项目资源

github–shop-vue2-shangpinhui

阿里云

通用步骤

1,静态页面

2,拆分组件

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

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

注意: 使用less样式要安装less, less-loader

npm install --save less less-loader
样式添加
<style scoped lang="less">

images

环境

1,拆分header和footer

注意把样式和HTML和图片一起导入

2,路由组件搭建

安装vue-router

npm i --save vue-router@3

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

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

总结

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

  • 1:路由组件一般放置在pages(views文件夹,非路由组件一般放置components文件夹中
  • 2:路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以

标签的形式使用

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

$route:一般获取路由信息【路径、query、params等等】
$router:一般进行编程式导航进行路由跳转【push|replace】

3,footer组件显示与隐藏

Footer组件显示与隐藏

显示或者隐藏组件: v-if|v-show
Footer组件:在Home, Search显示Footer组件
Footer组件:在登录、注册时候隐藏的

  • 1我们可以根据组件身上的$route获取当前路由的信息,通过路由路径判断Footer显示与隐藏。I
  • 2配置的路由的时候,可以给路由添加路由元信息【meta】,路由需要配置对象,它的key不能乱写

在router配置文件中添加meta

routes:[
    {
        path:'/home',
        component:Home,
        meta:{show:true}
    },

App.vue中添加v-show

<Footer v-show="$route.meta.show"/>

4,路由传参

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

  • 比如: A->B
  • 声明式导航: router-link (务必要有to属性),可以实现路由的跳转
  • 编程式导航:利用的是组件实例的Srouter.push|replace方法,可以实现路由的跳转。(可以书写一些自己业务)

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

  • params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位
  • query参数:不属于路径当中的一部分,类似于ajax中的querystring /home?k=v&kv=,不需要占位
goSearch(){
  //路由传参,方式一,字符串
  this.$router.push('/search/'+this.keyword+'?key='+this.keyword.toUpperCase())
  //方式二: 模板字符串
  this.$router.push(`/search/${this.keyword}?key=${this.keyword.toUpperCase()}`)
  //第三种:对象, 需要在路由中配置name属性
  this.$router.push({name:'search',params:{keyword:this.keyword},query:{key:this.keyword.toUpperCase()}})
}

面试题

1: 使用path能不能使用param传参?

可以拼接

this.$router.push({path:'/search/'+this.keyword,query:{key:this.keyword.toUpperCase()}})

2: 如何指定params参数, 可传可不传怎么配置?

需要配置路由,在占位的后面添加?号

{
    path:'/search/:keyword?',
    component:Search,
    meta:{show:true},
    name:'search',
}
this.$router.push({name:'search',query:{key:this.keyword.toUpperCase()}})

3: params如果传入空串, URL出现异常怎么处理?

使用undefined解决

//使用undefined解决
this.$router.push({name:'search',params:{keyword:''||undefined},query:{key:this.keyword.toUpperCase()}})

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

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

常用函数写法

{
    path:'/search/:keyword?',
    component:Search,
    meta:{show:true},
    name:'search',
    //路由组件能不能传递props数据
    //布尔值写法: 只能传递params参数
    props:true
    //对象写法:额外的给路由组件传递一些props参数
    props:{a:1,b:2}
    //函数写法(常用),可以接收params参数,query参数,通过props传递
    props:($route)=>{
        return {keyword:$route.params.keyword,k:$route.query.k}
    }
    //简写
    props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})
}

错误–NavigationDuplicated

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

image-20220430181624317

pushVueRouter类的一个原型方法,$routerVueRouter类的实例,类的实例可以直接调用类的原型方法

所以对原型方法push进行修改,修改结果就会作用于组件实例的$router实例。

解决

在router配置里重写push 和 replace方法

//先把vueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push;
let orginReplace = VueRouter.prototype.replace;
//重写push | replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
//二:成功的回调,   三: 失败的回调
VueRouter.prototype. push = function(location, resolve, reject){
    if(resolve && reject){
        //call] |apply区别
        //相同点,都可以调用函数一次,都可以算改函数的上下文一次
        //不同点: call与apply传递参数: cal1传递参数用逗号隔开, apply方法执行,传递数组
        originPush.call(this, location,resolve, reject);
    }else {
        originPush.call(this, location, ()=>{}, ()=>{});
    }
}
VueRouter.prototype.replace = function (location,rosole,reject){
    if (rosole && reject){
        orginReplace.call(this,location,resolve,reject);
    }else {
        orginReplace.call(this,location,()=>{},()=>{})
    }
}

5,home模块组件拆分

导航:全部商品分类

在main.js中注册为全局组件, 在Home中直接使用标签

//三级联动组件---全局组件
import TypeNav from "@/components/TypeNav/TypeNav";
//参数1, 全局组件名, 参数2哪个组件
Vue.component('TypeNav',TypeNav)

三级联动组件完成
由于三级联动,在Home, Search、 Detail,把三级联动注册为全局组件。
好处:只需要注册一次,就可以在项目任意地方使用

其他静态资源组件

HTML + CSS + 图片资源
请添加图片描述

效果

image-20220501010910471

6,接口测试

测试软件postman

Download Postman | Get Started for Free

1.1 服务器地址

最新接口地址:http://gmall-h5-api.atguigu.cn

1.2 公共请求参数

每个接口需要的Header参数值(登录接口不需要):

参数名称类型是否必选描述
tokenStringY登录的token
userTempIdString(通过uuidjs生成)Y未登陆用户生成的临时ID

例如:

token: d90aa16f24d04c7d882051412f9ec45b 后台生成

userTempId: b2f79046-7ee6-4dbf-88d0-725b1045460b 前台生成

1.3 首页三级分类

请求地址

/api/product/getBaseCategoryList

测试

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

image-20220501004405463

  • 经过postman工具测试,接口是没有问题的
  • 如果服务器返回的数据code字段200,代表服务器返回数据成功
  • 整个项目,接口前缀都有/api字样

7,Axios二次封装

为什么需要二次封装

  • 请求拦截器, 响应拦截器

安装axiosnpm i axios

src/api/request.js

  • 导入axios
  • 1,axios.create配置
    • 配置基础路径, baseURL
    • 请求超市时间, timeout
  • 2,请求拦截器–request(config)
  • 3,响应拦截器–response(success回调,error回调)
  • 4,对外暴露
//导入axios
import axios from "axios";
const myAxios = axios.create({
    //配置对象
    //基础路径, 发请求的时候,路径中会出现api
    baseURL:'/api',
    timeout:5000,//请求超时时间
});
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
    return config
});

//响应拦截器
myAxios.interceptors.response.use((response)=>{
    //响应成功的回调函数
    return response.data
},(error)=>{
    //响应失败的回调函数
    console.log(error)
    return Promise.reject(new Error('faile'))
})

export default myAxios;

8,接口统一管理

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

/api/product/getBaseCategoryList

项目很小:完全可以在组件的生命周期函数中发请求
项目大: axios.get(‘xxx’)

/api/index.js

//当前这个模块, API进行同一管理
import request from "@/api/request";
///product/getBaseCategoryList
export const reqCategoryList = ()=>{
    //发请求
   return  request({url:'/product/getBaseCategoryList',method:'get'})
}

8.1跨域问题

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

配置代理(webpack)

DevServer | webpack 中文文档 (docschina.org)

vue.config.js中添加配置

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  //关闭eslint
  lintOnSave:false,
    //代理跨域
  devServer: {
      proxy:{
        '/api':{
          target:'http://gmall-h5-api.atguigu.cn'
        }
      }
  },
})

9,nprogress进度条的使用

npm i --save nprogress
  • 在请求拦截器中引入

    • //引入进度条
      import nprogress from 'nprogress';
      //start:进度条开始, done :进度条结束
      //引入进度条样式
      import 'nprogress/nprogress.css'
      
  • 在请求拦截器中执行start() nprogress.start()

  • 响应拦截器中(成功)执行done() nprogress.done();

修改进度条颜色

修改css文件夹

image-20220501172714113

效果

vue-sgg项目-9

10,vuex状态管理库

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

  • 项目大需要才使用

安装

  • Vue2中,要用Vuex的3版本
  • Vue3中,要用Vuex的4版本

这里使用的是vue2, 所以安装vuex3

npm i vuex@3

配置vuex, /src/strore/index.js

import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
//state:存储数据
const state = {};
//mutations: 修改state的唯一手段
const mutations = {};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters,
});

在main.js中引入, 注册仓库

//引入仓库
import store from './store'

new Vue({
  render: h => h(App),
  //注册路由
  router,
  //注册仓库: 组件实例上就会多一个$store属性
  store
}).$mount('#app')

模块化开发

store_home.js

const state = {
};
//mutations: 修改state的唯一手段
const mutations = {

};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

index.js

import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
//引入小仓库
import store_home from "@/store/store_home";
import store_search from "@/store/store_search";
//对外暴露
export default new Vuex.Store({
    //导入模块
    modules:{
        store_search,
        store_home
    }
});

image-20220501180421775

TypeNav三级联动功能

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

store_home.js

import {reqCategoryList} from "@/api";
//home模块小仓库
const state = {
    //state数据默认初始值要和服务器返回的类型一致
    categoryList:[]
};
//mutations: 修改state的唯一手段
const mutations = {
    CATEGORYLIST(state,value){
        state.categoryList = value
    }

};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    async categoryList({commit}){
        let  result = await reqCategoryList();
        if (result.code==200){
            commit('CATEGORYLIST',result.data)
        }
    }
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

TypeNav.vue

  computed:{
      ...mapState({
        //右侧需要是一个函数, 当使用这个计算属性时, 右侧函数就会立即执行一次
        //注入一个参数state, 这个state是大仓库的数据, 要指向对应小仓库
/*        categoryList:(state)=>{
          return state.store_home.categoryList;
        }*/
        //简写
        categoryList:state=>state.store_home.categoryList

      })
  }

image-20220501182152782

拿到数据后展示页面v-for

<template>
  <!-- 商品分类导航 -->
  <div class="type-nav">
<!--    <h1>{{categoryList}}</h1>-->
    <div class="container">
      <h2 class="all">全部商品分类</h2>
      <nav class="nav">
        <a href="###">服装城</a>
        <a href="###">美妆馆</a>
        <a href="###">尚品汇超市</a>
        <a href="###">全球购</a>
        <a href="###">闪购</a>
        <a href="###">团购</a>
        <a href="###">有趣</a>
        <a href="###">秒杀</a>
      </nav>
      <div class="sort">
        <div class="all-sort-list2">
          <div class="item" v-for="c1 in categoryList" :key="c1.categoryId">
            <h3>
              <a href="">{{c1.categoryName}}</a>
            </h3>
            <div class="item-list clearfix">
              <div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId">
                <dl class="fore">
                  <dt>
                    <a href="">{{c2.categoryName}}</a>
                  </dt>
                  <dd>
                    <em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
                      <a href="">{{ c3.categoryName }}</a>
                    </em>
                  </dd>
                </dl>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

</template>

<script>
import {mapState} from 'vuex'
export default {
  name: "TypeNav",
  //组件挂载完毕, 向服务器发请求
  mounted() {
    //通知vuex发请求, 获取数据, 存储于仓库中
    this.$store.dispatch('categoryList')
  },
  computed:{
      ...mapState({
        //右侧需要是一个函数, 当使用这个计算属性时, 右侧函数就会立即执行一次
        //注入一个参数state, 这个state是大仓库的数据, 要指向对应小仓库
/*        categoryList:(state)=>{
          return state.store_home.categoryList;
        }*/
        //简写
        categoryList:state=>state.store_home.categoryList

      })
  }
}
</script>

<style scoped lang="less">
.all-sort-list2 {         height: 450px;         overflow: hidden;}
.type-nav {
  border-bottom: 2px solid #e1251b;

  .container {
    width: 1200px;
    margin: 0 auto;
    display: flex;
    position: relative;
  
    .all {
      width: 210px;
      height: 45px;
      background-color: #e1251b;
      line-height: 45px;
      text-align: center;
      color: #fff;
      font-size: 14px;
      font-weight: bold;
    }
    
    .nav {
      a {
        height: 45px;
        margin: 0 22px;
        line-height: 45px;
        font-size: 16px;
        color: #333;
      }
    }
    
    .sort {
      position: absolute;
      left: 0;
      top: 45px;
      width: 210px;
      height: 461px;
      position: absolute;
      background: #fafafa;
      z-index: 999;
    
    .all-sort-list2 {
      .item {
        h3 {
          line-height: 30px;
          font-size: 14px;
          font-weight: 400;
          overflow: hidden;
          padding: 0 20px;
          margin: 0;
        
          a {
            color: #333;
          }
        }
        
        .item-list {
          display: none;
          position: absolute;
          width: 734px;
          min-height: 460px;
          background: #f7f7f7;
          left: 210px;
          border: 1px solid #ddd;
          top: 0;
          z-index: 9999 !important;
        
        .subitem {
          float: left;
          width: 650px;
          padding: 0 4px 0 8px;
        
        dl {
          border-top: 1px solid #eee;
          padding: 6px 0;
          overflow: hidden;
          zoom: 1;
        
        &.fore {
           border-top: 0;
         }
        
        dt {
          float: left;
          width: 54px;
          line-height: 22px;
          text-align: right;
          padding: 3px 6px 0 0;
          font-weight: 700;
        }
        
        dd {
          float: left;
          width: 415px;
          padding: 3px 0 0;
          overflow: hidden;
        
        em {
          float: left;
          height: 14px;
          line-height: 14px;
          padding: 0 8px;
          margin-top: 5px;
          border-left: 1px solid #ccc;
        }
        }
        }
        }
        }
        
        &:hover {
          .item-list {
            display: block;
          }
        }
      }
    }
    }
  }
}

</style>

效果

vue-sgg项目-11

鼠标移动到一级分类时添加背景颜色

<template>
  <!-- 商品分类导航 -->
  <div class="type-nav">
<!--    <h1>{{categoryList}}</h1>-->
    <div class="container">
      <!--事件委派给父亲完成      -->
      <div  @mouseleave="leaveIndex"><!--离开清除样式 -->
        <h2 class="all">全部商品分类</h2>
        <div class="sort">
          <div class="all-sort-list2">
            <div class="item" v-for="(c1,index) in categoryList"
                 :key="c1.categoryId"
                 :class="{cur:currentIndex===index}"<!--鼠标移动到对应的目录添加样式 -->
            >
              <h3 @mouseenter="changeIndex(index)">
                <a href="">{{c1.categoryName}}</a>
              </h3>
              <div class="item-list clearfix">
                <div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId">
                  <dl class="fore">
                    <dt>
                      <a href="">{{c2.categoryName}}</a>
                    </dt>
                    <dd>
                      <em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
                        <a href="">{{ c3.categoryName }}</a>
                      </em>
                    </dd>
                  </dl>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <nav class="nav">
        <a href="###">服装城</a>
        <a href="###">美妆馆</a>
        <a href="###">尚品汇超市</a>
        <a href="###">全球购</a>
        <a href="###">闪购</a>
        <a href="###">团购</a>
        <a href="###">有趣</a>
        <a href="###">秒杀</a>
      </nav>

    </div>
  </div>

</template>

<script>
import {mapState} from 'vuex'
export default {
  name: "TypeNav",
  data(){
    return {
      currentIndex : -1,
    }
  },
  methods:{
    changeIndex(index){
      this.currentIndex = index
    },
    leaveIndex(){
      this.currentIndex = -1
    }
  }
}
</script>

<style scoped lang="less">
/*填加cur背景样式*/
.all-sort-list2 {
	height: 450px;
    overflow: hidden;
	.cur{
        background: skyblue;
      }
}
</style>
效果

请添加图片描述

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

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

去掉原来的css
&:hover {
  .item-list {
    display: block;
  }
}
修改为动态style
<!-- 二三级分类             -->
<div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">

12,演示卡顿现象(正常-节流-防抖)

vue-sgg项目-12

  • 快速滑动时,会触发不完全,浏览器反应不过来

  • 如果业务过多, 可能会出现卡顿

  • 正常:

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

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

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

节流(规定时间内只触发第一次)

防抖(规定时间内只触发最后一次)

1,lodash插件安装

Lodash 简介 | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)

安装

浏览器环境:

<script src="lodash.js"></script>

通过 npm:

$ npm i -g npm
$ npm i --save lodash

2,防抖 _.debounce

_.debounce(func, [wait=0], [options=])

创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。 debounced(防抖动)函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。 可以提供一个 options(选项) 对象决定如何调用 func 方法,options.leading 与|或 options.trailing 决定延迟前后如何触发(注:是 先调用后等待 还是 先等待后调用)。 func 调用时会传入最后一次提供给 debounced(防抖动)函数 的参数。 后续调用的 debounced(防抖动)函数返回是最后一次 func 调用的结果。

注意: 如果 leadingtrailing 选项为 true, 则 func 允许 trailing 方式调用的条件为: 在 wait 期间多次调用防抖方法。

如果 wait0 并且 leadingfalse, func调用将被推迟到下一个点,类似setTimeout0的超时。

SeeDavid Corbacho’s articlefor details over the differences between_.debounce and_.throttle.

参数

  1. func (Function): 要防抖动的函数。
  2. [wait=0] (number): 需要延迟的毫秒数。
  3. [options=] (Object): 选项对象。
  4. [options.leading=false] (boolean): 指定在延迟开始前调用。
  5. [options.maxWait] (number): 设置 func 允许被延迟的最大值。
  6. [options.trailing=true] (boolean): 指定在延迟结束后调用。

返回

(Function): 返回新的 debounced(防抖动)函数。

例子
//防抖前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发只会执行一次
let input = document.querySelector ('input');
//文木发生变化立即执行
input.oninput =_.debounce(function(){
	console.log('ajax请求')	
}, 1000);
//lodash插件:里面封装函数的防抖与节流的业务【闭包+延迟器】
//1:lodash函数库对外暴露-函数

3,节流 _.throttle

_.throttle(func, [wait=0], [options=])

创建一个节流函数,在 wait 秒内最多执行 func 一次的函数。 该函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。 可以提供一个 options 对象决定如何调用 func 方法, options.leading 与|或 options.trailing 决定 wait 前后如何触发。 func 会传入最后一次传入的参数给这个函数。 随后调用的函数返回是最后一次 func 调用的结果。

注意: 如果 leadingtrailing 都设定为 truefunc 允许 trailing 方式调用的条件为: 在 wait 期间多次调用。

如果 wait0 并且 leadingfalse, func调用将被推迟到下一个点,类似setTimeout0的超时。

查看David Corbacho’s article 了解_.throttle_.debounce 的区别。

参数

  1. func (Function): 要节流的函数。
  2. [wait=0] (number): 需要节流的毫秒。
  3. [options=] (Object): 选项对象。
  4. [options.leading=true] (boolean): 指定调用在节流开始前。
  5. [options.trailing=true] (boolean): 指定调用在节流结束后。
例子

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

//获取节点
let span = document.querySelector('span');
let button = document.querySelector('button');
let count = 0;
//计数器:在一秒以内,数字只能加上1
button.onclick =_.throttle(function ()f
    count++;
    span.innerHTML = count;
    console.log('执行');
], 1000);

4,完成三级联动菜单的节流操作

检查node_modules有无lodash

image-20220502005615807

TypeNav.vue

//全部引入lodash
// import _ from 'lodash'
//按需引入lodash
import throttle from 'lodash/throttle'
//========================================================================
  methods:{
    // changeIndex(index){
    //   console.log(index)
    //   this.currentIndex = index
    // },
    //节流throttle回调函数别用箭头函数,可能出现上下文this
    changeIndex:throttle(function (index) {
        console.log(index)
        this.currentIndex = index
    },50),

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

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

路由跳转:

  • 声明式导航:router-link
    • 如果使用声明式导航router-link,可以实现路由的跳转与传递参数。
    • 但是需要注意,出现卡顿现象。
    • router-link:可以一个组件,当服务器的数据返回之后,循环出很多的router-link组件【创建组件实例的】1000+
    • 创建组件实例的时候,一瞬间创建1000+很好内存的,因此出现了卡顿现象。
  • 编程式导航: push| replace
    • 利用时间委派(给父节点)+编程式导航实现路由跳转与传递参数
    • 存在的问题
      • 怎么判断点击的是a标签?
        • event.target.nodeName=='a'
      • 传递参数问题
            <!--goSearch事件委派给父亲完成      -->
            <div class="all-sort-list2" @click="goSearch($event)">
              <div class="item" v-for="(c1,index) in categoryList"
                   :key="c1.categoryId"
                   :class="{cur:currentIndex===index}"
              >
                <h3 @mouseenter="changeIndex(index)">
                  <a :data-categoryname="c1.categoryName"
                     :data-category1Id="c1.categoryId">
                    {{c1.categoryName}}
                  </a>
                  <!--                <router-link to="/search">{{c1.categoryName}}</router-link>-->
                </h3>
                <!-- 二三级分类             -->
                <div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">
                  <div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId">
                    <dl class="fore">
                      <dt>
                        <a :data-categoryname="c2.categoryName"
                           :data-category2Id="c2.categoryId" >
                          {{c2.categoryName}}
                        </a>
                        <!--                      <router-link to="/search">{{c2.categoryName}}</router-link>-->

                      </dt>
                      <dd>
                        <em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
                          <a :data-categoryname="c3.categoryName"
                             :data-category3Id="c3.categoryId">
                            {{ c3.categoryName }}
                          </a>
                          <!--                        <router-link to="/search">{{c3.categoryName}}</router-link>-->
                        </em>
                      </dd>
                    </dl>
                  </div>
                </div>
              </div>
            </div>

 methods:{
    // changeIndex(index){
    //   console.log(index)
    //   this.currentIndex = index
    // },
    //节流throttle回调函数别用箭头函数,可能出现上下文this
    changeIndex:throttle(function (index) {
        this.currentIndex = index
    },50),
    leaveIndex(){
      this.currentIndex = -1;
      if (this.$route.path!='/home'){
        this.showNav = false
      }
    },
    enterIndex(){
      if (this.$route.path!='/home'){
        this.showNav = true
      }
    },
    goSearch(event){
      //第一个问题:把子节点当中a标签,我加上自定义属性data-categoryName,其余的子节点是没有的
      let elment = event.target;
      //获取到当前出发这个事件的节点【h3、a、dt、dl】,需要带有data-categoryname这样节点【一定是a标签】
      //节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
      let {categoryname,category1id,category2id,category3id} = elment.dataset;
      //如果标签身上拥有categoryname一定是a标签
      if (categoryname){
        //整理路由跳转的参数
        let location = {name:'search'};
        let query = {categoryname:categoryname}
        //一级分类、二级分类、三级分类的a标签
        if (category1id){
          // console.log('@@',category1id);
          query.category1Id = category1id;
        }else if (category2id){
          // console.log('@@',category2id)
          query.category2Id = category2id;
        }else if (category3id) {
          // console.log(category3id)
          query.category3Id = category3id;
        }
        //整理完参数
        if (this.$route.params){//如果有params参数合并到参数中
          location.params = this.$route.params
        }
        location.query = query
        this.$router.push(location)
      }
    }
  }

效果

vue-sgg项目-13

14,三级列表动态形式总结

  • 获取服务器数据: 解决跨域问题
    • jsonp
    • pro
    • 代理服务器
  • 函数防抖和节流
  • 路由跳转
    • 声明式导航(router-link): 会创建组件实例, 过多消耗内存,导致卡顿
    • 编程式导航:
      • 事件委派
      • 使用自定义属性—区分标签, 区分层级目录

15,Search模块中的TypeNav商品分类菜单显示, 动画效果

显示隐藏

给标签添加鼠标事件,

v-show控制菜单的显示, 默认showNav=true

<div  @mouseleave="leaveIndex" @mouseenter="enterIndex" >
  <h2 class="all">全部商品分类</h2>
  <!--三级联动        -->
  <div class="sort" v-show="showNav">

path不是home才会有变化

leaveIndex(){
  this.currentIndex = -1;
  if (this.$route.path!='/home'){
    this.showNav = false
  }
},
enterIndex(){
  if (this.$route.path!='/home'){
    this.showNav = true
  }
},

过度动画

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

给三级联动加一层transition

<!--过度动画        -->
<transition name="sort">
    <!--三级联动        -->
    <div class="sort" v-show="showNav"></div>
</transition>

style中添加样式

//过度动画的样式
//开始状态
.sort-enter{
  height: 0;
  //transform: rotate(0deg);/*旋转*/
}
//结束状态
.sort-enter-to{
  height: 461px;
  //transform: rotate(360deg);/*旋转*/

}
//定义动画时间, 速率
.sort-enter-active{
  transition: all .5s linear;
  overflow: hidden;
}

效果

vue-sgg项目-15

16,三级列表优化

原来是卸载TypeNav.vue的挂载钩子上获取数据

//组件挂载完毕, 向服务器发请求
mounted() {
  //通知vuex发请求, 获取数据, 存储于仓库中
  this.$store.dispatch('categoryList');
}

这种方式会导致每切换一次就会向服务器发一次请求

解决

放到App.vue的mounted钩子里获取

根组件的mounted只会执行一次

export default {
  name: 'App',
  components: {
    Header,Footer
  },
  mounted() {
    //派发一个action, 获取分类三级列表数据
    this.$store.dispatch('categoryList');
  }
}

17,合并参数(query,params)

将header搜索参数(params)和TypeNav菜单参数(query)合并

image-20220502164209683

Header.vue

如果有query参数就合并

goSearch(){
  let location = {name:'search',params:{keyword:this.keyword || undefined}};
  if (this.$route.query){ //如果有query参数就合并query参数
    location.query = this.$route.query
  }
  this.$router.push(location)
}

TypeNav.vue

如果有params参数合并

goSearch(event){
    //整理完参数
    if (this.$route.params){//如果有params参数合并到参数中
      location.params = this.$route.params
    }
    location.query = query
    this.$router.push(location)
  }
}

效果

既有query, 又有params

一,完善Home1(ListContainer)

1,mock数据(模拟)

生成随机数据,拦截 Ajax 请求

Home · nuysoft/Mock Wiki (github.com)

# 安装
npm install mockjs
#第一步: src中创建mock文件夹
#第二步: 创建json假数据,去掉空格
#3: 把mock数据需要的图片放到public目录下
#4: 创建mockServe.js通过mock.js创建虚拟数据
#5: mockServe.js在入口文件中引入(至少要执行一次,才能模拟数据)

json数据

首页广告轮播数据: src/mock/banners.json
[
    {
        "id":"1",
        "imgUrl":"/images/banner1.jpg"
    },
    {
        "id":"2",
        "imgUrl":"/images/banner2.jpg"
    },
    {
        "id":"3",
        "imgUrl":"/images/banner3.jpg"
    },
    {
        "id":"4",
        "imgUrl":"/images/banner4.jpg"
    }
]
首页楼层数据: src/mock/floors.json
[{
  "id":"001",
  "name":"家用电器",
  "keywords":["节能补贴","4K电视","空气净化器","IH电饭煲","滚筒洗衣机","电热水器"],
  "imgUrl":"/images/floor-1-1.png",
  "navList":[{
    "url":"#",
    "text":"热门"
  },
    {
      "url":"#",
      "text":"大家电"
    },
    {
      "url":"#",
      "text":"生活电器"
    },
    {
      "url":"#",
      "text":"厨房电器"
    },
    {
      "url":"#",
      "text":"应季电器"
    },
    {
      "url":"#",
      "text":"空气/净水"
    },
    {
      "url":"#",
      "text":"高端电器"
    }
  ],
  "carouselList":[{
    "id":"0011",
    "imgUrl":"/images/floor-1-b01.png"
  },
    {
      "id":"0012",
      "imgUrl":"/images/floor-1-b02.png"
    },
    {
      "id":"0013",
      "imgUrl":"/images/floor-1-b03.png"
    }
  ],
  "recommendList":[
    "/images/floor-1-2.png",
    "/images/floor-1-3.png",
    "/images/floor-1-5.png",
    "/images/floor-1-6.png"
  ],
  "bigImg":"/images/floor-1-4.png"
},
  {
    "id":"002",
    "name":"手机通讯",
    "keywords":["节能补贴2","4K电视2","空气净化器2","IH电饭煲2","滚筒洗衣机2","电热水器2"],
    "imgUrl":"/images/floor-1-1.png",
    "navList":[{
      "url":"#",
      "text":"热门2"
    },
      {
        "url":"#",
        "text":"大家电2"
      },
      {
        "url":"#",
        "text":"生活电器2"
      },
      {
        "url":"#",
        "text":"厨房电器2"
      },
      {
        "url":"#",
        "text":"应季电器2"
      },
      {
        "url":"#",
        "text":"空气/净水2"
      },
      {
        "url":"#",
        "text":"高端电器2"
      }
    ],
    "carouselList":[{
      "id":"0011",
      "imgUrl":"/images/floor-1-b01.png"
    },
      {
        "id":"0012",
        "imgUrl":"/images/floor-1-b02.png"
      },
      {
        "id":"0013",
        "imgUrl":"/images/floor-1-b03.png"
      }
    ],
    "recommendList":[
      "/images/floor-1-2.png",
      "/images/floor-1-3.png",
      "/images/floor-1-5.png",
      "/images/floor-1-6.png"
    ],
    "bigImg":"/images/floor-1-4.png"
  }
]

mockServe.js

//引入mockjs模块
import Mock from  'mockjs';
//引入json数据(json数据没有对外暴露也能引入)
//webpack默认暴露的: 图片, json数据
import banner from './banner.json';
import floor from './floor.json'
//mock数据: 参数1:请求地址,  参数2:请求数据
Mock.mock("/mock/banner",{code:200,data:banner});//模拟首页轮播图数据
Mock.mock("/mock/floor",{code:200,data:floor});//底部的家用电器数据

入口文件引入main.js

//引入MockServe.js
import '@/mock/mockServe'

2,获取轮播图的数据

1,创建src/api/request_mock.js

把原来的复制一份, 修改baseURL

//导入axios
import axios from "axios";
//引入进度条
import nprogress from 'nprogress';
//start:进度条开始, done :进度条结束
//引入进度条样式
import 'nprogress/nprogress.css'
const myAxios = axios.create({
    //配置对象
    //基础路径, 发请求的时候,路径中会出现api
    baseURL:'/mock',
    timeout:5000,//请求超时时间
});
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
    //进度条开始动
    nprogress.start();
    return config
});

//响应拦截器
myAxios.interceptors.response.use((response)=>{
    //进度条结算
    nprogress.done();
    //响应成功的回调函数
    return response.data
},(error)=>{
    //响应失败的回调函数
    console.log(error)
    return Promise.reject(new Error('faile'))
})

export default myAxios;

2,/src/api/index.js添加request_mock

//当前这个模块, API进行同一管理
import request from "@/api/request";
import request_mock from "@/api/request_mock";
///product/getBaseCategoryList
export const reqCategoryList = ()=>{
    //发请求
   return  request({url:'/product/getBaseCategoryList',method:'get'})
}
/*export const reqMockBannerList = ()=>{
   return request_mock({url:'/banner',method:'get'})
}*/
//简写
export const reqMockBannerList = ()=>request_mock.get('/banner')

3,vuex仓库store添加获取bannerlist的方法数据

store_home.js
import {reqCategoryList, reqMockBannerList} from "@/api";//1,把reqMockBannerList添加进来
//home模块小仓库
const state = {
    //state数据默认初始值要和服务器返回的类型一致
    categoryList:[],
    //2,添加bannerList空数组(类型根据json)
    bannerList:[]
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    async categoryList({commit}){
        let  result = await reqCategoryList();
        if (result.code==200){
            commit('CATEGORYLIST',result.data)
        }
    },
    async getBannerList({commit}){//3,获取首页轮播图的数据
        let  result = await reqMockBannerList();
        console.log(result)
        if (result.code==200){
            commit('GETBANNERLIST',result.data)
        }
    }
};
//mutations: 修改state的唯一手段
const mutations = {
    CATEGORYLIST(state,value){
        state.categoryList = value
    },
    GETBANNERLIST(state,value){//4,覆盖仓库的空数组bannerList
        state.bannerList = value
    }

};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

4,ListContainer.vue发请求获取bannerList

<script>
import {mapState} from "vuex";
export default {
  name: "ListContainer",
  mounted() {
    //通知vuex发请求, 获取数据, 存储于仓库中
    this.$store.dispatch('getBannerList');
    console.log(this.$store.state.store_home)
  },
  computed:{
    ...mapState({
      bannerList:(state)=>{
        return state.store_home.bannerList
      }
    })
  }
}
</script>

5,检查是否获取到

image-20220502181632161

3,将轮播图数据放到页面中

1,swiper

Swiper中文网-轮播图幻灯片js插件,H5页面前端开发

第一步:引包(相应Jscss)
第二步:页面中结构务必要有
第三步(页面当中务必要有结构): new Swiper实例【轮播图添加动态效果】

2,ListContainer组件使用swiper

安装swiper

npm i swiper@5
第一步:引包(相应Jscss)

ListContainer.vue中引入swiper

import Swiper from  'swiper'

引入样式

//在main.js中引入swiper样式 (由于很多地方需要使用, 直接到入口文件引入)
import 'swiper/css/swiper.css'
二, 页面遍历
<div class="swiper-container" :ref="mySwiper">
    <div class="swiper-wrapper" >
        <div class="swiper-slide" v-for="carousel in bannerList" :key="carousel.id">
            <img :src="carousel.imgUrl" />
    </div>
</div>
三,ListContainer.vue中创建swiper

image-20220502190206824

  • 直接new的话bannerList的数据还没到页面中
  • 可使用定时器,如果请求超过定时时间也会没有效果
  • 最好解决方案是watch+nextTick(数据监听: 监听已有数据变化)
    • $nextTick:在下次DOM更新循环结束之后执行延迟回调。在 修改数据之后 立即使用这个方法,获取更新后的DOM。
    • $nextTick:可以保证也页面中的解构一定是有的,经常和很多插件一起使用【都需要DOM存在了】
  watch:{
    //监听bannerList数据的变化, 由空数组变为请求后的4个元素
    bannerList:{
      handler(){
        //现在咱们通过watch监听bannerList属性的属性值的变化
        //如果执行handler方法,代表组件实例身上这个属性的属性以己经有了【数组:四个元素】
        //当前这个函数执行:只能保证pannerList数据已经有了,但是你没办法保证v-for已经执行结束了
        //v-for执行完毕,才有结构【你现在在watch当中没办法保证的】
        //netxTick:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DoM。
        this.$nextTick(()=>{
          var mySwiper = new Swiper(this.$refs.mySwiper, {
            loop: true,
            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              clickable:true //小球点击跳转
            },
            //如果需要前进后退按钮
            navigation: {
              nextEl: ".swiper-button-next",
              prevEl: ".swiper-button-prev",
            },
          });
          console.log(mySwiper)
        });
      }
    }
  }

一, 完善Home2(Floor)

1,完成floorList的获取

1.1,api设置

src/api/index.js

//获取floor数据
export const reqMockFloorList = ()=>request_mock.get('/floor')

1.2,vuex三步曲(action-mutations-state)

state

floorList:[]

actions

async getFloorList({commit}){//获取floor数据
    let result = await reqMockFloorList();
    console.log('getFloorList',result)
    if (result.code == 200){
        commit('REQMOCKFLOORLIST',result.data);
    }
}

mutations

REQMOCKFLOORLIST(state,value){
    state.floorList = value
}

2,在home中获取仓库数据,传给floor

组件通信的方式有哪些?

  • props:用于父子组件通信
  • 自定义事件:@on @emit可以实现子给父通信
  • 全局事件总线:$bus 全能
  • pubsub-js:vue当中几乎不用全能
  • 插槽
  • vuex
<!--
	采用props方式传给floor
    v-for也可以在自定义标签中使用
 -->
<Floor v-for="floor in floorList" :key="floor.id" :list="floor"></Floor>
import {mapState} from "vuex";
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Home",
  components:{
    ListContainer,Recommend,Rank,Like,Floor,Brand
  },
  mounted() {
    //vuex-action方法获取floorList
    this.$store.dispatch('getFloorList')
  },
  computed:{
    //从仓库获取floorLIst
    ...mapState({
      floorList:(state)=>{
        return state.store_home.floorList
      }
    })
  }

3,floor中展示数据

<template>
  <!--楼层-->
  <div class="floor">
    <div class="py-container" >
      <div class="title clearfix">
        <h3 class="fl">{{list.name}}</h3>
        <div class="fr">
          <ul class="nav-tabs clearfix" v-for="(nav,index) in list.navList" :key="index">
            <li class="active">
              <a href="#tab1" data-toggle="tab">{{nav.text}}</a>
            </li>
          </ul>
        </div>
      </div>
      <div class="tab-content">
        <div class="tab-pane">
          <div class="floor-1">
            <div class="blockgary">
              <ul class="jd-list">
                <li   v-for="(kw,index) in list.keywords" :key="index">{{kw}}</li>
              </ul>
              <div>
                <img :src="list.imgUrl" />
              </div>
            </div>
            <div class="floorBanner">
              <!-- 轮播图             -->
              <div class="swiper-container"  ref="floorSwiper">
                <div class="swiper-wrapper">
                  <div class="swiper-slide" v-for="cr in list.carouselList" :key="cr.id">
                    <img :src="cr.imgUrl">
                  </div>s
                </div>
                <!-- 如果需要分页器 -->
                <div class="swiper-pagination"></div>

                <!-- 如果需要导航按钮 -->
                <div class="swiper-button-prev"></div>
                <div class="swiper-button-next"></div>
              </div>
            </div>
            <div class="split">
              <span class="floor-x-line"></span>
              <div class="floor-conver-pit">
                <img :src="list.recommendList[0]" />
              </div>
              <div class="floor-conver-pit">
                <img :src="list.recommendList[1]" />
              </div>
            </div>
            <div class="split center">
              <img :src="list.bigImg" />
            </div>
            <div class="split">
              <span class="floor-x-line"></span>
              <div class="floor-conver-pit">
                <img :src="list.recommendList[2]" />
              </div>
              <div class="floor-conver-pit">
                <img :src="list.recommendList[3]" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

</template>

<script>

import Swiper from "swiper";
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Floor",
  props:['list'],
  //第一次写swiper的时候在mounted中不可以, 为什么这里可以了?
  //这次写轮播图时, 数据没有在floor中发, 直接从父组件props获取的
  mounted() {
    this.$nextTick(()=>{
      var mySwiper = new Swiper(this.$refs.floorSwiper, {
        loop: true,
        // 如果需要分页器
        pagination: {
          el: ".swiper-pagination",
          clickable:true //小球点击跳转
        },
        //如果需要前进后退按钮
        navigation: {
          nextEl: ".swiper-button-next",
          prevEl: ".swiper-button-prev",
        },
      });
      console.log(mySwiper)
    });
  }
}
</script>

4,将轮播图封装成全局组件(复用)

4.1,先把ListContainer和floor写法改一致

原来:

  • ListContainer: 在watch监听中实现
  • floor: 在computed中

把floor也改成监听

watch: {
  list:{
    immediate:true,//立即监听:不管数据有没有改变, 上来就监听一次
    handler(){//直接监听不到, 因为数据从home传过来就没有改变
      this.$nextTick(()=>{
        var mySwiper = new Swiper(this.$refs.floorSwiper, {
          loop: true,
          // 如果需要分页器
          pagination: {
            el: ".swiper-pagination",
            clickable:true //小球点击跳转
          },
          //如果需要前进后退按钮
          navigation: {
            nextEl: ".swiper-button-next",
            prevEl: ".swiper-button-prev",
          },
        });
        console.log(mySwiper)
      });
    }
  }

4.2,components下新建全局组件Carousel

  • 注意:传入的数据叫carouselList
<template>
  <!-- 轮播图             -->
  <div class="swiper-container"  ref="floorSwiper">
    <div class="swiper-wrapper">
      <div class="swiper-slide" v-for="cr in carouselList" :key="cr.id">
        <img :src="cr.imgUrl">
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>

    <!-- 如果需要导航按钮 -->
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
  </div>
</template>

<script>
import Swiper from "swiper";
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Carousel",
  props:['carouselList'],
  watch: {
    carouselList:{
      immediate:true,//立即监听:不管数据有没有改变, 上来就监听一次
      handler(){//直接监听不到, 因为数据从home传过来就没有改变
        this.$nextTick(()=>{
          var mySwiper = new Swiper(this.$refs.floorSwiper, {
            loop: true,
            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              clickable:true //小球点击跳转
            },
            //如果需要前进后退按钮
            navigation: {
              nextEl: ".swiper-button-next",
              prevEl: ".swiper-button-prev",
            },
          });
          console.log(mySwiper)
        });
      }
    }
  }
}
</script>

<style scoped>

</style>

4.3,main.js中注册为全局组件

//注册轮播图全局组件
import Carousel from "@/components/Carousel/Carousel";
// eslint-disable-next-line vue/multi-word-component-names
Vue.component('Carousel',Carousel)

4.4,修改Floor和ListContainer

去掉原来的写法,新增Carousel标签即可

<!-- 将原来的轮播图改为全局组件Carousel-->
<Carousel :carouselList="list.carouselList"></Carousel>

请添加图片描述

二,搜索模块开发(search)

  1. 静态页面+静态组件拆分
  2. 发请求API
  3. vuex(action-mutations-state)–请求返回存放到仓库中
  4. 组件获取仓库数据, 动态展示数据

2.1,获取search模块数据

1,请求API

src/api/index.js

//获取search模块数据, 地址:/api/list  请求方式post, 需要带参数
//当前这个函数需不需要按受外部传递参数
//当前这个接口(获取搜索模块的数据),给服务器传递一个默认参数【至少是一个空对象】
//如reqGetSearchInfo({})
export const reqGetSearchInfo = (params)=>request({url:'/list',method:'post',data:params})

2,vuex

src/store/store_search.js

import {reqGetSearchInfo} from "@/api";

const state = {
    searchInfo: {}
};

//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    async getSearchList({commit},params={}){
        //调用api中reqGetSearchInfo函数时至少传递一个参数
        //params形参, 是用户派发action时, 第二个参数传过来的
        let result =  await reqGetSearchInfo(params);
        console.log(result)
        if (result.code==200){
            commit('GETSEARCHLIST',result.data)
        }
    }
};
//mutations: 修改state的唯一手段
const mutations = {
    GETSEARCHLIST(state,value){
        state.searchInfo = value
    }
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
    //当前形参state相当于当前仓库的state
    goodsList(state) {
        //如果没有网络goodsList返回是undefined,所以以防万一返回空数组[]
        return state.searchList.goodsList || []
    },
    trademarkList(state) {
        return state.searchList.trademarkList || []
    },
    attrsList(state) {
        return state.searchList.attrsList || []
    },
};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

3,search中获取展示页面

<script>
  import SearchSelector from './SearchSelector/SearchSelector'
  import {mapGetters} from "vuex";
  export default {
    // eslint-disable-next-line vue/multi-word-component-names
    name: 'Search',

    components: {
      SearchSelector
    },
    computed:{
      //mapGetters写法: 传递数组, 因为getters计算没有划分模块
      ...mapGetters(['goodsList'])
    },
    mounted() {
      //测试接口数据
      this.$store.dispatch("getSearchList",{})
    }
  }
</script>

image-20220502235946384

image-20220503003315370

4,页面展示

<div class="goods-list">
  <ul class="yui3-g">
    <li class="yui3-u-1-5"  v-for="goods in goodsList" :key="goods.id">
      <div class="list-wrap">
        <div class="p-img">
          <a href="item.html" target="_blank"><img :src="goods.defaultImg" /></a>
        </div>
        <div class="price">
          <strong>
            <em>¥</em>
            <i>{{goods.price}}</i>
          </strong>
        </div>
        <div class="attr">
          <a target="_blank" href="item.html" title="促销信息,下单即赠送三个月CIBN视频会员卡!【小米电视新品4A 58 火爆预约中】">
            {{goods.title}}
          </a>
        </div>
        <div class="commit">
          <i class="command">已有<span>2000</span>人评价</i>
        </div>
        <div class="operate">
          <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
          <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
        </div>
      </div>
    </li>
  </ul>
</div>

这数据…被改坏了

请添加图片描述

2.2,通过参数发送请求

通过mounted只能发送一次

<script>
  import SearchSelector from './SearchSelector/SearchSelector'
  import {mapGetters} from "vuex";
  export default {
    // eslint-disable-next-line vue/multi-word-component-names
    name: 'Search',
    data(){
      return {
        searchParams:{
          //一级分类的id
          category1Id: "",
          //二级分类id
          category2Id:"",
          //三级分类的id
          category3Id:"",
          //分类名字
          categoryName:"",
          //关键字
          keyword: "",
          //排序
          order: "",
          //分页器用的:代表的是当前是第几页
          pageNo: 1,
          //代表的是每一个展示数据个数
          pageSize: 20,
          //平台售卖属性换作带的参数
          props: [],
          //品牌
          trademark: "",
        }
      }
    },
    components: {
      SearchSelector
    },
    computed:{
      //mapGetters写法: 传递数组, 因为getters计算没有划分模块
      ...mapGetters(['goodsList'])
    },
    beforeMount() {//组件挂载完毕之前执行, 先与mounted
      //挂载完毕之前把query, params参数合并到searchParams中
      //复杂写法
      // this.searchParams. categorylId = this.Sroute.query.category1Id;
      // this.searchParams.category2Id = this.$route.query.category2Id;
      // this.searchParams.category3Id = this.Sroute.query.category3Id;
      // this.searchParams.categoryName = this.Sroute.query.categoryName;
      // this.searchParams. keyword = this. Sroute.params. keyword;
      //Object.assign: ES6新增的语法,合并对象
      Object.assign(this.searchParams, this.$route.query, this.$route.params);
    },
    mounted() { //仅执行一次
      //放到函数中
      // this.$store.dispatch("getSearchList",this.searchParams)
      this.getSearchData()
    },
    methods:{
      getSearchData(){
        //获取search数据
        this.$store.dispatch("getSearchList",this.searchParams)
      }
    }
  }
</script>

2.3,SearchSelector动态展示数据

search组件的子组件SearchSelector, 分类模块

image-20220504162834126

<template>
  <div class="clearfix selector">
    <div class="type-wrap logo">
      <div class="fl key brand">品牌</div>
      <div class="value logos">
        <ul class="logo-list">
          <li v-for="td in trademarkList" :key="td.tmId">{{td.tmName}}</li>
        </ul>
      </div>
      <div class="ext">
        <a href="javascript:void(0);" class="sui-btn">多选</a>
        <a href="javascript:void(0);">更多</a>
      </div>
    </div>
    <div class="type-wrap" v-for="attr in attrsList" :key="attr.attrId">
      <div class="fl key">{{attr.attrName}}</div>
      <div class="fl value">
        <ul class="type-list">
          <li v-for="(attrValue,index) in attr.attrValueList" :key="index">
            <a>{{attrValue}}</a>
          </li>
        </ul>
      </div>
      <div class="fl ext"></div>
    </div>
  </div>
</template>

<script>
import {mapGetters} from "vuex";

export default {
    name: 'SearchSelector',
    computed:{
      ...mapGetters(['attrsList','trademarkList'])
    }
  }
</script>

image-20220504162554606

2.4,解决搜索只能搜索一次的问题

原来是放在mounted中只能搜索一次

mounted() { //仅执行一次
  //放到函数中
  // this.$store.dispatch("getSearchList",this.searchParams)
  this.getSearchData()
},
methods:{
  getSearchData(){
    //获取search数据
    this.$store.dispatch("getSearchList",this.searchParams)
  }
}

每次搜索$route都会发生变化, 所以可以监听$route

    mounted() { //仅执行一次
      //放到函数中
      // this.$store.dispatch("getSearchList",this.searchParams)
      //第一次需要合并到searchParams中, 让面包屑显示
      Object.assign(this.searchParams, this.$route.query, this.$route.params)
      this.getSearchData()
    },
    methods:{
      getSearchData(){
        //挂载完毕之前把query, params参数合并到searchParams中
        //复杂写法
        // this.searchParams. categorylId = this.Sroute.query.category1Id;
        // this.searchParams.category2Id = this.$route.query.category2Id;
        // this.searchParams.category3Id = this.Sroute.query.category3Id;
        // this.searchParams.categoryName = this.Sroute.query.categoryName;
        // this.searchParams. keyword = this. Sroute.params. keyword;
        //Object.assign: ES6新增的语法,合并对象
        //Object.assign(this.searchParams, this.$route.query, this.$route.params)
        //获取search数据
        this.$store.dispatch("getSearchList",this.searchParams)
      },
      
    },
    watch:{
      $route:{//路由信息变化就重新发起请求
        handler(){
          console.log(this.searchParams)
          //合并参数
          Object.assign(this.searchParams, this.$route.query, this.$route.params)
          this.getSearchData()
          //每次请求完毕后清空1,2,3级分类id
          this.searchParams.category1Id= undefined;
          this.searchParams.category2Id= undefined;
          this.searchParams.category3Id= undefined;
        }
      }
    }

2.5,面包屑处理分类

image-20220504170848133

<!--面包屑-->
<ul class="fl sui-tag">
    <!--分类的面包屑-->
    <li class="with-x" v-show="searchParams.categoryName" @click="removeCateGoryName">
        {{searchParams.categoryName}}<i >x</i>
	</li>
</ul>
methods:{
    removeCateGoryName(){
        //undefined请求不会带给服务器
        this.searchParams.categoryName = undefined;//清空对应名字
        this.getSearchData()//重新发请求
      }
}

image-20220504173443757

1,发现清空搜索之后地址没有改变

removeCateGoryName(){
  //undefined请求不会带给服务器
  this.searchParams.categoryName = undefined;//清空对应名字
  // this.getSearch();//重新发请求
  //地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
  this.$router.push({name :'search',params:this.$route.params})

}

2,搜索的时候把keyword放到面包屑中

image-20220504175045680

<!--面包屑-->
<ul class="fl sui-tag">
    <!--分类的面包屑-->
    <li class="with-x" v-show="searchParams.categoryName" @click="removeCateGoryName">
        {{searchParams.categoryName}}<i >x</i>
</li>
<!--关键字的面包屑-->
<li class="with-x" v-show="searchParams.keyword"  @click="removeKeyword">
    {{searchParams.keyword}}<i>x</i>
</li>
</ul>


    methods:{
      removeCateGoryName(){
        //undefined请求不会带给服务器
        this.searchParams.categoryName = undefined;//清空对应名字
        // this.getSearch();//重新发请求
        //地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
        this.$router.push({name :'search',params:this.$route.params})
      },
      removeKeyword(){
        this.searchParams.keyword = undefined;//清空对应名字
        //地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
        this.$router.push({name :'search',query:this.$route.query})
      }
    }

3,还需清除兄弟组件Header的输入框

请添加图片描述

设计组件通信:

  • props:父
  • 自定义事件:子父
  • vuex:万能
  • 插槽:父子
  • pubsub-js:完成
  • $bus:全局事件总线

这里回顾下全局事件总结

main.js,注册$bus
new Vue({
  render: h => h(App),
  //注册路由
  router,
  //注册仓库: 组件实例上就会多一个$store属性
  store,
  beforeCreate() {//注册全局时间总线
    Vue.prototype.$bus = this
  }
}).$mount('#app')
Header.vue中绑定自定义事件
mounted() {
  this.$bus.$on('clearKeyword',()=>{
    this.keyword = ''
  })
}
Search.vue中调用自定义事件完成keyword清空
removeKeyword(){
  this.searchParams.keyword = undefined;//清空对应名字
  //地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
  this.$router.push({name :'search',query:this.$route.query});
  //清空header组件的文本框
  this.$bus.$emit('clearKeyword');
}

vue-sgg项目-search-面包屑清除keyword

4,加入品牌搜索

品牌来自Search子组件SearchSelector.vue, 需要给父组件传递参数, 可以使用自定义事件

1,Search父组件给子标签添加自定义事件
<SearchSelector @trademarkInfo="trademarkInfo"/>
    
//methods回调-------------------------------------------------
  trademarkInfo(trademark){
        //整理品牌名称字段参数,  ID: 品牌名称
        this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
        // this.$router.push({name :'search',query:this.$route.query,params:this.$route.params})
        this.getSearchData()//重新发起请求
   }  
2,SearchSelector子组件调用传参
<ul class="logo-list">
  <!--给品牌添加点击事件并传递品牌信息 -->
  <li v-for="td in trademarkList" :key="td.tmId" @click="trademarkHandler(td)">{{td.tmName}}</li>
</ul>

//methods调用------------------------------------------
    methods:{
      trademarkHandler(trademark){
        this.$emit('trademarkInfo',trademark);//调用自定义事件,传参
      }
3,品牌搜索添加到面包屑

Search中添加标签

<!--品牌的面包屑-->
<li class="with-x" v-if="searchParams.trademark"  @click="removeTrademark">
    <!--split:将字符串转为数组, 注意不要使用v-show,split转undefiede会报错-->
    {{searchParams.trademark.split(":")[1]}}<i>x</i>
</li>

//methods----------------------------------
      removeTrademark(){//删除品牌信息
        this.searchParams.trademark = undefined;//要使用v-show,这里要 = 空字符串""
        //重新发起请求
        this.getSearchData();
      }

vue-sgg项目-search-品牌搜索

5,加入平台属性搜索(props)

1,同样的套路父子传递参数

Search

<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/>
//methods------------------------
      attrInfo(attr,attrValue){
        //属性ID:属性值:属性名
        let props =  `${attr.attrId}:${attrValue}:${attr.attrName}`;
        //判断数组中是否存在属性, 存在再添加
        if (this.searchParams.props.indexOf(props)==-1){
          this.searchParams.props.push(props);
          this.getSearchData()
        }
      }
2,子类调用

SearchSelector.vue

<!-- 添加点击事件, 传递属性         -->
  <li v-for="(attrValue,index) in attr.attrValueList" :key="index" @click="attrInfo(attr,attrValue)">
    <a>{{attrValue}}</a>
  </li>
//methods-----------------------
      attrInfo(attr,attrValue){
        //属性ID:属性值:属性名
        this.$emit('attrInfo',attr,attrValue)
      }
3,属性添加面包屑

Search

<!--平台售卖属性的面包屑, 数组,需要遍历-->
<li class="with-x" v-for="(attr,index) in searchParams.props" :key="index"  @click="removeProps(index)"><!--删除需要传数组下标-->
  <!--属性名称也在数组1位上-->
  {{attr.split(":")[1]}}<i>x</i>
</li>
//methods----------------------------------
      removeProps(index){
        //再次整理参数,删除对应下标的数据
        this.searchParams.props.splice(index,1);
        this.getSearchData()//再发请求
      }

vue-sgg项目-search-属性搜索

2.6,Search模块商品排序

接口排序字段order

1:综合,2:价格asc:升序,desc:降序
示例:“1:desc”

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

  • 1:asc
  • 1:desc
  • 2:asc
  • 2:desc

iconfont-阿里巴巴矢量图标库

index.html中导入样式

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

页面中使用样式就可以了: class=”iconfont icon-xxx”

Search.vue

<!--排序-->
<ul class="sui-nav">
  <li :class="{active: isOrder1}" @click="orderBy('1')">
    <a>综合 <span v-show="isOrder1" :class="icon"></span></a>
  </li>
  <li :class="{active: isOrder2}" @click="orderBy('2')">
    <a>价格 <span v-show="isOrder2" :class="icon"></span></a>
  </li>
</ul>
//计算属性-----------------------------------------------
    computed:{
      //mapGetters写法: 传递数组, 因为getters计算没有划分模块
      ...mapGetters(['goodsList']),
      isOrder1(){//控制综合排序的显示(1)隐藏(2)
        return this.searchParams.order.indexOf('1')!==-1
      },
      isOrder2(){//控制价格排序的显示(2)隐藏(1)
        return this.searchParams.order.indexOf('2')!==-1
      },
      icon(){//控制矢量图的上下, asc上, desc下
        let iconName = "iconfont ";
        if (this.searchParams.order.indexOf('asc')!==-1){
          iconName += "icon-xiangshang";
        }else if(this.searchParams.order.indexOf('desc')!==-1){
          iconName += "icon-xiangxia";
        }
        return iconName;
      }
    }
//methods-------------------------------------------------
import throttle from 'lodash/throttle'//按需引入节流
    methods:{
      orderBy:throttle(function (flag){//排序+节流
        //flag:1是综合, 2是价格
        let oldOrder = this.searchParams.order.split(':')[1];//查询之前的排序是asc还是desc
        if (this.searchParams.order.includes(flag)){//如果排序类型没有改变就取反
          oldOrder = oldOrder==='asc'? 'desc':'asc';//取反, asc就取desc
        }
        this.searchParams.order=`${flag}:${oldOrder}`;//如果改变了类型, 赋值给新的flag
        this.getSearchData();//重新发起请求
      },1000)
    }

效果

vue-sgg项目-2-6-排序

2.7,分页(1)注册全局组件分页器

/src/components/Pagination.vue, 创建好在main.js中注册好

<template>
  <div class="pagination">
    <button>上一页</button>
    <button>1</button>
    <button>···</button>

    <button>3</button>
    <button>4</button>
    <button>5</button>
    <button>6</button>
    <button>7</button>

    <button>···</button>
    <button>9</button>
    <button>下一页</button>

    <button style="margin-left: 30px">60</button>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Pagination",
}
</script>

<style lang="less" scoped>
.pagination {
  text-align: center;
  button {
    margin: 0 5px;
    background-color: #f4f4f5;
    color: #606266;
    outline: none;
    border-radius: 2px;
    padding: 0 4px;
    vertical-align: top;
    display: inline-block;
    font-size: 13px;
    min-width: 35.5px;
    height: 28px;
    line-height: 28px;
    cursor: pointer;
    box-sizing: border-box;
    text-align: center;
    border: 0;

    &[disabled] {
      color: #c0c4cc;
      cursor: not-allowed;
    }

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

Search中使用组件

请添加图片描述

2.8,分页(2)自定义分页器

分页展示需要的数据

  • pageNo: 当前第几页
  • pageSize: 页面大小
  • total: 一共多少数据
  • 一共多少页=tatal/pageSize 有余+1
    • 向上取整Math.ceil(tatal/pageSize)
  • continues:分页器连续页面个数: 5|7, 奇数对称,好看
    • 1 2 3…5 6 7 8 9 … 15 16 17

计算出continue开启页数和结束页数

computed:{
  getTotalPage(){
    //向上取整
    return  Math.ceil(this.total/this.pageSize);
  },
  startNumAndEndNum(){
    const {continues,getTotalPage,pageNo} = this;
    let startNum = 0,endNum = 0;
    //如果当前页码数小于连续数就取基本
    if (getTotalPage<=continues){
      startNum = 1;
      endNum = getTotalPage;
    }else{
      startNum = pageNo-parseInt(continues/2)
      endNum = pageNo+parseInt(continues/2)
    }
    if (startNum<1){//起始页数不能小于1
      startNum = 1;
      endNum = continues
    }
    if (endNum>getTotalPage){//结束页数不能大于最大页数
      endNum = getTotalPage;
      startNum = endNum-continues
    }
    return {startNum,endNum}
  }
}

2.9,分页(3)分页器动态展示

<template>
  <div class="pagination">
    <button>上一页</button>
    <button v-show="startNumAndEndNum.startNum>1">1</button>
    <button v-show="startNumAndEndNum.startNum>1">···</button>
    <!-- 中间部分  -->
    <button v-for="(page,index) in startNumAndEndNum.endNum"
            :key="index"
            v-show="page>=startNumAndEndNum.startNum">
      {{ page }}
    </button>
    <button v-show="startNumAndEndNum.endNum<getTotalPage-1">···</button>
    <button v-show="startNumAndEndNum.endNum<getTotalPage">{{ getTotalPage }}</button>
    <button>下一页</button>

    <button style="margin-left: 30px">{{total}}</button>
    {{startNumAndEndNum}}--{{getTotalPage}}
  </div>
</template>

Search添加分页效果

Search.vue传递参数给分页组件, 绑定自定义事件传递pageNo

  <!--分页器组件-->
  <Pagination    :pageNo ='searchParams.pageNo'
                 :pageSize="searchParams.pageSize"
                 :total="getTotal"
                 :continues="5"
                 @getPageNo="getPageNo"/>
</div>
//methods----------------------------------------------------\
      getPageNo(num){
        this.searchParams.pageNo=num;
        this.getSearchData()
      }

Pagination.vue分页组件调用自定义事件传递pageNo,

<template>
  <div class="pagination">
    <!--如果是第一页不能点上一页    -->
    <button :disabled="pageNo==1" @click="putPageNo(pageNo-1)">上一页</button>
    <button v-show="startNumAndEndNum.startNum>1" @click="putPageNo(1)">1</button>
    <button v-show="startNumAndEndNum.startNum>1">···</button>
    <!-- 中间部分  -->
    <button v-for="(page,index) in startNumAndEndNum.endNum"
            :key="index"
            v-show="page>=startNumAndEndNum.startNum"
            @click="putPageNo(page)">
      {{ page }}
    </button>
    <button v-show="startNumAndEndNum.endNum<getTotalPage-1">···</button>
    <button v-show="startNumAndEndNum.endNum<getTotalPage" @click="putPageNo(getTotalPage)">
      {{ getTotalPage }}
    </button>
    <button :disabled="pageNo==getTotalPage" @click="putPageNo(pageNo+1)">下一页</button>
	<button style="margin-left: 15px"><input type="text" style="width: 20px" ref="inputNo" :value="pageNo" @blur="searchByPageNo($event.target.value)"></button>
    <button style="margin-left: 30px">{{total}}</button>
  </div>
</template>
//methods-------------------------------
  methods:{
    putPageNo(no){
      this.$emit('getPageNo',no)
    },
    searchByPageNo(value){//按页数搜索
      let pageNo = parseInt(value.replace(/ /g,''));//去除所有空格
      if (pageNo<1 || pageNo>this.getTotalPage){
        this.$refs.inputNo.value=''
        alert(`请输入1~${this.getTotalPage}的数字!!!`);
        return
      }
      this.putPageNo(pageNo);
    }

效果

vue-sgg项目-search-分页

2.10,添加类名, 给当前页添加样式

<button v-show="startNumAndEndNum.startNum>1" @click="putPageNo(1)" :class="{active:pageNo==1}">1</button>

<!-- 中间部分  -->
<button v-for="(page,index) in startNumAndEndNum.endNum"
    :key="index"
    v-show="page>=startNumAndEndNum.startNum"
    @click="putPageNo(page)" :class="{active:pageNo==page}">
    {{ page }}
</button>

<button v-show="startNumAndEndNum.endNum<getTotalPage" 
@click="putPageNo(getTotalPage)"  
:class="{active:pageNo==getTotalPage}">
    {{ getTotalPage }}
</button>

//style-----------------------------
.active{
  background-color: skyblue;
}

image-20220506201353206

三, 商品详情(Detail)

拆分组件:这里使用拆好的

image-20220506220514945

3.1,Search传递参数到Dtail组件

1,注册路由(router)

import Detail from "@/pages/Detail/Detail";

export default new VueRouter({
    //配置路由
    routes:[
    	{
            path:'/detail/:skuId',//查询商品详细需要传递id
            component:Detail,
        }
    ]

2,Search传递参数

<!-- 商品图片 -->
<div class="p-img">
  <router-link :to="`/detail/${goods.id}`" ><img :src="goods.defaultImg" /></router-link>
</div>

3,测试

image-20220506211155327

发现跳转停留在中间, 应跳到顶部

4,滚动行为设置

官网说明 vue3的用的{top:0},vue2的要用{y:0}

滑动行为可以完全的被定制化处理 - 甚至为每次路由进行定制也可以满足。这将会开启很多新的可能,但是简单的复制旧的行为:

scrollBehavior: function (to, from, savedPosition) {
  return savedPosition || { x: 0, y: 0 }
}

添加到路由中, 与routes平级

export default new VueRouter({
    //配置路由
    routes:[...],
    scrollBehavior: function (to, from, savedPosition) {
      return savedPosition || { y: 0 } //这里只关心纵向位置, 只留Y即可
    }

测试正常

image-20220506211641425

5,将routes分离出来(可选)

router/routes.js

//引入路由组件
import Home from "@/pages/Home/Home";
import Login from "@/pages/Login/Login";
import Register from "@/pages/Register/Register";
import Search from "@/pages/Search/Search";
import Detail from "@/pages/Detail/Detail";


export default [
    {
        path:'/home',
        component:Home,
        meta:{show:true}
    },
    {
        path:'/login',
        component:Login,
        meta:{show:false}

    },
    {
        path:'/register',
        component:Register,
        meta:{show:false}
    },
    {
        path:'/search/:keyword?',
        component:Search,
        meta:{show:true},
        name:'search',
        //路由组件能不能传递props数据
        //布尔值写法: 只能传递params参数
        // props:true
        //对象写法:额外的给路由组件传递一些props参数
        // props:{a:1,b:2}
        //函数写法(常用),可以接收params参数,query参数,通过props传递
        /*            props:($route)=>{
                        return {keyword:$route.params.keyword,k:$route.query.k}
                    }*/
        //简写
        // props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})
    },
    {//重定向, /访问首页
        path:'*',
        // component:Home,
        redirect:'/home',//重定向到home
        meta:{show:true}
    },
    {
        path:'/detail/:skuId',//查询商品详细需要传递id
        component:Detail,
    }
]

index.js

引入routes即可

//配置路由
import Vue from "vue";
import VueRouter from "vue-router";
//使用插件
Vue.use(VueRouter)
//配置路由
//引入routes
import routes from "@/router/routes";
//先把vueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push;
let orginReplace = VueRouter.prototype.replace;
//重写push | replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
//二:成功的回调,   三: 失败的回调
VueRouter.prototype. push = function(location, resolve, reject){
    if(resolve && reject){
        //call] |apply区别
        //相同点,都可以调用函数一次,都可以算改函数的上下文一次
        //不同点: call与apply传递参数: cal1传递参数用逗号隔开, apply方法执行,传递数组
        originPush.call(this, location,resolve, reject);
    }else {
        originPush.call(this, location, ()=>{}, ()=>{});
    }
}
VueRouter.prototype.replace = function (location,resolve,reject){
    if (resolve && reject){
        orginReplace.call(this,location,resolve,reject);
    }else {
        orginReplace.call(this,location,()=>{},()=>{})
    }
}

export default new VueRouter({
    //配置路由
    routes,
    scrollBehavior: function (to, from, savedPosition) {
        return savedPosition || { y: 0 }//这里只关心纵向位置, 只留Y即可
    }
})

3.2,发请求获取商品详细信息

1,api添加方法

src/api/index.js

//获取产品详细信息 RUL: /api/item/{skuId} 请求方式get
export const reqGetGoodsDetail=(skuId)=>request.get(`/item/${skuId}`)

2,vuex中发请求

/store/store_detail.js

import {reqGetGoodsDetail} from "@/api";

const state = {
    goodsDetail: {}
};

//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    async getDetail({commit},skuId){
      let result = await reqGetGoodsDetail(skuId);
      if (result.code===200){
          commit('GETDETAIL',result.data)
      }
    }
};
//mutations: 修改state的唯一手段
const mutations = {
    GETDETAIL(state,data){
        state.goodsDetail =data
    }
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
	categoryView(state){
        //计算出来的属性为undefined时至少是一个空对象, 不会报错
        return state.goodsDetail.categoryView || {}
    },
    skuInfo(state){
        return state.goodsDetail.skuInfo || {}
    }
};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

index.js注册

import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
//引入小仓库
import store_home from "@/store/store_home";
import store_search from "@/store/store_search";
import store_detail from "@/store/store_detail";
//对外暴露
export default new Vuex.Store({
    //导入模块
    modules:{
        store_search,
        store_home,
        store_detail
    }
});

3,Detail.vue中获取派发Action获取数据

mounted() {

  //派发action获取产品信息
  this.$store.dispatch('getDetail',this.$route.params.skuId)
}

请求成功

image-20220506220338017

vuex中有数据

image-20220506220359830

3.3,动态展示主内容区信息

<!-- 右侧选择区域布局 -->
<div class="InfoWrap">
  <div class="goodsDetail">
    <h3 class="InfoName">{{skuInfo.skuName}}</h3>
    <p class="news">{{skuInfo.skuDesc}}</p>
    <div class="priceArea">
      <div class="priceArea1">
        <div class="title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div>
        <div class="price">
          <i>¥</i>
          <em>{{skuInfo.price}}</em>

          <!-- 选择区域动态展示 -->
          <div class="choose">
            <div class="chooseArea">
              <div class="choosed"></div>
              <dl v-for="spuSaleAttr in spuSaleAttrList" :key="spuSaleAttr.id">
                <dt class="title">{{spuSaleAttr.saleAttrName}}</dt>
                <dd changepirce="0"
                    :class="{active:spuSaleAttrValue.isChecked==='1'}"
                    v-for="spuSaleAttrValue in spuSaleAttr.spuSaleAttrValueList"
                    :key="spuSaleAttrValue.id">{{spuSaleAttrValue.saleAttrValueName}}</dd>
              </dl>
            </div>
<script>
  import ImageList from './ImageList/ImageList'
  import Zoom from './Zoom/Zoom'
 import {mapGetters} from "vuex";

  export default {
    // eslint-disable-next-line vue/multi-word-component-names
    name: 'Detail',

    components: {
      ImageList,
      Zoom
    },
    mounted() {

      //派发action获取产品信息
      this.$store.dispatch('getDetail',this.$route.params.skuId)
    },
    computed:{
      ...mapGetters(['categoryView','skuInfo','spuSaleAttrList']),
    }
  }
</script>

image-20220506232218073

选择区域点击切换高亮

<dd changepirce="0"
    :class="{active:spuSaleAttrValue.isChecked==='1'}"
    v-for="spuSaleAttrValue in spuSaleAttr.spuSaleAttrValueList"
    :key="spuSaleAttrValue.id"
    <!--添加点击事件,传入参数1: 当前选中的对象,参数2:包括兄弟对象-->
    @click="changeChecked(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)">
  {{spuSaleAttrValue.saleAttrValueName}}
</dd>
//methods
    methods:{
      changeChecked(value,list){
        list.forEach((item)=>{
          item.isChecked = '0';//将所有的都设置为没选择
        });
        value.isChecked = '1';//将选择的设置为已选择
      }
    }

vue-sgg项目-3.3-detail功能区

3.4,Zoom放大镜展示数据- 裁剪

1,Detail传递数据给Zoom组件展示

Detail

<!--放大镜效果-->
<Zoom :skuImageList="skuImageList"/>
<!-- 小图列表 -->
<ImageList :skuImageList="skuImageList"/>
//计算属性-------------------------------------------
    computed:{
      ...mapGetters(['categoryView','skuInfo']),
      skuImageList(){
        //解决undefined报错方法:1.在父组件的计算属性中写的是 || [{ }],
        // 2.在子组件中props用对象写法,default用函数写法返回 [{ }]
        return this.skuInfo.skuImageList || [{}]
      }
    }

Zoom.vue展示

<template>
  <div class="spec-preview">
    <img :src="imgObj"/>
    <div class="event"></div>
    <div class="big">
      <img :src="imgObj"/>
    </div>
    <div class="mask"></div>
  </div>
</template>

<script>
  export default {
    // eslint-disable-next-line vue/multi-word-component-names
    name: "Zoom",
    props:['skuImageList'],
    data(){
      return {
        currentIndex:0
      }
    },
    computed:{
      imgObj(){
        return this.skuImageList[this.currentIndex].imgUrl
      }
    },
    mounted() {
      this.$bus.$on('getImg',(index)=>{
        this.currentIndex = index
      });
    }
  }
</script>

ImageList.vue展示

<template>
  <div class="swiper-container" ref="floorSwiper">
    <div class="swiper-wrapper">
      <div class="swiper-slide" v-for="(list,index) in skuImageList" :key="list.id">
        <img :src="list.imgUrl"
             :class="{active:currentIndex===index}"
             @click="changeCurrentIndex(index)">
      </div>
    </div>
    <div class="swiper-button-next"></div>
    <div class="swiper-button-prev"></div>
  </div>
</template>

<script>

  import Swiper from 'swiper'
  export default {
    name: "ImageList",
    props:['skuImageList'],
    data(){
      return {currentIndex:0}
    },
    watch: {
        skuImageList:{
          handler(){
            this.$nextTick(()=> {
              new Swiper(this.$refs.floorSwiper, {
                //如果需要前进后退按钮
                navigation: {
                  nextEl: ".swiper-button-next",
                  prevEl: ".swiper-button-prev",
                },
                slidesPerView:3,//显示几个图片
                slidesPerGroup:1,//每一次切几张图
              });
            })
          }
        }
    },
    methods:{
      changeCurrentIndex(index){
        this.currentIndex = index;
        this.$bus.$emit('getImg',index)
      }
    }
  }
</script>

<style lang="less" scoped>
  .swiper-container {
        &.active {
          border: 2px solid #f60;
          padding: 1px;
        }

/*        &:hover { 把这里注掉, 使用js写
          border: 2px solid #f60;
          padding: 1px;
        }*/
      }
    }
效果

vue-sgg项目-3.4-轮播图

2,放大镜效果

<template>
  <div class="spec-preview" ref="preview">
    <img :src="imgObj"/>
    <div class="event" @mousemove="handler"></div>
    <div class="big" >
      <img :src="imgObj" ref="big"/>
    </div>
    <!-- 遮罩层   -->
    <div class="mask" ref="mask"></div>
  </div>
</template>
//methods------------------------------
    methods:{
      handler(e){
        let mask = this.$refs.mask
        let big = this.$refs.big
        let left = e.offsetX-mask.offsetWidth/2
        let top = e.offsetY-mask.offsetHeight/2
        if (left<0){//限制左边
          left = 0;
        }else if (left>mask.offsetWidth){//限制右边
          left = mask.offsetWidth;
        }
        if (top<0){//限制上边
          top = 0;
        }else if(top>mask.offsetHeight){//限制下边
          top = mask.offsetHeight;
        }
        mask.style.left = left+'px'
        mask.style.top = top+'px'
        //big是原来图的2倍
        big.style.left = - 2 * left+'px'
        big.style.top = - 2 * top+'px'
      }
    }

vue-sgg项目-3.4-2-放大镜

四,购物车

4.1,购买商品个数

<!--商品个数-->
<div class="cartWrap">
  <div class="controls">
    <input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum">
    <a  class="plus" @click="skuNum++">+</a>
    <a  class="mins" @click="skuNum>1 ? skuNum--:skuNum=1">-</a>
  </div>
//methods------------------------------
      changeSkuNum(e){//购买个数
        let num = parseInt(e.target.value.replace(/ /g,''))//去除所有空格,取整
        if (isNaN(num) || num<1){//输入不是数字,或负数
          num = 1
        }
        this.skuNum = num ;
        console.log(this.skuNum)
      }

4.2, 加入购物车

  • 1,发请求
  • 2,跳转成功加入
  • 3,显示购买信息

1,发送请求获取数据

/api/index.js

//存入购物车(或者更新产品数量)
//URL: /api/cart/addToCart/{skuId}/{skuNum} post
export const reqShopCart = (skuId,skuNum)=>request.post(`/cart/addToCart/${skuId}/${skuNum}`)

Vuex: store_detail.js

import {reqGetGoodsDetail,reqShopCart} from "@/api";

const state = {
    goodsDetail: {},
};

//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    async getDetail({commit},skuId){//获取产品详情
      let result = await reqGetGoodsDetail(skuId);
      if (result.code===200){
          commit('GETDETAIL',result.data)
      }
    },
    async updateShopCart(_, {skuId, skuNum}){//添加购物车
        let result = await reqShopCart(skuId,skuNum);
        //加入购物车服务器不会返回数据
        console.log(result.data)
        //当前函数加上了async数返回Promise
        if (result.code===200){//成功
            return 'ok';
        }else {//失败
            return Promise.reject(new Error('faile'));
        }
    }
};
//mutations: 修改state的唯一手段
const mutations = {
    GETDETAIL(state,data){
        state.goodsDetail =data
    },
    UPDATESHOPCART(state,data){
        state.shopCart = data
    }
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
    categoryView(state){
        //计算出来的属性为undefined时至少是一个空对象, 不会报错
        return state.goodsDetail.categoryView || {}
    },
    skuInfo(state){
        return state.goodsDetail.skuInfo || {}
    },
    spuSaleAttrList(state){
        return state.goodsDetail.spuSaleAttrList || []
    }
};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

Detail.vue

<div class="add">
  <a @click="addShopCart">加入购物车</a>
</div>
//methods------------------------------
      async addShopCart() {
        //相当于调用函数, 函数加上了async数返回Promise
        try {
          await this.$store.dispatch('updateShopCart', {skuId: this.$route.params.skuId,skuNum:this.skuNum});
        } catch (error) {
          console.log(error.message)
        }
      }

2,跳转路由

page下添加组件AddCartSuccess.vue

image-20220508154532284

router下注册路由

import AddCartSuccess from '@/pages/AddCartSuccess/AddCartSuccess'
{
        path: '/addcartsuccess',
        name: 'addcartsuccess',
        component: AddCartSuccess
}

Detail.vue中跳转

async addShopCart() {
  //相当于调用函数, 函数加上了async数返回Promise
  try {
    await this.$store.dispatch('updateShopCart', {skuId: this.$route.params.skuId,skuNum:this.skuNum});
    //路由跳转
    this.$router.push({name:'addcartsuccess'});
  } catch (error) {
    console.log(error.message)
  }
}

3,路由传参,展示加入购物车数据

Detail.vue

async addShopCart() {
  //相当于调用函数, 函数加上了async数返回Promise
  try {
    await this.$store.dispatch('updateShopCart', {skuId: this.$route.params.skuId,skuNum:this.skuNum});
    //路由跳转
    // this.$router.push({name:'addcartsuccess'});
    //需要传递数据给成功组件显示
    //方式一导致URL不好看http://localhost:8080/#addcartsuccess?skuInfo=%5Bobject%20Object%5D&skuNum=3
    // this.$router.push({name:'addcartsuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})
    //方式二, 只传skuNum, skuInfo放到浏览器会话存储
    sessionStorage.setItem('skuInfo',JSON.stringify(this.skuInfo));
    this.$router.push({name:'addcartsuccess',query:{skuNum:this.skuNum}})
  } catch (error) {
    console.log(error.message)
  }
}

AddCartSuccess.vue

获取数据, 展示

<template>
  <div class="cart-complete-wrap">
    <div class="cart-complete">
      <h3><i class="sui-icon icon-pc-right"></i>商品已成功加入购物车!</h3>
      <div class="goods">
        <div class="left-good">
          <div class="left-pic">
            <img :src="skuInfo.skuDefaultImg">
          </div>
          <div class="right-info">
            <p class="title">{{skuInfo.skuName}}</p>
            <p class="attr">{{skuInfo.skuDesc}} 数量:{{ $route.query.skuNum }}</p>
          </div>
        </div>
        <div class="right-gocart">
          <a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a>
          <a href="javascript:" >去购物车结算 > </a>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'AddCartSuccess',
    computed:{
      skuInfo(){
        //获取浏览器会话存储的skuInfo并转为对象
        return JSON.parse(sessionStorage.getItem('skuInfo'));
      }
    }
  }
</script>

image-20220508162159822

4,存入用户信息uuid

4.1,工具类uuid

utils/uuid_token.js

import {v4 as uuidv4} from "uuid";
export const getUUID = ()=>{
    //先查看本地存储有没有uuid
    let uuid_token = localStorage.getItem('uuid_token');
    //如果没有就新建一个
    if (!uuid_token){
        uuid_token = uuidv4();
        //存到本地存储中
        localStorage.setItem('uuid_token',uuid_token);
    }
    return uuid_token;

}
4.2,在请求拦截器中放入uuid
//引入store(获取uuid)
import store_detail from "@/store/store_detail";
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
    if (store_detail.state.uuid_token){
        //请求头添加字段userTempId--和后台商量好了
        config.headers.userTempId = store_detail.state.uuid_token
    }
    //进度条开始动
    nprogress.start();
    return config
});

随便找一个请求查看

image-20220508170627345

4.3,购物车–查看商品详情

router-link返回到detail即可

<div class="right-gocart">
    <!--<a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a>-->
    <router-link :to="`/detail/${skuInfo.id}`" class="sui-btn btn-xlarge">查看商品详情</router-link>
	<a href="javascript:" >去购物车结算 > </a>
</div>

4.4,购物车结算

导入购物车组件

请添加图片描述

注册路由

import ShopCart from '@/pages/ShopCart/ShopCart'
{
    path: '/shopcart',
    name: 'shopcart',
    component: ShopCart
}

AddCartSuccess.vue购物车结算跳转

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

1,发请求

api/index.js

/*
* 获取购物车列表接口
* URL: /api/cart/cartList
* method = get
* */
export  const  reqCartList = ()=>request.get('/cart/cartList')

vuex获取数据

store_shopcart.js

import {reqCartList} from "@/api";
const state = {
    cartList:[]
};

//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    async getCartList({commit}){
        let result = await reqCartList();
        if (result.code===200){
            commit('GETCARTLIST',result.data);
        }
    }
};
//mutations: 修改state的唯一手段
const mutations = {
    GETCARTLIST(state,data){
        state.cartList = data
    }
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
    cartInfoList(state){
        return state.cartList.length>0?state.cartList[0].cartInfoList : [] ;
    }
};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

2,展示数据

<template>
  <div class="cart">
    <h4>全部商品</h4>
    <div class="cart-main">
      <div class="cart-th">
        <div class="cart-th1">全部</div>
        <div class="cart-th2">添加日期</div>
        <div class="cart-th3">单价(元)</div>
        <div class="cart-th4">数量</div>
        <div class="cart-th5">小计(元)</div>
        <div class="cart-th6">操作</div>
      </div>
      <div class="cart-body">
        <ul class="cart-list" v-for="(cart,index) in cartInfoList" :key="cart.id">
          <li class="cart-list-con1">
            <input type="checkbox" name="chk_list" :checked="cart.isChecked===1" @click="checkCart(index)">
          </li>
          <li class="cart-list-con2">
            <img :src="cart.imgUrl">
            <div class="item-msg">{{cart.skuName}}</div>
          </li>
          <li class="cart-list-con3">
            <div class="item-txt">{{ cart.operateTime }}</div>
          </li>
          <li class="cart-list-con4">
            <span class="price">{{cart.skuPrice}}.00</span>
          </li>
          <li class="cart-list-con5">
            <a href="javascript:void(0)" class="mins">-</a>
            <input autocomplete="off" type="text" value="1" minnum="1" class="itxt">
            <a href="javascript:void(0)" class="plus">+</a>
          </li>
          <li class="cart-list-con6">
            <span class="sum">{{cart.skuPrice * cart.skuNum}}</span>
          </li>
          <li class="cart-list-con7">
            <a href="#none" class="sindelet">删除</a>
            <br>
            <a href="#none">移到收藏</a>
          </li>
        </ul>
      </div>
    </div>
    <div class="cart-tool">
      <div class="select-all"  v-show="cartInfoList.length>0">
        <input class="chooseAll" type="checkbox"
               @click="chooseAll($event)"
               :checked="IsChooseAll">
        <span>全选</span>
      </div>
      <div class="option">
        <a href="#none">删除选中的商品</a>
        <a href="#none">移到我的关注</a>
        <a href="#none">清除下柜商品</a>
      </div>
      <div class="money-box">
        <div class="chosed">已选择
          <span>0</span>件商品</div>
        <div class="sumprice">
          <em>总价(不含运费) :</em>
          <i class="summoney">{{totalPrice  }}</i>
        </div>
        <div class="sumbtn">
          <a class="sum-btn" href="###" target="_blank">结算</a>
        </div>
      </div>
    </div>
  </div>
</template>

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

export default {
    name: 'ShopCart',
    mounted() {
      this.$store.dispatch('getCartList')
    },
    computed:{
      ...mapState({
        cartList:(state)=>{
          return state.store_shopcart.cartList[0] || [];
        }
      }),
      cartInfoList(){
        return this.cartList.cartInfoList || []
      },
      totalPrice(){//遍历购物车计算总价
        let sum = 0;
        this.cartInfoList.forEach((cart)=>{
          if (cart.isChecked===1){ //计算勾选上的
            sum +=  cart.skuPrice * cart.skuNum;
          }
        })
        return sum;
      },
      IsChooseAll(){//是否全选
        //every:若数组每一个对象都符合条件,则返回true ,只要有一个不符合条件就返回false
        // every 就当作是 &&  some看作是 ||
        return this.cartInfoList.every(item=>{
          return item.isChecked ===1
        })
      }
    },
  methods:{
    checkCart(index){//单选
      let check =  this.cartInfoList[index].isChecked;
      if (check===1){
        this.cartInfoList[index].isChecked = 0
      }else {
        this.cartInfoList[index].isChecked = 1
      }
    },
    chooseAll(){//全选
      this.cartInfoList.forEach((cart)=>{
        if (cart.isChecked===1){
          cart.isChecked=0
        }else {
          cart.isChecked=1
        }
      })
    }
  }
  }
</script>

4.5,购物车数量增减

<li class="cart-list-con5">
  <a href="javascript:void(0)" class="mins" @click="handlerNum('minus',-1,cart,index)">-</a>
  <input autocomplete="off" ref="itxt" type="text" :value="cart.skuNum" minnum="1" class="itxt" @change="handlerNum('text',$event.target.value,cart,index)">
  <a href="javascript:void(0)" class="plus" @click="handlerNum('add',1,cart,index)">+</a>
</li>
//methods
    handlerNum:throttle(async function  (type,num,cart,index){//节流
      switch (type){
        case "minus":
          num = cart.skuNum >=1 ? -1 : 0; //不能小于0
          break;
        case "text":
          num = parseInt(num.replace(/ /g,''));//去除输入空格
            //如果值未改变, 非法数字, 负数都不修改值
          if (num-cart.skuNum===0 || isNaN(num) || num<0 ){
            num = 0
            this.$refs.itxt[index].value = cart.skuNum//值不对修改为原来的值
            return
          }
          num = num - cart.skuNum;
          break;
      }
      if(num ===0) return;//如果没有修改就返回

      try {
         await this.$store.dispatch('updateShopCart',{
          skuId:cart.skuId,
          skuNum:num
        });
        this.getShopCartData();//重新发起请求
      }catch (error){
        console.log(error.message)
      }
      console.log(type,num,cart.skuNum)
    },500)
  }

效果

请添加图片描述

4.6,删除购物车产品

api

/*删除购物车产品
* URL: /api/cart/deleteCart/${skuId}
* method: DELETE
* */
export const reqDeleteCartById = (skuId)=>request.delete(`/cart/deleteCart/${skuId}`);

vuex

    async deleteCartListById(_,skuId){
        let result = await reqDeleteCartById(skuId);
        if (result.code===200){
            return 'ok';
        }else {
            return Promise.reject(new Error('faile'))
        }
    }

ShopCart.vue

<li class="cart-list-con7">
  <a class="sindelet" @click="deleteCartById(cart.skuId)">删除</a>
  <br>
  <a >移到收藏</a>
</li>
//methods
    async deleteCartById(skuId){
      try {
        console.log(skuId)
        await this.$store.dispatch('deleteCartListById',skuId);
        //产出成功重新发请求
        this.getShopCartData();
      }catch (error){
        console.log(error.message)
      }
    }

4.7,修改产品状态

api

/*切换商品选中状态
* URL: /api/cart/checkCart/${skuId}/${isChecked}
* method: get
* */
export const reqUpdateCartChecked = (skuId,isChecked)=>request.get(`/cart/checkCart/${skuId}/${isChecked}`)

vuex

async updateCartChecked(_, {skuId,isChecked}){
    let result = await reqUpdateCartChecked(skuId,isChecked);
    if (result.code===200){
        return 'ok';
    }else {
        return Promise.reject(new Error('faile'))
    }
}

ShopCart.vue

<!--单选-->
<li class="cart-list-con1">
  <input type="checkbox" name="chk_list"
         :checked="cart.isChecked===1"
         @click="checkCart(cart,$event)">
</li>
<!--全选-->
<div class="select-all"  v-show="cartInfoList.length>0">
    <input class="chooseAll" type="checkbox"
	@click="chooseAll($event)"
    :checked="IsChooseAll">
    <span>全选</span>
</div>
//computed
IsChooseAll(){//是否全选
    //every:若数组每一个对象都符合条件,则返回true ,只要有一个不符合条件就返回false
    // every 就当作是 &&  some看作是 ||
    if (this.cartInfoList.length<1){
        return
    }
    return this.cartInfoList.every(item=>{
        return item.isChecked ===1
    })
}
//methods
//按需引入lodash
//节流, 第一次点击生效, 最后一次再生效
// import throttle from 'lodash/throttle'
//防抖, 最后一次生效
import debounce from 'lodash/debounce'
//---------------------------------------------------------------------------------
    checkCart:debounce(async function (cart,event){//单选+防抖
      try {
        let check =  event.target.checked ? 1:0; //通过事件获取单选框是否选中, 选中返回1
        //发请求修改点击状态
        await this.$store.dispatch('updateCartChecked',{skuId:cart.skuId,isChecked:check})
        this.getShopCartData();//发请求更新数据
      }catch (error){
        console.log(error.message)
      }
    },500),
    chooseAll:debounce(function (event){//全选+防抖
      try {
         this.cartInfoList.forEach(async (cart)=>{//遍历所有产品
          let check = event.target.checked?1:0;//通过事件获取单选框是否选中, 选中返回1
           //发请求修改所有产品点击状态
          await this.$store.dispatch('updateCartChecked',{skuId:cart.skuId,isChecked:check})
        })
        setTimeout(()=>{//等待所有产品的状态修改完成
          this.getShopCartData();//发请求更新数据
        },200)
      }catch (error){
        alert(error.message)
      }
    },500),

Promise.all实现全选

store_shopcart.js–action中添加方法

//全选购物车,更改产品状态
updateCheckedAllCart(context, isCheched){
    let cartInfoList = context.getters.cartInfoList;//获取购物车产品列表
    if (cartInfoList.length<1){
        return 'no';//没有数据返回
    }
    let promiseAll = []
    cartInfoList.forEach((cart)=>{
        let check = isCheched?1:0;
        let promise =  context.dispatch('updateCartChecked',{skuId:cart.skuId,isChecked:check});
        //把每一次返回的promise放到数组中
        promiseAll.push(promise)
    });
    //只要所有的promise都成功, 结果为成功, 有一个失败就失败
    return Promise.all(promiseAll)
},

ShopCart.vue

    chooseAll:debounce(async function (event){//全选+防抖
      try {
        //updateCheckedAllCart
        let result = await this.$store.dispatch('updateCheckedAllCart',event.target.checked)
        console.log(result)
        if (result==='no') return alert('当前购物车无数据')
        this.getShopCartData();
      }catch (error){
        alert(error.message)
      }
    },500),

效果

请添加图片描述

4.8,删除选中的全部产品

没有一次删除多个产品的接口, 有删除一个的接口

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

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

store_shopcart.js–action中添加方法

//删除全部勾选产品
deleteAllCheckedCart(context){
    let cartInfoList = context.getters.cartInfoList;//获取购物车产品列表
    if (cartInfoList.length<1){
        return 'no';//没有数据返回
    }
    let promiseAll = []
    cartInfoList.forEach((item)=>{
        if (item.isChecked===1){
            let promise =  context.dispatch('deleteCartListById',item.skuId);
            //把每一次返回的promise放到数组中
            promiseAll.push(promise)
        }
    });
    //只要所有的promise都成功, 结果为成功, 有一个失败就失败
    return Promise.all(promiseAll)
}

ShopCart.vue

 <a @click="deleteAllChecked">删除选中的商品</a>

//methods---------------------------------------------------
//删除所有勾选的产品
async deleteAllChecked(){
    try {
        let result = await this.$store.dispatch('deleteAllCheckedCart');
        if (result==='no') return alert('当前购物车无数据')
        this.getShopCartData();
    }catch (error){
        alert(error.message)
    }
}

效果

请添加图片描述

五, 注册

静态组件准备好

请添加图片描述

1,获取手机验证码

api

/*获取验证码
* URL: /api/user/passport/sendCode/${phone}
* method:get
* */
export const reqGetCode = (phone)=>request.get(`/user/passport/sendCode/${phone}`)

store_user.js

/*登录注册共用仓库*/
import {reqGetCode} from "@/api";
const state = {
    code:''
};

//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    //获取验证码
    async getCode({commit},phone){
        let result =  await reqGetCode(phone)
        if (result.code==200){
            commit('GETCODE',result.data);
            return 'ok'
        }else {
            return Promise.reject(new Error('faile'))
        }
    }

};
//mutations: 修改state的唯一手段
const mutations = {
    GETCODE(state,value){
        state.code = value
    }

};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {

};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

Register.vue

      <div class="content">
        <label>手机号:</label>
        <input type="text" placeholder="请输入你的手机号" v-model="phone">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>验证码:</label>
        <input type="text" placeholder="请输入验证码" v-model="code">
<!--        <img ref="code" src="http://182.92.128.115/api/user/passport/code" alt="code">-->
        <button style="width: 100px;height: 40px" @click="getCode">获取验证码</button>
        <span class="error-msg">错误提示信息</span>
      </div>
     <div class="content">
        <label>登录密码:</label>
        <input type="text" placeholder="请输入你的登录密码" v-model="password">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input type="text" placeholder="请输入确认密码" v-model="password1">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="controls">
        <input name="m1" type="checkbox" :checked="isAgree">
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">错误提示信息</span>
      </div>
//methods-------------------------------------
    data(){
    return {
      phone:'',//电话号码
      code:'',//验证码
      password:'',//密码
      password1:'',//确认密码
      isAgree:true,//是否同意协议
    }
  },methods:{
    //获取验证码, 防抖
     getCode:debounce(async function (){
      try {
        //如果获取到验证码再继续往下走
        if (this.phone==='') return
        await this.$store.dispatch('getCode',this.phone)
        this.code = this.$store.state.store_user.code
      }catch (error){
        alert(error.message)
      }
    },1000)
  }

2,完成注册

api

/*注册用户
* URL: /api/user/passport/register
* methods: post
* */
export const reqUserRegister=(data)=>request.post(`/user/passport/register`,data)

store_user.js

/*登录注册共用仓库*/
import {reqGetCode,reqUserRegister} from "@/api";

//用户注册,无返回值
async userRegister(_,data){
    let result =  await reqUserRegister(data)
    console.log(result)
    if (result.code==200){
        return 'ok'
    }else {
        return Promise.reject(new Error(result.message))
    }
},

Register.vue

<div class="btn">
  <button @click="userRegister">完成注册</button>
</div>
//methods
    //注册提交
    async userRegister(){
      try {
        const {phone,code,password,password1} =this;
        //验证成功再发请求
        if(phone!==''&&code!==''&&password===password1){
          await this.$store.dispatch('userRegister',{phone,code,password})
          this.$router.push('/login');//注册成功跳转到登录
        }
        return
      }catch (error){
        alert(error.message)
      }
    }

六,登录

1,登录跳转

api

/*登录
* URL: /api/user/passport/login
* methods: post
* */
export const reqUserLogin=(data)=>request.post(`/user/passport/login`,data)

store_user.js

/*登录注册共用仓库*/
import {reqGetCode,reqUserRegister,reqUserLogin} from "@/api";
//用户登录
async userLogin({commit},data){
    let result = await reqUserLogin(data);
    console.log(result)
    if (result.code===200){
        //登录成功存储token
        commit('USERLOGIN',result.data);
    }else {
        Promise.reject(new Error(result.message))
    }
}
    USERLOGIN(state,value){
        state.user = value
    },

Login.vue

<!--阻止默认行为修饰符, prevent -->
<button class="btn" @click.prevent="login">&nbsp;&nbsp;</button>
//----------------------------------------------------------
  data(){
    return {
      phone:'',
      password:''
    }
  },
  methods:{
    async login(){
      try {
        const {phone,password} = this
        if (phone!=='' && password!==''){
          await this.$store.dispatch('userLogin',{phone,password})
          await this.$router.push('/home')
        }
      }catch (error){
        alert(error.message)
      }
    }
  }

2,登录携带token获取用户信息

通过token获取用户信息

api
/*通过token获取用户信息
* URL: /api/user/passport/auth/getUserInfo
* method: get, 可通过header传递token
* */
export const reqGetUserInfo=()=>request.get(`/user/passport/auth/getUserInfo`)
store_user.js
//通过token获取用户信息
async getUserInfo({commit}){
    let result = await reqGetUserInfo();
    console.log(result,commit)
    if (result.code===200){
        //存储用户信息
        commit('GETUSERINFO',result.data)
    }else {
        return  Promise.reject(new Error(result.message))
    }
}

    GETUSERINFO(state,value){
        state.userInfo = value
    },
request.js, 请求头中加入token
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
    if (store_detail.state.uuid_token){
        //请求头添加字段userTempId--和后台商量好了
        config.headers.userTempId = store_detail.state.uuid_token
    }
    if (store_user.state.token){//如果仓库中有token, 加入到header中
        config.headers.token = store_user.state.token
    }
    //进度条开始动
    nprogress.start();
    return config
});
Home.vue中加载数据
mounted() {
  //vuex-action方法获取floorList
  this.$store.dispatch('getFloorList');
  //获取用户信息
  this.$store.dispatch('getUserInfo')
},

Header.vue中显示用户信息

<div class="loginList">
  <p>尚品汇欢迎您!</p>
  <p v-if="!userName">
    <span></span>
    <router-link to="/login">登录</router-link>
    <router-link class="register" to="/register">免费注册</router-link>
  </p>
  <p v-else>
    <span>用户 </span>
        <a >{{userName}}</a>
        <a  class="register">退出登录</a>
  </p>
</div>
//------------------------------------------------
  computed:{
    userName(){
      return this.$store.state.store_user.userInfo.name
    }
  },

3,持久化存储token

const state = {
    code:'',
    token:localStorage.getItem("TOKEN"),//state中取
    userInfo:{}
};

//用户登录,登录后把token存放到本地存储中
async userLogin({commit},data){
    let result = await reqUserLogin(data);
    if (result.code===200){
        //登录成功存储token
        commit('USERLOGIN',result.data.token);
        //持久化存储token
        localStorage.setItem("TOKEN",result.data.token)
    }else {
        return  Promise.reject(new Error(result.message))
    }
},

4,退出登录

api

/*退出登录
* URL: /api/user/passport/logout
* method: get,
* */
export const reqLogout=()=>request.get(`/user/passport/logout`)

store_user.js

//通过token获取用户信息
async logout({commit}){
    let result = await reqLogout();
    console.log(result)
    if (result.code===200){
        //清除仓库中的用户信息(action不直接操作state, 交给mutation操作)
        commit('CLEAR');

    }else {
        return  Promise.reject(new Error(result.message))
    }
},
    //--------------------------------------
    CLEAR(state){
        //把仓库中user信息清空
        state.token = '';
        state.userInfo = {};
        //清除本地token
        localStorage.removeItem('TOKEN')
    }

5,添加访问限制–路由守卫

全局守卫:

  • 只要路由变化, 守卫就能监控到

Home.vue去掉获取用户信息

  mounted() {
    //vuex-action方法获取floorList
    this.$store.dispatch('getFloorList');
/*    //获取用户信息
    this.$store.dispatch('getUserInfo')*/
  },

router/index.js

//配置路由
import Vue from "vue";
import VueRouter from "vue-router";
import store from '@/store'
//使用插件
Vue.use(VueRouter)
//配置路由
//引入routes
import routes from "@/router/routes";
//先把vueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push;
let orginReplace = VueRouter.prototype.replace;
//重写push | replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
//二:成功的回调,   三: 失败的回调
VueRouter.prototype. push = function(location, resolve, reject){
    if(resolve && reject){
        //call] |apply区别
        //相同点,都可以调用函数一次,都可以算改函数的上下文一次
        //不同点: call与apply传递参数: cal1传递参数用逗号隔开, apply方法执行,传递数组
        originPush.call(this, location,resolve, reject);
    }else {
        originPush.call(this, location, ()=>{}, ()=>{});
    }
}
VueRouter.prototype.replace = function (location,resolve,reject){
    if (resolve && reject){
        orginReplace.call(this,location,resolve,reject);
    }else {
        orginReplace.call(this,location,()=>{},()=>{})
    }
}

let router= new VueRouter({
    //配置路由
    routes,
    scrollBehavior: function (to, from, savedPosition) {
        return savedPosition || { y: 0 }//这里只关心纵向位置, 只留Y即可
    }
})
/*全局前置守卫,  在路由跳转之前拦截
*  to: 获取跳转的路由信息
*  from: 从哪个路由来的信息
*  next: 放行函数
*       next()直接放行
*       next('/login')---放行到指定路由
*       next(false)   中断当前导航, 重置到 from路由地址
* */
router.beforeEach(async (to, from, next)=>{
    // console.log(to,from,next)
    let token = store.state.store_user.token;
    let name = store.state.store_user.userInfo.name
    let path = to.fullPath
    console.log(token,name,path)
    if (token ){ //已登录情况
        if (path==='/login'){//不能去login
            alert('您已登录!  重新登录点击退出登录');
            next(from.fullPath)
        }else {//去其他 home-search-detail-shopcart
            if (name){
                next()
            }else {
                try {
                    //没有用户名, 派发action查询用户
                    await store.dispatch('getUserInfo');
                    next()
                }catch (error){
                    //token失效
                    await store.dispatch('logout');
                    alert('身份已失效, 请重新登录')
                    next('/login');
                }
            }
        }
    }else {//未登录情况
        next()
    }
})


export default router

七,购物车结算-微信支付

导入静态页面

image-20220514161045162

1,获取结算所需信息

api

/*获取用户地址信息
* URL:/api/user/userAddress/auth/findUserAddressList
* method: get
* */
export const reqUserAddress = ()=>request.get(`/user/userAddress/auth/findUserAddressList`)

/*获取商品清单
* URL:/api/order/auth/trade
* method: get
* */
export const reqUserOrder = ()=>request.get(`/order/auth/trade`)

store_trade.js发起请求

/*登录注册共用仓库*/
import {reqUserAddress,reqUserOrder} from "@/api";
const state = {
    addressList:[],
    orderList:[]
};

//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
    //获取用户地址
    async getUserAddress({commit}){
        let result = await reqUserAddress();
        if (result.code===200){
            commit('GETUSERADDRESS',result.data);
        }else {
            return  Promise.reject(new Error(result.message))
        }
    },
    //获取商品清单
    async getUserOrder({commit}){
        let result = await reqUserOrder();
        if (result.code===200){
            commit('GETUSERORDER',result.data);
        }else {
            return  Promise.reject(new Error(result.message))
        }
    }
};
//mutations: 修改state的唯一手段
const mutations = {
    GETUSERADDRESS(state,data){
        state.addressList = data
    },
    GETUSERORDER(state,data){
        state.orderList = data
    },
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {

};
//对外暴露
export default({
    state,
    mutations,
    actions,
    getters,
});

Trade.vue, 派发action

mounted() {
  this.$store.dispatch('getUserAddress')
  this.$store.dispatch('getUserOrder')
}

2,动态展示结算数据

<template>
  <div class="trade-container">
    <h3 class="title">填写并核对订单信息</h3>
    <div class="content">
      <h5 class="receive">收件人信息</h5>
      <div class="address clearFix" v-for="address in addressList" :key="address.id">
        <span class="username " :class="{selected:address.isDefault==='1'}">{{ address.consignee }}</span>
        <p @click="changeDefalt(address,addressList)">
          <span class="s1">{{ address.fullAddress }}</span>
          <span class="s2">{{ address.phoneNum }}</span>
          <span class="s3" v-show="address.isDefault==='1'">默认地址</span>
        </p>
      </div>

      <div class="line"></div>
      <h5 class="pay">支付方式</h5>
      <div class="address clearFix">
        <span class="username selected">在线支付</span>
        <span class="username" style="margin-left:5px;">货到付款</span>

      </div>
      <div class="line"></div>
      <h5 class="pay">送货清单</h5>
      <div class="way">
        <h5 class="pay">配送方式</h5>
        <div class="info clearFix">
          <span class="s1">天天快递</span>
          <p>配送时间:预计810日(周三)09:00-15:00送达</p>
        </div>
      </div>
      <div class="detail">
        <h5 class="pay">商品清单</h5>
        <ul class="list clearFix" v-for="(order,index) in orderList.detailArrayList" :key="index">
          <li>
            <img :src="order.imgUrl" alt="" style="width: 100px;height: 100px">
          </li>
          <li>
            <p>
              {{ order.skuName }}</p>
            <h4 >7天无理由退货</h4>
          </li>
          <li>
            <h3>{{ order.orderPrice }}.00</h3>
          </li>
          <li>X{{ order.skuNum }}</li>
          <li>有货</li>
        </ul>
      </div>
      <div class="bbs">
        <h5>买家留言:</h5>
        <textarea placeholder="建议留言前先与商家沟通确认" class="remarks-cont" v-model="message"></textarea>

      </div>
      <div class="line"></div>
      <div class="bill">
        <h5>发票信息:</h5>
        <div>普通发票(电子) 个人 明细</div>
        <h5>使用优惠/抵用</h5>
      </div>
    </div>
    <div class="money clearFix">
      <ul>
        <li>
          <b><i>{{ orderList.totalNum }}</i>件商品,总商品金额</b>
          <span>¥{{ orderList.totalAmount }}.00</span>
        </li>
        <li>
          <b>返现:</b>
          <span>0.00</span>
        </li>
        <li>
          <b>运费:</b>
          <span>0.00</span>
        </li>
      </ul>
    </div>
    <div class="trade">
      <div class="price">应付金额:<span>¥{{ orderList.totalAmount }}.00</span></div>
      <div class="receiveInfo">
        寄送至:
        <span>{{ defaultAddress.fullAddress }}</span>
        收货人:<span>{{ defaultAddress.consignee }}</span>
        <span>{{ defaultAddress.phoneNum }}</span>
      </div>
    </div>
    <div class="sub clearFix">
      <router-link class="subBtn" to="/pay">提交订单</router-link>
    </div>
  </div>
</template>

<script>
import {mapState} from "vuex";
export default {
    // eslint-disable-next-line vue/multi-word-component-names
    name: 'Trade',
    data(){
      return {
        message:''
      }
    },
    mounted() {
      this.$store.dispatch('getUserAddress')
      this.$store.dispatch('getUserOrder')
    },
    computed:{
      ...mapState({
        addressList:state=>state.store_trade.addressList,
        orderList:state=>state.store_trade.orderList,
      }),
      defaultAddress(){
        //find查找数组中符合条件的元素
        return this.addressList.find(item=>item.isDefault==='1')
      }
    },
    methods:{
      //修改默认数组
      changeDefalt(address,addressList){
        addressList.forEach((item)=>{
          item.isDefault = '0'
        })
        address.isDefault = '1'
      }
    }
  }
</script>

3,提交订单

静态组件

请添加图片描述

api

/*获取商品清单
* URL:/api/order/auth/submitOrder?tradeNo=${tradeNo}
* method: post
* */
export const reqSubmitOrder = (tradeNo,data)=>request.post(`/order/auth/submitOrder?tradeNo=${tradeNo}`,data)

不使用vuex发请求了, 在main.js中引入全部api

//引入api
import * as API from '@/api'
new Vue({
  render: h => h(App),
  //注册路由
  router,
  //注册仓库: 组件实例上就会多一个$store属性
  store,
  beforeCreate() {//注册全局时间总线
    Vue.prototype.$bus = this;
    Vue.prototype.$API = API;
  }
}).$mount('#app')

Trade.vue

    <div class="sub clearFix">
<!--      <router-link class="subBtn" to="/pay">提交订单</router-link>-->
      <a class="subBtn" @click="submitOrder">提交订单</a>
    </div>
//method-----------------------------------------------------------------
      async submitOrder(){
        /*需要携带的参数
        * tradeNo 交易编码
        * */
        let data={
          "consignee": this.addressList.consignee,//收件人名字
          "consigneeTel": this.addressList.phoneNum,//收件人手机
          "deliveryAddress": this.addressList.fullAddress,//收件人地址
          "paymentWay": "ONLINE",//支付方式
          "orderComment": this.message,//买家留言
          "orderDetailList": this.orderList.detailArrayList //商品清单
        }
        let result = await this.$API.reqSubmitOrder(this.orderList.tradeNo,data);
        if (result.code===200){
          this.orderId = result.data;
          //跳转到支付页面, 传递参数
          this.$router.push(`/pay?orderId=${this.orderId}`)
        }else {
          console.log('提交订单失败',result.message)
        }
      }

4,支付页面

接收参数

// eslint-disable-next-line vue/multi-word-component-names
name: 'Pay',
computed:{
  orderId(){
    return this.$route.query.orderId
  }
},

通过订单号获取支付信息

api

/*通过订单号获取支付信息
* URL:/api/payment/weixin/createNative/${orderId}
* method: get
* */
export const reqPayInfo = (orderId)=>request.get(`/payment/weixin/createNative/${orderId}`)

Pay.vue

<template>
  <div class="pay-main">
    <div class="pay-container">
      <div class="checkout-tit">
        <h4 class="tit-txt">
          <span class="success-icon"></span>
          <span class="success-info">订单提交成功,请您及时付款,以便尽快为您发货~~</span>
        </h4>
        <div class="paymark">
          <span class="fl">请您在提交订单<em class="orange time">4小时</em>之内完成支付,超时订单会自动取消。订单号:<em>{{orderId}}</em></span>
          <span class="fr"><em class="lead">应付金额:</em><em class="orange money">{{payInfo.totalFee}}</em></span>
        </div>
      </div>
      <div class="checkout-info">
        <h4>重要说明:</h4>
        <ol>
          <li>尚品汇商城支付平台目前支持<span class="zfb">支付宝</span>支付方式。</li>
          <li>其它支付渠道正在调试中,敬请期待。</li>
          <li>为了保证您的购物支付流程顺利完成,请保存以下支付宝信息。</li>
        </ol>
        <h4>支付宝账户信息:(很重要,<span class="save">请保存!!!</span></h4>
        <ul>
          <li>支付帐号:11111111</li>
          <li>密码:111111</li>
          <li>支付密码:111111</li>
        </ul>
      </div>
      <div class="checkout-steps">
        <div class="step-tit">
          <h5>支付平台</h5>
        </div>
        <div class="step-cont">
          <ul class="payType">
            <li><img src="./images/pay2.jpg"></li>
            <li><img src="./images/pay3.jpg"></li>
          </ul>

        </div>
        <div class="hr"></div>

        <div class="payshipInfo">
          <div class="step-tit">
            <h5>支付网银</h5>
          </div>
          <div class="step-cont">
            <ul class="payType">
              <li><img src="./images/pay10.jpg"></li>
              <li><img src="./images/pay11.jpg"></li>
              <li><img src="./images/pay12.jpg"></li>
              <li><img src="./images/pay13.jpg"></li>
              <li><img src="./images/pay14.jpg"></li>
              <li><img src="./images/pay15.jpg"></li>
              <li><img src="./images/pay16.jpg"></li>
              <li><img src="./images/pay17.jpg"></li>
              <li><img src="./images/pay18.jpg"></li>
              <li><img src="./images/pay19.jpg"></li>
              <li><img src="./images/pay20.jpg"></li>
              <li><img src="./images/pay21.jpg"></li>
              <li><img src="./images/pay22.jpg"></li>

            </ul>
          </div>

        </div>
        <div class="hr"></div>

        <div class="submit">
<!--          <router-link class="btn" to="/paysuccess">立即支付</router-link>-->
          <a class="btn" @click="pay">立即支付</a>
        </div>
        <div class="otherpay">
          <div class="step-tit">
            <h5>其他支付方式</h5>
          </div>
          <div class="step-cont">
            <span><a href="weixinpay.html" target="_blank">微信支付</a></span>
            <span>中国银联</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    // eslint-disable-next-line vue/multi-word-component-names
    name: 'Pay',
    data(){
      return {
        payInfo:{}
      }
    },
    computed:{
      orderId(){
        return this.$route.query.orderId
      }
    },
    mounted() {
      //不能在生命周期函数中使用async
      this.getPayInfo();
    },
    methods:{
      async getPayInfo(){
        let result =  await this.$API.reqPayInfo(this.orderId)
        if (result.code===200){
          this.payInfo = result.data;
        }else {
          alert(`获取支付信息失败!${result.message}`)
        }
      },
      pay(){
        
      }
    }
  }
</script>

5,ElmentUI按需引入

安装elementui

npm install element-ui

按需引入

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

首先,安装 babel-plugin-component:

npm install babel-plugin-component -D

然后,将 .babelrc 修改为:

新版脚手架叫babel.config.js

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
    //"presets": [["es2015", { "modules": false }]],es2015报错
    ["@babel/preset-env", { "modules": false }]
  ],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

main.js中引入

重启服务器

//按需引入ElementUI
import {Button, MessageBox} from 'element-ui'
Vue.use(Button);//注册全局组件
//另一种方式,挂在原型上
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;

测试

<el-button type="primary" class="el-icon-phone">测试</el-button>

image-20220514203127104

6, 弹框支付–Message Box

        <div class="submit">
<!--          <router-link class="btn" to="/paysuccess">立即支付</router-link>-->
          <a class="btn" @click="openPay">立即支付</a>
//method----------------------------------------------------------------------
      openPay(){
        this.$alert('这是一段内容', '标题名称', {
          confirmButtonText: '已支付成功',
          //中间布局
          center:true,
          //是否显示取消按钮
          showCancelButton:true,
          cancelButtonText:'支付遇见问题',//取消按钮的文本
          showClose:false,//右上角X

        });
      }

7,生成微信支付二维码(获取支付信息)

使用qrcode

npm i qrcode
import QRCode from 'qrcode'
let url = await QRCode.toDataURL(this.payInfo.codeUrl);

获取支付状态

api

/*通过订单号获取支付信息
* URL:/api/payment/weixin/queryPayStatus/${orderId}
* method: get
* */
export const reqPayStatus = (orderId)=>request.get(`/payment/weixin/queryPayStatus/${orderId}`)

Pay.vue

data(){
  return {
    payInfo:{},
    timer:null,//存放定时器
    isPay:false,//存放是否支付成功标识
  }
},
methods:{
  async openPay(){
    //生成二维码地址
    let url = await QRCode.toDataURL(this.payInfo.codeUrl);
    console.log(url)
    this.$alert(`<img src="${url}" />`, '标题名称', {
      dangerouslyUseHTMLString:true,//将 message 属性作为 HTML 片段处理
      confirmButtonText: '已支付成功',
      //中间布局
      center:true,
      //是否显示取消按钮
      showCancelButton:true,
      cancelButtonText:'支付遇见问题',//取消按钮的文本
      // showClose:false,//右上角X
      callback: ()=>{ //关闭后的回调
        //清除定时器
        clearInterval(this.timer);
        this.timer = null ;
      },
      beforeClose:(type,instance,done)=>{//关闭前的回调,会暂停实例的关闭
        /*type: 区分确定cancel|取消按钮 , 右上角X也是cancel
        * instance: 当前组件实例
        * done, 关闭函数*/
        if (type==='cancel'){
          alert('请联系管理员')
          done()
        }else {
          if (this.isPay){
            done();
            this.$router.push('/paysuccess');
            return
          }
          alert('支付失败, 请重试')
        }
      }
    });
    //支付成功路由跳转, 失败提示信息
    this.timer = setInterval(async()=>{//设置定时器, 定时查询支付状态
      let result = await this.$API.reqPayStatus(this.orderId);
      console.log(result)
      if (result.code === 200){
        //保存支付成功信息
        this.isPay = true;
        this.$msgbox.close();//关闭支付弹窗
        //清除定时器
        clearInterval(this.timer);
        this.timer = null ;
        //跳转到支付成功页面
        this.$router.push('/paysuccess');
      }
    },2000)

  }
}

支付效果

请添加图片描述

8,我的订单

1,组件拆分

引入组件

image-20220516003304221

支付成功跳转订单页面 PaySuccess.vue

      <div class="paydetail">
        <p class="button">
          <router-link class="btn-look" to="/center">查看订单</router-link>
          <router-link class="btn-goshop" to="/home">继续购物</router-link>
        </p>
      </div>

Center.vue , 右侧内容拆分到MyOrder组件中

<!-- 右侧内容 -->
<router-view></router-view>

2,注册路由

import Center from "@/pages/Center/Center";
//二级组件
import MyOrder from "@/pages/Center/children/MyOrder";
import GroupOrder from "@/pages/Center/children/GroupOrder";
//--------------------------------------------------------------
    {
        path: '/center',
        component: Center,
        meta:{show:true},
        redirect: '/center/my',//默认进入我的订单
        children:[
            {
                path:'my',
                component:MyOrder,
            },
            {
                path:'group',
                component:GroupOrder,
            }
        ]
    }

3,获取订单数据, 动态展示

api

/*通过我的订单数据
* URL:/api/order/auth/${page}/${limit}
* method: get
* */
export const reqMyOrders = (page,limit)=>request.get(`/order/auth/${page}/${limit}`)

MyOrder.vue

<template>
  <div class="order-right">
    <div class="order-content">
      <div class="title">
        <h3>我的订单</h3>
      </div>
      <div class="chosetype">
        <table>
          <thead>
          <tr>
            <th width="29%">商品</th>
            <th width="31%">订单详情</th>
            <th width="13%">收货人</th>
            <th>金额</th>
            <th>状态</th>
            <th>操作</th>
          </tr>
          </thead>
        </table>
      </div>
      <div class="orders">
        <!--每一笔订单-->
        <table class="order-item" v-for="record in myOrders.records" :key="record.id">
          <thead>
          <tr>
            <th colspan="5">
                      <span class="ordertitle">{{record.createTime}} 订单编号:{{record.outTradeNo}} <span
                          class="pull-right delete"><img src="../images/delete.png"></span></span>
            </th>
          </tr>
          </thead>
          <tbody>
          <tr v-for="(orderDetail,index) in record.orderDetailList" :key="orderDetail.id">
            <td width="60%">
              <div class="typographic">
                <img :src="orderDetail.imgUrl" style="height: 60px;height: 60px">
                <a href="#" class="block-text">{{ orderDetail.skuName }}</a>
                <span>{{ orderDetail.skuNum }}</span>
                <a href="#" class="service">售后申请</a>
              </div>
            </td>
            <!-- rowspan和并单元格, 只需要遍历第一次的数据, 所以v-if="index===0"-->
            <td  width="8%" class="center" :rowspan="record.orderDetailList.length" v-if="index===0">{{record.consignee || '暂无'}}</td>
            <td :rowspan="record.orderDetailList.length" v-if="index===0" width="13%" class="center">
              <ul class="unstyled">
                <li>总金额¥{{ record.totalAmount }}.00</li>
                <li>在线支付</li>
              </ul>
            </td>
            <td :rowspan="record.orderDetailList.length" v-if="index===0" width="8%" class="center">
              <a href="#" class="btn">{{ record.orderStatusName }} </a>
            </td>
            <td :rowspan="record.orderDetailList.length" v-if="index===0" width="13%" class="center">
              <ul class="unstyled">
                <li>
                  <a href="mycomment.html" target="_blank">评价|晒单</a>
                </li>
              </ul>
            </td>
          </tr>
          </tbody>
        </table>

      </div>
      <div class="choose-order">
        <!--分页器组件-->
        <Pagination
             :pageNo ='page'
             :pageSize="limit"
             :total="myOrders.total"
             :continues="5"
             @getPageNo="getPageNo"/>
      </div>
    </div>
    <!--猜你喜欢-->
    <div class="like">
      <h4 class="kt">猜你喜欢</h4>
      <ul class="like-list">
        <li class="likeItem">
          <div class="p-img">
            <img src="../images/itemlike01.png" />
          </div>
          <div class="attr">
            <em>DELL戴尔Ins 15MR-7528SS 15英寸 银色 笔记本</em>
          </div>
          <div class="price">
            <em>¥</em>
            <i>3699.00</i>
          </div>
          <div class="commit">已有6人评价
          </div>
        </li>
        <li class="likeItem">
          <div class="p-img">
            <img src="../images/itemlike02.png" />
          </div>
          <div class="attr">
            Apple苹果iPhone 6s/6s Plus 16G 64G 128G
          </div>
          <div class="price">
            <em>¥</em>
            <i>4388.00</i>
          </div>
          <div class="commit">已有700人评价
          </div>
        </li>
        <li class="likeItem">
          <div class="p-img">
            <img src="../images/itemlike03.png" />
          </div>
          <div class="attr">DELL戴尔Ins 15MR-7528SS 15英寸 银色 笔记本
          </div>
          <div class="price">
            <em>¥</em>
            <i>4088.00</i>
          </div>
          <div class="commit">已有700人评价
          </div>
        </li>
        <li class="likeItem">
          <div class="p-img">
            <img src="../images/itemlike04.png" />
          </div>
          <div class="attr">DELL戴尔Ins 15MR-7528SS 15英寸 银色 笔记本
          </div>
          <div class="price">
            <em>¥</em>
            <i>4088.00</i>
          </div>
          <div class="commit">已有700人评价
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: "MyOrder",
  data(){
    return{
      //初始化参数
      page:'1',
      limit:'3',
      myOrders:{}
    }
  },
  mounted() {
    //获取订单数据
    this.getMyOrders();
  },
  methods:{
    async getMyOrders(){
      let result = await this.$API.reqMyOrders(this.page,this.limit)
      if (result.code===200){
        this.myOrders = result.data;
      }else {
        alert('获取我的订单失败: ',result.message)
      }
    },
    getPageNo(page){//修改点击页数
      this.page = page;
      this.getMyOrders();
    }
  }
}
</script>

4,订单效果

vue-sgg项目-7-8-完成我的订单展示

八,完善项目

1,未登录的路由守卫判断

/router/index.js

/*全局前置守卫,  在路由跳转之前拦截
*  to: 获取跳转的路由信息
*  from: 从哪个路由来的信息
*  next: 放行函数
*       next()直接放行
*       next('/login')---放行到指定路由
*       next(false)   中断当前导航, 重置到 from路由地址
* */
router.beforeEach(async (to, from, next)=>{
    // console.log(to,from,next)
    let token = store.state.store_user.token;
    let name = store.state.store_user.userInfo.name
    let path = to.fullPath
    // console.log(token,name,path)
    if (token ){ //已登录情况
		//...
    }else {//未登录情况
        // console.log(to)
        //未登录不能去的地址
        let noPath = ['/trade','/center','/pay'];
        if (noPath.find(item=>path.indexOf(item)!==-1)){
            alert('您还没有登录, 请登录后重试');
            if (path.indexOf('/pay')===0){ //不保存pay页面
                next('/login');
                return
            }
            //将上次点击的页面保存参数传给login
            next('/login'+'?redirect='+to.path);
        }
        next()
    }
})

Login.vue

methods:{
  async login(){
    try {
      const {phone,password} = this
      if (phone!=='' && password!==''){
        await this.$store.dispatch('userLogin',{phone,password});
        //如果有传之前点击的页面,就跳转, 没有就跳转home
        let toPath = this.$route.query.redirect || '/home'
        await this.$router.push(toPath)
      }
    }catch (error){
      alert(error.message)
    }
  }
}

2,已登录路由守卫判断(独享守卫)

已登录用户不能直接进入的页面

  • 结算页面 - trade
  • 支付页面- pay
  • 只能从购物车shopcart点击结算跳转

router/routes.js

    {
        path: '/shopcart',
        component: ShopCart,
        meta:{show:true}
    },
    {
        path: '/trade',
        component: Trade,
        meta:{show:true},
        //路由独享守卫
        beforeEnter:(to,from,next)=>{
            //只能是购物车来的地址才放行, 或者当前页面(刷新)
            if (from.path === '/shopcart' || from.path==='/'){
                next()
            }else {
                next(false);//从哪来回哪去
                // next(from)
            }
        }
    },
    {
        path: '/pay',
        component: Pay,
        meta:{show:true},
        beforeEnter:(to,from,next)=>{
            console.log(to,from)
            //只能是结算页面来的地址才放行, 或者当前页面(刷新)
            if (from.path === '/trade' || from.path==='/'){
                next()
            }else {
                next(false);//从哪来回哪去
                // next(from)
            }
        }
    },
    {
        path: '/paysuccess',
        component: PaySuccess,
        meta:{show:true},
/*        beforeEnter:(to,from,next)=>{
            if (from.path === '/pay' || from.path==='/'){//只能是支付页面来的地址才放行, 或者当前页面(刷新)
                next()
            }else {
                next(false);//从哪来回哪去
                // next(from)
            }
        }*/
    },

3,组件内路由(不常用)

PaySuccess.vue

<script>
  export default {
    name: 'PaySuccess',
    beforeRouteEnter(to,from,next){
      /*渲染改组件对应路由被confirm 前调用
      * 不能获取组件实例this因为组件实例还未创建
      * */
      if (from.path==='/pay' || from.path==='/'){
        next();
      }else {
        next(false);
      }
    },
    beforeUpdate(to,from,next) {
      /*在当前路由改变, 但是该组件被复用时调用
      * 举例: 带有动态参数的路径: /aaa/:id  在 /aaa/1 和 /aaa/2 跳转时
      * 由于会渲染同一的aaa组件, 因此组件实例会被复用, 这是本钩子被调用
      * 可以访问组件实例,  this
      * */
      console.log('@@@',to,from,next)
    },
    beforeRouteLeave(to,from,next){
      //在当前路由改变,但是该组件被复用时调用
      //举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候,
      //由于会演染同样的Foo组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
      //可以访问组件实例'this'
      if (confirm('是否确认离开?')){
        next()
      }
    }
  }
</script>

4,图片懒加载

vue-lazyload - npm (npmjs.com)

#安装
npm i vue-lazyload

main.js入口文件使用

import VueLazyload from 'vue-lazyload';
 
Vue.use(VueLazyload);
import gif from '../assets/1.gif';//引入图片 
// or with options
Vue.use(VueLazyload, {
  //preLoad: 1.3,
  error:gif,//错误图片
  loading: gif,//只需要loading即可
  //attempt: 1
})

测试search.vue

<!-- 商品图片 -->
<div class="p-img">
    <router-link :to="`/detail/${goods.id}`" >
        <!--<img :src="goods.defaultImg" /> 换成v-lazy-->
        <img v-lazy="goods.defaultImg" />
            </router-link>
</div>

vue-sgg项目-8-4-图片懒加载

5,自定义插件

myplugins.js

//Vue插件一定暴露一个对象, 指定install方法
let myplugins = {};
myplugins.install = function (Vue,options){
    /*能获取Vue, 可以做很多事,options: 使用插件时传入的值
    * Vue.prototype.$bus: 任何组件都能使用
    * Vue.directive(); 指令 , 主要用来操作DOM
    * Vue.component 全局组件
    * Vue.filter..... 过滤器
    * */
    Vue.directive(options.name,(elment,params)=>{//全局指令
        /*elment: dom元素
        * params: 可以获取dom元素信息
        * */
        //将params值改成大小字母
        elment.innerHTML = params.value.toUpperCase();

    })

}
export default myplugins

main.js引入

//引入自定义插件
import myplugins from '@/plugins/myplugins';
Vue.use(myplugins,{
  name:'upper',//可以传入参数
});

App.vue测试

<div id="app">
  <h1 v-upper="msg"></h1>
//----------------------------------------------
name: 'App',
    data(){
    return{msg:'aaa'}
},

请添加图片描述

6,表单验证-Elment-UI

vee-validate - npm (npmjs.com)

npm i vee-validate
npm i vee-validate@3 //这里安装3

组件 | Element

1,main.js引入

//按需引入ElementUI
import { MessageBox,Button,Form,FormItem,Input,Select,Col,Row,Checkbox,Footer,Header,Main,Container} from 'element-ui';
Vue.use(Button);//注册全局组件
Vue.use(Form);//注册全局组件
Vue.use(Input);//注册全局组件
Vue.use(Select);//注册全局组件
Vue.use(FormItem);//注册全局组件
Vue.use(Col);//注册全局组件
Vue.use(Row);//注册全局组件
Vue.use(Checkbox);//注册全局组件
Vue.use(Footer);//注册全局组件
Vue.use(Header);//注册全局组件
Vue.use(Main);//注册全局组件
Vue.use(Container);//注册全局组件
//另一种方式,挂在原型上
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;

2,Register.vue

<template>
  <el-container>
      <!-- 注册内容 -->
    <el-main>
      <div class="register">
        <h3>注册新用户
          <span class="go">我有账号,去 <router-link to="/login">登陆</router-link>
        </span>
        </h3>
        <el-row type="flex" justify="center" class="row-bg">
          <el-form :model="ruleForm"
                   :rules="rules"
                   ref="ruleForm"
                   label-width="100px"
                   :inline="false"
                   class="reg-ruleForm">
            <el-form-item label="手机号" prop="phone"  >
              <el-input placeholder="请输入你的手机号"  v-model="ruleForm.phone"></el-input>
            </el-form-item>
            <el-form-item  label="验证码" prop="code"  :inline="true" >
              <el-row type="flex" justify="left" class="row-bg">
                <el-col :span="60">
                  <el-input  placeholder="请输入验证码"  v-model="ruleForm.code"></el-input>
                </el-col>
                <el-col :span="6">
                  <el-button
                      style="margin-left: 30px"
                      type="info"
                      icon="el-icon-message"
                      @click="getCode">验证码</el-button>
                </el-col>
              </el-row>
            </el-form-item>
            <el-form-item label="登录密码" prop="password"  >
              <el-input placeholder="请输入登录密码"  v-model="ruleForm.password"></el-input>
            </el-form-item>
            <el-form-item label="确认密码" prop="password2"  >
              <el-input placeholder="请输入确认密码"  v-model="ruleForm.password2"></el-input>
            </el-form-item>
            <el-form-item prop="isAgree"  >
              <el-checkbox v-model="ruleForm.isAgree" label="同意协议并注册《尚品汇用户协议》" name="type" ></el-checkbox>
            </el-form-item>
            <el-form-item>
              <el-button type="danger"
                         @click="submitRegForm('ruleForm')"
                         style="width: 150px;background-color: rgba(255,2,2,0.77)">完成注册</el-button>
              <el-button @click="resetForm('ruleForm')">重置</el-button>
            </el-form-item>
          </el-form>
        </el-row>

      </div>

    </el-main>
    <el-footer>
      <!-- 底部 -->
      <div class="copyright">
        <ul>
          <li>关于我们</li>
          <li>联系我们</li>
          <li>联系客服</li>
          <li>商家入驻</li>
          <li>营销中心</li>
          <li>手机尚品汇</li>
          <li>销售联盟</li>
          <li>尚品汇社区</li>
        </ul>
        <div class="address">地址:北京市昌平区宏福科技园综合楼6</div>
        <div class="beian">ICP19006430</div>
      </div>
    </el-footer>
  </el-container>
</template>

<script>
import debounce from 'lodash/debounce'
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Register',
  data() {
    let validatePhone = (rule, value, callback) => {
      let verify = /^1[34578]\d{9}$/;
      if (!value) {
        callback(new Error('请输入手机号'));
      } else if (!verify.test(value)) {
        callback(new Error('请输入正确格式手机号'));
      }
      callback()
    };
    let validateCode = (rule, value, callback) => {
      if (!value) {
        callback(new Error('请输入验证码'));
      } else {
        if (this.ruleForm.code !== this.$store.state.store_user.code) {
          callback(new Error('验证码输入错误'));
        }
        callback()
      }
    };
    let validatePwd = (rule, value, callback) => {
      if (!value) {
        callback(new Error('请输入密码'));
      } else {
        if (this.ruleForm.password.length < 6) {
          callback(new Error('请输入6位数以上密码'));
        }
        callback()
      }
    };
    let validatePwd2 = (rule, value, callback) => {
      if (!value) {
        callback(new Error('请输入确认密码'));
      } else {
        if (this.ruleForm.password!==this.ruleForm.password2) {
          callback(new Error('两次密码不一致'));
        }
        callback()
      }
    };
    let validateAgree = (rule, value, callback) => {
      if (!value) {
        callback(new Error('-----------请勾选协议'));
      } else {
        callback();
      }
    };
    return {
      ruleForm: {
        phone:'',//电话号码
        code:'',//验证码
        password:'',//密码
        password2:'',//确认密码
        isAgree:true,//是否同意协议
      },

      rules: {
        phone: [
          { required: true,  validator: validatePhone,trigger:'blur'},
          // { min: 11, max: 11, message: '长度在 11 个字符', trigger: 'blur' }
        ],
        code: [
          { required: true, validator: validateCode,trigger:'change'},
        ],
        password: [
          { required: true,  validator: validatePwd,trigger:'blur' }
        ],
        password2: [
          { required: true, validator: validatePwd2,trigger:'blur'  }
        ],
        isAgree: [
          { required: true, validator: validateAgree,trigger:'change' }
        ],
      }
    };
  },
  methods: {
    //注册提交
    submitRegForm(formName) {
      this.$refs[formName].validate(async (valid) => {
        if (valid) {
          console.log('!!')
          try {
            const {phone,code,password} =this.ruleForm;
            await this.$store.dispatch('userRegister',{phone,code,password})
            this.$router.push('/login');//注册成功跳转到登录
            return
          }catch (error){
            alert(error.message)
          }
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    //重置
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
    //获取验证码, 防抖
    getCode:debounce(async function (){
      try {
        //如果获取到手机号再继续往下走
        if (this.ruleForm.phone==='') return alert('请输入手机号')
        await this.$store.dispatch('getCode',this.ruleForm.phone)
        this.ruleForm.code = this.$store.state.store_user.code
      }catch (error){
        alert(error.message)
      }
    },1000),


  }

}
</script>
<style lang="less" scoped>
.register {
  width: 1200px;
  height: 455px;
  border: 1px solid rgb(223, 223, 223);
  margin: 0 auto;

  h3 {
    background: #ececec;
    margin: 20px;
    padding: 6px 15px;
    color: #333;
    border-bottom: 1px solid #dfdfdf;
    font-size: 20.04px;
    line-height: 30.06px;

    span {
      font-size: 14px;
      float: right;

      a {
        color: #e1251b;
      }

    }
  }


  .content {
    padding-left: 0px;
    margin-bottom: 18px;
    position: relative;


    label {
      font-size: 14px;
      width: 96px;
      text-align: right;
      display: inline-block;
    }

    input {
      width: 270px;
      height: 38px;
      padding-left: 8px;
      box-sizing: border-box;
      margin-left: 5px;
      outline: none;
      border: 1px solid #999;
    }

    img {
      vertical-align: sub;
    }

    .error-msg {
      position: absolute;
      top: 100%;
      left: 555px;
      color: red;
    }
  }

  .controls {
    text-align: center;
    position: relative;

    input {
      vertical-align: middle;
    }

    .error-msg {
      position: absolute;
      top: 100%;
      left: 495px;
      color: red;
    }
  }

  .btn {
    text-align: center;
    line-height: 36px;
    margin: 17px 0 0 55px;

    button {
      outline: none;
      width: 270px;
      height: 36px;
      background: #e1251b;
      color: #fff !important;
      display: inline-block;
      font-size: 16px;
    }
  }
}
.copyright {
  width: 1200px;
  margin: 0 auto;
  text-align: center;
  line-height: 24px;

  ul {
    li {
      display: inline-block;
      border-right: 1px solid #e4e4e4;
      padding: 0 20px;
      margin: 15px 0;
    }
  }
}
</style>

3,效果

vue-sgg项目-8-6-表单验证

7,路由懒加载

当打包构建应用时, JavaScript包会变得非常大,影响页面加载。

如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

结合Vue的异步组件和Webpack的代码分割功能,轻松实现路由组件的懒加载。

routes.js

把直接引入换成访问时引入

//引入路由组件
// import Home from "@/pages/Home/Home";
let Home = ()=>{
    return import("@/pages/Home/Home");
}
//简写
let Home = ()=> import("@/pages/Home/Home");
//再简写
    {
        path:'/home',
        component:()=> import("@/pages/Home/Home"),
        meta:{show:true}
    },

全改

/*如果我们能把不同路由对应的组件分割成不同的代码块,
*然后当路由被访问的时候才加载对应组件,这样就更加高效了。
*/
export default [
    {
        path:'/home',
        component:()=> import("@/pages/Home/Home"),
        meta:{show:true}
    },
    {
        path:'/login',
        component:()=> import("@/pages/Login/Login"),
        meta:{show:false}

    },
    {
        path:'/register',
        component:()=> import("@/pages/Register/Register"),
        meta:{show:false}
    },
    {
        path:'/search/:keyword?',
        component:()=> import("@/pages/Search/Search"),
        meta:{show:true},
        name:'search',
        //路由组件能不能传递props数据
        //布尔值写法: 只能传递params参数
        // props:true
        //对象写法:额外的给路由组件传递一些props参数
        // props:{a:1,b:2}
        //函数写法(常用),可以接收params参数,query参数,通过props传递
        /*            props:($route)=>{
                        return {keyword:$route.params.keyword,k:$route.query.k}
                    }*/
        //简写
        // props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})
    },
    {//重定向, /访问首页
        path:'*',
        // component:Home,
        redirect:'/home',//重定向到home
        meta:{show:true}
    },
    {
        path:'/detail/:skuId',//查询商品详细需要传递id
        component:()=> import("@/pages/Detail/Detail"),
        meta:{show:true}
    },
    {
        path: '/addcartsuccess',
        name: 'addcartsuccess',
        component: ()=> import("@/pages/AddCartSuccess/AddCartSuccess"),
        meta:{show:true}
    },
    {
        path: '/shopcart',
        component: ()=> import("@/pages/ShopCart/ShopCart"),
        meta:{show:true}
    },
    {
        path: '/trade',
        component: ()=> import("@/pages/Trade/Trade"),
        meta:{show:true},
        //路由独享守卫
        beforeEnter:(to,from,next)=>{
            console.log(to,from)
            //只能是购物车来的地址才放行, 或者当前页面(刷新)
            if (from.path === '/shopcart' || from.path==='/'){
                next()
            }else {
                next(false);//从哪来回哪去
                // next(from)
            }
        }
    },
    {
        path: '/pay',
        component: ()=> import("@/pages/Pay/Pay"),
        meta:{show:true},
        beforeEnter:(to,from,next)=>{
            console.log(to,from)
            if (from.path === '/trade' || from.path==='/'){//只能是结算页面来的地址才放行, 或者当前页面(刷新)
                next()
            }else {
                next(false);//从哪来回哪去
                // next(from)
            }
        }
    },
    {
        path: '/paysuccess',
        component: ()=> import("@/pages/PaySuccess/PaySuccess"),
        meta:{show:true},
/*        beforeEnter:(to,from,next)=>{
            if (from.path === '/pay' || from.path==='/'){//只能是支付页面来的地址才放行, 或者当前页面(刷新)
                next()
            }else {
                next(false);//从哪来回哪去
                // next(from)
            }
        }*/
    },
    {
        path: '/center',
        component: ()=> import("@/pages/Center/Center"),
        meta:{show:true},
        redirect: '/center/my',//默认进入我的订单
        children:[
            {
                path:'my',
                component:()=> import("@/pages/Center/children/MyOrder"),
            },
            {
                path:'group',
                component:()=> import("@/pages/Center/children/GroupOrder"),
            }
        ]
    }
]

九,打包上线

1,打包

vue-cli-service build

生成dist目录

image-20220518000726095

发现生成了很多map文件

image-20220518001354272

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

所以该文件如果项目不需要是可以去除掉

2,去除打包生成的map文件

  • 可以减少体积

vue.config.js 配置

productionSourceMap:false

重新打包

vue-cli-service build

3,nginx部署上线

安装nginx

#下载安装包
wget http://nginx.org/download/nginx-1.20.2.tar.gz
#安装所需依赖
yum -y install pcre pcre-devel zlib zlib-devel openssl openssl-devel
#解压
tar -zxvf nginx-1.20.2.tar.gz
#进入nginx目录
cd nginx-1.20.2
#预编译
#--prefix是指定安装目录
./configure --prefix=/opt/nginx
#编译
make
#安装
make install
#运行nginx
cd /opt/nginx/sbin/
./nginx
#查看进程
ps -ef|grep nginx
root     115792      1  0 14:40 ?        00:00:00 nginx: master process ./nginx
nobody   115793 115792  0 14:40 ?        00:00:00 nginx: worker process
root     116079  15539  0 14:40 pts/2    00:00:00 grep --color=auto nginx
#无法访问关闭防火墙或者开放端口
systemctl stop firewalld.service                       #临时防火墙
systemctl disable firewalld.service                   #永久关闭防火墙
#开放80端口
firewall-cmd --zone=public --add-port=80/tcp --permanent      # 开放80端口
firewall-cmd --zone=public --remove-port=80/tcp --permanent   #关闭80端口
firewall-cmd --reload                                         # 配置立即生效
firewall-cmd --zone=public --list-ports                  #查看防火墙开放的端口

#启动
/opt/nginx/sbin/nginx
#停止服务
/opt/nginx/sbin/nginx -s quit 
#重新加载配置
/opt/nginx/sbin/nginx -s reload

image-20220518003446447

  • 安装成功访问测试

1,将构建的dist目录放到喜欢的路径下

我这里放home

image-20220518004705338

2,编辑nginx配置文件

#根据安装目录下的conf
vim /opt/nginx/conf/nginx.conf

修改ngxin.conf—>http—server–>

        location / {
            root   /home/dist;
            index  index.html;
			try_files $uri $uri/ /index.html;
        }
		location /api {
			proxy_pass http://gmall-h5-api.atguigu.cn;
        }
总文件ngxin.conf
user  root;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   /home/dist;
            index  index.html;
			try_files $uri $uri/ /index.html;
        }
		location /api {
			proxy_pass http://gmall-h5-api.atguigu.cn;
        }

        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

3,重启nginx,测试

#重新加载配置
/opt/nginx/sbin/nginx -s reload
#停止服务
/opt/nginx/sbin/nginx -s quit
#启动
/opt/nginx/sbin/nginx

4,无法访问-报错Uncaught ReferenceError

image-20220518011905849

  • 查阅资料是组件中使用到了async/await
  • Babel在转化的问题
#安装
npm install --save-dev @babel/plugin-transform-runtime

main.js中引入

//不清楚是不是必须的, 我加了
import 'babel-polyfill'

babel.config.js添加配置

  • 注意放的位置
  • 'plugins': ['@babel/plugin-transform-runtime']

查半天资料, 位置没放对, 导致一直报plugins错

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
    //"presets": [["es2015", { "modules": false }]],es2015报错
    ["@babel/preset-env", { "modules": false }]
  ],
  "plugins": [
      //解决报错Uncaught ReferenceError: regeneratorRuntime is not defined
      '@babel/plugin-transform-runtime',
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      },
    ]
  ],


}

5,访问成功

vue-sgg项目-9-4-部署上线

6,去除地址栏#号-history模式

1,在路由主文件下添加mode=history

默认是hash

mode:'history',

请添加图片描述

2,重新发布

vue-cli-service build

请添加图片描述

  • 放到home目录下, 这里就叫shop_history

3, 修改nginx配置文件

/opt/nginx/sbin/nginx -s quit #退出nginx
vim /opt/nginx/conf/nginx.conf
nginx.conf
user  root;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   /home/shop_history;
            index  index.html;
            #指向@router
			try_files $uri $uri/ @router;
        }
        #主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件
		#因此需要rewrite到index.html中,然后交给路由在处理请求资源
		location @router {
			rewrite ^.*$ /index.html last;
		}
		location /api {
			proxy_pass http://gmall-h5-api.atguigu.cn;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

4,访问测试

#启动nginx
/opt/nginx/sbin/nginx

vue-sgg项目-9-6-地址栏去除#

  • 刷新无问题

7,docker快速部署

1,安装docker

#安装常用的安装包
yum install -y bash-completion vim lrzsz wget expect net-tools nc nmap tree dos2unix htop iftop iotop unzip telnet sl psmisc nethogs glances bc ntpdate openldap-devel -y
#获取yum源
rm -f /etc/yum.repos.d/*  
curl -o /etc/yum.repos.d/docker-ce.repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
curl -o /etc/yum.repos.d/Centos-7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
#清空yum缓存
yum clean all
#生成新缓存
yum makecache
#清空系统规则
iptables -F
3.关闭selinux
setenforce 0 # 临时关闭
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config # 永久关闭
systemctl disable firewalld && systemctl stop firewalld


#modprobe:用于向内核中加载模块或者从内核中移除模块。
modprobe br_netfilter
cat <<EOF > /etc/sysctl.d/docker.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.all.rp_filter = 0
net.ipv4.ip_forward=1
EOF
#卸载
yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
##yum安装
yum install docker-ce -y


#配置镜像加速
sudo mkdir -p /etc/docker

#2.编写配置文件
sudo tee /etc/docker/daemon.json <<-'EOF'
{
    
  "registry-mirrors": ["https://8xpk5wnt.mirror.aliyuncs.com"]
}
EOF

#3.重启服务
sudo systemctl daemon-reload
sudo systemctl restart docker

#查看docker启动状态
docker version

2,启动nginx镜像

docker volume create nghtml
docker volume create ngconf



docker run -dp 80:80 --name nginx-1 --mount source=nghtml,target=/home/nginx/ --mount source=ngconf,target=/etc/nginx nginx

cp shop/ /var/lib/docker/volumes/nghtml/_data

3,替换nginx.conf

echo 1 > /var/lib/docker/volumes/ngconf/_data/nginx.conf
vim /var/lib/docker/volumes/ngconf/_data/nginx.conf
#sudo tee /var/lib/docker/volumes/ngconf/_data/nginx.conf << EOF 
user  root;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   /home/nginx/shop;
            index  index.html;
            #指向@router
                        try_files $uri $uri/ @router;
        }
        #主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件
                #因此需要rewrite到index.html中,然后交给路由在处理请求资源
                location @router {
                        rewrite ^.*$ /index.html last;
                }
                location /api {
                        proxy_pass http://gmall-h5-api.atguigu.cn;
        }

        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
#EOF

4,放入项目shop

cp -r shop /var/lib/docker/volumes/nghtml/_data
cp nginx.conf /var/lib/docker/volumes/ngconf/_data/nginx.conf
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值