项目资料
项目源码:
https://gitee.com/HusePanghu/project-SP
项目地址:HusePanghu.SPH
前言
提示:本文档的编撰初衷是用于复习和回顾该项目,而非该项目的教程文档,弊处多多,敬请包涵。欢迎大家在评论区交流。
1、各种基础文件介绍
node_modules:存储项目依赖文件
public:存放项目静态资源,注意:当项目打包时,webpack会把public文件夹原封不动的打包到dist文件中
src:程序员代码文件夹
assets:组件中的静态资源文件夹,一般存放组件公用的静态资源;在webpack打包的时候会将assets作为一个模块,打包到一个js文件夹中。
components:组件文件夹,存放vue中的各个组件。(一般是非路由组件)
App.vue:唯一的一个根组件
main.js:项目的入口文件,也是项目中最先执行的一个文件。babel.config.js:与Babel相关的配置文件
package.js:相当于项目‘身份证’,记录着项目的名称,项目有哪些依赖,项目怎么运行。
package-lock.json:缓存性文件
2、项目的一些其他配置:
1、项目启动 npm run serve 之后自动打开网页(localhost:8080)
package.json
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}
2、 关闭eslint校验提示
在vue.config.js文件夹中 设置 lintOnSave:false
//关闭lint校验提示
lintOnSave:false,
3、src文件夹设置别名 @ ,src文件夹创建别名,src/...简化为@/...
"paths": {
"@/*": [
"src/*"
]
},
3、项目路由的分析:
1、vue-router
何为路由:kv键值对
key:路由组件的路径
value:路由组件
2、项目中的路由组件:
home组件、search组件、login组件、register组件
3、项目中的非路由组件:
header组件、footer组件(在home、search组件中显示,在login、regist中不展示)
4、项目流程
项目以开发业务和逻辑为主,css和HTML开发为次
1、编辑静态页面、确定页面样式
2、拆分组件
3、获取服务器的动态展示数据
4、完成相应的动态业务逻辑
注意事项:1、编辑静态页面时,组件的结构+组件的样式+图片资源,编辑不要有遗漏 2、本项目中样式使用的是less样式,在<style>中需标注lang='less',还需npm 安装less-loader 3、当样式编辑好之后,记得得清除默认样式,否则编辑的样式可能被覆盖而显示不出来。 public/index.html <link rel="stylesheet" href="./reset.css">
5、项目路由的设定 vue-router
1、路由组件一般放置在pages||views文件夹中,components放置的为非路由组件
2、部分路由组件:home、search、login、register
3、设置路由器router,暴露并注册到main.js文件中;在路由器中注册路由组件
4、路由配置文件一般放在router文件中
5、当路由注册完成,不管是路由组件还是非路由组件,他们身上都有$router、$route属性
$router:一般进行编程式路由导航跳转(push、replace)
$route:一般获取路由信息(params、query、路径等信息)
6、声明式导航<router-link>,编程式导航push\replace 声明式导航功能<编程式导航功能
6、Footer的显示与隐藏,mate路由元信息
routes:[{
path:''',
components:,
mate:{show:false}
}]
v-show="$route.meta.show"
7、路由传参
传参的类型:params传参、query传参
params参数:属于路径中的一部分,注意在路由中配置路径是需要占位,占位符 ‘:‘
query参数:不属于路径的一部分,不需要占位,类似于ajax中的queryString,/home?k=v&kv=
三种方式:字符串传参、模板字符串、对象传参
//字符串传参
this.$router.push('/search/'+this.keyword+'?k='+this.keyword.toUpperCase())
//模板字符串传参
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
//对象传参(最常用)
this.$router.push({
name:'search',
params:{keyword:this.keyword},
query:{k:this.keyword.toUpperCase()}
})
8、拆分 home组件
把预先准备好的静态页面拆分成七个路由组件
css+HTML+图片资源,缺一不可,细心!
分别注册引入到home组件中
<template>
<div>
<TypeNav></TypeNav>
<ListContainer></ListContainer>
<TodayRecomment></TodayRecomment>
<Rank></Rank>
<Like></Like>
<Floor v-for="(floor,index) in floorList" :key="floor.id" :floor="floor"></Floor>
<Brand></Brand>
</div>
</template>
9、测试api接口(postman)
使用postman工具测试尚硅谷提供的接口没有问题
接口返回的code字段为200,则表示接口访问成功,状态正常
整个项目接口前缀都有/api字样
10、axios二次封装,二次封装的目的
1、配置请求拦截器和响应拦截器
请求拦截器:在发起请求访问之前,进行一些业务处理。比如添加请求头关键字、设置请求进度条
响应拦截器:在获得返回数据之后,可以进行一些业务处理。
2、api文件夹
用来存放api接口相关的文件和配置文件
接口文档中都有/api,baseURL:'/api'
'
//利用axios里的create方法创建axios实例
const requests=axios.create({
//设置基础路径
baseURL:'/api',
//设置访问超时时间
timeout:5000
})
11、跨域问题
1、什么是跨域?从本地访问协议、端口、域名不同的地址叫做跨域访问,区别于本地访问
HTTP://localhost:8080/#/home 本地服务器
http://gmall-h5-api.atguigu.cn 后台服务器,非本地服务器
2、跨域的解决办法:JOSNP、CORS、代理proxy
12、引入进度条nprogress
npm i nprogress@0.2.0
在请求拦截器中开始,在响应拦截器中结束
引入nprogress的样式,(/node_modules/nprogress/nprogress.css),样式可修改 蓝色 #29d
//设置请求拦截器
requests.interceptors.request.use((config)=>{
//进度条开始
nprogress.start();
return config;
})
//设置响应拦截器
requests.interceptors.response.use((res)=>{
//成功的回调函数:当访问数据返回后,可以响应拦截,做一些业务处理
//进度条结束
nprogress.done();
return res.data;
},(error)=>{
return Promise.reject(new Error('fail'))
})
13、vuex
1、多个组件间状态共享的集中式状态管理工具,用于组件间通讯
大项目可用来维护数据,使数据维护轻量化、便捷化,小项目没必要使用vuex
属性:state,actions,mutations,getter,modules
2、vuex模块化开发
把所有的状态(数据)放到一个store中,当项目数据过多时,就会显得store过于臃肿,
数据的易维护特点便也失去了意义,所以就有了vuex模块化开发
modules:{}
14、完成TypeNav的三级联动数据展示
1、全局组件TyepNav,使用vuex发起api接口访问,获得数据
mounted() {
//vue一挂载就立马发起请求CategoryList,存储于仓库中
this.$store.dispatch('categoryList')
}
2、mounted(){}挂载里编辑回调函数,vue一挂载便开始请求api获取数据
store.home actions->mutations->state使用mapstate展示数据
<div class="sort" v-show="show" @mouseleave="showLeave">
<div class="all-sort-list2" @click="goSearch">
<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"
:class="{cur:index==currentIndex}">
<h3 >
<!-- 渲染后 元素的属性名会自动转为小写 -->
<a @mouseenter="indexChange(index)" :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{ c1.categoryName }}</a>
</h3>
<!--二三级展示列表 -->
<div class="item-list clearfix" :style="{display:index==currentIndex?'block':'none'}">
<div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a :data-categoryName="c2.categoryName"
:data-category2Id="c2.categoryId">{{ c2.categoryName }}</a>
</dt>
<dd>
<em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
<a :data-categoryName="c3.categoryName"
:data-category3Id="c3.categoryId">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
15、向一级分类动态添加背景色
1、第一种解决方案,采用样式
.item:hover{
background: skyblue;
}
2、第二种解决方法,TypeScript
创建参数currentindex获取category列表的index值,当index==currentindex时 触发样式
:class="{cur:index==currentindex}"
样式cur .cur{blackground:skyblue}
在methods中写触发事件函数,@mouseenter=“”,@mouseleave=“”
16、通过js控制二三级样式的隐藏与展示
原生css中的style为:display:block|none
//js控制样式:
:style="{display:index==currentIndex?'block':'none'}
//vue v-show:
v-show="index==currentIndex"
17、卡顿现象
当事件触发非常频繁,每次事件触发都会进行一次回调,频繁执行回调则有可能导致浏览器卡顿。
节流:在规定的时间间隔内不会触发事件回调,只有当时间间隔大于设定值回调才会被执行,使频繁触发变为少量触发
防抖:前面的触发执行都被取消,只有最后一次在规定时间间隔里的触发才被执行。频繁的时间触发只执行一次。debounce
18、完成三级联动的节流操作
//节流器写法,频繁触发->少量触发,节流触发次数
import throttle from "lodash/throttle"
indexChange:throttle(function (index){ this.currentIndex=index; },50),
19、三级联动的路由跳转->search.html
策略:a标签+编程式导航+事件委派 event.target定位标签+自定义属性
缘由:页面一渲染,声明式导航<router-link>就会在循环体里会自动生成很多导航标签,内存使用率高,效率低。编程式导航同样存在内存占用高,页面卡顿的情况,所以均不采用
goSearch(event) {
//获取event事件的点击位置
let element = event.target
// console.log(element)
let {
//前端标签里的属性名自动转为小写,取值的时候得注意
categoryname,
category1id,
category2id,
category3id
} = element.dataset
console.log(categoryname, category1id, category2id, category3id)
//整理路由跳转的参数
if (categoryname) {
let location = {name: 'search'}
let query = {categoryName: categoryname}
if (category1id) {
query.category1Id = category1id
} else if (category2id) {
query.category2Id = category2id
} else {
query.category3Id = category3id
}
if(this.$route.params){
location.query = query
location.params=this.$route.params
// console.log(location);
this.$router.push(location);
}
}
},
20、对search页面进行categoryList显示控制
1、鼠标悬停展示,鼠标移走关闭显示
2、均在TypeNav.vue 编辑js操作,使用this.$route.path 获取当前页面路径
3、添加过渡效果 <transition name="xxx"></transition>
组件必须要有v-if/v-show属性才能使过渡效果生效
样式: .xxx-enter{} .xxx-enter-to{} .xxx-enter-active{transition: all 0.2s linear;}
21、search页面的性能优化问题
每跳转一次search页面就会发起一次categoryList的get请求,而在第一次获取到请求结果时,store中已经存储了请求数据 所以只需请求一次即可 将请求代码 this.$store.dispatch('categoryList')写在App.vue的mounted中,数据全局可使用。
22、合并参数query和params参数
在search的input框中执行回调函数时,向push中加入query参数
在三级联动的路由跳转回调中,加入params参数
location.query=this.$route.query;
location.params=this.$route.params;
23、swiper 轮播图插件
import ‘/swiper/css/swiper.css’
swiper轮播图生效的前提是必须先渲染前端结构
mock 模仿 模拟发起访问请求,获取数据。数据是假数据,前提写好的。
mockServe.js
import Mock from 'mock.js'//导入的Mock为一个函数
import banner from './banner.json'
Mock.mock(url:'/mock/banner',{code:200,data:banner})
24、轮播图效果生效时间问题。
由于swiper生效的前提是页面dom必须渲染完成,而swiper实例创建在哪里就成了问题
1、swiper实例创建在mounted中,执行swiper实例创建时,数据也还没拿到,页面dom就没完成渲染,轮播效果不生成。
2、在mounted中写一个计时器setTimeout(),实例创建放在计时器中,实例创建完成时,dom创建了,页面也有了,轮播效果也有,但有延时,不完美!
nextTick:在下一次更新dom 循环结束之后,开始执行延时回调函数 异步操作, nextTick保证了执行延时回调时,dom已经更新。
3、在mounted中使用nextTick,实例创建放在nextTick中,轮播有效果,但有延时,因为mounted执行完成就会开始执行nextTick,但此时bannerList获取数据这个流程却还没有完成,页面依旧没有渲染(异步操作的原因)。
4、watch+nextTick解题,监视mounted中的bannerList是否有完成数据获取,当bannerList中获取了数据,再开始执行swiper实例创建
此方法完美解决轮播生效问题。
watch:{
bannerList:{
handler(newValue,oldValue){
this.$nextTick(()=>{
const mySwiper = new Swiper ('.swiper-container', {
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
clickable:true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})})}}}
25、组件中通讯的方式
props通讯 父传子
自定义绑定事件:@on,@emit 子传父
消息全局总线:$bus 全能
消息订阅与发布:pubsub 全能
插槽
vuex 全能
26、Carsouel全局组件
将swiper轮播图组件全局注册,实现全局复用
并利用props传参。:list="list" 组件props接收
//注册 Carsouel轮播组件为全局组件
import Carsouel from '@/components/Carsouel/index'
Vue.use(Carsouel.name,Carsouel)
<!--轮播图组件Carsouel-->
<Carsouel :list="floor.carouselList"></Carsouel>
27、Search路由组件
1、search路由静态页面拆分和资源部署
css+html+images,并注册路由组件
2、获取search页面resSearchInfo数据
1、api/index.js const reqresSearchInfo=(parmas)=>{
return requests({rul:'/list',method:'post',data:parmas})
}
2、在search mounted(){} this.$store.dispatch();
store 三连载 actions->mutations->getters->mapGetters
3、在search页面更新动态数据
3、发起请求访问时,需要携带参数
this.$store.dispatch("resSearchInfo",this.searchParmas);
params=Object.assign(this.searchParmas,this.$route.parmas,this.$route.query)//合并相同的参数
28、search页面存在的一个问题
在搜索框中输入keyword点击搜索之后页面没反应,只有刷新页面才会展示搜索内容
是因为在点击搜索之后,并没有触发访问请求api,dispatch
所以可以对keyword进行watch监视,监视页面路由route变化,一旦路由发生变化就发起访问请求
watch: {
$route(newValue, oldValue) {
//再次更新请求参数,
//Object.assign()合并具有相同参数的对象,并更新参数
Object.assign(this.searchParams, this.$route.params, this.$route.query)
this.getData();
//请求结束后需要清空路由参数,防止下一请求参数叠加混乱
this.searchParams.category1Id = "";
this.searchParams.category2Id = "";
this.searchParams.category3Id = "";
}
}
29、面包屑
页面上细小的属性块,面包屑展示的是路由信息,可删除
1、search页面的面包屑,展示了当前搜索的关键信息,categoryName、keyword、trademark、AttrValue
2、每当添加、删除一个面包屑,搜索页的关键信息改变,也就是路由路径改变,都需要重新发起一次访问请求来更新search页面。绑定自定义点击事件
<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i
@click="removeCategoryName">×</i></li>
removeCategoryName() {
//点击面包屑‘×’标志,请求路径中的属性被清除,并且重新发起访问请求并刷新页面
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
//路径请求属性清除之后,再次发起访问请求,刷新页面
// this.getData();//由于路径改变,watch监视路径能监视到变化,所以会触发watch里的请求,而这里就不需要重复请求一次了。
//但是清除面包屑属性并不会修改搜索框params属性,所以params属性在请求路径中需要保留
if (this.$route.params) {
// console.log(this.$route.params)
this.$router.push({name: 'search', params: this.$route.params})
}
},
30、售卖属性的操作 升序与降序
1、属性:综合和价格
点击”综合“展示综合属性的商品列表,默认降序展示,再点一次”综合”升序展示,同时样式改变↓->↑
changeSort(flag) {
//获取之前的flag和sort信息
let originFlag = this.searchParams.order.split(":")[0];
let originSort = this.searchParams.order.split(":")[1];
//定义一个新order
let newOrder = "";
if (flag == originFlag) {//当页面的flay没有发生改变,则改变排序方式
newOrder = `${flag}:${originSort == 'desc' ? 'asc' : 'desc'}`;
} else {//如果改变了,则为newOrder输入新flay和默认排序方式
newOrder = `${flag}:desc`;//${"desc"}
}
//更新order属性,发起访问请求,刷新页面
this.searchParams.order = newOrder;
this.getData();
},
2、引入阿里巴巴图标库
1、在public的index.html中添加引入的图标样式链接
<!-- iconfont阿里图片链接-->
<link rel="stylesheet" href="https://at.alicdn.com/t/c/font_3566864_wlpeakzjqio.css">
2、引入样式 class="iconfont" 绑定样式 :class="{'icon-long-arrow-up'}"
<span v-show="isOne" class="iconfont"
:class="{'icon-long-arrow-up':isUp,'icon-long-arrow-down':isDown}">
</span>
31、手写分页器 重难点
1、分页器的结构一般分为6部分
«上一页 第一页 ... 中 ... 最后一页 下一页» 共10条数据
中间部分的连续展示页continues一般为奇数长度(5、7、9页),居中效果美观。
2、难点:如何在不同情况下确定连续展示页的起始页和结束页(start,end)
startNumAndendNum() {
let start = 0;
let end = 0;
const {pageNo, totalPage, continues} = this;
if (totalPage < continues) {//当总页数小于连续页长度时
start = 1;
end = totalPage;
} else { //正常情况下确定起始页和结束页
start = pageNo - Math.floor(continues / 2);
end = pageNo + Math.floor(continues / 2);
if (start < 1) { //起始页小于1时重新确定起始页、结束页
start = 1;
end = continues;
}
if (end > totalPage) { //结束页大于总页数时
start = totalPage - continues + 1;
end = totalPage;
}
}
return {start, end}
}
3、页面编辑分页器结构
<!-- 上-->
<button :disabled="pageNo==1" @click="goPageNo(pageNo-1)">上一页</button>
<!-- 第一页-->
<button v-if="pageNo>Math.ceil(continues/2)" @click="goPageNo(1)" :class="{active:pageNo==1}">1</button>
<button v-if="pageNo>1+Math.ceil(continues/2)">···</button>
<!-- 中-->
<button v-for="(n,index) in startNumAndendNum.end-startNumAndendNum.start+1"
:key="index" @click="goPageNo(startNumAndendNum.start+n-1)"
:class="{active:pageNo==startNumAndendNum.start+n-1 }">{{ startNumAndendNum.start + n - 1 }}
</button>
<!-- 下-->
<button v-if="pageNo<totalPage-Math.ceil(continues/2)">···</button>
<!-- 最后一页-->
<button v-if="pageNo<totalPage-Math.ceil(continues/2)+1"
@click="goPageNo(totalPage)"
:class="{active:pageNo==totalPage}">{{ totalPage }}
</button>
<button :disabled="pageNo==totalPage" @click="goPageNo(pageNo+1)">下一页</button>
<!--数据列表的数据条数-->
<button style="margin-left: 30px">共 {{total }} 条</button>
4、访问指定页 点击分页器中的某一页跳转到该页
绑定点击事件@click 获取当前页 pageNo 更新pageNo,发起请求访问
searchPage(page) {
this.searchParams.pageNo = page;
this.getData();
}
5、注册分页器为全局组件,全局复用
//注册Pagination分页器组件为全局组件
import Pagination from '@/components/Pagination/index'
Vue.use(Pagination.name,Pagination)
<Pagination :pageNo="searchParams.pageNo"
:pageSize="searchParams.pageSize"
:total="total"
:continues="5"
@goPageNo="searchPage"></Pagination>
32、商品详情页面
1、编辑页面: HTML、css、静态资源,在router中注册路由组件
2、动态展示商品详情页信息
1、准备vuex,新建一个子库,detail.js
2、detail.js state、actives、mutations、getters 在主库中注册
3、向后端发起请求,获取数据
const reqGoodList=(skuId)=>{ return requests({url:`/detail/${skuId}`,method:"get"})}
mounted(){ this.#store.dispatch("GoodInfo",this.$route.params.skuId)}
vuex actives->mutations
mapState({
goodInfo:(state)=>{
return state.detail.goodInfo.categoryView
}
})
注意:由于存在页面已经开始渲染了但绑定的属性却还是undefined的情况,控制台会报错,
避免报错,采用mapGetters获取数据可以避免该问题,因为在detail的store中可
可以getters预先处理数据,若还未获取到数据赋空{}|[]
categoryView(){
return state.goodInfo.categoryView||{};
},
...mapGetters(['categoryView'])
4、在页面用插值语法{{}}展示数据
33、详情页面的放大镜
根据鼠标事件获取展示图片的像素坐标(event.offsetX,event.offsetY),以像素坐标为中心点展示一个放大的图片mask
难点:1、获取的像素坐标的约束范围(left,top)
//属性约束
if(left<=0) left=0;
if(left>=mask.offsetWidth) left=mask.offsetWidth;
if(top<=0) top=0;
if(top>=mask.offsetHeight) top=mask.offsetHeight;
2、放大倍数的方向问题
放大之后,放大的倍数好确定,但得注意,像素坐标得反方向放大,展示的才为像素坐标为中心的图片
//放大展示 反方向
big.style.left=-2*left+'px';
big.style.top=-2*top+'px'
34、商品可选属性的排他操作
商品的颜色、配置、内存容量等属性可供用户选择,但选择一个高亮展示,其他的同类型属性就得关闭高亮展示,高亮只能有一个,排他操作。
v-for遍历每个属性,在属性中配置一个点击事件,并传递当前属性和全部属性
@click="isCheck(spuSaleAttr,spuSaleAttrValue)"
spuSaleAttr.forEach((item)=>{
//关闭高亮效果
item.isChecked=0;
)
spuSaleAttrValue.isChecked=1;
forEach(()={})遍历每一对象
35、将商品添加到购物车
1、<a @click="addShopCart">加入购物车</a>
2、派发actions,三连环:dispatch->actions->reqAddOrUpdataCart
问题:商品添加到谁的购物车里?由于此时未登录,认定添加到了一个游客的购物车里。一般购物网站点击到了这一步会要求登录才能进行下一步
问题:怎么添加到游客的购物车里?访问请求的接口reqAddOrUpdataCart会根据请求头headers里的关键字段uuid_token,将商品信息添加到该游客的购物车里。
3、设置uuid_token
import {getUUID} from '@/utils/uuid_token.js'
在store.detail中 uuid_token=getUUID();
import {v4 as uuidv4} from 'uuid'
export const getUUID=()={
let uuid_token=localstoreg.getItem('UUIDTOKEN')
if(!uuid_token){
uuid_token=uuidv4();
//将uuid_token存储到本地存储中,持久化存储
localStorage.setItem.setItem('UUIDTOKEN',uuid_token)
}
return uuid_token;
}
//将uuid_token添加到requests.js的请求拦截器的headers中
//返回config配置对象,里边有一个属性很重要,header请求头
//给请求头添加一个userTempId临时id,userTempId字段为后端接收的字段名,不可随意编辑,否则后端显示无该参数
if(store.state.detail.uuid_token){
config.headers.userTempId=store.state.detail.uuid_token;
}
36、成功添加购物车
若let result=await reqAddOrUpdataCart() result.code==200,则购物车添加成功
//添加或更新购物车,传递参数skuId,skuNum
export const reqAddOrUpdataCart=(skuId,skuNum)=>{
return requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:'post'})
}
37、购物车页面
1、HTML、style、css、静态资源,在router中注册路由组件
2、创建shopCart的vuex
1、/store shopCart.js state->actions->mutations->getters
3、获取购物车商品列表 getCart();请求接口会根据headers 的userTempId获取该游客(未登录)的cartList
mounted(){this.getCart()}
getCart()->actions->reqShopCartList->mutations->getters return state.cartList[0]|| [];//当数据还未获取到时,返回一个空数据,避免报错的保险操作
computed:{...getters(['cartList'])}//获取数据,页面展示 v-for
4、删除购物车某一商品
<a class="sindelet" @click="DeleteCartGood(good.skuId)">删除</a>
DeleteCartGood(skuId)->dispatch->actions->reqDeleteCarListById->回到DeleteCartGood,再次调用getCart()重新获取购物车页面商品信息
5、修改某一个商品的勾选状态
<input @change="updateChecked(good,$event)">
updateChecked->dispatch->actions->reqUpdateCheckedById(skuId,isChecked)->回到updateChecked,再次调用getCart()重新获取购物车页面商品信息
38、删除全部勾选的商品
<a @click="deleteAllChecked">删除选中的商品</a>
deleteAllChecked->dispatch->actions->在actions中派发dispatch,dispatch('deleteCartList',item.skuId)->getCart();
代码实现:forEach逐一删除
deleteAllCheckedList({dispatch,getters}){
let PromiseAll=[]
getters.cartList.cartInfoList.forEach(item=>{
let promise=item.isChecked==1?dispatch('deleteCartList',item.skuId):''
PromiseAll.push(promise)
})
//只要全部的p1|p2....都成功,返回结果即为成功
//如果有一个失败,返回即为失败结果
return Promise.all(PromiseAll);
}
39、勾选全选框控制所有商品的勾选
updateAllChecked({dispatch,getters},isChecked){
let PromiseAll=[]
getters.cartList.cartInfoList.forEach(item=>{
let promise=dispatch('updateCheckedById',{skuId:item.skuId,isChecked})
PromiseAll.push(promise)
})
return Promise.all(PromiseAll);
}
//computed:
isAllCheck(){
//every是forEach的break用法,
//遍历数组里面原理,只要全部元素isChecked属性都为1===>真 true
//只要有一个不是1======>假false
let checked= this.cartInfoList.every((good)=>good.isChecked==1)
return checked;
},
40、登录注册
1、HTML、css、style、静态资源,在router中注册路由组件
2、创建user store库 vuex
用于存储用户的相关消息
user.js (state actions mutations getters)->store index.js->向主库注册子库
41、注册组件
1、四个input框,手机号、验证码、密码、再次输入密码
data(){return{phone:"",code:'',password:'',password1:''}}
全部v-model=''绑定
2、获取验证码
由于向手机号发送验证码,有成本,所以就直接从后端获取验证码之后直接自动输入到input框中
<button @click="getCode" >获取验证码</button>
getCode->dispatch->actions->reqGetCode->mutations->state.code
this.code=this.$store.state.user.code,v-model双向绑定,input自动填入
3、注册用户信息
由于验证码为自动填入,则没有验证验证码是否正确的一步
所以直接验证,phone、code是否填入,password==password1
//注意!访问请求的参数一定不能写错,参数名保持一致,顺序保持一致
this.$store.dispatch('userRegister',{phone,password,code})->actions->reqUserRegister
code=200 后端成功注册用户信息->注册成功跳转到登录页面 this.$router.push('/login')
42、用户登录
1、在用户注册之后,后台会为用户生成一个用户令牌token。
2、用户登录时输入的邮箱/用户名/手机号、password仅用于验证登录,用户信息均已后台获取得到的为准,获得用户信息需要token
3、用户登录成功之后,会返回一个token,将token存储到localStorage中,持久化存储,页面刷新用户也不会丢失。
//给请求头添加token
if(store.state.user.token){
config.headers.token=store.state.user.token;
}
export const=setToken(token){localStorage.setItem('TOKEN',token)}
//引入setToken
import {setToken} from '@/utils/token.js'
this.$store.dispatch('userLogin',{password,phone})->actions setToken(result.data.token)->mutations->state.token或者 state.token:getToken();
//获取token
export const getToken=()=>{
return localStorage.getItem('TOKEN')
}
4、获取用户信息
1、home页面根据用户token,获取用户信息 this.$store.dispatch('getUserInfoByToken'),在home的header中展示个人信息
2、获取到userInfo信息之后存储到user state.userInfo中,在header页面展示用户信息||退出登录,如果userInfo.name为空则展示登录||注册
43、退出登录
1、发起退出登录的请求,并清空用户信息(userInfo,token)
logout->actions->reqLogout->mutations
//mutations 操作state,退出登录,清除用户信息
LOGOUT(state){
state.token=''
state.userInfo=''
clearToken()
}
import {clearToken} from '@/utils/token.js'
//清除本地存储数据
export const clearToken=()=>{
localStorage.removeItem('TOKEN');
}
44、路由守卫
1、全局守卫
对项目中全局的路由跳转都进行监控和管制,可通过全局前置守卫(router.beforeEach())
和后置守卫(router.afterEach())进行路由跳转管制。
2、独享守卫
某一个路由所独享的一个守卫,路由代码写在routes的组件中,独享守卫没有后置守卫,但可以和全局后置守卫配合使用。router.beforeEach((to,from,next)={})。
3、组件内守卫
路由规则写在路由组件中。beforeRouterEnter(){}进入守卫,进入组件时路由规则被调用
beforeRouterLeave(){}离开守卫,路由规则在离开该组件时被调用
beforeRouterUpdate(){}更新守卫,路由更新时路由规则被调用。
4、vue的路由访问形式/模式:hash和history。有#的为hash路由,#值后边的内容称之为hash值。
45、使用路由导航控制用户的登录注册行为
当用户未登录时,无法访问交易页面、订单页面、支付页面、个人主页并自动跳转到登录页面。
当用户登录之后,home页面的登录/注册将会被替换为个人信息和退出登录。当用户在网址导航栏中跳转时,将会被自动跳转到主页。
//@/router/index.js
Router.beforeEach((to,from,next)=>{
const toPath=to.path;
const fromPath=from.path;
const token=this.$store.state.user.token;
if(!token){//用户未登录,无token
if(toPath.indexOf('trade')!=-1||toPath.indexOf('pay')!=-1||toPath.indexOf('center')!=-1){
next('/login');//路由守卫管制,跳转到login页面
}else{next();//除了上述地址,其余可以访问,放行}
}else{//用户已登录
if(toPath=='/login'||toPath=='/register'){
next('/home');//自动跳转到主页。
}else{
if(name){next();}else{
//当用户登录之后,用户名获取不到,则重新获取
try{
//重新获取userInfo
await store.dispatch('getUserInfoByToken')
next();
}catch (error) {
//如果userInfo获取失败,则表明token失效,获取不到userInfo
//token失效,重新登录
await store.dispatch('logout')
next('/login')
}
}
}
}
})
46、订单提交页面(trade)
1、html、css、images、注册路由
2、动态绑定数据
//获取用户地址信息
this.$store.dispatch('getUserAddressInfo')
//获取订单信息
this.$store.dispatch('getOrderInfo')
store三连环:dispatch->actions->mutations->state
...mapState({userAddressInfo:(state)=>{return state.trade.userAddressInfo},
orderInfo:(state)=>{return state.trade.orderInfo}})
3、绑定路由
在ShopCart页面声明式导航,跳转到'/trade'
编辑组件内路由守卫路由规则,只能从ShopCart页面跳转到Trade页面
在trade页面:
//组件内守卫
//只能从购物车跳转到交易页面
beforeRouteEnter(to,from,next){
if(from.path=='/shopcart'){
// next('/trade');
next();
}else {
next(false);
}
},
4、提交订单,并跳转到支付页面
1、编程式导航
<a class="subBtn" @click="submitOider">提交订单</a>
this.$router.push(`/pay?orderId=${result.data}`)
2、向后端接口发起请求,提交订单信息
async submitOider(){
//提交订单
const {orderInfo}=this;
this.orderId=orderInfo.tradeNo;
this.orderDetailList=orderInfo.detailArrayList;
let data={
consignee: orderInfo.consignee,
consigneeTel: orderInfo.phoneNum,
deliveryAddress:orderInfo.fullAddress,
paymentWay: "ONLINE",
orderComment: this.msg,
orderDetailList:this.orderDetailList
}
let result=await this.$API.reqSubmitOrder(this.orderId,data)
console.log(result)
//订单提交成功,跳转到pay页面
if(result.code==200){
this.$router.push(`/pay?orderId=${result.data}`)
}else {
return result.message;
}
}
47、支付页面Pay
1、html、css、images
2、获取支付页面信息
1、不再使用state三联环获取数据,使用全局API发起请求,返回结果直接存储在data()中。原来是在actions中发起请求,在mutations中处理数据,存储在state中,现在尝试一种新方式,直接在组件中操作api并存储返回结果。
1、将api接口js文件全局暴露,并全局引入。
在mian.js文件中 import * as API from '@/api/index'
beforeCreate中 Vue.prototype.$API=API 全局引入完成。
2、发起api请求
data(){return{payMentInfo: {}}}
const result=this.$API.reqPaymentInfo(this.orderId);//注意异步请求
if(result.code==200){this.payMentInfo=result.data||{}}
3、绑定动态数据,订单号、订单总金额
48、订单支付功能 使用element-ui messageBox支付二维码弹窗
1、element-ui按需引入,全局引入会使得项目打包之后的文件非常庞大,占用空间。
//按需引入需要安装插件babel-plugin-component,详情查询官网
//按需引入element-ui
import {Button, MessageBox} from 'element-ui';
Vue.use(Button);
Vue.use(MessageBox)
//或者组件方式引入
// Vue.component(Button.name, Button);
// Vue.component(Select.name, Select);
//使用
<Button/>
2、引入element 消息盒子和消息弹窗
import MessageBox from 'element-ui';
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
3、QRcode 链接转二维码插件,npm i qrcode 下载
import QRCode from 'qrcode'
let url=await QRCode.toDataURL(this.payMentInfo.codeUrl)
4、使用element组件 创建定时器setInterval,不断查看支付状态,当获取并保存支付状态,关闭定时器clearInterval(this.timer),this.timer=null;
async open() {
let url=await QRCode.toDataURL(this.payMentInfo.codeUrl)
this.$alert(`<img src= ${url} />`, '微信支付', {
dangerouslyUseHTMLString:true,
center:true,
confirmButtonText: '已完成支付',
showCancelButton:true,
cancelButtonText:'支付遇到问题',
beforeClose:(action,instance,done)=>{
if(action=='cancel'){
alert('请稍后再试')
//关闭定时器
clearInterval(this.timer);
this.timer=null;
done();
}else {
if(this.payMentCode==205){
// clearInterval(this.timer)
// this.timer=null;
done();
this.$router.push('/paysuccess');
}
}
}
});
//在支付框里发起请求,查看支付状态
if(!this.timer){
this.timer=setInterval(async ()=>{
let result=await this.$API.reqPaymentStatus(this.payMentInfo.orderId);
//保存订单状态码
console.log(result)
this.payMentCode=result.code;
if(result.code==205){
clearInterval(this.timer);
this.timer=null;
console.log('定时器已关闭')
// this.$msgbox.close();
// this.$router.push('/paysuccess')
}
},2000)
}
},
5、支付完成跳转paysuccess页面
<router-link to="/paysuccess">支付完成</router-link>
49、我的订单页面Center(个人中心)
1、HTML、css、images
2、注册路由
{
name:'center',
component:Center,
path:'/center',
mate:{
show:true
}
},
3、订单页面为二级路由页面
注册二级路由
{
name:'center',
component:Center,
path:'/center',
mate:{
show:true
},
children:[
{
name:'myorder',
component:MyOrder,
path:"myorder",//二级路由下,路径直接写路径名,不用带'/'.
},
{
name:'grouporder',
component:GroupOrder,
path:'grouporder',
},
{//重定向,自动展示二级路由下的myorder
path:'/center',
redirect:'/center/myorder'
}
]
},
<router-view />
50、编辑myorder页面
1、HTML、css、images
2、请求访问全局api获取数据
const result=this.$API.reqAllPaymentInfo(this.page, this.limit);
data(){return orders:{}}
if(result.code==200){this.orders=result.data;}
3、页面绑定动态数据
4、注册到center路由下的子路由中,完成路由绑定
51、图片懒加载 lazyload
import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyLoad,{
loading:'@/assets/images/img.gif'
})
在页面使用v-lazy='' 代替img=''就实现了图片懒加载的运用
52、路由懒加载
component:()=>{return import('@/pages/search/index.vue')}
只有当调用路由时才加载路由,而非项目一挂载就全部加载,提高了路由的高效性
const foo=()=>{ruturn import('@/pages/search/index.vue')}
{
name:'search',
path:'/search',
component:foo//调用路由
}
//简写形式:
{
name:'search',
path:'/search',
component:()=>import('@/pages/search/index.vue'),
}
53、vee-validate表单验证
1、plugins思维,当使用一款插件时,插件的引入、调用、属性编辑、编辑规则我们可以全部放在一个js文件里,在main.js文件里全局引入。这样就不用全部堆在main.js文件里显得臃肿。实现分块管理。比如,element-ui的引入和Vue.use()使用全部放在一个js文件里,在main.js文件里导入该文件。实现element-ui的按需引用。
2、在@/plugins/validate.js文件里编辑相关信息
//vee-validate 插件导入
import Vue from 'vue'
import VeeValidate from 'vee-validate'
//引入中文验证
import zh_CN from 'vee-validate/dist/locale/zh_CN'
Vue.use(VeeValidate);
//编辑属性
VeeValidate.Validator.localize('zh_CN',{
messages:{
...zh_CN.messages,
is:(fail)=>{
return `${fail}必须与密码相同`;
}
},
attributes:{
phone:'手机号',
code:"验证码",
password:"密码",
password1:"确认密码",
agree:"协议"
},
})
//自定义校验规则
VeeValidate.Validator.extend('agree',{
validate:(value)=>{
return value;
},
getMessage(field) {
return field+'必须同意'
}
})
3、在页面绑定校验信息
<input type="text" placeholder="请输入你的手机号"
v-model="phone"
name="phone" v-validate="{required:true,regex:/^1\d{10}$/}"//编辑输入框的输入规则
:class="{invalid:errors.has('phone')}"
>
//绑定样式
<span class="error-msg">{{ errors.first('phone') }}</span>//显示提示信息
54、项目打包
1、在项目文件夹下npm run build,生成dist文件
2、项目上线参考:(2条消息) ubuntu20.04安装nginx并配置反向代理_HusePanghu的博客-CSDN博客