商品分类
首页头部分类导航交互
目的:实现点击的时候跳转,且能关闭二级分类弹窗。
描述:由于是单页面路由跳转不会刷新页面,css的hover一直触发无法关闭分类弹窗。
大致逻辑:
- 配置路由组件支持分类跳转
- 鼠标进入一级分类展示对应的二级分类弹窗
- 点击一级分类,二级分类,隐藏二级分类弹窗
- 离开一级分类,二级分类,隐藏二级分类弹窗
落地代码:
1) 配置路由和组件实现跳转
- 配置路由规则
src/router/index.js
+import TopCategory from '@/views/category'
+import SubCategory from '@/views/category/sub'
const routes = [
{
path: '/',
component: Layout,
children: [
{ path: '/', component: Home },
+ { path: '/category/:id', component: TopCategory },
+ { path: '/category/sub/:id', component: SubCategory }
]
}
]
- 创建分类组件
src/views/category/index.vue
<template>
<div>Top-Category</div>
</template>
<script>
export default {
name: 'TopCategory'
}
</script>
<style scoped lang="less"></style>
src/views/category/sub.vue
<template>
<div>Sub-Category</div>
</template>
<script>
export default {
name: 'SubCategory'
}
</script>
<style scoped lang="less"></style>
- 修改跳转的链接
<template>
<ul class="app-header-nav">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li :key='item.id' v-for='item in list'>
<RouterLink :to='"/category/" + item.id'>{{item.name}}</RouterLink>
<div class="layer">
<ul>
<li v-for="cate in item.children" :key="cate.id">
<RouterLink :to='"/category/sub/" + cate.id'>
<img :src="cate.picture" alt="">
<p>{{cate.name}}</p>
</RouterLink>
</li>
</ul>
</div>
</li>
</ul>
</template>
2)跳转后关闭二级分类弹窗
- 给每一个一级分类定义控制显示隐藏的数据,
open布尔类型,通过open设置类名控制显示隐藏。 - 当进入一级分类的时候,将open改为true
- 当离开一级分类的时候,将open改为false
- 点击一级分类,二级分类,将open改为false
在vuex种给一级分类加open数据 src/store/modules/category.js
async getCategory ({ commit }) {
const { result } = await findHeadCategory()
// 给一级分类加上一个控制二级分类显示隐藏的数据open
+ result.forEach(item => {
+ item.open = false
+ })
// 获取数据成功,提交mutations进行数据修改
commit('setCategory', result)
}
添加了 show hide vuex的mutations函数修改 open src/store/modules/category.js
// 修改当前一级分类下的open数据为true
show (state, item) {
const category = state.list.find(category => category.id === item.id)
category.open = true
},
// 修改当前一级分类下的open数据为false
hide (state, item) {
const category = state.list.find(category => category.id === item.id)
category.open = false
}
再 头部导航组件 实现显示和隐藏 src/components/app-header-nav.vue
import { useStore } from 'vuex'
import { computed } from 'vue'
export default {
name: 'AppHeaderNav',
setup () {
const store = useStore()
const list = computed(()=>{
return store.state.category.list
})
+ const show = (item) => {
+ store.commit('category/show', item)
+ }
+ const hide = (item) => {
+ store.commit('category/hide', item)
+ }
+ return { list, show, hide}
}
}
+ <li v-for="item in list" :key="item.id" @mouseenter="show(item)" @mouseleave="hide(item)">
+ <RouterLink :to="`/category/${item.id}`" @click="hide(item)">{{item.name}}</RouterLink>
<div class="layer" :class="{open:item.open}">
<ul>
<li v-for="sub in item.children" :key="sub.id">
+ <RouterLink :to="`/category/sub/${sub.id}`" @click="hide(item)">
<img :src="sub.picture" alt="">
<p>{{sub.name}}</p>
</RouterLink>
</li>
</ul>
</div>
</li>
- // > .layer {
- // height: 132px;
- // opacity: 1;
- // }
}
}
}
.layer {
+ &.open {
+ height: 132px;
+ opacity: 1;
+ }
总结: 在组件中调用vuex的mutation函数控制每个一级分类下二级分类的显示隐藏。
顶级类目-面包屑组件-初级
目的: 封装一个简易的面包屑组件,适用于两级场景。
大致步骤:
- 准备静态的
xtx-bread.vue组件 - 定义
props暴露parentPathparentName属性,默认插槽,动态渲染组件 - 在
library/index.js注册组件,使用验证效果。
落的代码:
- 基础结构
src/components/library/xtx-bread.vue
<template>
<div class='xtx-bread'>
<div class="xtx-bread-item">
<RouterLink to="/">首页</RouterLink>
</div>
<i class="iconfont icon-angle-right"></i>
<div class="xtx-bread-item">
<RouterLink to="/category/10000">电器</RouterLink>
</div>
<i class="iconfont icon-angle-right"></i>
<div class="xtx-bread-item">
<span>空调</span>
</div>
</div>
</template>
<script>
export default {
name: 'XtxBread'
}
</script>
<style scoped lang='less'>
.xtx-bread{
display: flex;
padding: 25px 10px;
&-item {
a {
color: #666;
transition: all .4s;
&:hover {
color: @xtxColor;
}
}
}
i {
font-size: 12px;
margin-left: 5px;
margin-right: 5px;
line-height: 22px;
}
}
</style>
- 定义props进行渲染
src/components/library/xtx-bread.vue
<template>
<div class='xtx-bread'>
<div class="xtx-bread-item">
<RouterLink to="/">首页</RouterLink>
</div>
<i class="iconfont icon-angle-right"></i>
+ <div class="xtx-bread-item" v-if="parentName">
+ <RouterLink v-if="parentPath" :to="parentPath">{{parentName}}</RouterLink>
+ <span v-else>{{parentName}}</span>
+ </div>
+ <i v-if="parentName" class="iconfont icon-angle-right"></i>
<div class="xtx-bread-item">
+ <span><slot /></span>
</div>
</div>
</template>
<script>
export default {
name: 'XtxBread',
+ props: {
+ parentName: {
+ type: String,
+ default: ''
+ },
+ parentPath: {
+ type: String,
+ default: ''
+ }
+ }
}
</script>
- 注册使用
src/components/library/index.js
+import XtxBread from './xtx-bread.vue'
export default {
install (app) {
+ app.component(XtxBread.name, XtxBread)
使用: <XtxBread parentPath="/category/1005000" parentName="电器">空调</XtxBread>
总结: 采用基本的封装手法,灵活度不是很高。
顶级类目-面包屑组件-高级
目的: 封装一个高复用的面包屑组件,适用于多级场景。认识 render 选项和 h 函数。
参考element-ui的面包屑组件:
大致步骤:
- 使用插槽和封装选项组件完成面包屑组件基本功能,但是最后一项多一个图标。
- 学习 render 选项,h 函数 的基本使用。
- 通过 render 渲染,h 函数封装面包屑功能。
落的代码:
- 我们需要两个组件,
xtx-bread和xtx-bread-item才能完成动态展示。
定义单项面包屑组件 src/components/library/xtx-bread-item.vue
<template>
<div class="xtx-bread-item">
<RouterLink v-if="to" :to="to"><slot /></RouterLink>
<span v-else><slot /></span>
<i class="iconfont icon-angle-right"></i>
</div>
</template>
<script>
export default {
name: 'XtxBreadItem',
props: {
to: {
type: [String, Object]
}
}
}
</script>
在 library/index.js注册
+import 'XtxBreadItem' from './xtx-bread-item.vue'
export default {
install (app) {
+ app.component(XtxBreadItem.name, XtxBread)
- 过渡版,你发现结构缺少风格图标,如果在item中加上话都会有图标,但是最后一个是不需要的。
<template>
<div class='xtx-bread'>
<slot />
</div>
</template>
<script>
export default {
name: 'XtxBread'
}
</script>
<!-- 去掉scoped全局作用 -->
<style lang='less'>
<!-- 面包屑 -->
<XtxBread>
<XtxBreadItem to="/">首页</XtxBreadItem>
<XtxBreadItem to="/category/1005000">电器</XtxBreadItem>
<XtxBreadItem >空调</XtxBreadItem>
</XtxBread>
顶级类目-面包屑组件-终极
- 终极版,使用render函数自己进行拼接创建。
createElement render render选项与h函数
- 指定组件显示的内容:
new Vue({选项})-
el 选项,通过一个选择器找到容器,容器内容就是组件内容
-
template 选项,
<div>组件内容</div>作为组件内容 -
render选项,它是一个函数,函数回默认传人createElement的函数(h),这个函数用来创建结构,再render函数返回渲染为组件内容。它的优先级更高。
-
//import App from './App.vue'
//new Vue({
// render:h=>h(App)
//}).mount('#app')
// h() =====> createElement()
// h(App) =====> 根据APP组件创建html结构
// render的返回值就是html结构,渲染到#app容器
// h() 函数参数,1.节点名称 2. 属性|数据 是对象 3. 子节点
xtx-bread-item.vue
<template>
<div class="xtx-bread-item">
<RouterLink v-if="to" :to="to"><slot /></RouterLink>
<span v-else><slot /></span>
- <i class="iconfont icon-angle-right"></i>
</div>
</template>
xtx-bread.vue
<script>
import { h } from 'vue'
export default {
name: 'XtxBread',
render () {
// 用法
// 1. template 标签去除,单文件组件
// 2. 返回值就是组件内容
// 3. vue2.0 的h函数传参进来的,vue3.0 的h函数导入进来
// 4. h 第一个参数 标签名字 第二个参数 标签属性对象 第三个参数 子节点
// 需求
// 1. 创建xtx-bread父容器
// 2. 获取默认插槽内容
// 3. 去除xtx-bread-item组件的i标签,因该由render函数来组织
// 4. 遍历插槽中的item,得到一个动态创建的节点,最后一个item不加i标签
// 5. 把动态创建的节点渲染再xtx-bread标签中
const items = this.$slots.default()
const dymanicItems = []
items.forEach((item, i) => {
dymanicItems.push(item)
if (i < (items.length - 1)) {
dymanicItems.push(h('i', { class: 'iconfont icon-angle-right' }))
}
})
return h('div', { class: 'xtx-bread' }, dymanicItems)
}
}
</script>
<style lang='less'>
// 去除 scoped 属性,目的:然样式作用到xtx-bread-item组件
.xtx-bread{
display: flex;
padding: 25px 10px;
// ul li:last-child {}
// 先找到父元素,找到所有的子元素,找到最后一个,判断是不是LI,是就是选中,不是就是无效选择器
// ul li:last-of-type {}
// 先找到父元素,找到所有的类型为li的元素,选中最后一个
&-item {
a {
color: #666;
transition: all .4s;
&:hover {
color: @xtxColor;
}
}
}
i {
font-size: 12px;
margin-left: 5px;
margin-right: 5px;
line-height: 22px;
// 样式的方式,不合理
// &:last-child {
// display: none;
// }
}
}
</style>
- 使用代码
<!-- 面包屑 -->
<XtxBread>
<XtxBreadItem to="/">首页</XtxBreadItem>
<XtxBreadItem to="/category/1005000">电器</XtxBreadItem>
<XtxBreadItem >空调</XtxBreadItem>
</XtxBread>
-
总结,一下知识点
-
render 是vue提供的一个渲染函数,优先级大于el,template等选项,用来提供组件结构。
-
注意:
-
vue2.0 render函数提供h(createElement)函数用来创建节点
-
vue3.0 h(createElement)函数有 vue 直接提供,需要按需导入
-
-
this.$slots.default() 获取默认插槽的node结构,按照要求拼接结构。
-
h函数的传参 tag 标签名|组件名称, props 标签属性|组件属性, node 子节点|多个节点
-
具体参考 render
-
-
注意:不要在 xtx-bread 组件插槽写注释,也会被解析。
顶级类目-批量注册组件
目的: 自动的批量注册组件。
大致步骤:
-
使用
require提供的函数context加载某一个目录下的所有.vue后缀的文件。 -
然后
context函数会返回一个导入函数importFn-
它又一个属性
keys()获取所有的文件路径
-
-
通过文件路径数组,通过遍历数组,再使用
importFn根据路径导入组件对象 -
遍历的同时进行全局注册即可
落的代码:
src/components/library/index.js
// 其实就是vue插件,扩展vue功能,全局组件、指令、函数 (vue.30取消过滤器)
// 当你在mian.js导入,使用Vue.use() (vue3.0 app)的时候就会执行install函数
// import XtxSkeleton from './xtx-skeleton.vue'
// import XtxCarousel from './xtx-carousel.vue'
// import XtxMore from './xtx-more.vue'
// import XtxBread from './xtx-bread.vue'
// import XtxBreadItem from './xtx-bread-item.vue'
// 导入library文件夹下的所有组件
// 批量导入需要使用一个函数 require.context(dir,deep,matching)
// 参数:1. 目录 2. 是否加载子目录 3. 加载的正则匹配
const importFn = require.context('./', false, /\.vue$/)
// console.dir(importFn.keys()) 文件名称数组
export default {
install (app) {
// app.component(XtxSkeleton.name, XtxSkeleton)
// app.component(XtxCarousel.name, XtxCarousel)
// app.component(XtxMore.name, XtxMore)
// app.component(XtxBread.name, XtxBread)
// app.component(XtxBreadItem.name, XtxBreadItem)
// 批量注册全局组件
importFn.keys().forEach(key => {
// 导入组件
const component = importFn(key).default
// 注册组件
app.component(component.name, component)
})
// 定义指令
defineDirective(app)
}
}
const defineDirective = (app) => {
// 图片懒加载指令 v-lazyload
app.directive('lazyload', {
// vue2.0 inserted函数,元素渲染后
// vue3.0 mounted函数,元素渲染后
mounted (el, binding) {
// 元素插入后才能获取到dom元素,才能使用 intersectionobserve进行监听进入可视区
// el 是图片元素 binding.value 图片地址
const observe = new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting) {
el.src = binding.value
// 取消观察
observe.unobserve(el)
}
}, {
threshold: 0.01
})
// 进行观察
observe.observe(el)
}
})
}
总结,知识点:
- require.context() 是webpack提供的一个自动导入的API
- 参数1:加载的文件目录
- 参数2:是否加载子目录
- 参数3:正则,匹配文件
- 返回值:导入函数 fn
-
keys() 获取读取到的所有文件列表
-
顶级类目-基础布局搭建
目的: 完成顶级分类的,面包屑+轮播图+所属全部子级分类展示。
大致步骤:
- 准备基础结构,获取轮播图数据给组件使用
- 获取面包屑和所有分类数据给子级分类展示使用
落的代码:
- 基本结构和轮播图渲染
src/views/category/index.vue
<template>
<div class="top-category">
<div class="container">
<!-- 面包屑 -->
<XtxBread>
<XtxBreadItem to="/">首页</XtxBreadItem>
<XtxBreadItem>空调</XtxBreadItem>
</XtxBread>
<!-- 轮播图 -->
<XtxCarousel :sliders="sliders" style="height:500px" />
<!-- 所有二级分类 -->
<div class="sub-list">
<h3>全部分类</h3>
<ul>
<li v-for="i in 8" :key="i">
<a href="javascript:;">
<img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/img/category%20(9).png" >
<p>空调</p>
</a>
</li>
</ul>
</div>
<!-- 不同分类商品 -->
</div>
</div>
</template>
<script>
import { findBanner } from '@/api/home'
export default {
name: 'TopCategory',
setup () {
// 轮播图
const sliders = ref([])
findBanner().then(data => {
sliders.value = data.result
})
return { sliders }
}
}
</script>
<style scoped lang="less">
.top-category {
h3 {
font-size: 28px;
color: #666;
font-weight: normal;
text-align: center;
line-height: 100px;
}
.sub-list {
margin-top: 20px;
background-color: #fff;
ul {
display: flex;
padding: 0 32px;
flex-wrap: wrap;
li {
width: 168px;
height: 160px;
a {
text-align: center;
display: block;
font-size: 16px;
img {
width: 100px;
height: 100px;
}
p {
line-height: 40px;
}
&:hover {
color: @xtxColor;
}
}
}
}
}
}
</style>
- 从vuex获取分类数据,进行渲染。
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
import { computed, ref } from 'vue'
// 面包屑+所有分类
const store = useStore()
const route = useRoute()
const topCategory = computed(() => {
let cate = {}
const item = store.state.category.list.find(item => {
return item.id === route.params.id
})
if (item) cate = item
return cate
})
return {
sliders,
topCategory,
}
<template>
<div class="top-category">
<div class="container">
<!-- 面包屑 -->
<XtxBread>
<XtxBreadItem to="/">首页</XtxBreadItem>
<XtxBreadItem>{{topCategory.name}}</XtxBreadItem>
</XtxBread>
<!-- 轮播图 -->
<XtxCarousel :sliders="sliders" style="height:500px" />
<!-- 所有二级分类 -->
<div class="sub-list">
<h3>全部分类</h3>
<ul>
<li v-for="item in topCategory.children" :key="item.id">
<a href="javascript:;">
<img :src="item.picture" >
<p>{{item.name}}</p>
</a>
</li>
</ul>
</div>
<!-- 不同分类商品 -->
</div>
</div>
</template>
顶级类目-分类商品-布局
目的: 展示各个子级分类下推荐的商品基础布局
大致步骤:
- 准备单个商品组件
- 完成推荐商品区块布局
落的代码:
- 商品信息组件
src/views/category/components/goods-item.vue
<template>
<RouterLink to="/" class='goods-item'>
<img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/fresh_goods_2.jpg" alt="">
<p class="name ellipsis">红功夫 麻辣小龙虾 19.99/500g 实惠到家</p>
<p class="desc ellipsis">火锅食材</p>
<p class="price">¥19.99</p>
</RouterLink>
</template>
<script>
export default {
name: 'GoodsItem'
}
</script>
<style scoped lang='less'>
.goods-item {
display: block;
width: 220px;
padding: 20px 30px;
text-align: center;
.hoverShadow();
img {
width: 160px;
height: 160px;
}
p {
padding-top: 10px;
}
.name {
font-size: 16px;
}
.desc {
color: #999;
height: 29px;
}
.price {
color: @priceColor;
font-size: 20px;
}
}
</style>
- 顶级分类组件,进行布局
src/views/category/index.vue
+import GoodsItem from './components/goods-item'
export default {
name: 'TopCategory',
components: {
+ GoodsItem
},
<!-- 分类关联商品 -->
<div class="ref-goods">
<div class="head">
<h3>- 海鲜 -</h3>
<p class="tag">温暖柔软,品质之选</p>
<XtxMore />
</div>
<div class="body">
<GoodsItem v-for="i in 5" :key="i" />
</div>
</div>
.ref-goods {
background-color: #fff;
margin-top: 20px;
position: relative;
.head {
.xtx-more {
position: absolute;
top: 20px;
right: 20px;
}
.tag {
text-align: center;
color: #999;
font-size: 20px;
position: relative;
top: -20px;
}
}
.body {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
padding: 0 65px 30px;
}
}
顶级类目-分类商品-展示
根据切换路由的时候,根据分类ID获取数据,渲染分类商品。
大致步骤:
- 定义API,组件初始化要去加载数据,但是动态路由不会重新初始化组件。
- 如果监听地址栏id的变化,然后变化了就去加载数据,但是初始化有不会加载了。
- 不过watch提供了 immediate: true 可让watch初始化的时候主动触发一次。
落的代码:
- 定义API
src/api/category.js
// 分类商品数据接口
export const findTopCategory = (id) => {
return request({
method: 'get',
url: '/category',
data: id
})
}
- 使用watch加载数据
src/views/category/index.vue
// 推荐商品
const subList = ref([])
const getSubList = () => {
findTopCategory(route.params.id).then(data => {
subList.value = data.result.children
})
}
watch(() => route.params.id, (newVal) => {
newVal && getSubList()
}, { immediate: true })
return {
sliders,
topCategory,
subList
}
- 开始渲染
src/views/category/index.vue
<!-- 分类关联商品 -->
<div class="ref-goods" v-for="item in subList" :key="item.id">
<div class="head">
<h3>- {{item.name}} -</h3>
<p class="tag">{{item.desc}}</p>
<XtxMore />
</div>
<div class="body">
<GoodsItem v-for="g in item.goods" :key="g.id" :goods="g" />
</div>
</div>
- 开始渲染
src/views/category/components/goods-item.vue
<template>
<RouterLink to="/" class='category-goods'>
<img :src="goods.picture" alt="">
<p class="name ellipsis">{{goods.name}}</p>
<p class="desc ellipsis">{{goods.tag}}</p>
<p class="price">¥{{goods.price}}</p>
</RouterLink>
</template>
<script>
export default {
name: 'CategoryGoods',
props: {
goods: {
type: Object,
default: () => {}
}
}
}
</script>
顶级类目-面包屑切换动画
目的: 由于切换顶级类目,面包屑文字瞬间完成,体验差,给切换的文字加上动画。
大致步骤:
- 给面包屑ITEM组件加上Transition组件并且创建 动画条件
- 定义动画css样式
落地代码:
- 加transition和name属性,以及加上key属性关联ID才会创建和移除。
<transition name="fade-right" mode="out-in">
<XtxBreadItem :key="currCategory.id">{{currCategory.name}}</XtxBreadItem>
</transition>
- 写动画样式
common.less做为公用
.fade-right-enter-to,
.fade-right-leave-from{
opacity: 1;
transform: none;
}
.fade-right-enter-active,
.fade-right-leave-active{
transition: all .5s;
}
.fade-right-enter-from,
.fade-right-leave-to{
opacity: 0;
transform: translate3d(20px,0,0);
}
**总结: ** 不同的key可以创建移除元素,创造触发动画条件。
二级类目-处理跳转细节
目的: 在路由跳转的时候,优化跳转的细节。
大致需求:
- 现在的路由跳转默认在当前浏览的位置(卷曲的高度),我们需要会到顶部。
- 在点击二级类目的时候,页面滚动到顶部,造成进入一级类名事件触发,显示其对应二级弹窗,需要处理。
- 切换到二级类目路由的时候也有ID,但是也触发了watch导致发送了请求,需要处理。
落的代码:
- 每次切换路由的时候滚动到顶部
src/router/index.js
const router = createRouter({
history: createWebHashHistory(),
routes,
+ scrollBehavior () {
+ return { left: 0, top: 0 }
+ }
})
- 滚动到顶部,鼠标有时候会进入一级类目上,触发弹出二级类目。改成在一级类目上移动弹出二级类目。
src/components/app-header-nav.vue
<li class="home"><RouterLink to="/">首页</RouterLink></li>
+ <li @mousemove="show(item)"
- 切换到二级类目路由的时候也有ID,但是也触发了watch导致发送了请求,需要处理。
src/views/category/index.vue
watch(() => route.params.id, (newVal) => {
- newVal && getSubList()
+ if (newVal && `/category/${newVal}` === route.path) getSubList()
}, { immediate: true })
总结:
- 控制路由跳转时,滚动条的位置:基于前端路由的配置选项
- mouseenter和mousemove的区别
- watchz侦听器路由参数的问题:不同的动态路由参数的变化都会被侦听到
总结
- 配置一级和二级路由组件和跳转
- 控制二级类目的显示和隐藏(需要修改吸顶组件的渲染条件)
- 面包屑导航组件封装
- 基础封装:组件封装需要酬去动态Props
- 高级封装:模仿Element-Ui的面包屑组件进行封装(封装两个组件;并使用插槽)
- 终极封装:基于Render函数定制插槽的模板(
**this.$slots.default()**) - 超级封装:基于
JSX进行重构封装
- 批量注册全局组件
**require.context**** **方法 - 一级分类的基本布局
- 基于计算属性获取一级分类的相关属性:面包屑名称和二级分类的数据
- 动态渲染分类相关的商品数据:监听路由参数的变化
**watch** - 二级分类的切换问题
- 控制滚动条的效果
- 悬停二级分类的问题
- 一级和二级分类的参数侦听问题:区分路由 路径:
**route.path**
二级类目-展示面包屑
目的:根据二级类目ID展示多级面包屑
大致思路:
- 封装一个独立的组件来完成,因为需要加动画。
- 使用组合API的方式通过计算属性得到所需数据
逻辑代码:
- 从vuex中通过计算属性得到面包屑所需数据
src/views/category/sub-bread.vue
<template>
<XtxBread>
<XtxBreadItem to="/">首页</XtxBreadItem>
<XtxBreadItem to="/category/123123">{{ info.fname }}</XtxBreadItem>
<XtxBreadItem>{{ info.sname }}</XtxBreadItem>
</XtxBread>
</template>
<script>
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
import { reactive, computed } from 'vue'
export default {
name: 'SubBread',
setup () {
// 需要根据当前的二级分类id找到二级分类的名称和所属的一级分类的名称
const store = useStore()
const route = useRoute()
const info = computed(() => {
const cateInfo = reactive({
// 一级分类名称
fname: '',
// 二级分类名称
sname: ''
})
store.state.cate.list.forEach(firstCate => {
// 获取一级分类的下面的所有二级分类,并且根据当前二级分类的id查询二级分类的名称
const secondCate = firstCate.children.find(item => {
return item.id === route.params.id
})
if (secondCate) {
// 获取二级分类的名称
cateInfo.sname = secondCate.name
// 获取此时的一级分类的名称
cateInfo.fname = firstCate.name
}
})
return cateInfo
})
return { info }
}
}
</script>
<style scoped lang="less"></style>
- 将该组件在
src/views/category/sub-bread.vue中使用
<template>
<div class='sub-category'>
<div class="container">
<!-- 面包屑 -->
<SubBread />
</div>
</div>
</template>
<script>
import SubBread from './components/sub-bread'
export default {
name: 'SubCategory',
components: { SubBread}
}
</script>
- 给面包屑导航添加动画
<Transition name='fade-right' mode='out-in'>
<XtxBreadItem :key='info.top.id' v-if='info.top' :to='"/category/" + info.top.id'>{{info.top.name}}</XtxBreadItem>
</Transition>
<Transition name='fade-right' mode='out-in'>
<XtxBreadItem :key='info.sub.id' v-if='info.sub'>{{info.sub.name}}</XtxBreadItem>
</Transition>
<script>
import { computed } from 'vue'
// cateInfo = { top: {id: 123, name: '电器}, sub: {id: 456, name: '电视'}}
const cateInfo = {}
// 数组的some方法也可以遍历数组,好处是可以终止遍历
// forEach无法终止遍历,用some替代forEach
store.state.cate.list.some(firstCate => {
// 获取一级分类的下面的所有二级分类,并且根据当前二级分类的id查询二级分类的名称
const secondCate = firstCate.children.find(item => {
return item.id === route.params.id
})
if (secondCate) {
// 获取二级分类的名称
cateInfo.sname = secondCate.name
cateInfo.sub = {
id: secondCate.id,
name: secondCate.name
}
// 获取此时的一级分类的名称
cateInfo.fname = firstCate.name
cateInfo.top = {
id: firstCate.id,
name: firstCate.name
}
// 找到我们需要的数据了,此时需要终止继续遍历
return true
}
</script>
<style scoped lang='less'>
.fade-right-enter-from,
.fade-right-leave-to {
transform: none;
opacity: 1;
}
.fade-right-enter-active {
transition: all 0.5s;
}
.fade-right-enter-to,
.fade-right-leave-from {
transform: translateX(40px);
opacity: 0;
}
</style>
二级类目-筛选区展示
目的:根据后台返回的筛选条件展示筛选区域。
大致步骤:
- 定义一个组件来展示筛选区域
- 获取数据进行品牌和属性的渲染
落地代码:
- 基础布局:
src/views/category/sub-filter.vue
<template>
<!-- 筛选区 -->
<div class="sub-filter">
<div class="item" v-for="i in 4" :key="i">
<div class="head">品牌:</div>
<div class="body">
<a href="javascript:;">全部</a>
<a href="javascript:;" v-for="i in 4" :key="i">小米</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SubFilter'
}
</script>
<style scoped lang='less'>
// 筛选区
.sub-filter {
background: #fff;
padding: 25px;
.item {
display: flex;
line-height: 40px;
.head {
width: 80px;
color: #999;
}
.body {
flex: 1;
a {
margin-right: 36px;
transition: all .3s;
display: inline-block;
&.active,
&:hover {
color: @xtxColor;
}
}
}
}
}
</style>
在 sub 组件使用
<template>
<div class='sub-category'>
<div class="container">
<!-- 面包屑 -->
<SubBread />
<!-- 筛选区 -->
+ <SubFilter />
</div>
</div>
</div>
</template>
<script>
import SubBread from './components/sub-bread'
+import SubFilter from './components/sub-filter'
export default {
name: 'SubCategory',
+ components: { SubBread, SubFilter}
}
</script>
- 获取数据:在地址栏二级类目ID改变的时候去加载筛选条件数据
src/api/category.js 定义API
/**
* 获取二级分类筛选条件数据
* @param {String} id - 二级分类ID
*/
export const findSubCategoryFilter = (id) => {
return request('/category/sub/filter', 'get', { id })
}
src/views/category/sub-filter.vue 获取数据,组装数据。
import { findSubCategoryFilter } from '@/api/category'
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'
export default {
name: 'SubFilter',
setup () {
// 1. 获取数据
// 2. 数据中需要全部选中,需要预览将来点击激活功能。默认选中全部
// 3. 完成渲染
const route = useRoute()
const filterData = ref(null)
const filterLoading = ref(false)
// 4. 分类发生变化的时候需要重新获取筛选数据,需要使用侦听器
watch(() => route.params.id, (newVal, oldVal) => {
// 当你从二级分类去顶级分类也会拿到ID,不能去加载数据因为它不是二级分类的ID
if (newVal && route.path === ('/category/sub/' + newVal)) {
filterLoading.value = true
newVal && findSubCategoryFilter(route.params.id).then(({ result }) => {
// 品牌全部
result.selectedBrand = null
result.brands.unshift({ id: null, name: '全部' })
// 销售属性全部
result.saleProperties.forEach(p => {
p.selectedProp = undefined
p.properties.unshift({ id: null, name: '全部' })
})
filterData.value = result
filterLoading.value = false
})
}
}, { immediate: true })
return { filterData, filterLoading }
}
}
添加高亮效果
<a @click='filterDate.brands.selectedBrand=attr.id'>
<a @click='item.selectedProp=attr.id'>
<script>
// 添加品牌的选中值的属性
data.result.brands.selectedBrand = null
// 销售属性全部
data.result.saleProperties.forEach(item => {
// 动态给每一种规则添加一个选中值的属性
item.selectedProp = null
item.properties.unshift({
id: null,
name: '全部'
})
})
</script>
渲染模板:src/views/category/sub-filter.vue 且加上骨架效果
<template>
<div class="sub-filter" v-if="filterData && !filterLoading">
<div class="item">
<div class="head">品牌:</div>
<div class="body">
<a :class="{active:filterData.selectedBrand===brand.id}" href="javasript:;" v-for="brand in filterData.brands" :key="brand.id">{{brand.name}}</a>
</div>
</div>
<div class="item" v-for="p in filterData.saleProperties" :key="p.id">
<div class="head">{{p.name}}:</div>
<div class="body">
<a :class="{active:p.selectedProp===attr.id}" href="javasript:;" v-for="attr in p.properties" :key="attr.id">{{attr.name}}</a>
</div>
</div>
</div>
<div v-else class="sub-filter">
<XtxSkeleton class="item" width="800px" height="40px" />
<XtxSkeleton class="item" width="800px" height="40px" />
<XtxSkeleton class="item" width="600px" height="40px" />
<XtxSkeleton class="item" width="600px" height="40px" />
<XtxSkeleton class="item" width="600px" height="40px" />
</div>
</template>
.xtx-skeleton {
padding: 10px 0;
}
二级类目-复选框组件封装
目的:实现一个自定义复选框组件。
大致步骤:
- 实现组件本身的选中与不选中效果
- 实现组件的v-model指令
- 改造成
@vueuse/core的函数写法
落地代码:
- 1)实现组件功能
[src/components/library/xtx-checkbox.vue](https://gitee.com/wzj1031/erabbit-128/blob/61ec54686b9e01b16b710d5dcfb0ce78c50c02c7/src/components/library/xtx-checkbox.vue)
<template>
<div class="xtx-checkbox" @click="changeChecked()">
<i v-if="checked" class="iconfont icon-checked"></i>
<i v-else class="iconfont icon-unchecked"></i>
<span v-if="$slots.default"><slot /></span>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'XtxCheckbox',
setup () {
const checked = ref(false)
const changeChecked = () => {
checked.value = !checked.value
}
return { checked, changeChecked }
}
}
</script>
<style scoped lang="less">
.xtx-checkbox {
display: inline-block;
margin-right: 2px;
.icon-checked {
color: @xtxColor;
~ span {
color: @xtxColor;
}
}
i {
position: relative;
top: 1px;
}
span {
margin-left: 2px;
}
}
</style>
- 2)实现双向绑定
-
vue3.0中v-model会拆解成 属性
modelValue和 事件update:modelValue
-
import { ref, watch } from 'vue'
// v-model ====> :modelValue + @update:modelValue
export default {
name: 'XtxCheckbox',
props: {
modelValue: {
type: Boolean,
default: false
}
},
setup (props, { emit }) {
const checked = ref(false)
const changeChecked = () => {
checked.value = !checked.value
// 使用emit通知父组件数据的改变
emit('update:modelValue', checked.value)
}
// 使用侦听器,得到父组件传递数据,给checked数据
watch(() => props.modelValue, () => {
checked.value = props.modelValue
}, { immediate: true })
return { checked, changeChecked }
}
}
- 3)补充
@vueuse/core的实现
import { useVModel } from '@vueuse/core'
// v-model ====> :modelValue + @update:modelValue
export default {
name: 'XtxCheckbox',
props: {
modelValue: {
type: Boolean,
default: false
}
},
setup (props, { emit }) {
// 使用useVModel实现双向数据绑定v-model指令
// 1. 使用props接收modelValue
// 2. 使用useVModel来包装props中的modelValue属性数据
// 3. 在使用checked.value就是使用父组件数据
// 4. 在使用checked.value = '数据' 赋值,触发emit('update:modelvalue', '数据')
const checked = useVModel(props, 'modelValue', emit)
const changeChecked = () => {
const newVal = !checked.value
// 通知父组件
checked.value = newVal
// 让组件支持change事件
emit('change', newVal)
}
return { checked, changeChecked }
}
}
总结: useVModel 工具函数可实现双向绑定。
二级类目-结果区-排序组件
目的:封装排序组件,完成排序切换效果
大致步骤:
- 定义一个组件
sub-sort,完成基础布局 - 在
sub.vue组件使用 - 完成切换排序时候的交换效果
落地代码:
- 1)基础布局:
src/views/category/components/sub-sort.vue
<template>
<div class='sub-sort'>
<div class="sort">
<a href="javascript:;">默认排序</a>
<a href="javascript:;">最新商品</a>
<a href="javascript:;">最高人气</a>
<a href="javascript:;">评论最多</a>
<a href="javascript:;">
价格排序
<i class="arrow up" />
<i class="arrow down" />
</a>
</div>
<div class="check">
<XtxCheckbox>仅显示有货商品</XtxCheckbox>
<XtxCheckbox>仅显示特惠商品</XtxCheckbox>
</div>
</div>
</template>
<script>
export default {
name: 'SubSort'
}
</script>
<style scoped lang='less'>
.sub-sort {
height: 80px;
display: flex;
align-items: center;
justify-content: space-between;
.sort {
display: flex;
a {
height: 30px;
line-height: 28px;
border: 1px solid #e4e4e4;
padding: 0 20px;
margin-right: 20px;
color: #999;
border-radius: 2px;
position: relative;
transition: all .3s;
&.active {
background: @xtxColor;
border-color: @xtxColor;
color: #fff;
}
.arrow {
position: absolute;
border: 5px solid transparent;
right: 8px;
&.up {
top: 3px;
border-bottom-color: #bbb;
&.active {
border-bottom-color: @xtxColor;
}
}
&.down {
top: 15px;
border-top-color: #bbb;
&.active {
border-top-color: @xtxColor;
}
}
}
}
}
.check {
.xtx-checkbox {
margin-left: 20px;
color: #999;
}
}
}
</style>
使用组件:src/views/category/sub.vue
<template>
<div class='sub-category'>
<div class="container">
<!-- 面包屑 -->
<SubBread />
<!-- 筛选区 -->
<SubFilter />
<!-- 结果区域 -->
+ <div class="goods-list">
+ <!-- 排序 -->
+ <SubSort />
+ <!-- 列表 -->
+ </div>
</div>
</div>
</template>
<script>
import SubBread from './components/sub-bread'
import SubFilter from './components/sub-filter'
+import SubSort from './components/sub-sort'
export default {
name: 'SubCategory',
+ components: { SubBread, SubFilter, SubSort}
}
</script>
<style scoped lang='less'>
+.goods-list {
+ background: #fff;
+ padding: 0 25px;
+ margin-top: 25px;
+}
</style>
- 2)交互效果:
<template>
<div class="sub-sort">
<div class="sort">
<a @click="handleSort()" :class="{active:sortParams.sortField===null}" href="javascript:;">默认排序</a>
<a @click="handleSort('publishTime')" :class="{active:sortParams.sortField==='publishTime'}" href="javascript:;">最新商品</a>
<a @click="handleSort('orderNum')" :class="{active:sortParams.sortField==='orderNum'}" href="javascript:;">最高人气</a>
<a @click="handleSort('evaluateNum')" :class="{active:sortParams.sortField==='evaluateNum'}" href="javascript:;">评论最多</a>
<a @click="handleSort('price')" href="javascript:;">
价格排序
<i class="arrow up" :class="{active:sortParams.sortField==='price'&&sortParams.sortMethod=='asc'}" />
<i class="arrow down" :class="{active:sortParams.sortField==='price'&&sortParams.sortMethod=='desc'}" />
</a>
</div>
<div class="check">
<XtxCheckbox v-model="sortParams.inventory">仅显示有货商品</XtxCheckbox>
<XtxCheckbox v-model="sortParams.onlyDiscount">仅显示特惠商品</XtxCheckbox>
</div>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'SubSort',
setup () {
// 排序相关的参数
const sortParams = reactive({
// sortField的取值由后端决定
sortField: null,
// 价格排序的顺序
sortMethod: null,
// 是否有货
inventory: false,
// 是否打折
onlyDiscount: false
})
// 点击排序
const handleSort = (type) => {
if (type === 'price') {
// 价格排序
if (sortParams.sortMethod === null) {
// 默认设置为降序排列
sortParams.sortMethod = 'desc'
} else {
// 本来有排序规则:把原来的排序进行取反处理即可
sortParams.sortMethod = sortParams.sortMethod === 'asc' ? 'desc' : 'asc'
}
}
// 非价格的其他排序方式
sortParams.sortField = type
}
return { sortParams, handleSort }
}
}
</script>
<style scoped lang="less">
.sub-sort {
height: 80px;
display: flex;
align-items: center;
justify-content: space-between;
.sort {
display: flex;
a {
height: 30px;
line-height: 28px;
border: 1px solid #e4e4e4;
padding: 0 20px;
margin-right: 20px;
color: #999;
border-radius: 2px;
position: relative;
transition: all 0.3s;
&.active {
background: @xtxColor;
border-color: @xtxColor;
color: #fff;
}
.arrow {
position: absolute;
border: 5px solid transparent;
right: 8px;
&.up {
top: 3px;
border-bottom-color: #bbb;
&.active {
border-bottom-color: @xtxColor;
}
}
&.down {
top: 15px;
border-top-color: #bbb;
&.active {
border-top-color: @xtxColor;
}
}
}
}
}
.check {
.xtx-checkbox {
margin-left: 20px;
color: #999;
}
}
}
</style>
二级类目-结果区-数据加载
目的:实现结果区域商品展示。
大致步骤:
- 完成结果区域商品布局
- 完成
xtx-infinite-loading组件封装 - 使用
xtx-infinite-loading完成数据加载和渲染
落地代码:src/views/category/sub.vue
- 基础布局
<template>
<div class='sub-category'>
<div class="container">
<!-- 面包屑 -->
<SubBread />
<!-- 筛选区 -->
<SubFilter />
<!-- 结果区域 -->
<div class="goods-list">
<!-- 排序 -->
<SubSort />
<!-- 列表 -->
<ul>
<li v-for="i in 20" :key="i" >
<GoodsItem :goods="{}" />
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import SubBread from './components/sub-bread'
import SubFilter from './components/sub-filter'
import SubSort from './components/sub-sort'
import GoodsItem from './components/goods-item'
export default {
name: 'SubCategory',
components: { SubBread, SubFilter, SubSort, GoodsItem }
}
</script>
<style scoped lang='less'>
.goods-list {
background: #fff;
padding: 0 25px;
margin-top: 25px;
ul {
display: flex;
flex-wrap: wrap;
padding: 0 5px;
li {
margin-right: 20px;
margin-bottom: 20px;
&:nth-child(5n) {
margin-right: 0;
}
}
}
}
</style>
- 无限列表加载组件
src/components/xtx-infinite-loading.vue
<template>
<div class="xtx-infinite-loading" ref="container">
<div class="loading" v-if="loading">
<span class="img"></span>
<span class="text">正在加载...</span>
</div>
<div class="none" v-if="finished">
<span class="img"></span>
<span class="text">亲,没有更多了</span>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
export default {
name: 'XtxInfiniteLoading',
props: {
loading: {
type: Boolean,
default: false
},
finished: {
type: Boolean,
default: false
}
},
setup (props, { emit }) {
const container = ref(null)
useIntersectionObserver(
container,
([{ isIntersecting }], dom) => {
if (isIntersecting) {
if (props.loading === false && props.finished === false) {
emit('infinite')
}
}
},
{
threshold: 0
}
)
return { container }
}
}
</script>
<style scoped lang='less'>
.xtx-infinite-loading {
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
.img {
width: 50px;
height: 50px;
background: url(../../assets/images/load.gif) no-repeat center / contain;
}
.text {
color: #999;
font-size: 16px;
}
}
.none {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
.img {
width: 200px;
height: 134px;
background: url(../../assets/images/none.png) no-repeat center / contain;
}
.text {
color: #999;
font-size: 16px;
}
}
}
</style>
- 定义获取数据的API
src/api/category.js
// 二级分类下商品的筛选接口
export const findSubCategoryGoods = (data) => {
return request({
method: 'post',
url: '/category/goods/temporary',
data
})
}
- 在
src/views/category/sub.vue使用组件
<!-- 结果区域 -->
<div class="goods-list">
<!-- 排序 -->
<GoodsSort />
<!-- 列表 -->
<ul>
<li v-for="item in list" :key="item.id" >
<GoodsItem :goods="item" />
</li>
</ul>
<!-- 加载 -->
+ <XtxInfiniteLoading :loading="loading" :finished="finished" @infinite="getData" />
</div>
<script>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import SubBread from './components/sub-bread.vue'
import SubFilter from './components/sub-filter.vue'
import SubSort from './components/sub-sort.vue'
import GoodsItem from './components/goods-item.vue'
import { findSubCategoryGoods } from '@/api/cate.js'
export default {
name: 'SubCategory',
components: {
SubBread,
SubFilter,
SubSort,
GoodsItem
},
setup () {
// const job = ref(true)
const route = useRoute()
const loading = ref(false)
const finished = ref(false)
const list = ref([])
// 查询参数
const reqParams = {
page: 1,
pageSize: 20
}
// 调用接口获取下一页数据
const getData = () => {
// 接口正在调用
loading.value = true
// 请求参数需要添加二级分类id
reqParams.categoryId = route.params.id
findSubCategoryGoods(reqParams).then(data => {
list.value.push(...data.result.items)
// 获取数据后,重置加载状态
loading.value = false
// 页码需要继续累加
reqParams.page++
// 是否已经全部加载完成
if (data.result.counts === list.value.length) {
// 没有更多数据了
finished.value = true
}
})
}
// 侦听二级分类的切换操作
watch(() => route.params.id, (newId) => {
// \排除一级分类的参数变化
if (newId && route.path === ('/category/sub/' + newId)) {
// 清空数据即可
list.value = []
// 重新加载第一页数据
reqParams.page = 1
// 重置完成的状态
finished.value = false
}
})
return { getData, loading, finished, list }
}
}
二级类目-结果区-进行筛选
目的:在做了筛选和排序的时候从新渲染商品列表。
大致步骤:
- 排序组件,当你点击了排序后 或者 复选框改变后 触发自定义事件
sort-change传出排序参数 - 筛选组件,当你改了品牌,或者其他筛选添加,触发自定义事件
filter-change传出筛选参数 - 在sub组件,分别绑定
sort-changefilter-change得到参数和当前参数合并,回到第一页,清空数据,设置未加载完成,触发加载。
- 排序规则变化
- 监听条件的触发动作:基于watch侦听即可
- 侦听到条件变化后,把最新的参数传递给父组件
- 父组件获取到这些变化的参数后,添加到接口调用的参数中
- 清空重置列表相关数据即可(加载更多的组件会进入可视区,触发接口调用,调用时就会采用最新的参数)
// 侦听排序相关参数的变化
watch(sortParams, (newValue) => {
// 把子组件选中的数据传递给父组件
emit('change-sort', newValue)
})
<!-- 排序组件 -->
<SubSort @change-sort='changeSortParams' />
// 接收排序组件传递过来的参数
const changeSortParams = (sortParams) => {
// ES6的方法Object.assign,用于合并两个对象中的属性(浅拷贝)
// 把sortParams对象中的所有属性放到reqParams对象里面
Object.assign(reqParams, sortParams)
// 重置数据即可(只要数据置空,加载更多的组件就会进入可视区,那么就触发load数据)
list.value = []
reqParams.page = 1
loading.value = false
finished.value = false
}
- 筛选条件变化
- 获取选中的筛选条件值
- 把选中的品牌和规格的参数传递给父组件
- 父组件接收到值后,合并到请求参数中,重置列表相关数据即可(接口自动调用)
// 最终选中品牌
const brandId = ref(-1)
// 最终选中的规格
const conditionArr = ref([])
const selectBrand = (id) => {
brandId.value = id
filterParams.value.brands.selectedBrand = id
// 把选中的参数传递到父组件
emit('change-filter', {
brandId: brandId.value,
arttrs: conditionArr.value
})
}
// 最终选中的规格
const selectProps = (item, attr) => {
// 设置选中的样式
item.selectedProp = attr.id
// 单个条件参数
const param = {
groupName: item.name,
propertyName: attr.name
}
// 判断是否已经存在这个数据了
const flag = conditionArr.value.some(data => {
if (data.groupName === item.name) {
// 已经存在了,更新选中的值
data.propertyName = attr.name
// 终止继续遍历
return true
}
})
if (!flag) {
// 不存在
conditionArr.value.push(param)
}
// 把选中的参数传递到父组件
emit('change-filter', {
brandId: brandId.value,
arttrs: conditionArr.value
})
}
XTXR\src\views\category\sub.vue
<SubFilter @change-filter='changeFilterParams' />
// 接收筛选条件传递过来的参数
const changeFilterParams = (filterParams) => {
// ES6的方法Object.assign,用于合并两个对象中的属性(浅拷贝)
// 把sortParams对象中的所有属性放到reqParams对象里面
Object.assign(reqParams, filterParams)
// 重置数据即可(只要数据置空,加载更多的组件就会进入可视区,那么就触发load数据)
list.value = []
reqParams.page = 1
loading.value = false
finished.value = false
}
关于数据的响应式补充
- Vue2中数据的响应式
注意:组件的data选项中提取定义好的数据才具备响应式能力
const vm = new Vue({
data: {
info: 'hello',
obj: {
abc: 123
}
}
})
// msg是后续添加的属性,不具备响应式能力
vm.obj.msg = 'nihao'
// 如果我们希望后续添加的属性具有响应式能力,那么需要按照如下方式处理
// 动态添加的响应式数据,需要添加到响应式对象中
Vue.set(vm.obj, 'msg', 'nihao')
vm.$set(vm.obj, 'msg', 'nihao')
- Vue3中数据的响应式
注意:如果涉及数据的侦听,需要保证被侦听的数据提前定义为响应式类型的
const myInfo = reactive({
abc: {}
})
// myInfo.abc中值的变化可以侦听到
watch(myInfo, (newValue) => {
console.log(newValue)
})
// ---------------------------------
const filterParams = ref(null)
// 如果后续给filterParams初始化一个对象形式的数据,当对象中的数据发生变化时,侦听器无法侦听到
watch(filterParams, (newValue) => {
console.log(newValue)
})