文章目录
- 项目资源
- 环境
- TypeNav三级联动功能
- 一,完善Home1(ListContainer)
- 一, 完善Home2(Floor)
- 二,搜索模块开发(search)
- 三, 商品详情(Detail)
- 四,购物车
- 五, 注册
- 六,登录
- 七,购物车结算-微信支付
- 八,完善项目
- 九,打包上线
项目资源
通用步骤
1,静态页面
2,拆分组件
3,获取服务器的数据动态展示
4,完成相应的动态业务逻辑
注意: 使用less样式要安装less, less-loader
npm install --save less less-loader
样式添加
<style scoped lang="less">
环境
1,拆分header和footer
注意把样式和HTML和图片一起导入
2,路由组件搭建
安装vue-router
npm i --save vue-router@3
分析,路由组件应该有四个: Home, Search、 Login, Register
- components文件夹:经常放置的非路由组件(共用全局组件)
- pages |views文件夹:经常放置路由组件
总结
路由组件与非路由组件的区别?
- 1:路由组件一般放置在pages(views文件夹,非路由组件一般放置components文件夹中
- 2:路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以
标签的形式使用
- 注册完路由,不管路由路由组件、还是非路由组件身上都有
$route.$router
属性
$route:一般获取路由信息【路径、query、params等等】
$router:一般进行编程式导航进行路由跳转【push|replace】
3,footer组件显示与隐藏
Footer组件显示与隐藏
显示或者隐藏组件: v-if|v-show
Footer组件:在Home, Search显示Footer组件
Footer组件:在登录、注册时候隐藏的
- 1我们可以根据组件身上的$route获取当前路由的信息,通过路由路径判断Footer显示与隐藏。I
- 2配置的路由的时候,可以给路由添加路由元信息【meta】,路由需要配置对象,它的key不能乱写
在router配置文件中添加meta
routes:[
{
path:'/home',
component:Home,
meta:{show:true}
},
App.vue
中添加v-show
<Footer v-show="$route.meta.show"/>
4,路由传参
1:路由跳转有几种方式?
- 比如: A->B
- 声明式导航: router-link (务必要有to属性),可以实现路由的跳转
- 编程式导航:利用的是组件实例的
Srouter.push|replace
方法,可以实现路由的跳转。(可以书写一些自己业务)
2:路由传参,参数有几种写法?
- params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位
- query参数:不属于路径当中的一部分,类似于ajax中的querystring /home?k=v&kv=,不需要占位
goSearch(){
//路由传参,方式一,字符串
this.$router.push('/search/'+this.keyword+'?key='+this.keyword.toUpperCase())
//方式二: 模板字符串
this.$router.push(`/search/${this.keyword}?key=${this.keyword.toUpperCase()}`)
//第三种:对象, 需要在路由中配置name属性
this.$router.push({name:'search',params:{keyword:this.keyword},query:{key:this.keyword.toUpperCase()}})
}
面试题
1: 使用path能不能使用param传参?
可以拼接
this.$router.push({path:'/search/'+this.keyword,query:{key:this.keyword.toUpperCase()}})
2: 如何指定params参数, 可传可不传怎么配置?
需要配置路由,在占位的后面添加?号
{
path:'/search/:keyword?',
component:Search,
meta:{show:true},
name:'search',
}
this.$router.push({name:'search',query:{key:this.keyword.toUpperCase()}})
3: params如果传入空串, URL出现异常怎么处理?
使用undefined解决
//使用undefined解决
this.$router.push({name:'search',params:{keyword:''||undefined},query:{key:this.keyword.toUpperCase()}})
4: 路由组件能不能传递props数据?
this.$router.push({name:'search',params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}})
常用函数写法
{
path:'/search/:keyword?',
component:Search,
meta:{show:true},
name:'search',
//路由组件能不能传递props数据
//布尔值写法: 只能传递params参数
props:true
//对象写法:额外的给路由组件传递一些props参数
props:{a:1,b:2}
//函数写法(常用),可以接收params参数,query参数,通过props传递
props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k}
}
//简写
props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})
}
错误–NavigationDuplicated
编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated
的警告错误?
push
是VueRouter
类的一个原型方法,$router
是VueRouter
类的实例,类的实例可以直接调用类的原型方法
所以对原型方法push
进行修改,修改结果就会作用于组件实例的$router
实例。
解决
在router配置里重写push 和 replace方法
//先把vueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push;
let orginReplace = VueRouter.prototype.replace;
//重写push | replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
//二:成功的回调, 三: 失败的回调
VueRouter.prototype. push = function(location, resolve, reject){
if(resolve && reject){
//call] |apply区别
//相同点,都可以调用函数一次,都可以算改函数的上下文一次
//不同点: call与apply传递参数: cal1传递参数用逗号隔开, apply方法执行,传递数组
originPush.call(this, location,resolve, reject);
}else {
originPush.call(this, location, ()=>{}, ()=>{});
}
}
VueRouter.prototype.replace = function (location,rosole,reject){
if (rosole && reject){
orginReplace.call(this,location,resolve,reject);
}else {
orginReplace.call(this,location,()=>{},()=>{})
}
}
5,home模块组件拆分
导航:全部商品分类
在main.js中注册为全局组件, 在Home中直接使用标签
//三级联动组件---全局组件
import TypeNav from "@/components/TypeNav/TypeNav";
//参数1, 全局组件名, 参数2哪个组件
Vue.component('TypeNav',TypeNav)
三级联动组件完成
由于三级联动,在Home, Search、 Detail,把三级联动注册为全局组件。
好处:只需要注册一次,就可以在项目任意地方使用
其他静态资源组件
HTML + CSS + 图片资源
效果
6,接口测试
测试软件postman
Download Postman | Get Started for Free
1.1 服务器地址
最新接口地址:http://gmall-h5-api.atguigu.cn
1.2 公共请求参数
每个接口需要的Header参数值(登录接口不需要):
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
token | String | Y | 登录的token |
userTempId | String(通过uuidjs生成) | Y | 未登陆用户生成的临时ID |
例如:
token: d90aa16f24d04c7d882051412f9ec45b 后台生成
userTempId: b2f79046-7ee6-4dbf-88d0-725b1045460b 前台生成
1.3 首页三级分类
请求地址
/api/product/getBaseCategoryList
测试
http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList
- 经过postman工具测试,接口是没有问题的
- 如果服务器返回的数据code字段200,代表服务器返回数据成功
- 整个项目,接口前缀都有/api字样
7,Axios二次封装
为什么需要二次封装
- 请求拦截器, 响应拦截器
安装axiosnpm i axios
src/api/request.js
- 导入axios
- 1,
axios.create
配置- 配置基础路径, baseURL
- 请求超市时间,
timeout
- 2,请求拦截器–request(config)
- 3,响应拦截器–response(success回调,error回调)
- 4,对外暴露
//导入axios
import axios from "axios";
const myAxios = axios.create({
//配置对象
//基础路径, 发请求的时候,路径中会出现api
baseURL:'/api',
timeout:5000,//请求超时时间
});
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
return config
});
//响应拦截器
myAxios.interceptors.response.use((response)=>{
//响应成功的回调函数
return response.data
},(error)=>{
//响应失败的回调函数
console.log(error)
return Promise.reject(new Error('faile'))
})
export default myAxios;
8,接口统一管理
http://gmall-h5-api.atguigu.cn
/api/product/getBaseCategoryList
项目很小:完全可以在组件的生命周期函数中发请求
项目大: axios.get(‘xxx’)
/api/index.js
//当前这个模块, API进行同一管理
import request from "@/api/request";
///product/getBaseCategoryList
export const reqCategoryList = ()=>{
//发请求
return request({url:'/product/getBaseCategoryList',method:'get'})
}
8.1跨域问题
什么是跨域:协议、域名、端口号不同请求
配置代理(webpack)
DevServer | webpack 中文文档 (docschina.org)
vue.config.js中添加配置
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
//关闭eslint
lintOnSave:false,
//代理跨域
devServer: {
proxy:{
'/api':{
target:'http://gmall-h5-api.atguigu.cn'
}
}
},
})
9,nprogress进度条的使用
npm i --save nprogress
-
在请求拦截器中引入
-
//引入进度条 import nprogress from 'nprogress'; //start:进度条开始, done :进度条结束 //引入进度条样式 import 'nprogress/nprogress.css'
-
-
在请求拦截器中执行start()
nprogress.start()
-
响应拦截器中(成功)执行done()
nprogress.done();
修改进度条颜色
修改css文件夹
效果
10,vuex状态管理库
vuex是官方提供一个插件,状态管理库,集中式管理项目中组件共用的数据。
- 项目大需要才使用
安装
- Vue2中,要用Vuex的3版本
- Vue3中,要用Vuex的4版本
这里使用的是vue2, 所以安装vuex3
npm i vuex@3
配置vuex, /src/strore/index.js
import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
//state:存储数据
const state = {};
//mutations: 修改state的唯一手段
const mutations = {};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default new Vuex.Store({
state,
mutations,
actions,
getters,
});
在main.js中引入, 注册仓库
//引入仓库
import store from './store'
new Vue({
render: h => h(App),
//注册路由
router,
//注册仓库: 组件实例上就会多一个$store属性
store
}).$mount('#app')
模块化开发
store_home.js
const state = {
};
//mutations: 修改state的唯一手段
const mutations = {
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
index.js
import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
//引入小仓库
import store_home from "@/store/store_home";
import store_search from "@/store/store_search";
//对外暴露
export default new Vuex.Store({
//导入模块
modules:{
store_search,
store_home
}
});
TypeNav三级联动功能
11:完成TypeNav三级联动展示数据业务
store_home.js
import {reqCategoryList} from "@/api";
//home模块小仓库
const state = {
//state数据默认初始值要和服务器返回的类型一致
categoryList:[]
};
//mutations: 修改state的唯一手段
const mutations = {
CATEGORYLIST(state,value){
state.categoryList = value
}
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
async categoryList({commit}){
let result = await reqCategoryList();
if (result.code==200){
commit('CATEGORYLIST',result.data)
}
}
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
TypeNav.vue
computed:{
...mapState({
//右侧需要是一个函数, 当使用这个计算属性时, 右侧函数就会立即执行一次
//注入一个参数state, 这个state是大仓库的数据, 要指向对应小仓库
/* categoryList:(state)=>{
return state.store_home.categoryList;
}*/
//简写
categoryList:state=>state.store_home.categoryList
})
}
拿到数据后展示页面v-for
<template>
<!-- 商品分类导航 -->
<div class="type-nav">
<!-- <h1>{{categoryList}}</h1>-->
<div class="container">
<h2 class="all">全部商品分类</h2>
<nav class="nav">
<a href="###">服装城</a>
<a href="###">美妆馆</a>
<a href="###">尚品汇超市</a>
<a href="###">全球购</a>
<a href="###">闪购</a>
<a href="###">团购</a>
<a href="###">有趣</a>
<a href="###">秒杀</a>
</nav>
<div class="sort">
<div class="all-sort-list2">
<div class="item" v-for="c1 in categoryList" :key="c1.categoryId">
<h3>
<a href="">{{c1.categoryName}}</a>
</h3>
<div class="item-list clearfix">
<div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a href="">{{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: "TypeNav",
//组件挂载完毕, 向服务器发请求
mounted() {
//通知vuex发请求, 获取数据, 存储于仓库中
this.$store.dispatch('categoryList')
},
computed:{
...mapState({
//右侧需要是一个函数, 当使用这个计算属性时, 右侧函数就会立即执行一次
//注入一个参数state, 这个state是大仓库的数据, 要指向对应小仓库
/* categoryList:(state)=>{
return state.store_home.categoryList;
}*/
//简写
categoryList:state=>state.store_home.categoryList
})
}
}
</script>
<style scoped lang="less">
.all-sort-list2 { height: 450px; overflow: hidden;}
.type-nav {
border-bottom: 2px solid #e1251b;
.container {
width: 1200px;
margin: 0 auto;
display: flex;
position: relative;
.all {
width: 210px;
height: 45px;
background-color: #e1251b;
line-height: 45px;
text-align: center;
color: #fff;
font-size: 14px;
font-weight: bold;
}
.nav {
a {
height: 45px;
margin: 0 22px;
line-height: 45px;
font-size: 16px;
color: #333;
}
}
.sort {
position: absolute;
left: 0;
top: 45px;
width: 210px;
height: 461px;
position: absolute;
background: #fafafa;
z-index: 999;
.all-sort-list2 {
.item {
h3 {
line-height: 30px;
font-size: 14px;
font-weight: 400;
overflow: hidden;
padding: 0 20px;
margin: 0;
a {
color: #333;
}
}
.item-list {
display: none;
position: absolute;
width: 734px;
min-height: 460px;
background: #f7f7f7;
left: 210px;
border: 1px solid #ddd;
top: 0;
z-index: 9999 !important;
.subitem {
float: left;
width: 650px;
padding: 0 4px 0 8px;
dl {
border-top: 1px solid #eee;
padding: 6px 0;
overflow: hidden;
zoom: 1;
&.fore {
border-top: 0;
}
dt {
float: left;
width: 54px;
line-height: 22px;
text-align: right;
padding: 3px 6px 0 0;
font-weight: 700;
}
dd {
float: left;
width: 415px;
padding: 3px 0 0;
overflow: hidden;
em {
float: left;
height: 14px;
line-height: 14px;
padding: 0 8px;
margin-top: 5px;
border-left: 1px solid #ccc;
}
}
}
}
}
&:hover {
.item-list {
display: block;
}
}
}
}
}
}
}
</style>
效果
鼠标移动到一级分类时添加背景颜色
<template>
<!-- 商品分类导航 -->
<div class="type-nav">
<!-- <h1>{{categoryList}}</h1>-->
<div class="container">
<!--事件委派给父亲完成 -->
<div @mouseleave="leaveIndex"><!--离开清除样式 -->
<h2 class="all">全部商品分类</h2>
<div class="sort">
<div class="all-sort-list2">
<div class="item" v-for="(c1,index) in categoryList"
:key="c1.categoryId"
:class="{cur:currentIndex===index}"<!--鼠标移动到对应的目录添加样式 -->
>
<h3 @mouseenter="changeIndex(index)">
<a href="">{{c1.categoryName}}</a>
</h3>
<div class="item-list clearfix">
<div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a href="">{{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
<nav class="nav">
<a href="###">服装城</a>
<a href="###">美妆馆</a>
<a href="###">尚品汇超市</a>
<a href="###">全球购</a>
<a href="###">闪购</a>
<a href="###">团购</a>
<a href="###">有趣</a>
<a href="###">秒杀</a>
</nav>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: "TypeNav",
data(){
return {
currentIndex : -1,
}
},
methods:{
changeIndex(index){
this.currentIndex = index
},
leaveIndex(){
this.currentIndex = -1
}
}
}
</script>
<style scoped lang="less">
/*填加cur背景样式*/
.all-sort-list2 {
height: 450px;
overflow: hidden;
.cur{
background: skyblue;
}
}
</style>
效果
2,通过Js控制二三级商品分类的显示与隐藏
最开始的时候,是通过css样式display: block |none显示与隐藏二三级商品分类
去掉原来的css
&:hover {
.item-list {
display: block;
}
}
修改为动态style
<!-- 二三级分类 -->
<div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">
12,演示卡顿现象(正常-节流-防抖)
-
快速滑动时,会触发不完全,浏览器反应不过来
-
如果业务过多, 可能会出现卡顿
-
正常:
- 事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿) 41
-
节流:
- 在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
-
防抖:
- 前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发只会执行一次
节流(规定时间内只触发第一次)
防抖(规定时间内只触发最后一次)
1,lodash插件安装
Lodash 简介 | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)
浏览器环境:
<script src="lodash.js"></script>
通过 npm:
$ npm i -g npm
$ npm i --save lodash
2,防抖 _.debounce
_.debounce(func, [wait=0], [options=])
创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait
毫秒后调用 func
方法。 debounced(防抖动)函数提供一个 cancel
方法取消延迟的函数调用以及 flush
方法立即调用。 可以提供一个 options(选项) 对象决定如何调用 func
方法,options.leading
与|或 options.trailing
决定延迟前后如何触发(注:是 先调用后等待 还是 先等待后调用)。 func
调用时会传入最后一次提供给 debounced(防抖动)函数 的参数。 后续调用的 debounced(防抖动)函数返回是最后一次 func
调用的结果。
注意: 如果 leading
和 trailing
选项为 true
, 则 func
允许 trailing 方式调用的条件为: 在 wait
期间多次调用防抖方法。
如果 wait
为 0
并且 leading
为 false
, func
调用将被推迟到下一个点,类似setTimeout
为0
的超时。
SeeDavid Corbacho’s articlefor details over the differences between_.debounce
and_.throttle
.
参数
func
(Function): 要防抖动的函数。[wait=0]
(number): 需要延迟的毫秒数。[options=]
(Object): 选项对象。[options.leading=false]
(boolean): 指定在延迟开始前调用。[options.maxWait]
(number): 设置func
允许被延迟的最大值。[options.trailing=true]
(boolean): 指定在延迟结束后调用。
返回
(Function): 返回新的 debounced(防抖动)函数。
例子
//防抖前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发只会执行一次
let input = document.querySelector ('input');
//文木发生变化立即执行
input.oninput =_.debounce(function(){
console.log('ajax请求')
}, 1000);
//lodash插件:里面封装函数的防抖与节流的业务【闭包+延迟器】
//1:lodash函数库对外暴露-函数
3,节流 _.throttle
_.throttle(func, [wait=0], [options=])
创建一个节流函数,在 wait 秒内最多执行 func
一次的函数。 该函数提供一个 cancel
方法取消延迟的函数调用以及 flush
方法立即调用。 可以提供一个 options 对象决定如何调用 func
方法, options.leading 与|或 options.trailing 决定 wait 前后如何触发。 func
会传入最后一次传入的参数给这个函数。 随后调用的函数返回是最后一次 func
调用的结果。
注意: 如果 leading
和 trailing
都设定为 true
则 func
允许 trailing 方式调用的条件为: 在 wait
期间多次调用。
如果 wait
为 0
并且 leading
为 false
, func
调用将被推迟到下一个点,类似setTimeout
为0
的超时。
查看David Corbacho’s article 了解_.throttle
与_.debounce
的区别。
参数
func
(Function): 要节流的函数。[wait=0]
(number): 需要节流的毫秒。[options=]
(Object): 选项对象。[options.leading=true]
(boolean): 指定调用在节流开始前。[options.trailing=true]
(boolean): 指定调用在节流结束后。
例子
在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
//获取节点
let span = document.querySelector('span');
let button = document.querySelector('button');
let count = 0;
//计数器:在一秒以内,数字只能加上1
button.onclick =_.throttle(function ()f
count++;
span.innerHTML = count;
console.log('执行');
], 1000);
4,完成三级联动菜单的节流操作
检查node_modules有无lodash
TypeNav.vue
//全部引入lodash
// import _ from 'lodash'
//按需引入lodash
import throttle from 'lodash/throttle'
//========================================================================
methods:{
// changeIndex(index){
// console.log(index)
// this.currentIndex = index
// },
//节流throttle回调函数别用箭头函数,可能出现上下文this
changeIndex:throttle(function (index) {
console.log(index)
this.currentIndex = index
},50),
13,三级联动组件的路由跳转与传递参数
三级联动用户可以点击的:一级分类、二级分类、三级分类,当你点击的时候
Home模块跳转到Search模块,一级会把用户选中的产品(产品的名字、产品的ID)在路由跳转的时候,进行传递
路由跳转:
- 声明式导航:router-link
- 如果使用声明式导航router-link,可以实现路由的跳转与传递参数。
- 但是需要注意,出现卡顿现象。
- router-link:可以一个组件,当服务器的数据返回之后,循环出很多的router-link组件【创建组件实例的】1000+
- 创建组件实例的时候,一瞬间创建1000+很好内存的,因此出现了卡顿现象。
- 编程式导航: push| replace
- 利用时间委派(给父节点)+编程式导航实现路由跳转与传递参数
- 存在的问题
- 怎么判断点击的是a标签?
event.target.nodeName=='a'
- 传递参数问题
- 怎么判断点击的是a标签?
<!--goSearch事件委派给父亲完成 -->
<div class="all-sort-list2" @click="goSearch($event)">
<div class="item" v-for="(c1,index) in categoryList"
:key="c1.categoryId"
:class="{cur:currentIndex===index}"
>
<h3 @mouseenter="changeIndex(index)">
<a :data-categoryname="c1.categoryName"
:data-category1Id="c1.categoryId">
{{c1.categoryName}}
</a>
<!-- <router-link to="/search">{{c1.categoryName}}</router-link>-->
</h3>
<!-- 二三级分类 -->
<div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">
<div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a :data-categoryname="c2.categoryName"
:data-category2Id="c2.categoryId" >
{{c2.categoryName}}
</a>
<!-- <router-link to="/search">{{c2.categoryName}}</router-link>-->
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
<a :data-categoryname="c3.categoryName"
:data-category3Id="c3.categoryId">
{{ c3.categoryName }}
</a>
<!-- <router-link to="/search">{{c3.categoryName}}</router-link>-->
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
methods:{
// changeIndex(index){
// console.log(index)
// this.currentIndex = index
// },
//节流throttle回调函数别用箭头函数,可能出现上下文this
changeIndex:throttle(function (index) {
this.currentIndex = index
},50),
leaveIndex(){
this.currentIndex = -1;
if (this.$route.path!='/home'){
this.showNav = false
}
},
enterIndex(){
if (this.$route.path!='/home'){
this.showNav = true
}
},
goSearch(event){
//第一个问题:把子节点当中a标签,我加上自定义属性data-categoryName,其余的子节点是没有的
let elment = event.target;
//获取到当前出发这个事件的节点【h3、a、dt、dl】,需要带有data-categoryname这样节点【一定是a标签】
//节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
let {categoryname,category1id,category2id,category3id} = elment.dataset;
//如果标签身上拥有categoryname一定是a标签
if (categoryname){
//整理路由跳转的参数
let location = {name:'search'};
let query = {categoryname:categoryname}
//一级分类、二级分类、三级分类的a标签
if (category1id){
// console.log('@@',category1id);
query.category1Id = category1id;
}else if (category2id){
// console.log('@@',category2id)
query.category2Id = category2id;
}else if (category3id) {
// console.log(category3id)
query.category3Id = category3id;
}
//整理完参数
if (this.$route.params){//如果有params参数合并到参数中
location.params = this.$route.params
}
location.query = query
this.$router.push(location)
}
}
}
效果
14,三级列表动态形式总结
- 获取服务器数据: 解决跨域问题
- jsonp
- pro
- 代理服务器
- 函数防抖和节流
- 路由跳转
- 声明式导航(router-link): 会创建组件实例, 过多消耗内存,导致卡顿
- 编程式导航:
- 事件委派
- 使用自定义属性—区分标签, 区分层级目录
15,Search模块中的TypeNav商品分类菜单显示, 动画效果
显示隐藏
给标签添加鼠标事件,
v-show控制菜单的显示, 默认showNav=true
<div @mouseleave="leaveIndex" @mouseenter="enterIndex" >
<h2 class="all">全部商品分类</h2>
<!--三级联动 -->
<div class="sort" v-show="showNav">
path不是home才会有变化
leaveIndex(){
this.currentIndex = -1;
if (this.$route.path!='/home'){
this.showNav = false
}
},
enterIndex(){
if (this.$route.path!='/home'){
this.showNav = true
}
},
过度动画
前提组件|元素务必要有v-iflv-show指令才可以进行过渡动画
给三级联动加一层transition
<!--过度动画 -->
<transition name="sort">
<!--三级联动 -->
<div class="sort" v-show="showNav"></div>
</transition>
style中添加样式
//过度动画的样式
//开始状态
.sort-enter{
height: 0;
//transform: rotate(0deg);/*旋转*/
}
//结束状态
.sort-enter-to{
height: 461px;
//transform: rotate(360deg);/*旋转*/
}
//定义动画时间, 速率
.sort-enter-active{
transition: all .5s linear;
overflow: hidden;
}
效果
16,三级列表优化
原来是卸载TypeNav.vue的挂载钩子上获取数据
//组件挂载完毕, 向服务器发请求
mounted() {
//通知vuex发请求, 获取数据, 存储于仓库中
this.$store.dispatch('categoryList');
}
这种方式会导致每切换一次就会向服务器发一次请求
解决
放到App.vue的mounted钩子里获取
根组件的mounted只会执行一次
export default {
name: 'App',
components: {
Header,Footer
},
mounted() {
//派发一个action, 获取分类三级列表数据
this.$store.dispatch('categoryList');
}
}
17,合并参数(query,params)
将header搜索参数(params)和TypeNav菜单参数(query)合并
Header.vue
如果有query参数就合并
goSearch(){
let location = {name:'search',params:{keyword:this.keyword || undefined}};
if (this.$route.query){ //如果有query参数就合并query参数
location.query = this.$route.query
}
this.$router.push(location)
}
TypeNav.vue
如果有params参数合并
goSearch(event){
//整理完参数
if (this.$route.params){//如果有params参数合并到参数中
location.params = this.$route.params
}
location.query = query
this.$router.push(location)
}
}
效果
既有query, 又有params
一,完善Home1(ListContainer)
1,mock数据(模拟)
生成随机数据,拦截 Ajax 请求
Home · nuysoft/Mock Wiki (github.com)
# 安装
npm install mockjs
#第一步: src中创建mock文件夹
#第二步: 创建json假数据,去掉空格
#3: 把mock数据需要的图片放到public目录下
#4: 创建mockServe.js通过mock.js创建虚拟数据
#5: mockServe.js在入口文件中引入(至少要执行一次,才能模拟数据)
json数据
首页广告轮播数据: src/mock/banners.json
[
{
"id":"1",
"imgUrl":"/images/banner1.jpg"
},
{
"id":"2",
"imgUrl":"/images/banner2.jpg"
},
{
"id":"3",
"imgUrl":"/images/banner3.jpg"
},
{
"id":"4",
"imgUrl":"/images/banner4.jpg"
}
]
首页楼层数据: src/mock/floors.json
[{
"id":"001",
"name":"家用电器",
"keywords":["节能补贴","4K电视","空气净化器","IH电饭煲","滚筒洗衣机","电热水器"],
"imgUrl":"/images/floor-1-1.png",
"navList":[{
"url":"#",
"text":"热门"
},
{
"url":"#",
"text":"大家电"
},
{
"url":"#",
"text":"生活电器"
},
{
"url":"#",
"text":"厨房电器"
},
{
"url":"#",
"text":"应季电器"
},
{
"url":"#",
"text":"空气/净水"
},
{
"url":"#",
"text":"高端电器"
}
],
"carouselList":[{
"id":"0011",
"imgUrl":"/images/floor-1-b01.png"
},
{
"id":"0012",
"imgUrl":"/images/floor-1-b02.png"
},
{
"id":"0013",
"imgUrl":"/images/floor-1-b03.png"
}
],
"recommendList":[
"/images/floor-1-2.png",
"/images/floor-1-3.png",
"/images/floor-1-5.png",
"/images/floor-1-6.png"
],
"bigImg":"/images/floor-1-4.png"
},
{
"id":"002",
"name":"手机通讯",
"keywords":["节能补贴2","4K电视2","空气净化器2","IH电饭煲2","滚筒洗衣机2","电热水器2"],
"imgUrl":"/images/floor-1-1.png",
"navList":[{
"url":"#",
"text":"热门2"
},
{
"url":"#",
"text":"大家电2"
},
{
"url":"#",
"text":"生活电器2"
},
{
"url":"#",
"text":"厨房电器2"
},
{
"url":"#",
"text":"应季电器2"
},
{
"url":"#",
"text":"空气/净水2"
},
{
"url":"#",
"text":"高端电器2"
}
],
"carouselList":[{
"id":"0011",
"imgUrl":"/images/floor-1-b01.png"
},
{
"id":"0012",
"imgUrl":"/images/floor-1-b02.png"
},
{
"id":"0013",
"imgUrl":"/images/floor-1-b03.png"
}
],
"recommendList":[
"/images/floor-1-2.png",
"/images/floor-1-3.png",
"/images/floor-1-5.png",
"/images/floor-1-6.png"
],
"bigImg":"/images/floor-1-4.png"
}
]
mockServe.js
//引入mockjs模块
import Mock from 'mockjs';
//引入json数据(json数据没有对外暴露也能引入)
//webpack默认暴露的: 图片, json数据
import banner from './banner.json';
import floor from './floor.json'
//mock数据: 参数1:请求地址, 参数2:请求数据
Mock.mock("/mock/banner",{code:200,data:banner});//模拟首页轮播图数据
Mock.mock("/mock/floor",{code:200,data:floor});//底部的家用电器数据
入口文件引入main.js
//引入MockServe.js
import '@/mock/mockServe'
2,获取轮播图的数据
1,创建src/api/request_mock.js
把原来的复制一份, 修改baseURL
//导入axios
import axios from "axios";
//引入进度条
import nprogress from 'nprogress';
//start:进度条开始, done :进度条结束
//引入进度条样式
import 'nprogress/nprogress.css'
const myAxios = axios.create({
//配置对象
//基础路径, 发请求的时候,路径中会出现api
baseURL:'/mock',
timeout:5000,//请求超时时间
});
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
//进度条开始动
nprogress.start();
return config
});
//响应拦截器
myAxios.interceptors.response.use((response)=>{
//进度条结算
nprogress.done();
//响应成功的回调函数
return response.data
},(error)=>{
//响应失败的回调函数
console.log(error)
return Promise.reject(new Error('faile'))
})
export default myAxios;
2,/src/api/index.js添加request_mock
//当前这个模块, API进行同一管理
import request from "@/api/request";
import request_mock from "@/api/request_mock";
///product/getBaseCategoryList
export const reqCategoryList = ()=>{
//发请求
return request({url:'/product/getBaseCategoryList',method:'get'})
}
/*export const reqMockBannerList = ()=>{
return request_mock({url:'/banner',method:'get'})
}*/
//简写
export const reqMockBannerList = ()=>request_mock.get('/banner')
3,vuex仓库store添加获取bannerlist的方法数据
store_home.js
import {reqCategoryList, reqMockBannerList} from "@/api";//1,把reqMockBannerList添加进来
//home模块小仓库
const state = {
//state数据默认初始值要和服务器返回的类型一致
categoryList:[],
//2,添加bannerList空数组(类型根据json)
bannerList:[]
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
async categoryList({commit}){
let result = await reqCategoryList();
if (result.code==200){
commit('CATEGORYLIST',result.data)
}
},
async getBannerList({commit}){//3,获取首页轮播图的数据
let result = await reqMockBannerList();
console.log(result)
if (result.code==200){
commit('GETBANNERLIST',result.data)
}
}
};
//mutations: 修改state的唯一手段
const mutations = {
CATEGORYLIST(state,value){
state.categoryList = value
},
GETBANNERLIST(state,value){//4,覆盖仓库的空数组bannerList
state.bannerList = value
}
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
const getters = {};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
4,ListContainer.vue发请求获取bannerList
<script>
import {mapState} from "vuex";
export default {
name: "ListContainer",
mounted() {
//通知vuex发请求, 获取数据, 存储于仓库中
this.$store.dispatch('getBannerList');
console.log(this.$store.state.store_home)
},
computed:{
...mapState({
bannerList:(state)=>{
return state.store_home.bannerList
}
})
}
}
</script>
5,检查是否获取到
3,将轮播图数据放到页面中
1,swiper
第一步:引包(相应Jscss)
第二步:页面中结构务必要有
第三步(页面当中务必要有结构): new Swiper实例【轮播图添加动态效果】
2,ListContainer组件使用swiper
安装swiper
npm i swiper@5
第一步:引包(相应Jscss)
ListContainer.vue中引入swiper
import Swiper from 'swiper'
引入样式
//在main.js中引入swiper样式 (由于很多地方需要使用, 直接到入口文件引入)
import 'swiper/css/swiper.css'
二, 页面遍历
<div class="swiper-container" :ref="mySwiper">
<div class="swiper-wrapper" >
<div class="swiper-slide" v-for="carousel in bannerList" :key="carousel.id">
<img :src="carousel.imgUrl" />
</div>
</div>
三,ListContainer.vue中创建swiper
- 直接new的话bannerList的数据还没到页面中
- 可使用定时器,如果请求超过定时时间也会没有效果
- 最好解决方案是watch+nextTick(数据监听: 监听已有数据变化)
- $nextTick:在下次DOM更新循环结束之后执行延迟回调。在 修改数据之后 立即使用这个方法,获取更新后的DOM。
- $nextTick:可以保证也页面中的解构一定是有的,经常和很多插件一起使用【都需要DOM存在了】
watch:{
//监听bannerList数据的变化, 由空数组变为请求后的4个元素
bannerList:{
handler(){
//现在咱们通过watch监听bannerList属性的属性值的变化
//如果执行handler方法,代表组件实例身上这个属性的属性以己经有了【数组:四个元素】
//当前这个函数执行:只能保证pannerList数据已经有了,但是你没办法保证v-for已经执行结束了
//v-for执行完毕,才有结构【你现在在watch当中没办法保证的】
//netxTick:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DoM。
this.$nextTick(()=>{
var mySwiper = new Swiper(this.$refs.mySwiper, {
loop: true,
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable:true //小球点击跳转
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
console.log(mySwiper)
});
}
}
}
一, 完善Home2(Floor)
1,完成floorList的获取
1.1,api设置
src/api/index.js
//获取floor数据
export const reqMockFloorList = ()=>request_mock.get('/floor')
1.2,vuex三步曲(action-mutations-state)
state
floorList:[]
actions
async getFloorList({commit}){//获取floor数据
let result = await reqMockFloorList();
console.log('getFloorList',result)
if (result.code == 200){
commit('REQMOCKFLOORLIST',result.data);
}
}
mutations
REQMOCKFLOORLIST(state,value){
state.floorList = value
}
2,在home中获取仓库数据,传给floor
组件通信的方式有哪些?
- props:用于父子组件通信
- 自定义事件:@on @emit可以实现子给父通信
- 全局事件总线:$bus 全能
- pubsub-js:vue当中几乎不用全能
- 插槽
- vuex
<!--
采用props方式传给floor
v-for也可以在自定义标签中使用
-->
<Floor v-for="floor in floorList" :key="floor.id" :list="floor"></Floor>
import {mapState} from "vuex";
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: "Home",
components:{
ListContainer,Recommend,Rank,Like,Floor,Brand
},
mounted() {
//vuex-action方法获取floorList
this.$store.dispatch('getFloorList')
},
computed:{
//从仓库获取floorLIst
...mapState({
floorList:(state)=>{
return state.store_home.floorList
}
})
}
3,floor中展示数据
<template>
<!--楼层-->
<div class="floor">
<div class="py-container" >
<div class="title clearfix">
<h3 class="fl">{{list.name}}</h3>
<div class="fr">
<ul class="nav-tabs clearfix" v-for="(nav,index) in list.navList" :key="index">
<li class="active">
<a href="#tab1" data-toggle="tab">{{nav.text}}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane">
<div class="floor-1">
<div class="blockgary">
<ul class="jd-list">
<li v-for="(kw,index) in list.keywords" :key="index">{{kw}}</li>
</ul>
<div>
<img :src="list.imgUrl" />
</div>
</div>
<div class="floorBanner">
<!-- 轮播图 -->
<div class="swiper-container" ref="floorSwiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="cr in list.carouselList" :key="cr.id">
<img :src="cr.imgUrl">
</div>s
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="list.recommendList[0]" />
</div>
<div class="floor-conver-pit">
<img :src="list.recommendList[1]" />
</div>
</div>
<div class="split center">
<img :src="list.bigImg" />
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="list.recommendList[2]" />
</div>
<div class="floor-conver-pit">
<img :src="list.recommendList[3]" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Swiper from "swiper";
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: "Floor",
props:['list'],
//第一次写swiper的时候在mounted中不可以, 为什么这里可以了?
//这次写轮播图时, 数据没有在floor中发, 直接从父组件props获取的
mounted() {
this.$nextTick(()=>{
var mySwiper = new Swiper(this.$refs.floorSwiper, {
loop: true,
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable:true //小球点击跳转
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
console.log(mySwiper)
});
}
}
</script>
4,将轮播图封装成全局组件(复用)
4.1,先把ListContainer和floor写法改一致
原来:
- ListContainer: 在watch监听中实现
- floor: 在computed中
把floor也改成监听
watch: {
list:{
immediate:true,//立即监听:不管数据有没有改变, 上来就监听一次
handler(){//直接监听不到, 因为数据从home传过来就没有改变
this.$nextTick(()=>{
var mySwiper = new Swiper(this.$refs.floorSwiper, {
loop: true,
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable:true //小球点击跳转
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
console.log(mySwiper)
});
}
}
4.2,components下新建全局组件Carousel
- 注意:传入的数据叫
carouselList
<template>
<!-- 轮播图 -->
<div class="swiper-container" ref="floorSwiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="cr in carouselList" :key="cr.id">
<img :src="cr.imgUrl">
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
import Swiper from "swiper";
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: "Carousel",
props:['carouselList'],
watch: {
carouselList:{
immediate:true,//立即监听:不管数据有没有改变, 上来就监听一次
handler(){//直接监听不到, 因为数据从home传过来就没有改变
this.$nextTick(()=>{
var mySwiper = new Swiper(this.$refs.floorSwiper, {
loop: true,
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable:true //小球点击跳转
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
console.log(mySwiper)
});
}
}
}
}
</script>
<style scoped>
</style>
4.3,main.js中注册为全局组件
//注册轮播图全局组件
import Carousel from "@/components/Carousel/Carousel";
// eslint-disable-next-line vue/multi-word-component-names
Vue.component('Carousel',Carousel)
4.4,修改Floor和ListContainer
去掉原来的写法,新增Carousel标签即可
<!-- 将原来的轮播图改为全局组件Carousel-->
<Carousel :carouselList="list.carouselList"></Carousel>
二,搜索模块开发(search)
- 静态页面+静态组件拆分
- 发请求API
- vuex(action-mutations-state)–请求返回存放到仓库中
- 组件获取仓库数据, 动态展示数据
2.1,获取search模块数据
1,请求API
src/api/index.js
//获取search模块数据, 地址:/api/list 请求方式post, 需要带参数
//当前这个函数需不需要按受外部传递参数
//当前这个接口(获取搜索模块的数据),给服务器传递一个默认参数【至少是一个空对象】
//如reqGetSearchInfo({})
export const reqGetSearchInfo = (params)=>request({url:'/list',method:'post',data:params})
2,vuex
src/store/store_search.js
import {reqGetSearchInfo} from "@/api";
const state = {
searchInfo: {}
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
async getSearchList({commit},params={}){
//调用api中reqGetSearchInfo函数时至少传递一个参数
//params形参, 是用户派发action时, 第二个参数传过来的
let result = await reqGetSearchInfo(params);
console.log(result)
if (result.code==200){
commit('GETSEARCHLIST',result.data)
}
}
};
//mutations: 修改state的唯一手段
const mutations = {
GETSEARCHLIST(state,value){
state.searchInfo = value
}
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
//当前形参state相当于当前仓库的state
goodsList(state) {
//如果没有网络goodsList返回是undefined,所以以防万一返回空数组[]
return state.searchList.goodsList || []
},
trademarkList(state) {
return state.searchList.trademarkList || []
},
attrsList(state) {
return state.searchList.attrsList || []
},
};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
3,search中获取展示页面
<script>
import SearchSelector from './SearchSelector/SearchSelector'
import {mapGetters} from "vuex";
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Search',
components: {
SearchSelector
},
computed:{
//mapGetters写法: 传递数组, 因为getters计算没有划分模块
...mapGetters(['goodsList'])
},
mounted() {
//测试接口数据
this.$store.dispatch("getSearchList",{})
}
}
</script>
4,页面展示
<div class="goods-list">
<ul class="yui3-g">
<li class="yui3-u-1-5" v-for="goods in goodsList" :key="goods.id">
<div class="list-wrap">
<div class="p-img">
<a href="item.html" target="_blank"><img :src="goods.defaultImg" /></a>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>{{goods.price}}</i>
</strong>
</div>
<div class="attr">
<a target="_blank" href="item.html" title="促销信息,下单即赠送三个月CIBN视频会员卡!【小米电视新品4A 58 火爆预约中】">
{{goods.title}}
</a>
</div>
<div class="commit">
<i class="command">已有<span>2000</span>人评价</i>
</div>
<div class="operate">
<a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
</div>
</div>
</li>
</ul>
</div>
这数据…被改坏了
2.2,通过参数发送请求
通过mounted只能发送一次
<script>
import SearchSelector from './SearchSelector/SearchSelector'
import {mapGetters} from "vuex";
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Search',
data(){
return {
searchParams:{
//一级分类的id
category1Id: "",
//二级分类id
category2Id:"",
//三级分类的id
category3Id:"",
//分类名字
categoryName:"",
//关键字
keyword: "",
//排序
order: "",
//分页器用的:代表的是当前是第几页
pageNo: 1,
//代表的是每一个展示数据个数
pageSize: 20,
//平台售卖属性换作带的参数
props: [],
//品牌
trademark: "",
}
}
},
components: {
SearchSelector
},
computed:{
//mapGetters写法: 传递数组, 因为getters计算没有划分模块
...mapGetters(['goodsList'])
},
beforeMount() {//组件挂载完毕之前执行, 先与mounted
//挂载完毕之前把query, params参数合并到searchParams中
//复杂写法
// this.searchParams. categorylId = this.Sroute.query.category1Id;
// this.searchParams.category2Id = this.$route.query.category2Id;
// this.searchParams.category3Id = this.Sroute.query.category3Id;
// this.searchParams.categoryName = this.Sroute.query.categoryName;
// this.searchParams. keyword = this. Sroute.params. keyword;
//Object.assign: ES6新增的语法,合并对象
Object.assign(this.searchParams, this.$route.query, this.$route.params);
},
mounted() { //仅执行一次
//放到函数中
// this.$store.dispatch("getSearchList",this.searchParams)
this.getSearchData()
},
methods:{
getSearchData(){
//获取search数据
this.$store.dispatch("getSearchList",this.searchParams)
}
}
}
</script>
2.3,SearchSelector动态展示数据
search组件的子组件SearchSelector, 分类模块
<template>
<div class="clearfix selector">
<div class="type-wrap logo">
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list">
<li v-for="td in trademarkList" :key="td.tmId">{{td.tmName}}</li>
</ul>
</div>
<div class="ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
<a href="javascript:void(0);">更多</a>
</div>
</div>
<div class="type-wrap" v-for="attr in attrsList" :key="attr.attrId">
<div class="fl key">{{attr.attrName}}</div>
<div class="fl value">
<ul class="type-list">
<li v-for="(attrValue,index) in attr.attrValueList" :key="index">
<a>{{attrValue}}</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
</div>
</template>
<script>
import {mapGetters} from "vuex";
export default {
name: 'SearchSelector',
computed:{
...mapGetters(['attrsList','trademarkList'])
}
}
</script>
2.4,解决搜索只能搜索一次的问题
原来是放在mounted中只能搜索一次
mounted() { //仅执行一次
//放到函数中
// this.$store.dispatch("getSearchList",this.searchParams)
this.getSearchData()
},
methods:{
getSearchData(){
//获取search数据
this.$store.dispatch("getSearchList",this.searchParams)
}
}
每次搜索
$route
都会发生变化, 所以可以监听$route
mounted() { //仅执行一次
//放到函数中
// this.$store.dispatch("getSearchList",this.searchParams)
//第一次需要合并到searchParams中, 让面包屑显示
Object.assign(this.searchParams, this.$route.query, this.$route.params)
this.getSearchData()
},
methods:{
getSearchData(){
//挂载完毕之前把query, params参数合并到searchParams中
//复杂写法
// this.searchParams. categorylId = this.Sroute.query.category1Id;
// this.searchParams.category2Id = this.$route.query.category2Id;
// this.searchParams.category3Id = this.Sroute.query.category3Id;
// this.searchParams.categoryName = this.Sroute.query.categoryName;
// this.searchParams. keyword = this. Sroute.params. keyword;
//Object.assign: ES6新增的语法,合并对象
//Object.assign(this.searchParams, this.$route.query, this.$route.params)
//获取search数据
this.$store.dispatch("getSearchList",this.searchParams)
},
},
watch:{
$route:{//路由信息变化就重新发起请求
handler(){
console.log(this.searchParams)
//合并参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
this.getSearchData()
//每次请求完毕后清空1,2,3级分类id
this.searchParams.category1Id= undefined;
this.searchParams.category2Id= undefined;
this.searchParams.category3Id= undefined;
}
}
}
2.5,面包屑处理分类
<!--面包屑-->
<ul class="fl sui-tag">
<!--分类的面包屑-->
<li class="with-x" v-show="searchParams.categoryName" @click="removeCateGoryName">
{{searchParams.categoryName}}<i >x</i>
</li>
</ul>
methods:{
removeCateGoryName(){
//undefined请求不会带给服务器
this.searchParams.categoryName = undefined;//清空对应名字
this.getSearchData()//重新发请求
}
}
1,发现清空搜索之后地址没有改变
removeCateGoryName(){
//undefined请求不会带给服务器
this.searchParams.categoryName = undefined;//清空对应名字
// this.getSearch();//重新发请求
//地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
this.$router.push({name :'search',params:this.$route.params})
}
2,搜索的时候把keyword放到面包屑中
<!--面包屑-->
<ul class="fl sui-tag">
<!--分类的面包屑-->
<li class="with-x" v-show="searchParams.categoryName" @click="removeCateGoryName">
{{searchParams.categoryName}}<i >x</i>
</li>
<!--关键字的面包屑-->
<li class="with-x" v-show="searchParams.keyword" @click="removeKeyword">
{{searchParams.keyword}}<i>x</i>
</li>
</ul>
methods:{
removeCateGoryName(){
//undefined请求不会带给服务器
this.searchParams.categoryName = undefined;//清空对应名字
// this.getSearch();//重新发请求
//地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
this.$router.push({name :'search',params:this.$route.params})
},
removeKeyword(){
this.searchParams.keyword = undefined;//清空对应名字
//地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
this.$router.push({name :'search',query:this.$route.query})
}
}
3,还需清除兄弟组件Header的输入框
设计组件通信:
- props:父
- 自定义事件:子父
- vuex:万能
- 插槽:父子
- pubsub-js:完成
- $bus:全局事件总线
这里回顾下全局事件总结
main.js,注册$bus
new Vue({
render: h => h(App),
//注册路由
router,
//注册仓库: 组件实例上就会多一个$store属性
store,
beforeCreate() {//注册全局时间总线
Vue.prototype.$bus = this
}
}).$mount('#app')
Header.vue中绑定自定义事件
mounted() {
this.$bus.$on('clearKeyword',()=>{
this.keyword = ''
})
}
Search.vue中调用自定义事件完成keyword清空
removeKeyword(){
this.searchParams.keyword = undefined;//清空对应名字
//地址栏也需要改,进行路由跳转,watch监听到有改变也会发请求
this.$router.push({name :'search',query:this.$route.query});
//清空header组件的文本框
this.$bus.$emit('clearKeyword');
}
4,加入品牌搜索
品牌来自Search子组件
SearchSelector.vue
, 需要给父组件传递参数, 可以使用自定义事件
1,Search父组件给子标签添加自定义事件
<SearchSelector @trademarkInfo="trademarkInfo"/>
//methods回调-------------------------------------------------
trademarkInfo(trademark){
//整理品牌名称字段参数, ID: 品牌名称
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
// this.$router.push({name :'search',query:this.$route.query,params:this.$route.params})
this.getSearchData()//重新发起请求
}
2,SearchSelector子组件调用传参
<ul class="logo-list">
<!--给品牌添加点击事件并传递品牌信息 -->
<li v-for="td in trademarkList" :key="td.tmId" @click="trademarkHandler(td)">{{td.tmName}}</li>
</ul>
//methods调用------------------------------------------
methods:{
trademarkHandler(trademark){
this.$emit('trademarkInfo',trademark);//调用自定义事件,传参
}
3,品牌搜索添加到面包屑
Search中添加标签
<!--品牌的面包屑-->
<li class="with-x" v-if="searchParams.trademark" @click="removeTrademark">
<!--split:将字符串转为数组, 注意不要使用v-show,split转undefiede会报错-->
{{searchParams.trademark.split(":")[1]}}<i>x</i>
</li>
//methods----------------------------------
removeTrademark(){//删除品牌信息
this.searchParams.trademark = undefined;//要使用v-show,这里要 = 空字符串""
//重新发起请求
this.getSearchData();
}
5,加入平台属性搜索(props)
1,同样的套路父子传递参数
Search
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/>
//methods------------------------
attrInfo(attr,attrValue){
//属性ID:属性值:属性名
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
//判断数组中是否存在属性, 存在再添加
if (this.searchParams.props.indexOf(props)==-1){
this.searchParams.props.push(props);
this.getSearchData()
}
}
2,子类调用
SearchSelector.vue
<!-- 添加点击事件, 传递属性 -->
<li v-for="(attrValue,index) in attr.attrValueList" :key="index" @click="attrInfo(attr,attrValue)">
<a>{{attrValue}}</a>
</li>
//methods-----------------------
attrInfo(attr,attrValue){
//属性ID:属性值:属性名
this.$emit('attrInfo',attr,attrValue)
}
3,属性添加面包屑
Search
<!--平台售卖属性的面包屑, 数组,需要遍历-->
<li class="with-x" v-for="(attr,index) in searchParams.props" :key="index" @click="removeProps(index)"><!--删除需要传数组下标-->
<!--属性名称也在数组1位上-->
{{attr.split(":")[1]}}<i>x</i>
</li>
//methods----------------------------------
removeProps(index){
//再次整理参数,删除对应下标的数据
this.searchParams.props.splice(index,1);
this.getSearchData()//再发请求
}
2.6,Search模块商品排序
接口排序字段order
1:综合,2:价格asc:升序,desc:降序
示例:“1:desc”
order属性的属性值最多有多少种写法:
- 1:asc
- 1:desc
- 2:asc
- 2:desc
index.html中导入样式
<link rel="stylesheet" href="https://at.alicdn.com/t/xxxx.css">
页面中使用样式就可以了: class=”iconfont icon-xxx”
Search.vue
<!--排序-->
<ul class="sui-nav">
<li :class="{active: isOrder1}" @click="orderBy('1')">
<a>综合 <span v-show="isOrder1" :class="icon"></span></a>
</li>
<li :class="{active: isOrder2}" @click="orderBy('2')">
<a>价格 <span v-show="isOrder2" :class="icon"></span></a>
</li>
</ul>
//计算属性-----------------------------------------------
computed:{
//mapGetters写法: 传递数组, 因为getters计算没有划分模块
...mapGetters(['goodsList']),
isOrder1(){//控制综合排序的显示(1)隐藏(2)
return this.searchParams.order.indexOf('1')!==-1
},
isOrder2(){//控制价格排序的显示(2)隐藏(1)
return this.searchParams.order.indexOf('2')!==-1
},
icon(){//控制矢量图的上下, asc上, desc下
let iconName = "iconfont ";
if (this.searchParams.order.indexOf('asc')!==-1){
iconName += "icon-xiangshang";
}else if(this.searchParams.order.indexOf('desc')!==-1){
iconName += "icon-xiangxia";
}
return iconName;
}
}
//methods-------------------------------------------------
import throttle from 'lodash/throttle'//按需引入节流
methods:{
orderBy:throttle(function (flag){//排序+节流
//flag:1是综合, 2是价格
let oldOrder = this.searchParams.order.split(':')[1];//查询之前的排序是asc还是desc
if (this.searchParams.order.includes(flag)){//如果排序类型没有改变就取反
oldOrder = oldOrder==='asc'? 'desc':'asc';//取反, asc就取desc
}
this.searchParams.order=`${flag}:${oldOrder}`;//如果改变了类型, 赋值给新的flag
this.getSearchData();//重新发起请求
},1000)
}
效果
2.7,分页(1)注册全局组件分页器
/src/components/Pagination.vue, 创建好在main.js中注册好
<template>
<div class="pagination">
<button>上一页</button>
<button>1</button>
<button>···</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button>6</button>
<button>7</button>
<button>···</button>
<button>9</button>
<button>下一页</button>
<button style="margin-left: 30px">共 60 条</button>
</div>
</template>
<script>
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: "Pagination",
}
</script>
<style lang="less" scoped>
.pagination {
text-align: center;
button {
margin: 0 5px;
background-color: #f4f4f5;
color: #606266;
outline: none;
border-radius: 2px;
padding: 0 4px;
vertical-align: top;
display: inline-block;
font-size: 13px;
min-width: 35.5px;
height: 28px;
line-height: 28px;
cursor: pointer;
box-sizing: border-box;
text-align: center;
border: 0;
&[disabled] {
color: #c0c4cc;
cursor: not-allowed;
}
&.active {
cursor: not-allowed;
background-color: #409eff;
color: #fff;
}
}
}
</style>
Search中使用组件
2.8,分页(2)自定义分页器
分页展示需要的数据
- pageNo: 当前第几页
- pageSize: 页面大小
- total: 一共多少数据
- 一共多少页=tatal/pageSize 有余+1
- 向上取整
Math.ceil(tatal/pageSize)
- 向上取整
- continues:分页器连续页面个数: 5|7, 奇数对称,好看
- 1 2 3…5 6 7 8 9 … 15 16 17
计算出continue开启页数和结束页数
computed:{
getTotalPage(){
//向上取整
return Math.ceil(this.total/this.pageSize);
},
startNumAndEndNum(){
const {continues,getTotalPage,pageNo} = this;
let startNum = 0,endNum = 0;
//如果当前页码数小于连续数就取基本
if (getTotalPage<=continues){
startNum = 1;
endNum = getTotalPage;
}else{
startNum = pageNo-parseInt(continues/2)
endNum = pageNo+parseInt(continues/2)
}
if (startNum<1){//起始页数不能小于1
startNum = 1;
endNum = continues
}
if (endNum>getTotalPage){//结束页数不能大于最大页数
endNum = getTotalPage;
startNum = endNum-continues
}
return {startNum,endNum}
}
}
2.9,分页(3)分页器动态展示
<template>
<div class="pagination">
<button>上一页</button>
<button v-show="startNumAndEndNum.startNum>1">1</button>
<button v-show="startNumAndEndNum.startNum>1">···</button>
<!-- 中间部分 -->
<button v-for="(page,index) in startNumAndEndNum.endNum"
:key="index"
v-show="page>=startNumAndEndNum.startNum">
{{ page }}
</button>
<button v-show="startNumAndEndNum.endNum<getTotalPage-1">···</button>
<button v-show="startNumAndEndNum.endNum<getTotalPage">{{ getTotalPage }}</button>
<button>下一页</button>
<button style="margin-left: 30px">共{{total}}条</button>
{{startNumAndEndNum}}--{{getTotalPage}}
</div>
</template>
Search添加分页效果
Search.vue传递参数给分页组件, 绑定自定义事件传递pageNo
<!--分页器组件-->
<Pagination :pageNo ='searchParams.pageNo'
:pageSize="searchParams.pageSize"
:total="getTotal"
:continues="5"
@getPageNo="getPageNo"/>
</div>
//methods----------------------------------------------------\
getPageNo(num){
this.searchParams.pageNo=num;
this.getSearchData()
}
Pagination.vue分页组件调用自定义事件传递pageNo,
<template>
<div class="pagination">
<!--如果是第一页不能点上一页 -->
<button :disabled="pageNo==1" @click="putPageNo(pageNo-1)">上一页</button>
<button v-show="startNumAndEndNum.startNum>1" @click="putPageNo(1)">1</button>
<button v-show="startNumAndEndNum.startNum>1">···</button>
<!-- 中间部分 -->
<button v-for="(page,index) in startNumAndEndNum.endNum"
:key="index"
v-show="page>=startNumAndEndNum.startNum"
@click="putPageNo(page)">
{{ page }}
</button>
<button v-show="startNumAndEndNum.endNum<getTotalPage-1">···</button>
<button v-show="startNumAndEndNum.endNum<getTotalPage" @click="putPageNo(getTotalPage)">
{{ getTotalPage }}
</button>
<button :disabled="pageNo==getTotalPage" @click="putPageNo(pageNo+1)">下一页</button>
<button style="margin-left: 15px">
第<input type="text" style="width: 20px" ref="inputNo" :value="pageNo" @blur="searchByPageNo($event.target.value)">页
</button>
<button style="margin-left: 30px">共{{total}}条</button>
</div>
</template>
//methods-------------------------------
methods:{
putPageNo(no){
this.$emit('getPageNo',no)
},
searchByPageNo(value){//按页数搜索
let pageNo = parseInt(value.replace(/ /g,''));//去除所有空格
if (pageNo<1 || pageNo>this.getTotalPage){
this.$refs.inputNo.value=''
alert(`请输入1~${this.getTotalPage}的数字!!!`);
return
}
this.putPageNo(pageNo);
}
效果
2.10,添加类名, 给当前页添加样式
<button v-show="startNumAndEndNum.startNum>1" @click="putPageNo(1)" :class="{active:pageNo==1}">1</button>
<!-- 中间部分 -->
<button v-for="(page,index) in startNumAndEndNum.endNum"
:key="index"
v-show="page>=startNumAndEndNum.startNum"
@click="putPageNo(page)" :class="{active:pageNo==page}">
{{ page }}
</button>
<button v-show="startNumAndEndNum.endNum<getTotalPage"
@click="putPageNo(getTotalPage)"
:class="{active:pageNo==getTotalPage}">
{{ getTotalPage }}
</button>
//style-----------------------------
.active{
background-color: skyblue;
}
三, 商品详情(Detail)
拆分组件:这里使用拆好的
3.1,Search传递参数到Dtail组件
1,注册路由(router)
import Detail from "@/pages/Detail/Detail";
export default new VueRouter({
//配置路由
routes:[
{
path:'/detail/:skuId',//查询商品详细需要传递id
component:Detail,
}
]
2,Search传递参数
<!-- 商品图片 -->
<div class="p-img">
<router-link :to="`/detail/${goods.id}`" ><img :src="goods.defaultImg" /></router-link>
</div>
3,测试
发现跳转停留在中间, 应跳到顶部
4,滚动行为设置
官网说明 vue3的用的{top:0},vue2的要用{y:0}
滑动行为可以完全的被定制化处理 - 甚至为每次路由进行定制也可以满足。这将会开启很多新的可能,但是简单的复制旧的行为:
scrollBehavior: function (to, from, savedPosition) {
return savedPosition || { x: 0, y: 0 }
}
添加到路由中, 与routes平级
export default new VueRouter({
//配置路由
routes:[...],
scrollBehavior: function (to, from, savedPosition) {
return savedPosition || { y: 0 } //这里只关心纵向位置, 只留Y即可
}
测试正常
5,将routes分离出来(可选)
router/routes.js
//引入路由组件
import Home from "@/pages/Home/Home";
import Login from "@/pages/Login/Login";
import Register from "@/pages/Register/Register";
import Search from "@/pages/Search/Search";
import Detail from "@/pages/Detail/Detail";
export default [
{
path:'/home',
component:Home,
meta:{show:true}
},
{
path:'/login',
component:Login,
meta:{show:false}
},
{
path:'/register',
component:Register,
meta:{show:false}
},
{
path:'/search/:keyword?',
component:Search,
meta:{show:true},
name:'search',
//路由组件能不能传递props数据
//布尔值写法: 只能传递params参数
// props:true
//对象写法:额外的给路由组件传递一些props参数
// props:{a:1,b:2}
//函数写法(常用),可以接收params参数,query参数,通过props传递
/* props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k}
}*/
//简写
// props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})
},
{//重定向, /访问首页
path:'*',
// component:Home,
redirect:'/home',//重定向到home
meta:{show:true}
},
{
path:'/detail/:skuId',//查询商品详细需要传递id
component:Detail,
}
]
index.js
引入routes即可
//配置路由
import Vue from "vue";
import VueRouter from "vue-router";
//使用插件
Vue.use(VueRouter)
//配置路由
//引入routes
import routes from "@/router/routes";
//先把vueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push;
let orginReplace = VueRouter.prototype.replace;
//重写push | replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
//二:成功的回调, 三: 失败的回调
VueRouter.prototype. push = function(location, resolve, reject){
if(resolve && reject){
//call] |apply区别
//相同点,都可以调用函数一次,都可以算改函数的上下文一次
//不同点: call与apply传递参数: cal1传递参数用逗号隔开, apply方法执行,传递数组
originPush.call(this, location,resolve, reject);
}else {
originPush.call(this, location, ()=>{}, ()=>{});
}
}
VueRouter.prototype.replace = function (location,resolve,reject){
if (resolve && reject){
orginReplace.call(this,location,resolve,reject);
}else {
orginReplace.call(this,location,()=>{},()=>{})
}
}
export default new VueRouter({
//配置路由
routes,
scrollBehavior: function (to, from, savedPosition) {
return savedPosition || { y: 0 }//这里只关心纵向位置, 只留Y即可
}
})
3.2,发请求获取商品详细信息
1,api添加方法
src/api/index.js
//获取产品详细信息 RUL: /api/item/{skuId} 请求方式get
export const reqGetGoodsDetail=(skuId)=>request.get(`/item/${skuId}`)
2,vuex中发请求
/store/store_detail.js
import {reqGetGoodsDetail} from "@/api";
const state = {
goodsDetail: {}
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
async getDetail({commit},skuId){
let result = await reqGetGoodsDetail(skuId);
if (result.code===200){
commit('GETDETAIL',result.data)
}
}
};
//mutations: 修改state的唯一手段
const mutations = {
GETDETAIL(state,data){
state.goodsDetail =data
}
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
categoryView(state){
//计算出来的属性为undefined时至少是一个空对象, 不会报错
return state.goodsDetail.categoryView || {}
},
skuInfo(state){
return state.goodsDetail.skuInfo || {}
}
};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
index.js注册
import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);
//引入小仓库
import store_home from "@/store/store_home";
import store_search from "@/store/store_search";
import store_detail from "@/store/store_detail";
//对外暴露
export default new Vuex.Store({
//导入模块
modules:{
store_search,
store_home,
store_detail
}
});
3,Detail.vue中获取派发Action获取数据
mounted() {
//派发action获取产品信息
this.$store.dispatch('getDetail',this.$route.params.skuId)
}
请求成功
vuex中有数据
3.3,动态展示主内容区信息
<!-- 右侧选择区域布局 -->
<div class="InfoWrap">
<div class="goodsDetail">
<h3 class="InfoName">{{skuInfo.skuName}}</h3>
<p class="news">{{skuInfo.skuDesc}}</p>
<div class="priceArea">
<div class="priceArea1">
<div class="title">价 格</div>
<div class="price">
<i>¥</i>
<em>{{skuInfo.price}}</em>
<!-- 选择区域动态展示 -->
<div class="choose">
<div class="chooseArea">
<div class="choosed"></div>
<dl v-for="spuSaleAttr in spuSaleAttrList" :key="spuSaleAttr.id">
<dt class="title">{{spuSaleAttr.saleAttrName}}</dt>
<dd changepirce="0"
:class="{active:spuSaleAttrValue.isChecked==='1'}"
v-for="spuSaleAttrValue in spuSaleAttr.spuSaleAttrValueList"
:key="spuSaleAttrValue.id">{{spuSaleAttrValue.saleAttrValueName}}</dd>
</dl>
</div>
<script>
import ImageList from './ImageList/ImageList'
import Zoom from './Zoom/Zoom'
import {mapGetters} from "vuex";
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Detail',
components: {
ImageList,
Zoom
},
mounted() {
//派发action获取产品信息
this.$store.dispatch('getDetail',this.$route.params.skuId)
},
computed:{
...mapGetters(['categoryView','skuInfo','spuSaleAttrList']),
}
}
</script>
选择区域点击切换高亮
<dd changepirce="0"
:class="{active:spuSaleAttrValue.isChecked==='1'}"
v-for="spuSaleAttrValue in spuSaleAttr.spuSaleAttrValueList"
:key="spuSaleAttrValue.id"
<!--添加点击事件,传入参数1: 当前选中的对象,参数2:包括兄弟对象-->
@click="changeChecked(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)">
{{spuSaleAttrValue.saleAttrValueName}}
</dd>
//methods
methods:{
changeChecked(value,list){
list.forEach((item)=>{
item.isChecked = '0';//将所有的都设置为没选择
});
value.isChecked = '1';//将选择的设置为已选择
}
}
3.4,Zoom放大镜展示数据- 裁剪
1,Detail传递数据给Zoom组件展示
Detail
<!--放大镜效果-->
<Zoom :skuImageList="skuImageList"/>
<!-- 小图列表 -->
<ImageList :skuImageList="skuImageList"/>
//计算属性-------------------------------------------
computed:{
...mapGetters(['categoryView','skuInfo']),
skuImageList(){
//解决undefined报错方法:1.在父组件的计算属性中写的是 || [{ }],
// 2.在子组件中props用对象写法,default用函数写法返回 [{ }]
return this.skuInfo.skuImageList || [{}]
}
}
Zoom.vue展示
<template>
<div class="spec-preview">
<img :src="imgObj"/>
<div class="event"></div>
<div class="big">
<img :src="imgObj"/>
</div>
<div class="mask"></div>
</div>
</template>
<script>
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: "Zoom",
props:['skuImageList'],
data(){
return {
currentIndex:0
}
},
computed:{
imgObj(){
return this.skuImageList[this.currentIndex].imgUrl
}
},
mounted() {
this.$bus.$on('getImg',(index)=>{
this.currentIndex = index
});
}
}
</script>
ImageList.vue展示
<template>
<div class="swiper-container" ref="floorSwiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(list,index) in skuImageList" :key="list.id">
<img :src="list.imgUrl"
:class="{active:currentIndex===index}"
@click="changeCurrentIndex(index)">
</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</template>
<script>
import Swiper from 'swiper'
export default {
name: "ImageList",
props:['skuImageList'],
data(){
return {currentIndex:0}
},
watch: {
skuImageList:{
handler(){
this.$nextTick(()=> {
new Swiper(this.$refs.floorSwiper, {
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
slidesPerView:3,//显示几个图片
slidesPerGroup:1,//每一次切几张图
});
})
}
}
},
methods:{
changeCurrentIndex(index){
this.currentIndex = index;
this.$bus.$emit('getImg',index)
}
}
}
</script>
<style lang="less" scoped>
.swiper-container {
&.active {
border: 2px solid #f60;
padding: 1px;
}
/* &:hover { 把这里注掉, 使用js写
border: 2px solid #f60;
padding: 1px;
}*/
}
}
效果
2,放大镜效果
<template>
<div class="spec-preview" ref="preview">
<img :src="imgObj"/>
<div class="event" @mousemove="handler"></div>
<div class="big" >
<img :src="imgObj" ref="big"/>
</div>
<!-- 遮罩层 -->
<div class="mask" ref="mask"></div>
</div>
</template>
//methods------------------------------
methods:{
handler(e){
let mask = this.$refs.mask
let big = this.$refs.big
let left = e.offsetX-mask.offsetWidth/2
let top = e.offsetY-mask.offsetHeight/2
if (left<0){//限制左边
left = 0;
}else if (left>mask.offsetWidth){//限制右边
left = mask.offsetWidth;
}
if (top<0){//限制上边
top = 0;
}else if(top>mask.offsetHeight){//限制下边
top = mask.offsetHeight;
}
mask.style.left = left+'px'
mask.style.top = top+'px'
//big是原来图的2倍
big.style.left = - 2 * left+'px'
big.style.top = - 2 * top+'px'
}
}
四,购物车
4.1,购买商品个数
<!--商品个数-->
<div class="cartWrap">
<div class="controls">
<input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum">
<a class="plus" @click="skuNum++">+</a>
<a class="mins" @click="skuNum>1 ? skuNum--:skuNum=1">-</a>
</div>
//methods------------------------------
changeSkuNum(e){//购买个数
let num = parseInt(e.target.value.replace(/ /g,''))//去除所有空格,取整
if (isNaN(num) || num<1){//输入不是数字,或负数
num = 1
}
this.skuNum = num ;
console.log(this.skuNum)
}
4.2, 加入购物车
- 1,发请求
- 2,跳转成功加入
- 3,显示购买信息
1,发送请求获取数据
/api/index.js
//存入购物车(或者更新产品数量)
//URL: /api/cart/addToCart/{skuId}/{skuNum} post
export const reqShopCart = (skuId,skuNum)=>request.post(`/cart/addToCart/${skuId}/${skuNum}`)
Vuex: store_detail.js
import {reqGetGoodsDetail,reqShopCart} from "@/api";
const state = {
goodsDetail: {},
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
async getDetail({commit},skuId){//获取产品详情
let result = await reqGetGoodsDetail(skuId);
if (result.code===200){
commit('GETDETAIL',result.data)
}
},
async updateShopCart(_, {skuId, skuNum}){//添加购物车
let result = await reqShopCart(skuId,skuNum);
//加入购物车服务器不会返回数据
console.log(result.data)
//当前函数加上了async数返回Promise
if (result.code===200){//成功
return 'ok';
}else {//失败
return Promise.reject(new Error('faile'));
}
}
};
//mutations: 修改state的唯一手段
const mutations = {
GETDETAIL(state,data){
state.goodsDetail =data
},
UPDATESHOPCART(state,data){
state.shopCart = data
}
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
categoryView(state){
//计算出来的属性为undefined时至少是一个空对象, 不会报错
return state.goodsDetail.categoryView || {}
},
skuInfo(state){
return state.goodsDetail.skuInfo || {}
},
spuSaleAttrList(state){
return state.goodsDetail.spuSaleAttrList || []
}
};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
Detail.vue
<div class="add">
<a @click="addShopCart">加入购物车</a>
</div>
//methods------------------------------
async addShopCart() {
//相当于调用函数, 函数加上了async数返回Promise
try {
await this.$store.dispatch('updateShopCart', {skuId: this.$route.params.skuId,skuNum:this.skuNum});
} catch (error) {
console.log(error.message)
}
}
2,跳转路由
page下添加组件AddCartSuccess.vue
router下注册路由
import AddCartSuccess from '@/pages/AddCartSuccess/AddCartSuccess'
{
path: '/addcartsuccess',
name: 'addcartsuccess',
component: AddCartSuccess
}
Detail.vue中跳转
async addShopCart() {
//相当于调用函数, 函数加上了async数返回Promise
try {
await this.$store.dispatch('updateShopCart', {skuId: this.$route.params.skuId,skuNum:this.skuNum});
//路由跳转
this.$router.push({name:'addcartsuccess'});
} catch (error) {
console.log(error.message)
}
}
3,路由传参,展示加入购物车数据
Detail.vue
async addShopCart() {
//相当于调用函数, 函数加上了async数返回Promise
try {
await this.$store.dispatch('updateShopCart', {skuId: this.$route.params.skuId,skuNum:this.skuNum});
//路由跳转
// this.$router.push({name:'addcartsuccess'});
//需要传递数据给成功组件显示
//方式一导致URL不好看http://localhost:8080/#addcartsuccess?skuInfo=%5Bobject%20Object%5D&skuNum=3
// this.$router.push({name:'addcartsuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})
//方式二, 只传skuNum, skuInfo放到浏览器会话存储
sessionStorage.setItem('skuInfo',JSON.stringify(this.skuInfo));
this.$router.push({name:'addcartsuccess',query:{skuNum:this.skuNum}})
} catch (error) {
console.log(error.message)
}
}
AddCartSuccess.vue
获取数据, 展示
<template>
<div class="cart-complete-wrap">
<div class="cart-complete">
<h3><i class="sui-icon icon-pc-right"></i>商品已成功加入购物车!</h3>
<div class="goods">
<div class="left-good">
<div class="left-pic">
<img :src="skuInfo.skuDefaultImg">
</div>
<div class="right-info">
<p class="title">{{skuInfo.skuName}}</p>
<p class="attr">{{skuInfo.skuDesc}} 数量:{{ $route.query.skuNum }}</p>
</div>
</div>
<div class="right-gocart">
<a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a>
<a href="javascript:" >去购物车结算 > </a>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AddCartSuccess',
computed:{
skuInfo(){
//获取浏览器会话存储的skuInfo并转为对象
return JSON.parse(sessionStorage.getItem('skuInfo'));
}
}
}
</script>
4,存入用户信息uuid
4.1,工具类uuid
utils/uuid_token.js
import {v4 as uuidv4} from "uuid";
export const getUUID = ()=>{
//先查看本地存储有没有uuid
let uuid_token = localStorage.getItem('uuid_token');
//如果没有就新建一个
if (!uuid_token){
uuid_token = uuidv4();
//存到本地存储中
localStorage.setItem('uuid_token',uuid_token);
}
return uuid_token;
}
4.2,在请求拦截器中放入uuid
//引入store(获取uuid)
import store_detail from "@/store/store_detail";
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
if (store_detail.state.uuid_token){
//请求头添加字段userTempId--和后台商量好了
config.headers.userTempId = store_detail.state.uuid_token
}
//进度条开始动
nprogress.start();
return config
});
随便找一个请求查看
4.3,购物车–查看商品详情
router-link返回到detail即可
<div class="right-gocart">
<!--<a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a>-->
<router-link :to="`/detail/${skuInfo.id}`" class="sui-btn btn-xlarge">查看商品详情</router-link>
<a href="javascript:" >去购物车结算 > </a>
</div>
4.4,购物车结算
导入购物车组件
注册路由
import ShopCart from '@/pages/ShopCart/ShopCart'
{
path: '/shopcart',
name: 'shopcart',
component: ShopCart
}
AddCartSuccess.vue购物车结算跳转
<router-link to="/shopcart" >去购物车结算 > </router-link>
1,发请求
api/index.js
/*
* 获取购物车列表接口
* URL: /api/cart/cartList
* method = get
* */
export const reqCartList = ()=>request.get('/cart/cartList')
vuex获取数据
store_shopcart.js
import {reqCartList} from "@/api";
const state = {
cartList:[]
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
async getCartList({commit}){
let result = await reqCartList();
if (result.code===200){
commit('GETCARTLIST',result.data);
}
}
};
//mutations: 修改state的唯一手段
const mutations = {
GETCARTLIST(state,data){
state.cartList = data
}
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
cartInfoList(state){
return state.cartList.length>0?state.cartList[0].cartInfoList : [] ;
}
};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
2,展示数据
<template>
<div class="cart">
<h4>全部商品</h4>
<div class="cart-main">
<div class="cart-th">
<div class="cart-th1">全部</div>
<div class="cart-th2">添加日期</div>
<div class="cart-th3">单价(元)</div>
<div class="cart-th4">数量</div>
<div class="cart-th5">小计(元)</div>
<div class="cart-th6">操作</div>
</div>
<div class="cart-body">
<ul class="cart-list" v-for="(cart,index) in cartInfoList" :key="cart.id">
<li class="cart-list-con1">
<input type="checkbox" name="chk_list" :checked="cart.isChecked===1" @click="checkCart(index)">
</li>
<li class="cart-list-con2">
<img :src="cart.imgUrl">
<div class="item-msg">{{cart.skuName}}</div>
</li>
<li class="cart-list-con3">
<div class="item-txt">{{ cart.operateTime }}</div>
</li>
<li class="cart-list-con4">
<span class="price">{{cart.skuPrice}}.00</span>
</li>
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins">-</a>
<input autocomplete="off" type="text" value="1" minnum="1" class="itxt">
<a href="javascript:void(0)" class="plus">+</a>
</li>
<li class="cart-list-con6">
<span class="sum">{{cart.skuPrice * cart.skuNum}}</span>
</li>
<li class="cart-list-con7">
<a href="#none" class="sindelet">删除</a>
<br>
<a href="#none">移到收藏</a>
</li>
</ul>
</div>
</div>
<div class="cart-tool">
<div class="select-all" v-show="cartInfoList.length>0">
<input class="chooseAll" type="checkbox"
@click="chooseAll($event)"
:checked="IsChooseAll">
<span>全选</span>
</div>
<div class="option">
<a href="#none">删除选中的商品</a>
<a href="#none">移到我的关注</a>
<a href="#none">清除下柜商品</a>
</div>
<div class="money-box">
<div class="chosed">已选择
<span>0</span>件商品</div>
<div class="sumprice">
<em>总价(不含运费) :</em>
<i class="summoney">{{totalPrice }}</i>
</div>
<div class="sumbtn">
<a class="sum-btn" href="###" target="_blank">结算</a>
</div>
</div>
</div>
</div>
</template>
<script>
import {mapState} from "vuex";
export default {
name: 'ShopCart',
mounted() {
this.$store.dispatch('getCartList')
},
computed:{
...mapState({
cartList:(state)=>{
return state.store_shopcart.cartList[0] || [];
}
}),
cartInfoList(){
return this.cartList.cartInfoList || []
},
totalPrice(){//遍历购物车计算总价
let sum = 0;
this.cartInfoList.forEach((cart)=>{
if (cart.isChecked===1){ //计算勾选上的
sum += cart.skuPrice * cart.skuNum;
}
})
return sum;
},
IsChooseAll(){//是否全选
//every:若数组每一个对象都符合条件,则返回true ,只要有一个不符合条件就返回false
// every 就当作是 && some看作是 ||
return this.cartInfoList.every(item=>{
return item.isChecked ===1
})
}
},
methods:{
checkCart(index){//单选
let check = this.cartInfoList[index].isChecked;
if (check===1){
this.cartInfoList[index].isChecked = 0
}else {
this.cartInfoList[index].isChecked = 1
}
},
chooseAll(){//全选
this.cartInfoList.forEach((cart)=>{
if (cart.isChecked===1){
cart.isChecked=0
}else {
cart.isChecked=1
}
})
}
}
}
</script>
4.5,购物车数量增减
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins" @click="handlerNum('minus',-1,cart,index)">-</a>
<input autocomplete="off" ref="itxt" type="text" :value="cart.skuNum" minnum="1" class="itxt" @change="handlerNum('text',$event.target.value,cart,index)">
<a href="javascript:void(0)" class="plus" @click="handlerNum('add',1,cart,index)">+</a>
</li>
//methods
handlerNum:throttle(async function (type,num,cart,index){//节流
switch (type){
case "minus":
num = cart.skuNum >=1 ? -1 : 0; //不能小于0
break;
case "text":
num = parseInt(num.replace(/ /g,''));//去除输入空格
//如果值未改变, 非法数字, 负数都不修改值
if (num-cart.skuNum===0 || isNaN(num) || num<0 ){
num = 0
this.$refs.itxt[index].value = cart.skuNum//值不对修改为原来的值
return
}
num = num - cart.skuNum;
break;
}
if(num ===0) return;//如果没有修改就返回
try {
await this.$store.dispatch('updateShopCart',{
skuId:cart.skuId,
skuNum:num
});
this.getShopCartData();//重新发起请求
}catch (error){
console.log(error.message)
}
console.log(type,num,cart.skuNum)
},500)
}
效果
4.6,删除购物车产品
api
/*删除购物车产品
* URL: /api/cart/deleteCart/${skuId}
* method: DELETE
* */
export const reqDeleteCartById = (skuId)=>request.delete(`/cart/deleteCart/${skuId}`);
vuex
async deleteCartListById(_,skuId){
let result = await reqDeleteCartById(skuId);
if (result.code===200){
return 'ok';
}else {
return Promise.reject(new Error('faile'))
}
}
ShopCart.vue
<li class="cart-list-con7">
<a class="sindelet" @click="deleteCartById(cart.skuId)">删除</a>
<br>
<a >移到收藏</a>
</li>
//methods
async deleteCartById(skuId){
try {
console.log(skuId)
await this.$store.dispatch('deleteCartListById',skuId);
//产出成功重新发请求
this.getShopCartData();
}catch (error){
console.log(error.message)
}
}
4.7,修改产品状态
api
/*切换商品选中状态
* URL: /api/cart/checkCart/${skuId}/${isChecked}
* method: get
* */
export const reqUpdateCartChecked = (skuId,isChecked)=>request.get(`/cart/checkCart/${skuId}/${isChecked}`)
vuex
async updateCartChecked(_, {skuId,isChecked}){
let result = await reqUpdateCartChecked(skuId,isChecked);
if (result.code===200){
return 'ok';
}else {
return Promise.reject(new Error('faile'))
}
}
ShopCart.vue
<!--单选-->
<li class="cart-list-con1">
<input type="checkbox" name="chk_list"
:checked="cart.isChecked===1"
@click="checkCart(cart,$event)">
</li>
<!--全选-->
<div class="select-all" v-show="cartInfoList.length>0">
<input class="chooseAll" type="checkbox"
@click="chooseAll($event)"
:checked="IsChooseAll">
<span>全选</span>
</div>
//computed
IsChooseAll(){//是否全选
//every:若数组每一个对象都符合条件,则返回true ,只要有一个不符合条件就返回false
// every 就当作是 && some看作是 ||
if (this.cartInfoList.length<1){
return
}
return this.cartInfoList.every(item=>{
return item.isChecked ===1
})
}
//methods
//按需引入lodash
//节流, 第一次点击生效, 最后一次再生效
// import throttle from 'lodash/throttle'
//防抖, 最后一次生效
import debounce from 'lodash/debounce'
//---------------------------------------------------------------------------------
checkCart:debounce(async function (cart,event){//单选+防抖
try {
let check = event.target.checked ? 1:0; //通过事件获取单选框是否选中, 选中返回1
//发请求修改点击状态
await this.$store.dispatch('updateCartChecked',{skuId:cart.skuId,isChecked:check})
this.getShopCartData();//发请求更新数据
}catch (error){
console.log(error.message)
}
},500),
chooseAll:debounce(function (event){//全选+防抖
try {
this.cartInfoList.forEach(async (cart)=>{//遍历所有产品
let check = event.target.checked?1:0;//通过事件获取单选框是否选中, 选中返回1
//发请求修改所有产品点击状态
await this.$store.dispatch('updateCartChecked',{skuId:cart.skuId,isChecked:check})
})
setTimeout(()=>{//等待所有产品的状态修改完成
this.getShopCartData();//发请求更新数据
},200)
}catch (error){
alert(error.message)
}
},500),
Promise.all实现全选
store_shopcart.js–action中添加方法
//全选购物车,更改产品状态
updateCheckedAllCart(context, isCheched){
let cartInfoList = context.getters.cartInfoList;//获取购物车产品列表
if (cartInfoList.length<1){
return 'no';//没有数据返回
}
let promiseAll = []
cartInfoList.forEach((cart)=>{
let check = isCheched?1:0;
let promise = context.dispatch('updateCartChecked',{skuId:cart.skuId,isChecked:check});
//把每一次返回的promise放到数组中
promiseAll.push(promise)
});
//只要所有的promise都成功, 结果为成功, 有一个失败就失败
return Promise.all(promiseAll)
},
ShopCart.vue
chooseAll:debounce(async function (event){//全选+防抖
try {
//updateCheckedAllCart
let result = await this.$store.dispatch('updateCheckedAllCart',event.target.checked)
console.log(result)
if (result==='no') return alert('当前购物车无数据')
this.getShopCartData();
}catch (error){
alert(error.message)
}
},500),
效果
4.8,删除选中的全部产品
没有一次删除多个产品的接口, 有删除一个的接口
Promise.all([p1,p2,p3])
p1lp2|p3:每一个都是Promise对象,如果有一个Promise失败,都失败,如果都成功,返回成功。
store_shopcart.js–action中添加方法
//删除全部勾选产品
deleteAllCheckedCart(context){
let cartInfoList = context.getters.cartInfoList;//获取购物车产品列表
if (cartInfoList.length<1){
return 'no';//没有数据返回
}
let promiseAll = []
cartInfoList.forEach((item)=>{
if (item.isChecked===1){
let promise = context.dispatch('deleteCartListById',item.skuId);
//把每一次返回的promise放到数组中
promiseAll.push(promise)
}
});
//只要所有的promise都成功, 结果为成功, 有一个失败就失败
return Promise.all(promiseAll)
}
ShopCart.vue
<a @click="deleteAllChecked">删除选中的商品</a>
//methods---------------------------------------------------
//删除所有勾选的产品
async deleteAllChecked(){
try {
let result = await this.$store.dispatch('deleteAllCheckedCart');
if (result==='no') return alert('当前购物车无数据')
this.getShopCartData();
}catch (error){
alert(error.message)
}
}
效果
五, 注册
静态组件准备好
1,获取手机验证码
api
/*获取验证码
* URL: /api/user/passport/sendCode/${phone}
* method:get
* */
export const reqGetCode = (phone)=>request.get(`/user/passport/sendCode/${phone}`)
store_user.js
/*登录注册共用仓库*/
import {reqGetCode} from "@/api";
const state = {
code:''
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
//获取验证码
async getCode({commit},phone){
let result = await reqGetCode(phone)
if (result.code==200){
commit('GETCODE',result.data);
return 'ok'
}else {
return Promise.reject(new Error('faile'))
}
}
};
//mutations: 修改state的唯一手段
const mutations = {
GETCODE(state,value){
state.code = value
}
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
Register.vue
<div class="content">
<label>手机号:</label>
<input type="text" placeholder="请输入你的手机号" v-model="phone">
<span class="error-msg">错误提示信息</span>
</div>
<div class="content">
<label>验证码:</label>
<input type="text" placeholder="请输入验证码" v-model="code">
<!-- <img ref="code" src="http://182.92.128.115/api/user/passport/code" alt="code">-->
<button style="width: 100px;height: 40px" @click="getCode">获取验证码</button>
<span class="error-msg">错误提示信息</span>
</div>
<div class="content">
<label>登录密码:</label>
<input type="text" placeholder="请输入你的登录密码" v-model="password">
<span class="error-msg">错误提示信息</span>
</div>
<div class="content">
<label>确认密码:</label>
<input type="text" placeholder="请输入确认密码" v-model="password1">
<span class="error-msg">错误提示信息</span>
</div>
<div class="controls">
<input name="m1" type="checkbox" :checked="isAgree">
<span>同意协议并注册《尚品汇用户协议》</span>
<span class="error-msg">错误提示信息</span>
</div>
//methods-------------------------------------
data(){
return {
phone:'',//电话号码
code:'',//验证码
password:'',//密码
password1:'',//确认密码
isAgree:true,//是否同意协议
}
},methods:{
//获取验证码, 防抖
getCode:debounce(async function (){
try {
//如果获取到验证码再继续往下走
if (this.phone==='') return
await this.$store.dispatch('getCode',this.phone)
this.code = this.$store.state.store_user.code
}catch (error){
alert(error.message)
}
},1000)
}
2,完成注册
api
/*注册用户
* URL: /api/user/passport/register
* methods: post
* */
export const reqUserRegister=(data)=>request.post(`/user/passport/register`,data)
store_user.js
/*登录注册共用仓库*/
import {reqGetCode,reqUserRegister} from "@/api";
//用户注册,无返回值
async userRegister(_,data){
let result = await reqUserRegister(data)
console.log(result)
if (result.code==200){
return 'ok'
}else {
return Promise.reject(new Error(result.message))
}
},
Register.vue
<div class="btn">
<button @click="userRegister">完成注册</button>
</div>
//methods
//注册提交
async userRegister(){
try {
const {phone,code,password,password1} =this;
//验证成功再发请求
if(phone!==''&&code!==''&&password===password1){
await this.$store.dispatch('userRegister',{phone,code,password})
this.$router.push('/login');//注册成功跳转到登录
}
return
}catch (error){
alert(error.message)
}
}
六,登录
1,登录跳转
api
/*登录
* URL: /api/user/passport/login
* methods: post
* */
export const reqUserLogin=(data)=>request.post(`/user/passport/login`,data)
store_user.js
/*登录注册共用仓库*/
import {reqGetCode,reqUserRegister,reqUserLogin} from "@/api";
//用户登录
async userLogin({commit},data){
let result = await reqUserLogin(data);
console.log(result)
if (result.code===200){
//登录成功存储token
commit('USERLOGIN',result.data);
}else {
Promise.reject(new Error(result.message))
}
}
USERLOGIN(state,value){
state.user = value
},
Login.vue
<!--阻止默认行为修饰符, prevent -->
<button class="btn" @click.prevent="login">登 录</button>
//----------------------------------------------------------
data(){
return {
phone:'',
password:''
}
},
methods:{
async login(){
try {
const {phone,password} = this
if (phone!=='' && password!==''){
await this.$store.dispatch('userLogin',{phone,password})
await this.$router.push('/home')
}
}catch (error){
alert(error.message)
}
}
}
2,登录携带token获取用户信息
通过token获取用户信息
api
/*通过token获取用户信息
* URL: /api/user/passport/auth/getUserInfo
* method: get, 可通过header传递token
* */
export const reqGetUserInfo=()=>request.get(`/user/passport/auth/getUserInfo`)
store_user.js
//通过token获取用户信息
async getUserInfo({commit}){
let result = await reqGetUserInfo();
console.log(result,commit)
if (result.code===200){
//存储用户信息
commit('GETUSERINFO',result.data)
}else {
return Promise.reject(new Error(result.message))
}
}
GETUSERINFO(state,value){
state.userInfo = value
},
request.js, 请求头中加入token
//请求拦截器, 可以在发请求前做一些事
myAxios.interceptors.request.use((config)=>{
if (store_detail.state.uuid_token){
//请求头添加字段userTempId--和后台商量好了
config.headers.userTempId = store_detail.state.uuid_token
}
if (store_user.state.token){//如果仓库中有token, 加入到header中
config.headers.token = store_user.state.token
}
//进度条开始动
nprogress.start();
return config
});
Home.vue中加载数据
mounted() {
//vuex-action方法获取floorList
this.$store.dispatch('getFloorList');
//获取用户信息
this.$store.dispatch('getUserInfo')
},
Header.vue中显示用户信息
<div class="loginList">
<p>尚品汇欢迎您!</p>
<p v-if="!userName">
<span>请</span>
<router-link to="/login">登录</router-link>
<router-link class="register" to="/register">免费注册</router-link>
</p>
<p v-else>
<span>用户 </span>
<a >{{userName}}</a>
<a class="register">退出登录</a>
</p>
</div>
//------------------------------------------------
computed:{
userName(){
return this.$store.state.store_user.userInfo.name
}
},
3,持久化存储token
const state = {
code:'',
token:localStorage.getItem("TOKEN"),//state中取
userInfo:{}
};
//用户登录,登录后把token存放到本地存储中
async userLogin({commit},data){
let result = await reqUserLogin(data);
if (result.code===200){
//登录成功存储token
commit('USERLOGIN',result.data.token);
//持久化存储token
localStorage.setItem("TOKEN",result.data.token)
}else {
return Promise.reject(new Error(result.message))
}
},
4,退出登录
api
/*退出登录
* URL: /api/user/passport/logout
* method: get,
* */
export const reqLogout=()=>request.get(`/user/passport/logout`)
store_user.js
//通过token获取用户信息
async logout({commit}){
let result = await reqLogout();
console.log(result)
if (result.code===200){
//清除仓库中的用户信息(action不直接操作state, 交给mutation操作)
commit('CLEAR');
}else {
return Promise.reject(new Error(result.message))
}
},
//--------------------------------------
CLEAR(state){
//把仓库中user信息清空
state.token = '';
state.userInfo = {};
//清除本地token
localStorage.removeItem('TOKEN')
}
5,添加访问限制–路由守卫
全局守卫:
- 只要路由变化, 守卫就能监控到
Home.vue去掉获取用户信息
mounted() {
//vuex-action方法获取floorList
this.$store.dispatch('getFloorList');
/* //获取用户信息
this.$store.dispatch('getUserInfo')*/
},
router/index.js
//配置路由
import Vue from "vue";
import VueRouter from "vue-router";
import store from '@/store'
//使用插件
Vue.use(VueRouter)
//配置路由
//引入routes
import routes from "@/router/routes";
//先把vueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push;
let orginReplace = VueRouter.prototype.replace;
//重写push | replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
//二:成功的回调, 三: 失败的回调
VueRouter.prototype. push = function(location, resolve, reject){
if(resolve && reject){
//call] |apply区别
//相同点,都可以调用函数一次,都可以算改函数的上下文一次
//不同点: call与apply传递参数: cal1传递参数用逗号隔开, apply方法执行,传递数组
originPush.call(this, location,resolve, reject);
}else {
originPush.call(this, location, ()=>{}, ()=>{});
}
}
VueRouter.prototype.replace = function (location,resolve,reject){
if (resolve && reject){
orginReplace.call(this,location,resolve,reject);
}else {
orginReplace.call(this,location,()=>{},()=>{})
}
}
let router= new VueRouter({
//配置路由
routes,
scrollBehavior: function (to, from, savedPosition) {
return savedPosition || { y: 0 }//这里只关心纵向位置, 只留Y即可
}
})
/*全局前置守卫, 在路由跳转之前拦截
* to: 获取跳转的路由信息
* from: 从哪个路由来的信息
* next: 放行函数
* next()直接放行
* next('/login')---放行到指定路由
* next(false) 中断当前导航, 重置到 from路由地址
* */
router.beforeEach(async (to, from, next)=>{
// console.log(to,from,next)
let token = store.state.store_user.token;
let name = store.state.store_user.userInfo.name
let path = to.fullPath
console.log(token,name,path)
if (token ){ //已登录情况
if (path==='/login'){//不能去login
alert('您已登录! 重新登录点击退出登录');
next(from.fullPath)
}else {//去其他 home-search-detail-shopcart
if (name){
next()
}else {
try {
//没有用户名, 派发action查询用户
await store.dispatch('getUserInfo');
next()
}catch (error){
//token失效
await store.dispatch('logout');
alert('身份已失效, 请重新登录')
next('/login');
}
}
}
}else {//未登录情况
next()
}
})
export default router
七,购物车结算-微信支付
导入静态页面
1,获取结算所需信息
api
/*获取用户地址信息
* URL:/api/user/userAddress/auth/findUserAddressList
* method: get
* */
export const reqUserAddress = ()=>request.get(`/user/userAddress/auth/findUserAddressList`)
/*获取商品清单
* URL:/api/order/auth/trade
* method: get
* */
export const reqUserOrder = ()=>request.get(`/order/auth/trade`)
store_trade.js发起请求
/*登录注册共用仓库*/
import {reqUserAddress,reqUserOrder} from "@/api";
const state = {
addressList:[],
orderList:[]
};
//action: 处理action, 可以写业务逻辑, 也可以处理异步
const actions = {
//获取用户地址
async getUserAddress({commit}){
let result = await reqUserAddress();
if (result.code===200){
commit('GETUSERADDRESS',result.data);
}else {
return Promise.reject(new Error(result.message))
}
},
//获取商品清单
async getUserOrder({commit}){
let result = await reqUserOrder();
if (result.code===200){
commit('GETUSERORDER',result.data);
}else {
return Promise.reject(new Error(result.message))
}
}
};
//mutations: 修改state的唯一手段
const mutations = {
GETUSERADDRESS(state,data){
state.addressList = data
},
GETUSERORDER(state,data){
state.orderList = data
},
};
//getters:理解为计算属性, 简化仓库数据, 让组件获取仓库数据更加方便
//可以把将来组件中用到的数据简化, 获取的时候就方便些
const getters = {
};
//对外暴露
export default({
state,
mutations,
actions,
getters,
});
Trade.vue, 派发action
mounted() {
this.$store.dispatch('getUserAddress')
this.$store.dispatch('getUserOrder')
}
2,动态展示结算数据
<template>
<div class="trade-container">
<h3 class="title">填写并核对订单信息</h3>
<div class="content">
<h5 class="receive">收件人信息</h5>
<div class="address clearFix" v-for="address in addressList" :key="address.id">
<span class="username " :class="{selected:address.isDefault==='1'}">{{ address.consignee }}</span>
<p @click="changeDefalt(address,addressList)">
<span class="s1">{{ address.fullAddress }}</span>
<span class="s2">{{ address.phoneNum }}</span>
<span class="s3" v-show="address.isDefault==='1'">默认地址</span>
</p>
</div>
<div class="line"></div>
<h5 class="pay">支付方式</h5>
<div class="address clearFix">
<span class="username selected">在线支付</span>
<span class="username" style="margin-left:5px;">货到付款</span>
</div>
<div class="line"></div>
<h5 class="pay">送货清单</h5>
<div class="way">
<h5 class="pay">配送方式</h5>
<div class="info clearFix">
<span class="s1">天天快递</span>
<p>配送时间:预计8月10日(周三)09:00-15:00送达</p>
</div>
</div>
<div class="detail">
<h5 class="pay">商品清单</h5>
<ul class="list clearFix" v-for="(order,index) in orderList.detailArrayList" :key="index">
<li>
<img :src="order.imgUrl" alt="" style="width: 100px;height: 100px">
</li>
<li>
<p>
{{ order.skuName }}</p>
<h4 >7天无理由退货</h4>
</li>
<li>
<h3>{{ order.orderPrice }}.00</h3>
</li>
<li>X{{ order.skuNum }}</li>
<li>有货</li>
</ul>
</div>
<div class="bbs">
<h5>买家留言:</h5>
<textarea placeholder="建议留言前先与商家沟通确认" class="remarks-cont" v-model="message"></textarea>
</div>
<div class="line"></div>
<div class="bill">
<h5>发票信息:</h5>
<div>普通发票(电子) 个人 明细</div>
<h5>使用优惠/抵用</h5>
</div>
</div>
<div class="money clearFix">
<ul>
<li>
<b><i>{{ orderList.totalNum }}</i>件商品,总商品金额</b>
<span>¥{{ orderList.totalAmount }}.00</span>
</li>
<li>
<b>返现:</b>
<span>0.00</span>
</li>
<li>
<b>运费:</b>
<span>0.00</span>
</li>
</ul>
</div>
<div class="trade">
<div class="price">应付金额:<span>¥{{ orderList.totalAmount }}.00</span></div>
<div class="receiveInfo">
寄送至:
<span>{{ defaultAddress.fullAddress }}</span>
收货人:<span>{{ defaultAddress.consignee }}</span>
<span>{{ defaultAddress.phoneNum }}</span>
</div>
</div>
<div class="sub clearFix">
<router-link class="subBtn" to="/pay">提交订单</router-link>
</div>
</div>
</template>
<script>
import {mapState} from "vuex";
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Trade',
data(){
return {
message:''
}
},
mounted() {
this.$store.dispatch('getUserAddress')
this.$store.dispatch('getUserOrder')
},
computed:{
...mapState({
addressList:state=>state.store_trade.addressList,
orderList:state=>state.store_trade.orderList,
}),
defaultAddress(){
//find查找数组中符合条件的元素
return this.addressList.find(item=>item.isDefault==='1')
}
},
methods:{
//修改默认数组
changeDefalt(address,addressList){
addressList.forEach((item)=>{
item.isDefault = '0'
})
address.isDefault = '1'
}
}
}
</script>
3,提交订单
静态组件
api
/*获取商品清单
* URL:/api/order/auth/submitOrder?tradeNo=${tradeNo}
* method: post
* */
export const reqSubmitOrder = (tradeNo,data)=>request.post(`/order/auth/submitOrder?tradeNo=${tradeNo}`,data)
不使用vuex发请求了, 在main.js中引入全部api
//引入api
import * as API from '@/api'
new Vue({
render: h => h(App),
//注册路由
router,
//注册仓库: 组件实例上就会多一个$store属性
store,
beforeCreate() {//注册全局时间总线
Vue.prototype.$bus = this;
Vue.prototype.$API = API;
}
}).$mount('#app')
Trade.vue
<div class="sub clearFix">
<!-- <router-link class="subBtn" to="/pay">提交订单</router-link>-->
<a class="subBtn" @click="submitOrder">提交订单</a>
</div>
//method-----------------------------------------------------------------
async submitOrder(){
/*需要携带的参数
* tradeNo 交易编码
* */
let data={
"consignee": this.addressList.consignee,//收件人名字
"consigneeTel": this.addressList.phoneNum,//收件人手机
"deliveryAddress": this.addressList.fullAddress,//收件人地址
"paymentWay": "ONLINE",//支付方式
"orderComment": this.message,//买家留言
"orderDetailList": this.orderList.detailArrayList //商品清单
}
let result = await this.$API.reqSubmitOrder(this.orderList.tradeNo,data);
if (result.code===200){
this.orderId = result.data;
//跳转到支付页面, 传递参数
this.$router.push(`/pay?orderId=${this.orderId}`)
}else {
console.log('提交订单失败',result.message)
}
}
4,支付页面
接收参数
// eslint-disable-next-line vue/multi-word-component-names
name: 'Pay',
computed:{
orderId(){
return this.$route.query.orderId
}
},
通过订单号获取支付信息
api
/*通过订单号获取支付信息
* URL:/api/payment/weixin/createNative/${orderId}
* method: get
* */
export const reqPayInfo = (orderId)=>request.get(`/payment/weixin/createNative/${orderId}`)
Pay.vue
<template>
<div class="pay-main">
<div class="pay-container">
<div class="checkout-tit">
<h4 class="tit-txt">
<span class="success-icon"></span>
<span class="success-info">订单提交成功,请您及时付款,以便尽快为您发货~~</span>
</h4>
<div class="paymark">
<span class="fl">请您在提交订单<em class="orange time">4小时</em>之内完成支付,超时订单会自动取消。订单号:<em>{{orderId}}</em></span>
<span class="fr"><em class="lead">应付金额:</em><em class="orange money">¥{{payInfo.totalFee}}</em></span>
</div>
</div>
<div class="checkout-info">
<h4>重要说明:</h4>
<ol>
<li>尚品汇商城支付平台目前支持<span class="zfb">支付宝</span>支付方式。</li>
<li>其它支付渠道正在调试中,敬请期待。</li>
<li>为了保证您的购物支付流程顺利完成,请保存以下支付宝信息。</li>
</ol>
<h4>支付宝账户信息:(很重要,<span class="save">请保存!!!</span>)</h4>
<ul>
<li>支付帐号:11111111</li>
<li>密码:111111</li>
<li>支付密码:111111</li>
</ul>
</div>
<div class="checkout-steps">
<div class="step-tit">
<h5>支付平台</h5>
</div>
<div class="step-cont">
<ul class="payType">
<li><img src="./images/pay2.jpg"></li>
<li><img src="./images/pay3.jpg"></li>
</ul>
</div>
<div class="hr"></div>
<div class="payshipInfo">
<div class="step-tit">
<h5>支付网银</h5>
</div>
<div class="step-cont">
<ul class="payType">
<li><img src="./images/pay10.jpg"></li>
<li><img src="./images/pay11.jpg"></li>
<li><img src="./images/pay12.jpg"></li>
<li><img src="./images/pay13.jpg"></li>
<li><img src="./images/pay14.jpg"></li>
<li><img src="./images/pay15.jpg"></li>
<li><img src="./images/pay16.jpg"></li>
<li><img src="./images/pay17.jpg"></li>
<li><img src="./images/pay18.jpg"></li>
<li><img src="./images/pay19.jpg"></li>
<li><img src="./images/pay20.jpg"></li>
<li><img src="./images/pay21.jpg"></li>
<li><img src="./images/pay22.jpg"></li>
</ul>
</div>
</div>
<div class="hr"></div>
<div class="submit">
<!-- <router-link class="btn" to="/paysuccess">立即支付</router-link>-->
<a class="btn" @click="pay">立即支付</a>
</div>
<div class="otherpay">
<div class="step-tit">
<h5>其他支付方式</h5>
</div>
<div class="step-cont">
<span><a href="weixinpay.html" target="_blank">微信支付</a></span>
<span>中国银联</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Pay',
data(){
return {
payInfo:{}
}
},
computed:{
orderId(){
return this.$route.query.orderId
}
},
mounted() {
//不能在生命周期函数中使用async
this.getPayInfo();
},
methods:{
async getPayInfo(){
let result = await this.$API.reqPayInfo(this.orderId)
if (result.code===200){
this.payInfo = result.data;
}else {
alert(`获取支付信息失败!${result.message}`)
}
},
pay(){
}
}
}
</script>
5,ElmentUI按需引入
安装elementui
npm install element-ui
按需引入
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
首先,安装 babel-plugin-component:
npm install babel-plugin-component -D
然后,将 .babelrc 修改为:
新版脚手架叫babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
//"presets": [["es2015", { "modules": false }]],es2015报错
["@babel/preset-env", { "modules": false }]
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
main.js中引入
重启服务器
//按需引入ElementUI
import {Button, MessageBox} from 'element-ui'
Vue.use(Button);//注册全局组件
//另一种方式,挂在原型上
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
测试
<el-button type="primary" class="el-icon-phone">测试</el-button>
6, 弹框支付–Message Box
<div class="submit">
<!-- <router-link class="btn" to="/paysuccess">立即支付</router-link>-->
<a class="btn" @click="openPay">立即支付</a>
//method----------------------------------------------------------------------
openPay(){
this.$alert('这是一段内容', '标题名称', {
confirmButtonText: '已支付成功',
//中间布局
center:true,
//是否显示取消按钮
showCancelButton:true,
cancelButtonText:'支付遇见问题',//取消按钮的文本
showClose:false,//右上角X
});
}
7,生成微信支付二维码(获取支付信息)
使用qrcode
npm i qrcode
import QRCode from 'qrcode'
let url = await QRCode.toDataURL(this.payInfo.codeUrl);
获取支付状态
api
/*通过订单号获取支付信息
* URL:/api/payment/weixin/queryPayStatus/${orderId}
* method: get
* */
export const reqPayStatus = (orderId)=>request.get(`/payment/weixin/queryPayStatus/${orderId}`)
Pay.vue
data(){
return {
payInfo:{},
timer:null,//存放定时器
isPay:false,//存放是否支付成功标识
}
},
methods:{
async openPay(){
//生成二维码地址
let url = await QRCode.toDataURL(this.payInfo.codeUrl);
console.log(url)
this.$alert(`<img src="${url}" />`, '标题名称', {
dangerouslyUseHTMLString:true,//将 message 属性作为 HTML 片段处理
confirmButtonText: '已支付成功',
//中间布局
center:true,
//是否显示取消按钮
showCancelButton:true,
cancelButtonText:'支付遇见问题',//取消按钮的文本
// showClose:false,//右上角X
callback: ()=>{ //关闭后的回调
//清除定时器
clearInterval(this.timer);
this.timer = null ;
},
beforeClose:(type,instance,done)=>{//关闭前的回调,会暂停实例的关闭
/*type: 区分确定cancel|取消按钮 , 右上角X也是cancel
* instance: 当前组件实例
* done, 关闭函数*/
if (type==='cancel'){
alert('请联系管理员')
done()
}else {
if (this.isPay){
done();
this.$router.push('/paysuccess');
return
}
alert('支付失败, 请重试')
}
}
});
//支付成功路由跳转, 失败提示信息
this.timer = setInterval(async()=>{//设置定时器, 定时查询支付状态
let result = await this.$API.reqPayStatus(this.orderId);
console.log(result)
if (result.code === 200){
//保存支付成功信息
this.isPay = true;
this.$msgbox.close();//关闭支付弹窗
//清除定时器
clearInterval(this.timer);
this.timer = null ;
//跳转到支付成功页面
this.$router.push('/paysuccess');
}
},2000)
}
}
支付效果
8,我的订单
1,组件拆分
引入组件
支付成功跳转订单页面 PaySuccess.vue
<div class="paydetail">
<p class="button">
<router-link class="btn-look" to="/center">查看订单</router-link>
<router-link class="btn-goshop" to="/home">继续购物</router-link>
</p>
</div>
Center.vue , 右侧内容拆分到MyOrder组件中
<!-- 右侧内容 -->
<router-view></router-view>
2,注册路由
import Center from "@/pages/Center/Center";
//二级组件
import MyOrder from "@/pages/Center/children/MyOrder";
import GroupOrder from "@/pages/Center/children/GroupOrder";
//--------------------------------------------------------------
{
path: '/center',
component: Center,
meta:{show:true},
redirect: '/center/my',//默认进入我的订单
children:[
{
path:'my',
component:MyOrder,
},
{
path:'group',
component:GroupOrder,
}
]
}
3,获取订单数据, 动态展示
api
/*通过我的订单数据
* URL:/api/order/auth/${page}/${limit}
* method: get
* */
export const reqMyOrders = (page,limit)=>request.get(`/order/auth/${page}/${limit}`)
MyOrder.vue
<template>
<div class="order-right">
<div class="order-content">
<div class="title">
<h3>我的订单</h3>
</div>
<div class="chosetype">
<table>
<thead>
<tr>
<th width="29%">商品</th>
<th width="31%">订单详情</th>
<th width="13%">收货人</th>
<th>金额</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
</table>
</div>
<div class="orders">
<!--每一笔订单-->
<table class="order-item" v-for="record in myOrders.records" :key="record.id">
<thead>
<tr>
<th colspan="5">
<span class="ordertitle">{{record.createTime}} 订单编号:{{record.outTradeNo}} <span
class="pull-right delete"><img src="../images/delete.png"></span></span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(orderDetail,index) in record.orderDetailList" :key="orderDetail.id">
<td width="60%">
<div class="typographic">
<img :src="orderDetail.imgUrl" style="height: 60px;height: 60px">
<a href="#" class="block-text">{{ orderDetail.skuName }}</a>
<span>{{ orderDetail.skuNum }}</span>
<a href="#" class="service">售后申请</a>
</div>
</td>
<!-- rowspan和并单元格, 只需要遍历第一次的数据, 所以v-if="index===0"-->
<td width="8%" class="center" :rowspan="record.orderDetailList.length" v-if="index===0">{{record.consignee || '暂无'}}</td>
<td :rowspan="record.orderDetailList.length" v-if="index===0" width="13%" class="center">
<ul class="unstyled">
<li>总金额¥{{ record.totalAmount }}.00</li>
<li>在线支付</li>
</ul>
</td>
<td :rowspan="record.orderDetailList.length" v-if="index===0" width="8%" class="center">
<a href="#" class="btn">{{ record.orderStatusName }} </a>
</td>
<td :rowspan="record.orderDetailList.length" v-if="index===0" width="13%" class="center">
<ul class="unstyled">
<li>
<a href="mycomment.html" target="_blank">评价|晒单</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
<div class="choose-order">
<!--分页器组件-->
<Pagination
:pageNo ='page'
:pageSize="limit"
:total="myOrders.total"
:continues="5"
@getPageNo="getPageNo"/>
</div>
</div>
<!--猜你喜欢-->
<div class="like">
<h4 class="kt">猜你喜欢</h4>
<ul class="like-list">
<li class="likeItem">
<div class="p-img">
<img src="../images/itemlike01.png" />
</div>
<div class="attr">
<em>DELL戴尔Ins 15MR-7528SS 15英寸 银色 笔记本</em>
</div>
<div class="price">
<em>¥</em>
<i>3699.00</i>
</div>
<div class="commit">已有6人评价
</div>
</li>
<li class="likeItem">
<div class="p-img">
<img src="../images/itemlike02.png" />
</div>
<div class="attr">
Apple苹果iPhone 6s/6s Plus 16G 64G 128G
</div>
<div class="price">
<em>¥</em>
<i>4388.00</i>
</div>
<div class="commit">已有700人评价
</div>
</li>
<li class="likeItem">
<div class="p-img">
<img src="../images/itemlike03.png" />
</div>
<div class="attr">DELL戴尔Ins 15MR-7528SS 15英寸 银色 笔记本
</div>
<div class="price">
<em>¥</em>
<i>4088.00</i>
</div>
<div class="commit">已有700人评价
</div>
</li>
<li class="likeItem">
<div class="p-img">
<img src="../images/itemlike04.png" />
</div>
<div class="attr">DELL戴尔Ins 15MR-7528SS 15英寸 银色 笔记本
</div>
<div class="price">
<em>¥</em>
<i>4088.00</i>
</div>
<div class="commit">已有700人评价
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "MyOrder",
data(){
return{
//初始化参数
page:'1',
limit:'3',
myOrders:{}
}
},
mounted() {
//获取订单数据
this.getMyOrders();
},
methods:{
async getMyOrders(){
let result = await this.$API.reqMyOrders(this.page,this.limit)
if (result.code===200){
this.myOrders = result.data;
}else {
alert('获取我的订单失败: ',result.message)
}
},
getPageNo(page){//修改点击页数
this.page = page;
this.getMyOrders();
}
}
}
</script>
4,订单效果
八,完善项目
1,未登录的路由守卫判断
/router/index.js
/*全局前置守卫, 在路由跳转之前拦截
* to: 获取跳转的路由信息
* from: 从哪个路由来的信息
* next: 放行函数
* next()直接放行
* next('/login')---放行到指定路由
* next(false) 中断当前导航, 重置到 from路由地址
* */
router.beforeEach(async (to, from, next)=>{
// console.log(to,from,next)
let token = store.state.store_user.token;
let name = store.state.store_user.userInfo.name
let path = to.fullPath
// console.log(token,name,path)
if (token ){ //已登录情况
//...
}else {//未登录情况
// console.log(to)
//未登录不能去的地址
let noPath = ['/trade','/center','/pay'];
if (noPath.find(item=>path.indexOf(item)!==-1)){
alert('您还没有登录, 请登录后重试');
if (path.indexOf('/pay')===0){ //不保存pay页面
next('/login');
return
}
//将上次点击的页面保存参数传给login
next('/login'+'?redirect='+to.path);
}
next()
}
})
Login.vue
methods:{
async login(){
try {
const {phone,password} = this
if (phone!=='' && password!==''){
await this.$store.dispatch('userLogin',{phone,password});
//如果有传之前点击的页面,就跳转, 没有就跳转home
let toPath = this.$route.query.redirect || '/home'
await this.$router.push(toPath)
}
}catch (error){
alert(error.message)
}
}
}
2,已登录路由守卫判断(独享守卫)
已登录用户不能直接进入的页面
- 结算页面 - trade
- 支付页面- pay
- 只能从购物车shopcart点击结算跳转
router/routes.js
{
path: '/shopcart',
component: ShopCart,
meta:{show:true}
},
{
path: '/trade',
component: Trade,
meta:{show:true},
//路由独享守卫
beforeEnter:(to,from,next)=>{
//只能是购物车来的地址才放行, 或者当前页面(刷新)
if (from.path === '/shopcart' || from.path==='/'){
next()
}else {
next(false);//从哪来回哪去
// next(from)
}
}
},
{
path: '/pay',
component: Pay,
meta:{show:true},
beforeEnter:(to,from,next)=>{
console.log(to,from)
//只能是结算页面来的地址才放行, 或者当前页面(刷新)
if (from.path === '/trade' || from.path==='/'){
next()
}else {
next(false);//从哪来回哪去
// next(from)
}
}
},
{
path: '/paysuccess',
component: PaySuccess,
meta:{show:true},
/* beforeEnter:(to,from,next)=>{
if (from.path === '/pay' || from.path==='/'){//只能是支付页面来的地址才放行, 或者当前页面(刷新)
next()
}else {
next(false);//从哪来回哪去
// next(from)
}
}*/
},
3,组件内路由(不常用)
PaySuccess.vue
<script>
export default {
name: 'PaySuccess',
beforeRouteEnter(to,from,next){
/*渲染改组件对应路由被confirm 前调用
* 不能获取组件实例this因为组件实例还未创建
* */
if (from.path==='/pay' || from.path==='/'){
next();
}else {
next(false);
}
},
beforeUpdate(to,from,next) {
/*在当前路由改变, 但是该组件被复用时调用
* 举例: 带有动态参数的路径: /aaa/:id 在 /aaa/1 和 /aaa/2 跳转时
* 由于会渲染同一的aaa组件, 因此组件实例会被复用, 这是本钩子被调用
* 可以访问组件实例, this
* */
console.log('@@@',to,from,next)
},
beforeRouteLeave(to,from,next){
//在当前路由改变,但是该组件被复用时调用
//举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候,
//由于会演染同样的Foo组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
//可以访问组件实例'this'
if (confirm('是否确认离开?')){
next()
}
}
}
</script>
4,图片懒加载
vue-lazyload - npm (npmjs.com)
#安装
npm i vue-lazyload
main.js入口文件使用
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload);
import gif from '../assets/1.gif';//引入图片
// or with options
Vue.use(VueLazyload, {
//preLoad: 1.3,
error:gif,//错误图片
loading: gif,//只需要loading即可
//attempt: 1
})
测试search.vue
<!-- 商品图片 -->
<div class="p-img">
<router-link :to="`/detail/${goods.id}`" >
<!--<img :src="goods.defaultImg" /> 换成v-lazy-->
<img v-lazy="goods.defaultImg" />
</router-link>
</div>
5,自定义插件
myplugins.js
//Vue插件一定暴露一个对象, 指定install方法
let myplugins = {};
myplugins.install = function (Vue,options){
/*能获取Vue, 可以做很多事,options: 使用插件时传入的值
* Vue.prototype.$bus: 任何组件都能使用
* Vue.directive(); 指令 , 主要用来操作DOM
* Vue.component 全局组件
* Vue.filter..... 过滤器
* */
Vue.directive(options.name,(elment,params)=>{//全局指令
/*elment: dom元素
* params: 可以获取dom元素信息
* */
//将params值改成大小字母
elment.innerHTML = params.value.toUpperCase();
})
}
export default myplugins
main.js引入
//引入自定义插件
import myplugins from '@/plugins/myplugins';
Vue.use(myplugins,{
name:'upper',//可以传入参数
});
App.vue测试
<div id="app">
<h1 v-upper="msg"></h1>
//----------------------------------------------
name: 'App',
data(){
return{msg:'aaa'}
},
6,表单验证-Elment-UI
vee-validate - npm (npmjs.com)
npm i vee-validate
npm i vee-validate@3 //这里安装3
1,main.js引入
//按需引入ElementUI
import { MessageBox,Button,Form,FormItem,Input,Select,Col,Row,Checkbox,Footer,Header,Main,Container} from 'element-ui';
Vue.use(Button);//注册全局组件
Vue.use(Form);//注册全局组件
Vue.use(Input);//注册全局组件
Vue.use(Select);//注册全局组件
Vue.use(FormItem);//注册全局组件
Vue.use(Col);//注册全局组件
Vue.use(Row);//注册全局组件
Vue.use(Checkbox);//注册全局组件
Vue.use(Footer);//注册全局组件
Vue.use(Header);//注册全局组件
Vue.use(Main);//注册全局组件
Vue.use(Container);//注册全局组件
//另一种方式,挂在原型上
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
2,Register.vue
<template>
<el-container>
<!-- 注册内容 -->
<el-main>
<div class="register">
<h3>注册新用户
<span class="go">我有账号,去 <router-link to="/login">登陆</router-link>
</span>
</h3>
<el-row type="flex" justify="center" class="row-bg">
<el-form :model="ruleForm"
:rules="rules"
ref="ruleForm"
label-width="100px"
:inline="false"
class="reg-ruleForm">
<el-form-item label="手机号" prop="phone" >
<el-input placeholder="请输入你的手机号" v-model="ruleForm.phone"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="code" :inline="true" >
<el-row type="flex" justify="left" class="row-bg">
<el-col :span="60">
<el-input placeholder="请输入验证码" v-model="ruleForm.code"></el-input>
</el-col>
<el-col :span="6">
<el-button
style="margin-left: 30px"
type="info"
icon="el-icon-message"
@click="getCode">验证码</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="登录密码" prop="password" >
<el-input placeholder="请输入登录密码" v-model="ruleForm.password"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="password2" >
<el-input placeholder="请输入确认密码" v-model="ruleForm.password2"></el-input>
</el-form-item>
<el-form-item prop="isAgree" >
<el-checkbox v-model="ruleForm.isAgree" label="同意协议并注册《尚品汇用户协议》" name="type" ></el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="danger"
@click="submitRegForm('ruleForm')"
style="width: 150px;background-color: rgba(255,2,2,0.77)">完成注册</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</el-row>
</div>
</el-main>
<el-footer>
<!-- 底部 -->
<div class="copyright">
<ul>
<li>关于我们</li>
<li>联系我们</li>
<li>联系客服</li>
<li>商家入驻</li>
<li>营销中心</li>
<li>手机尚品汇</li>
<li>销售联盟</li>
<li>尚品汇社区</li>
</ul>
<div class="address">地址:北京市昌平区宏福科技园综合楼6层</div>
<div class="beian">京ICP备19006430号
</div>
</div>
</el-footer>
</el-container>
</template>
<script>
import debounce from 'lodash/debounce'
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Register',
data() {
let validatePhone = (rule, value, callback) => {
let verify = /^1[34578]\d{9}$/;
if (!value) {
callback(new Error('请输入手机号'));
} else if (!verify.test(value)) {
callback(new Error('请输入正确格式手机号'));
}
callback()
};
let validateCode = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入验证码'));
} else {
if (this.ruleForm.code !== this.$store.state.store_user.code) {
callback(new Error('验证码输入错误'));
}
callback()
}
};
let validatePwd = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入密码'));
} else {
if (this.ruleForm.password.length < 6) {
callback(new Error('请输入6位数以上密码'));
}
callback()
}
};
let validatePwd2 = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入确认密码'));
} else {
if (this.ruleForm.password!==this.ruleForm.password2) {
callback(new Error('两次密码不一致'));
}
callback()
}
};
let validateAgree = (rule, value, callback) => {
if (!value) {
callback(new Error('-----------请勾选协议'));
} else {
callback();
}
};
return {
ruleForm: {
phone:'',//电话号码
code:'',//验证码
password:'',//密码
password2:'',//确认密码
isAgree:true,//是否同意协议
},
rules: {
phone: [
{ required: true, validator: validatePhone,trigger:'blur'},
// { min: 11, max: 11, message: '长度在 11 个字符', trigger: 'blur' }
],
code: [
{ required: true, validator: validateCode,trigger:'change'},
],
password: [
{ required: true, validator: validatePwd,trigger:'blur' }
],
password2: [
{ required: true, validator: validatePwd2,trigger:'blur' }
],
isAgree: [
{ required: true, validator: validateAgree,trigger:'change' }
],
}
};
},
methods: {
//注册提交
submitRegForm(formName) {
this.$refs[formName].validate(async (valid) => {
if (valid) {
console.log('!!')
try {
const {phone,code,password} =this.ruleForm;
await this.$store.dispatch('userRegister',{phone,code,password})
this.$router.push('/login');//注册成功跳转到登录
return
}catch (error){
alert(error.message)
}
} else {
console.log('error submit!!');
return false;
}
});
},
//重置
resetForm(formName) {
this.$refs[formName].resetFields();
},
//获取验证码, 防抖
getCode:debounce(async function (){
try {
//如果获取到手机号再继续往下走
if (this.ruleForm.phone==='') return alert('请输入手机号')
await this.$store.dispatch('getCode',this.ruleForm.phone)
this.ruleForm.code = this.$store.state.store_user.code
}catch (error){
alert(error.message)
}
},1000),
}
}
</script>
<style lang="less" scoped>
.register {
width: 1200px;
height: 455px;
border: 1px solid rgb(223, 223, 223);
margin: 0 auto;
h3 {
background: #ececec;
margin: 20px;
padding: 6px 15px;
color: #333;
border-bottom: 1px solid #dfdfdf;
font-size: 20.04px;
line-height: 30.06px;
span {
font-size: 14px;
float: right;
a {
color: #e1251b;
}
}
}
.content {
padding-left: 0px;
margin-bottom: 18px;
position: relative;
label {
font-size: 14px;
width: 96px;
text-align: right;
display: inline-block;
}
input {
width: 270px;
height: 38px;
padding-left: 8px;
box-sizing: border-box;
margin-left: 5px;
outline: none;
border: 1px solid #999;
}
img {
vertical-align: sub;
}
.error-msg {
position: absolute;
top: 100%;
left: 555px;
color: red;
}
}
.controls {
text-align: center;
position: relative;
input {
vertical-align: middle;
}
.error-msg {
position: absolute;
top: 100%;
left: 495px;
color: red;
}
}
.btn {
text-align: center;
line-height: 36px;
margin: 17px 0 0 55px;
button {
outline: none;
width: 270px;
height: 36px;
background: #e1251b;
color: #fff !important;
display: inline-block;
font-size: 16px;
}
}
}
.copyright {
width: 1200px;
margin: 0 auto;
text-align: center;
line-height: 24px;
ul {
li {
display: inline-block;
border-right: 1px solid #e4e4e4;
padding: 0 20px;
margin: 15px 0;
}
}
}
</style>
3,效果
7,路由懒加载
当打包构建应用时, JavaScript包会变得非常大,影响页面加载。
如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合Vue的异步组件和Webpack的代码分割功能,轻松实现路由组件的懒加载。
routes.js
把直接引入换成访问时引入
//引入路由组件
// import Home from "@/pages/Home/Home";
let Home = ()=>{
return import("@/pages/Home/Home");
}
//简写
let Home = ()=> import("@/pages/Home/Home");
//再简写
{
path:'/home',
component:()=> import("@/pages/Home/Home"),
meta:{show:true}
},
全改
/*如果我们能把不同路由对应的组件分割成不同的代码块,
*然后当路由被访问的时候才加载对应组件,这样就更加高效了。
*/
export default [
{
path:'/home',
component:()=> import("@/pages/Home/Home"),
meta:{show:true}
},
{
path:'/login',
component:()=> import("@/pages/Login/Login"),
meta:{show:false}
},
{
path:'/register',
component:()=> import("@/pages/Register/Register"),
meta:{show:false}
},
{
path:'/search/:keyword?',
component:()=> import("@/pages/Search/Search"),
meta:{show:true},
name:'search',
//路由组件能不能传递props数据
//布尔值写法: 只能传递params参数
// props:true
//对象写法:额外的给路由组件传递一些props参数
// props:{a:1,b:2}
//函数写法(常用),可以接收params参数,query参数,通过props传递
/* props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k}
}*/
//简写
// props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})
},
{//重定向, /访问首页
path:'*',
// component:Home,
redirect:'/home',//重定向到home
meta:{show:true}
},
{
path:'/detail/:skuId',//查询商品详细需要传递id
component:()=> import("@/pages/Detail/Detail"),
meta:{show:true}
},
{
path: '/addcartsuccess',
name: 'addcartsuccess',
component: ()=> import("@/pages/AddCartSuccess/AddCartSuccess"),
meta:{show:true}
},
{
path: '/shopcart',
component: ()=> import("@/pages/ShopCart/ShopCart"),
meta:{show:true}
},
{
path: '/trade',
component: ()=> import("@/pages/Trade/Trade"),
meta:{show:true},
//路由独享守卫
beforeEnter:(to,from,next)=>{
console.log(to,from)
//只能是购物车来的地址才放行, 或者当前页面(刷新)
if (from.path === '/shopcart' || from.path==='/'){
next()
}else {
next(false);//从哪来回哪去
// next(from)
}
}
},
{
path: '/pay',
component: ()=> import("@/pages/Pay/Pay"),
meta:{show:true},
beforeEnter:(to,from,next)=>{
console.log(to,from)
if (from.path === '/trade' || from.path==='/'){//只能是结算页面来的地址才放行, 或者当前页面(刷新)
next()
}else {
next(false);//从哪来回哪去
// next(from)
}
}
},
{
path: '/paysuccess',
component: ()=> import("@/pages/PaySuccess/PaySuccess"),
meta:{show:true},
/* beforeEnter:(to,from,next)=>{
if (from.path === '/pay' || from.path==='/'){//只能是支付页面来的地址才放行, 或者当前页面(刷新)
next()
}else {
next(false);//从哪来回哪去
// next(from)
}
}*/
},
{
path: '/center',
component: ()=> import("@/pages/Center/Center"),
meta:{show:true},
redirect: '/center/my',//默认进入我的订单
children:[
{
path:'my',
component:()=> import("@/pages/Center/children/MyOrder"),
},
{
path:'group',
component:()=> import("@/pages/Center/children/GroupOrder"),
}
]
}
]
九,打包上线
1,打包
vue-cli-service build
生成dist目录
发现生成了很多map文件
项目打包后,代码都是经过压加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。
有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
所以该文件如果项目不需要是可以去除掉
2,去除打包生成的map文件
- 可以减少体积
vue.config.js 配置
productionSourceMap:false
重新打包
vue-cli-service build
3,nginx部署上线
#下载安装包
wget http://nginx.org/download/nginx-1.20.2.tar.gz
#安装所需依赖
yum -y install pcre pcre-devel zlib zlib-devel openssl openssl-devel
#解压
tar -zxvf nginx-1.20.2.tar.gz
#进入nginx目录
cd nginx-1.20.2
#预编译
#--prefix是指定安装目录
./configure --prefix=/opt/nginx
#编译
make
#安装
make install
#运行nginx
cd /opt/nginx/sbin/
./nginx
#查看进程
ps -ef|grep nginx
root 115792 1 0 14:40 ? 00:00:00 nginx: master process ./nginx
nobody 115793 115792 0 14:40 ? 00:00:00 nginx: worker process
root 116079 15539 0 14:40 pts/2 00:00:00 grep --color=auto nginx
#无法访问关闭防火墙或者开放端口
systemctl stop firewalld.service #临时防火墙
systemctl disable firewalld.service #永久关闭防火墙
#开放80端口
firewall-cmd --zone=public --add-port=80/tcp --permanent # 开放80端口
firewall-cmd --zone=public --remove-port=80/tcp --permanent #关闭80端口
firewall-cmd --reload # 配置立即生效
firewall-cmd --zone=public --list-ports #查看防火墙开放的端口
#启动
/opt/nginx/sbin/nginx
#停止服务
/opt/nginx/sbin/nginx -s quit
#重新加载配置
/opt/nginx/sbin/nginx -s reload
- 安装成功访问测试
1,将构建的dist目录放到喜欢的路径下
我这里放home
2,编辑nginx配置文件
#根据安装目录下的conf
vim /opt/nginx/conf/nginx.conf
修改ngxin.conf—>http—server–>
location / {
root /home/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://gmall-h5-api.atguigu.cn;
}
总文件ngxin.conf
user root;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /home/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://gmall-h5-api.atguigu.cn;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
3,重启nginx,测试
#重新加载配置
/opt/nginx/sbin/nginx -s reload
#停止服务
/opt/nginx/sbin/nginx -s quit
#启动
/opt/nginx/sbin/nginx
4,无法访问-报错Uncaught ReferenceError
- 查阅资料是组件中使用到了async/await
- Babel在转化的问题
#安装
npm install --save-dev @babel/plugin-transform-runtime
main.js中引入
//不清楚是不是必须的, 我加了
import 'babel-polyfill'
babel.config.js添加配置
- 注意放的位置
'plugins': ['@babel/plugin-transform-runtime']
查半天资料, 位置没放对, 导致一直报plugins错
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
//"presets": [["es2015", { "modules": false }]],es2015报错
["@babel/preset-env", { "modules": false }]
],
"plugins": [
//解决报错Uncaught ReferenceError: regeneratorRuntime is not defined
'@babel/plugin-transform-runtime',
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
},
]
],
}
5,访问成功
6,去除地址栏#号-history模式
1,在路由主文件下添加mode=history
默认是hash
mode:'history',
2,重新发布
vue-cli-service build
- 放到home目录下, 这里就叫
shop_history
3, 修改nginx配置文件
/opt/nginx/sbin/nginx -s quit #退出nginx
vim /opt/nginx/conf/nginx.conf
nginx.conf
user root;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /home/shop_history;
index index.html;
#指向@router
try_files $uri $uri/ @router;
}
#主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件
#因此需要rewrite到index.html中,然后交给路由在处理请求资源
location @router {
rewrite ^.*$ /index.html last;
}
location /api {
proxy_pass http://gmall-h5-api.atguigu.cn;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
4,访问测试
#启动nginx
/opt/nginx/sbin/nginx
- 刷新无问题
7,docker快速部署
1,安装docker
#安装常用的安装包
yum install -y bash-completion vim lrzsz wget expect net-tools nc nmap tree dos2unix htop iftop iotop unzip telnet sl psmisc nethogs glances bc ntpdate openldap-devel -y
#获取yum源
rm -f /etc/yum.repos.d/*
curl -o /etc/yum.repos.d/docker-ce.repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
curl -o /etc/yum.repos.d/Centos-7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
#清空yum缓存
yum clean all
#生成新缓存
yum makecache
#清空系统规则
iptables -F
3.关闭selinux
setenforce 0 # 临时关闭
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config # 永久关闭
systemctl disable firewalld && systemctl stop firewalld
#modprobe:用于向内核中加载模块或者从内核中移除模块。
modprobe br_netfilter
cat <<EOF > /etc/sysctl.d/docker.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.all.rp_filter = 0
net.ipv4.ip_forward=1
EOF
#卸载
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
##yum安装
yum install docker-ce -y
#配置镜像加速
sudo mkdir -p /etc/docker
#2.编写配置文件
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://8xpk5wnt.mirror.aliyuncs.com"]
}
EOF
#3.重启服务
sudo systemctl daemon-reload
sudo systemctl restart docker
#查看docker启动状态
docker version
2,启动nginx镜像
docker volume create nghtml
docker volume create ngconf
docker run -dp 80:80 --name nginx-1 --mount source=nghtml,target=/home/nginx/ --mount source=ngconf,target=/etc/nginx nginx
cp shop/ /var/lib/docker/volumes/nghtml/_data
3,替换nginx.conf
echo 1 > /var/lib/docker/volumes/ngconf/_data/nginx.conf
vim /var/lib/docker/volumes/ngconf/_data/nginx.conf
#sudo tee /var/lib/docker/volumes/ngconf/_data/nginx.conf << EOF
user root;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /home/nginx/shop;
index index.html;
#指向@router
try_files $uri $uri/ @router;
}
#主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件
#因此需要rewrite到index.html中,然后交给路由在处理请求资源
location @router {
rewrite ^.*$ /index.html last;
}
location /api {
proxy_pass http://gmall-h5-api.atguigu.cn;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
#EOF
4,放入项目shop
cp -r shop /var/lib/docker/volumes/nghtml/_data
cp nginx.conf /var/lib/docker/volumes/ngconf/_data/nginx.conf