步骤:Vue
(1)写静态页面、拆分为静态组件;
(2)发请求(API);
(3)vuex(actions、mutations、state三连操作);
(4)组件获取仓库数据,动态展示;
一、初始化项目
1.vue-cli 脚手架初始化项目、
2.node+ wabpake + 淘宝镜像
3.打开的新建的文件夹,在地址栏输入cmd ,打开小黑窗输入vue create app 初始化项目
二、项目的其他配置
1、项目运行起来的时候,设置浏览器自动打开(package.js)
"scripts": {
"serve": "vue-cli-service serve --open", // 设置浏览器自动打开 --open
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
2 、eslint校验功能关闭
----在根目录下,创建一个vue.config.js
module.exports = {
lintOnSave: false,//关闭校验功能
}
3、src文件夹简写,配置别名为@
jsconfig.json配置别名@提示【@代表src文件夹】
配置后写@会有提示代码
{//jsconfig.json配置别名@提示
//【@代表的是src文件夹,这样将来文件过多,找的时候方便很多】
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
//不能在node_modules、dist文件夹使用
"exclude": ["node_modules", "dist"]
}
4、跨域配置
vue.config.js
module.exports = {
productionSourceMap:false,
// 关闭ESLINT校验工具
lintOnSave: false,
//配置代理跨域
devServer: {
proxy: {
"/api": {
target: "http://39.98.123.211", // 公共接口地址
},
},
},
};
三、目录配置文件
public文件夹
1、 node _ modules 文件夹:
项目依赖文件夹
2、public 文什夹:
一般放置一些静态资源(图片),需要注意,放在 public 文件夹中的静态资源,webpack 进行打包的时候,会原封不动打包到 dist 文件夹中。
3、src 文件夹(程序原代码文件夹):
assets 文件夹:存放公用的静态资源,需要注意,放置在 assets 文件里面静态资源,在 webpack 打包的时候, webpack 会把静态资源当做一个模块,打包 JS 文件里面。(run serve build 打包,assets就没有了,出现一个dist文件放静态文件)
components 文件:放置的是非路出组件(全局组件)
App . Vue :唯一的根组件,Vue 当中的组件(. vue )
main.js: 程序入口文件,最先执行的文件
babel.config.js: 配置文件(babel相关)
package.json: 项目的详细信息记录
package-lock.json: 缓存性文件(各种包的来源)
五、 pages文件夹
创建pages文件夹,并创建路由组件
5.1创建router文件夹,并创建index.js进行路由配置,最终在main.js中引入注册
5.2 总结 路由组件和非路由组件区别:
非路由组件放在components中,路由组件放在pages或views中
非路由组件通过标签使用,路由组件通过路由使用
在main.js注册完路由,所有的路由和非路由组件身上都会拥有$router $route属性
$router:一般进行编程式导航进行路由跳转
$route: 一般获取路由信息(name path params等)
5.3 路由跳转方式
声明式导航:router-link标签 ,可以把router-link相当于一个a标签,它 也可以加class修饰
编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务
六、 组件显示与隐藏
显示或者隐藏组件: v - if I v - show
v-if 是对dom元素的控制,会删掉这个元素。因频繁的操作dom元素消耗性能大。不建议频繁使用。
v-show 是 css样式的控制,也就是display:none的改变。
如: Footer 组件:在 Home 、 Search 显示 Footer 组件 Footer 组件:在登录,注册时候隐藏的
6.1我们可以根据组件身上的$ route 获取当前路由的信息,通过路由路径判断 Footer组件的 显示与隐藏。
6.2配置的路由的时候,可以给路由添加路由元信息【 meta 】,路由需要配置对象,它的 key 不能瞎写、胡写、乱写
方法一:不建议使用,原因:可能有很多组件需要与Footer组件一起搭配使用,这样代码量过多,不易书写。
<template>
<div>
<Header></Header>
<router-view></router-view>
<!-- footer组件在home与search页面显示 :使用路由地址进行显示与隐藏 -->
<Footer v-show='$route.path == "/home" || $route.path == "/search"'></Footer>
</div>
</template>
<script>
// 非路由组件注册
import Header from './components/Header'
import Footer from './components/Footer'
export default {
components: {
Header,
Footer
},
}
</script>
方法二:建议使用。利用路由元信息。
步骤:1.在router文件夹中配置路由元信息,2.在app中显示隐藏
routes:[
{
path:'/Home',
component:Home,
meta: { show : true } //路由元信息
},
{
path:'/Login',
component: resolve => require(['@/pages/Login'], resolve),
meta: { show : false }
},
]
<template>
<div>
<Header></Header>
<router-view></router-view>
<!-- footer组件在home与search页面显示 -->
<Footer v-show='$route.meta.show'></Footer>
</div>
</template>
<script>
// 非路由组件注册
import Header from './components/Header'
import Footer from './components/Footer'
export default {
components: {
Header,$
Footer
},
}
</script>
七、路由传参
路由传参的几种方式?比如:从A路由跳到B 路由
声明式导航:router-link标签(务必要有属性to),可以实现路由的跳转。
编程式导航:利用是组件实例的$router.push|replace方法,可以实现路由的跳转。(可以书写一些自己的业务逻辑)
路由传参?参数有几种写法?
prams参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位。地址栏表现为 /search/v1/v2
{
path:'/Search/:keyword',// :keyword 是占位的
name:'Search',
component: resolve => require(['@/pages/Search'], resolve),
meta: { show : true }
},
query参数:不属于路径中的一部分,类似于ajax中queryString(get请求) ,不需要占位。地址栏表现为 /search?k1=v1&k2=v2
路由传递参数相关面试题
1:路由传递参数(对象写法)path是否可以结合params参数一起使用?
不可以:不能这样书写,程序会崩掉
//如果路由path要求传递params参数,但是没有传递,会发现地址栏URL有问题,详情如下:
//Search路由项的path已经指定要传一个keyword的params参数,如下所示:
//路由:path: "/search/:keyword",
this.$router.push({path:'/Search',params:{keyword:this.keyword},query:{k: this.keyword.toUpperCase()}})
// 以上为错误写法
地址错误:http://localhost:8080/#/?k=DFASDFASD **缺少Search**
正常地址:http://localhost:8080/#/Search/xxx?k=XXX
2:如何指定params参数可传可不传?
可传可不传
//Search路由项的path已经指定要传一个keyword的params参数,如下所示:
//路由:path: "/search/:keyword",
this.$router.push({name:'Search',query:{k: this.keyword.toUpperCase()}})
解决方法:
{
path:'/Search/:keyword?',// 后面加一个?,可理解为正则的0或1个
name:'Search',
component: resolve => require(['@/pages/Search'], resolve),
meta: { show : true }
},
3:params参数可以传递也可以不传递,但是如果传递是空串,如何解决?如果指定name与params配置, 但params中数据是一个"", 无法跳转,路径会出问题
可传可不传,但params传的是空字符串,可以加入|| undefined.
{
path:'/Search/:keyword?',// 后面加一个?,可理解为正则的0或1个
name:'Search',
component: resolve => require(['@/pages/Search'], resolve),
meta: { show : true }
},
解决方法:
this.$router.push({name:'Search',params:{keyword:''||undefined},query:{k: this.keyword.toUpperCase()}})
4: 路由组件能不能传递props数据?
可以
{
path:'/Search/:keyword?',
name:'Search',
component: resolve => require(['@/pages/Search'], resolve),
meta: { show : true },
// props:true //第一种方式,布尔值
// props:{a:1,b:2} //对象
// props:($route)=>({keyword:$route.params.keyword,k:$route.params.k})
},
在组件中接受属性,直接调用
<template>
<div>
{{k}} //现在这样调用,不常用
{{keyword}}
{{$route.params.keyword}} //之前调用,常用
</div>
</template>
<script>
export default {
props:['keyword','k']
}
</script>
第一种:字符串方式
//this.keyword 是参数
this.$router.push('/search/' + this.keyword +"?k="+ this.keyword.toUpperCase())
{
path:'/Search/:keyword', //params传参占位
name:'Search',
component: resolve => require(['@/pages/Search'], resolve),
meta: { show : true }
},
this.$route.params.keyword ----this.keyword
this.$route.query.k ------ this.keyword.toUpperCase()
第二种方式:模板字符串
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()} `)
第三种方式:对象(常用)
this.$router.push({name:“路由名字”,params:{传参},query:{传参})。
注意:以对象方式传参时,如果我们传参中使用了params,只能使用name,不能使用path,如果只是使用query传参,可以使用path 。
this.$router.push({name:'Search',params:{keyword:this.keyword},query:{k: this.keyword.toUpperCase()}})
八、报错:多次执行相同的push问题
编程式导航路由跳转到当前路由(参数不变), 多次执行会抛出NavigationDuplicated的警告错误?
注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。
这种异常,对于程序没有任何影响的。
为什么会出现这种现象:
由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,因此出现上面现象,
第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调
const res = this.$router.push({name:'Search',params:{keyword:this.keyword},query:{k: this.keyword.toUpperCase()}},()={},()=>{})
// res是一个promise
第一种解决方案可以暂时解决当前问题,但是以后再用push|replace还是会出现类似现象,因此我们需要从‘根’治病;
解决方法:地址— https://blog.csdn.net/qq_29252021/article/details/109615753
我的情况使用的第一种:
在router.js中添加下面的命令
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
两种解决方案:
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
// 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)
}
// 第一个参数,告诉原来的push方法,往哪里调
// resolve:成功的回调
// reject:失败的回调
// call与apaly :都可以调用函数一次,都可以篡改上下文一次
VueRouter.prototype.push = function (location,resolve,reject){
if(resolve && reject){
// originalPush() //这是错误的写法,这样改变了this,此时指向window
originalPush.call(this,location,resolve,reject)
}else{
originalPush.call(this,location,()=>{},()=>{})
}
}
九、全局组件:三级联动
全局组件:全局的配置都需要在main.js中配置
// 注册全局三级联动组件
import TypeNav from '@/pages/Home/TypeNav';
// 第一个参数是组件的名字,第二个参数是哪一个组件(下面是两种方式)
// Vue.component(TypeNav.name,TypeNav)
Vue.component('TypeNav',TypeNav)
组件本身
<template>
<!-- 商品分类导航 -->
</template>
<script>
export default {
name: 'TypeNav',
}
</script>
在Home中使用组件
<template>
<div>
<!-- 三级联动全局组件已经注册为全局组件,因此不需要引入-->
<TypeNav/>
</div>
</template>
注意:
全局注册组件是component
局部注册组件是components
十、静态组件
非路由组件在使用的时候分为三大步:定义、注册、使用
HTML
十一、安装 scss
安装了错误的版本报错,要不就是版本过高,要不就是node-sass sass-loader版本对应问题
卸载错误的安装版本,然后安装对应的版本号
如:我的版本号node -v v12.18.2,
安装的命令:npm install sass-loader@7.3.1 node-sass@4.14.1 --save-dev,这样就不会报错
版本对应:
sass-loader 4.1.1,node-sass 4.3.0
sass-loader 7.0.3,node-sass 4.7.2
sass-loader 7.3.1,node-sass 4.7.2
sass-loader 7.3.1,node-sass 4.14.1
npm uninstall node-sass sass-loader
npm install sass-loader@版本号 node-sass@版本号 --save-dev //安装对应的版本
以下是更详细的地址
地址:https://blog.csdn.net/weixin_44748205/article/details/115118322
报错:Invalid CSS after ".brand {": expected "}", was "{"
解决:这里是lang="scss"不是"sass"
十二、Vue使用NProgress
10.1安装
$ npm install --save nprogress 或者
$ yarn add nprogress
10.2使用
router.js
//导入
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
router.beforeEach((to, from, next) => {
NProgress.start()
next()
})
router.afterEach(() => {
NProgress.done()
})
原文链接:https://blog.csdn.net/wn1245343496/article/details/82151273
10.3修改进度条颜色
在APP.vue 中style中添加
#nprogress .bar {
background: red !important; //自定义颜色
}
也可以直接修改nprogress.css文件夹中的background来修改进度条颜色。(一般不建议直接修改源代码)
#nprogress .bar{
background:#29d;
.......
}
十三、二次封装axios
// 二次封装axios
import axios from "axios";
import NProgress from 'nprogress';
import 'nprogress/nprogress.css'
const requests = axios.create({
baseURL:'/api/' ,//配置基础路径,,路径当中会出现api
withCredentials: true, // 允许携带token ,这个是解决跨域产生的相关问题
timeout:2500 //请求延时
})
// 请求拦截器 在项目中发请求(请求没有发出去)可以做一些事情
requests.interceptors.request.use(config=>{
NProgress.start()
// config 配置对象,headers是其中一个属性,
// config.headers.Authorization ={}
return config
})
// 响应拦截器 在项目中后端返回数据
requests.interceptors.response.use(res=>{
NProgress.done()
// 成功的回调函数
return res.data
},error=>{
// 失败后的回调函数
return Promise.reject(new Error ('fail'))
})
// 向外暴露
export default requests;
封装方法
import requests from './request';
export const reqCateGoryList = () => {
return requests({
url: '/product/getBaseCategoryList',
method: 'GET'
})
}
// post :data默认传一个空对象
export const reqGetListInfo = (data)=> requests({
url: '/list',
method:'POST',
data:data
})
十四、vuex
14.1 安装
npm install vuex@3.6.2 --save
或
yarn add vuex@3.6.2
14.2 store中的index.js
//引入Vuex -----相当于咱们最大的仓库
import Vuex from "vuex";
//引入Vue
import Vue from "vue";
//使用插件
Vue.use(Vuex);
// 引入home模块的仓库
import home from "./home";
const store = new Vuex.Store({
state: {
num:1,
},
mutations: {},
//模块:把小仓库进行合并变为大仓库
modules: {
home,
},
})
//需要暴露Vuex.Store类的实例(你需要暴露这个类的实例,如果你不对外暴露,外部是不能使用的)
export default store
14.3 hone仓库
import { reqCateGoryList} from "@/api/index";
const state = {
// 存放数据
categoryList: [],
};
const mutations = {
// mutions唯一可以修改state的地方
GETCATEGORYLIST(state, categoryList) {
state.categoryList = categoryList;
},
}
const actions ={
// action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
async categoryList({ commit }) {
//reqgetCategoryList返回的是一个Promise对象
//需要用await接受成功返回的结果,await必须要结合async一起使用(CP)
let result = await reqCateGoryList();
if (result.code == 200) {
commit("GETCATEGORYLIST", result.data);
}
},
}
//计算属性
const getters = {};
export default {
state,
mutations,
actions,
getters,
};
14.3 使用vuex需要在main.js中引入
main.js:
(1) 引入文件
(2) 注册store
但凡是在main.js中的Vue实例中注册的实体,在所有的组件中都会有(this.$.实体名)属性
import store from './store'
new Vue({
render: h => h(App),
//注册路由,此时组件中都会拥有$router $route属性
router,
//注册store,此时组件中都会拥有$store
store
}).$mount('#app')
14.4 调用仓库以及方法
import { mapState } from 'vuex'
export default {
name: 'TypeNav',
mounted(){
// 挂载完毕,可以向服务器发请求
// 通知vuex发请求,获取数据,存储于仓库中
this.$store.dispatch('categoryList')
},
computed:mapState ({
// 箭头函数可使代码更简练
categoryList: state => state.home.categoryList,
// 传字符串参数 'count' 等同于 `state => state.count`
// countAlias: 'count',
// // 为了能够使用 `this` 获取局部状态,必须使用常规函数
// countPlusLocalState (state) {
// return state.count + this.localCount
// }
})
}
actions使用总结:
第一步:this.$store.dispatch(‘方法名’,传参)
第二步:actions中写方法,在mutations中出发。(记得在state中写初始化数据)
14.5 不引入会报错
14.6解决前端Object(…) is not a function
1、函数命名是否是正确
2、当前界面调用别的引用文件的时候大小写是不是正确
这里显示的是这个对象不存在,通过断点调试,发现是在执行一个函数的时候显示的,
继而查看引用文件,发现是因为在引用一个请求api的时候,暴露的函数和引用的函数大小写不一致,
导致请求该函数,找不到该函数,所以出现这类型错误,建议检查下大小写
14.7 map辅助函数
mapState =>computed:
mapGetters => computed:
mapMutations => methods:
mapActions => methods:
解构引入、…展开
import {mapState,mapMutations,mapGetters ,mapActions } from 'vuex'
<el-button @click="add">点击++</el-button>
<span>{{num}}</span>
computed: {
//可直接使用name,通过{{name}}
...mapGetters(['name'],
// 使用对象展开运算符将此对象混入外部对象中
...mapState({
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
num(state) {
return state.num
},
categoryList(state){
return state.home.categoryList
}
}),
/**
相当于
num: state => state.count
message: state => state.msg
*/
...mapState({ num: 'count', message: 'msg' }),
/**
相当于
reverseMsg: getters.reverseMsg
*/
...mapGetters(['reverseMsg']),
/**
重命名,防止与组件内data数据冲突
相当于
msgBeReversed: getters.reverseMsg
*/
...mapGetters({ msgBeReversed: 'reverseMsg' }),
},
methods: {
//<el-button @click="add">点击++</el-button>对应
...mapMutations([
'add' // 将 `this.add()` 映射为`this.$store.commit('add')`
]),
// 这个是修改名字 //<el-button @click="increment">点击++</el-button>对应
...mapMutations({ //将add 修改为increment
// 如果想将一个属性另取一个名字,使用以下形式。注意这是写在对象中
increment: 'add' // 将 `this.increment()` 映射为`this.$store.commit('add')`
}),
// --------------------------- Actions
// this.increateAsync() 映射到 this.$store.dispatch('increateAsync')
...mapActions(['increateAsync']),
// 💛 💛 💛 💛 💛 💛 💛 💛 💛 💛
// 如果是某一个module下的action,需要带上 namespace
// 格式: mapActions(namespace,[mutationName])
...mapActions('home', ['setHomeListAsync']),
...mapActions('home', { setListHomeAsync: 'setHomeListAsync' }),
}
mutations: {
add(state){
state.num ++
},
},
state: {
num:1,
}
十五、三级联动:为一级分类动态添加背景
第一种解决方案:采用css样式完成
.item:hover{
background: seagreen;
}
第二种解决方案:通过js完成
思路:
1、数一下一级分类的个数
2、定义一个存储鼠标移到哪一个分类的index,注意:初始值默认-1,代表鼠标都没有移上
3、在一级分类的html上定义一个事件,鼠标进入的事件
4、当移入的时候给存储的index进行赋值,
5、定义一个class:当鼠标移入的index == index时,背景变色
.cur{
background: seagreen;
}
<h3 @mouseover='changeIndex(index)' :class="{cur:categoryIndex == index}">
<a href="">{{v.categoryName}}</a>
</h3>
data(){
return{
categoryIndex:-1 // 鼠标移动到哪一个
}
},
changeIndex(index){
this.categoryIndex = index
}
坑:鼠标移除事件,没有做记得做。注意:不能将事件写在H3中,这样移出h3的时候会失去背景颜色。所以需要将元素放在同一个盒子中移出父元素的时候,再将失去背景颜色。
leaveIndex(){
this.categoryIndex = -1
}
<div class="container" @mouseleave="leaveIndex">
<h2 class="all">全部商品分类</h2>
<div class="sort">
<div class="all-sort-list2" v-for="(v1,index) in categoryList" :key="v1.categoryId">
<div class="item">
<h3 @mouseover='changeIndex(index)' :class="{cur:categoryIndex == index}">
<a href="">{{v1.categoryName}}</a>
</h3>
<div class="item-list clearfix">
<div class="subitem" v-for="(v2,index) in v1.categoryChild" :key="v2.categoryId">
<dl class="fore">
<dt>
<a href="">{{v2.categoryName}}</a>
</dt>
<dd v-for="(v3,index) in v2.categoryChild" :key="v3.categoryId">
<em>
<a href="">{{v3.categoryName}}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
十六、mouseout 与 mouseleave 区别
mouseout: 鼠标离开事件
mouseleave: 鼠标离开事件
区别:
不论鼠标指针离开被选元素还是任何子元素,都会触发 mouseout 事件。
只有在鼠标指针离开被选元素时,才会触发 mouseleave 事件。
mouseover: 鼠标移入事件
mouseenter: 鼠标移入事件
不论鼠标指针穿过被选元素或其子元素,都会触发 mouseover 事件。对应mouseout
只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件。对应mouseleave
坑: mouseout: 会造成反复触发事件, 用mouseleave代替即可
原文链接:https://blog.csdn.net/qq940853667/article/details/79357635
十七、防抖与节流
[防抖和节流详情]https://www.jianshu.com/p/c8b86b09daf0
正常:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿)
防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次
节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
防抖与节流的原理:通过JS实现【闭包 + 延迟器】
优化项目: v-if|v-show 按需加载 函数防抖与节流 按需加载:对于loadsh插件,它里面封装的函数功能很多 import _ from lodash 相当于把全部功能引入进来,但是我们只是需要节流。
十八、防抖与节流lodash插件使用
上面并没有通过 npm 下载 loadsh插件,因为vue脚手架自带,直接引入即可。
18.1安装(可以不安装vue自带lodash)
npm i --save lodash
18.2 引入(将lodash全部引入)
import _ from 'lodash'
18.3使用
mounted(){
this.find()
},
methods: {
find(){
let input = document.getElementById('int')
input.oninput = _.debounce(function(){
console.log(11);
},1000);
}
}
18.4报错代码:Cannot set properties of null (setting ‘oninput’)
created(){
this.find()
},// 这里是错误代码。
methods: {
find(){
let input = document.getElementById('int')
input.oninput = _.debounce(function(){
console.log(11);
},1000);
}
}
Cannot set properties of null (setting 'oninput') // 报错结果;意思是文档未加载完成,找不到次方法需要,在mounted中加载。
18.5项目中使用(不安装lodash,vue自带)
1.引入(按需引入)
import {throttle} from 'lodash'
2.使用
methods:{
// es5的函数写法 k:v的形式
changeIndex: throttle(function (index){
this.currentIndex = index
},50)
//es6的函数写法
leaveIndex(){
this.categoryIndex = -1
}
},
十九、编程时导航+事件委派:路由的跳转与传参
三级联动:a标签的跳转
声明式导航(不推荐)
第一种声明式导航:为什么使用router-link组件的时候,会出现卡顿那? router-link是一个组件:相当于VueComponent类的实例对象,一瞬间 new VueComponent很多实例(1000+),很消耗内存,因此导致卡顿。
编程式导航(不推荐)
第二种编程式导航:push|replace ,有多少个a标签就会有多少个触发函数,会卡顿
使用编程时导航+事件委派 的方式实现路由跳转 :(推荐)
第三种编程时导航+事件委派
思路:路由跳转的时候【home->search】:需要进行路由传递参数【分类的名字、一、二、三级分类的id】
问题:事件委托是什么?如何确定点击的就是a标签?并传递相应的参数?
事件委派即把子节点的触发事件都委托给父节点,这样只需要一个回调函数 goSearch 就可以解决。
为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
为三个等级的 a 标签分别 添加自定义属性data-categoryId1(一级)、data-categoryId2(二级)、data-categoryId3(三级) 并获取各级商品id,用于路由跳转。
<!--
1.a=>router-link 会卡顿 每个router-link都有一个实例,占内存大
2.给每个a使用push,同理有多少个a标签就会有多少个触发函数,会卡顿
3.委托给父元素使用 push 不会卡顿,问题不知道点击的那个元素【a h3 em dd】。需要给a自定义命名
-->
<div @mouseleave="leaveShow" @mouseenter="enterShow">
<h2 class="all">全部商品分类</h2>
<!-- 过渡动画 -->
<transition name="sort">
<div class="sort" v-show="show">
<div class="all-sort-list2" @click="goSearch">
<div
class="item"
v-for="(c1, index) in categoryList"
:key="c1.categoryId"
>
<h3
:class="{ cur: currentIndex == index }"
@mouseenter="changeIndex(index)"
>
<a
:data-categoryName="c1.categoryName"
:data-category1Id="c1.categoryId"
>{{ c1.categoryName }}</a
>
</h3>
<div
class="item-list clearfix"
:style="{ display: currentIndex == index ? 'block' : 'none' }" // 这个是隐藏的关键
>
<div class="subitem">
<dl
class="fore"
v-for="(c2, index) in c1.categoryChild"
:key="c2.categoryId"
>
<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>
</transition>
</div>
goSearch(e){
// 只有自定义的data-才有dataset属性
let { categoryname , categoryid1 , categoryid2 , categoryid3} = e.target.dataset
// categoryname存在,表示点击的a 标签
if(categoryname){
// console.log(e.target); // 获取a标签
// 整理路由跳转的参数
let location = {name:'Search'} //跳转路由name
let query = {categoryName:categoryname} //路由参数
if(categoryid1){
query.categoryid1 = categoryid1 //一级
}else if(categoryid2){
query.categoryid2 = categoryid2 //二级
}else if(categoryid3){
query.categoryid3 = categoryid3 //三级
}
location.query = query
this.$router.push(location)
}
}
二十、动画过渡效果 vue :transition
过渡动画:
前提: 组件或元素务必要有v-if|v-show指令才可以进行
transition 要有属性name 如name = “fade”
提供了 class 来切换:
v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
v-enter-to: 2.1.8版及以上 定义进入过渡的结束状态。在元素被插入之后下一帧生效,在过渡/动画完成之后移除。
v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
v-leave-to: 2.1.8版及以上 定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除
<div class="container" @mouseleave="leaveIndex" @mouseenter="enterShow">
<h2 class="all">全部商品分类</h2>
<transition name='sorts'>
<div class="sort" v-show="show">
<div
class="all-sort-list2"
v-for="(v1, index) in categoryList"
:key="v1.categoryId"
@click="goSearch($event)"
>
<div class="item">
<h3
@mouseover="changeIndex(index)"
:class="{ cur: categoryIndex == index }"
>
<a
data-categoryName="v1.categoryName"
data-categoryId1="v1.categoryId"
>{{ v1.categoryName }}</a
>
</h3>
<div class="item-list clearfix">
<div
class="subitem"
v-for="(v2, index) in v1.categoryChild"
:key="v2.categoryId"
>
<dl class="fore">
<dt>
<a
data-categoryName="v2.categoryName"
data-categoryId2="v2.categoryId"
>{{ v2.categoryName }}</a
>
</dt>
<dd
v-for="(v3, index) in v2.categoryChild"
:key="v3.categoryId"
>
<em>
<a
data-categoryName="v3.categoryName"
data-categoryId3="v3.categoryId"
>{{ v3.categoryName }}</a
>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
leaveIndex() {
this.categoryIndex = -1;
if (this.$route.path != "/home") {
this.show = false; // 这里只有search当鼠标离开三级目录时是隐藏的
}
},
enterShow() {
this.show = true;
},
.sorts-enter{
height: 0px;
/* opacity: 0 */
}
.sorts-leave-to{
height: 461px;
/* opacity: 0.5;*/
}
// 定义动画时间、速率
.sorts-enter-active {
transition: all 0.5s linear;
/* transition: opacity 5s*/
}
二十一、Vue 路由销毁问题:优化三级导航接口
Vue 在路由切换的时候会销毁旧路由。
解决:当一个接口在home,search页面分别调用一次的,需要的数据是一样的时候。处于性能考虑我们希望该数据只请求一次,所以我们把这次请求放在 App.vue 的mounted中,根组件App.vue 的mounted只会执行一次获取的数据放在vuex中。
原因:home与search是两个组件,由home跳到search组件的时候会销毁home,创建search,反之也是。所有同一个接口就会频繁被调用。
二十二、合并query与params
//搜索按钮的事件处理函数,用于跳转到search路由组件当中
goSearch(){
if(this.$route.query){
let location = {
name:'Search',
params:{keyword:'123'||undefined},
}
loction.query = this.$route.query;
this.$router.push(location)
}
}
// 点击三级菜单的时候,判断如果路由跳转的时候,带有params参数,搜索框中的内容传递过去
if (this.$route.params)
loction.params = this.$route.params;
//动态给location配置对象添加query属性
loction.query = query;
//路由跳转
this.$router.push(loction);
}
这里就是要考虑到当你点击三级菜单跳转到search页时候,会将相应的id带过去,
但点击搜索框的时候不能将相应的id带过去,仅仅带着你搜索的值。
二十三、v-bind:class 与 v-bind:style
:class
<div :class="[{active: isActive},errorClass]" >
一行文字
</div>
export default {
data() {
return {
isActive:true,
errorClass: 'text-danger'
}
}
.active{
color: aqua;
font-size: 50px;
}
.text-danger{
background-color: burlywood;
}
:style
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div v-bind:style="styleObject"></div>
export default {
data() {
return {
activeColor:'red',
fontSize: 30, // 数字
styleObject:{
color: 'red',
fontSize: '13px' // 字符串
}
}
}
二十四、mock数据
注意:mock(模拟数据)数据需要使用到mockjs模块,可以帮助我们模拟数据。 注意:mockjs【并非mock.js mock-js】
官方地址
mock官网一句话:晚上练习的时候,如果网速可以,看看mock的官网,看看语法规则; 生成随机数据,拦截 Ajax 请求 mock官网当中这句话的理解: 模拟的数据一般:对象、数组 { ‘a|1-10’:‘我爱你’ } 拦截ajax请求:请求发布出去【浏览器会进行拦截:笨想,因为服务器】,只是项目当中本地自己玩耍数据。
mockjs
第一步:安装依赖包mockjs
第二部:在src文件夹下创建一个文件夹,文件夹mock文件夹。
第三步:准备模拟的数据 把mock数据需要的图片放置于public文件夹中! 比如:listContainer中的轮播图的数据 [{id:1,imgUrl:'xxxxxxxxx'}, {id:2,imgUrl:'xxxxxxxxx'}, {id:3,imgUrl:'xxxxxxxxx'}, ]。
第四步:在mock文件夹中创建一个server.js文件 注意:在server.js文件当中对于banner.json||floor.json的数据没有暴露,但是可以在server模块中使用。 对于webpack当中一些模块:图片、json,不需要对外暴露,因为默认就是对外暴露。
第五步:通过mock模块模拟出数据
1.安装:npm install mockjs
2.目录结构
--mock
------banner.json
------floor.json
------mockServe.js // 用来mock假数据的
--api
------index.js // 用来引入axios封装的方法和mockAjax封装的方法
------mockAjax.js // 封装用来请求的方法(不能通过axios请求,只能另写mock的方法,与axios一样。)
- banner.json
注意:这里 "imgUrl": "/images/banner1.jpg"的路径是指的public下的images
[{
"id": "1",
"imgUrl": "/images/banner1.jpg"
},
{
"id": "2",
"imgUrl": "/images/banner2.jpg"
},
{
"id": "3",
"imgUrl": "/images/banner3.jpg"
},
{
"id": "4",
"imgUrl": "/images/banner4.jpg"
}
]
4.mockServe.js // 用来mock假数据的
//先引入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});
5.vuex=>store=>home
// 引入方法
import {
reqBanners,
} from "@/api/index";
const state = {
banners: [], // 广告位轮播数据的数组
};
const mutations = {
// mutions唯一可以修改state的地方
/*
接收广告轮播列表
*/
RECEIVE_BANNERS(state, banners) {
state.banners = banners
},
}
const actions = {
// action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
//异步获取广告位轮播数据
async getBannerList({ commit }) {
const result = await reqBanners();
if (result.code === 200) {
commit('RECEIVE_BANNERS', result.data)
}
},
}
//计算属性
const getters = {};
export default {
state,
mutations,
actions,
getters,
};
6.在main.js中引入,就像引入样式一样
import '@/mock/mockServe'
调用(这里是通过vuex)
// 调用数据
import {mapState} from 'vuex'
mounted(){
// 派发action.,通过vuex发起ajax请求,将数据存储在仓库中
this.$store.dispatch('getBannerList')
},
computed:{//banners
...mapState({
banners:state=>state.banners
})
}
二十五、目前不熟悉CSS使用拓展:首行文本的缩进等
text-indent
text-indent 属性规定文本块中首行文本的缩进。
注意: 负值是允许的。如果值是负数,将第一行左缩进。
text-indent: 2rem;//两个字符的缩进
text-align
left:把文本排列到左边。默认值:由浏览器决定
right:把文本排列到右边
center:把文本排列到中间
justify:实现两端对齐文本效果
inherit:规定应该从父元素继承 text-align 属性的值。
如:text-align:center
transition详细介绍
这是css3动画效果。
// 鼠标滑过的时候的放大效果
.course-box:hover {
transform: scale(1.2);
transition: all 0.8s;
}
css中也可以用@符号,注意@前面必须加~
background-image: url(~@/assets/images/icons.png);
二十六、swiper插件与封装
安装(一般不使用最新版本的,高版本问题多)
npm install --save swiper@5 //下载的是版本5
引入包(js\css)
import Swiper from 'swiper'
import 'swiper/css/swiper.min.css'
// 一般在使用的页面引用,当多个页面有同样样式的轮播图的时候,可以在main.js中时候
轮播图在使用的时候,必须先加载样式,然后执行动画
解决在 mounted 创建 swiper实例图片无法加载问题?
我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。
由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,
而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
解决思路:
方法一:使用定时器 (不完美) : 分页器组件在页面延迟加载
方法二:使用 watch,只能保证在bannerList变化时创建swiper对象,不能保证此时v-for已经执行完了。
方法三:使用 watch与 this.$nextTick()结合 (完美)
方法三:使用监听的方式与this.$nextTick()相结合
<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', // Carousel是轮播图的意思
props: ['list'],
watch: {
list: {
// 为什么watch监听不到list:如果数据是从父组件(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)传过来,数据从未发生过变化,直接写watch监听是监听不到数据的,所以通过立即监听:不管你数据有没有变化,我上来立即监听一次
immediate: true,
handler() {
//只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTick延迟执行一次
this.$nextTick(() => {
var mySwiper = new Swiper(this.$refs.cur, {
autoplay: {
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: false
},
loop: true,
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
//点击小球的时候也切换图片
clickable: true
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
})
})
}
}
}
}
</script>
组件封装
使用 ref 来避免页面中多个轮播图组件返回的是同样的数据
<template>
<div class="swiper-container" id="" ref='cur'>
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="img in banners" :key="img.id">
<img :src="img.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
import Swiper from "swiper";
export default {
name:'Carousel',//轮播
props:['banners'],
watch: {
banners: {
handler(nVal, oVal) {
this.$nextTick(() => {
var mySwiper = new Swiper(this.$refs.cur, {
// direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable :true,// 点击原点切换
},
// 如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
// // 如果需要滚动条
// scrollbar: {
// el: '.swiper-scrollbar',
// },
});
});
},
immediate: true,//立即监听一次,list数据发生过变化
},
},
}
</script>
二十七、props属性的几种写法
props: ['propA']
props: {
A: Number, // 必须是数字类型
B: [String, Number], // 必须是字符串或数字类型
C: {
type: Boolean,
default: true
},
D: {
type: Number,
required: true
}
// 如果是数组或对象,默认值必须是一个函数来返回
E: {
type: Array,
default: () => []
},
F: {
type: Object,
default: () => {}
}
// 自定义一个验证函数
G: {
validator: (value) => {
return value > 10
}
}
}
验证的type类型可以是:
String,Number,Boolean,Object,Array,Function
当prop验证失败时,在开发版本下会在控制台抛出一条警告
二十八、使用watch监听路由变化实现动态搜索
//数据监听:监听组件实例身上的属性的属性值变化
watch: {
//监听路由的信息是否发生变化,如果发生变化,再次发起请求
$route(newValue, oldValue) {
//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3,如果不置空,如你上一次搜索的1级,这次你搜索3级,会将1、3级的id都带过去。
// 合并参数对象,再次发请求之前整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
//再次发起ajax请求
this.getData()
//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数
//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
//所以每次请求结束后将相应参数置空
this.searchParams.category1Id = undefined
// 使用 undefined是为了提示性能,路由将不会携带undefined参数,如果使用空字符串还是会被传入
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
}
}
二十九、Object.assign合并对象
this.$route.query = {
categoryName: "手机",
category3Id: "88",
},
this.$route.params = {
keyword: "我是参数",
}
let searchParams: {
//产品相应的id
category1Id: "",
category2Id: "",
category3Id: "",
//产品的名字
categoryName: "",
//搜索的关键字
keyword: "",
//排序:初始状态应该是综合且降序
order: "1:desc",
//第几页
pageNo: 1,
//每一页展示条数
pageSize: 3,
//平台属性的操作
props: [],
//品牌
trademark: "",
},
Object.assign(searchParams,this.$route.query,this.$route.params)
// 以下为合并的数据
searchParams: {
//产品相应的id
category1Id: "",
category2Id: "",
category3Id: "88",
//产品的名字
categoryName: "手机",
//搜索的关键字
keyword: "我是参数",
//排序:初始状态应该是综合且降序
order: "1:desc",
//第几页
pageNo: 1,
//每一页展示条数
pageSize: 3,
//平台属性的操作
props: [],
//品牌
trademark: "",
},
二十九、打开新的窗口
遇到的问题:地址如果是动态加载来的,a标签可能会一开始获取不到。用 window.open(‘地址’,"_blank" )解决。
a标签
<a :href="地址" target="_blank" > 跳转</a>
window.open(‘地址’,“_blank” )
window.open(‘地址’,"_blank")
三十、查看当前使用的地址:location.origin
以下情况是在你本地跑项目的时候的地址。
let x = location.origin
console.log(x) //'http://localhost:8080'
三十一、手写面包屑
面包屑操作:三个删除逻辑。
第一个:当分类属性(query)删除时删除面包屑同时修改路由信息。
第二个:当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
第三个:仅仅删除品牌面包屑
1、query删除时
因为此部分在面包屑中是通过categoryName展示的,所以删除时应将该属性值置空。
可以通过路由再次跳转修改路由信息和url链接(自己跳自己:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着)
2、params删除时
删除输入框内的关键字(因为params参数是从输入框内获取的)
输入框数据是在Header组件中的
<!--bread:面包屑,带有x的结构的-->
<div class="bread">
<ul class="fl sui-breadcrumb">
<li>
<a href="#">全部结果</a>
</li>
</ul>
<ul class="fl sui-tag">
<!-- 分类的面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">
{{ searchParams.categoryName
}}<i @click="removeCategoryName">×</i>
</li>
<!-- 关键字的面包屑 -->
<li class="with-x" v-if="searchParams.keyword">
{{ searchParams.keyword }}<i @click="removeKeyword">×</i>
</li>
<!-- 品牌的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
{{ searchParams.trademark.split(":")[1]
}}<i @click="removeTradeMark">×</i>
</li>
<!--平台的售卖的属性值展示 -->
<li
class="with-x"
v-for="(attrValue, index) in searchParams.props"
:key="index"
>
{{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">×</i>
</li>
</ul>
</div>
<!--selector-->
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo" />
//把发请求的这个action封装到一个函数里面
//将来需要再次发请求,你只需要在调用这个函数即可
getData() {
this.$store.dispatch("getSearchList", this.searchParams);
},
//删除分类的名字
removeCategoryName() {
//把带给服务器的参数置空了,还需要向服务器发请求
//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
//但是你把相应的字段变为undefined,当前这个字段不会带给服务器
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
this.getData();
//地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)
//严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
if (this.$route.params) {
this.$router.push({ name: "search", params: this.$route.params });
}
},
//删除关键字
removeKeyword() {
//给服务器带的参数searchParams的keyword置空
this.searchParams.keyword = undefined;
//再次发请求
this.getData();
//通知兄弟组件Header清除关键字
this.$bus.$emit("clear");
//进行路由的跳转
if (this.$route.query) {
this.$router.push({ name: "search", query: this.$route.query });
}
},
//自定义事件回调
trademarkInfo(trademark) {
//1:整理品牌字段的参数 "ID:品牌名称"
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
//再次发请求获取search模块列表数据进行展示
this.getData();
},
//删除品牌的信息
removeTradeMark() {
//将品牌信息置空
this.searchParams.trademark = undefined;
//再次发请求
this.getData();
},
三十二、组件通信
全局事件总线(兄弟组件传值),
在mian.js挂载事件总线
// router最好用小写的
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus = this
},
store,
router
}).$mount('#app')
通知兄弟组件Header清除关键字
//删除关键字
removeKeyword() {
//给服务器带的参数searchParams的keyword置空
this.searchParams.keyword = undefined;
//再次发请求
this.getData();
//通知兄弟组件Header清除关键字
this.$bus.$emit("clear");
//进行路由的跳转
if (this.$route.query) {
this.$router.push({ name: "search", query: this.$route.query });
}
},
清除数据
mounted() {
// 通过全局事件总线清除关键字
this.$bus.$on("clear", (val) => {
// 如果有属性 如:this.$bus.$emit("clear",属性);这里的val就代表属性值
this.keyword = "";
});
},
插槽(父子传值)
父组件
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
name:'Father'
}
</script>
<style>
</style>
子组件
<Father>
<div>
<img>
</div>
</Father>
三十三、平台的售卖的属性值(面包屑)
用 v-for,不用v-if:searchParams.props是数组,可以有多个属性值
<ul class="fl sui-tag">
<!-- 分类的面包屑 -->
<!-- 关键字的面包屑 -->
<!-- 品牌的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
{{ searchParams.trademark.split(":")[1]
}}<i @click="removeTradeMark">×</i>
</li>
<!--平台的售卖的属性值展示 -->
<li
class="with-x"
v-for="(attrValue, index) in searchParams.props"
:key="index"
>
{{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">×</i>
</li>
</ul>
// 平台属性的组件
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo" />
这里需要数组去重,否则出现这个效果
//收集平台属性地方回调函数(自定义事件)
attrInfo(attr, attrValue) {
//["属性ID:属性值:属性名"]
console.log(attr, attrValue);
//参数格式整理好
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
//数组去重
//if语句里面只有一行代码:可以省略大花括号
if (this.searchParams.props.indexOf(props) == -1)this.searchParams.props.push(props);
//再次发请求
this.getData();
},
//removeAttr删除售卖的属性
removeAttr(index) {
//再次整理参数
this.searchParams.props.splice(index, 1);
//再次发请求
this.getData();
},
SearchSelector 组件
<div class="type-wrap" v-for="item in attrsList" :key="item.attrId">
<div class="fl key">{{item.attrName}}</div>
<div class="fl value">
<ul class="type-list">
<li v-for="item2 in item.attrValueList" :key="item2" @click="attrClick(item,item2)">
<a>{{item2}}</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
attrClick(list,attr){
this.$emit('attrInfo',list,attr)
}
三十四、splice
splice() 方法用于添加或删除数组中的元素。
注意:这种方法会改变原始数组
三十五、阿里图标库使用
选择你需要的添加到你的项目中
点击查看在线链接,复制链接在html—》link中添加地址
点击复制代码即可
在pubilic heml中添加( 注意:自己添加https:)
<link rel="stylesheet" href="https://at.alicdn.com/t/font_3287598_4lkdo8h9vj.css">
使用 class=“iconfont
icon-UP” 下面是动态的箭头
// 静态使用
<span class="iconfont icon-direction-up"></span>
// 动态使用
<span
v-show="isTwo"
class="iconfont"
:class="{ 'icon-UP': isAsc, 'icon-DOWN': isDesc }"></span>
三十六、排序
需求:
1、综合与价格按钮,点击谁,谁的背景颜色变为红色。(类名:active)
谁有类这件事情,区分开综合与价格
2、将来点击综合||价格,还是需要给服务器发请求
【价格升序:把这个信息给服务器传递过去,服务器接收到信息,数据库自动把排序这件事情做了,
把排序做好的数据返回给你,你展示即可】
1:代表综合 2:代表价格 3:asc代表升序 4:desc代表降序
告诉服务器排序方式有几种情况?
"1:asc" "1:desc" "2:asc" "2:desc"
综合与价格箭头的显示与隐藏
1 箭头【选用阿里图标库】 https://www.iconfont.cn/
2 对于综合|价格旁边的箭头【动态显示:时而有,时而没有】,带有类名active的,就拥有箭头
3 根据1、2区分谁有类名(背景)、谁有箭头
根据asc|desc区分它用哪一个箭头【上、下】
带有类名active
.active {
a {
background: #e1251b;
color: #fff;
}
}
具体代码 class=“iconfont” //使用阿里图标库必须带的属性
<div class="navbar-inner filter">
<ul class="sui-nav">
<li :class="{active: isOne}" @click="changeOrder('1')">
<a href="#" >综合<span
v-show="isOne"
class="iconfont"
:class="{'icon-direction-up' : isAsc,'icon-todown':isDesc}"></span></a>
</li>
<li :class="{active: isTwo}" @click="changeOrder('2')">
<a href="#" >价格<span v-show="isTwo" class="iconfont"
:class="{'icon-direction-up' : isAsc,'icon-todown':isDesc}"></span></a>
</li>
</ul>
</div>
order:服务器需要字段,代表的是排序方式
order这个字段需要的是字符串(可以传递也可以不传递)
computed:{
...mapGetters(
// {goodsList:['goodsList'],
// attrsList:['attrsList'],
// trademarkList:['trademarkList']}
//这两种方式都可以
['goodsList','attrsList','trademarkList']
),
isTwo(){
return this.paramsList.order.indexOf('2') != -1
},
isOne(){
return this.paramsList.order.indexOf('1') != -1
},
isAsc(){
return this.paramsList.order.indexOf('asc') != -1
},
isDesc(){
return this.paramsList.order.indexOf('desc') != -1
},
},
排序
//排序的操作
changeOrder(flag) {
//flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)
//现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】
let originOrder = this.paramsList.order;
let orginsFlag = originOrder.split(":")[0];
let originSort = originOrder.split(":")[1];
//新的排序方式
let newOrder = "";
//判断的是多次点击的是不是同一个按钮
if (flag == orginsFlag) {
newOrder = `${orginsFlag}:${originSort == "desc" ? "asc" : "desc"}`;
} else {
//点击不是同一个按钮
newOrder = `${flag}:${"desc"}`;
}
//需要给order重新赋值
this.paramsList.order = newOrder;
//再次发请求
this.initData();
},
三十七、删除本地以及仓库远程分支
具体操作:
我现在在dev20181018分支上,想删除dev_20181018分支
1 先切换到别的分支: git checkout dev_20180927
2 删除本地分支: git branch -d dev_20181018
3 如果删除不了可以强制删除,git branch -D dev_20181018
4 有必要的情况下,删除远程分支:git push origin --delete dev_20181018
5 在从公用的仓库fetch代码:git fetch origin dev20181018:dev_20181018
6 然后切换分支即可:git checkout dev_20181018
注:上述操作是删除个人本地和个人远程分支,如果只删除个人本地,请忽略第4步
链接:https://www.jianshu.com/p/9fbe77ad1058
三十八、手写分页器
核心属性:
pageNo(当前页码)、pageSize(每页的数据有几条)、total(总数据)、continues(连续展示的页码)、totalPage (总页数)
分页器组件
遍历数字v-for v-for="(page, index) in startNumAndEndNum.end"
<template>
<div class="pagination">
<!-- 上 -->
<button :disabled="pageNo == 1"
@click="$emit('getPageNo', pageNo - 1)">
上一页
</button>
<button
v-if="startNumAndEndNum.start > 1"
@click="$emit('getPageNo', 1)"
:class="{ active: pageNo == 1 }"
>
1
</button>
<button v-if="startNumAndEndNum.start > 2">···</button>
<!-- 中间部分 -->
<button
v-for="(page, index) in startNumAndEndNum.end"
:key="index"
v-if="page >= startNumAndEndNum.start"
@click="$emit('getPageNo', page)"
:class="{ active: pageNo == page }"
>
{{ page }}
</button>
<!-- 下 -->
<button v-if="startNumAndEndNum.end < totalPage - 1">···</button>
<button
v-if="startNumAndEndNum.end < 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>
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)
export default {
name: "Pagination",
props: ["pageNo", "pageSize", "total", "continues"],
computed: {
//总共多少页
totalPage() {
//向上取证
return Math.ceil(this.total / this.pageSize);
},
//计算出连续的页码的起始数字与结束数字[连续页码的数字:至少是5]
startNumAndEndNum() {
const { continues, pageNo, totalPage } = this;
//先定义两个变量存储起始数字与结束数字
let start = 0,
end = 0;
//连续页码数字5【就是至少五页】,如果出现不正常的现象【就是不够五页】
//不正常现象【总页数没有连续页码多】
if (continues > totalPage) {
start = 1;
end = totalPage;
} else {
//正常现象【连续页码5,但是你的总页数一定是大于5的】
//起始数字
start = pageNo - parseInt(continues / 2);
//结束数字
end = pageNo + parseInt(continues / 2);
//把出现不正常的现象【start数字出现0|负数】纠正
if (start < 1) {
start = 1;
end = continues;
}
//把出现不正常的现象[end数字大于总页码]纠正
if (end > totalPage) {
end = totalPage;
start = totalPage - continues + 1;
}
}
return { start, end };
},
},
};
.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;
}
}
}
.active{
background: skyblue;
}
父组件
当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
<!-- 分页器 -->
<Pagination
:pageNo="paramsList.pageNo"
:pageSize="paramsList.pageSize"
:total="total"
:continues="5"
@getPageNo='getPageNo'
/>
import { mapState} from 'vuex'
pageNoInfo(pageNo){
this.paramsList.pageNo= pageNo;
this.initData()
}
computed:{
...mapState(
{
total:(state)=>state.search.listInfo.total
}
)
},
三十九、路由:vue-router 滚动条
问题
解决
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router ,可以自定义路由切换时页面如何滚动。
router滚动行为
scrollBehavior(to, from, savedPosition) {
//返回的这个y=0,代表的滚动条在最上方
return { y: 0 };
},
router =》routes.js 具体代码
import Detail from '@/pages/Detail'
export default [
{
path:'/Detail/:id',
name: 'Detail',
component:Detail,
meta: { show : true }
},
{
path:'/Search/:keyword?',
name:'Search',
component: resolve => require(['@/pages/Search'], resolve),
meta: { show : true }
},
]
router =》index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes'
// 使用路由插件
Vue.use(VueRouter)
// 引入路由组件
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
VueRouter.prototype.push = function (location,resolve,reject){
if(resolve && reject){
// originalPush() //这是错误的写法,这样改变了this,此时指向window
originalPush.call(this,location,resolve,reject)
}else{
originalPush.call(this,location,()=>{},()=>{})
}
}
//对外暴露VueRouter类的实例
let router = new VueRouter({
//配置路由
//第一:路径的前面需要有/(不是二级路由)
//路径中单词都是小写的
//component右侧V别给我加单引号【字符串:组件是对象(VueComponent类的实例)】
routes,
//滚动行为
scrollBehavior(to, from, savedPosition) {
//返回的这个y=0,代表的滚动条在最上方
return { y: 0 };
},
});
export default router;
字符串拼接地址:详情跳转需要带着id
<div class="p-img" style="width:150px;">
<router-link :to="`/Detail/${item.id}`">
<img :src="item.defaultImg" style="width:100%;height:80%"/>
</router-link>
</div>
四十、nvm安装
nvm 安装镜像
第一步:配置下载镜像,在nvm安装路径下找到setting.txt文件,打开,新增如下信息
node_mirror: https://npm.taobao.org/mirrors/node/
npm_mirror: https://npm.taobao.org/mirrors/npm/
第二步:安装
打开cmd,输入nvm,出现下面这一堆字就是安装成功了
第三步:nvm使用
0,找到cmd,使用管理员模式打开
1,安装指定node.js版本 `nvm install 14.16`
2,安装完成之后,需要使用才能生效` nvm use 14.16`
3,查看当前已安装的node.js版本,带*号的是正在使用的 `nvm ls`
第四步:修改npm默认镜像源
1,修改npm镜像源为淘宝镜像
npm config set registry https://registry.npm.taobao.org,
2,检查是否成功
npm config get registry
3,或者直接安装淘宝镜像(命令如下,使用起来就是cnpm)
npm i -g cnpm --registry=https://registry.npm.taobao.org
四十一、获取undefined的属性报错
处理:undefined的属性报错
通过计算属性,将获取到的空数组的属性置空 this.skuImageList[0] || {}
<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: {
type: Array,
default: () => []
}
},
// 处理skuImageList数组为空的时候的报错。
computed:{
imgObj(){
return this.skuImageList[0] || {}
}
}
}
</script>
原因:假设我们网络故障,导致skuImageList的数据没有请求到,即skuImageList是一个空的对象,当我们去调用computed中的return this.skuImageList[0]时,因为skuImageList为空,所以也不存在长度,即我们this.skuImageList[0]为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。
即:网络正常时不会出错,一旦无网络或者网络问题就会报错。
总结:所以我们在写对象的时候要养成一个习惯在返回值后面加一个||条件。即当属性值undefined时,会返回||后面的数据,这样就不会报错。
如果返回值为对象加||{},数组:||[ ]。
四十二、排他
点击谁,谁高亮
数据结构:
属性:[
{颜色:[{黑色;isChecked :0},{白色;isChecked :0}]},
{版本:[{1111;isChecked :0},{222;isChecked :0}] }
]
Attrlist:[{1111;isChecked :0},{222;isChecked :0}]
item :{222;isChecked :0}
排他就是先将数组中的全部置为不高亮,然后将点击的那个置为高亮
methods:{
curIndexClick(Attrlist,item){
Attrlist.forEach(element => {
element.isChecked = 0
});
item.isChecked = 1
}
}
<dl v-for="(spuSaleAttr) in spuSaleAttrList"
:key="spuSaleAttr.id">
<dt class="title">{{spuSaleAttr.saleAttrName}}</dt>
<dd changepirce="0"
v-for="(item,index) in spuSaleAttr.spuSaleAttrValueList"
:key="item.id"
:class="item.isChecked == '1' ? 'active' : ''"
@click="curIndexClick(spuSaleAttr.spuSaleAttrValueList,item)">
{{item.saleAttrValueName}}
</dd>
</dl>
四十三、放大镜下轮播图选中
在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex 实现点击图片边框高亮设置,当符合图片的下标满足currentIndex===index时,该图片就会被标记为选中。
<template>
<div class="swiper-container" ref='cur'>
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(imgattr,index) in skuImageList" :key="imgattr.id" >
<img
:src="imgattr.imgUrl"
@click="clickCurindex(index)"
:class="{active:curIndex == index}">
</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</template>
data(){
return{
curIndex:0
}
},
methods:{
clickCurindex(index){
this.curIndex = index;
this.$bus.$emit('getIndex',index)
}
}
img {
width: 100%;
height: 100%;
border: 1px solid #ccc;
padding: 2px;
width: 50px;
height: 50px;
display: block;
&.active {
border: 2px solid #f60;
padding: 1px;
}
css写法:也可以用
// &:hover {
// border: 2px solid #f60;
// padding: 1px;
// }
}
四十四、购物车:input失焦,NAN,promise等
input为用户输入:用户什么都有可能输入,比如输入汉字,负数,小数等
对输入框进行监听:然后相应的进行处理
<input autocomplete="off" class="itxt" v-model="curNum" @change="curNumblur">
字符串乘数字 = NAN
e.target.value 获取的是文本框中的值
curNumblur(e){
//e.target.value 获取的是文本框中的值
let value = e.target.value *1
// 字符串*1 得到的是NAN
if(isNaN(value)|| value < 1){
this.curNum = 1
}else{
// 出现小数的情况
this.curNum = parseInt(value)
}
}
点击加入购物车的操作:当你点击加入购物车的时候,首先要给服务器发个消息通知他自己加的那个手机以及加的数量。然后服务器添加成功后,可以跳转到购物车详情页。
<a @click="addShopcar">加入购物车</a>
注意:async函数执行返回的结果一定是一个promise【要么成功,要么失败】
const actions = {
//加入购物车的||修改某一个产品的个数
async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
//发请求:前端带一些参数给服务器【需要存储这些数据】,存储成功了,没有给返回数据
//不需要在三连环(仓库存储数据了)
let result = await reqAddOrUpdateShopCart(skuId, skuNum);
if (result.code == 200) {
//返回的是成功的标记
return "ok";
} else {
//返回的是失败的标记
return Promise.reject(new Error("faile"));
}
},
};
vuex 仓库中返回的是一个状态
//加入购物车
async addShopcar() {
//1:在点击加入购物车这个按钮的时候,做的第一件事情,将参数带给服务器(发请求),通知服务器加入购车的产品是谁
//this.$store.dispatch('addOrUpdateShopCart'),说白了,它是在调用vuex仓库中的这个addOrUpdateShopCart函数。
//2:你需要知道这次请求成功还是失败,如果成功进行路由跳转,如果失败,需要给用户提示
try {
//成功 (这里的res是一个promise.)
let res = await this.$store.dispatch("addOrUpdateShopCart", {
skuId: this.$route.params.skuid,
skuNum: this.skuNum,
});
//3:进行路由跳转
//4:在路由跳转的时候还需要将产品的信息带给下一级的路由组件
//一些简单的数据skuNum,通过query形式给路由组件传递过去
//产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据在消失)
//本地存储|会话存储,一般存储的是字符串
sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo));
this.$router.push({name:'addcartsuccess',query:{skuNum:this.skuNum}});
} catch (error) {
//失败
alert(error.message);
}
},
四十五、try catch finally 处理异常
执行规则:首先执行try中的代码 如果抛出异常会由catch去捕获并执行
如果没有发生异常 catch去捕获会被忽略掉 但是不管有没有异常最后都会执行。
try{
//在此运行代码:成功也在此处理
}
catch(err){
//失败在此处理错误
}
finally{
// 不管成功还是失败都执行
}
四十六、H5的本地存储 :传递对象
sessionStorage (会话存储,关闭页面消失) /localStorage (本地存储) 区别
传递对象之类的复杂信息,通过Web Storage实现。
组件与组件通信(无关系的组件跳转),可以通过query携带信息,但会造成地址栏过长。也可以通过web Storage进行携带信息跳转,web Storage只能传递字符串的形式
,若想传送对象,可通过JSON.stringify()
转成字符串,获取数据可通过JSON.parse()
将字符串转为对象。
存储对象 this.skuInfo
sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo));
获取数据
computed: {
skuInfo() {
return JSON.parse(sessionStorage.getItem("SKUINFO"));
},
},
四十七、uuid:临时游客
api
vuex
组件
购物车列表的数据
此时购物车中结算的列表是空的,因为在加入购物车的时候只给服务器添加了数量以及产品id,并未告知是哪个用户添加的购物车,此时购物车的列表中虽存有数据,但不知道将那条数据返回给你。所以需要告知服务器你的身份。此时可以通过uuid
来验证用户身份。
由上图可以看出,添加购物车的时候没有传递游客身份参数的地方,所以此时,我们要将这个身份写在在请求头中。
这个身份使用期间,不能随意变换,
单例模式,只执行一次
import { v4 as uuidv4 } from 'uuid';
//要生成一个随机字符串,且每次执行不能发生变化,游客身份持久存储
export const getUUID = ()=>{
//先从本地存储获取uuid(看一下本地存储里面是否有)
let uuid_token = localStorage.getItem('UUIDTOKEN');
//如果没有
if(!uuid_token){
//我生成游客临时身份
uuid_token = uuidv4();
//本地存储存储一次
localStorage.setItem('UUIDTOKEN',uuid_token);
}
//切记有返回值,没有返回值undefined
return uuid_token;
}
请求拦截器中添加uuid
//对于axios进行二次封装
import axios from "axios";
//在当前模块中引入store
import store from '@/store';
//底下的代码也是创建axios实例
let requests = axios.create({
//基础路径
baseURL: "/api",
//请求不能超过5S
timeout: 5000,
});
//请求拦截器----在项目中发请求(请求没有发出去)可以做一些事情
requests.interceptors.request.use((config) => {
//现在的问题是config是什么?配置对象
//可以让进度条开始动
if(store.state.detail.uuid_token){
//请求头添加一个字段(userTempId):和后台老师商量好了
config.headers.userTempId = store.state.detail.uuid_token;
}
return config;
});
存在仓库中
import { getUUID } from '@/utils/uuid_token'
const state = {
uuid_token: getUUID()
};
获取到数据
四十八、购物车数量处理:点击同一个事件
处理产品数量:数量发生变化就发送请求
<ul
class="cart-list"
v-for="(cart, index) in cartInfoList"
:key="cart.id"
>
<li class="cart-list-con1">
<input
type="checkbox"
name="chk_list"
:checked="cart.isChecked == 1"
@change="updateChecked(cart, $event)"
/>
</li>
<li class="cart-list-con2">
<img :src="cart.imgUrl" />
<div class="item-msg">{{ cart.skuName }}</div>
</li>
<li class="cart-list-con5">
<a
href="javascript:void(0)"
class="mins"
@click="handler('minus', -1, cart)"
>-</a
>
<input
autocomplete="off"
type="text"
minnum="1"
class="itxt"
:value="cart.skuNum"
@change="handler('change', $event.target.value * 1, cart)"
/>
<a
href="javascript:void(0)"
class="plus"
@click="handler('add', 1, cart)"
>+</a
>
</li>
<li class="cart-list-con6">
<span class="sum">{{ cart.skuNum * cart.skuPrice }}</span>
</li>
<li class="cart-list-con7">
<a class="sindelet" @click="deleteCartById(cart)">删除</a>
<br />
<a href="#none">移到收藏</a>
</li>
</ul>
节流作用:就是当用户点击‘-’过快的时候,会导致购买件数出现负数,解决这一个BUG。
//修改某一个产品的个数[节流]
handler: throttle(async function(type, disNum, cart) {
//type:为了区分这三个元素
//disNum形参:+ 变化量(1) -变化量(-1) input最终的个数(并不是变化量)
//cart:哪一个产品【身上有id】
//向服务器发请求,修改数量
switch (type) {
//加号
case "add":
disNum = 1;
break;
case "minus":
//判断产品的个数大于1,才可以传递给服务器-1
//如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
disNum = cart.skuNum > 1 ? -1 : 0;
break;
case "change":
// //用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
disNum = parseInt(disNum) - cart.skuNum;
}
// disNum = (isNaN(disNum)||disNum<1)?0:parseInt(disNum) - cart.skuNum;
break;
}
//派发action
try {
//代表的是修改成功
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: cart.skuId,
skuNum: disNum,
});
//再一次获取服务器最新的数据进行展示
this.getData();
} catch (error) {}
}, 500),
全选 与 总价
四十九、购物车修改状态与删除:every Promise.all
购物车修改状态
修改某个勾选的状态
修改全选的勾选的状态
全选的勾选情况
isAllCheck(){
return this.cartInfoList.every(item=>item.isChecked == 1)
},
这里修改状态成功之后,没有数据返回,data为null,如图所示。
// 切换商品选中状态
async checkCartStute({commit},{skuID,isChecked}){
let res = await reqCheckCart(skuID,isChecked)
console.log(res,'切换商品选中状态');
if(res.code == 200){
return 'ok'
}else{
Promise.reject(new Error('fail'))
}
},
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
// 切换全选状态
allCheckData({dispatch,state},isChecked){
let promiseAll = []
state.cartList[0].cartInfoList.forEach(item=>{
let promise = dispatch('checkCartStute',{skuID:item.skuId,isChecked})
promiseAll.push(promise)
})
return Promise.all(promiseAll)
},
// 更改单个产品的状态
async updateChecked(cart,event){
try{
let isChecked = cart.isChecked == '1' ? '0' :'1'
await this.$store.dispatch('checkCartStute',
{skuID:cart.skuId,isChecked:isChecked})
// 修改表格数据
this.getData()
}catch(error){
alert(error.message)
}
},
// 更改全部产品的选中状态(全选)
// (后台没有单独更改全部产品的选中状态的接口,这里使用单个需改接口)
async allCheck(event){
try{
let isChecked = event.target.checked ? '1' :'0'
await this.$store.dispatch('allCheckData',isChecked)
this.getData()
}catch(error){
alert(error.message)
}
},
// 切换商品选中状态
async checkCartStute({commit},{skuID,isChecked}){
let res = await reqCheckCart(skuID,isChecked)
if(res.code == 200){
return 'ok'
}else{
Promise.reject(new Error('fail'))
}
},
// 切换全选的状态
allCheckData({dispatch,state},isChecked){
let promiseAll = []
state.cartList[0].cartInfoList.forEach(item=>{
let promise = dispatch('checkCartStute',{skuID:item.skuId,isChecked})
promiseAll.push(promise)
})
return Promise.all(promiseAll)
},
bug:当我取消其中一个单选框的时候,全选依旧被勾选。
解决方式,监听数据变化。
watch:{
cartInfoList:{
handler(n,o){
this.isAllCheck = n.every(item=>{
return item.isChecked == 1
})
}
}
},
购物车删除
页面
组件
vuex
五十、阻止原生from表单默认跳转
由于登录按钮的父节点是一个form表单,如果使用@click触发登录事件,form表单会执行默认事件action实现页面跳转。这里我们使用@click.prevent,它可以阻止自身默认事件的执行。
<form>
<div class="input-text clearFix">
<span></span>
<input type="text" placeholder="邮箱/用户名/手机号" v-model="phone">
</div>
<div class="input-text clearFix">
<span class="pwd"></span>
<input type="text" placeholder="请输入密码" v-model="password">
</div>
<div class="setting clearFix">
<label class="checkbox inline">
<input name="m1" type="checkbox" value="2" checked="">
自动登录
</label>
<span class="forget">忘记密码?</span>
</div>
<button class="btn" @click.prevent="goUserLogin">登 录</button>
</form>
五十一、token
用户点击登录时候获取token,通过token辨认身份获取用户详细信息。当你登录之后就不允许在退回登录页面,没有登录之前也不能进入支付页面等。但是刷新页面的时候token数据丢失,所以要做持久化存储。
登录获取token
async goUserLogin(){
try{
const { password , phone } = this
password && phone && await this.$store.dispatch('goLogin',{ password , phone })
// 未登录点击购物车按钮是跳到登录页,登录之后跳到购物车页面。通过登录的地址栏是否带有query字符串,如下图。如直接登录,跳到首页。
this.$router.push(this.$route.query.redirect || '/home')
}catch(error){
alert(error.message);
}
}
token 在 src下面的 utils的 token.js文件中
// 首先向外暴露
// 存储token
export const setToken = (token)=>{
localStorage.setItem('TOKEN',token)
}
//获取token,需要将获取的值返给页面
export const getToken =()=>{
return localStorage.getItem('TOKEN')
}
// 清除本地存储的token
export const removeToken =()=>{
return localStorage.removeItem('TOKEN')
}
vuex 三连 存储token
import {setToken , getToken, removeToken} from '@/utils/token'
const state = {
token:getToken(),
}
const mutations = {
GOLOGIN(state,token){
state.token = token
}
}
async goLogin({commit},data){
let res = await reqLogin(data)
if(res.code == 200){
commit('GOLOGIN',res.data.token)
setToken(res.data.token)
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
},
退出登录
业务:退出登录的时候发请求告知服务器退出登录并清除token, 也要清除本地存储的token以及用户的数据
// 获取登录信息
const mutations = {
CLEAR(state){
state.token = '',
state.userInfo = {}
removeToken()
}
}
const actions = {
async getUserInfo({commit}){
let res = await reqUserInfo()
if(res.code == 200){
commit('FINDINFO',res.data)
}
},
}
此时问题:获取用户信息的接口如果在首页触发,一旦跳转页面就会丢失token数据。如果在app.vue触发,因为接口只触发一次,第一次页面中没有token数据,但是刷新页面后才能有。此时要考虑放在什么地方
五十二、路由导航守卫
业务:
- 登录之后获取token,说明有token就登录了,没有token就是未登录
- 有token就登录,就不能去登录以及注册页面,去非登录以及非注册页面或者刷新页面,token会失去数据。此时要判断有无用户信息。
2.1 有用户信息的情况下,可全部放行。
2.2 没有用户信息,通过token,在次获取用户信息,然后放行
2.3 token存在,但是由于时间过长,而导致token失效时,要退出登录页面,让他去登录- 没有token,就是未登录,目前先不处理,暂时全部放行。
在router index.js
// 全局守卫(在跳转到其他页面前先检查)
router.beforeEach(async(to, from, next) => {
// to:要去哪
// from:从哪来
// next 放行 next(path) 放行到指定组件
// next() //全部放行
// 判断空对象永远为真:userInfo
const name = store.state.user.userInfo.consignee
const token = store.state.user.token
//获取仓库中的token-----可以确定用户是登录了
let token = store.state.user.token;
let name = store.state.user.userInfo.name;
//用户登录了
if(token){
//已经登录而且还想去登录(地址栏修改测试),登录之后注册也不能去------不行
if(to.path=="/login"||to.path=='/register'){
next('/');
}else{
//已经登陆了,访问的是非登录与注册
//登录了且拥有用户信息放行(空对象 也是真的)
if(name){
next();
}else{
//登陆了且没有用户信息
//在路由跳转之前获取用户信息且放行
try {
await store.dispatch('getUserInfo');
next();
} catch (error) {
//token失效重新登录
await store.dispatch('userLogout');
next('/login')
}
}
}
}else{
// 未登录,不能去交易相关的页面,不能去支付相关页面,不能去个人中心。凡是以上页面都去登录页面
let toPath = to.path
if(toPath.indexOf('/trade') !=-1 || toPath.indexOf('/pay') !=-1) || toPath.indexOf('/center') !=-1){
// //把未登录的时候向去而没有去成的信息,存储于地址栏中【路由】(如未登录,你点击个人中心,跳到登录页登录之后,要跳转到个人中心页面)。这里的redirect代表从哪个页面调过来的。
next('/login?redirect='+toPath)
}
}
登录页面
async userLogin() {
try {
//登录成功
const { phone, password } = this;
phone&&password&&(await this.$store.dispatch("userLogin", { phone, password }));
//登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
let toPath = this.$route.query.redirect||"/home";
this.$router.push(toPath);
} catch (error) {
alert(error.message);
}
},
总的来说就是登录获取token,通过token获取用户信息,在跳转页面以及刷新页面的时候能够都带有用户信息,token判断跳转那个页面(要做一个持久化的存储),用户信息判断你能跳转到哪去
五十三、main.js封装全局api
//统一引入:表示trade接口文件夹:将所有的接口都用API表示
import * as API from '@/api/trade';
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus = this;
Vue.prototype.$API = API;
},
store,
router
}).$mount('#app')
let result = await this.$API.reqSubmitOrder(tradeNo, data); // 使用,不用再次引入
五十四、qrcode二维码插件
插件地址: https://www.npmjs.com/package/qrcode
扫扫向我付款
安装
1.npm install --save qrcode
引入
import QRCode from "qrcode";
使用
QRCode.toDataURL 得到的结果是
Promise
,
注意:如果做分享功能是分享当前的页面,因此需要拿到当前页面的地址,通过location.href来拿地址。
async open(){
let url = location.href;
let res = await QRCode.toDataURL(url)
}
五十五、支付
element ui 全局引入 与 按需引入 可参考官网
全局引入
// 全局引入
import ElementUI from 'element-ui';
Vue.use(ElementUI);
按需引入
import { Button,MessageBox} from 'element-ui';
Vue.component(Button.name,Button);
// ElementUI注册组件的时候,还有一种写法,挂在原型上
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
业务:点击立即支付按钮,弹出一个支付的二维码,只有支付成功之后才能点击已支付成功去回到支付成功的页面。
<a class="btn" @click="open">立即支付</a>
//弹出框
async open() {
//生成二维(地址)
let url = await QRCode.toDataURL(this.payInfo.codeUrl);
this.$alert(`<img src=${url} />`, "请你微信支付", {
dangerouslyUseHTMLString: true,
//中间布局
center: true,
//是否显示取消按钮
showCancelButton: true,
//取消按钮的文本内容
cancelButtonText: "支付遇见问题",
//确定按钮的文本
confirmButtonText: "已支付成功",
//右上角的叉子没了
showClose: false,
//关闭弹出框的配置值
beforeClose: (type, instance, done) => {
//type:区分取消|确定按钮
//instance:当前组件实例
//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");
// }
}
},
});
//你需要知道支付成功|失败
//支付成功,路由的跳转,如果支付失败,提示信息
//定时器没有,开启一个新的定时器
if (!this.timer) {
this.timer = setInterval(async () => {
//发请求获取用户支付状态
let result = await this.$API.reqPayStatus(this.orderId);
//如果code==200
if (result.code == 200) {
//第一步:清除定时器
clearInterval(this.timer);
this.timer = null;
//保存支付成功返回的code
this.code = result.code;
//关闭弹出框
this.$msgbox.close();
//跳转到下一路由
this.$router.push("/paysuccess");
}
}, 1000);
}
},
五十六、二级路由
做路由时,可先通过组件进行测试文件能不能运行。
// 使用
<groupOrder></groupOrder>
// 引入
import groupOrder from '@/pages/Center/groupOrder'
// 注册
components:{
groupOrder
}
二级路由
警告
二级路由
{
path:'/center',
component: resolve => require(['@/pages/Center'], resolve),
meta: { show : true },
children:[
// 第一种方法
{
path:'/center/groupOrder',
name:'groupOrder',
component: resolve => require(['@/pages/Center/groupOrder'], resolve),
},
// 第二种方法:不能带'/'表示一级路由。要么写全,如第一种方法
{
path:'myOrder',
name:'myOrder',
component: resolve => require(['@/pages/Center/myOrder'], resolve),
},
{
path: '',
redirect: 'myorder'
}
]
},
五十七、路由独享守卫
next():你本来想去哪里,我就放行,你就去完事了。
next(‘/login’):执行守卫放行到执行的路由。
next(false):路由跳转的时候,从哪里来回那里去。
单个路由使用
{
path:'/pay',
name: 'Pay',
component:()=>import('@/pages/Pay'),
meta: { show : true },
beforeEnter: (to, from,next) => {
/* 只有从交易界面, 才能跳转到支付界面 */
if(from.path == '/trade'){
next()
}else{
return false
// 或者 next(false) :本地路由跳转
}
},
},
五十八、报错:core-js与npm install --save vue/types/umd
报错 These dependencies were not found:
- core-js/modules/es.array.concat.js in ./src/api/shopCart/index.js, ./src/api/trade/index.js and 1 other
解决方法:添加红色代码
presets: [
'@vue/cli-plugin-babel/preset',
[ "@vue/app", { useBuiltIns: "entry" } ]
],
报错 npm install --save vue/types/umd
解决方法:删除框起来的代码
五十九、vue-lazyload:图片懒加载
图片:比用用户网络不好,服务器的数据没有回来,总不可能让用户看白色,至少有一个默认图片在展示。
官网地址
安装
npm i vue-lazyload -S
main.js
//引入图片
import atm from '@/assets/1.gif';
//引入插件
import VueLazyload from 'vue-lazyload'
//注册插件
Vue.use(VueLazyload)
Vue.use(VueLazyload, {
loading: atm,
})
组件中使用
<img v-lazy="good.defaultImg" />
Vue自定义插件一定暴露一个对象
//引入自定义插件
import myPlugins from '@/plugins/myPlugins';
Vue.use(myPlugins,{
name:'upper'
});
plugins myPlugins.js
//Vue插件一定暴露一个对象
let myPlugins = {};
myPlugins.install = function(Vue,options){
//全局指令
Vue.directive(options.name,(element,params)=>{
element.innerHTML = params.value.toUpperCase();
console.log(params);
});
}
//对外暴露组件对象
export default myPlugins;
六十、ES6 解构
const { phone , password , code } = this
// 相当于
const phone = this.phone
六十一、打包上线、map文件处理:输出报错信息
打包上线。在项目文件夹下执行npm run build。会生成dist打包文件。
dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。
map文件作用:因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以向未加密的代码一样,准确的输出是哪一行那一列有错。
在vue.config.js配置productionSourceMap: false
即可。
注意:vue.config.js配置改变,需要重启项目
vue.config.js
productionSourceMap:false,
六十二、全局过滤
过滤 filter.js
import Vue from 'vue'
Vue.filter('filterActions', (actors, options = ' ') => {
return actors.map(act => {
if (act.role !== '导演') {
return act.name
}
}).join(options)
})
使用
<p class="yihang">主演:{{data.actors | filterActions}}</p>
main.js引入