npm install请切换到app目录下安装
https://www.bilibili.com/video/BV1Vf4y1T7bw?spm_id_from=333.999.0.0
尚品汇:
https://gitee.com/jch1011/shangpinhui_0415.git
尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili
尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili5:50秒左右
传送门:(1) 浅谈 JS 防抖和节流 - SegmentFault 思否
尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili
尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili 36分开始
很想知道后台是如何实现的
如何防止用户通过地址栏进入不该进入的页面。见第十一天第二点bug描述
vue create 项目名即可创建一个vue项目,前提是配好webpack+node.js环境详见
尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通_哔哩哔哩_bilibili
node_modules:放置项目依赖的地方
public:一般放置一些共用的静态资源,打包上线的时候,public文件夹里面资源原封不动打包到dist文件夹里面
src:程序员源代码文件夹
-----assets文件夹:经常放置一些静态资源(一般为全局静态资源),assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
-----components文件夹:一般放置非路由组件(或者项目共用的组件)
App.vue 唯一的根组件
main.js 入口文件【程序最先执行的文件】
babel.config.js:babel配置文件
package.json:看到项目描述、项目依赖、项目运行指令
README.md:项目说明文件
package.json下
"scripts": {
"serve": "vue-cli-service serve --open",这个地方加上--open
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
根目录下创建vueconfig.js文件
module.exports = {
lintOnSave:false,
}
因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些
创建jsconfig.json文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"以后@就相当于src了
]
}
},
"exclude": [
"node_modules",
"dist" 在这两个文件夹下面不能使用@别名
]
}
注意如果在css里要用到别名,要在别名前加一个~
一般路由组件写在pages文件夹下(view)需要自己创建。
安装路由:
cnpm install --save vue-router --save:可以让你安装的依赖,在package.json文件当中进行记录.
关于npm和cnpm的说明
npm 和 cnpm 的区别,你真的搞懂了嘛 - 爱看星星的稻草人 - 博客园 (cnblogs.com)
然后我们在src下创建router/index.js在里面写我们的路由配置。(后期路由配置太多,也可以把index.js拆开。比如当前项目就拆开了一个routes.js)
项目采用的less样式,浏览器不识别less语法,需要一些loader进行处理,把less语法转换为CSS语法
切记less-loader安装5版本的,不要安装在最新版本,安装最新版本less-loader会报错,报的错误setOption函数未定义
定义路由的时候可以配置 meta
字段:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
我们可以用这个meta字段里面配置的键值对来进行v-if或者v-show判断是否显示组件
使用的时候:
<Footer v-show="this.$route.meta.show"></Footer>这样就可以根据元信息里的show键判断该不该显示foot组件了
简单说params参数通过网址反映就是一连串/来传参
query就是?后接参数,=连接多个参数
路由传递参数先关面试题
1、路由传递参数(对象写法)path是否可以结合params参数一起使用?
不可以:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置!
2、如何指定params参数可传可不传
配置路由的时候在占位符后面加一个?
详见尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili
10:00开始
3、params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
如果指定name与params配置, 但params中数据是一个"", 无法跳转,路径会出问题。这个时候可以用undefine解决
this.$router.push({
name:'search',//注意这个地方需要路由组件配置name属性
params:{
keyword:""||undefine
},
query:{
k:this.keyword.toUpperCase()
}
})
4、路由组件能不能传递props数据?
可以,有三种写法
{
name:'xiangqing',
path:'detail/:id',
component:Detail,
//第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
// props:{a:900}
//第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
// props:true
//第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
//第三种写法很强大,有简写形式详见
// https://www.bilibili.com/video/BV1Zy4y1K7SH?p=124 13:30左右
props(route){//route这个参数是$route
return {
id:route.query.id,
title:route.query.title
}
}
}
这部分详见尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili
注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。
这种异常,对于程序没有任何影响的。
为什么会出现这种现象:
由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,因此出现上面现象,
第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调
第一种解决方案可以暂时解决当前问题,但是以后再用push|replace还是会出现类似现象,因此我们需要从‘根’治病;
解决方法见尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili
说实话我感觉没必要解决这个问题的
拆分组件:结构+样式+图片资源
一共要拆分为七个组件
axios二次封装: XMLHttpRequest、fetch、JQ、axios 为什么需要进行二次封装axios? 请求拦截器、响应拦截器:请求拦截器,可以在发请求之前可以处理一些业务、响应拦截器,当服务器数据返回以后,可以处理一些事情 在项目当中经常API文件夹【axios】 接口当中:路径都带有/api baseURL:"/api" I 有的同学axios基础不好,可以参考git|NPM关于axios文档
在src下创建api文件夹,在里面创建request.js
//对于axios进行二次封装
import axios from "axios";
//1:利用axios对象的方法create,去创建一个axios实例
//2:request就是axios,只不过稍微配置一下
const requests = axios.create({
//配置对象
//基础路径,发请求的时候,路径当中会出现api
baseURL:"/api",
//要在vue.config里配置好代理服务器
//代表请求超时的时间5S
timeout:5000,
});
//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
//config:配置对象,对象里面有一个属性很重要,header请求头
return config;
})
requests.interceptors.response.use((res)=>{
//成功的回调函数,服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情
return res.data;
},(error)=>{
//失败的回调函数
return Promise.reject(new Error("faile"));
})
//对外暴露
export default requests;
项目很小:完全可以在组件的生命周期函数中发请求项目大:axios.get('xxx') I
什么是跨域:协议、域名、端口号不同请求,称之为跨域
在vue.config.js配置下
cnpm install --save nprogress
app\src\api\request.js下
import nprogress from "nprogress"
//如果出现进度条没有显示:一定是你忘记了引入样式了
import "nprogress/nprogress.css";
//start表示进度条开始 done表示结束
在请求拦截器里面:
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("faile"));
})
这样我们每次请求开始就会有进度条,响应开始进度条就结束
进度条样式可以改的,但是需要改别人的源码nprogress/nprogress.css的样式
cnpm install --save vuex
创建src/store/index.js
import Vue from "vue";
import Vuex from 'vuex'
//使用Vuex插件一次
Vue.use(Vuex)
//state (厨房)存储数据的地方
const state={}
//actions (服务员)处理action,可以在这里书写自己的业务逻辑,也可以处理异步
const actions={}
//mutations (厨师)处理state数据的唯一手段
// 一般把mutations里写的方法写成大写,用于区分actions中的方法
const mutations={}
//getters 理解为计算属性,用于简化仓库数据,让组件获取仓库数据更方便
const getters={}
//对外暴露Store类的一个实例
export default new Vuex.Store({
state,
actions,
mutations,
getters
})
分别创建store/home/index.js和store/search/index.js,内容一样
const state={}
const actions={}
const mutations={}
const getters={}
export default{
state,
actions,
mutations,
getters
}
import Vue from "vue";
import Vuex from 'vuex'
Vue.use(Vuex)
//引入小仓库
import home from './home'
import search from './search'
export default new Vuex.Store({
modules:{
home,
search
}
})
import {reqCategoryList} from '@/api/index'
const state={
//这个地方的数据类型不要乱写,要根据接口文档写合适的数据类型
categoryList:[]
}
const actions={
//这个地方的 async await还不清楚作用
async categoryList(context){
let result=await reqCategoryList();
//这个地方的result本质就是:前台通过axios传递给后台的这条请求的结果
if(result.code==200){
//如果请求成功,那么服务员把后端请求到的数据交给大厨
context.commit('CATEGORYLIST',result.data)
}
}
}
const mutations={
CATEGORYLIST(state,value){
state.categoryList=value
}
}
const getters={}
export default{
namespaced:true,//开启命名空间
state,
actions,
mutations,
getters
}
<script>
import {mapState} from 'vuex'
export default {
name:'TypeNav',
//组件一挂载完毕,就向服务器发送请求
mounted() {
this.$store.dispatch('home/categoryList')//使用命名空间后一定要指明是向哪个模块的服务员发送
},
computed:{
...mapState('home',['categoryList'])//使用命名空间后一定要指明是向哪个模块的服务员发送
}
};
</script>
改用js
如果业务逻辑过多,浏览器就会来不及计算,出现卡顿现象
防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次
使用Lodash插件解决
官方文档lodash.debounce | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)
节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
官方文档:lodash.throttle | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)
这个插件node_modules下已经有了,最好按需引入
这种方法相当于给每一个商品类别绑定了一个rotuer-link
router-link是一个组件:相当于VueComponent类的实例对象。快速滑动鼠标,一瞬间new VueComponent很多实例(1000+),很消耗内存,因此导致卡顿。
这种方法相当于给每一个商品类别绑定了一个a标签
通过给a标签绑定事件,在事件里面进行push。这种也不是最好,因为有很多个a标签,链接过多
但这样也有很多问题,我们一个一个解决
有两个问题
我们给一二三级联动都加上两个自定义属性:data-categoryName和data-categoryId1(这里只演示一级的,二三级改下名字)
由此解决了a标签的问题。
参数问题通过第二个属性data-categoryId1拼接下也可以解决
//路由跳转到search
//1.如何确定点击a标签才会跳转
//2.如何给这些a标签传参,以获取不同路由参数
goSearch(event){
//获取当前事件节点
let element=event.target
//节点有个dataset属性,可以获取节点的自定义属性和自定义值
let {categoryname,categoryid1,categoryid2,categoryid3}=element.dataset
//如果标签上有categoryName肯定是a标签,有categoryid1肯定是一级联动,二三级同理
let location={name:'search'}
let query={categoryName:categoryname}
if(categoryname){
if(categoryid1){
//注意大小写问题
query.categoryId1=categoryid1
}else if(categoryid2){
query.categoryId2=categoryid2
}else{
query.categoryId3=categoryid3
}
}
//整理完参数
location.query=query
this.$router.push(location)
// console.log(location);
}
这样我们的TypeNav在home里就会直接显示,在search里会隐藏
我们希望在home里,鼠标滑动移除后TypeNav显示,而在search里的TypeNav鼠标滑动移除后隐藏
最后达成的效果就是home里的TypeNav一上来就展示,而且鼠标划出一级标题后,TypeNav不会隐藏
而在search里的TypeNav一上来不展示一级标题,而且鼠标划出一级标题后,TypeNav会隐藏
我们想在TypeNav一级目录显示时,有一段动画效果。这个地方我使用的是第三方的animate.css库,和老师不一样。注意只有有v-show或v-if的标签才能使用动画
详见尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通_哔哩哔哩_bilibili
cnpm install --save animate.css
引用
import 'animate.css'
发现一个问题来回跳转home和search组件会不断发送请求
我们把这行代码写在根组件App的mounted里就行了
我们希望得到的效果是这样,用户在搜索框搜索的数据(用的params)和在三级联动(用的query)中选择的数据,可以一起作为参数传过去,这就需要合并参数。
这两个if不用也行。而且注意,由于这两个location他们的name都是search,并且分别写了
location.query = this.$route.query;和
location.params = this.$route.params;
所以才会更新
cnpm install --save mockjs
把mock数据需要的图片放到public文件夹中,(public文件夹打包的时候,会把相应的资源原封不动打包到dist文件夹中)
//先引入mockjs模块
import Mock from 'mockjs';
//把JSON数据格式引入进来[JSON数据格式根本没有对外暴露,但是可以引入]
//webpack默认对外暴露的:图片、JSON数据格式
import banner from './banner.json';
import floor from './floor.json';
//mock数据:第一个参数请求地址 第二个参数:请求数据
Mock.mock("/mock/banner",{code:200,data:banner});//模拟首页大的轮播图的数据
Mock.mock("/mock/floor",{code:200,data:floor});
main.js里面import
新建文件
//这个文件和request.js几乎一样,只改了baseURL和对外暴露名
import axios from "axios";
import nprogress from "nprogress"
import "nprogress/nprogress.css";
const mockRequests = axios.create({
baseURL:"/mock",
timeout:5000,
});
mockRequests.interceptors.request.use((config)=>{
nprogress.start()//进度条开始
return config;
})
mockRequests.interceptors.response.use((res)=>{
nprogress.done()//进度条结束
return res.data;
},(error)=>{
return Promise.reject(new Error("faile"));
})
//对外暴露
export default mockRequests;
不要忘记修改api下的index.js
import requests from './request'
import mockRequests from './mockAxios'
//三级联动接口
///api/product/getBaseCategoryList get 无参数
export const reqCategoryList=()=>{
//这里就不用写全api/product/getBaseCategoryList因为requests已经处理好基础路径了
return requests({url:'product/getBaseCategoryList',method:'get'})
}
export const reqGetBannerList=()=>{
return mockRequests({url:'/banner',method:'get'})
}
我们在ListContainer里发送请求到仓库的服务员那里
在仓库里写好对应的函数名
接下来,我们和以前一样把vuex完善好,最好不要忘记使用mapState取出来,这部分就不截图了,和以前一样的
测试一下(这是在官网抄的)Swiper使用方法 - Swiper中文网
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="js/swiper.js"></script>
<link rel="stylesheet" href="css/swiper.min.css">
<style>
.swiper-container{
width: 600px;
height: 300px;
}
</style>
</head>
<body>
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- 如果需要滚动条 -->
<div class="swiper-scrollbar"></div>
</div>
<script>
//这个参数
var mySwiper = new Swiper('.swiper-container', {
// direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
</script>
</body>
</html>
也就是说第一个参数也可以是真实dom
cnpm install --save swiper@5
我们在ListContainer里面写好v-for,写好对应的swiper所需的必要结构,正确引入swiper
正确引入swiper,要引入相关js和css
在写必要的swiper js代码时,遇见了一个问题,不知道在什么时机写,因为涉及到异步请求数据和v-for遍历数据的问题。
具体来说是因为swiper需要等待页面结构初始化完成,才生效。但是我们有一个v-for里面在用axios获取后台数据,这就产生了问题。
我们目前用两个不是很好的办法解决。
1、写在update里,这样如果有其他数据发生改变会重复使用js代码不好
2、写在定时器里,这样也不好,因为不好确定异步请求时间
使用watch发现还是不行
使用watch只能保证数据由空变化到有数组,但不能保证v-for执行完了
和开发ListContainer组件差不多,也是使用mockjs请求假数据,然后vuex。这里有个不同的地方,就是我们发给仓库服务员dispatch的方法要写在home组件里不能写在floor组件里,因为我们用了两次floor组件。然后我们mockjs里面也是传的两套结构一样内容不一样的json,所以在home使用floor组件时用v-for遍历,再使用props传递参数给子组件floor,以便于后续操作。
可以看到请求返回的数据结构,(我们用的mockjs,其实就是我们floor.json里面的数据)下面我们一个一个替换(这里只展示关键的)。
我们都是通过父组件穿给我们的list也就是props属性来收数据
原因是因为,我们是通过props来接收数据的,这和以前不同,swiper需要的结构一定是建立好了的,所以可以直接卸载mouted里,不用像以前用watch+$nextTick的方式
我们发现在ListContainer和floor中都会用到轮播图,于是我们把它抽取出来做成一个公共组件。注意红字部分
有两个地方可以到达search模块,一个是公共组件TypeNav里面的三级联动,这部分是传递的query参数;
另一个地方是header组件的搜索按钮,它传递的是params参数
详见第四天第1节内容
还是那几套路
这个老师都拆分好了
注意这次请求要传参数
//这个请求要传参数,默认参数至少是一空对象
export const reqGetSearchInfo = (params) => {
return requests({ url: '/list', method: 'post', data:params })
}
和以前一样,但这次我们为了简化仓库数据,用了getters
const getters={
goodsList(state){
return state.searchList.goodsList
},
attrsList(state){
return state.searchList.attrsList
},
trademarkList(state){
return state.searchList.trademarkList
}
}
和以前一样用v-for,把mapGetters里面的数据遍历出来
Object.assign:这个就是把this.$route.query和this.$route.params的键值对转到this.searchParams上
这个发请求的问题我们后面跟着再解决。
然后就是params单词不要拼错了!!!!!!!!我找了好久的错
这里老师都封装好了,我们只需动态展示数据就行。我们直接到仓库里捞数据就行,不用发请求了。然后v-for遍历出来就行
我们之前写的只能发送1次请求,但是我们的参数是在动态发生变化的,这部分逻辑见第四天第一点
注意请求发完以后,一定要把3个商品ID清空,防止出现这种情况:用户第一次点击一个一级联动,正确返回结果:如果用户再点击一个二级联动,那么searchParms里上一次的categoryId1就还存在,这一次的categoryId2也存在,这显然是一种错误的情况。我们在这里清空后,具体工作流程是这样的:
用户第一次点进来(通过搜索栏或者home的三级联动),这个时候路由信息正确改变,详见第四天第一点。首先会触发beforeMount里的内容,也就是把路由参数合并下(注意不要把params写错了!!!),searchParams参数正确。然后会触发mounted函数内容,也就是发送一次请求。这个地方要注意,如果用户不刷新,或者点出去又点进来search路由组件,beforeMount和mounted两个生命周期函数就不执行了。那么接下来,如果用户再次点击搜索框或者三级联动,就会触发watch监听函数,因为$route里面的params或者query改变了。watch函数先再次合并一下正确的参数searchParams,然后发送请求,接着把searchParams的三个商品ID清空。注意这个地方如果用户下次再次点击搜索框或者三级联动触发watch时,searchParams就不会有上一次searchParams残留的商品ID了,就只含有新的的parms或者新的query的参数了,这就是把searchParams的三个商品ID清空的作用
就是这个东西
这里我们还可以顺便改变地址栏
这个地方大部分和分类面包屑一样
但是我们还想加一个需求,就是用户点击删除关键字后,搜索框里的内容置空。这就涉及到serach组件与header组件两个兄弟组件之间的通讯,这里我们使用全局事件总线$bus解决
在search组件里
在header组件里
在header组件里
最后我们像在分类面包屑一样,处理下地址栏就行了
需求:我们点击search子组件searchSelector中的品牌,可以正确向后台发送请求
父组件触发回调
演示
格式
整理参数
处理品牌面包屑
品牌面包屑必须用v-if不用v-show。原因:删除品牌后trademark是undefined,而undefined肯定不能split进行分割。所以这段逻辑直接v-if不予渲染代码,v-show虽然也能不进行展示,但是代码还是存在会报错。因为v-show只是display:none
接口格式
代码查看3个参数
这个地方和4.1大体一样也是用自定义事件的方法传参,这里我就只展示父组件search自定义事件的回调了
测试时发现错误
props数组不能有重复元素
又发现一个错误,删除分类面包屑之后也要把,售卖属性和品牌置空。这样更符合逻辑
也就是说总共只有4种:
1:desc,1:asc,2:desc,2:asc
接下来我们优化下箭头样式,在阿里图标里面找
详细操作见
尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili24分10秒
isOne判断order里面有无1,如果有那么就是综合可以给综合添加背景色,综合的箭头也可以显示,isTwo同理
isAsc判断order里面有无asd,如果有那么就是升序,对应箭头显示升序,isDesc同理
概述: 为什么很多项目采用分页功能:比如电商平台同时展示的数据有很多(1万+),采用分页功能.ElementUI是有相应的分页组件,使用起来超级简单,但是咱们前台项目目前不用【掌握自定义分页功能】 分页器展示,需要哪些数据(条件)? 需要知道当前是第几个:pageNo字段代表当前页数
需要知道每一个需要展示多少条数据:pagesize字段进行代表
需要知道整个分页器一共有多少条数据:total字段进行代表---[获取另外一条信息:一共多少页]
需要知道分页器连续页码的个数:5|7【奇数】,常为奇数对称(好看)
总结:对于分页器而言,自定义前提需要知道四个前提条件。 pageNo:当前第几个 pageSize:代表每一页展示多少条数据 total:代表整个分页一共要展示多少条数据 continues:代表分页连续页码个数
举例子:每一页3条数据 一共91条数据 【一共是31页】
我们把分页器做成一个全局组件,拆分全局组件步骤和以前一样,见
静态资源见下
<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 {
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>
子组件计算参数步骤:(父子组件通过props传参数)
export default {
name: "Pagination",
props:['pageNo','pageSize','total','continues'],
computed:{
//总共多少页
totalPage(){
//向上取整
return Math.ceil(this.total/this.pageSize)
},
//连续页码的起始页和终止页
startAndEndPage(){
// 先定义两个变量
let start=0,end=0;
//非正常现象1:连续页比总页数多
if(this.continues>this.totalPage){
start=1,
end=this.totalPage
}else{
start=this.pageNo-parseInt(this.continues/2)
end=this.pageNo+parseInt(this.continues/2)
// 非正常现象2:start出现非正数
if(start<1){
start=1,
end=this.continues
}
// 非正常现象3:end>总页数
if(end>this.totalPage){
end=this.totalPage,
start=this.totalPage-this.continues+1
}
}
return {start,end}
}
}
}
设计子(全局分页组件)父(search组件)通信,使用自定义事件
父组件search里
子组件全局分页组件里
完整代码
<template>
<div class="pagination">
<!-- 上 -->
<button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">
上一页
</button>
<button
v-if="startAndEndPage.start > 1"
@click="$emit('getPageNo', 1)"
:class="{ active: pageNo == 1 }"
>
1
</button>
<button v-if="startAndEndPage.start > 2">···</button>
<!-- 中 -->
<button
v-for="(page, index) in startAndEndPage.end"
:key="index"
v-if="page >= startAndEndPage.start"
@click="$emit('getPageNo', page)"
:class="{ active: pageNo == page }"
>
{{ page }}
</button>
<!-- 下 -->
<button v-if="startAndEndPage.end < totalPage - 1">···</button>
<button
v-if="startAndEndPage.end < totalPage"
@click="$emit('getPageNo', totalPage)"
:class="{ active: pageNo == totalPage }"
>
{{ totalPage }}
</button>
<button
:disabled="pageNo == totalPage"
@click="$emit('getPageNo', pageNo + 1)"
>
下一页
</button>
<button style="margin-left: 30px">共 {{ total }} 条</button>
</div>
</template>
<script>
export default {
name: "Pagination",
props: ["pageNo", "pageSize", "total", "continues"],
computed: {
//总共多少页
totalPage() {
//向上取整
return Math.ceil(this.total / this.pageSize);
},
//连续页码的起始页和终止页
startAndEndPage() {
// 先定义两个变量
let start = 0,
end = 0;
//非正常现象1:连续页比总页数多
if (this.continues > this.totalPage) {
(start = 1), (end = this.totalPage);
} else {
start = this.pageNo - parseInt(this.continues / 2);
end = this.pageNo + parseInt(this.continues / 2);
// 非正常现象2:start出现非正数
if (start < 1) {
(start = 1), (end = this.continues);
}
// 非正常现象3:end>总页数
if (end > this.totalPage) {
(end = this.totalPage), (start = this.totalPage - this.continues + 1);
}
}
return { start, end };
},
},
};
</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组件的商品图片可以跳转到详情页,为此必须传参(用params参数),所以要加一个占位符
使用的时候声明式导航就可以了
我们发现路由信息太多把它拆分出去
我们发现一个问题,在search组件点击图片经过路由跳转到detail组件时,滚轮和之前的位置持平,在靠下的位置。而我们希望,点过来后,默认在最上边,这个时候就可以使用VueRouter的进阶操作:滚动行为.
详见官方文档
接口文档
添加api
vuex三件套
小仓库
dispatch发送时机,当detail组件挂载完毕就发送
我们用getters简化仓库数据
使用
发现报错
原因是因为我们的getters写的还不够完善.当服务器数据还没传过来时,categoryView是undefine,取categoryView里面的category1Name这些数据时自然就报错了,但是当后面服务器数据传过来时,就没这个问题了。所以说这个报错是一个假报错
解决办法:
我们接着动态展示数据
在放大镜效果这里,我们需要把skuInfo.skuImageList的数据传给子组件
v-for去取
我们使用swiper,并让其一次展示3张图
需求:点击轮播图小图,放大镜展示。
由于轮播图ImageList组件和放大镜Zoom组件是兄弟组件,我们用全局时间总线解决
完整代码
methods: {
//这里event不写也有
handler(event){
let mask=this.$refs.mask
let big=this.$refs.big
//计算并修改left和top
let left=event.offsetX-mask.offsetWidth/2
let top=event.offsetY-mask.offsetHeight/2
//约束范围
if(left<=0)left=0;
if(left>=mask.offsetWidth) left=mask.offsetWidth
if(top<=0) top=0
if(top>=mask.offsetHeight) top=mask.offsetHeight
mask.style.left=left+'px'
mask.style.top=top+'px'
//放大效果,这里要看样式调整放大倍数
big.style.left=-2*left+'px'
big.style.top=-2*top+'px'
}
},
我们将购物车数据写入服务器成功后,需要进行路由跳转。在此之前,在组件里如何判断写入服务器是否成功呢?有两个办法,我们在actions里判断,成功或者失败,新创造一个参数存储在state里,detail组件里去取就行了。第二个办法使用Promise,这个地方详细步骤
见尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili16分钟开始
大概意思就是我们的async函数最后返回的一定是个Promise,我们再用trycatch判断就行了
actions
detail组件
组件老师已写好,注册使用即可
(这个组件只是用来提示用户,添加购物车成功。然后也可以跳转到结算购物车和商品详情页面)
我们跳转路由的时候需要传递参数给AddCartSuccess组件,可以使用路由query传参的方式把产品详细信息(对象,数据很多)和产品数目传过去,虽然可以办到,但是这样地址栏不美观(因为有产品详细信息这个对象做参数),于是我们想到路由传参只传产品数目,产品详细信息存储在浏览器上。
我们这里选择使用会话存储,不使用本地存储。本地存储是持久化存储,我们这个只需要存储产品详细信息,会话存储足够了。
注意浏览器本地存储只能存储字符串,使用JSON.stringfy,取得时候用JSON.parse
Detail组件
AddCartSuccess组件
接下来就把computed里的数据动态展示就行了
效果
需要带参数说明是哪一个商品
AddCartSuccess组件
老师已写好ShopCart组件,记得注册路由
shopCart组件
在store/detail/index.vue 也就是小仓库下调用
把这个uuid_token写在响应头里,要和后台商量好
带上uuid后,便可正常获取购物车数据
actions里result格式
使用
这个地方太简单了,就不写了……
用户每次在购物车里增加,减少或者直接修改产品数我们都要向后台发送请求,这个请求我们已经写了
注意skuNum参数是新的产品数-原来产品数。比如以前是4台手机,新的是7台,那么skuNum就是3;再比如以前是4台,新的是2台,那么skuNum就是-2
(这个地方如果我们购买同一产品,后台会自动加上产品数的)
//改变产品数量
//type有三种,对应+,-和直接输入; cartInfo商品信息,disNum相对改变数目
hander(type, cartInfo, disNum) {
//该标志位为false时,不必发送请求
let flag = true;
switch (type) {
//直接加最简单
case "plus":
break;
case "minus":
if (cartInfo.skuNum <= 1) {
//小于1不发请求
flag = false;
}
break;
case "change":
if (isNaN(disNum) || disNum < 0) {
//负数和非数字不发请求,但是要再次获取购物车数据
flag = false;
this.getData()
} else {
//要考虑小数
disNum = parseInt(disNum) - cartInfo.skuNum;
}
break;
}
try {
if (flag) {
//修改成功后,要再次获取购物车数据
this.$store.dispatch("detail/AddOrUpdateShopCart", {
skuId: cartInfo.skuId,
skuNum: disNum,
});
this.getData()
}
} catch (error) {
alert(error.message)
}
},
接口
api
vuex
使用
1.7修改购物车商品状态
接口
api
vuex
使用
没有接口,我们要利用之前写的根据ID删除商品的方法
使用
vuex
//删除所有选中的商品
deleteAllselected({ dispatch, getters }) {
//获取当前购物车中全部的产品
let PromiseArray = []
getters.cartList.cartInfoList.forEach(cartInfo => {
//把选中的商品,调用deleteCart方法
let promise = cartInfo.isChecked == 1 ? dispatch("deleteCart", cartInfo.skuId) : ""
//将每次返回的promise写入数组
PromiseArray.push(promise)
//console.log(promise);
});
//只有当PromiseArray中所有的Promise都成功,返回结果才成功
return Promise.all(PromiseArray)
}
这个地方也没有接口,逻辑上和1.7删除所选商品很像
使用
注意如果没有购物车没有商品,全选按钮会有bug
我们最后一天再解决电话,密码这些格式问题,现在注重业务逻辑
登录注册组件老师已写好,直接引用。注意引用照片路径问题,在css使用别名要加一个~
接口
api
vuex三连环
我们把登录注册业务再注册一个小仓库
使用
接口
api
vuex
使用
注册完毕记得跳转路由,目前还只是做了一半,还没有做验证。最后一天做
接口
api
vuex(这次要存token)
token一般来说就是识别用户的唯一标志符,我们注册成功后后台会为我们的账号添加一个token。在前台我们想要获取特定用户的信息往往只需要向后台发出token就可以了。本质上token和我们项目里的uuid功能是一样的,不同的是uuid是我们发送给后台,token是后台发送给我们
使用
注意阻止一下form标签默认行为
登录成功后台显示的数据
不完善的地方
由此需要新的接口
接口
api
vuex
请求拦截器
验证
观察
完善vuex
完善登录成功后header组件展示信息
原因,刷新后vuex仓库数据清空,没有token了。
解决方法:持久化存储token
vuex
但这样还是有问题:用户一旦离开home组件,再次刷新header组件还是变成未登录状态
这是因为header组件要想变成登录状态,要靠vuex仓库中的userInfo。这个userInfo又需要
actions中的getuserInfo,而我们只在home组件mounted时dispatch了,所以离开home组件时再刷新
就会出现问题。这个我们以后用导航守卫解决。
我们需要再写一个退出登录的功能,这个退出功能也是有接口的
接口api就不截屏了
vuex
使用
完成退出功能后,再使用导航守卫解决bug
在router/index.js里配置全局前置守卫
router.beforeEach(async (to,from,next)=>{
let token=store.state.user.token
let name=store.state.user.userInfo.name
// console.log(store.state.user.userInfo,"userInfo");
// console.log(store.state.user.token,"token");
// 用户已登录
if(token){
//用户已登录还想去登录
if(to.path=='/login'){
//直接回首页,或者写/home也行
next('/')
// console.log("用户已登录还想去登录");
}else{
//用户有name信息
if(name){
//放行
next()
// console.log("用户已登录且有用户名");
}//没有用户信息,dispatch后再放行
else{
try {
await store.dispatch("user/getUserInfo")
next()
// console.log("用户已登录但没有用户名");
// console.log(store.state.user.userInfo,"userInfo");
// console.log(store.state.user.token,"token");
} catch (error) {
//token失效,需要用户重新登录
//首先清空用户信息
await store.dispatch("user/userLogout")
next('/login')
// console.log("用户已登录但token失效");
}
}
}
}
//未登录放行
else{
//这里的逻辑较多,日后再写
next()
// console.log("用户还未登录");
}
})
这下就可以完美解决bug
注意这部分必须用老师账号登录才有数据
账号:13700000000 密码:111111
账号:我的电话 密码:123
点击购物车的结算按钮后可跳转到结算页面
页面开发,接口api,vuex就不截图了
收件人及其地址信息:
获取商品清单格式:纯粹动态展示,没啥好说的
页面编写,配api,router……
看看接口参数有点多
例子
{
"consignee": "admin",
"consigneeTel": "15011111111",
"deliveryAddress": "北京市昌平区2",
"paymentWay": "ONLINE",
"orderComment": "xxx",
"orderDetailList": [
{
"id": null,
"orderId": null,
"skuId": 6,
"skuName": " Apple iPhone 11 (A2223) 128GB 红色 移动联通电信22",
"imgUrl": "http://182.92.128.115:8080//rBFUDF6V0JmAG9XGAAGL4LZv5fQ163.png",
"orderPrice": 4343,
"skuNum": 2,
"hasStock": null
},
{
"id": null,
"orderId": null,
"skuId": 4,
"skuName": "Apple iPhone 11 (A2223) 128GB 红色",
"imgUrl": "http://182.92.128.115:80800/rBFUDF6VzaeANzIOAAL1X4gVWEE035.png",
"orderPrice": 5999,
"skuNum": 1,
"hasStock": null
}
]
}
{
"code": 200,
"message": "成功",
"data": 71, // orderId 订单号
"ok": true
}
常理接下来就是vuex三连环,但是我们现在不用vuex
我们直接在组件里调用接口
假如一个组件里需要调用很多接口,一个一个调用太麻烦了,我们进行统一调用。
在main.js中
最后挂载在Vue原型上
使用
async submitOrder() {
let { tradeNo } = this.orderInfo;
let data = {
consignee: this.userDefault.consignee,
consigneeTel: this.userDefault.phoneNum,
deliveryAddress: this.userDefault.fullAddress,
paymentWay: "ONLINE", //支付方式
orderComment: this.msg, //买家的留言信息
orderDetailList: this.orderInfo.detailArrayList, //商品清单
};
let result = await this.$API.reqSubmitOrder(tradeNo, data);
// console.log(result);
//成功返回订单号
if (result.code == 200) {
this.orderId = result.data;
//跳转路由,并当上query参数
this.$router.push("/pay?orderId=" + this.orderId);
} else {
alert(result.data);
}
},
result
实际上只有微信支付可以实现
获取订单信息的api
注意尽量别在生命周期函数中使用async-await
使用
从query中获取orderId
result格式
我们使用elementui实现
安装
cnpm install --save element-ui
按需引入
cnpm install babel-plugin-component -D
这部分可以看官网
main.js中
pay组件中点击立即支付
接下来需要使用一个插件qrcode
npm网站上可以看到使用方式
安装
cnpm insatll i qrcode --save
那么这个qrcode就是把特定的字符串(后台传过来的)变成二维码图片
接下来
我们使用setInterval每隔一段时间就向服务器发送请求,直到支付成功为止
最后编辑一下确认和取消按钮
this.$alert(`<img src=${url} />`, "请你微信支付", {
dangerouslyUseHTMLString: true,
//中间布局
center: true,
//是否显示取消按钮
showCancelButton: true,
//取消按钮的文本内容
cancelButtonText: "支付遇见问题",
//确定按钮的文本
confirmButtonText: "已支付成功",
//右上角的叉子没了
showClose: false,
//关闭弹出框的配置值
beforeClose: (type, instance, done) => {
//type:区分取消|确定按钮
//instance:当前组件实例
//done:关闭弹出框的方法
if (type == "cancel") {
alert("请联系管理员");
//清除定时器
clearInterval(this.timer);
this.timer = null;
//关闭弹出框
done();
} else {
//判断是否真的支付了
//开发人员:为了自己方便,这里判断可以先不要了
if (this.code == 200) {
clearInterval(this.timer);
this.timer = null;
done();
this.$router.push("/paysuccess");
}
}
},
});
beforeClose配置项,官网也是有的
该组件是多级路由
router文件
{
path:'/center',
component:Center,
meta:{
show:true
},
children:[
{
path:"myOrder",
component:myOrder
},
{
path:"groupOrder",
component:groupOrder
},
{
path:"/center",
redirect:"/center/myOrder"
// 重定向,访问center时,改为访问/center/myOrder
}
]
},
这个地方也只有老师的账号可以获取数据
api
成功实例
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": 70,
"consignee": "admin",
"consigneeTel": "15011111111",
"totalAmount": 29495,
"orderStatus": "UNPAID",
"userId": 2,
"paymentWay": "ONLINE",
"deliveryAddress": "北京市昌平区2",
"orderComment": "",
"outTradeNo": "ATGUIGU1584247289311481",
"tradeBody": "Apple iPhone 11 (A2223) 128GB手机 双卡双待 A",
"createTime": "2020-03-15 12:41:29",
"expireTime": "2020-03-16 12:41:29",
"processStatus": "UNPAID",
"trackingNo": null,
"parentOrderId": null,
"imgUrl": null,
"orderDetailList": [
{
"id": 81,
"orderId": 70,
"skuId": 2,
"skuName": "Apple iPhone 11 (A2223) 64GB 红色",
"imgUrl": "http://192.168.200.128:8080/xxx.jpg",
"orderPrice": 5499,
"skuNum": 1,
"hasStock": null
},
…
],
"orderStatusName": "未支付",
"wareId": null
},
…
],
"total": 41,
"size": 2,
"current": 1,
"pages": 21
},
"ok": true
}
剩下的就简单了,请求成功(用this.$API发请求)。存储在组件的data中,动态展示数据就行了
这里数据格式比较复杂,一定要注意。
就不展示截图了
涉及到表格合并问题
处理一下分页器
这部分详细可见第六天分页器的内容
主要在全局导航守卫里完成
//未登录
else{
let path=to.path
//未登录不能去结算组件,支付组件,和个人中心组件相关页面
if(path.indexOf("/trade")!=-1||path.indexOf("/pay")!=-1||path.indexOf("/center")!=-1){
alert("请登录")
//我们创造一个query参数配合login使用
next("/login?redirect="+path)
}
next()
}
这样就可以拦截用户在未登录的情况下取不该去的页面,同时自动跳到登录页面。用户登录后又可以自动跳到自己原先想去的页面
我们希望用户要进入结算组件只能通过购物车组件进入。这里可以用独享守卫。但是这样依然有一个bug:用户在购物车组件,但是
通过地址栏输入/trade依然能进入结算组件。
类似的可以完成到达pay组件只能从trade组件,进入paysuccess组件只能从pay组件进入。但他们都有同一个bug
npm网址
vue-lazyload - npm (npmjs.com)
下载
cnpm install --save vue-lazyload
尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili 11分钟开始讲自定义插件
这个了解一下,平常用的不是很多。
使用vee-validate(这个其实不是很好用,费力不讨好,大概看的懂就行。我们以后用elementUi替代它
网址 组件 | Element)
网址:vee-validate - npm (npmjs.com)
安装
cnpm install vee-validate@2 --save
我们安装一个低版本的,版本2
使用
编写app\src\plugins\vee-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: (field) => `${field}必须与密码相同`, // 修改内置规则的 message,让确认密码和密码相同
},
attributes: {
phone: "手机号",
code: "验证码",
password: "密码",
password1: "确认密码",
agree:'协议'
},
});
//自定义校验规则
VeeValidate.Validator.extend("tongyi", {
validate: (value) => {
return value;
},
getMessage: (field) => field + "必须同意",
});
没有必要深究,看的懂就行
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
见官网路由懒加载 | Vue Router (vuejs.org)
我们也把routes文件改写一下
以前的:需要import
import Home from '@/pages/Home'
{
path: '/home',
// 懒加载,其他组件类似
component: Home,
meta: {
show: true
}
},
现在
{
path: '/home',
// 懒加载,其他组件类似
component: ()=>import('@/pages/Home'),
meta: {
show: true
}
},
项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。有了 map 就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。 所以该文件如果项目不需要是可以去除掉
可以在vue.config.js 配置 productionSourceMap:false
可在打包时不生成map文件