我的笔记(主要是尚品汇前台项目)

步骤: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一样。)
  1. 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>

list,attr

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 根据12区分谁有类名(背景)、谁有箭头
 根据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;
   // }
 }

Js放大镜

鼠标属性

在这里插入图片描述

四十四、购物车: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">&nbsp;&nbsp;</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数据,但是刷新页面后才能有。此时要考虑放在什么地方

五十二、路由导航守卫

业务:

  1. 登录之后获取token,说明有token就登录了,没有token就是未登录
  2. 有token就登录,就不能去登录以及注册页面,去非登录以及非注册页面或者刷新页面,token会失去数据。此时要判断有无用户信息。
    2.1 有用户信息的情况下,可全部放行。
    2.2 没有用户信息,通过token,在次获取用户信息,然后放行
    2.3 token存在,但是由于时间过长,而导致token失效时,要退出登录页面,让他去登录
  3. 没有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引入
在这里插入图片描述

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值