尚品汇项目

学习时间:2022年7月14日

学习来源:尚硅谷

1 简介

技术架构

  • 前端
    • vue,webpack,vuex,vue-router,axios,less
  • 后端

    • vue,webpack,vuex,vue-router,axios,scss,elementUI
  • 数据可视化

    • echarts数据可视化开源库
    • canvas画布
    • svg矢量图

2 初始化项目

使用vue-cli脚手架进行项目的初始化,选择vue2版本:

1
vue create mall-demo

2.1 文件目录分析

image-20220714112753172

  • public文件夹:静态资源,webpack进行打包的时候会原封不动打包到dist文件夹中。
  • pubilc/index.html是一个模板文件,作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html。
  • src文件夹(程序员代码文件夹)

    • assets: 存放公用的静态资源,一般放置多个组件共用的静态资源,注意webpack打包的时候会把静态资源当做一个模块,打包到js文件里。
    • components: 非路由组件(全局组件,公共组件等),其他组件放在views或者pages文件夹中
    • App.vue: 唯一的根组件
    • main.js: 程序入口文件,最先执行的文件
  • babel.config.js: 配置文件(babel相关)

  • package.json: 项目的详细信息记录
  • package-lock.json: 缓存性文件(各种包的来源)

2.2 项目配置

  • 项目运行,浏览器自动打开

修改package.json

1
2
3
4
5
"scripts": {
"serve": "vue-cli-service serve --open --host=localhost", // 添加--open和--host选项
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}
  • 关闭eslint校验工具

在根目录下的vue.config.js进行配置:

1
2
3
4
module.exports = {
//关闭eslint
lintOnSave: false
}
  • src文件夹配置别名为@

创建jsconfig.json,用@/代替src/exclude表示不可以使用该别名的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}

2.3 路由配置

2.3.1 分析

  • 前端所谓路由,本质就是KV键值对

    • key:URL(地址栏中的路径)
    • value:相应的路由组件
  • 简要分析

    • 路由组件:Home首页路由组件,Search搜索路由组件,Login登录路由组件,Register注册路由组件
    • 非路由组件:Header,Footer(在首页和搜索有,但在登录和注册页中没有)

2.3.2 非路由组件

  • 开发步骤:

    • 书写静态页面(HTML和CSS,但不是本课程重点)
    • 拆分组件
    • 获取服务器的数据动态展示
    • 完成相应的动态业务逻辑
    • 注意:创建组件的时候为组件结构 + 组件的样式 + 图片资源
  • 使用组件的步骤

    • 创建或者定义组件
    • 引入组件:App.vue中引入
    • 注册组件:App.vue中注册
    • 使用组件

项目结构

image-20220814101254353

部分代码

  • App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<!-- 使用组件 -->
<Header></Header>
<Footer></Footer>
</div>
</template>

<script>
// 引入组件
import Header from './components/Header'
import Footer from './components/Footer'
export default {
components: {
Header,
Footer,
}
}
</script>

效果

image-20220814111754419

2.3.3 路由组件

  • 在开发中,通常在src/components文件夹下放置非路由组件(共用的全局组件),在src/pages|views文件夹下放置路由组件。

image-20220814144224546

  • 配置路由:一般放置在src/router文件夹中

  • 路由组件和非路由组件的区别

    • 路由组件一般需要在router文件夹中进行注册(index.js,使用的即为组件的名字);非路由组件在使用的时候以标签的形式使用

    • 注册完路由时,不管是路由组件,还是非路由组件身上都会挂载$route$router属性(使用this点出来)

      • $route:一般获取路由信息【路径,query,params等】,或称为参数对象

        image-20220814114023013

        image-20220814113930393

      • $router:一般进行编程式导航的路由跳转【push|replace等】,或称为导航对象

  • 路由的跳转

    • 声明式导航:router-link
    • 编程式导航:push|replace
    • 区别:后者既可以完成前者的功能,还能实现其他的业务逻辑,例如登录时,除了要进行路由跳转,还需要进行其他的一些业务逻辑

部分代码

  • 路由入口文件router/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 配置路由
import Vue from 'vue';
import VueRouter from 'vue-router';

// 使用插件
Vue.use(VueRouter);

// 引入需要的路由组件
import Home from '@/pages/Home'
import Login from '@/pages/Login'
import Search from '@/pages/Search'
import Register from '@/pages/Register'

// 配置路由
export default new VueRouter({
// 配置路由
routes: [
{
path: '/home',
component: Home
},
{
path: '/login',
component: Login
},
{
path: '/search',
component: Search
},
{
path: '/register',
component: Register
},
// 重定向:使用redirect属性
// 在项目启动访问 / 时,立马定向到首页
{
path: '/',
redirect: '/home'
}
]
})
  • 入口文件main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import App from './App.vue'

// 引入路由,注意是小写的router
import router from '@/router'

Vue.config.productionTip = false

new Vue({
render: h => h(App),
// 注册路由(挂载路由模块)
// 这里的es6写法为kv一致时可以省略v
// 当这里书写router的时候,组件身上都拥有$route和$router属性
router
}).$mount('#app')
  • 根组件App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<!-- 使用组件 -->
<Header></Header>
<!-- 路由组件出口的地方:路由组件展示的地方 -->
<router-view></router-view>
<Footer></Footer>
</div>
</template>

<script>
// 引入组件
import Header from "./components/Header";
import Footer from "./components/Footer";
export default {
components: {
Header,
Footer,
},
};
</script>
  • Header.vue部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<!--其他代码...-->

<!-- <a href="###">登录</a> -->
<!-- <a href="###" class="register">免费注册</a> -->
<router-link to="/login">登录</router-link>
<router-link class="register" to="/register">免费注册</router-link>

<!--其他代码...-->

<button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch">
搜索
</button>

<!--其他代码...-->
</template>

<script>
export default {
methods: {
// 搜索按钮的回调,向search路由跳转
goSearch() {
// 其他的业务逻辑...
// ...
// 路由跳转
this.$router.push('/search');
}
}
};
</script>

2.3.4 路由元信息

文档:https://router.vuejs.org/zh/guide/advanced/meta.html

有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。

需求:Footer组件:在Home、Search显示,而在Register和Login中不显示。

  • 方式一:

    • 显示或隐藏组件:v-if|v-show
    • 可以通过组件的$route获取当前路由的信息,通过路径判断Footer的显示与隐藏

      image-20220814142256157

      1
      <Footer v-show="$route.path=='/home' || $route.path=='/search'"></Footer>
  • 方式二:

    • 利用路由元信息meta

代码示例

  • 路由index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 配置路由
export default new VueRouter({
// 配置路由
routes: [
{
path: '/home',
component: Home,
// 路由元信息
// show属性为自定义的名称
meta: {show: true}
},
{
path: '/login',
component: Login,
meta: {show: false}
},
{
path: '/search',
component: Search,
meta: {show: true}
},
{
path: '/register',
component: Register,
meta: {show: false}
}
// other code...
]
})
  • 根组件
1
2
<!-- 在Home、Search显示,在Register和Login隐藏 -->
<Footer v-show="$route.meta.show"></Footer>

image-20220814143653472

2.3.5 路由传递参数

  • 路由跳转有几种方式?
    • 声明式导航
    • 编程式导航
  • 路由传参的参数写法?
    • 例如/home/1?name=zs&age=24
    • params参数:即路径参数,属于路径中的一部分(/1),注意在配置路由的时候,需要占位
    • query参数:即查询参数,不属于路径中的一部分(?后面的参数),不需要占位
    • 二者都挂载在参数对象$route

代码示例

  • 路由配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 配置路由
export default new VueRouter({
// 配置路由
routes: [
{
// 注意使用方式三进行路由跳转时,需要给该条路由起一个名字
name: 'search',
// 使用 : 接收动态路由的路径参数
path: '/search/:keyword',
component: Search,
meta: {show: true}
}
]
})
  • Header中搜索框
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
<header class="header">
<!--头部第二行 搜索区域-->
<div class="bottom">
<h1 class="logoArea">
<router-link class="logo" to="/home">
<img src="./images/logo.png" alt="" />
</router-link>
</h1>
<div class="searchArea">
<form action="###" class="searchForm">
<input
type="text"
id="autocomplete"
class="input-error input-xxlarge"
v-model="keyword"
/>
<button
class="sui-btn btn-xlarge btn-danger"
type="button"
@click="goSearch"
>
搜索
</button>
</form>
</div>
</div>
</header>
</template>

<script>
export default {
data() {
return {
keyword: "",
};
},
methods: {
// 搜索按钮的回调,向search路由跳转
goSearch() {
// 例如向 /search/abc?k=ABC 跳转
// 方式1:纯字符串
// this.$router.push("/search/" + this.keyword + "?k=" + this.keyword.toUpperCase());

// 方式2:模板字符串
// this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`);

// 方式3:对象形式 常用
this.$router.push({
// 注意:这里不是path属性,而是name属性:要跳转的路由的名称
// path: "/search" 错误的写法
name: "search",
// kv键值对
params: {
keyword: this.keyword,
},
// kv键值对
query: {
k: this.keyword.toUpperCase(),
},
});
},
},
};
</script>
  • Search组件:展示用
1
2
3
4
5
6
<template>
<div>
<h1>params参数---{{ $route.params.keyword }}</h1>
<h1>query参数---{{ $route.query.k }}</h1>
</div>
</template>

image-20220814151314247

相关问题

  • 如何指定params不传送?

例如从/home跳转到/search?k=v,不携带路径参数params

1
2
3
4
5
6
7
{
name: 'search',
// 加上一个 ? 即可,表示params可传可不传
path: '/search/:keyword?',
component: Search,
meta: {show: true}
}
  • params如果是空串?
1
2
3
4
5
6
7
8
this.push({
name: "search",
// 一个空串
params: "",
query: {
k: this.keyword.toUpperCase()
}
});

问题:跳转到/search/?k=ABC

解决:使用undefined

1
2
3
4
5
6
7
8
this.push({
name: "search",
// 一个空串
params: "" || undefined,
query: {
k: this.keyword.toUpperCase()
}
});
  • 路由组件能否传递props数据?

能,详见vue2学习笔记

常用函数写法:可以将params和query参数通过props传递给路由组件

1
2
3
4
5
6
7
8
9
10
11
12
{
name: 'search',
path: '/search/:keyword?',
component: Search,
meta: { show: true },
props:($route) => {
return {
keyword: $route.params.keyword,
k: $route.query.k
}
},
}

Search组件:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<h1>params参数---{{ $route.params.keyword }}---{{ keyword }}</h1>
<h1>query参数---{{ $route.query.k }}---{{ k }}</h1>
</div>
</template>

<script>
export default {
props: ["keyword", "k"]
}
</script>

2.3.6 重写push和replace方法

问题:编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误。该问题在vue3中已经得到了解决。

此外,各版本的vue对于声明式导航没有这类问题。

image-20220814171255687

原因:vue-router引入了promise

解决:通过给push方法传递相应的成功和失败的回调函数,可以捕获到当前的错误。

2.4 Home模块组件拆分

分析:略

2.4.1 三级联动组件

image-20220814173631746

三级联动组件:在Home、Search和Detail组件中出现,因此拆分并注册为为全局组件。好处:只需要注册一次,就可以在项目中的任意地方使用。

  • 入口文件
1
2
3
4
5
6
7
8
9
// ...

// 全局组件
import TypeNav from '@/pages/Home/TypeNav'

// 参数:[全局组件的名字, 哪一个组件]
Vue.component(TypeNav.name, TypeNav)

// ...
  • 三级联动组件:html和css代码略
1
2
3
4
5
6
<script>
export default {
// 给组件起一个名字
name: "TypeNav",
};
</script>
  • Home组件引用三级联动组件
1
2
3
4
5
6
<template>
<div>
<!-- 三级联动组件 -->
<TypeNav></TypeNav>
</div>
</template>

2.4.2 其余静态组件

拆分结果:

image-20220815101526643

  • Home.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
<!-- 三级联动组件 -->
<TypeNav></TypeNav>
<ListContainer></ListContainer>
<Recommend></Recommend>
<Rank></Rank>
<Like></Like>
<Floor></Floor>
<Brand></Brand>
</div>
</template>

<script>
// 引入其余的组件
import ListContainer from "@/pages/Home/ListContainer";
import Recommend from "@/pages/Home/Recommend";
import Rank from "@/pages/Home/Rank";
import Like from "@/pages/Home/Like";
import Floor from "@/pages/Home/Floor"
import Brand from "@/pages/Home/Brand"
export default {
components: {
ListContainer,
Recommend,
Rank,
Like,
Floor,
Brand,
},
};
</script>

其余代码略。

效果:

image-20220815101607409

image-20220815101617593

image-20220815101624774

3 项目开发

3.1 接口相关

3.1.1 Poatman接口测试

image-20220815102453140

3.1.2 axios二次封装

  • 为什么需要进行二次封装axios?
    • 请求拦截器:可以在发送请求之前处理一些业务
    • 响应拦截器:当服务器返回数据后,可以处理一些业务

安装

1
npm i axios@0.24.0 -s

一般在项目中会有一个文件夹/src/api,里面存放封装好的axios

代码示例

新建/api/request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// axios二次封装
import axios from "axios";

// 创建一个axios实例api
// api实质上就是axios,只不过稍微配置了一下
const api = axios.create({
// 基础路径,发送请求时,路径当中会出现api
baseURL: "/api",
// 请求超时时间5s
timeout: 5000
});

// 设置请求拦截器
api.interceptors.request.use((config) => {
// config:配置对象,对象里面有一个非常重要的属性header请求头
return config;
});

// 设置响应拦截器
api.interceptors.response.use((res) => {
// 成功的回调函数
return res.data;
}, (error) => {
// 失败的回调函数
console.log(error);
return Promise.reject(new Error("fail"));
});

// 对外暴露
export default api;

3.1.3 API接口统一管理

  • 项目很小:完全可以在组件的生命周期函数(created)中单独发送请求

  • 项目很大:将所有的接口调用api都写在一个js文件里,向外暴露各个请求的api

代码示例

新建/api/index.js

1
2
3
4
5
6
7
8
9
10
// 该模块:API进行统一管理
// 模块要发送请求时,从这个文件中统一调用接口
import api from "./request";

// 三级联动接口
// 接口:/api/product/getBaseCategoryList GET 无参数

// 发送请求
// 注意axios返回结果是Promise对象
export const reqCategoryList = () => api({url: "/product/getBaseCategoryList",method: "GET"});

跨域解决

通常有三种方案:JSONP,CROS和代理,本项目使用代理。

vue.config.js中:

1
2
3
4
5
6
7
8
9
10
module.exports = {
// 代理跨域
devServer: {
proxy: {
"/api": {
target: "http://gmall-h5-api.atguigu.cn",
},
},
}
}

测试

入口文件:

1
2
3
// 测试接口
import {reqCategoryList} from "@/api"
reqCategoryList();

测试结果:

image-20220815112700895

3.2 nprogress进度条

安装:

1
npm i nprogress@0.2.0 -s

在拦截器request.js中设置:(部分代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 引入进度条
import nprogress from "nprogress";
// 引入进度条的样式
import "nprogress/nprogress.css"

// 请求拦截器
api.interceptors.request.use((config) => {
// 进度条开始动
nprogress.start();
return config;
});

// 响应拦截器
api.interceptors.response.use((res) => {
// 进度条结束
nprogress.done();
return res.data;
}, (error) => {
// 失败的回调函数
console.log(error);
return Promise.reject(new Error("fail"));
});

3.3 vuex模块式开发

vuex官网

vuex是官方提供的一个插件,状态管理库:集中式管理项目中组件共用的数据。

vuex模块式开发:不将所有组件的共享数据放置在总的index.js中,而是分模块放置数据。

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

结构

image-20220816100129555

代码示例

  • /store/home/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// home模块的仓库

// state 仓库存储数据的地方
const state = {};

// mutations 修改state的唯一手段
const mutations = {};

// action 处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {};

// getters 理解为计算属性
const getters = {};

export default {
state,
mutations,
actions,
getters
}
  • /store/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

// 引入各个模块的仓库
import home from './home'
import search from "./search";

// 对外暴露store类的实例
export default new Vuex.Store({
// 使用modules属性集成
modules: {
home,
search
}
});
  • 入口文件注册
1
2
3
4
5
6
7
8
9
10
// 引入vuex
import store from '@/store'

new Vue({
render: h => h(App),
// 注册仓库
// 当这里书写store的时候,组件身上都拥有$store属性
store
}).$mount('#app')

image-20220816101957622

3.4 三级联动导航开发

3.4.1 动态展示数据

代码示例

  • /store/home/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { reqCategoryList } from "@/api";
// home模块的仓库

const state = {
// 注意state的初始值形式由服务器返回的类型决定
// 如果服务器返回数组,则写成数组;返回对象就写成空对象
categoryList: []
};

const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList;
}
};

const actions = {
// 通过API里面的接口函数调用,向服务器发送请求,获取服务器的数据
async categoryList({ commit }) {
let res = await reqCategoryList();
console.log(res);
if (res.code === 200) {
// 触发mutation以修改state的数据
// 注意action里不能直接修改state的数据
commit("CATEGORYLIST", res.data);
}
}
};

其中,后端传过来的数据res的形式为:

image-20220816110041645

  • TypeNav组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<!-- 商品分类导航 -->
<div class="type-nav">
<div class="container">
<div class="sort">
<div class="all-sort-list2">
<div
class="item"
v-for="(c1, index) in categoryList"
:key="c1.categoryId"
>
<h3>
<a href="">{{ c1.categoryName }}</a>
</h3>
<div class="item-list clearfix">
<div
class="subitem"
v-for="(c2, index) in c1.categoryChild"
:key="c2.categoryId"
>
<dl class="fore">
<dt>
<a href="">{{ c2.categoryName }}</a>
</dt>
<dd>
<em
v-for="(c3, index) in c2.categoryChild"
:key="c3.categoryId"
>
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import { mapState } from "vuex";
export default {
name: "TypeNav",
// 组件挂载完毕时,向服务器发送请求获取数据
mounted() {
// 触发名为categoryList的action
this.$store.dispatch("categoryList");
},
computed: {
// mapState可以传入数据或对象
...mapState({
// 此处传入的是一个对象
// 右侧需要的是一个函数,当使用这个计算属性时,右侧的函数会立即执行一次
// 注入一个参数state,即为大仓库的数据
categoryList: (state) => state.home.categoryList,
}),
},
};
</script>

补充:mapState可以传入数据或对象

1
2
3
4
5
6
7
8
9
// mapState()可以传入对象或者数组
// 传入数组用法: mapState(['counter', 'name','age'])
// 传入对象用法:可以重命名store中的数据
computed: {
mapState({
sCounter: state => state.name,
// ......
})
}

3.4.2 动态背景颜色

  • 方式一:采用样式来完成

  • 方式二:通过js完成

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<template>
<!-- 商品分类导航 -->
<div class="type-nav">
<div class="container">
<div @mouseleave="leaveIndex">
<h2 class="all">全部商品分类</h2>
<!-- 一级分类 -->
<div class="sort">
<div class="all-sort-list2">
<div
class="item"
v-for="(c1, index) in categoryList"
:key="c1.categoryId"
:class="{ cur: currentIndex == index }"
>
<h3 @mouseenter="changeIndex(index)">
<a href="">{{ c1.categoryName }} --- {{ index }}</a>
</h3>
<!-- 二三级分类 -->
<div class="item-list clearfix">
<div
class="subitem"
v-for="c2 in c1.categoryChild"
:key="c2.categoryId"
>
<dl class="fore">
<dt>
<a href="">{{ c2.categoryName }}</a>
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
<nav class="nav">
<a href="###">服装城</a>
<a href="###">美妆馆</a>
<a href="###">尚品汇超市</a>
<a href="###">全球购</a>
<a href="###">闪购</a>
<a href="###">团购</a>
<a href="###">有趣</a>
<a href="###">秒杀</a>
</nav>
</div>
</div>
</template>

<script>
import { mapState } from "vuex";
export default {
name: "TypeNav",
data() {
return {
// 存储用户鼠标移动到哪一个一级分类
currentIndex: -1,
};
},
methods: {
// 鼠标进入时,修改currentIndex
changeIndex(index) {
this.currentIndex = index;
},
// 鼠标移除时的回调
leaveIndex() {
this.currentIndex = -1;
},
},
};
</script>

<style lang="less" scoped>
.cur {
background: skyblue;
}
</style>

3.4.3 子级分类的显示和隐藏

最开始的时候是采用css来控制display: bolck|none。下面利用js来实现:

代码示例

1
2
3
4
<!-- 二三级分类 -->
<div
class="item-list clearfix"
:style="{ display: currentIndex == index ? 'block' : 'none' }">

3.4.4 防抖与节流

① 概念
  • 节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,即把频繁触发变为少量触发
  • 防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行一次。
② 三级联动节流

利用lodash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
// 按需加载lodash的节流功能throttle
import throttle from "lodash/throttle";
export default {
name: "TypeNav",
methods: {
// 鼠标进入时,修改currentIndex
changeIndex: throttle(function (index) {
// 正常情况(用户慢慢的操作):鼠标进入,每一个一级分类h3都会触发鼠标进入事件
// 非正常情况(用户操作很快):只有部分h3事件触发了
// 原因:浏览器反应不过来,如果当前回调中有大量业务,可能出现卡顿现象
this.currentIndex = index;
}, 50),
},
};
</script>

3.4.5 路由跳转

分析:用户可以点击的:一二三级分类,当点击的时候,会从Home模块跳转到Search模块,一级会把用户选中的产品(产品名称和id)在路由跳转的时候进行传递。

注意:如果使用声明式导航router-link,可以实现路由的跳转与传递参数,但要注意会出现卡顿现象,原因在于它是一个组件,当服务器的数据返回后,循环出许多router-link组件,非常消耗内存。

解决:最好的方式是编程式导航+事件委派(把全部的子节点【h3,dt,dl,em】的事件委派给父亲结点,这样整个结构只有一个事件的回调,防止页面卡顿)

代码思路

1
2
<!-- 所有a标签最外层的标签 -->
<div class="all-sort-list2" @click="goSearch">
1
2
3
4
5
6
// 三级联动导航路由跳转
goSearch() {
// 最好的解决方案:编程式导航 + 事件委派
// 存在的问题:1.怎么确定点击的标签一定是a标签? 2.如何获取参数(1/2/3级分类的产品的名字)?
this.$router.push("/search");
},

代码示例

  • 如何确定点击的标签是否是a标签?

    • 在a标签上加上自定义属性data-categoryName,其余子节点没有
  • 如何传递路由参数(产品id和名称)?

    • 产品名称:利用上面的自定义属性data-categoryName
    • 产品id:新增自定义属性data-categoryid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 一级分类 -->
<a
:data-categoryName="c1.categoryName"
:data-category1Id="c1.categoryId"
>{{ c1.categoryName }}
</a>
<!-- 二级分类 -->
<a
:data-categoryName="c2.categoryName"
:data-category2Id="c2.categoryId"
>{{ c2.categoryName }}
</a>
<!-- 三级分类 -->
<a
:data-categoryName="c3.categoryName"
:data-category3Id="c3.categoryId"
>{{ c3.categoryName }}
</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
goSearch(e) {
// 最好的解决方案:编程式导航 + 事件委派
// 存在的问题:1.点击一定是a标签 2.如何获取参数(1/2/3级分类的产品的名字)

// 是哪一个标签触发了event?
let element = e.target;
// 获取当前触发这个事件的节点,需要带有data-categoryname这样的节点
// 节点有一个属性dataset,可以获取节点的自定义属性与属性值
let { categoryname, category1id, category2id, category3id } = element.dataset;
if (categoryname) {
// 整理路由跳转的参数
let location = { name: "search" };
let query = { categoryName: categoryname };
// 判断分类级别
if (category1id) {
query.category1id = category1id;
} else if (category2id) {
query.category2id = category2id;
} else {
query.category3id = category3id;
}
location.query = query;
this.$router.push(location);
}
},

3.5 Search模块开发

3.5.1 商品分类菜单

在search模块中,三级联动导航默认是折叠的,鼠标放上后出现菜单,鼠标移除时折叠菜单,因此需要在TypeNav组件上新增data属性show,默认为真。

首先在Search模块使用全局组件TypeNav:

1
2
3
4
5
<template>
<div>
<TypeNav></TypeNav>
</div>
</template>

TypeNav组件部分代码:

1
2
3
4
<!-- 鼠标进入和鼠标移除事件 -->
<div @mouseleave="leaveIndex" @mouseenter="enterShow">
<!-- 一级分类 -->
<div class="sort" v-show="show">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
export default {
name: "TypeNav",
data() {
return {
show: true,
};
},
methods: {
// 鼠标移除时的回调
leaveIndex() {
this.currentIndex = -1;
if (this.$route.path != "/home") {
this.show = false;
}
},
// 鼠标移入的时候,让商品分类列表进行展示
enterShow() {
if (this.$route.path != "/home") {
this.show = true;
}
},
},
// 组件挂载完毕时,向服务器发送请求获取数据
mounted() {
// 当组件挂载完毕,让show属性变为false
// 如果不是Home路由组件,则将TypeNav隐藏
if (this.$route.path != "/home") {
this.show = false;
}
},
};
</script>

3.5.2 分类菜过渡动画

1
2
3
4
<!-- 过渡动画 -->
<transition name="sort">
<div class="sort" v-show="show"></div>
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
// 过渡动画
// 进入
.sort-enter {
height: 0px;
}
// 结束
.sort-enter-to {
height: 461px;
}
// 时间和速率
.sort-enter-active {
transition: all .5s linear;
}
</style>