准备这些: node √+ webpack + 淘宝镜像√
进入到项目文件夹 输入 cmd
输入 vue create app
去初始化项目
介绍目录project-SPH/app
node_modules文件夹:放置项目的依赖
public文件夹:一般放置的是静态资源(图片),需要注意:放在public文件夹中的静态资源,webpack进行打包的时候,会原封不动打包到dist文件夹中,不会当做一个模块打包到 JS 里面
src文件夹(程序员源代码文件夹):
—assets文件夹:一般放置的是静态资源(一般放置多个组件公用的的静态资源),需要注意:放置在assets文件夹里面的资源,webpack打包的时候,会把静态资源当做一个模块,打包到JS文件里面
—components文件夹:一般放置的是非路由组件(全局组件)
—App.vue:唯一的根组件,Vue当中的组件都是(.vue)
—main.js:程序的入口文件,也是整个程序当中最先执行的文件
babel.config.js:配置文件,与babel相关
package.json文件:记录着项目信息,叫什么…有哪些依赖…项目怎么运行…
package-lock.json:缓存性文件
README.md:说明性文件
eslint校验功能关闭
— 在根目录下,创建一个vue.config.js的文件,加入这行代码lintOnSave:false
src文件夹简写方式,配置别名
vue-router
前端所谓路由:key-value键值对
key:URL(地址栏中的路径)
value:相应的路由组件
注意:该项目是 上中下结构 ---- 跟品优购一样…
非路由组件:Header,Footer(首页,搜索页)
路由组件:Home首页路由组件,Search路由组件,login登录路由组件(无Footer),register路由组件(无Footer)
注意1:创建组件的时候,找准 组件结构 + 组件的样式 + 图片资源
注意2:咱们项目采用的是less样式,浏览器不识别less样式,需要通过less,less-loader【安装 六 版本的】进行处理less,把less样式变为css样式,浏览器才可以识别
npm i less-loader@6
安装vue-router插件 npm i vue-router@3
路由组件具体学习–> Vue | 路由
路由组件有四个:Home,Search,Login,Register(注册)
// 配置路由的地方
import Vue from 'vue';
import VueRouter from 'vue-router';
// 使用插件
Vue.use(VueRouter);
// 引入路由组件
import Home from '../pages/Home'
import Login from '../pages/Login'
import Register from '../pages/Register'
import Search from '../pages/Search'
// 配置路由
export default new VueRouter({
// 配置路由
routes:[
{
path:'/home',
component:Home
},
{
path:'/login',
component:Login
},{
path:'/segister',
component:Register
},{
path:'/search',
component:Search
},
]
})
总结:
路由组件与非路由组件的区别?
$route
,$router
属性$route:一般获取路由信息【路径,query,params等等】
$router:一般进行编程式导航进行路由跳转【push|replace】
重定向:在项目跑起来的时候,访问/,立马让他定向到首页 写在 src/router/index.js
// 重定向:在项目跑起来的时候,访问/,立马让他定向到首页
{
path:'*',
redirect:'/home'
}
路由的跳转?
路由的跳转有两种形式:
声明式路由导航可以做的事情 编程式导航都能做,除此之外,编程式导航还能做一些其他的业务
显示或隐藏组件:v-if | v-show
Footer组件在 Home,Search中是显示的,在登录和注册页面中是隐藏的
路由跳转有几种方式?
声明式导航:router-link(务必要有to属性)
编程式导航:利用的是组件实例的$router.push | replace 方法
路由传参,参数有几种写法?
params参数:属于路径当中的一部分,需要注意:在配置路由的时候,需要占位
query参数:不属于路径当中的一部分,类似于Ajax中的queryString /home?k=v&kv=,不需要占位
**特别注意:**路由携带
params
参数时,若使用to
的对象写法,则不能使用path
配置项,必须使用name
配置!params需要去占位!
// 搜索按钮的回调函数,需要向Search路由进行跳转
goSearch(){
// 路由传递参数
this.$router.push({
// 第一种:字符串形式
// path:'/search' + this.keyWord + "?k="+ this.keyWord.toUpperCase(),
// 第二种:模板字符串
// path:`/search/${this.keyWord}?k=${this.keyWord.toUpperCase()}`
// 第三种:对象写法
name:'sousuo',
// params参数
params:{
keyWord:this.keyWord,
},
// query参数
query:{
k:this.keyWord.toUpperCase()
}
})
}
路由传递参数(对象写法) path是否可以结合params参数一起使用?
不可以。
路由跳转传参的时候,对象的写法可以是name,path的形式,但需要注意的是,path这种写法不能与params参数一起使用
如何指定params参数可传可不传?
在配置路由的时候,给params占位 的后面加上?
,代表可传递也可以不传递
比如:配置路由的时候,已经给params参数占位了,但是路由跳转的时候就不传递参数,路径会出现问题 。
你跳转的本来是 http://localhost:8081/#/search/k=QWE 这个位置,结果你跳转的是 http://localhost:8081/#/k=QWE 这个位置
params参数可传递也可以不传递,但是如果传递是空串,如何解决?
传递的是空串的话,路径有问题(和上面路径问题一样)
使用undefined
解决:params参数可传递不可传递的时候,传递是空串路径 有问题的错误
params:{
keyWord:'' || undefined,
},
路由组件能不能传递props数据?
可以的。
关于路由相关详细笔记---->路由的props配置
配置props写在 src/router/index.js 里
第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
第二种写法:值为布尔值,若布尔值为真,就会把该路由组件收到的params参数,以props的形式传给Detail组件 -------缺点:query参数不能用这个
写法三:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
个人认为这个挺好
问:编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误?
路由跳转有两种形式:声明式导航,编程式导航
声明式导航没有这类问题,因为vue-router底层已经处理好了
为什么编程式导航进行路由跳转的时候,就有这种警告错误?
“vue-router”: “^3.5.4”:最新的vue-router引入promise,promise有两个形参,成功返回的函数和失败返回的函数
通过push方法传递相应的成功,失败的回调函数,可以捕获到当前错误,可以解决
通过底部的代码可以实现解决错误
this.$router.push({
// 第三种:对象写法
name:'sousuo',
// params参数
params:{
keyWord:'' || undefined,
},
// query参数
query:{
k:this.keyWord.toUpperCase()
}
},()=>{},()=>{})
这种写法治标不治本,将来在别的组件当中 push | replace,编程式导航还是有类似错误
重写push 与 replace 方法
// src/router/index.js
// 先把VueRouter原型对象的push,先保存一份
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
// 重写push | replace
// 参数:告诉原来的push方法,你往哪里跳转(传递哪些参数)
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
Home拆分为7个组件
三级联动Home,Search,Detail组件都在使用,可以把它注册为全局组件
好处:只需要注册一次,就可以在项目任意地方使用
我用的是Vscode插件【 Postcode】
刚刚经过测试,接口没有问题
如果服务器返回的数据code是200,代表服务器返回数据成功
整个项目,接口前缀都有/api
最新接口地址:
http://gmall-h5-api.atguigu.cn
后台文档swagger地址:
http://39.98.123.211:8510/swagger-ui.html#/
首页三级联动的请求地址是: /api/product/getBaseCategoryList
请求方式是 GET
向服务器发请求的方式有:XMLHttpRequest,fetch,JQ,axios
这里用axios
问:为什么需要二次封装axios
因为要用到 请求和响应拦截器。
请求拦截器:可以在发请求之前处理一些业务
响应拦截器:当服务器数据返回以后,可以处理一些事情
在终端里安装axiosnpm i axios
在项目当中经常会出现 api 文件夹,一般是放关于【axios】请求的
baseURL:'/api',
:基础路径,发请求的时候,路径当中会出现基础api
timeout:5000,
: 代表请求超时的时间5s,在5s之内没有响应就失败了
axios基础不好,可以参考 git | axios 文档
// src/api/request.js
// 对于axios进行二次封装
import axios from 'axios';
// 1. 利用axios对象的方法create,去创建一个axios实例
// 2. request就是axios,只不过稍微配置一下
const requests = axios.create({
// 配置对象
// 基础路径,发请求的时候,路径当中会出现基础api
baseURL:'/api',
// 代表请求超时的时间5s,在5s之内没有响应就失败了
timeout:5000,
});
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
// config:配置对象,对象立马有一个属性很重要,headers请求头
return config;
})
// 响应拦截器
requests.interceptors.response.use((res)=>{
// 成功的回调函数:服务器相应数据回来以后,相应拦截器可以检测到,可以做一些事情
return res.data;
},(error)=>{
// 服务器响应失败的回调函数
return Promise.reject(new Error('failed'))
})
// 对外暴露
export default requests;
项目很小:完全可以在组件的生命周期函数中发请求
项目大,有很多接口:axios.get(‘xxx’)
// src/api/index.js
// 当前这个模块:api进行统一管理
import requests from "./request";
// 三级联动接口
// /api/product/getBaseCategoryList GET 无参数
export const reqCategoryList = ()=>{ // 要请求数据的时候直接调用reqCategoryList这个函数就可以了
// 发请求:axios发请求返回结果是Promise对象
return requests({url:'/product/getBaseCategoryList',method:'GET',})
}
跨域问题
什么是跨域:协议,域名,端口号不同的请求,称之为跨域
从这里http://localhost:8081/#/home ----前端项目本地服务器
向这里发请求 http://gmall-h5-api.atguigu.cn ---- 后台服务器
跨域的解决方案:JSONP,CROS,数据代理
// Vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 关闭eslint
lintOnSave: false,
// 代理跨域
devServer: {
proxy: {
'/api': {// 匹配所有以 '/api'开头的请求路径
target: 'http://gmall-h5-api.atguigu.cn',// 获取数据的目标地址
changeOrigin: true,
},
}
}
})
具体跨域看这里—>解决开发环境Ajax跨域问题
安装nprogress插件 npm i nprogress
只要项目当中发请求,进度条就开始往前动,服务器数据返回之后,进度条就结束
用在 请求和响应拦截器 src/api/request.js
nprogress.start方法:进度条开始
vuex是什么?
vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据。
切记,并不是全部项目都需要vuex
如果项目很小,完全不需要vuex
如果项目很大,组件很多,数据很多,数据维护很费劲,需要vuex
安装vuex npm i vuex@3
vuex基本使用
看这个 -->Vue | vuex【理解 vuex + vuex开发者工具的使用 + vuex的基本使用和API + 四个map方法的使用+多组件共享数据 +模块化+命名空间 】_不爱吃菜的蔡菜的博客-CSDN博客
vuex实现模块化开发
如果项目过大,组件过多,接口也很多,数据也很多,可以让vuex实现模块化开发
当TypeNav挂载完毕就开始请求数据然后展示数据
这里模块化记得给每个组件开启命名空间!!!
namespace:true
让TypeNav组件挂载,挂载的时候通知vuex发请求,将数据存储于 home仓库中,用dispatch,于是要书写actions
actions执行的时候,要通过api里面的接口函数调用,向服务器发请求,获取服务器的数据
需要把之前的api引入进来,在这里发请求就是要调用这个
reqCategoryList
函数发请求后返回的是一个promise函数,当里面的code是200,代表成功,我们要修改仓库里面的数据
我们要解构commit,提交给mutations
result.data是一个数组形式
mutations里面开始更改数据
传入的参数是state是数据,categoryList这个参数就是服务器返回的result.data
仓库当中应该有个起始值,于是让state中的数据类型为服务器返回的数据类型
这个时候home仓库已经有了相应的数组,但是我们需要在TypeNav组件中拿到数据并进行展示
进入到TypeNav组件当中,用
mapState
去获取组件中的数据
对页面的三级联动进行分析
采用样式完成
.item:hover{
background:skyblue;
}
通过JS完成
给一级分类添加鼠标经过和鼠标离开事件
<div
class="item"
v-for="(c1, index) in categoryList"
:key="c1.categoryId"
:class="{ cur: currentIndex == index }"
>
<h3 @mouseenter="changeIndex(index)" @mouseleave="leaveIndex">
<a href="">{{ c1.categoryName }}a>
h3>
....
div>
先设置一个响应式属性,存储用户鼠标移上哪一个一级分类currentIndex = -1 ;
代表鼠标谁都没有移上去
然后鼠标进入和移出 修改响应式数据currentIndex属性
当data中的currentIndex==该item中的index时添加cur属性
data() {
return {
// 响应式属性,存储用户鼠标移上哪一个一级分类
currentIndex: -1, // 代表鼠标谁都没有移上去
};
},
methods: {
// 鼠标进入修改响应式数据currentIndex属性
changeIndex(index) {
// index 鼠标移上某一个一级分类的元素的索引值
this.currentIndex = index;
},
// 一级分类鼠标移出的事件回调
leaveIndex(){
// 鼠标移出currentIndex=-1
this.currentIndex =-1;
}
},
通过CSS样式display:block|none;
来控制二三级商品分类的显示与隐藏
通过JS控制二三级商品分类的显示与隐藏
.....
正常:事件触发的非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而函数内部有计算,那么很可能出现浏览器卡顿)
节流:在规定的时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
防抖:前面的所有的触发都被取消,最后一次执行在规定的事件之后才会触发,也就是说如果连续的快速触发,只会执行一次 ----------------------当事件被触发后,延迟 n 秒后再执行回调
具体看这个笔记---->防抖和节流
lodash插件:里面封装函数的防抖与节流的业务【闭包+延迟期】
npm i lodash
lodash函数库对外暴露的是 _
函数-----------比如:JQ暴露的是$
函数
lodash.debounce | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)防抖函数
lodash.throttle | Lodash 中文文档 | Lodash 中文网 (lodashjs.com)节流函数
鼠标来回滑动的时候,把频繁触发变成少量触发,进行节流
// 这种引入的方式,是把lodash全部功能函数引入
import _ from 'lodash';
// 最好的引入方式:按需加载
import {throttle} from 'lodash/throttle'
...
// 鼠标进入修改响应式数据currentIndex属性
changeIndex:throttle(function(index){
// index 鼠标移上某一个一级分类的元素的索引值
this.currentIndex = index;
},50),
这里的throttle回调函数别用箭头函数,可能出现上下文this问题
当你点击分类的时候,会从home模块跳转到search模块,并且把分类的名字categoryName和ID传递给search模块
然后search模块拿到这些参数向服务器发请求展示相应数据
路由跳转:
声明式导航 :router-llink
编程式导航 :push | replace
最好的解决方案:编程式导航+事件委派
利用事件委派存在一些问题:
1. 你怎么知道点击的是a标签 1. 事件委派,是把全部的子节点【h3,dt,dl,em】的事件委派给父亲节点 2. 点击a标签的时候,才会进行路由跳转
- 如何获取参数【1,2,3级分类的产品的名字和ID】
给a标签加上自定义属性:data-categoryName="c3.categoryName"
,如果有这个自定义属性就是a标签
利用event.target
获取事件对象的子节点
goSearch(event){
// 最好的解决方案:编程式导航+事件委派
// 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
console.log(event.target);
}
注意:这里的自定义属性时全小写的,我加上的是驼峰写法!
节点有一个属性dataset
属性,它可以获取节点的自定义属性与属性值
goSearch(event){
// 最好的解决方案:编程式导航+事件委派
// 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
let element = event.target;
console.log(element.dataset);
}
goSearch(event){
// 最好的解决方案:编程式导航+事件委派
// 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
let element = event.target;
console.log(element.dataset); // 返回的是一个对象
// 解构对象
let {categoryname} = element.dataset;
// 如果标签身上有categoryname属性,一定是a标签
if(categoryname){
alert(123)
}
}
上面已经知道了点击的是否是a标签
下面解决怎么知道是一级分类还是二级分类的a标签
和上面一样,给a标签添加自定义属性:data-category1Id="c1.categoryId"
另外路由要进行跳转和传参,我们这里选择编程式导航携带query参数
element.dataset
对象来获得a标签的分类ID,根据if语句来判断他在哪一级分类,如果是第一级分类,就在query参数里面添加一级分类的ID… goSearch(event) {
// 最好的解决方案:编程式导航+事件委派
// 利用事件委派存在一些问题:1. 你怎么知道点击的是a标签 2. 如何获取参数【1,2,3级分类的产品的名字和ID】
let element = event.target;
console.log(element.dataset); // 返回的是一个对象
// 解构对象
let { categoryname, category1id, category2id, category3id } =
element.dataset;
// 如果标签身上有categoryname属性,一定是a标签
if (categoryname) {
// 整理路由跳转的参数
let location = { name: "sousuo"};
let query = {categoryName:categoryname};
// 怎么知道一级分类还是二级分类的a标签
if (category1id) {
query.category1Id = category1id;
} else if (category2id) {
query.category2Id = category2id;
} else if (category3id) {
query.category3Id = category3id;
}
// 整理完参数,现在location和query是两个参数
location.query = query;
// 实现路由的跳转
this.$router.push(location);
}
},
当从Home进入到Search的时候,TypeNav会再一次挂载,所以当进入的时候,让自定义属性show变成false
判断当前路由是不是search
mounted() {
// 通知Vuex发请求,获取数据,存储于仓库当中,当前三级联动在home组件当中!!!
this.$store.dispatch("home/categoryList");
// 当组件挂载完毕,让show的属性变为false
// 如果不是home路由组件,将TypeNav进行隐藏
if(this.$route.path !== '/home'){
this.show = false;
}
},
这里用到事件委派
当鼠标移入的时候,让商品分类列表进行展示,让 this.show = true;
鼠标移出的时候,隐藏,让this.show = false;
过渡动画:前提是 组件 | 元素 必须要有 v-if
| v-show
指令才可以进行过渡动画
加上过渡动画,需要用transition包裹,取名字为sort
在CSS样式里书写过渡动画 -------注意:动画样式和.sort
是同一级别的
// 过渡动画的样式
// 过渡动画开始状态(进入)
.sort-enter,.sort-leave-to{
height: 0;
}
.sort-enter-to,.sort-leave{
height: 461px;
}
// 定义动画事件,速率
.sort-enter-active,.sort-leave-active{
transition: all 0.3s linear;
}
现在咱们的商品分类三级列表可以优化
当路由跳转的时候—home和search来回切换的时候 会重新发起TypeNav中
getBaseCategoryList
的数据请求 我们这里优化:只发起一次请求可以更好的优化性能
问:哪个地方只执行一次?
最先执行的是入口文件 main.js,引入了App组件,App是唯一的根组件,它的mounted
只会执行一次,且App组件早就先执行的,所以把TypeNav的 派发action 放到App根组件里
不能放到入口文件上,只能放在组件上,因为只有组件上面才有$store
当你先点击三级列表然后再搜索的时候,传递的参数可以又有params参数,也有query参数
注意!接口返回的数据只有商品菜单分类数据(三级联动),对于ListContainer组件与Floor组件数据服务器没有提供的
mock模拟数据:如果你想mock数据,需要用到一个插件mockjs
前端mock数据不会和你的服务器进行任何通信
mockjs文档
安装mockjsnpm i mockjs
使用步骤:
在项目当中src文件夹中创建mock文件夹--------提供假数据的
准备JSON数据(mock文件夹中创建相应的JSON文件夹)-----记得格式化一下
把mock数据需要的图片放置到public文件夹中【public文件夹在打包的时候,会把相应的资源原封不动打包到dist文件夹中】
在mock文件夹下创建mockServe.js
开始mock 虚拟的数据了,通过mockjs模块实现
// 引入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}); // 模拟楼梯floor
mockServe.js
文件在入口文件中引入(至少需要执行一次,才能模拟数据)
// 引入MockServe.js-----mock数据
import 'src/mock/mockServe';
获取到Banner轮播图的数据
当ListCointainer挂载的时候开始 发请求
组件派发action:通过vuex发起ajax请求,将数据存储在仓库中
// src/pages/Home/ListContainer
this.$store.dispatch('home/getBannerList'); // 调用仓库中的这个函数
// src/store/home
const actions = {
// 获取首页Banner轮播图的数据
async getBannerList({commit}) {
let result = await reqGetBannerList();
console.log(result);
if (result.code === 200) {
commit('GETBANNERLIST', result.data)
}
}
}
const mutations = {
GETBANNERLIST(state,bannerList){
state.bannerList = bannerList;
}
};
现在仓库已经有数据了,但是listContainer没有数据----引入仓库中的数据
// src/pages/Home/ListContainer
import {mapState} from 'vuex';
export default {
name: "ListContainer",
mounted(){
// 派发action:通过vuex发起ajax请求,将数据存储在仓库中
this.$store.dispatch('home/getBannerList'); // 调用仓库中的这个函数
},
computed:{
...mapState('home',['bannerList'])
}
}
swiper基本使用 —> Swiper中文网-轮播图幻灯片js插件,H5页面前端开发
安装swiper插件 npm i swiper@5
引入相应的包(js/css)
样式在入口文件引入,因为下面也有轮播图样式和这个一样
引入你需要的页面中的结构
carousel 轮播图的意思
v-for 在遍历服务器返回的数据,要等服务器返回完数据后才渲染遍历
给轮播图添加动态效果
在new Swiper实例之前,页面中结构必须要有!
当把new Swiper实例放在mounted这里发现不行
– 因为dispatch当中涉及到异步语句,导致v-for遍历的时候结构还没有完全
弄成定时器(不完美)
// src/pages/Home/ListContainer
mounted() {
// 派发action:通过vuex发起ajax请求,将数据存储在仓库中
this.$store.dispatch("home/getBannerList"); // 调用仓库中的这个函数
setTimeout(() => {
var mySwiper = new Swiper(".swiper-container", {
loop:true,
cssMode: true,
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
clickable:true, // 点击小球的时候也切换
},
mousewheel: true,
keyboard: true,
});
}, 800);
},
完美写法
watch
:数据监听,监听已有数据的变化
v-for
已经执行完成了$nextTick
:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
当你执行这个回调的时候,保证服务器的数据回来了,v-for执行完毕了【轮播图的结构已经完成了】
$nextTick:可以保证页面中的结构一定是有的,经常和很多插件一起使用【都需要DOM存在】
// src/pages/Home/ListContainer
watch: {
// 监听bannerList数据的变化,因为这条数据 ---由空数组变为数组里面有四个元素
bannerList: {
handler() {
// 通过watch监听bannerList属性的属性值的变化
// 如果执行handler方法,代表组件实例身上这个属性的属性值数组已经有了四个元素
// nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
this.$nextTick(function () {
var swiper = new Swiper(".swiper-container", {
loop:true,
cssMode: true,
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
},
mousewheel: true,
keyboard: true,
});
});
},
},
},
将动态数据渲染到页面上的步骤:
①静态组件完成以后写 api 发请求,
②数据存储到仓库,
③组件拿到仓库里的数据,
④然后数据渲染到页面上
静态组件完成以后写 api 发请求
// src/api/index.js
// 获取floor组件的数据
export const reqGetFloorList = ()=>{
// 发请求
return mockRequests.get('/floor');// 获取mock里面floor的数据
}
数据存储到仓库 src/store/home/index.js
actions
// 获取floor的数据
async getFloorList({ commit }) {
let result = await reqGetFloorList();
console.log(result);
if (result.code === 200) {
commit('GETFLOORLIST', result.data)
}
}
state
const state = {
// state中数据默认初始值别瞎写类型【根据接口返回值初始化】
categoryList: [], // 服务器返回的数据是数组
// 轮播图的数据
bannerList: [],
// 请求floor数据的格式是数组
floorList:[],
};
mutations
const mutations = {
...
// 开始更换数据
GETFLOORLIST(state,floorList){
state.floorList = floorList;
}
};
dispatch
问:getFloorList在哪里触发?
因为主页结构里面有两个floor,服务器发送的数据是一个数组里面有两个对象[{ },{ }]
,如果你请求数据在floor组件里面请求,那么你请求到的数据怎么给另一个floor,你没办法遍历出两个floor组件,所以发请求应该在Home路由组件里面触发
// src/pages/Home/index.vue
mounted(){
// 派发action,获取floor组件的数据
this.$store.dispatch("home/getFloorList")
}
让Home组件拿到数据
// src/pages/Home/index.vue
import {mapState} from 'vuex';
export default {
name:'Home',
// 注册组件
components:{
ListContainer,
Recommend,
Rank,
Like,
Floor,
Brand
},
mounted(){
// 派发action,获取floor组件的数据
this.$store.dispatch("home/getFloorList")
},
computed:{
...mapState('home',['floorList']) // 获得state大仓库下的home里面的floorList
}
}
数据渲染到页面
v-for
在自定义遍历使用
现在数据在home组件上面拿着,但是floor要有数据,第一个floor用数组里的第一个对象,第二个floor用数组里的第二个对象---->父子组件通信
<Floor v-for="(floor,index) in floorList" :key = floor.id :list="floor"></Floor>
export default {
name: "Floor",
props:['list'],
};
动态展示数据
开始替换数据
开始轮播图
<div class="swiper-container" id="floor1Swiper">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="carousel in list.carouselList"
:key="carousel.id"
>
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
...
export default {
name: "Floor",
props: ["list"],
mounted() {
var swiper = new Swiper(".swiper-container", {
cssMode: true,
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
},
mousewheel: true,
keyboard: true,
});
},
};
问:为什么这里可以挂载的时候可以new Swiper实例?
第一次书写轮播图的时候,在当前组件的内部发请求以及动态的渲染结构【前提是只要服务器的数据要回来渲染完毕】,因此不行
这里的时候,发请求是在父组件Home里面挂载完毕发的,父组件把数据给子组件,所以子组件早就把数据遍历完了,可以在mounted挂载的时候可以new Swiper实例
props:用于父给子组件通信的
自定义事件:$on
$emit
可以实现子给父通信
全局事件总线:$bus
全能
消息订阅与发布 pubsub-js:vue中几乎不用 全能
插槽slot :适用于 父组件 ===> 子组件
vuex:全能
以后在开发项目的时候,如果看到某一个组件在很多地方都使用,你把它变成全局组件,注册一次,可以在任意地方使用,公用的组件 | 非路由组件放到components文件夹中
结构,样式,行为要几乎一样才能封装成全局组件,大家一起公用
现在 ListContainer 和 Floor 的JS 代码有点不一样
为什么watch监听不到list:是因为这个数据没有发生变化(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)
为了可以监听到list:
immediate: true, // 立即监听,不管你数据有没有变化,立即上来监听一次
步骤:
先把JS结构改成相似的
去components文件夹下创建Carousel轮播图的文件夹,把轮播图的结构和JS代码放进去(因为之前已经把轮播图样式在入口文件main.js中引入了,所以这里不需要再放置轮播图样式了)
<template>
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="carousel in list"
:key="carousel.id"
>
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
// 引入swiper
import Swiper from "swiper"
export default {
name: "Carousel",
props:['list'],
watch: {
list: {
// 监听list
immediate: true, // 立即监听,不管你数据有没有变化,立即上来监听一次
handler() {
this.$nextTick(function(){
var swiper = new Swiper(this.$refs.cur, {
cssMode: true,
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
},
mousewheel: true,
keyboard: true,
});
})
},
},
},
};
</script>
其中 v-for遍历的是 父组件 通信给子组件的数据,统称为list
然后 当 ListContainer 和 Floor组件需要用到轮播图的时候,在结构中引入
ListContainer 中传入的数据是bannerList
,Floor中传入的数据是 list.carouselList
开发项目的步骤:
// src/api/index.js
// 获取搜索模块的数据 /api/list POST 需要带参数
export const reqGetSearchInfo = (params)=>{ // params是服务器传递的参数,至少是一个空对象
// 发请求
return requests({url:'/list',method:'POST',data:params}) // data 是携带的参数
}
当前这个【获取搜索模块的数据】 接口,会给服务器传递一个默认参数,必须得有个参数,至少是一个空对象
actions
const actions = {
// 获取search模块的数据
async getSearchList({commit }, params) {
// context是一个迷你版的store,上面有vuex常用的一些方法,{commit}解构commit,这样下面就可以直接调用commit函数
// dispatch的时候传递的第二个参数就是params
let result = await reqGetSearchInfo(params);
if (result.code === 200) {
commit('GETSEARCHLIST', result.data)
}
}
};
mutations
const mutations = { // 进行更换数据
GETSEARCHLIST(state, searchList) {
state.searchList = searchList;
}
};
state:
const state = {
searchList:{}, // 类型为对象,可以模拟一下请求数据
};
dispatch
mounted(){
this.$store.dispatch('getSearchList',{}); // {}传的参数
}
如果用...mapState
拿数据过于繁琐了【套娃】
所以我们使用getters
// 计算属性,在项目当中,为了简化数据而生
// 可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候方便了】
const getters = {
goodsList(state){ // state是当前仓库中的state,并非大仓库中的state
// 如果searchList是空对象,则return的是undefined
return state.searchList.goodsList || []; // 假如没有网络,应该返回的是数组形式,数组可以遍历
},
trademarkList(state){
return state.searchList.trademarkList;
},
attrsList(state){
return state.searchList.attrsList;
}
};
注意:我开启了命名空间,所以从组件中读取getters数据是这样写的
...mapGetters('search',['goodsList']),
问题:当用户 搜索的时候 需要再发一次请求来获取相应的数据,而我们派发action请求数据的时候是放在mounted挂载函数里面的,请求只在挂载的时候发请求,不能多次发请求,所以我们不能放在这里
解决方法:我们把请求search模块的数据 封装成一个函数,当你需要调用的时候调用即可
当我们点击三级联动或者搜索的时候,搜索页面会根据点击|搜索不同的数据进行展示
所以 search模块要根据不同的参数获取数据展示
我们要向服务器发请求的时候携带 参数【根据api接口文档 发现最多携带10个参数】
我们先把 searchParams 【带给服务器的参数】有哪些写到data中,到时候发请求携带的就是data中的 searchParams
在组件挂载完毕之前 再发一次请求,将咱们带的参数传到服务器获取到 有关参数的数据
这里就是要将我们获取到的参数覆盖掉初始带给服务器的参数
Object.assign
:对象的合并,第二个参数覆盖掉第一个参数,返回值是覆盖后的对象
beforeMount(){
// console.log(this.$route.query); // 可以拿到传递过来的query参数
// console.log(this.$route.params); // 可以拿到传递过来的params参数
//在发请求之前,把接口需要传递的参数,进行整理
Object.assign(this.searchParams,this.$route.query,this.$route.params)
},
注意:这里还是只发了一次请求,因为只在mounted里面调用了getData
-----所以,当有参数的时候地址栏会发生变化,我们可以监听
$route
里面属性的属性值的变化,再一次整理参数-发送请求
watch: {
// 监听路由的信息是否发生变化,如果发生变化再次发请求
deep:true,
$route() {
// 整理数据
Object.assign(this.searchParams, this.$route.query, this.$route.params);
// 发请求 ---发请求之前再整理一下数据
this.getData(); // 调用getData函数,再次发请求
// 这里assing只是替换数据,但是如果你第一次点一级分类,第二次点二级分类,id并没有干扰,所以没有替换,这里请求就有错误
// 每一次请求完毕,应该把相应的1,2,3级分类的id置空,让他接收下一次的相应1,2,3id
this.searchParams.category1Id = '';
this.searchParams.category2Id = '';
this.searchParams.category3Id = '';
},
},
从仓库中捞到数据
<script>
import {mapGetters} from 'vuex'
export default {
name: 'SearchSelector',
computed:{
...mapGetters('search',['trademarkList','attrsList']), // // 去search仓库里面 拿到这两个数据
}
}
</script>
变为动态数据
这个面包屑 可能有 也可能没有,如果你携带的参数searchParams.categoryName
为空,就是没有面包屑,有的话,面包屑就是这个参数的值
所以用v-if
来展示是否有面包屑
<ul class="fl sui-tag">
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i>×</i></li>
</ul>
当你搜索的时候或者点击的时候 ----携带的query参数和params参数都要在面包屑上显示,当你点X号的时候,面包屑和它所对应的参数也应该删掉,然后再次发请求渲染页面
也就是说,把searchParams.categoryName
和那些分级的id置空,然后再次发个请求
带给服务器参数 是可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
但是你把相应的字段变为undefined,当前这个字段不会带给服务器-------性能更好
当你点X号的时候,地址栏也要改:进行路由跳转----自己跳自己
removeCategoryName(){
// 把searchParams.categoryName和哪些分级的id也置空
// 带给服务器参数 是可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
// 但是你把相应的字段变为undefined,当前这个字段不会带给服务器-------性能更好
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
this.getData();
// 自己跳转自己的路由
// 严谨:本意是删除query参数,如果路径当中出现了params参数,路由跳转的时候应该带着
if(this.$route.params){
this.$router.push({
name:'sousuo',
params:this.$route.params,
})
}
},
需求:
当你搜索关键字的时候,面包屑会出现该关键字,路由的参数也会携带
当你点面包屑X号的时候,面包屑会删除该关键字,搜索框为空,路由携带的参数也为空
当面包屑中的关键字清除以后,需要让兄弟组件Header中的keyword关键字清除,这样搜索框的关键字也清除了 -------组件通信----------全局事件总线
// pages/Search/index.vue 需求1
<ul class="fl sui-tag">
<!-- 分类的面包屑---query参数 -->
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">×</i></li>
<!-- 关键字的面包屑---params参数 -->
<li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">×</i></li>
</ul>
// pages/Search/index.vue 需求2
// 删除关键字面包屑
removeKeyword(){
// 给服务器带的参数searchParams的keyword给置空
this.searchParams.keyword = undefined;
// 再次发请求
this.getData();
// 通知Header组件的搜索框为空----兄弟组件通信----全局事件总线
this.$bus.$emit('clear')
// 自己跳转自己的路由
// 严谨:本意是删除params参数,如果路径当中出现了query参数,路由跳转的时候应该带着
if(this.$route.query){
this.$router.push({
name:'sousuo',
query:this.$route.query,
})
}
},
// Header/index.vue
mounted(){
// 通过全局事件总线清除搜索框里的关键字
this.$bus.$on('clear',()=>{
// 把keyword置空
this.keyword='';
})
}
当你点击VIVO的时候,面包屑会出现该词汇,然后页面展示该相关数据
点击以后,子组件需要整理参数,向服务器发请求 获取相应的数据在面包屑展示
子组件把要传递的信息(点击的品牌的信息)给父组件,然后父组件把searchParams参数带给服务器参数
tradeMarkHandler(trademark)
回调函数,并把品牌的信息当做参数传过去 <ul class="logo-list">
<li v-for="trademark in trademarkList" :key="trademark.tmId" @click="tradeMarkHandler(trademark)">{{trademark.tmName}}</li>
</ul>
methods:{
// 点击logo的时候触发----品牌logo的事件处理函数
tradeMarkHandler(trademark){ // 接收trademark品牌的信息
// 点击以后,子组件需要整理参数,向服务器发请求获取相应的数据在面包屑展示
// 子组件把要传递的信息(点击的品牌的信息)给父组件,然后父组件发请求
// console.log(trademark); // 是个对象 {tmId: 4,tmName: "尚硅谷"}
// 用自定义事件来传参
this.$emit('trademarkInfo',trademark); // 触发自定义事件trademarkInfo,把trademark的信息传给父组件
}
}
trademarkInfo
<!--selector-->
<SearchSelector @trademarkInfo="trademarkInfo"></SearchSelector>
tmId:xxx,tmName:xx
,然后赋值给 searchParams.trademarksearchParams
传给服务器 // 自定义事件的回调 --- 在子组件触发
trademarkInfo(trademark){ // trademark从子组件接收到的信息
// 1.父组件整理品牌字段参数 "ID:品牌名称"
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
// 2.再次发请求并展示相关数据
this.getData();
},
面包屑品牌信息进行展示
如果 trademark 有内容,就进行展示面包屑,当点击X号的时候,就执行删除操作
<ul class="fl sui-tag">
<!-- 分类的面包屑---query参数 -->
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">×</i></li>
<!-- 关键字的面包屑---params参数 -->
<li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">×</i></li>
<!-- 品牌logo信息展示 -->
<li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(':')[1]}}<i @click="removeTrademark">×</i></li>
</ul>
让trademark为undefined,然后再次发请求
// 删除品牌logo面包屑
removeTrademark(){
this.searchParams.trademark = undefined;
// 再次发请求
this.getData();
},
点击以后,子组件把要传递的信息给父组件,然后父组件把searchParams参数带给服务器参数
// 子组件
<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 @click="attrInfo(attr,attrValue)">
<a>{{attrValue}}</a>
</li>
</ul>
</div>
点击----------------------------触发点击事件的回调attrInfo
通过自定义事件把 点击的属性相关信息给父组件
// 子组件
// 平台售卖属性值的点击事件
attrInfo(attr,attrValue){
// console.log(attr); attr里面有id和name
// console.log(attrValue); 这个直接就是你点击的属性值
this.$emit('attrInfo',attr,attrValue); // 自定义事件名字叫 attrInfo
},
父组件绑定自定义事件
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo">SearchSelector>
父组件通过自定义事件收集到传过来的属性信息,然后进行整理数据,再次发请求重新渲染页面
// 自定义事件的回调---收集平台属性地方 回调函数
attrInfo(attr,attrValue){ // 数组里面存储
// 整理参数["属性ID:属性值:属性名"] 数组里面放字符串
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
// 数组去重---判断数组里面是否有重复元素,如果有就不push追加了
if(this.searchParams.props.indexOf(props)==-1){
// 把元素放到数组里面
this.searchParams.props.push(props);
}
// 再次发请求
this.getData();
},
将 点击的属性值 在面包屑展示,里面的属性值可以多次点击展示不同的属性,所以不能用v-if
,props是一个数组,需要遍历把里面的 属性名 进行展示
props.attrValue:[“106:安卓手机:手机一级”],只需要展示属性名,也就是用:
来分割数组,展示索引号为1的值
<ul class="fl sui-tag">
...
<li class="with-x" v-for="(attrValue,index) in searchParams.props" :key=index>{{attrValue.split(':')[1]}}<i @click="removeAttr(index)">×i>li>
ul>
当点击面包屑X号的时候,删除掉 props数组里的 该属性的信息,用到删除数组splice
// 删除售卖属性的面包屑
removeAttr(index){
// array.splice(从哪开始删除, 删除几个)
this.searchParams.props.splice(index,1);
this.getData();
},
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
order | string | N | 排序方式 1: 综合,2: 价格 asc: 升序,desc: 降序 示例: “1:desc” |
问题:
order属性的属性值最多有多少种写法?
1:asc
2:asc
1:desc (初始值)
2:desc
谁应该有类名 active
默认是 综合,当点了哪个,那个就有active
类名
通过order属性值当中是包含1 (综合) 还是 2(价格)
<ul class="sui-nav">
<li :class="{active:searchParams.order.indexOf('1')!=-1}">
<a>综合a>
li>
<li :class="{active:searchParams.order.indexOf('2')!=-1}">
<a>价格a>
li>
ul>
:class="{active:searchParams.order.indexOf('1')!=-1}"
动态添加类名 active :查看
searchParams.order
里面是否有1
,有的话就是综合,没有的话就是价格,indexOf
如果不包含返回-1,我们要包含就!=-1
将动态添加类名用计算属性:
<ul class="sui-nav">
<li :class="{active:isOne}">
<a>综合a>
li>
<li :class="{active:isTwo}">
<a>价格a>
li>
ul>
computed: {
...mapGetters("search", ["goodsList"]),
// 动态添加类名
isOne(){
return this.searchParams.order.indexOf('1')!=-1
},
isTwo(){
return this.searchParams.order.indexOf('2')!=-1
},
},
谁身上应该有箭头↑ ↓
谁有active
类名,谁就有箭头
<ul class="sui-nav">
<li :class="{active:isOne}">
<a>综合<span v-show="isOne" >↑</span></a>
</li>
<li :class="{active:isTwo}">
<a>价格<span v-show="isTwo">↓</span></a>
</li>
</ul>
箭头应该如何展示
用阿里图标库…iconfont-阿里巴巴矢量图标库
把所要的图标添加到里,然后下载到本地,在index.html引入css样式,然后就直接添加类名就可以
箭头的上下取向 取决于asc升序还是desc降序
:class="{'icon-up':isAsc,'icon-down':isDesc}"
给箭头添加动态类名,如果要显示↑箭头,则
isAsc
返回值true
<ul class="sui-nav">
<li :class="{active:isOne}">
<a>综合<span v-show="isOne" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
li>
<li :class="{active:isTwo}">
<a>价格<span v-show="isTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
li>
ul>
用计算属性来 看是升序还是降序:
asc升序还是desc降序
如果searchParams.order
里面包含asc
,则显示升序,添加icon-up
类名
// 是升序还是降序
isAsc(){ // 升序
return this.searchParams.order.indexOf('asc') !=-1;
},
isDesc(){ // 降序
return this.searchParams.order.indexOf('desc') !=-1;
}
给 综合 和 价格 绑定点击事件
默认上来是 1:desc 综合降序
当我们再次点综合的时候,降序变为升序;
如果我们点 价格,就会变成 2:desc
绑定点击事件
<ul class="sui-nav">
<li :class="{active:isOne}" @click="changeOrder('1')">
<a>综合<span v-show="isOne" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
li>
<li :class="{active:isTwo}" @click="changeOrder('2')">
<a>价格<span v-show="isTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}">span>a>
li>
ul>
给 综合 和 价格 绑定点击事件并传参 ,flag他是一个标记,代表用户点击的时候综合(1)还是价格(2)【用户点击的时候传进来的】
orginFlag 是初始值,flag是我们点击哪个传过来的参数,如果相同,说明我们一直在点【综合】或者【价格】,这时候要切换升序和降序
orginFlag如果和flag不相同,说明初始值是【综合】我们却点了【价格】,就让传过来的flag为我们现在的searchParams.order里面的参数,排序方式默认为降序
// 排序操作
changeOrder(flag){
// flag是一个形参,他是一个标记,代表用户点击的时候综合(1)还是价格(2)【用户点击的时候传进来的】
let orginOrder = this.searchParams.order; // 获得起始的状态 1:desc默认综合
let orginFlag = this.searchParams.order.split(":")[0]; // 拿到现在状态是 综合还是 价格
let orginSort = this.searchParams.order.split(":")[1]; // 拿到现在是升序还是降序
// 准备一个新的order属性值
let newOrder = '';
// 这个语句可以确定默认的和你点击的orginFlag是一样的
if(orginFlag == flag) {
// 如果默认值和咱们点击的是一样的,就切换升序和降序
newOrder = `${flag}:${orginSort=='desc'?'asc':'desc'}`
}else {
// 假如默认是综合,我们却点击了价格,让点击传入的参数flag=2 就是价格,然后默认是降序
newOrder = `${flag}:${orginSort='desc'}`
}
this.searchParams.order = newOrder;
// 再次发请求
this.getData();
}
服务器会自动帮我们排好序的…
分页功能很多地方都要用到,我们可以把分页功能 设成一个 全局组件
// src/components/Pagination/index.vue
为什么很多项目采用分页功能?
比如电商平台同时展示的数据很多(1w+),需要采用分页功能
问:分页展示,需要哪些数据(条件)?
需要知道当前是第几页:pageNo 字段代表当前页数
需要知道每一页需要展示多少条数据:pageSize字段进行代表
需要知道整个分页器一共有多少条数据:total字段进行代表----【获取另外一条数据:一共有几页】
需要知道分页器连续页码个数:continues字段进行代表 5 | 7 【为什么是奇数?因为对称】
自定义分页器,在开发的时候先自己传递假的数据进行调试,调试成功后再用服务器数据
props
// src/pages/Search/index.vue
<Pagination :pageNo="1" :pageSize="3" :total="91" :continues="5">Pagination>
对于分页器而言,很重要的一个地方即为【算出:连续页面起始数字和结束数字】
比如,当前是第8页,【6 7 8 9 10 】6是起始数字,10是结束数字
获得的数据有:
props: ["pageNo", "pageSize", "total", "continues"],
计算出总共有多少页数
Math.ceil()
向上取整
computed: {
// 计算出总共有多少页数
totalPage() {
// 向上取整
return Math.ceil(this.total / this.pageSize);
},
}
计算出连续的页码的起始数字和结束数字【连续页码的数字:至少是5,才能有连续页码】
parseInt()
取整数
前提:分页器数字没有0,也没有负数
假如当前是第1页,应该是1 2 3 4 5
假如当前是第2页,应该是 1 2 3 4 5
所以:start应该是1,end应该是你分页器的连续页码的数字
假如现在总共是31页
当前是31页,应该是 27 28 29 30 31
当前是30页,应该是 27 28 29 30 31
// 计算出连续的页码的起始数字和结束数字【连续页码的数字:至少是5,才能有连续页码】
startNumAndEndNum() {
// 先定义两个变量
let start = 0,
end = 0;
// 连续页码的数字:至少是5,才能有连续页码。所以至少有5页,也有可能是7
// 1. 如果数据不够5页【不正常现象:总页数没有连续页码多】
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数字出现了 0 | 负数】
if (start < 1) {
start = 1;
end = continues;
}
// 3. 把不正常的现象纠正【end数字大于总页码】
if (end > this.totalPage) {
start = this.totalPage - (this.continues - 1);
end = this.totalPage;
}
}
return {start,end};
},
注意!计算属性必须要有return返回值!!!
遍历连续页码的结束数字,如果页数 大于等于 起始数字就进行展示【为了防止出现这种情况】
<button
v-for="(page, index) in startNumAndEndNum.end"
:key="index"
v-if="page >= startNumAndEndNum.start"
>
{{ page }}
button>
上面部分
<button>上一页button>
<button v-if="startNumAndEndNum.start > 1">1button>
<button v-if="startNumAndEndNum.start > 2">···button>
中间部分进行展示的时候会出现 和上面部分相同的情况,所以用
v-if
来判断上面部分的展示
当连续页的起始值 大于 1 的时候,中间部分和上面部分没用重复的,这里要上面部分的 1当连续页的起始值 大于 2 的时候,起始值是3,和1中间有个2,可以要 …
当前页是第3页,应该把 1… 给去掉
当前页是第4页,要1,不要…
所以,当start > 1 就要 1,当start>2就要…
下面部分
<button v-if="startNumAndEndNum.end < totalPage - 1">···button>
<button v-if="startNumAndEndNum.end < totalPage">{{ totalPage }}button>
<button>下一页button>
当连续页的结束值 小于 总页数,中间部分和下面部分没重复,才需要 总页数,否则不显示
当连续页的结束值 小于 总页数-1,他们俩之间还隔着1个数,可以用…
当前页是第31页,应该把 … 31 给去掉
当前页是第30页,应该把 … 31 给去掉
当前页是第29页,应该把 … 31 给去掉
当前页是第28页,要31,不要…
当前页是第27页,要 … 31
所以,当end 小于 总页数 才要 31,当end小于 总页数-1,才要
仓库里面有数据
// 获取 search模块中展示产品一共有多少数据
...mapState('search',['searchList']),
替换好的真实数据:
<Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="searchList.total" :continues="5">Pagination>
当点击第 ? 页,子组件告诉父组件 你点了第 ? 页,父组件拿第 ? 页的数据
子给父通信----------自定义事件
给父组件绑定自定义事件@getPageNo="getPageNo"
获取你点击的是第几页
⚠注意 需要传参的话父组件自定义事件不需要带
()
<Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="searchList.total" :continues="5" @getPageNo="getPageNo">Pagination>
子组件触发自定义事件:
:disabled="pageNo==1"
,当可以点击的时候,传的参数是当前这页-1
<button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页button>
<button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)">1button>
<button v-if="startNumAndEndNum.start > 2">···button>
<button
v-for="(page, index) in startNumAndEndNum.end"
:key="index"
v-if="page >= startNumAndEndNum.start"
@click="$emit('getPageNo',page)"
>
{{ page }}
button>
<button v-if="startNumAndEndNum.end < totalPage - 1">···button>
<button v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo',totalPage)">{{ totalPage }}button>
<button :disabled="pageNo == totalPage" @click="$emit('getPageNo',pageNo+1)">下一页button>
触发自定义函数后 父组件的回调函数
// 自定义事件----获取当前第几页
getPageNo(pageNo){
// 整理带给服务器的参数
this.searchParams.pageNo = pageNo;
// 再次发请求
this.getData();
},
当点击哪个页码的时候,会有类名,背景色变色
动态添加类名-----:class=“{ xxxxxx }”
// 点击第1页的时候
<button
v-if="startNumAndEndNum.start > 1"
@click="$emit('getPageNo', 1)"
:class="{ active: pageNo == 1 }"
>
1
button>
// 点击中间部分连续页码的时候
<button
v-for="(page, index) in startNumAndEndNum.end"
:key="index"
v-if="page >= startNumAndEndNum.start"
@click="$emit('getPageNo', page)"
:class="{ active: pageNo == page }"
>
{{ page }}
button>
// 点击最后一页的时候
<button
v-if="startNumAndEndNum.end < totalPage"
@click="$emit('getPageNo', totalPage)"
:class="{ active: pageNo == totalPage }"
>
{{ totalPage }}
button>
静态组件 ----- Detail 详情页组件
发请求api
vuex
动态展示组件
当点击search下的产品图片的时候,会跳转到该产品详情页Detail,同时携带params参数
// 配置路由的地方
import Vue from 'vue';
import VueRouter from 'vue-router';
// 使用插件
Vue.use(VueRouter);
// 引入路由组件
import Detail from '../pages/Detail'
// 先把VueRouter原型对象的push,先保存一份
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
// 重写push | replace
// 参数:告诉原来的push方法,你往哪里跳转(传递哪些参数)
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
// 配置路由
export default new VueRouter({
// 配置路由
routes: [
.....
{
name: 'xiangqing',
path: '/detail/:skuId', // 需要携带params参数,所以要占位
component: Detail,
meta: { showFooter: true },
},
// 重定向:在项目跑起来的时候,访问/,立马让他定向到首页!!!
{
path: '*',
redirect: '/home',
}
]
})
对search里面的点击图片进行路由跳转
<router-link :to="`/detail/${good.id}`">
<img :src="good.defaultImg"/>
router-link>
当跳转的时候发现滚动条不动,和上一页面的滚动条在相同位置,咱们要让控制滚动条在顶部!
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { y: 0 }
},
})
// 获取产品详情页模块的数据 /api/item/{ skuId } GET 需要带参数
export const reqGoodsInfo = (skuId) =>{
return requests({url:`/item/${skuId}`,method:'GET'})
}
vuex仓库中还需要新增一个模块detail,需要回到大仓库中进行合并
// src/store/detail/index.js
// detail 产品详情页的仓库
import { reqGoodsInfo } from '@/api'
const state = {};
const mutations = {};
const actions = {};
const getters = {};
// 对外暴露
export default {
namespaced: true,
state,
mutations,
actions,
getters,
}
引入到大仓库中
// src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
// 需要使用插件
Vue.use(Vuex);
// 引入小仓库
import home from './home'
import search from './search'
import detail from './detail'
// 对外暴露Store类的一个实例
export default new Vuex.Store({
// 实现vuex仓库模块式开发存储数据
modules:{
home,
search,
detail,
}
})
派发action
什么时候派发action?
当你在搜索页点击某个产品的时候发生跳转,跳转到detail页面,当页面挂载完毕开始派发action
mounted(){
// 派发action获取产品详情的信息,后面要携带参数即为产品的id
this.$store.dispatch('detail/getGoodsInfo',this.$route.params.skuId);
},
vuex三件套 actions-mutations-state
// src/store/detail/index.js
// detail 产品详情页的仓库
import { reqGoodsInfo } from '@/api'
const state = {
goodsInfo: {}, // 看api文档,result.data返回的是一个对象
};
const mutations = {
GETGOODSINFO(state, goodsInfo) { // goodsInfo 是commit传过来的参数
state.goodsInfo = goodsInfo;
}
};
const actions = {
// 获取产品信息的action
async getGoodsInfo({ commit }, skuId) { // 需要带参数
let result = await reqGoodsInfo(skuId)
console.log(result.data);
if (result.code == 200) {
// 提交mutations,修改state
commit('GETGOODSINFO', result.data)
}
}
};
现在detail仓库里的state已经拿到数据了
数据存储到detail仓库里面,拿数据是这样的->state.detail.goodsInfo.xxx,比较麻烦,可以利用计算属性
// src/store/detail/index.js
// 简化数据
const getters = {
// 获取数据的时候直接getters获取然后categoryView来获取数据
categoryView(state){
// 比如:state.goodsInfo初始状态为空对象,空对象的categoryView属性为undefined,读属性undefined的xxx会报错
return state.goodsInfo.categoryView || {};
},
skuInfo(state){
return state.goodsInfo.skuInfo || {};
},
};
在组件中引入mapGetters
渲染数据
skuInfo.skuImageList里面存储着放大镜图片
现在detail仓库里面有数据,detail组件有数据,我们要让zoom组件拿到数据,就是父组件detail 向 子组件zoom传递数据,用props
<div class="previewWrap">
<Zoom :skuImageList="skuImageList">Zoom>
<ImageList />
div>
给子组件的数据
- 如果服务器数据没有回来,skuInfo是个空对象,
skuInfo.skuImageList
就是undefined,子组件出现skuImageList[0]
就会报错,因为undefined没有第0项,所以skuImageList应该至少是一个空数组
// src/pages/detail/index.vue
computed:{
...mapGetters('detail',['categoryView','skuInfo']),
// 给子组件的数据
skuImageList(){
return this.skuInfo.skuImageList || [];
}
},
- 同上,如果服务器数据没有回来,skuImageList应该至少是一个空数组,空数组的第0项还是undefined,
skuImageList[0].imgUrl
就是undefined的imgUrl还是不存在,所以skuImageList[0]至少是个空对象
// src/pages/detail/zoom/index.vue
<template>
<div class="spec-preview">
<img :src="imgObj.imgUrl" />
<div class="event">div>
<div class="big">
<img :src="imgObj.imgUrl" />
div>
<div class="mask">div>
div>
template>
<script>
export default {
name: "Zoom",
props: ["skuImageList"],
computed: {
imgObj() {
// 至少 skuImageList[0] 是一个空对象
return this.skuImageList[0] || {};
},
},
};
script>
从父组件detail拿数据到ImageList组件
<ImageList :skuImageList="skuImageList">ImageList>
接收数据props
props:["skuImageList"],
展示数据
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="slide in skuImageList" :key="slide.id">
<img :src="slide.imgUrl">
div>
div>
同样,我们轮播图采用 监听watch+$next Tick
watch: {
// 监听数据可以保证数据一定有,但是不能保证v-for遍历结构是否完事
skuImageList() {
// 延迟执行回调,当下次DOM更新循环结束后
this.$nextTick(() => {
new Swiper(".swiper-container", {
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
slidesPerView: 3, // 显示几个图片设置
slidesPerGroup:1, // 每一次切换图片的个数
});
});
},
},
当鼠标点击的时候,img添加动态classactive
// src/pages/detail/ImageList
<div class="swiper-slide" v-for="(slide,index) in skuImageList" :key="slide.id">
<img :src="slide.imgUrl" :class="{active:currentIndex==index}" @click="changeCurrentIndex(index)"/>
</div>
....
data(){
return {
// 响应式数据
currentIndex:0,
}
},
methods:{
// 修改响应式数据
changeCurrentIndex(index){
this.currentIndex = index;
}
},
下面的轮播图点击后 上面的轮播图就要展示相应图片-------兄弟间通信
当点击图片的时候,把图片当前的索引值传过去,然后进行兄弟间通信
methods:{
// 修改响应式数据
changeCurrentIndex(index){
this.currentIndex = index;
// 通知兄弟组件当前的索引值
this.$bus.$emit('getIndex',this.currentIndex);
}
},
zoom组件获取到兄弟组件传过来的数据,修改响应式数据
<template>
<div class="spec-preview">
<img :src="imgObj.imgUrl" />
<div class="event">div>
<div class="big">
<img :src="imgObj.imgUrl" />
div>
<div class="mask">div>
div>
template>
<script>
export default {
name: "Zoom",
props: ["skuImageList"],
data(){
return{
// 响应式数据
currentIndex:0,
}
},
computed: {
imgObj() {
return this.skuImageList[this.currentIndex] || {};
},
},
mounted(){
// 全局事件总线获取兄弟组件传过来的索引值
this.$bus.$on('getIndex',(index)=>{
// 修改当前响应式数据
this.currentIndex = index;
})
},
};
script>
pageX: 页面X坐标位置
pageY: 页面Y坐标位置offsetX:鼠标坐标到元素的左侧的距离
offsetY:鼠标坐标到元素的顶部的距离
methods:{
handler(event){ // event由用户触发的事件,这里可以不用传参,默认有的
let mask = this.$refs.mask; // 获得mask的DOM节点
// 要让mask的left = 鼠标的距离 - mask自身宽度的一半
let left = event.offsetX - mask.offsetWidth/2
let top = event.offsetY - mask.offsetHeight/2
// 约束范围
if(left<=0 ) left = 0;
if(left>=mask.offsetWidth) left = mask.offsetWidth;
if(top<=0 ) top = 0;
if(top>=mask.offsetHeight) Top = mask.offsetHeight;
// 修改元素的left 和 top 值 !!!一定要加'px'!!
mask.style.left = left + 'px';
mask.style.top = top + 'px';
}
},
要让上面右面的大图跟着动
遮挡层移动距离 left | top
遮挡层最大移动距离 mask.offsetWidth | mask.offsetHeight
大图片最大移动距离 imgWidth - bigWidth = 800-400=400px // 这里去看CSS样式就知道他们的宽度
求大图片移动距离 = 遮挡层移动距离*大图片最大移动距离/遮挡层最大移动距离
/* ----------要让上面右边的大图跟着动-------------------- */
let bigImg = this.$refs.bigImg;
// 大图片的宽度和高度
let bigImgWidth = bigImg.offsetWidth
let bigImgHeight = bigImg.offsetHeight;
// big盒子的高度和宽度
let big = this.$refs.big;
let bigWidth = big.offsetWidth
let bigHeight = big.offsetHeight
// 求大图片移动距离 = 遮挡层移动距离*大图片最大移动距离(bigImgWidth-bigWidth)/遮挡层最大移动距离
let imgLeft = left*(bigImgWidth-bigWidth)/mask.offsetWidth;
let imgTop = top*(bigImgHeight-bigHeight)/mask.offsetHeight;
// 修改大图片的left和top值!记得大图片是向左移动的,和mask是相反的
bigImg.style.left = -imgLeft + 'px';
bigImg.style.top = -imgTop + 'px';
完整代码:
<template>
<div class="spec-preview">
<!-- 上面左侧的大图 -->
<img :src="imgObj.imgUrl" />
<!-- 写事件的event,要放事件的地方 -->
<div class="event" @mousemove="handler"></div>
<!-- 上面右侧的大图 -->
<!-- big Width 400px bigImgWidth 800px -->
<div class="big" ref="big">
<img :src="imgObj.imgUrl" ref="bigImg" />
</div>
<!-- 遮罩层 -->
<div class="mask" ref="mask"></div>
</div>
</template>
...
methods:{
handler(event){ // event由用户触发的事件,这里可以不用传参,默认有的
let mask = this.$refs.mask; // 获得mask的DOM节点
// 要让mask的left = 鼠标的距离 - mask自身宽度的一半
let left = event.offsetX - mask.offsetWidth/2
let top = event.offsetY - mask.offsetHeight/2
// 约束范围
if(left<=0 ) left = 0;
if(left>=mask.offsetWidth) left = mask.offsetWidth;
if(top<=0 ) top = 0;
if(top>=mask.offsetHeight) top = mask.offsetHeight;
// 修改元素的left 和 top 值 !!!一定要加'px'!!
mask.style.left = left + 'px';
mask.style.top = top + 'px';
/* ----------要让上面右边的大图跟着动-------------------- */
let bigImg = this.$refs.bigImg;
// 大图片的宽度和高度
let bigImgWidth = bigImg.offsetWidth
let bigImgHeight = bigImg.offsetHeight;
// big盒子的高度和宽度
let big = this.$refs.big;
let bigWidth = big.offsetWidth
let bigHeight = big.offsetHeight
// 求大图片移动距离 = 遮挡层移动距离*大图片最大移动距离(bigImgWidth-bigWidth)/遮挡层最大移动距离
let imgLeft = left*(bigImgWidth-bigWidth)/mask.offsetWidth;
let imgTop = top*(bigImgHeight-bigHeight)/mask.offsetHeight;
// 修改大图片的left和top值!记得大图片是向左移动的,和mask是相反的
bigImg.style.left = -imgLeft + 'px';
bigImg.style.top = -imgTop + 'px';
}
},
detail组件从仓库里拿到商品售卖属性信息spuSaleAttrList,
简化数据
// src/store/detail/index.js
// 简化数据
const getters = {
// 产品售卖属性的简化
spuSaleAttrList(){
// 至少是一个数组
return state.goodsInfo.spuSaleAttrList || [];
},
};
子组件拿到数据
import { mapGetters } from "vuex";
....
computed: {
...mapGetters("detail", ["categoryView", "skuInfo","spuSaleAttrList"]),
},
展示数据
<div class="chooseArea">
<div class="choosed">div>
<dl v-for="spuSale in spuSaleAttrList" :key="spuSale.id">
<dt class="title">{{spuSale.saleAttrName}}dt>
<dd changepirce="0" :class="{active:spuSaleAttrValue.isChecked == 1}" v-for="spuSaleAttrValue in spuSale.spuSaleAttrValueList" :key="spuSaleAttrValue.id">{{spuSaleAttrValue.saleAttrValueName}}dd>
dl>
div>
:class
动态加高亮
点击事件--------排他思想
点哪个售卖属性值,谁高亮,其余属性值不亮---------排他思想
给属性值dd 绑定点击事件changeActive
<div class="chooseArea">
<div class="choosed">div>
<dl v-for="spuSale in spuSaleAttrList" :key="spuSale.id">
<dt class="title">{{ spuSale.saleAttrName }}dt>
<dd
changepirce="0"
:class="{ active: spuSaleAttrValue.isChecked == 1 }"
v-for="spuSaleAttrValue in spuSale.spuSaleAttrValueList"
:key="spuSaleAttrValue.id"
@click="changeActive(spuSaleAttrValue,spuSale.spuSaleAttrValueList)"
>
{{ spuSaleAttrValue.saleAttrValueName }}
dd>
dl>
div>
methods:{
// 产品的售卖属性值高亮
changeActive(spuSaleAttrValue,arr){
// spuSaleAttrValue 是你当前点击的那个属性值相关信息
// arr 是你这个属性所有的属性值的信息
// 遍历全部的属性值isChecked为0,这样就没有高亮了
arr.forEach(item => {
item.isChecked = 0;
});
// 你当前点击的属性值添加高亮
spuSaleAttrValue.isChecked=1;
},
},
将input框里的数据保存起来------
v-model
双向绑定skuNum用户点击 + 或者 - 输入框的数字进行变化
skuNum++
注意:点 - 的时候 不能小于1当用户在input框里面输入的时候注意规范性:不能为字符,不能是负数,不能是小数
- 不能为字符---------任何字符 * 1 就是NaN
- 不能是负数---------if判断,如果是NaN或者小于1,让skuNum = 1;
- 不能是小数---------最后让input输入框里的value赋值给skuNum的时候,对value取整数parseInt()
// src/pages/detail/index.vue
<div class="cartWrap">
<div class="controls">
<input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum"/>
<a href="javascript:;" class="plus" @click="skuNum++">+a>
<a href="javascript:;" class="mins" @click="skuNum > 1 ? skuNum-- : (skuNum = 1)" >-a>
div>
<div class="add">
<a href="javascript:">加入购物车a>
div>
div>
// 表单元素修改产品个数 --当表单元素发生变化就触发该回调
// 目的:让用户输入的文本赋值给data里的skuNum
changeSkuNum(event){
// 拿到表单元素文本 event.target.value
// 让表单元素文本为正数
// 1. 用户输进来的文本 * 1 就是NaN
// 2. 用户输入的小于1的
let value = event.target.value * 1;
if(isNaN(value) || value < 1) {
this.skuNum = 1;
}else {
// 3. 如果用户输入小数,进行取整数
this.skuNum = parseInt(value);
}
},
② 尚品汇的前台开发笔记【尚硅谷】【Vue】