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 -- 前端项目本地服务器
//代理跨域 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修改下面

则下面的回调也要修改

获取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" /> <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"> 登 录 </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" /> -->
<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
浙公网安备 33010602011771号