vue init webpack 项目名

 

=================

vue create 项目名

 

--open  项目启动后,自动浏览器打开

 

eslint校验功能关闭:

根目录下创建文件vue.config.js:

module.exports = {
  //关闭eslint
  lintOnSave:false
}

 不关闭eslint现象:

 

src文件夹简写方法,配置别名

根目录下创建jsconfig.json配置别名@提示「@代表src文件夹,后面文件太多方便查找」

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

项目路由分析

路由组件:

  Home首页路由组件,Search路由组件,login登录路由,Register注册路由

非路由组件:

  Header「首页,搜索页」

  Footer「首页,搜索页」,但是登录,注册页没有

Header和Footer非路由组件完成

  项目采用less样式,浏览器不识别,需使用less less-loader「版本5」进行处理(把less样式变为css样式,浏览器才可以识别)

  让组件识别less样式,需要在style标签加上lang=less

<style scoped lang="less">

  npm install --save less less-loader@5

  使用组件步骤(非路由组件):创建或定义--引入--注册--使用

<template>
    <div>
      <Header></Header>
        我是根组件
        <Footer></Footer>
    </div>
</template>

<script>
//引入
import Header from '@/components/Header';
import Footer from '@/components/Footer';
export default {
  name: '',
  components: {
    Header,
    Footer,
  },
};
</script>

路由组件搭建:Home,Search,Login,Register

安装vue-router:  npm install --save vue-router

  components:一般放置非路由组件

 

 

  pages | views:一般放置路由组件

 配置路由:

  一般放置在router文件夹中

   src下创建文件夹router--index.js

//配置路由的地方
import Vue from 'vue';
import VueRouter from 'vue-router';

//使用插件
Vue.use(VueRouter);
//引入路由组件
import Home from '@/pages/Home';
import Search from '@/pages/Search';
import Login from '@/pages/Login';
import Register from '@/pages/Register';

//配置路由
export default new VueRouter({
    routes:[
        {
            path:"/home",
            component:Home,
        },
        {
            path:"/search",
            component:Search,
        },
        {
            path:"/login",
            component:Login,
        },
        {
            path:"/register",
            component:Register,
        },
    ]
})

  src下main.js文件引入路由并注册

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

//引入路由
import router from '@/router';

new Vue({
  render: h => h(App),
  //注册路由:下面的写法KV一致省略V「router小写」
  //注册路由信息:router表示组件身上同时拥有$route和$router属性
  router
}).$mount('#app')

  src下APP.vue配置出口:router-view

<template>
    <div>
      <Header></Header>
        <!-- 路由组件出口的地方 -->
        <router-view></router-view>
        <Footer></Footer>
    </div>
</template>

<script>
//引入
import Header from '@/components/Header';
import Footer from '@/components/Footer';
export default {
  name: '',
  components: {
    Header,
    Footer,
  },
};

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

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

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

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

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

  项目启动后,重定向到首页

  src下router文件夹--index.js:

    {
            path: '*',
            redirect: '/home'
        },

路由跳转有两种方式:

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

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

编程式导航:声明式导航能做的,编程式导航也能做;编程式导航除了可以路由跳转,还可以做一些其他业务逻辑(比如点击确认登录的时候还可以收集用户名密码等信息)

 

 

Footer组件的显示与隐藏(路由元信息)

  Footer组件:在登录,注册的时候隐藏(v-if | v-show)v-if是操作dom,v-show是操作样式

 

<template>
  <div>
    <Header></Header>
    <!-- 路由组件出口的地方 -->
    <router-view></router-view>
    <!-- 在Home,Search显示,在登录Login,注册Register隐藏 -->
    <Footer
      v-show="$route.path == '/home' || $route.path == '/search'"
    ></Footer>
  </div>
</template>

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

若组件太多此种方法则行不通,可使用路由元信息:

 

 

 

 

配置路由的时候,可以给路由添加路由元信息「meta」,路由需要配置对象,key值不可以乱写

路由传参

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

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

写法一:字符串形式

 

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

 

 写法二:模板字符串

 

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

写法三:对象(常用)

  src下router文件夹--index.js:

 

 

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

input框输入:qwe点击搜索:

 

//面试题1:路由传递参数(对象写法)path是否可以结合params参数一起使用
//答:路由跳转传参的时候,对象的写法可以是name,path形式,但是path不能与params参数一起使用

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

 

 

//面试题2:如何指定params参数可传不可传
//如果路由要求传递params参数,可以传递或者不传递,在配置路由的时候,在占位的后面加上一个问号

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

 

 

//面试题3:params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
//使用undefined解决:params参数可以传递,不传递(空的字符串)

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

 

//面试题4:路由组件能不能传递props数据?
//可以:三种写法

  写法一:

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

 

 

 接收:

结果:

 

  写法二:

props:{a:1,b:2},

 

 

 

 

结果: 

   写法三:

props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})

 

结果:

 

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

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

声明式导航无此问题,因为vue-router底层已经处理了

 

解决方法:

this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}},()=>{},()=>{});

此方法治标不治本,后期在别的组件中push | replace,编程式导航还是会有类似的问题。

完美解法:

//先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;

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

 

 

 src下components目录下index.vue:

 

 

三级联动组件完成

三级联动在Home,Search,Detail,把三级联动注册为全局组件(只注册一次,可在项目任意地方使用)

  src下创建pages--Home--TypeNav--index.vue

 

main.js注册全局组件

//三级联动组件--全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件的名字,第二个参数:哪一个组件
Vue.component(TypeNav.name, TypeNav);

 

   src下pages--Home--index.vue

 

结果:

 其余静态组件完成 html + css + 图片资源

1.列表

  src--pages--Home--ListContainer--index.vue

    src--pages--Home--在index.vue中引入组件

 

 结果:

 

2.今日推荐

  src--pages--Home--Recommend--index.vue

    src--pages--Home--在index.vue中引入组件

 

 结果:

 

3.商品排行

  src--pages--Home--Rank--index.vue

    src--pages--Home--在index.vue中引入组件

结果:

4.猜你喜欢

  src--pages--Home--Like--index.vue

    src--pages--Home--在index.vue中引入组件

结果:

 

5.家用电器和手机通讯(格式一样,写一个组件复用即可)

  src--pages--Home--Floor--index.vue

    src--pages--Home--在index.vue中引入组件

结果:

 

6.商标

  src--pages--Home--Brand--index.vue

    src--pages--Home--在index.vue中引入组件

结果:

 

postman测试接口:http://39.98.123.211/api/product/getBaseCategoryList

返回200,成功

axios二次封装

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

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

  响应拦截器,当服务器数据返回后处理一些业务

npm install --save axios

src下创建文件夹api--创建文件request.js

//axios二次封装
import axios from "axios";

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

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

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

//对外暴露
export default requests;

 

接口统一管理

  src下api文件夹--创建文件index.js

//当前这个模块:api进行统一管理
import requests from "./request";
//三级联动接口
// /api/product/getBaseCategoryList     无参数  get请求
//发请求:axios发请求返回结果Promise对象
export const reqCategoryList = ()=>requests({url:'/product/getBaseCategoryList',method:'get'});

 

  main.js文件中进行测试

   会引起跨域问题:

 

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

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

http://39.98.123.211  --后台服务器
可以通过JSONP,CROS,代理服务器解决
 
vue.config.js文件中添加
//代理跨域
        devServer: {
          proxy: {
            '/api': {
              target: 'http://39.98.123.211',
            //   pathRewrite: { '^/api': '' },
            },
          },
        },

 

nprogress进度条的使用

  npm install --save nprogress

start:进度条开始 done:进度条结束

//axios二次封装
import axios from "axios";

//引入进度条
import nprogress from "nprogress";
//引入进度条样式,start:进度条开始,done:进度条结束
import "nprogress/nprogress.css";

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

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

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

//对外暴露
export default requests;

 

vuex状态管理库

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

  npm install --save vuex

  src下创建文件夹(大仓库)store--文件index.js

import Vue from 'vue';
import Vuex from 'vuex';

//需要使用插件一次
Vue.use(Vuex);

//引入小仓库
import home from './home';
import search from './search';

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

 

  src下(大仓库)store--创建文件夹(小仓库)home--文件index.js

//home模块小仓库
//state:仓库存储数据的地方
const state = {b:1};
//mutations:修改state的唯一手段
const mutations = {};
//action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {};
//getters:理解为计算属性:用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,
}

  src下(大仓库)store--创建文件夹(小仓库)search--文件index.js

//search
//state:仓库存储数据的地方
const state = {a:1};
//mutations:修改state的唯一手段
const mutations = {};
//action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {};
//getters:理解为计算属性:用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,
}

结果:

三级联动组件TypeNav动态展示

  将src--pages--Home下的TypeNav移动到全局组件components下

  修改main.js文件的引入路径

 

  src--componments--TypeNav--index.vue

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

 

  src--store--home--index.js

//home模块小仓库

import { reqCategoryList } from "@/api";

//state:仓库存储数据的地方
const state = {
    //state中数据默认值不可乱写,服务器返回对象则写对象,服务器返回值为数组则写数组「根据接口返回值初始化」
    categoryList: [],
};
//mutations:修改state的唯一手段
const mutations = {
    CATEGORYLIST(state, categoryList) {
        state.categoryList = categoryList;
    }
};
//action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {
    //通过api里面的接口函数调用,向服务器发请求,获取服务器数据
    async categoryList({ commit }) {
        let result = await reqCategoryList();
        if (result.code == 200) {
            commit("CATEGORYLIST", result.data);
        }
    }
};
//getters:理解为计算属性:用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,
}

 结果:可以拿到数组数据

 

  src--componments--TypeNav--index.vue

  computed: {
    ...mapState({
      //右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
      //注入一个参数state,其实即为大仓库中的数据
      categoryList: (state) => state.home.categoryList,
    }),

 

 结果:可以拿到数据

  src--componments--TypeNav--index.vue

<template>
  <div class="type-nav">
    <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,index) in categoryList" :key="c1.categoryId">
            <h3>
              <a href="">{{ c1.categoryName }}</a>
            </h3>
            <div class="item-list clearfix">
              <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                <dl class="fore">
                  <dt>
                    <a href="">{{ c2.categoryName }}</a>
                  </dt>
                  <dd>
                    <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                      <a href="">{{ c3.categoryName }}</a>
                    </em>
                    </em>
                  </dd>
                </dl>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

结果:

 

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

  方法一:添加样式  方法二:通过is

使用方法二,当鼠标引入分类的时候背景色变为天蓝色

  src--componments--TypeNav--index.vue

<template>
  <div class="type-nav">
    <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,index) in c1.categoryChild" :key="c2.categoryId">
                  <dl class="fore">
                    <dt>
                      <a href="">{{ c2.categoryName }}</a>
                    </dt>
                    <dd>
                      <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                        <a href="">{{ c3.categoryName }}</a>
                      </em>
                      </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,
    };
  },
  //组件挂载完毕,可以向服务器发请求
  mounted() {
    //通知Vuex发请求,获取数据,存储于仓库当中
    this.$store.dispatch("categoryList");
  },
  computed: {
    ...mapState({
      //右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
      //注入一个参数state,其实即为大仓库中的数据
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    //鼠标进入修改响应式数据currentIndex属性
    changeIndex(index) {
      //index:鼠标移上某一个一级分类的元素索引值
      this.currentIndex = index;
    },
    //一级分类鼠标移出的事件回调
    leaveIndex() {
      //鼠标移出currentIndex,变为-1
      this.currentIndex = -1;
    },
  },
};
</script>

 

样式中添加:

.cur {
  background: skyblue;
}

 

 js控制二三级分类显示与隐藏

  此处是通过css样式display:block|none显示与隐藏二三级分类,因为是通过js显示所以删除hover样式

 

  再在二三级分类添加

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

 

 

 

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

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

(背景:当鼠标快速从全部商品分类移动到底端,控制台不会全部执行每个tab)

防抖:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入lodash全部功能 -->
    <script src="./lodash.js"></script>
</head>
<body>
    <p>
        请输入要搜索的内容:<input type="text">
    </p>
</body>
<script>
    //防抖:前面的所有的触发都被取消,最后一次执行,在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行一次
    let input = document.querySelector('input');
    //文本发生变化时立即执行
    input.oninput = _.debounce(function(){
        console.log('ajax发请求')
    },1000);
</script>
</html>

文本框输入后,1秒后控制台打印

 

 节流:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./lodash.js"></script>
</head>
<body>
    <div>
        <h1>我是计数器<span>0</span></h1>
        <button>点击我加1</button>
    </div>
</body>
<script>
    //节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
    //获取节点
    let span = document.querySelector('span');
    let button = document.querySelector('button');
    let count = 0;
    //计数器:在一秒以内,数字只能加上1
    button.onclick = _.throttle(function(){
        //节流:目前这个回调函数3s执行一次,
        //假如这里面有很多的业务代码,可以给浏览器很充裕的时间解析
        count++;
        span.innerHTML = count;
        console.log('执行');
    },3000)
</script>
</html>

多次点击button,只有3秒后计数器才会加1

三级联动节流操作:运用lodash

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

// //此引用方式是把lodash全部函数引入
// import _ from "lodash";
//最好的引用方式,按需加载
import throttle from "lodash/throttle";

export default {
  name: "TypeNav",
  data() {
    return {
      //存储用户鼠标移上哪一个一级分类
      currentIndex: -1,
    };
  },
  //组件挂载完毕,可以向服务器发请求
  mounted() {
    //通知Vuex发请求,获取数据,存储于仓库当中
    this.$store.dispatch("categoryList");
  },
  computed: {
    ...mapState({
      //右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
      //注入一个参数state,其实即为大仓库中的数据
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    //鼠标进入修改响应式数据currentIndex属性
    //throttle回调函数别用箭头函数,可能出现上下文this
    changeIndex: throttle(function (index) {
      //index:鼠标移上某一个一级分类的元素索引值
      //正常情况(用户慢慢操作);鼠标进入,每一个一级分类h3,都会触发鼠标进入事件
      //非正常情况(用户操作过快):全部一级分类都应该触发鼠标进入事件,但是经测试,只有部分h3触发了
      //由于用户行为过快,呆滞浏览器反应不过来,如果当前回调函数中有一些大量业务,可能出现卡顿现象
      this.currentIndex = index;
    }, 50),
    //一级分类鼠标移出的事件回调
    leaveIndex() {
      //鼠标移出currentIndex,变为-1
      this.currentIndex = -1;
    },
  },
};
</script>

 

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

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

  路由跳转:声明式导航:router-link  编程式导航:push | replace

  三级联动:如果使用声明式导航router-link,可以实现路由跳转与传递参数。但是要注意会出现卡顿现象

  router-link:可以一个组件,当服务器的数据返回之后,循环出很多的router-link组件「创建组件实例的」1000+,因此出现卡顿

<template>
  <div class="type-nav">
    <div class="container">
      <!-- 事件委派|事件代理 -->
      <div @mouseleave="leaveIndex">
        <h2 class="all">全部商品分类</h2>
        <!-- 三级联动 -->
        <div class="sort">
          <!-- 利用事件委派+编程式导航实现路由跳转与传递参数 -->
          <div class="all-sort-list2" @click="goSearch">
            <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>
              </h3>
              <!-- 二,三级分类 -->
              <div class="item-list clearfix">
                <!-- <div class="item-list clearfix" :style="{dispaly:currentIndex==index?'block':'none'}"> -->
                <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                  <dl class="fore">
                    <dt>
                      <a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{ c2.categoryName }}</a>
                    </dt>
                    <dd>
                      <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                        <a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{ c3.categoryName }}</a>
                      </em>
                      </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";

// //此引用方式是把lodash全部函数引入
// import _ from "lodash";
//最好的引用方式,按需加载
import throttle from "lodash/throttle";

export default {
  name: "TypeNav",
  data() {
    return {
      //存储用户鼠标移上哪一个一级分类
      currentIndex: -1,
    };
  },
  //组件挂载完毕,可以向服务器发请求
  mounted() {
    //通知Vuex发请求,获取数据,存储于仓库当中
    this.$store.dispatch("categoryList");
  },
  computed: {
    ...mapState({
      //右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
      //注入一个参数state,其实即为大仓库中的数据
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    //鼠标进入修改响应式数据currentIndex属性
    //throttle回调函数别用箭头函数,可能出现上下文this
    changeIndex: throttle(function (index) {
      //index:鼠标移上某一个一级分类的元素索引值
      //正常情况(用户慢慢操作);鼠标进入,每一个一级分类h3,都会触发鼠标进入事件
      //非正常情况(用户操作过快):全部一级分类都应该触发鼠标进入事件,但是经测试,只有部分h3触发了
      //由于用户行为过快,呆滞浏览器反应不过来,如果当前回调函数中有一些大量业务,可能出现卡顿现象
      this.currentIndex = index;
    }, 50),
    //一级分类鼠标移出的事件回调
    leaveIndex() {
      //鼠标移出currentIndex,变为-1
      this.currentIndex = -1;
    },
    //进行路由跳转的方法
    goSearch() {
      //最好的解决方案:编程式导航 + 事件委派
      //利用事件委派存在一些问题:事件委派,是把全部的子节点「h3,dt,dl,em」的数据委派给父亲节点
      //点击a标签的时候,才会进行路由跳转「怎么能确定点击的一定是a标签」
      //另一个问题:即使你能确定点击的是a标签,如何区分是一级,二级,三级分类的标签

      //第一个问题:把子节点当中a标签,加上自定义属性data-categoryName,其余的子节点是没有的
      let element = event.target;
      //获取到当前触发这个时间的节点「h3,a,dt,dl」需要带有data-categoryname这样节点「一定是a标签」
      //节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
      let { categoryname, category1id, category2id, category3id } =
        element.dataset;
      //如果标签身上拥有categoryname一定是a标签
      if (categoryname) {
        let location = { name: "search" };
        let query = { categoryName: categoryname };
        //一级分类,二级分类,三级分类的a标签
        if (category1id) {
          query.category1id = category1id;
        } else if (category2id) {
          query.category2id = category2id;
        } else {
          query.category3id = category3id;
        }
        //整理好参数
        location.query = query;
        //路由跳转
        this.$router.push(location);
      }
    },
  },
};
</script>

Search模块中商品分类与过渡动画

  修改pages文件夹名称为views

  src下--views--Search--index.vue

    TypeNav为全局组件,所以不需要引入

 

  src下components--TypeNav--index.vue

<template>
  <div class="type-nav">
    <div class="container">
      <!-- 事件委派|事件代理 -->
      <div @mouseleave="leaveShow" @mouseenter="enterShow">
        <h2 class="all">全部商品分类</h2>
        <!-- 过渡动画 -->
        <transition name="sort">
          <div class="sort" v-show="show">
          <!-- 利用事件委派+编程式导航实现路由跳转与传递参数 -->
          <div class="all-sort-list2" @click="goSearch">
            <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>
              </h3>
              <!-- 二,三级分类 -->
              <div class="item-list clearfix">
                <!-- <div class="item-list clearfix" :style="{dispaly:currentIndex==index?'block':'none'}"> -->
                <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                  <dl class="fore">
                    <dt>
                      <a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{ c2.categoryName }}</a>
                    </dt>
                    <dd>
                      <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                        <a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{ c3.categoryName }}</a>
                      </em>
                      </em>
                    </dd>
                  </dl>
                </div>
              </div>
            </div>
          </div>
        </div>
        </transition>
      </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";

// //此引用方式是把lodash全部函数引入
// import _ from "lodash";
//最好的引用方式,按需加载
import throttle from "lodash/throttle";

export default {
  name: "TypeNav",
  data() {
    return {
      //存储用户鼠标移上哪一个一级分类
      currentIndex: -1,
      show: true,
    };
  },
  //组件挂载完毕,可以向服务器发请求
  mounted() {
    //通知Vuex发请求,获取数据,存储于仓库当中
    this.$store.dispatch("categoryList");
    //当组件挂载完毕,让show属性变为false
    //如果不是Home路由组件,将typeNav进行隐藏
    if (this.$route.path != "/home") {
      this.show = false;
    }
  },
  computed: {
    ...mapState({
      //右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
      //注入一个参数state,其实即为大仓库中的数据
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    //鼠标进入修改响应式数据currentIndex属性
    //throttle回调函数别用箭头函数,可能出现上下文this
    changeIndex: throttle(function (index) {
      //index:鼠标移上某一个一级分类的元素索引值
      //正常情况(用户慢慢操作);鼠标进入,每一个一级分类h3,都会触发鼠标进入事件
      //非正常情况(用户操作过快):全部一级分类都应该触发鼠标进入事件,但是经测试,只有部分h3触发了
      //由于用户行为过快,呆滞浏览器反应不过来,如果当前回调函数中有一些大量业务,可能出现卡顿现象
      this.currentIndex = index;
    }, 50),
    // //一级分类鼠标移出的事件回调
    // leaveIndex() {
    //   //鼠标移出currentIndex,变为-1
    //   this.currentIndex = -1;
    // },
    //进行路由跳转的方法
    goSearch() {
      //最好的解决方案:编程式导航 + 事件委派
      //利用事件委派存在一些问题:事件委派,是把全部的子节点「h3,dt,dl,em」的数据委派给父亲节点
      //点击a标签的时候,才会进行路由跳转「怎么能确定点击的一定是a标签」
      //另一个问题:即使你能确定点击的是a标签,如何区分是一级,二级,三级分类的标签

      //第一个问题:把子节点当中a标签,加上自定义属性data-categoryName,其余的子节点是没有的
      let element = event.target;
      //获取到当前触发这个时间的节点「h3,a,dt,dl」需要带有data-categoryname这样节点「一定是a标签」
      //节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
      let { categoryname, category1id, category2id, category3id } =
        element.dataset;
      //如果标签身上拥有categoryname一定是a标签
      if (categoryname) {
        let location = { name: "search" };
        let query = { categoryName: categoryname };
        //一级分类,二级分类,三级分类的a标签
        if (category1id) {
          query.category1Id = category1id;
        } else if (category2id) {
          query.category2Id = category2id;
        } else {
          query.category3Id = category3id;
        }
        //整理好参数
        location.query = query;
        //路由跳转
        this.$router.push(location);
      }
    },
    //当鼠标移入的时候,让商品分类列表进行展示
    enterShow() {
      if (this.$route.path != "/home") {
        this.show = true;
      }
    },
    //当鼠标离开的时候,让商品分类列表进行隐藏
    leaveShow() {
      this.currentIndex = -1;
      //判断如果是Search路由组件的时候才会执行
      if (this.$route.path != "/home") {
        this.show = false;
      }
    },
  },
};
</script>

  style中还需修改样式

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

 

则在search模块中实现鼠标移入逐渐显示的效果

typeNav商品分类列表优化

将src下components--TypeNav--index.vue中派发action剪切到App.vue根组件mounted中让其只执行一次

 

控制台只发一次请求:

合并query和params参数

//判断:如果路由跳转的时候,带有params参数,捎带着传递过去
        if (this.$route.params) {
          location.params = this.$route.params;
          //整理好参数
          location.query = query;
          //路由跳转
          this.$router.push(location);
        }

 

if (this.$route.query) {
        let location = { name: "search", params: { keyword: this.keyword } };
        location.query = this.$route.query;
        this.$router.push(location);
      }

 

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

  但是服务器返回的数据(接口)只有商品分类菜单数据,对于ListContainer组件与Floor组件服务器未通过

mockjs模拟数据

  1.npm install --save mockjs

  2.src下创建文件夹mock--banner.json和floor.json并粘贴JSON数据,需格式化一下,有空格跑不起来

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

  public文件夹下创建images文件夹,并复制图片进去

  4.开始mock虚拟数据,通过mockjs模块实现

  mock文件夹下创建mockServe.js文件通过mockjs插件实现模拟数据

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

//先引入mockjs模块
import Mock from "mockjs";

//把JSON数据格式引入进来「JSON数据格式根本没有对外暴露,但是可以引入」
//webpack默认对外暴露的:图片,JSON数据格式
import banner from "./banner.json";
import floor from "./floor.json";

//mock数据:第一个参数请求地址,第二个参数:请求数据
Mock.mock("/mock/banner", { code: 200, data: banner });//模拟首页顶部大的轮播图数据
Mock.mock("/mock/floor", { code: 200, data: floor });

main.js引入mock

 

获取Banner轮播图数据

  将src下--api--request.js文件改名为ajax.js并复制ajax.js文件为mockAjax.js并修改baseURL

   src--api--index.js

//当前这个模块:api进行统一管理
import requests from "./ajax";
import mockRequests from "./mockAjax";
//三级联动接口
// /api/product/getBaseCategoryList     无参数  get请求
//发请求:axios发请求返回结果Promise对象
export const reqCategoryList = () => requests({ url: '/product/getBaseCategoryList', method: 'get' });

//获取banner(Home首页轮播图接口)
export const reqGetBannerList = () => mockRequests.get('/banner');

  src--views--Home--ListContainer--index.vue中派发action

<script>
export default {
  name: "",
  mounted() {
    //派发action:通过Vuex发起ajax请求,将数据存储到仓库中
    this.$store.dispatch("getBannerList");
  },
};
</script>

  src--store--home--index.vue

//获取首页轮播图的数据
    async getBannerList(){
        let result = await reqGetBannerList();
        console.log(result);
    }

结果:控制台可以拿到数据

 

   src--store--home--index.vue

//home模块小仓库
import { reqCategoryList, reqGetBannerList } from "@/api";
//state:仓库存储数据的地方
const state = {
    //state中数据默认值不可乱写,服务器返回对象则写对象,服务器返回值为数组则写数组「根据接口返回值初始化」
    categoryList: [],
    //轮播图的数据
    bannerList: [],
};
//mutations:修改state的唯一手段
const mutations = {
    CATEGORYLIST(state, categoryList) {
        state.categoryList = categoryList;
    },
    GETBANNERLIST(state, bannerList) {
        state.bannerList = bannerList;
    }
};
//action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {
    //通过api里面的接口函数调用,向服务器发请求,获取服务器数据
    async categoryList({ commit }) {
        let result = await reqCategoryList();
        if (result.code == 200) {
            commit("CATEGORYLIST", result.data);
        }
    },
    //获取首页轮播图的数据
    async getBannerList({commit}) {
        let result = await reqGetBannerList();
        if (result.code == 200) {
            commit('GETBANNERLIST', result.data);
        }
    }
};
//getters:理解为计算属性:用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};

  src--views--Home--ListContainer--index.vue

<script>
import { mapState } from "vuex";
export default {
  name: "",
  mounted() {
    //派发action:通过Vuex发起ajax请求,将数据存储到仓库中
    this.$store.dispatch("getBannerList");
  },
  computed: {
    ...mapState({
      bannerList: (state) => state.home.bannerList,
    }),
  },
};
</script>

结果:bannerList组件有数据

 

swiper基本使用

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引包 -->
    <script src="./js/swiper.min.js"></script>
    <link rel="stylesheet" href="./css/swiper.min.css">
    <style>
        .swiper {
            width: 600px;
            height: 400px;
        }
    </style>
</head>


<body>
    <div class="swiper">
        <div class="swiper-wrapper">
            <div class="swiper-slide">Slide 1</div>
            <div class="swiper-slide">Slide 2</div>
            <div class="swiper-slide">Slide 3</div>
        </div>
        <!-- 如果需要分页器 -->
        <div class="swiper-pagination"></div>

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

</html>

<script>
    //页面已有的情况,初始化swiper实例
    //第一个参数,可以是字符串真是DOM节点
    var mySwiper = new Swiper(document.querySelector('.swiper'), {
        loop: true, // 循环模式选项

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

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

Banner实现轮播图

  安装swiper

  npm install --save swiper@5

  src--main.js引入swiper样式

//引入swiper样式
import "swiper/css/swiper.css";

  src--views--Home--ListContainer--index.vue引包

import Swiper from "swiper";

  添加定时器实现轮播图(1秒后才显示小圆圈)

//在new Swiper实例之前,页面中结构必须有「把Swiper实例放在mounted里发现不行,因为dispatch中涉及到异步语句,导致v-for遍历的时候结构不完全」
    //可以添加定时器解决,但是不推荐
    setTimeout(() => {
      var mySwiper = new Swiper(document.querySelector(".swiper-container"), {
        loop: true, // 循环模式选项

        // 如果需要分页器
        pagination: {
          el: ".swiper-pagination",
          //点击小球的时候页切换图片
          clickable: true,
        },

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

watch+nextTick实现完美解决轮播图

  完美解决方案watch+nextTick:数据监听,监听已有数据变化

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

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

  watch: {
    //监听bannerList数据的变化:因为这条数据发生过变化---有空数组变为数组里有四个元素
    bannerList: {
      handler(newValue, oldValue) {
        //现在通过watch监听bannerList属性的属性值变化
        //如果执行handler方法,代表组件实例身上这个属性的属性已经有了「数组四个元素」
        //当前这个函数执行:只能保证bannerList数据已经有了,但没法保证v-for已经执行结束
        //v-for执行完毕,才有结构「现在watch中没法保证」
        //nextTick:在下次DOM更新  循环结束之后 执行延迟回调,在 修改数据之后 立即使用这个方法,获取更新后的DOM
        this.$nextTick(() => {
          //当执行这个回调的时候,保证服务器数据回来了,v-for执行完毕了「轮播图的结构一定有了」
          var mySwiper = new Swiper(this.$refs.mySwiper, {
            loop: true, // 循环模式选项

            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              //点击小球的时候页切换图片
              clickable: true,
            },

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

 

 

  src--views--Home--ListContainer--index.vue修改下面

<div class="swiper-container" ref="mySwiper">

 

 则下面的回调也要修改

var mySwiper = new Swiper(this.$refs.mySwiper, {

获取floor组件mock数据

开发floor组件

  切记:仓库当中的state数据格式不可乱写,数据格式取决于服务器返回的数据

  1.getFloorList这个action在哪里触发:是需要在Home路由组件中发的,不能在Floor组件内部发action,因为需要v-for遍历floor组件

  2.v-for也可以在自定义标签中使用

  3.组件通信的方式有哪些?(面试重点)

    props:用于父子组件通信

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

    全局事件总线:$bus 全能

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

    插槽

    vuex

  

  src--api--index.js获取floor数据

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

  src--store--home--index.js

  getFloorList的action派发:src--views--Home--index.vue

mounted() {
    //派发action,获取floor组件的数据
    this.$store.dispatch("getFloorList");
  },

可以拿到服务器数据:

  src--views--Home--index.vue

<template>
  <div>
    <!-- 三级联动全局组件:三级联动已经注册为全局组件,因此不需要在引入 -->
    <TypeNav />
    <ListContainer />
    <Recommend />
    <Rank />
    <Like />
    <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor" />
    <Brand />
  </div>
</template>
<script>
//引入其余组件
import ListContainer from "@/views/Home/ListContainer";
import Recommend from "@/views/Home/Recommend";
import Rank from "@/views/Home/Rank";
import Like from "@/views/Home/Like";
import Floor from "@/views/Home/Floor";
import Brand from "@/views/Home/Brand";
import { mapState } from "vuex";
export default {
  name: "",
  components: {
    ListContainer,
    Recommend,
    Rank,
    Like,
    Floor,
    Brand,
  },
  mounted() {
    //派发action,获取floor组件的数据
    this.$store.dispatch("getFloorList");
  },
  computed: {
    ...mapState({
      floorList: (state) => state.home.floorList,
    }),
  },
};
</script>

 

 用props进行父子组件通信

  src--views--Home--Floor--index.vue

 

结果:

动态展示floor组件

  src--views--Home--Floor--index.vue

<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">
            <li
              class="active"
              v-for="(nav, index) in list.navList"
              :key="index"
            >
              <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="(keyword, index) in list.keywords" :key="index">
                  {{ keyword }}
                </li>
              </ul>
              <img :src="list.imgUrl" />
            </div>
            <div class="floorBanner">
              <!-- 轮播图的地方 -->
              <div class="swiper-container" ref="cur">
                <div class="swiper-wrapper">
                  <div
                    class="swiper-slide"
                    v-for="(carousel, index) in list.carouselList"
                    :key="carousel.id"
                  >
                    <img :src="carousel.imgUrl" />
                  </div>
                </div>
                <!-- 如果需要分页器 -->
                <div class="swiper-pagination"></div>

                <!-- 如果需要导航按钮 -->
                <div class="swiper-button-prev"></div>
                <div class="swiper-button-next"></div>
              </div>
            </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 {
  name: "",
  props: ["list"],
  //组件挂载完毕的地方
  mounted() {
    //ListContainer中第一次写轮播图的时候,在mounted当中书写是不可以的,但是在这里可以
    //第一次书写轮播图的时候,是在当前组件内部发请求,动态渲染结构「前台服务器数据需要回来」,因此写法不行
    //现在的这种写法:是父组件发的,父组件通过props传递过来的,而且结构已经有的情况下执行的mounted
    var mySwiper = new Swiper(this.$refs.cur, {
      loop: true, // 循环模式选项

      // 如果需要分页器
      pagination: {
        el: ".swiper-pagination",
        //点击小球的时候页切换图片
        clickable: true,
      },

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

公用组件carousel

  把首页当中轮播图拆分为一个公用全局组件

  src--views--Home--Floor--index.vue不在mounted中挂载了,而是在watch

<script>
import Swiper from "swiper";
export default {
  name: "",
  props: ["list"],
  //组件挂载完毕的地方
  mounted() {
    // //ListContainer中第一次写轮播图的时候,在mounted当中书写是不可以的,但是在这里可以
    // //第一次书写轮播图的时候,是在当前组件内部发请求,动态渲染结构「前台服务器数据需要回来」,因此写法不行
    // //现在的这种写法:是父组件发的,父组件通过props传递过来的,而且结构已经有的情况下执行的mounted
  },
  watch: {
    list: {
      //立即监听:不管数据有没有变化,立即监听一次
      //为何watch监听不到list:因为这个数据从来没有发生变化「数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的」
      immediate: true,
      handler() {
        //只能监听到数据已经有了,但是v-for动态渲染结构还是没有办法确定,因此还是需要用nextTick
        this.$nextTick(() => {
          var mySwiper = new Swiper(this.$refs.cur, {
            loop: true, // 循环模式选项

            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              //点击小球的时候页切换图片
              clickable: true,
            },

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

  src--views--Home--Floor--index.vue中的轮播图等信息剪贴到src--components--Carousel--index.vue

  并且将src--views-Home--Floor--index.vue中的watch代码剪贴到src--components--Carousel--index.vue使其成为全局组件

  此时src--components--Carousel--index.vue

<template>
  <div class="swiper-container" ref="cur">
    <div class="swiper-wrapper">
      <div
        class="swiper-slide"
        v-for="(carousel, index) in list"
        :key="carousel.id"
      >
        <img :src="carousel.imgUrl" />
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>

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

<script>
export default {
  name: "Carousel",
  props: ["list"],
  watch: {
    list: {
      //立即监听:不管数据有没有变化,立即监听一次
      //为何watch监听不到list:因为这个数据从来没有发生变化「数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的」
      immediate: true,
      handler() {
        //只能监听到数据已经有了,但是v-for动态渲染结构还是没有办法确定,因此还是需要用nextTick
        this.$nextTick(() => {
          var mySwiper = new Swiper(this.$refs.cur, {
            loop: true, // 循环模式选项

            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              //点击小球的时候页切换图片
              clickable: true,
            },

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

  回到入口文件main.js引入Carousel并注册

   src--views-Home--Floor--index.vue中只需要在轮播图的地方写一行代码即可

<Carousel :list="list.carouselList" />

  同样src--views-Home--ListContainer--index.vue中watch和轮播图干掉,只需要在轮播图的地方写一行

<Carousel :list="list.carouselList" />

  此时的src--components--Carousel为

<template>
  <div class="swiper-container" ref="cur">
    <div class="swiper-wrapper">
      <div
        class="swiper-slide"
        v-for="(carousel, index) in list"
        :key="carousel.id"
      >
        <img :src="carousel.imgUrl" />
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>

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

<script>
//引入Swiper
import Swiper from "swiper";
export default {
  name: "Carousel",
  props: ["list"],
  watch: {
    list: {
      //立即监听:不管数据有没有变化,立即监听一次
      //为何watch监听不到list:因为这个数据从来没有发生变化「数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的」
      immediate: true,
      handler() {
        //只能监听到数据已经有了,但是v-for动态渲染结构还是没有办法确定,因此还是需要用nextTick
        this.$nextTick(() => {
          var mySwiper = new Swiper(this.$refs.cur, {
            loop: true, // 循环模式选项

            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              //点击小球的时候页切换图片
              clickable: true,
            },

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

  search模块开发

    1.先静态页面 + 静态组件拆分出来

    2.发请求(API)

    3.vuex(三连环)

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

Search模块静态组件

  将静态组件Search文件夹替换掉(src--views--Search并将Search文件夹下的index.vue中的<TypeNav />改为<typeNav />)

search模块vuex操作

  src--api--index.js写搜索模块api

//获取搜索模块数据
/*
{
  "category3Id": "61",
  "categoryName": "手机",
  "keyword": "小米",
  "order": "1:desc",
  "pageNo": 1,
  "pageSize": 10,
  "props": ["1:1700-2799:价格", "2:6.65-6.74英寸:屏幕尺寸"],
  "trademark": "4:小米"
}
 */
//当前这个接口(获取搜索模块的数据),给服务器传递一个默认参数「至少是一个空对象」
export const reqGetSearchInfo = (params) => requests({ url: "/list", method: "post", data: params });

 

  入口文件main.js中测试(至少是一个空对象才会返回200)

 

 结果:

 

  src--store--search--index.js

import { reqGetSearchInfo } from "@/api";
//search模块仓库
//state:仓库存储数据的地方
const state = {
    //仓库初始状态
    searchList: {},
};
//mutations:修改state的唯一手段
const mutations = {
    GETSEARCHLIST(state, searchList) {
        state.searchList = searchList;
    }
};
//action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {
    //获取search模块数据
    async getSearchList({ commit }, params = {}) {
        //当前这个reqGetSearchInfo这个函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
        //params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
        let result = await reqGetSearchInfo(params);
        if (result.code == 200) {
            commit("GETSEARCHLIST", result.data);
        }
    }
};
//getters:理解为计算属性:用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,
}

  src--views--Search--index.vue进行测试接口

<script>
import SearchSelector from "./SearchSelector/SearchSelector";
export default {
  name: "Search",
  components: {
    SearchSelector,
  },
  mounted() {
    //先测试接口返回的数据格式
    this.$store.dispatch("getSearchList",{});
  },
};
</script>

 

  search模块产品列表动态展示

  src--store--search--index.js

//getters:理解为计算属性:用于简化仓库数据,让组件获取仓库的数据更加方便
//项目中getters的主要作用是:简化仓库中的数据「简化数据而生」
//可以把将来在组件中需要用得数据简化
const getters = {
    //当前这个形参state,当前仓库中的state,并非大仓库中的那个state
    goodsList(state) {
        //state.searchList.goodsList,如果服务器数据回来了,没问题是一个数组
        //假如网络不好或者没有网络state.searchList.goodsList应该返回的是undefined
        //计算新的属性的属性值至少来一个数组
        return state.searchList.goodsList || [];
    },
    trademarkList(state) {
        return state.searchList.trademarkList;
    },
    attrsList(state) {
        return state.searchList.attrsList;
    }
};

 

   src--views--Search--index.vue

<script>
import SearchSelector from "./SearchSelector/SearchSelector";
import { mapGetters } from "vuex";
export default {
  name: "Search",
  components: {
    SearchSelector,
  },
  mounted() {
    //先测试接口返回的数据格式
    this.$store.dispatch("getSearchList", {});
  },
  computed: {
    //mapGetters里面的写法:传递的数组,因为getters计算是没有划分模块的「home,search」
    ...mapGetters(["goodsList"]),
  },
};
</script>

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

<script>
import SearchSelector from "./SearchSelector/SearchSelector";
import { mapGetters } from "vuex";
export default {
  name: "Search",
  components: {
    SearchSelector,
  },
  data() {
    return {
      //带给服务器参数
      searchParams: {
        //一级分类id
        category1Id: "",
        //二级分类id
        category2Id: "",
        //三级分类id
        category3Id: "",
        //分类名字
        categoryName: "",
        //关键字
        keyword: "",
        //排序
        order: "",
        //分页器,代表当前是第几页
        pageNo: 1,
        //每一页展示的数据个数
        pageSize: 10,
        //平台售卖属性操作带的参数
        props: [],
        //品牌
        trademark: "",
      },
    };
  },
  //当组件挂载完毕之前执行一次「先于mounted之前」
  beforeMount() {
    //复杂的写法
    // this.searchParams.category1Id = this.$route.query.category1Id;
    // this.searchParams.category2Id = this.$route.query.category2Id;
    // this.searchParams.category3Id = this.$route.query.category3Id;
    // this.searchParams.categoryName = this.$route.query.categoryName;
    // this.searchParams.keyword = this.$route.params.keyword;
    //Object.assign:es6新增的语法,合并对象
    Object.assign(this.searchParams, this.$route.query, this.$route.params);
  },
  //组件挂载完毕执行一次「仅仅执行一次」
  mounted() {
    //再发请求之前带给服务器参数「searchParams参数发生变化有数值带给服务器」
    this.getData();
  },
  computed: {
    //mapGetters里面的写法:传递的数组,因为getters计算是没有划分模块的「home,search」
    ...mapGetters(["goodsList"]),
  },
  methods: {
    getData() {
      this.$store.dispatch("getSearchList", this.searchParams);
    },
  },
};
</script>

Search模块子组件动态开发

  src--views--Search--SearchSelector--SearchSelector.vue

<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="(trademark,index) in trademarkList" :key="trademark.tmId">{{ trademark.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,index) 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>

监听路由的变化再次发请求获取数据 

  //数据监听:监听组件实例身上的属性的属性值的变化
  watch: {
    //监听路由的信息是否发生变化,如果发生变化,再次发起请求
    $route(newValue, oldValue) {
      //再次发起请求之前整理的带给服务器参数
      Object.assign(this.searchParams, this.$route.query, this.$route.params);
      //再次发起ajax请求
      this.getData();
      //每次请求完毕,应该把相应的1,2,3级分类id置空,让接收下一次相应的1,2,3
      //分类名与关键字不用清理,因为每一次路由发生变化的时候,都会给他赋予新的数据
      this.searchParams.category1Id = "";
      this.searchParams.category2Id = "";
      this.searchParams.category3Id = "";
    },
  },

 

面包屑处理分类

<ul class="fl sui-tag">
            <li class="with-x" v-if="searchParams.categoryName">
              {{ searchParams.categoryName
              }}<i @click="removeCategoryName">×</i>
            </li>
          </ul>

 

    //删除分类名字
    removeCategoryName() {
      //把服务器的参数置空了,还需要向服务器发请求
      //带给服务器参数说明可有可无:如果属性值为空的字符串还是会把相应的字段带给服务器
      //但是把相应的字段变为undefined,当前这个字段不会带给服务器
      this.searchParams.categoryName = undefined;
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
      this.getData();
      //地址栏也需要修改:进行路由跳转「现在的路由跳转只是跳转到自己这里」
      //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
      if (this.$route.params) {
        this.$router.push({ name: "search", params: this.$route.params });
      }
    },

 

面包屑处理关键字

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

  main.js中配置全局事件总线$bus

  //全局事件总线$bus配置
  beforeCreate() {
    Vue.prototype.$bus = this;
  },

 

  回到src--components--Header--index.vue中

  //通过全局事件总线清除关键字
  mounted() {
    this.$bus.$on("clear", () => {
      this.keyword = "";
    });
  },

  再回到src--views--Search--index.vue中写关键字的面包屑

<li class="with-x" v-if="searchParams.keyword">
              {{ searchParams.keyword }}<i @click="removeKeyword">×</i>
            </li>

  并写removeKeyword方法

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

面包屑处理品牌信息 

   src--views--Search--SearchSelector--SearchSelector.vue

<script>
import { mapGetters } from "vuex";
export default {
  name: "SearchSelector",
  computed: {
    ...mapGetters(["trademarkList", "attrsList"]),
  },
  methods: {
    //品牌的事件处理函数
    trademMarkHandler(trademark) {
      // console.log(trademark);
      //点击了品牌(例如点击苹果),还是需要整理参数,向服务器发请求获取相应的数据进行展示
      //在父组件中发请求,因为父组件中searchParams参数是带给服务器参数,子组件把点击的品牌信息需要给父组件传递过去---自定义事件
      this.$emit("trademarkInfo", trademark);
    },
  },
};
</script>

  src--views--Search--index.vue

//自定义事件回调
    trademarkInfo(trademark) {
      //整理品牌字段的参数  "ID:品牌名称"
      // console.log("父组件",trademark);
      this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
      //再次发请求获取search模块列表数据进行展示
      this.getData();
    },
    //删除品牌的信息
    removeTradeMark() {
      //将品牌信息置空
      this.searchParams.trademark = undefined;
      //再次发请求
      this.getData();
    },

平台售卖属性的操作

  src--views--Search--SearchSelector--SearchSelector.vue

//平台售卖属性的操作
    attrInfo(attr, attrValue) {
      //["属性ID:属性值:属性名"]
      this.$emit("attrInfo", attr, attrValue);
    },

  src--views--Search--index.vue

//收集平台属性的回调函数(自定义事件)
    attrInfo(attr, attrValue) {
      //["属性ID:属性值:属性名"]
      console.log(attr, attrValue);
      //参数格式整理
      let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
      //if语句里面只有一行代码:可以省略花括号
      if (this.searchParams.props.indexOf(props) == -1)
        this.searchParams.props.push(props);
      //再次发请求
      this.getData();
    },
    //removeAttr删除售卖属性
    removeAttr(index) {
      //再次整理参数
      this.searchParams.props.splice(index, 1);
      //再次发请求
      this.getData();
    },

排序操作上

  public--index.html中引入阿里矢量图标库

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

  src--views--Search--index.vue

<div class="sui-navbar">
            <div class="navbar-inner filter">
              <!-- 排序的结构 -->
              <ul class="sui-nav">
                <li :class="{ active: isOne }">
                  <a>综合<span v-show="isOne" class="iconfont" :class="{'icon-shangjiantou-cu':isAsc,'icon-xiajiantou-cu':isDesc}"></span></a>
                </li>
                <li :class="{ active: isTwo }">
                  <a>价格<span v-show="isTwo" class="iconfont" :class="{'icon-shangjiantou-cu':isAsc,'icon-xiajiantou-cu':isDesc}"></span></a>
                </li>
              </ul>
            </div>
          </div>

    isOne() {
      return this.searchParams.order.indexOf("1") != -1;
    },
    isTwo() {
      return this.searchParams.order.indexOf("2") != -1;
    },
    isAsc() {
      return this.searchParams.order.indexOf("asc") != -1;
    },
    isDesc() {
      return this.searchParams.order.indexOf("desc") != -1;
    },

排序操作下

<div class="navbar-inner filter">
              <!-- 排序的结构 -->
              <ul class="sui-nav">
                <li :class="{ active: isOne }" @click="changeOrder('1')">
                  <a
                    >综合<span
                      v-show="isOne"
                      class="iconfont"
                      :class="{
                        'icon-shangjiantou-cu': isAsc,
                        'icon-xiajiantou-cu': isDesc,
                      }"
                    ></span
                  ></a>
                </li>
                <li :class="{ active: isTwo }" @click="changeOrder('2')">
                  <a
                    >价格<span
                      v-show="isTwo"
                      class="iconfont"
                      :class="{
                        'icon-shangjiantou-cu': isAsc,
                        'icon-xiajiantou-cu': isDesc,
                      }"
                    ></span
                  ></a>
                </li>
              </ul>
            </div>

 

    //排序的操作
    changeOrder(flag) {
      //flag形参:是一个标记,代表用户点击的是综合1价格2「用户点击的时候传递过来的」
      let originOrder = this.searchParams.order;
      //这里获取的是最开始的状态
      let originFlag = this.searchParams.order.split(":")[0];
      let originSort = this.searchParams.order.split(":")[1];
      //准备一个新的order属性值
      let newOrder = "";
      //点击的是综合
      if (flag == originFlag) {
        newOrder = `${originFlag}:${originSort == "desc" ? "asc" : "desc"}`;
      } else {
        //点击的是价格
        newOrder = `${flag}:${"desc"}`;
      }
      //将新的order赋给searchParams
      this.searchParams.order = newOrder;
      //再次发请求
      this.getData();
    },

分页器静态组件

<template>
  <div class="pagination">
    <button>1</button>
    <button>上一页</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 {
  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>

  分页器的展示需要:

    1.pageNo:当前页数

    2.pageSize:每一页展示多少条数据

    3.total:整个分页功能总共要展示多少条数据

    4.continues:分页连续页码个数[5 | 7]

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

  src--views--Search--index.vue先写点假数据

<!-- 分页器 -->
          <Pagination :pageNo="1" :pageSize="3" :total="91" :continues="5" />

  src--components--Pagination--index.vue子组件进行接收

export default {
  name: "Pagination",
  props: ["pageNo", "pageSize", "total", "continues"],
};

  修改后的结果为:

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

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

    <button>···</button>
    <button>{{ totalPage }}</button>
    <button>下一页</button>

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

<script>
export default {
  name: "Pagination",
  props: ["pageNo", "pageSize", "total", "continues"],
  computed: {
    //总共多少页
    totalPage() {
      //向上取整
      return Math.ceil(this.total / this.totalSize);
    },
    //计算出连续的页码起始数字与结束数字「连续页码的数字:至少是5」
    startNumAndEndNum() {
      const { continues, pageNo, totalPage } = this;
      //先定义两个变量存储起始数字与结束数字
      let start = 0,
        end = 0;
      //连续页码数5「即至少5页」,如果出现不正常的现象「即不够5页」
      //不正常的现象「总页数没有连续页码多」
      if (continues > totalPage) {
        start = 1;
        end = totalPage;
      } else {
        //正常现象「连续页码5,但是总页数一定是大于5的」
        //起始数字
        start = pageNo - parseInt(continues / 2);
        //结束数字
        end = pageNo + parseInt(continues / 2);
        //把出现不正常的现象「start数字出现0|负数」纠正
        if (start < 1) {
          start = 1;
          end = continues;
        }
        if (end > totalPage) {
          end = totalPage;
          start = totalPage - continues + 1;
        }
      }
    },
  },
};
</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>

分页器动态展示

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

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

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

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

分页器完成

  src--views--Search--index.vue

      <!-- 分页器 -->
          <Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" @getPageNo="getPageNo" />
    //自定义事件的回调函数---获取当前第几页
    getPageNo(pageNo){
      this.searchParams.pageNo = pageNo;
      //再次发请求
      this.getData();
    }

  src--Pagination--index.vue

<template>
  <div class="pagination">
      <!-- 上面部分 -->
    <button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页</button>
    <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)">1</button>
    <button v-if="startNumAndEndNum.start > 2">...</button>

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

    <!-- 下面部分 -->
    <button v-if="startNumAndEndNum.end < totalPage - 1">···</button>
    <button v-if="startNumAndEndNum.end < tatalPage" @click="$emit('getPageNo',totalPage)">{{ totalPage }}</button>
    <button :disabled="pageNo==totalPage" @click="$emit('getPageNo',pageNo+1)">下一页</button>

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

分页器添加类名

<template>
  <div class="pagination">
      <!-- 上面部分 -->
    <button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页</button>
    <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)" :class="{ active:pageNp==1}">1</button>
    <button v-if="startNumAndEndNum.start > 2">...</button>

    <!-- 中间部分 -->
    <button v-for="(page, index) in startNumAndEndNum.end" :key="index" v-if="page >= startNumAndEndNum.start" @click="$emit('getPageNo',page)" :class="{ active:pageNo==page}">
      {{ page }}
    </button>

    <!-- 下面部分 -->
    <button v-if="startNumAndEndNum.end < totalPage - 1">···</button>
    <button v-if="startNumAndEndNum.end < tatalPage" @click="$emit('getPageNo',totalPage)" :class="{ active:pageNo==totalPage}">{{ totalPage }}</button>
    <button :disabled="pageNo==totalPage" @click="$emit('getPageNo',pageNo+1)">下一页</button>

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

 

.active{
    background: skyblue;
}

滚动行为

  将Detail静态组件拷贝至src--views下

  src--router--index.js注册路由

import Detail from '@/views/Detail';

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

        {
            path: "/detail/:skuid",
            component: Detail,
            meta: { show: true },
        },

  src--router--创建routes.js将index.js中的路由配置等信息拷贝

//引入路由组件
import Home from '@/views/Home';
import Search from '@/views/Search';
import Login from '@/views/Login';
import Register from '@/views/Register';
import Detail from '@/views/Detail';
//路由配置信息
export default
    [
        {
            path: "/detail/:skuid",
            component: Detail,
            meta: { show: true },
        },
        {
            path: "/home",
            component: Home,
            meta: { show: true },
        },
        {
            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) => ({ keyword: $route.params.keyword, k: $route.query.k })
        },
        {
            path: "/login",
            component: Login,
            meta: { show: false },
        },
        {
            path: "/register",
            component: Register,
            meta: { show: false },
        },
        //重定向,项目启动后,访问/ 定向到首页
        {
            path: '*',
            redirect: '/home'
        },
    ]

  src--router--index.js中引入

import routes from './routes';

 

//配置路由
export default new VueRouter({
    routes,
    //滚动行为
    scrollBehavior(to, from, savedPosition) {
        //返回的这个y=0,代表滚动条在最上方
        return { y:0 };
    }
})

产品详情数据获取

  src--store--创建文件detail.js

import { reqGoodsInfo } from "@/api";
const state = {
    goodInfo: {},
}
const mutations = {
    GETGOODINFO(state, goodInfo) {
        state.goodInfo = goodInfo;
    }
}
const actions = {
    //获取产品信息的action
    async getGoodInfo({ commit }, skuId) {
        let result = await reqGoodsInfo(skuId);
        if (result.code == 200) {
            commit("GETGOODINFO", result.data);
        }
    }
}
const getters = {}

export default {
    state,
    mutations,
    actions,
    getters,
}

  src--views--Detail--index.vue

  //派发action获取产品详情的信息
  mounted() {
    this.$route.dispatch("getGoodInfo", this.$route.params.skuid);
  },

产品详情动态展示

  src--store--detail.js

const getters = {
    categoryView(state) {
        //当前计算出的categoryView属性值至少是一个空对象,假的报错不会有了
        return state.goodInfo.categoryView || {};
    },
    skuInfo(state) {
        return state.goodInfo.skuInfo || {};
    }
}

  src--views--Detail--index.vue

      <!-- 导航路径区域 -->
      <div class="conPoin">
        <span v-show="categoryView.category1Name">{{
          categoryView.category1Name
        }}</span>
        <span v-show="categoryView.category2Name">{{
          categoryView.category2Name
        }}</span>
        <span v-show="categoryView.category3Name">{{
          categoryView.category3Name
        }}</span>
      </div>

 

            <h3 class="InfoName">
              {{ skuInfo.skuName }}
            </h3>
            <p class="news">
              {{ skuInfo.skuDesc }}
            </p>

                <div class="price">
                  <i>¥</i>
                  <em>{{ skuInfo.price }}</em>
                  <span>降价通知</span>
                </div>

 

  computed: {
    ...mapGetters(["categoryView", "skuInfo"]),
  },

zoom放大镜展示数据

  src--views--Detail--Zoom--Zoom.vue

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

<script>
export default {
  name: "Zoom",
  props: ["skuImageList"],
  computed: {
    imgObj() {
      return this.skuImageList[0] || {};
    },
  },
};
</script>

  src--views--Detail--index.vue

computed: {
    ...mapGetters(["categoryView", "skuInfo"]),
    //给子组件数据
    skuImagesList() {
      //如果服务器数据没有回来,skuInfo这个对象是空对象
      return skuInfo.skuImagesList || [];
    },
  },

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

  src--views--Detail--ImageList--ImageList.vue

<template>
  <div class="swiper-container">
    <div class="swiper-wrapper">
      <div
        class="swiper-slide"
        v-for="(slide, index) in skuImageList"
        :key="slide.id"
      >
        <img :src="slide.imgUrl" />
      </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"],
};
</script>

  src--views--Detail--index.vue

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

产品售卖属性值排他操作

  src--views--Detail--index.vue

            <div class="chooseArea">
              <div class="choosed"></div>
              <dl
                v-for="(spuSaleAttr, index) in spuSaleAttrList"
                :key="spuSaleAttr.id"
              >
                <dt class="title">{{ spuSaleAttr.saleAttrName }}</dt>
                <dd
                  changepirce="0"
                  :class="{ active: spuSaleAttrValue.isChecked == 1 }"
                  v-for="(
                    spuSaleAttrValue, index
                  ) in spuSaleAttr.spuSaleAttrValueList"
                  :key="spuSaleAttrValue.id"
                  @click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)"
                >
                  {{ spuSaleAttrValue.saleAttrValueName }}
                </dd>
              </dl>
            </div>

 

  methods: {
    //产品的售卖属性值切换高亮
    changeActive(saleAttrValue, arr) {
      //遍历全部售卖属性值isChecked为零没有高亮
      arr.forEach((item) => {
        item.isChecked = 0;
      });
      //点击的那个售卖属性值
      saleAttrValue.isChecked = 1;
    },
  },

放大镜操作上

  src--views--Detail--ImageList--ImageList.vue

<template>
  <div class="swiper-container">
    <div class="swiper-wrapper">
      <div
        class="swiper-slide"
        v-for="(slide, index) in skuImageList"
        :key="slide.id"
      >
        <img
          :src="slide.imgUrl"
          :class="{ active: currentIndex == index }"
          @click="changeCurrentIndex(index)"
        />
      </div>
    </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: {
    //监听数据:可以保证数据一定有,但是不能保证v-for遍历结构是否完整
    skuImageList(newValue, oldValue) {
      this.$nextTick(() => {
        new Swiper(this.$refs.cur, {
          direction: "vertical", // 垂直切换选项
          // 如果需要前进后退按钮
          navigation: {
            nextEl: ".swiper-button-next",
            prevEl: ".swiper-button-prev",
          },
          //显示几个图片设置
          slidePerView: 3,
          //每一次切换图片个数
          slidePerGroup: 1,
        });
      });
    },
  },
  methods: {
    changeCurrentIndex(index) {
      //修改响应式数据
      this.currentIndex = index;
      //通知兄弟组件,当前的索引值为几
      this.$bus.$emit("getIndex", this.currentIndex);
    },
  },
};
</script>

完成放大镜操作

  src--views--Detail--Zoom--Zoom.vue

<template>
  <div class="spec-preview">
    <img :src="imgObj.imgUrl" />
    <div class="event" @mousemove="handler"></div>
    <div class="big">
      <img :src="imgObj.imgUrl" ref="big" />
    </div>
    <!-- 遮罩层 -->
    <div class="mask" ref="mask"></div>
  </div>
</template>

<script>
export default {
  name: "Zoom",
  props: ["skuImageList"],
  data() {
    return {
      currentIndex: 0,
    };
  },
  computed: {
    imgObj() {
      return this.skuImageList[0] || {};
    },
  },
  mounted() {
    //全局事件总线,获取兄弟组件传递过来的索引值
    this.$bus.$on("getIndex", (index) => {
      //修改当前响应式数据
      this.currentIndex = index;
    });
  },
  methods: {
    handler(event) {
      let mask = this.$refs.mask;
      let big = this.$refs.big;
      let left = event.offSetX - mask.offsetWidth / 2;
      let top = event.offSetY - mask.offsetHeight / 2;
      //约束范围
      if (left <= 0) left = 0;
      if (left >= mask.offsetWidth) left = mask.offsetWidth;
      if (top <= 0) top = 0;
      if (top >= mask.offsetHeight) top = mask.offsetHeight;
      //修改元素的left|top属性值
      mask.style.left = left + "px";
      mask.style.top = top + "px";
      mask.style.left = -2 * left + "px";
      mask.style.top = -2 * top + "px";
    },
  },
};
</script>

购买产品个数的操作

            <div class="cartWrap">
              <div class="controls">
                <input
                  autocomplete="off"
                  class="itxt"
                  v-model="skuNum"
                  @change="changeSkuNum"
                />
                <a href="javascript:" class="plus" @click="skuNum++">+</a>
                <a
                  href="javascript:"
                  class="mins"
                  @click="skuNum > 1 ? skuNum-- : (skuNum = 1)"
                  >-</a
                >
              </div>
              <div class="add">
                <a href="javascript:">加入购物车</a>
              </div>
            </div>

 

    //修改表单元素产品个数
    changeSkuNum(event) {
      //用户输入进来的文本 * 1
      let value = event.target.value * 1;
      //如果用户输入进来的非法,出现isNaN或者小于1
      if (isNaN(value) || value < 1) {
        this.skuNum = 1;
      } else {
        //正常:大于1「大于1的整数,不能出现小数」
        this.skuNum = parseInt(value);
      }
    },

加入购物车

  src--api--index.js

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

  src--store--detail.js

    //将产品添加到购物车
    async AddOrUpdateShopCart({ commit }, { skuId, skuNum }) {
        //加入购物车返回的结构
        //加入购物车以后(发请求),前台将参数带给服务器
        //服务器写入数据成功,并没有返回其他的数据,只是返回code==200,代表本次操作成功
        //因为服务器没有返回其他数据,因此不需要三连环存储数据
        await reqAddOrUpdateShopCart(skuId, skuNum);
    }

  src--views--Detail--index.vue

              <div class="add">
                <!-- 之前的路由跳转,从A跳转到B路由,此处的加购进行路由跳转之前,发请求把你购买的产品信息通过请求的形式通知服务器,服务器进行相应的存储 -->
                <a href="javascript:" @click="addShopcar">加入购物车</a>
              </div>

 

    //加入购物车的回调函数
    addShopcar() {
      //1:发请求--将产品加入到数据库(通知服务器)
      this.$store.dispatch("addOrUpdateShopCart",{skuId:this.$route.params.skuid,skuNum:this.skuNum});
      //2:服务器存储成功--进行路由跳转传递参数
      //3:失败,给用户进行提示
    },

加入购物车成功与失败的判断

  src--store--detail.js

    //将产品添加到购物车
    async AddOrUpdateShopCart({ commit }, { skuId, skuNum }) {
        //加入购物车返回的结构
        //加入购物车以后(发请求),前台将参数带给服务器
        //服务器写入数据成功,并没有返回其他的数据,只是返回code==200,代表本次操作成功
        //因为服务器没有返回其他数据,因此不需要三连环存储数据
        let result = await reqAddOrUpdateShopCart(skuId, skuNum);
        //代表服务器加入购物车成功
        if (result.code == 200) {
            return "ok"
        } else {
            //代表加入购物车失败
            return Promise.reject(new Error('faile'));
        }
    }

  src--views--Detail--index.vue

    //加入购物车的回调函数
    async addShopcar() {
      //1:发请求--将产品加入到数据库(通知服务器)
      /*当前派发action,也向服务器发请求,判断加入购物车是成功还是失败了,进行相应的操作
      this.$store.dispatch("addOrUpdateShopCart",{skuId:this.$route.params.skuid,skuNum:this.skuNum});
      这行代码:调用仓库中的addOrUpdateShopCart,这个方法加上async,返回一定是Promise,要么成功|要么失败
      */
      try {
        await this.$store.dispatch("addOrUpdateShopCart", {
          skuId: this.$route.params.skuid,
          skuNum: this.skuNum,
        });
      } catch (error) {
        alert(error.message);
      }
    },

加入购物车操作

  src--views--将AddCartSuccess拷贝进来

  src--router--routes.js

import AddCartSuccess from '@/views/AddCartSuccess';

 

        {
            path: "/addcartsuccess",
            name: 'addcartsuccess',
            component: AddCartSuccess,
            meta: { show: true },
        }, 

  src--views--Detail--index.vue

        //路由跳转
        this.$router.push({ name: `addcartsuccess` });

路由传参结合会话存储

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

  会话存储:非持久化--会话结束就消失

  本地存储和会话存储都不要用对象

  src--views--Detail--index.vue

    //加入购物车的回调函数
    async addShopcar() {
      //1:点击加入购物车按钮,将参数带给服务器(发请求),通知服务器加入购物车的是哪个
      /*当前派发action,也向服务器发请求,判断加入购物车是成功还是失败了,进行相应的操作
      this.$store.dispatch("addOrUpdateShopCart"),是在调用vuex仓库中的函数addOrUpdateShopCart
      2.需要知道请求成功还是失败,如果成功进行路由跳转,如果失败,需要给用户提示
      */
      try {
        //成功
        await this.$store.dispatch("addOrUpdateShopCart", {
          skuId: this.$route.params.skuid,
          skuNum: this.skuNum,
        });
        //3.路由跳转
        //4.路由跳转的时候还需要将产品信息带给下一级路由组件
        //一些简单的数据skuNum,通过query形式给路由组件传递过去
        //产品信息的数据「比较复杂:skuInfo」,通过会话存储(不持久化,会话结束数据小时)
        //本地存储|会话存储,一般存储的是字符串
        sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo));
        this.$router.push({ name: 'addcartsuccess',query:{skuNum:this.skuNum} });
      } catch (error) {
        alert(error.message);
      }
    },

  src--views--AddCartSuccess--index.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",
  mounted() {
    return JSON.parse(sessionStorage.getItem("SKUINFO"));
  },
};
</script>

购物车静态组件修改

  src--views--粘贴ShopCart静态组件

  src--views--AddShopCartSuccess--index.vue

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

  src--router--routes.js引入

  src--views--ShopCart--index.vue修改对应比例等信息(cart-list-con1下面的第三项删除,修改css样式width比例分别为15,35,10,17,10,13)

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

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

  src--store--detail.js

//封装游客身份模块uuid--生成一个随机字符串(不能改变)
import { getUUID } from '@/utils/uuid_token';
const state = {
    goodInfo: {},
    //游客临时身份
    uuid_token: getUUID()
}

  src--创建utils文件夹--uuid_token.js文件

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

  可拿到uuid_token

  src--api--ajax.js

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

  点击搜索发请求后也可拿到

 

购物车动态展示

  src--store--shopcart.js

import { reqCartList } from "@/api"
const state = {
    cartList: [],
}
const mutations = {
    GETCARTLIST(state, cartList) {
        state.cartList = cartList;
    }
}
const actions = {
    //获取购物车列表数据
    async getCartList({ commit }) {
        let result = await reqCartList();
        if (result.code == 200) {
            commit('GETCARTLIST', result.data);
        }
    }
}
const getters = {
    cartList(state) {
        return state.cartList[0] || {}
    },
}

export default {
    state,
    mutations,
    actions,
    getters,
}

  src--views--ShopCart--index.vue

      <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"
            />
          </li>
          <li class="cart-list-con2">
            <img :src="cart.imgUrl" />
            <div class="item-msg">
              {{ cart.skuName }}
            </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="cart.skuNum"
              minnum="1"
              class="itxt"
            />
            <a href="javascript:void(0)" class="plus">+</a>
          </li>
          <li class="cart-list-con6">
            <span class="sum">{{ cart.skuNum * cart.skuPrice }}</span>
          </li>
          <li class="cart-list-con7">
            <a href="#none" class="sindelet">删除</a>
            <br />
            <a href="#none">移到收藏</a>
          </li>
        </ul>
      </div>

 

<script>
import { mapGetters } from "vuex";
export default {
  name: "ShopCart",
  mounted() {
    this.getData();
  },
  methods: {
    //获取个人购物车的数据
    getData() {
      this.$store.dispatch("getCartList");
    },
  },
  computed: {
    ...mapGetters(["cartList"]),
    //购物车的数据
    cartInfoList() {
      return this.cartList.cartInfoList || [];
    },
    //计算购买产品的总价
    totalPrice() {
      let sum = 0;
      this.cartInfoList.forEach((item) => {
        sum += item.skuNum * item.skuPrice;
      });
      return sum;
    },
    //判断底部复选框是否勾选「全部产品都选中,才勾选」
    isAllCheck() {
      //遍历数组原理:只有全部元素isChecked属性都为1--真,只要有一个不是1--假
      return this.cartInfoList.every((item) => item.isChecked == 1);
    },
  },
};
</script>

处理购物车产品数量

          <li class="cart-list-con5">
            <a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cart)">-</a>
            <input
              autocomplete="off"
              type="text"
              minnum="1"
              class="itxt"
              :value="cart.skuNum"
              @change="handler('change',$event.target.value*1,cart)"
            />
            <a href="javascript:void(0)" class="plus" @click="handler('add',1,cart)">+</a>
          </li>

 

    //修改某一个产品的个数
    handler(type,disNum,cart){
      //type:为了区分这三个元素
      //目前disNum形参: + 变化量   - 变化量    input最终的个数(并不是变化量)
      //cart:哪一个产品「身上有id」
      console.log("派发action,通知服务器修改个数",type,disNum,cart);
    }

完成处理购物车产品数量

    //修改某一个产品的个数
    async handler(type, disNum, cart) {
      //type:为了区分这三个元素
      //目前disNum形参: + 变化量   - 变化量    input最终的个数(并不是变化量)
      //cart:哪一个产品「身上有id」
      //向服务器发请求,修改数量
      switch (type) {
        //加号
        case "add":
          disNum = 1;
          break;
        case "minus":
          //判断产品的个数大于1,才可以传递给服务器-1
          //如果出现产品的个数小于等于1,传递给服务器的个数0
          disNum = cart.skuNum > 1 ? -1 : 0;
          break;
        case "change":
          //用户输入的最终值非法,带有汉字或负数,则带给服务器零
          //写法一
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0;
          } else {
            //输入小数则取整,带给服务器的值=用户输入的-产品的起始个数
            disNum = parseInt(disNum) - cart.skuNum;
          }
          //写法二
          disNum = isNaN(disNum) || disNum < 1 ? 0 : parseInt(disNum) - cart.skuNum;
          break;
      }
      //派发action
      try {
        //代表修改成功
        await this.$store.dispatch("addOrUpdateShopCart", {
          skuId: cart.skuId,
          skuNum: disNum,
        });
        //再一次获取服务器最新的数据进行展示
        this.getData();
      } catch (error) {}
    },

删除购物车产品的操作

//获取购物产品接口
export const reqDeleteCartById = (skuId) => requests({ url: `/cart/deleteCart/${skuId}`, method: 'delete' });

  src--store--shopcart.js

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

  src--views--ShopCart--index.vue

          <li class="cart-list-con7">
            <a href="#none" class="sindelet" @click="deleteCartById(cart)"
              >删除</a
            >
            <br />
            <a href="#none">移到收藏</a>
          </li>

 

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

修改产品状态

  用户点击过快的时候会出现-1

  src--views--ShopCart--index.vue

  利用节流

   //修改某一个产品的个数「节流」
    handler: throttle(async function (type, disNum, cart) {
      //type:为了区分这三个元素
      //目前disNum形参: + 变化量   - 变化量    input最终的个数(并不是变化量)
      //cart:哪一个产品「身上有id」
      //向服务器发请求,修改数量
      switch (type) {
        //加号
        case "add":
          disNum = 1;
          break;
        case "minus":
          //判断产品的个数大于1,才可以传递给服务器-1
          //如果出现产品的个数小于等于1,传递给服务器的个数0
          disNum = cart.skuNum > 1 ? -1 : 0;
          break;
        case "change":
          //用户输入的最终值非法,带有汉字或负数,则带给服务器零
          //写法一
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0;
          } else {
            //输入小数则取整,带给服务器的值=用户输入的-产品的起始个数
            disNum = parseInt(disNum) - cart.skuNum;
          }
          //写法二
          disNum =
            isNaN(disNum) || disNum < 1 ? 0 : parseInt(disNum) - cart.skuNum;
          break;
      }
      //派发action
      try {
        //代表修改成功
        await this.$store.dispatch("addOrUpdateShopCart", {
          skuId: cart.skuId,
          skuNum: disNum,
        });
        //再一次获取服务器最新的数据进行展示
        this.getData();
      } catch (error) {}
    }, 500),

  src--api--index.js

//修改商品的状态
export const reqUpdateCheckedById = (skuId, isChecked) => requests({ url: `/cart/checkCart/${skuId}/${isChecked}`, method: 'get' });

  src--store--shopcart.js

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

  src--views--ShopCart--index.vue

        <li class="cart-list-con1">
            <input
              type="checkbox"
              name="chk_list"
              :checked="cart.isChecked == 1"
              @change="upDateChecked(cart, $event)"
            />
          </li>

 

    //修改某个产品的勾选状态
    async upDateChecked(cart, event) {
      //带给服务器的参数isChecked,不是布尔值,应该是0|1
      try {
        let isChecked = event.target.checked ? "1" : "0";
        this.$store.dispatch("reqUpdateCheckedById", {
          skuId: cart.skuId,
          isChecked,
        });
        this.getData();
      } catch (error) {
        //如果失败提示
        alert(error.message);
      }
    },

删除全部选中的商品 

  Promise.all([p1,p2,p3])  p1 | p2 | p3:每一个都是Promise对象,如果有一个Promise失败,都失败,如果都成功,则返回成功

   src--views--ShopCart--index.vue

      <div class="option">
        <a href="#none" @click="deleteAllCheckedCart">删除选中的商品</a>
        <a href="#none">移到我的关注</a>
        <a href="#none">清除下柜商品</a>
      </div>

 

    //删除全部选中的产品
    //这个回调函数无法收集到有用数据
    async deleteAllCheckedCart() {
      try {
        //派发一个action
        await this.$store.dispatch("deleteAllCheckedCart");
        //再次发请求获取购物车列表
        this.getData();
      } catch (error) {
        alert(error.message);
      }
    },

  src--store--shopcart.js

    //删除全部选中的商品
    deleteAllCheckedCart({ dispatch, getters }) {
        //context:小仓库,commit「提交mutations修改state」,getters「计算属性」,dispatch「派发action」,state「当前仓库数据」
        //获取购物车中全部的产品(是一个数组)
        let PromiseAll = [];
        getters.cartList.cartInfoList.forEach(item => {
            let promise = item.isChecked == 1 ? dispatch('deleteCartListBySkuId', item.skuId) : '';
            //将每一次返回的Promise添加到数组当中
            PromiseAll.push(promise);
        });
        //只要所有的p1|p2....都成功,返回结果即为成功
        //如果有一个失败,返回即为失败结果
        return Promise.all(PromiseAll);
    }

全部产品的勾选状态修改

  src--store--shopcart.js

    //修改全部产品的状态
    updateAllCartIsChecked({ dispatch, state }, isChecked) {
        //定义数组
        let promiseAll = [];
        state.cartList[0].cartInfoList.forEach(item => {
            let promise = dispatch('reqUpdateCheckedById', {
                skuId: item.skuId,
                isChecked,
            });
            promiseAll.push(promise);
        });
        //最终返回的结果
        return Promise.all(promiseAll);
    }

  src--views--ShopCart--index.vue

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

登录注册静态组件

  assets文件夹--放置全部组件公用静态资源

  在样式当中也可以使用@符号「src别名」,切记在前面加上~

  src--views--粘贴Login和Register组件

  复制icons.png到src--assets--images下

  src--views--Home--ListContainer--index.vue修改images的路径

          .list-item {
            background-image: url(~@/assets/images/icons.png);
            width: 61px;
            height: 40px;
            display: block;
          }

注册业务

  src--api--index.js

//获取验证码
export const reqGetCode = (phone) => requests({ url: `/user/passport/sendCode/${phone}`, method: 'get' });

//注册
export const reqUserRegister = (data) => requests({ url: `/user/passport/register`, data, method: 'post' });

  src--store--user.js

//登录与注册模块
import { reqGetCode, reqUserRegister } from '@/api';

const state = {
    code: ''
};
const mutations = {
    GETCODE(state, code) {
        state.code = code;
    }
};
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'));
        }
    },
    //用户注册
    async UserRegister({ commit }, user) {
        let result = await reqUserRegister(user);
        if (result.code == 200) {
            return "ok";
        } else {
            return Promise.reject(new Error('faile'));
        }
    }
};
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,
}

  src--views--Register--index.vue

  <div class="register-container">
    <!-- 注册内容 -->
    <div class="register">
      <h3>
        注册新用户
        <span class="go" >我有账号,去 <a href="login.html" target="_blank">登陆</a></span>
      </h3>
      <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" />&nbsp;
        <button style="width: 100px; height: 38px" @click="getCode">
          获取验证码
        </button>
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>登录密码:</label>
        <input
          type="password"
          placeholder="请输入你的登录密码"
          v-model="password"
        />
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input
          type="password"
          placeholder="请输入确认密码"
          v-model="password1"
        />
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="controls">
        <input name="m1" type="checkbox" :checked="agree" />
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="btn">
        <button @click="userRegister">完成注册</button>
      </div>
    </div>

 

<script>
export default {
  name: "Register",
  data() {
    return {
      //收集表单数据
      phone: "",
      //验证码
      code: "",
      //密码
      password: "",
      //确认密码
      password1: "",
      //是否同意
      agree: true,
    };
  },
  //获取验证码
  methods: {
    async getCode() {
      //简单判断--至少有数据
      try {
        //如果获取到验证码
        const { phone } = this;
        phone && (await this.$store.dispatch("getCode", phone));
        //将组件的code属性值变为仓库中验证码()
        this.code = this.$store.state.user.code;
      } catch (error) {}
    },
    //用户注册
    async userRegister() {
      try {
        const { phone, code, password, password1 } = this;
        phone && code && password == password1 && (await this.$store.dispatch("UserRegister", {
            phone,
            code,
            password,
          }));
        //如果成功路由跳转
        this.$router.push("/login");
      } catch (error) {
        alert(error.message);
      }
    },
  },
};
</script>

登录业务

  登陆成功的时候,后台为了区分用户,服务器下发token「令牌:唯一标识符」

  一般登陆成功服务器会下发token,前台持久化存储token,「带着token向服务器要用户信息展示」

  src--api--index.js

//登录
export const reqUserLogin = (data) => requests({ url: '/user/passport/login', data, method: 'post' });

  src--store--user.js

//登录与注册模块
import { reqGetCode, reqUserRegister, reqUserLogin } from '@/api';

const state = {
    code: '',
    token: '',
};
const mutations = {
    GETCODE(state, code) {
        state.code = code;
    },
    USERLOGIN(state, token) {
        state.token = token;
    }
};
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'));
        }
    },
    //用户注册
    async UserRegister({ commit }, user) {
        let result = await reqUserRegister(user);
        if (result.code == 200) {
            return "ok";
        } else {
            return Promise.reject(new Error('faile'));
        }
    },
    //登陆业务
    async userLogin({ commit }, data) {
        let result = await reqUserLogin(data);
        //服务器下发token,用户唯一标识符(uuid)
        //经常通过带token找服务器要用户信息展示
        if (result.code == 200) {
            commit("USERLOGIN", result.data.token);
            return "ok";
        } else {
            return Promise.reject(new Error('faile'));
        }
    }
};
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,
}

  src--views--Login--index.vue

<template>
  <div class="login-container">
    <!-- 登录 -->
    <div class="login-wrap">
      <div class="login">
        <div class="loginform">
          <ul class="tab clearFix">
            <li>
              <a href="##" style="border-right: 0">扫描登录</a>
            </li>
            <li>
              <a href="##" class="current">账户登录</a>
            </li>
          </ul>

          <div class="content">
            <form>
              <div class="input-text clearFix">
                <span></span>
                <input
                  type="text"
                  placeholder="邮箱/用户名/手机号"
                  v-model="phone"
                />
              </div>
              <div class="input-text clearFix">
                <span class="pwd"></span>
                <input
                  type="password"
                  placeholder="请输入密码"
                  v-model="password"
                />
              </div>
              <div class="setting clearFix">
                <label class="checkbox inline">
                  <input name="m1" type="checkbox" value="2" checked="" />
                  自动登录
                </label>
                <span class="forget">忘记密码?</span>
              </div>
              <!-- prevent:阻止默认行为 -->
              <button class="btn" @click.prevent="userLogin">&nbsp;&nbsp;录
              </button>
            </form>

            <div class="call clearFix">
              <ul>
                <li><img src="./images/qq.png" alt="" /></li>
                <li><img src="./images/sina.png" alt="" /></li>
                <li><img src="./images/ali.png" alt="" /></li>
                <li><img src="./images/weixin.png" alt="" /></li>
              </ul>
              <router-link class="register" to="/register"
                >立即注册</router-link
              >
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <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">京ICP备19006430号</div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Login",
  data() {
    return {
      phone: "",
      password: "",
    };
  },
  //登陆的回调
  methods: {
    async userLogin() {
      try {
        //登陆成功
        const { phone, password } = this;
        phone && password && (await this.$store.dispatch("userLogin", { phone, password }));
        //跳转到home首页
        this.$router.push("/home");
      } catch (error) {
        alert(error.message);
      }
    },
  },
};
</script>

可以拿到token

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

  src--api--index.js

//获取用户信息「需要带着用户的token向服务器要用户信息」
export const reqUserInfo = () => requests({ url: '/user/passport/auth/getUserInfo', method: 'get' });

  src--store--user.js

    //获取用户信息
    async getUserInfo(){
        let result = await reqUserInfo();
        console.log(result);
    }

  src--views--Home--index.vue派发action获取用户信息

    //获取用户信息
    this.$store.dispatch("getUserInfo");

  src--api--ajax.js带token给服务器

    //需要携带token带给服务器
    if (store.state.user.token) {
        config.headers.token = store.state.user.token;
    }

可拿到token

 

  src--store--user.js

    //获取用户信息
    async getUserInfo({ commit }) {
        let result = await reqUserInfo();
        if (result.code == 200) {
            //提交用户信息
            commit('GETUSERINFO', result.data);
        }
    }

  src--components--Header--index.vue

        <div class="loginList">
          <p>淘会品欢迎您!</p>
          <!-- 没有用户名,未登录 -->
          <p v-if="!userName">
            <span>请</span>
            <!-- 声明式导航:务必要有to属性 -->
            <router-link to="/login">登录</router-link>
            <router-link to="/register" class="register">免费注册</router-link>
          </p>
          <!-- 登录了 -->
          <p v-else>
            <a>{{ userName }}</a>
            <a class="register">退出登录</a>
          </p>
        </div>

 

  computed: {
    //用户名信息
    userName() {
      return this.$store.state.user.userInfo.name;
    },
  },

登录成功:

但是刷新页面token会丢失

 

登录业务存在问题

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

  2.用户已经登录就不应该有登录页

  src--store--user.js仓库中持久化token

import { setToken,getToken } from '@/utils/token';

 

const state = {
    code: '',
    token: getToken(),
    userInfo: {},
};

 

    //登陆业务
    async userLogin({ commit }, data) {
        let result = await reqUserLogin(data);
        //服务器下发token,用户唯一标识符(uuid)
        //经常通过带token找服务器要用户信息展示
        if (result.code == 200) {
            commit("USERLOGIN", result.data.token);
            //持久化token
            setToken(result.data.token);
            return "ok";
        } else {
            return Promise.reject(new Error('faile'));
        }
    },

  src--utils--创建token.js

//存储token
export const setToken = (token)=>{
    localStorage.setItem('TOKEN',token);
}
//获取token
export const getToken = ()=>{
    return localStorage.getItem('TOKEN');
}

退出登录

  src--api--index.js

//退出登录
export const reqLogout = () => requests({ url: '/user/passport/logout', method: 'get' });

  src--components--Header--index.vue

          <p v-else>
            <a>{{ userName }}</a>
            <a class="register" @click="logout">退出登录</a>
          </p>

 

    //退出登录
    async logout() {
      //退出登录需要发请求,通知服务器退出登录「清除一些数据,token」,清除项目当中的数据「userInfo,token」
      try {
        //如果退出成功
        await this.$store.dispatch("userLogout");
        //回到首页
        this.$router.push("/home");
      } catch (error) {}
    },

  src--store--user.js

import { removeToken } from '@/utils/token';

 

    //清除本地数据
    CLEAR(state) {
        state.token = '';
        state.userInfo = {};
        //清空本地存储
        removeToken();
    }

 

    //退出登录
    async userLogout({ commit }) {
        //只是向服务器发请求,通知服务器清除token
        let result = await reqLogout();
        //action里不能操作state,提交mutation修改state
        if (result.code == 200) {
            commit('CLEAR');
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'))
        }
    }

  src--utils--token.js

export const removeToken = () => {
    localStorage.removeItem('TOKEN');
}

导航守卫理解

  1.用户已经登录了,用户不应该还能到login页面

  全局守卫,路由独享守卫,组件守卫

   src--router--index.js

import store from '@/store';

 

//配置路由
let router = new VueRouter({
    routes,
    //滚动行为
    scrollBehavior(to, from, savedPosition) {
        //返回的这个y=0,代表滚动条在最上方
        return { y: 0 };
    }
});

//全局守卫:前置守卫(在路由跳转之前进行判断)
router.beforeEach((to, from, next) => {
    //to:可以获取到跳转到哪个路由信息
    //from:可以获取到从哪个路由而来的信息
    //next:放行函数  next()放行  next(path)放行到制定路由  next(false)
    next();
    console.log(store);
})
export default router;

导航守卫的判断

  src--router--index.js

//全局守卫:前置守卫(在路由跳转之前进行判断)
router.beforeEach(async (to, from, next) => {
    //to:可以获取到跳转到哪个路由信息
    //from:可以获取到从哪个路由而来的信息
    //next:放行函数  next()放行  next(path)放行到制定路由  next(false)
    // next();
    //用户登录了,才会有token,未登录一定不会有token
    let token = store.state.user.token;
    //用户信息
    let name = store.state.user.userInfo.name;
    //用户已经登录了
    if (token) {
        //用户已经登录了是不能跳转到login的,停留在首页
        if (to.path == '/login') {
            next('/home');
        } else {
            //登录,跳转的不是login「home|search|detail|shopcart」
            //如果用户名已经有了
            if (name) {
                next();
            } else {
                //没有用户信息,派发action让仓库存储用户信息在跳转
                try {
                    //获取用户信息成功
                    await store.dispatch('getUserInfo');
                    //放行
                    next();
                } catch (error) {
                    //token失效了获取不到用户信息,重新登录
                    //清除token
                    await store.dispatch('userLogout');
                    next('/login');
                }
            }
        }
    } else {
        //未登录
        next();
    }
})

trade静态组件

  src--views--复制trade静态组件并引入组件和路由

import Trade from '@/views/Trade';

 

    {
            path: "/trade",
            component: Trade,
            meta: { show: true },
        },

  点击结算按钮跳转到trade,src--views--ShopCart--index.vue

        <div class="sumbtn">
          <router-link class="sum-btn" to="/trade">结算</router-link>
        </div>

获取交易页数据

  src--api--index.js

//获取用户地址信息
export const reqAddressInfo = () => requests({ url: '/user/userAddress/auth/findUserAddressList', method: 'get' });

//获取商品清单
export const reqOrderInfo = () => requests({ url: '/order/auth/trade', method: 'get' });

  src--store--trade.js

import { reqAddressInfo, reqOrderInfo } from '@/api';
const state = {
    address: [],
    orderInfo: {},
};
const mutations = {
    GETUSERADDRESS(state, address) {
        state.address = address;
    },
    GETORDERINFO(state, orderInfo) {
        state.orderInfo = orderInfo;
    }
};
const actions = {
    //获取用户地址信息
    async GetUserAddress({ commit }) {
        let result = await reqAddressInfo();
        if (result.code == 200) {
            commit('GETUSERADDRESS', result.data);
        }
    },
    //获取商品清单信息
    async GetOrderInfo({ commit }) {
        let result = await reqOrderInfo();
        if (result.code == 200) {
            commit('GETORDERINFO', result.data);
        }
    }
};
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,
}

  src--views--Trade--index.vue派发action

<script>
export default {
  name: "Trade",
  //生命周期函数挂载完毕
  mounted() {
    this.$store.dispatch("GetUserAddress");
    this.$store.dispatch("GetOrderInfo");
  },
};
</script>

用户地址信息展示

  src--views--Trade--index.vue

      <div
        class="address clearFix"
        v-for="(address, index) in addressInfo"
        :key="address.id"
      >
        <span class="username" :class="{selected:address.isDefault==1}">{{ address.consignee }}</span>
        <p @click="changeDefault(address,addressInfo)">
          <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="trade">
      <div class="price">应付金额: <span>¥5399.00</span></div>
      <div class="receiveInfo">
        寄送至:
        <span>{{userDefaultAddress.fullAddress}}</span>
        收货人:<span>{{userDefaultAddress.consignee}}</span>
        <span>{{userDefaultAddress.phoneNum}}</span>
      </div>
    </div>

 

<script>
import { mapState } from "vuex";
export default {
  name: "Trade",
  //生命周期函数挂载完毕
  mounted() {
    this.$store.dispatch("GetUserAddress");
    this.$store.dispatch("GetOrderInfo");
  },
  computed: {
    ...mapState({
      addressInfo: (state) => state.trade.address,
    }),
    //提交订单最终选中地址
    userDefaultAddress(){
      //find:查找数组当中符合条件的元素返回,作为最终结果
      return this.addressInfo.find(item=>item.isDefault==1);
    }
  },
  methods:{
    //修改默认地址
    changeDefault(address,addressInfo){
      //全部的isDefault为零
      addressInfo.forEach(item=>item.isDefault=0);
      address.isDefault = 1;
    }
  }
};
</script>

交易页面完成

  src--views--Trade--index.vue

<template>
  <div class="trade-container">
    <h3 class="title">填写并核对订单信息</h3>
    <div class="content">
      <h5 class="receive">收件人信息</h5>
      <div
        class="address clearFix"
        v-for="(address, index) in addressInfo"
        :key="address.id"
      >
        <span class="username" :class="{selected:address.isDefault==1}">{{ address.consignee }}</span>
        <p @click="changeDefault(address,addressInfo)">
          <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>配送方式</h5>
        <div class="info clearFix">
          <span class="s1">天天快递</span>
          <p>配送时间:预计8月10日(周三)09:00-15:00送达</p>
        </div>
      </div>
      <div class="detail">
        <h5>商品清单</h5>
        <ul class="list clearFix" v-for="(order,index) in orderInfo.detailArrayList" :key="order.skuId">
          <li>
            <img :src="order.imgUrl" 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="msg"
        ></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>{{orderInfo.totalNum}}</i>件商品,总商品金额</b>
          <span>¥{{orderInfo.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>¥{{orderInfo.totalAmount}}.00</span></div>
      <div class="receiveInfo">
        寄送至:
        <span>{{userDefaultAddress.fullAddress}}</span>
        收货人:<span>{{userDefaultAddress.consignee}}</span>
        <span>{{userDefaultAddress.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 {
  name: "Trade",
  data(){
    return{
      //收集买家留言
      msg:''
    }
  },
  //生命周期函数挂载完毕
  mounted() {
    this.$store.dispatch("GetUserAddress");
    this.$store.dispatch("GetOrderInfo");
  },
  computed: {
    ...mapState({
      addressInfo: (state) => state.trade.address,
      orderInfo:state=>state.trade.orderInfo,
    }),
    //提交订单最终选中地址
    userDefaultAddress(){
      //find:查找数组当中符合条件的元素返回,作为最终结果
      return this.addressInfo.find(item=>item.isDefault==1)||{};
    }
  },
  methods:{
    //修改默认地址
    changeDefault(address,addressInfo){
      //全部的isDefault为零
      addressInfo.forEach(item=>item.isDefault=0);
      address.isDefault = 1;
    }
  }
};
</script>

提交订单

  点击提交订单按钮,向服务器发请求「把支付等信息传递给服务器」

  src--views--引入静态组件Pay

  src--api--index.js

//提交订单的接口
export const reqSubmitOrder = (tradeNo, data) => requests({ url: `/order/auth/SubmitOrder?tradeNo=${tradeNo}`, data, method: 'post' });

  main.js统一引入接口

//统一接口api文件夹里全部请求函数
//统一引入
import * as API from '@/api';

new Vue({
  render: h => h(App),
  //全局事件总线$bus配置
  beforeCreate() {
    Vue.prototype.$bus = this;
    Vue.prototype.$API = API;
  },

  src--views--Trade--index.vue

<a class="subBtn" @click="submitOrder">提交订单</a>

 

    //提交订单
    async submitOrder() {
      // console.log(this.$API);
      //交易编码
      let { tradeNo } = this.orderInfo;
      //其余6个参数
      let data = {
        consignee: this.userDefaultAddress.consignee,//最终收件人的名字
        consigneeTel: this.userDefaultAddress.phoneNum,//最终收件人的手机号
        deliveryAddress: this.userDefaultAddress.fullAddress,//收件人的地址
        paymentWay: "ONLINE",//支付方式
        orderComment: this.msg,//买家的留言信息
        orderDetailList: this.orderInfo.detailArrayList,//商品清单
      };

      //需要带参数:tradeNo
      let result = await this.$API.reqSubmitOrder(tradeNo,data);
      console.log(result);
    },

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

  生命周期函数mounted中不要使用async

  src--api--index.js

//获取支付信息
export const reqPayInfo = (orderId) => requests({ url: `/payment/weixin/createNative/${orderId}`, method: 'get' });

  src--views--Trade--index.vue

      //需要带参数:tradeNo
      let result = await this.$API.reqSubmitOrder(tradeNo, data);
      //提交订单成功
      if (result.code == 200) {
        this.orderId = result.data;
        //路由跳转
        this.$router.push("/pay?orderId="+this.orderId);
      } else {
        alert(result.data);
      }

  src--views--Pay--index.vue

        <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 class="submit">
          <!-- <router-link class="btn" to="/paysuccess">立即支付</router-link> -->
          <a class="btn">立即支付</a>
        </div>

 

<script>
export default {
  name: "Pay",
  data() {
    return {
      payInfo: {},
    };
  },
  computed: {
    orderId() {
      return this.$route.query.orderId;
    },
  },
  mounted() {
    this.getPayInfo();
  },
  methods: {
    async getPayInfo() {
      let result = await this.$API.reqPayInfo(this.orderId);
      //如果成功,组件当中存储支付信息
      if (result.code == 200) {
        this.payInfo = result.data;
      }
    },
  },
};
</script>

支付页面按需使用elementUI

  npm install --save element-ui

  npm install babel-plugin-component -D

 

 

  main.js

import {Button,MessageBox} from 'element-ui';

 

Vue.component(Button.name, Button);
//elementUI注册组件的时候,还有一种写法,挂在原型上
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;

  src--views--Pay--index.vue

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

 

          <!-- <router-link class="btn" to="/paysuccess">立即支付</router-link> -->
          <a class="btn" @click="open">立即支付</a>
        </div>

 

    //弹出框
    open() {
      this.$alert("<strong>这是 <i>HTML</i> 片段</strong>", "HTML 片段", {
        dangerouslyUseHTMLString: true,
        //中间布局
        center: true,
        //是否显示取消按钮
        showCancelButton: true,
        //取消按钮的文本内容
        cancelButtonText: "支付遇到问题",
        //确定按钮的文本
        confirmButtonText: "支付成功",
        //右上角的×
        showClose: false,
      });
    },

微信支付业务上

  npm i qrcode --save

  src--api--index.js

//获取支付订单状态
export const reqPayStatus = (orderId) => requests({ url: `/payment/weixin/queryPayStatus/${orderId}`, method: 'get' });

  src--views--Pay--index.vue

  data() {
    return {
      payInfo: {},
      timer: null,
      //支付状态码
      code: "",
    };
  },

 

    //弹出框
    async open() {
      //生成二维码(地址)
      let url = await QRcode.toDataURL(this.payInfo.codeUrl);
      this.$alert(`<img src=${url} />`, "请使用微信支付", {
        dangerouslyUseHTMLString: true,
        //中间布局
        center: true,
        //是否显示取消按钮
        showCancelButton: true,
        //取消按钮的文本内容
        cancelButtonText: "支付遇到问题",
        //确定按钮的文本
        confirmButtonText: "支付成功",
        //右上角的×
        showClose: false,
      });
      //需要知道支付成功|失败
      //支付成功,路由的跳转,如果支付失败,提示信息
      //定时器没有,开启一个新的定时器
      if (!this.timer) {
        this.timer = setInterval(async () => {
          //发请求获取用户支付状态
          let result = await this.$API.reqPayStatus(this.orderId);
          //如果code=200
          if (result.code == 200) {
            //第一步:清除定时器
            clearInterval(this.timer);
            this.timer = null;
            //保存支付成功的code
            this.code = result.code;
            //关闭弹出框
            this.$msgbox.close();
            //跳转到下一路由
            this.$router.push("/paysuccess");
          }
        }, 1000);
      }
    },

微信支付业务下

  src--views--Pay--index.vue

    //弹出框
    async open() {
      //生成二维码(地址)
      let url = await QRcode.toDataURL(this.payInfo.codeUrl);
      this.$alert(`<img src=${url} />`, "请使用微信支付", {
        dangerouslyUseHTMLString: true,
        //中间布局
        center: true,
        //是否显示取消按钮
        showCancelButton: true,
        //取消按钮的文本内容
        cancelButtonText: "支付遇到问题",
        //确定按钮的文本
        confirmButtonText: "支付成功",
        //右上角的×
        showClose: false,
        //关闭弹出框的配置值
        beforeClose: (type, instance, done) => {
          //type:区分取消|确定按钮
          //instance:当前组件实例
          //关闭弹出框的方法
          if (type == "cancel") {
            alert("请联系管理员");
            //清除定时器
            clearInterval(this.timer);
            this.timer = null;
            //关闭弹出框
            done();
          } else {
            //判断是否真的支付成功
            if (this.code == 200) {
              clearInterval(this.timer);
              this.timer = null;
              done();
              this.$router.push("paysuccess");
            }
          }
        },
      });
      //需要知道支付成功|失败
      //支付成功,路由的跳转,如果支付失败,提示信息
      //没有定时器,开启一个新的定时器
      if (!this.timer) {
        this.timer = setInterval(async () => {
          //发请求获取用户支付状态
          let result = await this.$API.reqPayStatus(this.orderId);
          //如果code=200
          if (result.code == 200) {
            //第一步:清除定时器
            clearInterval(this.timer);
            this.timer = null;
            //保存支付成功的code
            this.code = result.code;
            //关闭弹出框
            this.$msgbox.close();
            //跳转到下一路由
            this.$router.push("/paysuccess");
          }
        }, 1000);
      }
    },

个人中心二级路由组件

  src--views--粘贴个人中心Center静态组件,并在Center下创建二级groupOrder和myOrder

  src--views--Center--index.vue

            <dd><router-link to="/center/myorder">我的订单</router-link></dd>
            <dd><router-link to="/center/grouporder">团购订单</router-link></dd>

 

        <!-- 路由组件出口的位置 -->
        <router-view></router-view>

 

<script>
//引入子组件
import myOrder from "./myOrder";
import groupOrder from "./groupOrder";
export default {
  name: "",
  components: {
    myOrder,
    groupOrder,
  },
};
</script>

  src--router--routes.js

//引入二级路由组件
import myOrder from '@/views/Center/myOrder';
import groupOrder from '@/views/Center/groupOrder';

 

        {
            path: "/center",
            component: Center,
            meta: { show: true },
            //二级路由组件
            children: [
                {
                    path: 'myorder',
                    component: myOrder
                },
                {
                    path: 'grouporder',
                    component: groupOrder,
                },
                {
                    path: '/center',
                    redirect: '/center/myorder',
                }
            ]
        },

我的订单

   src--api--index.js

//获取个人中心数据
export const reqMyOrderList = (page, limit) => requests({ url: `/order/auth/${page}/${limit}`, method: 'get' });

  src--views--Center--myOrder--index.vue

    <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="(order, index) in myOrder.records"
          :key="order.id"
        >
          <thead>
            <tr>
              <th colspan="5">
                <span class="ordertitle"
                  >{{ order.createTime }} 订单编号:{{ order.outTradeNo }}
                  <span class="pull-right delete"
                    ><img src="../images/delete.png" /></span
                ></span>
              </th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(cart, index) in order.orderDetailList" :key="cart.id">
              <td width="60%">
                <div class="typographic">
                  <img :src="cart.imgUrl" style="width: 100px; height: 100px" />
                  <a href="#" class="block-text">{{ cart.skuName }}</a>
                  <span>x{{ cart.skuNum }}</span>
                  <a href="#" class="service">售后申请</a>
                </div>
              </td>
              <td
                :rowspan="order.orderDetatilList.length"
                v-if="index == 0"
                rowspan="2"
                width="8%"
                class="center"
              >
                {{ order.consignee }}
              </td>
              <td
                :rowspan="order.orderDetatilList.length"
                v-if="index == 0"
                rowspan="2"
                width="13%"
                class="center"
              >
                <ul class="unstyled">
                  <li>{{ order.totalAmount }}¥138.00</li>
                  <li>在线支付</li>
                </ul>
              </td>
              <td
                :rowspan="order.orderDetatilList.length"
                v-if="index == 0"
                rowspan="2"
                width="8%"
                class="center"
              >
                <a href="#" class="btn">{{ order.orderStatusName }}</a>
              </td>
              <td
                :rowspan="order.orderDetatilList.length"
                v-if="index == 0"
                rowspan="2"
                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="myOrder.total"
          :continues="5"
          @getPageNo="getPageNo"
        />
      </div>
    </div>

 

<script>
export default {
  name: "",
  data() {
    return {
      //初始化参数
      //当前第几页
      page: 1,
      //每一个展示数据个数
      limit: 3,
      //存储我的订单的数据
      myOrder: {},
    };
  },
  mounted() {
    //获取我的订单数据方法
    this.getData();
  },
  methods: {
    async getData() {
      //解构参数
      const { page, limit } = this;
      let result = await this.$API.reqMyOrderList(page, limit);
      if (result.code == 200) {
        this.myOrder = result.data;
      }
    },
    //获取当前点击那一页
    getPageNo(page){
        //修改组件响应式数据page
        this.page = page;
        this.getData();
    }
  },
};
</script>

未登录的导航守卫判断

  src--router--index.js

    //用户已经登录了
    if (token) {
        //用户已经登录了是不能跳转到login的,停留在首页
        if (to.path == '/login' || to.path == '/register') {
            next('/home');
        } else {
            //登录,跳转的不是login「home|search|detail|shopcart」
            //如果用户名已经有了
            if (name) {
                next();
            } else {
                //没有用户信息,派发action让仓库存储用户信息在跳转
                try {
                    //获取用户信息成功
                    await store.dispatch('getUserInfo');
                    //放行
                    next();
                } catch (error) {
                    //token失效了获取不到用户信息,重新登录
                    //清除token
                    await store.dispatch('userLogout');
                    next('/login');
                }
            }
        }
    } else {
        //未登录,不能去交易相关的,不能去支付相关「pay|paysuccess」,不能去个人中心
        //去的是上面这些路由 --登录
        let toPath = to.path;
        if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
            next('login?redirect=' + toPath);
        } else {
            //去的不是上面这些路由「home|search|shopcart」 --放行
            next();
        }
    }

  src--views--Login--index.vue

  //登陆的回调
  methods: {
    async userLogin() {
      try {
        //登陆成功
        const { phone, password } = this;
        phone &&
          password &&
          (await this.$store.dispatch("userLogin", { phone, password }));
        //登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
        let toPath = this.$route.query.redirect || "/home";
        this.$router.push(toPath);
      } catch (error) {
        alert(error.message);
      }
    },
  },

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

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

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

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

  src--router--routes.js

        {
            path: "/pay",
            component: Pay,
            meta: { show: true },
            beforeEnter: (to, from, next) => {
                if (from.path == "/trade") {
                    next();
                } else {
                    next(false);
                }
            }
        },
        {
            path: "/trade",
            component: Trade,
            meta: { show: true },
            //路由独享守卫
            beforeEnter: (to, from, next) => {
                //去交易页面,必须是从购物车来
                if (from.path == "/shopcart") {
                    next();
                } else {
                    //其他路由组件来的,停留在当前
                    next(false);
                }
            }
        },

  src--views--PaySuccess--index.vue

<script>
export default {
  name: "PaySuccess",
  //组件内守卫
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    if (from.path == "/pay") {
      next();
    } else {
      next(false);
    }
  },
};
</script>

图片懒加载:lazyload

  npm install vue-lazyload -S

  main.js

import atm from '@/assets/2.gif';
//引入插件
import VueLazyload from 'vue-lazyload';
//注册插件
Vue.use(VueLazyload, {
  //懒加载默认的图片
  loading: atm
})

  src--views--Search--index.vue

                  <div class="p-img">
                    <!-- 在路由跳转的时候要带id(params)参数 -->
                    <router-link :to="`/detail/${good.id}`">
                      <img v-lazy="good.defaultImg" />
                    </router-link>
                  </div>

表单验证:vee-validate

  npm install vee-validate@2 --save

  main.js

//引入表单校验插件
import "@/plugins/validate";

  src--plugins--validate.js

//vee-validate插件:表单验证区域
import Vue from "vue";
import VeeValidate from "vee-validate";
Vue.use(VeeValidate);
import zh_CN from 'vee-validate/dist/locale/zh_CN';   // 引入中文 message
Vue.use(VeeValidate);

//表单验证
VeeValidate.Validator.localize('zh_CN', {
    messages: {
        ...zh_CN.messages,
        is: (field) => `${field}必须与密码相同` // 修改内置规则的 message,让确认密码和密码相同
    },
    attributes: { // 给校验的 field 属性名映射中文名称
        phone: '手机号',
        code: '验证码',
        password: '密码',
        password1: '确认密码',
        agree: '协议'
    }
});
//自定义校验规则
//定义协议必须打勾同意
VeeValidate.Validator.extend('tongyi', {
    validate: value => {
        return value
    },
    getMessage: field => field + '必须同意'
})

  src--views--Register--index.vue

    <!-- 注册内容 -->
    <div class="register">
      <h3>
        注册新用户
        <span class="go"
          >我有账号,去 <a href="login.html" target="_blank">登陆</a></span
        >
      </h3>
      <div class="content">
        <label>手机号:</label>
        <!-- <input type="text" placeholder="请输入你的手机号" v-model="phone" /> -->
        <input
          placeholder="请输入你的手机号"
          v-model="phone"
          name="phone"
          v-validate="{ required: true, regex: /^1\d{10}$/ }"
          :class="{ invalid: errors.has('phone') }"
        />
        <!-- <span class="error-msg">错误提示信息</span> -->
        <span class="error-msg">{{ errors.first("phone") }}</span>
      </div>
      <div class="content">
        <label>验证码:</label>
        <!-- <input type="text" placeholder="请输入验证码" v-model="code" />&nbsp; -->
        <input
          placeholder="请输入验证码"
          v-model="code"
          name="code"
          v-validate="{ required: true, regex: /^\d{6}$/ }"
          :class="{ invalid: errors.has('code') }"
        />
        <button style="width: 100px; height: 38px" @click="getCode">
          获取验证码
        </button>
        <span class="error-msg">{{ errors.first("code") }}</span>
      </div>
      <div class="content">
        <label>登录密码:</label>
        <input
          placeholder="请输入密码"
          v-model="password"
          name="password"
          v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }"
          :class="{ invalid: errors.has('password') }"
        />
        <!-- <span class="error-msg">错误提示信息</span> -->
        <span class="error-msg">{{ errors.first("password") }}</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input
          placeholder="请输入确认密码"
          v-model="password1"
          name="password1"
          v-validate="{ required: true, is: password }"
          :class="{ invalid: errors.has('password1') }"
        />
        <span class="error-msg">{{ errors.first("password1") }}</span>
      </div>
      <div class="controls">
        <!-- <input name="m1" type="checkbox" :checked="agree" /> -->
        <input
          type="checkbox"
          v-model="agree"
          name="agree"
          v-validate="{ required: true, tongyi: true }"
          :class="{ invalid: errors.has('agree') }"
        />
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">{{ errors.first("agree") }}</span>
      </div>
      <div class="btn">
        <button @click="userRegister">完成注册</button>
      </div>
    </div>

 

    //用户注册
    async userRegister() {
      const success = await this.$validator.validateAll(); //全部表单验证
      //全部表单验证成功,再向服务器发请求,进行注册
      //只要有一个表单没有成功,不会发请求
      if (success) {
        try {
          const { phone, code, password, password1 } = this;
          await this.$store.dispatch("UserRegister", {
            phone,
            code,
            password,
          });
          //如果成功路由跳转
          this.$router.push("/login");
        } catch (error) {
          alert(error.message);
        }
      }
    },

路由懒加载

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

  src--router--routes.js

        {
            path: "/home",
            component: () => import("@/views/Home"),
            meta: { show: true },
        },

处理map文件

  项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的报错

  有了map文件就可以像未加密的代码一样,准确的输出是哪一行那一列有错

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

  vue.config.js文件添加:productionSourceMap:false 

 

posted on 2022-01-03 00:29  tom和mt  阅读(324)  评论(0)    收藏  举报