最新接口请求地址
- http://gmall-h5-api.atguigu.cn/
- 示例: http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList
axios
二次封装
- 前端请求方法大概有这几种: XMLHttpRequest,fetch,JQuery,axios(推荐)
- 为什么要二次封装axios,为了'请求拦截器'和'响应拦截器'
- 请求拦截器: 可以在请求发送之前,处理一些业务
- 响应拦截器: 当服务器返回数据时,处理一些业务
- 安装 axios: npm install axios
- 二次封装配置
### src.api.request.js
import axios from 'axios'
const requests = axios.create({ // 创建一个 axios实例并配置
baseURL:'/api', // 以后发请求,无需再写'/api'路径
timeout:5000, // 超时5秒即视为请求失败
})
requests.interceptors.request.use((config)=>{
return config // config里面有一个headers请求头,是重要参数
})
requests.interceptors.response.user((res)=>{
return res.data
},(error)=>{
return Promise.reject(new Error('faile'))
})
export default requests // 最后导出
API统一管理
-
引入场景: 当组件很多而且请求也多的时候,比如服务器的路径变了,得去组件一个个修改请求路径,显得不方便.所以,进行统一的API管理方便以后维护
类似django的settings配置项,各个模块引入.当修改settings配置时,各个模块无需再修改
-
api
新建index.js
,把API的逻辑放在里面
### api.index.js
import requests from './request'
// 使用 reqCategoryList 变量来保存 匿名函数对象
// axios 返回的是一个 Promise对象
export const reqCategoryList = ()=>requests({url:'/product/getBaseCategoryList',method:'get'})
- 要向后端发请求,先配置
代理服务器
解决跨域问题
### vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 关闭eslint
lintOnSave:false,
devServer: {
// 以下三个配置项: 启动项目的时候,就自动打开浏览器
open:true,
host:'localhost',
port:8080,
proxy: {
"/api": {
// 服务器目的地址
target: "http://gmall-h5-api.atguigu.cn",
// 重写路径
// pathRewrite: {"^/api": ""}
// changeOrigin: true,
},
},
},
})
- 测试: 我们在
main.js
测试效果
### main.js
......
import {reqCategoryList} from './api'
var res = reqCategoryList()
console.log(res) // res保存的是一个Promise对象,里面包含后端发过来的数据
new Vue({
......
}).$mount('#app')
进度条nprogress
的使用
- 安装
npm install nprogress
- 使用: 在
请求开始
和请求结束
中运行
### api.request.js
import axios from 'axios'
import nprogress from 'nprogress' // 引入 nprogress
import 'nprogress/nprogress.css' // 引入 nprogress 样式(可以修改源码的进度条颜色)
const requests = axios.create({
......
})
requests.interceptors.request.use((config)=>{
nprogress.start(); // 开始发请求就显示进度条
return config
})
requests.interceptors.response.use((res)=>{
nprogress.done(); // 请求发送结束,进度条也结束
return res.data
},(error)=>{
......
})
export default requests
如果不引入
nprogress.css
,网页是没有进度条效果的只有在发请求的时候,才有进度条效果(比如路由组件之间切换的时候,是不会有进度条效果的)
VueX: N多组件之间,共享数据的仓库
- 如果组件很少,完全可以不用,可以使用
$bus
来搞定即可
### store.index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
actions: { // 接收组件传过来的数据,提交给mutations;也可以与后端交互
},
mutations: { // 加工数据
},
state: { // 存放数据的仓库
},
getters: { // 计算属性:简化运算
},
modules: {
}
})
- 简单测试(点击按钮,变量自加1)
### store.index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
actions: {
add({commit}){
commit('ADD') // 提交给mutations
}
},
mutations: {
ADD(state){
state.num++; // 自加1
}
},
state: {
num:1 // 测试数据
},
......
})
### Home.vue
......
<template>
<div id="">
......
<h1>当前num的值为: {{$store.state.num}}</h1>
<button type="button" @click="add">num自加</button> <!--绑定事件-->
</div>
</template>
<script>
......
export default {
name:'Home',
components:{
......
},
methods:{
add(){
this.$store.dispatch('add') // 分发
}
},
}
</script>
<style>
</style>
VueX
模块化分工(便于日后维护各个组件的数据)
- store.home.index.js(search一样的)
const actions = {}
const mutations = {}
const state = {}
const getters = {}
export default {
actions,
mutations,
state,
getters
}
- store.index.js(注册模块)
import Vue from 'vue'
import Vuex from 'vuex'
import home from './home/index.js'
import search from './search/index.js'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
home,
search
}
})
项目规范: 把TypeNav
扔到components
目录(全局组件一般丢这里)
main.js
重新导入路径
......
import TypeNav from '@/components/TypeNav' // 重新导入
......
new Vue({
......
}).$mount('#app')
三级联动
获取服务器
数据
- 当
typeNav
组件挂载完毕,就向服务器发请求,获取数据
### typeNav.index.vue
......
<script>
export default {
name:'TypeNav',
mounted(){
this.$store.dispatch('categoryList') // 进行事件分发
}
}
</script>
### store.home.index.js
import {reqCategoryList} from "@/api" // 导入之前封装好的api接口
const actions = { // 向后端发请求的逻辑,一定是在 actions处理
categoryList(){ // 事件处理
var res = reqCategoryList() // 发请求
console.log(res) // 接收
}
}
- 返回结果是一个 Promise()对象,缺点是数据不够直观
Promise {<pending>}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Object // data数据藏在这里...
- 直观的返回data可以这么写:
const actions = {
async categoryList(){ // 用 async 修饰 categoryList函数对象
var res = await reqCategoryList() // 用 await 修饰 Promise对象
console.log(res)
}
}
- 注意事项: async和await一定是成对出现的
- 返回结果如下:
{code: 200, message: '成功', data: Array(17), ok: true}
index.js
的完整逻辑如下
import {reqCategoryList} from "@/api"
const actions = {
async categoryList({commit}){
var res = await reqCategoryList()
if(res.code == 200){ // 响应成功,才提交本次数据
commit('CATEGORYLIST',res.data)
}
}
}
const mutations = { // 赋值
CATEGORYLIST(state,categoryList){
state.categoryList = categoryList
}
}
const state = {
categoryList:[] // 初始值请不要瞎写,后端返回的是[...],就写[]
}
const getters = {}
export default {
actions,
mutations,
state,
getters
}
components.TypeNav.index.vue
组件测试数据
<div class="type-nav">
{{categoryList}} <!--测试数据-->
......
<script>
import {mapState} from 'vuex' // 使用 mapState 简化代码
export default {
name:'TypeNav',
computed:{
...mapState({ // 解构的写法
categoryList:state=>state.home.categoryList // 返回 categoryList
})
},
mounted(){
this.$store.dispatch('categoryList')
}
}
</script>
components.TypeNav.index.vue
组件渲染数据
<template>
<!-- 商品分类导航 -->
<div class="type-nav">
<div class="container">
<h2 class="all">全部商品分类</h2>
<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 class="sort">
<div class="all-sort-list2">
<!--渲染第一层数据-->
<div class="item" v-for="(item1,index1) in categoryList" :key="item1.categoryId">
<h3>
<a href="">{{item1.categoryName}}</a>
</h3>
<div class="item-list clearfix">
<!--渲染第二层数据-->
<div class="subitem" v-for="(item2,index2) in item1.categoryChild" :key="item2.categoryId">
<dl class="fore">
<dt>
<a href="">{{item2.categoryName}}</a>
</dt>
<dd>
<!--渲染第三层数据-->
<em v-for="(item3,index3) in item2.categoryChild" :key="item3.categoryId">
<a href="">{{item3.categoryName}}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name:'TypeNav',
computed:{
...mapState({
categoryList:state=>state.home.categoryList
})
},
mounted(){
this.$store.dispatch('categoryList')
}
}
</script>
<style lang="less" scoped>
......
</style>
- 类似
三级联动
这种数据格式,套路如下
[
{
id:1
name:'xxx'
categoryChild:[...] // 继续上面的套路,包裹一个个对象,里面再包裹[]...
}
{...}
{...}
]
增加一级导航
背景色高亮样式
- 当鼠标移动到
一级导航
时,立马显示背景色高亮效果
- 解决方式1: 一行css代码搞定
### index.vue
......
.item:hover {
background-color: skyblue;
}
- 本项目主要为了练习
js
,所以这次我们使用js
来实现这个功能
### index.vue
<div class="item" v-for="(item1,index1) in categoryList" :key="item1.categoryId" :class="{cur:currentIndex==index1}"> <!--新增:class样式,条件判断-->
<!--鼠标进入||鼠标离开-->
<h3 @mouseenter="changeIndex(index1)" @mouseleave="leaveIndex">
......
<script>
import {mapState} from 'vuex'
export default {
name:'TypeNav',
data(){
return {
currentIndex:-1 // 准备数据,值为-1的时候,不等于任何索引
}
},
methods:{
changeIndex(index){
this.currentIndex = index // 当满足索引相同的时候,点亮元素的样式
},
leaveIndex(){
this.currentIndex = -1 // 鼠标离开的时候,取消样式
}
},
computed:{
......
mounted(){
......
}
}
</script>
<style>
.....
.cur {
background: skyblue; // 新增样式
}
</style>
-
修复小样式: 当鼠标从
一级目录
移动到全部商品分类
的时候,此时一级目录
应该维持高亮效果我们新增
div
元素,把之前的结构包裹进去,从而实现这一效果
<div class="type-nav">
<div class="container">
<div @mouseleave="leaveIndex"> <!--新增div结构,把鼠标离开事件丢这里-->
<h2 class="all">全部商品分类</h2>
<div class="sort">
......
<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>
功能: 通过js控制二,三级商品分类的显示与隐藏
- 最开始的时候,是通过CSS样式 display: block|none 显示与隐藏二,三级商品分类
现在注释掉这些样式,我们使用js来实现
.item-list {
display: none; // 默认隐藏
......
&:hover { // 鼠标移动过来就展示
.item-list {
display: block;
}
- 模仿前面的套路
......
<!--新增display取值的判断-->
<div class="item-list clearfix" :style="{display:currentIndex==index1?'block':'none'}">
<div class="subitem" v-for="(item2,index2) in item1.categoryChild" :key="item2.categoryId">
<dl class="fore">
浏览器的卡顿
现象
- 当用户操作'一级菜单',鼠标移动速度很快的时候,导致浏览器来不及反应,部分'一级菜单'的效果没有呈现出来
如果里面包含大量的业务逻辑,这种情况显然是不允许发生的
函数的防抖和节流
- 原理:
闭包
&&延时器
- 防抖: 频繁发生的事情,最终只发生一次(以最后一次点击为准)
- 节流: 频繁发生的事情,由多次变成少次(即减少次数,以最开始一次点击为准)
- 防抖: 就是回城,被打断就要重新来
- 节流: 就是技能CD,没好就用不了技能
- 防抖
demo
演示: 搜索框用户输入内容的时候,有两种发请求的场景
- 只要搜索框的内容一改变,立马发请求到后端(先演示这个)
- 等用户输入完内容,再向后端发请求(推荐)
- 效果: 输入框的内容一旦发生变化,马上触发函数体里面的逻辑
<!DOCTYPE html>
<html>
<head>
......
</head>
<body>
<p>
搜索内容: <input type="text" name="" id="" value="" />
</p>
<script type="text/javascript">
var inputElement = document.querySelector('input');
inputElement.oninput = function(){ // oninput事件,实时监听输入框内容的变化
console.log('输入框的内容被改变了')
}
</script>
</body>
</html>
- 处理
防抖
和节流
的逻辑,我们使用第三方库lodash
来完成,使用方式和JQuery
类似
<!DOCTYPE html>
<html>
<head>
......
<!--引入-->
<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
......
<script type="text/javascript">
console.log(_) // 使用方式,就是一个'_'
......
</script>
</body>
</html>
- 使用
_.debounce
实现防抖
功能
- 效果: 用户的不断的输入,但是过5s以后才会触发函数体的逻辑,即多次触发,实际只执行一次
<!DOCTYPE html>
<html>
<head>
......
<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<p>
搜索内容: <input type="text" name="" id="" value="" />
</p>
<script type="text/javascript">
var inputElement = document.querySelector('input');
inputElement.oninput = _.debounce(function(){ // 防抖
console.log('输入框的内容被改变了')
},5000) // 延迟时间为5s
</script>
</body>
</html>
节流demo
引入场景: 每点击一次按钮,变量就自加1(计数器实例)
- 效果: 每点击一次按钮,就执行一次加1逻辑
<!DOCTYPE html>
<html>
<head>
......
</head>
<body>
<h1>我是计数器:<span>0</span></h1>
<button type="button">点我加1</button>
<script type="text/javascript">
var spanElement = document.querySelector('span')
var buttonElement = document.querySelector('button')
var count = 0
buttonElement.onclick = function(){
count++;
spanElement.innerHTML = count;
}
</script>
</body>
</html>
- 使用
_.throttle
来控制节流
- 效果: 点击一次按钮以后,立即执行一次,5s内不管再点击多少次,都不会再被执行
<!DOCTYPE html>
<html>
<head>
......
<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<h1>我是计数器:<span>0</span></h1>
<button type="button">点我加1</button>
<script type="text/javascript">
var spanElement = document.querySelector('span')
var buttonElement = document.querySelector('button')
var count = 0
buttonElement.onclick = _.throttle(function(){ // 调用
count++;
spanElement.innerHTML = count;
console.log('自加1了')
},5000)
</script>
</body>
</html>
- 使用
_.debounce
来演示:一模一样的代码,只不过调用换成了_.debounce
- 效果1: 只要一直点击按钮,函数体逻辑是不会被触发的
- 效果2: 以最后一次点击为准,等待5s后再执行函数体逻辑
<!DOCTYPE html>
<html>
<head>
......
<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<h1>我是计数器:<span>0</span></h1>
<button type="button">点我加1</button>
<script type="text/javascript">
var spanElement = document.querySelector('span')
var buttonElement = document.querySelector('button')
var count = 0
buttonElement.onclick = _.debounce(function(){
count++;
spanElement.innerHTML = count;
console.log('自加1了')
},5000)
</script>
</body>
</html>
- 小结:
节流
和防抖
效果最终区别
- 节流: 用户单击按钮,马上执行一次,设置延迟时间,在延迟时间内,不会再触发逻辑
- 防抖: 用户单击按钮,以最后一次点击为准,设置延迟时间,等延迟时间到了,再执行逻辑(如果一直点击,无法触发逻辑直到最后一次点击)
把lodash
引入项目
### TypeNav.index.vue
......
<script>
......
import _ from 'lodash' // node_modules自带有,无需再npm安装
export default {
......
methods:{
// 不能再使用简写形式了
changeIndex:_.throttle(function(index){
this.currentIndex = index
console.log(index)
},50),
leaveIndex(){
......
}
},
computed:{
......
},
mounted(){
......
}
}
</script>
- 注意事项:
import _ from 'lodash'
这种形式相当于引入全部,最好的引入方式是按需引入
### TypeNav.index.vue
......
<script>
......
import throttle from 'lodash/throttle' // 引入
export default {
......
methods:{
changeIndex:throttle(function(index){ // 稍改
this.currentIndex = index
},50),
leaveIndex(){
this.currentIndex = -1
}
},
......
}
</script>
throttle(func)
最好写成function
的形式,使用箭头函数
会有this
指向问题
三级联动的跳转和传参
- 场景一: 点击
一级目录
的时候,url
是这样
http://sph-list.atguigu.cn/list.html?category1Id=1
- 场景二: 点击
二级目录
的时候,url
是这样
http://sph-list.atguigu.cn/list.html?category2Id=22
- 场景三: 点击
三级目录
的时候,url
是这样
http://sph-list.atguigu.cn/list.html?category3Id=178
搜索
的时候,url
是这样
..../categoryName=手机&categoryId=2
- 使用
rounter-link
跳转,可能出现卡顿现象
<router-link>是一个组件,当服务器数据返回以后,若循环出很多<router-link>组件,有可能一下子占用很多内存,造成卡顿
三级联动
的跳转,我们使用编程式导航
来实现,可以这弄
- 为每个<a>标签绑定跳转事件goSearch
### TypeNav.vue
......
<a @click="goSearch">{{item1.categoryName}}</a>
<a @click="goSearch">{{item2.categoryName}}</a>
<a @click="goSearch">{{item3.categoryName}}</a>
......
- 更好的解决办法是利用
事件委托
来实现
- 不再为每个a标签绑定跳转事件,而是委托给div块来处理
<div class="all-sort-list2" @click="goSearch">
......
</div>
- 面临的问题
- 在div块中,一定要点到a标签才能跳转
- 传参如何实现?(比如如何区分一级,二级,三级标签),实现类似这样的url: .../search?categoryName=电子书&category3Id=1
- 解决办法:
- 给a标签绑定两个自定义属性: <a :data-categoryName="item1.categoryName" :data-category1Id="item1.categoryId">{{item1.categoryName}}</a>
- 当用户点击div块时,获取元素,若该元素拥有 categoryname 属性,则继续判断 categoryId
最后构造路由参数传给"$router.push()"
### TypeNav.index.vue
......
<div class="all-sort-list2" @click="goSearch"><!--委托div块处理-->
......
<!--为一级,二级,三级目录绑定两个自定义属性 data-categoryName && data-category1Id-->
<a :data-categoryName="item1.categoryName" :data-category1Id="item1.categoryId">{{item1.categoryName}}</a>
......
<a :data-categoryName="item2.categoryName" :data-category2Id="item2.categoryId">{{item2.categoryName}}</a>
......
<a :data-categoryName="item3.categoryName" :data-category3Id="item3.categoryId">{{item3.categoryName}}</a>
......
<script>
......
methods:{
......
goSearch(event){
var element = event.target; // 获取用户点击的目标元素
// 解构获取四个变量值(注意全部是小写,而不是驼峰的命名方式,想用驼峰就不要用解构赋值)
let {categoryname, category1id, category2id, category3id} = element.dataset;
// 若存在 categoryname,则说明用户点击的肯定是a标签
if(categoryname){
var location = {name:"search"} // 构造路由参数
var query = {categoryName:categoryname}
if(category1id){ // 判断 categoryId
query.category1Id = category1id
}else if(category2id){
query.category2Id = category2id
}else{
query.category3Id = category3id
}
location.query = query; // 收集完数据,丢给push()
this.$router.push(location);
}
}
}
</script>
- 最终效果举例: ....../search?categoryName=notepad&category3Id=178
Search组件引入全局组件TypeNav
-
效果: 页面进入
Search
组件的时候,导航菜单
是不显示的,等鼠标移动进入,再展示菜单等鼠标离开菜单的时候,
导航菜单
重新被隐藏
- 挂载 TypeNav 组件的时候
mounted(){
this.$store.dispatch('categoryList')
if(this.$route.path != '/home'){ // 只有 home 页才一直展示"导航菜单"
this.show = false
}
}
<!-- <div @mouseleave="leaveIndex"> -->
<!--鼠标进入就展示,离开就隐藏-->
<div @mouseleave="leaveShow" @mouseenter="enterShow">
......
<div class="sort" v-show="show"> <!--使用布尔变量show来控制菜单的展示/隐藏-->
......
export default {
......
data(){
return {
......
show:true // 默认true
}
},
methods:{
......
enterShow(){
// 挂载的时候,show为false,当鼠标进来的时候,就变更为true
if(this.$route.path != '/home'){
this.show = true
}
},
leaveShow(){
this.currentIndex = -1; // 继承之前的逻辑
if(this.$route.path != '/home'){
this.show = false // 鼠标离开的时候,就隐藏(除了home页)
}
},
- 为
一级导航菜单
添加动画效果: 使用transition name=xxx
- 使用动画的前提条件:
组件
或者元素
必须包含v-show/if
指令
### TypeNav
......
<div @mouseleave="leaveShow" @mouseenter="enterShow">
......
<transition name="sort"> <!--使用<transition name="sort">包裹起来-->
<div class="sort" v-show="show">
</transition>
</div>
......
.sort-enter{ // 开始的状态(进入)
height: 0px;
}
.sort-enter-to{ // 结束的状态(进入)
height: 461px;
}
.sort-enter-active{ // 动画的时间,速率
transition: all .5s linear;
}
- 还可以用动画的
旋转效果
.sort-enter{
height: 0px;
transform: rotate(0deg); // 开始旋转0度
}
.sort-enter-to{
height: 461px;
transform: rotate(360deg); // 后面旋转360度
}
.sort-enter-active{
transition: all .5s linear;
}
请求次数的优化
- 当
TypeNav
组件挂载完毕的时候,我们向后端发请求,获取导航条数据
......
mounted() {
this.$store.dispatch('categoryList') // 向后端发起请求
......
}
-
TypeNav
组件在Search
组件也有用到,所以当页面跳到Search
组件的时候,会再发一次请求其实,我们只需要发一次请求就够了,怎么处理,
- 可以把数据丢到
浏览器
,但这其实也是一种请求 - 答案是放到'App.vue',
根组件
只会被执行一次,然后把数据保存在vuex仓库
里 - 为什么不能扔到
main.js
(也只执行一次),因为js文件
没有this.$store.dispatch
- 可以把数据丢到
### App.vue
......
<script type="text/javascript">
......
export default {
name:'App',
......
mounted(){
this.$store.dispatch('categoryList') // 发请求的逻辑丢这里
}
}
</script>
- 再测试效果: 数据只在加载Home页的时候请求一次,就不会再请求
query
和params
参数一起传
- 实现点击
typeNav
菜单和Header
搜索时,把query
和params
参数一起传(不单单只传一个)
### typeNav.vue
......
<div class="all-sort-list2" @click="goSearch"> <!--点击分类菜单-->
......
goSearch(event) {
var element = event.target;
let {
categoryname,
category1id,
category2id,
category3id
} = element.dataset;
if (categoryname) {
var location = {
name: "search"
}
var query = {
categoryName: categoryname
}
if (category1id) {
query.category1Id = category1id
} else if (category2id) {
query.category2Id = category2id
} else {
query.category3Id = category3id
}
if(this.$route.params){ // 增加 params参数判断
location.params = this.$route.params
}
location.query = query;
this.$router.push(location);
}
}
### Header.index.vue
......
<button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch"> <!--点击搜索-->
......
goSearch(){
let location = {name:'search',params:{keyword:this.keyword || undefined}}
if(this.$route.query){ // 增加 query 判断
location.query = this.$route.query
}
this.$router.push(location,()=>{},()=>{})
// this.$router.push({
// name:'search',
// params:{keyword:this.keyword},
// query:{k:this.keyword.toUpperCase()}
// },()=>{},()=>{})
}
mock.js
: 模拟后端发假数据
- 安装
npm install mockjs
- 配置注意事项
- mock.banner.json 文件中: 别留有空格(空格会导致跑不起来)
- 数据格式: [{...},{...},{...}] # 无需对外暴露
- public新建images目录,把需要的图片都丢里面(打包的时候会原封不动,要的就是这个效果)
### mock.mockServe.js
import Mock from "mockjs" // 导入 Mock
import banner from './banner.json' // webpack默认暴露 JSON数据,图片
import floor from './floor.json'
Mock.mock('/mock/banner',{code:200,data:banner}) // 构造请求对象: 路径+数据
Mock.mock('/mock/floor',{code:200,data:floor})
### main.js 中执行一次
......
import '@/mock/mockServe' // 把整个模块导进来
### api.mockAjax.js(配置请求: 把 request.js的逻辑拷贝过来,小改一下即可)
import axios from 'axios'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'
const requests = axios.create({ // 创建一个 axios实例并配置
baseURL:'/mock',
timeout:5000,
})
requests.interceptors.request.use((config)=>{
......
})
requests.interceptors.response.use((res)=>{
......
export default requests
### index.js
import requests from './request'
import mockRequests from './mockAjax'
export const reqCategoryList = ()=>requests({url:'/product/getBaseCategoryList',method:'get'})
export const reqGetBannerList = ()=>mockRequests.get('/banner') // 发请求
ListContainer
组件发送请求并接收数据
### ListContainer.index.vue
......
<script>
import {mapState} from 'vuex'
export default {
name: 'ListContainer',
mounted(){
this.$store.dispatch('getBannerList') // 分发
},
computed:{
...mapState({
bannerList:state=>state.home.bannerList // 接收数据
})
}
}
</script>
### store.home.index.js
import {reqCategoryList,reqGetBannerList} from "@/api" // 导入
const actions = {
async categoryList({commit}){
......
},
async getBannerList({commit}){
let res = await reqGetBannerList();
if(res.code == 200){
commit('GETBANNERLIST',res.data) // 提交数据
}
}
}
const mutations = {
CATEGORYLIST(state,categoryList){
......
},
GETBANNERLIST(state,bannerList){
state.bannerList = bannerList // 绑定数据
}
}
const state = {
categoryList:[],
bannerList:[] // 初始化数据
}
......
swiper
专门用来处理轮播图
(不管移动端还是PC端都适用)
- 官方网址
https://swiper.com.cn/
- demo演示(三个步骤)
- 引入
swiper css
和js
文件 - 引入
swiper
页面结构 - 生成
swiper
实例 - 注意事项: 如果
swiper
页面结构不完整(后面js渲染数据就有这种问题),是不会有轮播图
效果的
- 引入
<!DOCTYPE html>
<html>
<head>
......
<!--引入swiper css和js文件-->
<script src="js/swiper.min.js"></script>
<link rel="stylesheet" type="text/css" href="./css/swiper.min.css"/>
</head>
<body>
<div class="swiper"> <!--引入swiper页面结构-->
<div class="swiper-wrapper">
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- 如果需要滚动条 -->
<div class="swiper-scrollbar"></div>
</div>
</body>
<script>
// 生成 swiper 实例
var mySwiper = new Swiper ('.swiper', {
// direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
</script>
</html>
vue
引入swiper
- npm install swiper@5 // 项目使用的是5的版本
- main.js 引入全局 css 样式(组件使用的时候,就不必再导入css样式了)
......
import 'swiper/css/swiper.css'
- 组件中,引入js
......
<script>
......
import Swiper from 'swiper'
### ListContainer.index.vue
.....
<div class="center">
<!--banner轮播-->
<div class="swiper-container" ref="cur">
<!--swiper页面结构-->
<div class="swiper-wrapper">
<!--遍历bannerList-->
<div class="swiper-slide" v-for="(carousel,index) in bannerList" :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>
</div>
......
<script>
......
import Swiper from 'swiper' // 导入
export default {
name: 'ListContainer',
mounted() {
this.$store.dispatch('getBannerList')
setInterval(() => { // 放入计时器,延迟2s再展示
// 生成 swiper 实例
var mySwiper = new Swiper('.swiper-container', {
// direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
clickable: true // 点击小球也切换图片
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
// scrollbar: {
// el: '.swiper-scrollbar',
// },
})
}, 2000)
},
computed: {
...mapState({ // 数据源
bannerList: state => state.home.bannerList
})
}
}
</script>
- 注意事项: 为什么要把
swiper
实例丢到计时器
?
- 如果丢到 mounted()处理,由于涉及到v-for遍历数据,导致页面结构还不完整,new Swipe实例就不会生效
- mounted 的定义是,等组件挂载完毕才执行一系列逻辑(前提是没有涉及到类似v-for这种遍历)
- 使用
watch
来解决也有问题: 数据是发生变化了,但是v-for
的遍历结束了吗?没结束的话,页面结构依然不完整,所以swiper实例
化失败
watch:{
bannerList:{
handler(newVal,oldVal){
var mySwiper = new Swiper('.swiper-container', {
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
}
}
}
- 最佳解决方案:
watch + vm.$nextTick
- vm.$nextTick: 循环结束,数据被修改时候生效
- 最终解决方案
watch:{
bannerList:{
handler(newVal,oldVal){
this.$nextTick(()=>{ // 把生成swiper实例的逻辑,丢到 $nextTick的回调里(等页面结构循环加载完毕后,再渲染swiper实例)
var mySwiper = new Swiper('.swiper-container', {
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
})
}
}
}
Floor组件
的数据渲染
- 实现的套路,和
banner
类似 - 封装发送请求
### api.index.js
.....
export const reqFloorList = ()=>mockRequests.get('/floor')
- mock数据如下: list里面,两组对象
[
{
"id":"001",
"name":"家用电器",
"keywords":[
"节能补贴",
"4K电视",
"空气净化器",
"IH电饭煲",
"滚筒洗衣机",
"电热水器"
],
"imgUrl":"/images/floor-1-1.png",
"navList":[
{
"url":"#",
"text":"热门"
},
{
"url":"#",
"text":"大家电"
},
{
"url":"#",
"text":"生活电器"
},
{
"url":"#",
"text":"厨房电器"
},
{
"url":"#",
"text":"应季电器"
},
{
"url":"#",
"text":"空气/净水"
},
{
"url":"#",
"text":"高端电器"
}
],
"carouselList":[
{
"id":"0011",
"imgUrl":"/images/floor-1-b01.png"
},
{
"id":"0012",
"imgUrl":"/images/floor-1-b02.png"
},
{
"id":"0013",
"imgUrl":"/images/floor-1-b03.png"
}
],
"recommendList":Array[4],
"bigImg":"/images/floor-1-4.png"
},
Object{...}
]
store.home.index
处理数据
import {......reqFloorList} from "@/api" // 导入请求对象
const actions = {
async categoryList({commit}){
......
},
async getBannerList({commit}){
......
},
async getFloorList({commit}){
let res = await reqFloorList(); // 请求并获取数据
if(res.code == 200){
commit('GETFLOORLIST',res.data)
}
}
}
const mutations = {
CATEGORYLIST(state,categoryList){
......
},
GETBANNERLIST(state,bannerList){
......
},
GETFLOORLIST(state,floorList){
state.floorList = floorList // 存储数据
}
}
const state = {
......
floorList:[] // 初始化
}
......
- 在
轮播图
示例中,请求是在listContainer
中发送,然后数据返回来,再渲染,本次把数据放在父组件Home
中,数据由父传子
### home.index.vue
<template>
<div id="">
......
<!--list有两组数据,故 Floor组件会被复用两次,把对象floor通过list参数传过去-->
<Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"></Floor>
<!-- <Floor></Floor> -->
</div>
</template>
<script>
import ListContainer from '@/pages/Home/ListContainer'
import Recommend from '@/pages/Home/Recommend'
import Rank from '@/pages/Home/Rank'
import Floor from '@/pages/Home/Floor'
import Like from '@/pages/Home/Like'
import {mapState} from 'vuex'
// import {reqCategoryList} from '../../api/index.js'
export default {
name:'Home',
components:{
ListContainer,
Recommend,
Rank,
Floor,
Like
},
// mounted(){
// console.log(reqCategoryList);
// var res = reqCategoryList();
// console.log(res)
// }
methods:{
// add(){
// this.$store.dispatch('add')
// }
},
computed:{
...mapState({
floorList:state=>state.home.floorList
})
},
mounted(){
this.$store.dispatch('getFloorList')
}
// mounted(){
// console.log(this.$store)
// }
}
</script>
<style>
</style>
......
<script>
......
export default {
name:'Home',
......
mounted(){
this.$store.dispatch('getFloorList') // 请求分发
},
computed:{
...mapState({
floorList:state=>state.home.floorList // 接收数据
})
},
}
</script>
Floor组件
接收数据并渲染
<template>
<!--楼层-->
<div class="floor">
<div class="py-container">
<div class="title clearfix">
<h3 class="fl">{{list.name}}</h3> <!--渲染name-->
<div class="fr">
<ul class="nav-tabs clearfix">
<li class="active" v-for="(nav,index) in list.navList"> <!--渲染导航数据-->
<a :href="nav.url" data-toggle="tab">{{nav.text}}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane">
<div class="floor-1">
<div class="blockgary">
<ul class="jd-list">
<!--渲染关键字-->
<li v-for="(keyword,index) in list.keywords" :key="index">{{keyword}}</li>
</ul>
<img :src="list.bigImg" />
</div>
<div class="floorBanner">
<div class="swiper-container" ref="cur"> <!--小轮播图结构-->
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carousel,index) in list.carouselList" :key="carousel.id">
<img :src="carousel.imgUrl"> <!--轮播图数据-->
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</div>
<div class="split">
<span class="floor-x-line"></span>
<!--以下数据不能v-for,因为数据结构不一样-->
<div class="floor-conver-pit">
<img :src="list.recommendList[0]" />
</div>
<div class="floor-conver-pit">
<img :src="list.recommendList[1]" />
</div>
</div>
<div class="split center">
<img :src="list.bigImg" />
</div>
<div class="split">
<span class="floor-x-line"></span>
<div class="floor-conver-pit">
<img :src="list.recommendList[2]" />
</div>
<div class="floor-conver-pit">
<img :src="list.recommendList[3]" />
</div>
......
</template>
<script>
import Swiper from 'swiper'
export default {
name:'Floor',
props:['list'], // 接收数据
mounted(){
var mySwiper = new Swiper(this.$refs.cur, {
loop: true, // 循环模式选项
pagination: {
el: '.swiper-pagination',
clickable: true
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
}
}
</script>
轮播图
结构注意事项
- 第一次写轮播图的时候,是在当前组件内部发请求,动态渲染数据(数据需要从服务器返回来),所有需要watch+$nextTick
- 本例中,请求是父组件发的,子组件通过props接收,所以'轮播图结构'已经有了,可以写在mounted
- 组件之间的通信方式
review
- props: 父==>子组件通信
- 自定义事件: @on,@emit 子==>父
- 全局事件总线: $bus(全能)
- pubsub-js: vue中几乎不用(全能)
- 插槽
- vuex(一般用这个)
把轮播图
搞成全局组件
-
home页
有三个地方用到,其他组件也有可能用到 -
注意事项
- 只有 template 结构,样式一样,才能这样拆分出来
- 有时候会看到 结构一致,但是大小不一样,这是正常的,通过样式去控制一下就可以了,不要以为不能拆分
- 把
floor
组件的轮播图js逻辑
改造成listContainer
组件的轮播图js逻辑
- 注意事项: list的数据是父组件home传过来的,没有发生变化,所以单纯写watch是检测不到数据变化的
所以配置了 immediate为true,不管数据有没有发生变化,先立即执行一次,从而实现需求
<script>
export default {
name:'Floor',
......
watch:{
list:{
immediate:true, // 必须加
handler(){
this.$nextTick(()=>{
var mySwiper = new Swiper(this.$refs.cur, {
......
})
})
}
}
}
}
</script>
- 把
轮播图
搞成全局组件
,新建Carousel
组件
### components.Carousel.index.vue
<template>
<!--把结构拿过来-->
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carousel,index) 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>
import Swiper from 'swiper'
export default {
name:"Carousel", // 名字
props:['list'], // 接收父组件传过来的数据
watch:{ // 之前Floor组件的逻辑剪过来
list:{
immediate:true,
handler(){
this.$nextTick(()=>{
var mySwiper = new Swiper(this.$refs.cur, {
......
})
})
}
}
}
}
</script>
<style scoped>
</style>
main.js
注册
......
import Carousel from '@/components/Carousel' // 导入并注册
Vue.component(Carousel.name,Carousel)
......
- 各个组件引用(注意:服务器返回的数据可能不太一样,要精准定位数据源,否则数据渲染出错,导出图片出不来)
### listContainer.index.vue
<template>
<!--列表-->
<div class="list-container">
......
<div class="center">
<!--传bannerList过去即可-->
<Carousel :list="bannerList"></Carousel>
</div>
......
</div>
</template>
### Home.Floor.vue
......
<div class="floorBanner">
<!--传carouselList,轮播图的图片地址包含在里面-->
<Carousel :list="list.carouselList"></Carousel>
</div>
模块开发流程
- 先静态页面+拆分组件
- 发请求(API)
vuex
三连环组件
获取仓库数据,动态展示数据
Search
组件开发
-
先静态页面+拆分组件(现成的,因为之前拆分组件的动作已经演示过了,所以这次不再演示)
- 把文件拷贝到
Search
组件目录,替换原来的index.vue
- 把文件拷贝到
-
发请求
以及vuex
三连环
### api.index.js
......
import requests from './request'
......
export const reqGetSearchInfo = (params)=>requests({
url:'/list',
method:'post',
data:params // 提交给服务器的数据
})
- 可以在main.js中,测试请求是正常响应
......
import {reqGetSearchInfo} from '@/api'
console.log(reqGetSearchInfo({})) // 注意传入一个空对象"{}",不加这个参数,无法收到服务器数据(之前已定义的形参)
......
- 在Search组件分发请求
......
mounted(){
this.$store.dispatch('getSearchList',{})
},
- store.search.index.js 处理数据
import { reqGetSearchInfo } from "@/api" // 导入请求
const actions = {
async getSearchList({commit},params={}){ // 发送请求并接收数据
let res = await reqGetSearchInfo(params)
if(res.code == 200){ // 响应成功就提交
commit('GETSEARCHLIST',res.data)
}
}
}
const mutations = {
GETSEARCHLIST(state,searchList){
state.searchList = searchList // 最终将数据存储到store
}
}
const state = {
searchList:{}
}
const getters = {}
export default {
actions,
mutations,
state,
getters
}
- 渲染数据之前,先使用
getters
简化代码
### store.search.index.js
......
const actions = {
.......
}
const mutations = {
......
}
const state = {
searchList:{}
}
const getters = {
goodsList(state){ // state 是上面的 state, 不是全局的state
return state.searchList.goodsList || []
},
trademarkList(state){
return state.searchList.trademarkList || [] // 断网状态下,返回空[],避免报"attrs error"
},
attrsList(state){
return state.searchList.attrsList || []
}
}
export default {
......
}
- 组件使用
### search.index.vue
......
<script>
......
import {mapState,mapGetters} from 'vuex'
export default {
......
mounted(){
this.$store.dispatch('getSearchList',{})
},
computed:{
......
// 映射三个 getters
...mapGetters(['goodsList','trademarkList','attrsList'])
}
}
</script>
- 渲染到网页
......
<ul class="yui3-g">
<!--遍历-->
<li class="yui3-u-1-5" v-for="(good,index) in goodsList" :key="good.id">
<div class="list-wrap">
<div class="p-img"> <!--渲染图片-->
<a href="item.html" target="_blank"><img :src="good.defaultImg" /></a>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>{{good.price}}</i> <!--渲染价格-->
</strong>
</div>
<div class="attr">
<a target="_blank" href="item.html" title="促销信息,下单即赠送三个月CIBN视频会员卡!【小米电视新品4A 58 火爆预约中】">{{good.title}}</a> <!--渲染标题-->
</div>
<div class="commit">
<i class="command">已有<span>2000</span>人评价</i>
</div>
<div class="operate">
<a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
</div>
</div>
</li>
</ul>
search组件
根据不同的参数获取数据并展示
<script>
......
export default {
name: 'Search',
data(){
return {
searchParams: { // 新增数据对象,接收typeNav和'搜索框'传递过来的参数
// 一级分类的ID
"category1Id": "",
// 二级分类的ID
"category2Id": "",
// 三级分类的ID
"category3Id": "",
// 分类的名字
"categoryName": "",
// 关键字
"keyword": "",
// 排序 应该有初始值
"order": "",
// 当前分页
"pageNo": 1,
// 每一页所带的数据数
"pageSize": 10,
// 平台售卖属性操作所带的数据
"props": [],
// 品牌
"trademark": ""
}
}
},
components: {
......
},
beforeMount(){ // 组件挂载完毕之前,填充 searchParams 数据对象
// 复杂写法
// this.category1Id = this.$route.query.category1Id;
// this.category2Id = this.$route.query.category2Id;
// this.category3Id = this.$route.query.category3Id;
// this.categoryName = this.$route.query.categoryName;
// this.keyword = this.$route.params.keyword;
// 简便写法: Object.assign()用于合并对象
// 把query和params值 合并到 searchParams
Object.assign(this.searchParams,this.$route.query,this.$route.params)
},
methods:{
getData(){ // 打包成方法,方便其他模块随时调用
this.$store.dispatch('getSearchList',this.searchParams)
},
},
mounted(){ // 挂载完毕以后,带着参数派发请求
this.getData()
},
computed:{
......
}
}
</script>
Search子组件
数据的渲染(品牌,具体属性)
### SearchSelector.vue
<template>
<div class="clearfix selector">
<div class="type-wrap logo">
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list">
<!--渲染品牌数据-->
<li v-for="(item,index) in trademarkList" :key="item.tmId">{{item.tmName}}</li>
</ul>
</div>
<div class="ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
<a href="javascript:void(0);">更多</a>
</div>
</div>
<!--渲染属性-->
<div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.attrId">
<div class="fl key">{{attr.attrName}}</div>
<div class="fl value">
<ul class="type-list">
<!--渲染具体属性值-->
<li v-for="(attrValue,index) in attr.attrValueList" :key="index">
<a>{{attrValue}}</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
name: 'SearchSelector',
computed:{
...mapGetters(['trademarkList','attrsList']), // 映射仓库两组数据
}
}
</script>
监视路由的变化,路由一旦发生变化,再次发起请求
### search.index.vue
......
watch:{
$route(newVal,oldVal){
// 一旦路由发生变化,重新刷新 searchParams值
Object.assign(this.searchParams,this.$route.query,this.$route.params);
// 再次发起请求
this.getData();
// 请求完毕以后,清空原来的参数(否则有残留)
// undefined的好处在于,不会被打包到请求里面去,避免发送空参数给服务端(节省性能)
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
}
}
面包屑
导航的处理
### search.index.vue
......
<div class="bread">
<ul class="fl sui-breadcrumb">
<li>
<a href="#">全部结果</a>
</li>
</ul>
<ul class="fl sui-tag">
<!--有传categoryName就显示,没有传就不显示(比如"搜索"这种情况)-->
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}</li>
</ul>
</div>
- 当用户点击
x
的时候,清空查询参数,返回默认的页面
### search.index.vue
......
<!--绑定点击事件来处理-->
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">x</i></li>
......
methods:{
getData(){
......
},
removeCategoryName(){
// 先置空发送服务器的数据
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
// 在向服务器发送请求
this.getData();
// 地址栏也需要改 进行路由跳转 自己跳转自己
// if (this.$route.params) {
if (this.$route.query || this.$route.params) {
this.$router.push({name: "search", params: this.$route.params})
}
}
},
关键字
的面包屑处理(和三级导航目录
一样的套路)
......
<div class="bread">
......
<ul class="fl sui-tag">
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">x</i></li>
<!--一样的套路-->
<li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">x</i></li>
</ul>
</div>
......
methods:{
getData(){
......
},
removeCategoryName(){
......
},
removeKeyword(){
this.searchParams.keyword = undefined;
this.getData();
}
},
-
现存代码的问题: 当X掉重新发送请求以后,
搜索框
的keyword
依然存在,而实际需求必须清空keyword
关键字存在于兄弟组件Header.vue
,所以涉及到兄弟组件
的通信操作
- props: 父=>子
- 自定义事件:子=>父
- vuex: 万能
- 插槽: 父=>子
- pubsub-js: 万能(很少人用)
- $bus: 万能
- 本次使用
$bus
来实现,先绑定$bus
### main.js
......
new Vue({
......
beforeCreate(){
Vue.prototype.$bus = this; // 绑定$bus
}
}).$mount('#app')
- 项目应用: 当用户点击
keyword X
的时候,由Search组件
通知Header组件
清空keyword
### Search.vue
......
removeKeyword(){
this.searchParams.keyword = undefined;
this.getData();
this.$bus.$emit('clear') // 触发 clear 事件
}
### Header.vue
......
mounted(){
this.$bus.$on('clear',()=>{ // 响应clear事件,清空keyword
this.keyword = '';
})
}
- 最后,路由跳转自己,保留
query
参数
### Search.vue
......
removeKeyword(){
this.searchParams.keyword = undefined;
this.getData();
this.$bus.$emit('clear')
if (this.$route.query) { // 路由自己跳转自己
this.$router.push({name: "search", query: this.$route.query})
}
}
-
面包屑
处理品牌信息: 当用户点击品牌
的时候,下方的商品列表要展示对应的品牌商品涉及
子传父
通信,本次使用自定义事件
来处理
### SearchSelector.vue
......
<div class="value logos">
<ul class="logo-list">
<!--当用户点击品牌的时候,绑定事件-->
<li v-for="(item,index) in trademarkList" :key="item.tmId" @click="trackmarkHandler(item)">{{item.tmName}}</li>
</ul>
</div>
......
methods:{ // 把参数传给父组件,让父组件带着参数,发请求
trackmarkHandler(trackmark){
this.$emit('trackmarkInfo',trackmark)
},
......
},
### Search.vue
......
<!--响应子组件的自定义事件-->
<SearchSelector @trackmarkInfo="trackmarkInfo" />
......
methods:{
......
trackmarkInfo(trackmark){ // 接收参数并赋值
this.searchParams.trademark = `${trackmark.tmId}:${trackmark.tmName}` // trademark:'1:苹果'
this.getData();
}
},
品牌
的面包屑展示
### Search.vue
......
<ul class="fl sui-tag">
......
<li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">x</i></li>
<!--一样的套路-->
<li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(':')[1]}}<i @click="removeTrackmark">x</i></li>
</ul>
......
removeTrackmark(){
this.searchParams.trademark = undefined;
this.getData();
}
售卖属性
的操作
- 要求的数据格式
- props:["属性ID:属性值:属性名"]
- 示例: ["1:6~6.24英寸:屏幕尺寸"]
-
实现需求: 当用户点击
属性值
时,页面展示对应属性值的商品列表比如用户选择
运行内存:8G
,页面返回对应规格的手机商品 -
思路分析
- 把用户选择的商品属性参数,发给父组件,父组件自己填充searchParams参数,然后发请求获取相关商品属性的数据
### SearchSelector.vue
......
<div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.attrId">
<div class="fl key">{{attr.attrName}}</div>
<div class="fl value">
<ul class="type-list">
<li v-for="(attrValue,index) in attr.attrValueList" :key="index" @click="attrInfo(attr,attrValue)"> <!--绑定点击事件并传参-->
<a>{{attrValue}}</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
......
methods:{
......
attrInfo(attr,attrValue){
this.$emit('attrInfo',attr,attrValue) // 触发事件
}
},
### Search.index.vue
......
<!--新增响应子组件事件-->
<SearchSelector @trackmarkInfo="trackmarkInfo" @attrInfo="attrInfo"/>
......
methods:{
......
attrInfo(attr,attrValue){
// 构造str数据,插入props列表项,最后发请求
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`
this.searchParams.props.push(props)
this.getData();
}
},
售卖属性
的面包屑
展示和删除
### Search.index.vue
......
......
<ul class="fl sui-tag">
......
<li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(':')[1]}}<i @click="removeTrackmark">x</i></li>
<!--不再使用v-if,而是使用v-for处理,要展示的属性值很多(空list是不会展示的)-->
<!--所以使用v-for就具有v-if的功能,而且实现了遍历-->
<!--同时绑定点X事件-->
<li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{{attrValue.split(':')[1]}}<i @click="removeAttrValue(index)">x</i></li>
</ul>
......
......
attrInfo(attr,attrValue){ // 面包屑展示
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`
// 增加'数组去重'的判断,重复的元素不再展示
if(this.searchParams.props.indexOf(props) == -1) this.searchParams.props.push(props)
this.getData(); // 不能把这句丢到上面的if里面,因为同一属性值的后端数据,可能更新
},
removeAttrValue(index){ // 面包屑删除
this.searchParams.props.splice(index,1); // 不要写成 slice,完全不一样
this.getData();
}