vue3项目-小兔鲜儿笔记-分类模块01
1.二级类目-筛选区展示
获取数据进行品牌和属性的渲染
<template>
<div class="sub-filter" v-if="filterData && !filterDataLoading">
<div class="item">
<div class="head">品牌:</div>
<div class="body">
<!-- 选中某个品牌 -->
<a
@click="changeBrand(brand.id)"
:class="{ active: brand.id === filterData.selectedBrand }"
href="javascript:;"
v-for="brand in filterData.brands"
:key="brand.id"
>
{{ brand.name }}
</a>
</div>
</div>
<div class="item" v-for="item in filterData.saleProperties" :key="item.id">
<div class="head">{{ item.name }}</div>
<div class="body">
<!-- 选中某个属性 -->
<a
@click="changeProp(item, prop.id)"
:class="{ active: prop.id === item.selectedProp }"
href="javascript:;"
v-for="prop in item.properties"
:key="prop.id"
>
{{ prop.name }}
</a>
</div>
</div>
</div>
<!-- 骨架屏处理等待 -->
<div class="sub-filter" v-else>
<xtx-skeleton class="item" width="800px" height="40px" />
<xtx-skeleton class="item" width="600px" height="40px" />
<xtx-skeleton class="item" width="600px" height="40px" />
<xtx-skeleton class="item" width="600px" height="40px" />
<xtx-skeleton class="item" width="600px" height="40px" />
</div>
</template>
逻辑处理:
主要部分是要记住选中的品牌和选中的属性,这两点很重要。
1. 原生数据缺失 ‘全部’ 属性,要在品牌和属性的最前面添加 ‘全部’
2.怎么才能记住选中的品牌和属性呢?
1.参考数据是:{brands:[{xxx}]},只需要在brands外层添加一个selectedBrand属性:{brands:[{xxx}], selectedBrand: xxx},这样查找时可以直接在外层filterData.value.selectedBrand找到。
2.参考数据是:每一个属性item:{name: xxx, properties: [{xxx}]},在每个属性的外层添加一个selectedProp属性:{name:xxx, selectedProp: xxx, properties:[{xxx}]},这样查找时直接在外层item.selectedProp找到。
3.选中筛选条件的时候,需要获取到选中的品牌和属性,并通知父组件筛选条件变更。
<script setup>
import { findSubCategoryFilter } from '@/api/category'
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'
const emit = defineEmits(['filterChange'])
const route = useRoute()
// 加载中
const filterDataLoading = ref(false)
// 获取二级分类筛选数据
const filterData = ref([])
const getSubCateFilter = async (id) => {
try {
filterDataLoading.value = true
const { result } = await findSubCategoryFilter(id)
// 原生数据缺失 全部 ,需要处理原生数据
// 1.品牌
// 在外层添加选中品牌,这样找brand的时候直接brands.find(brand=>brand.id===result.selectedBrand)
result.selectedBrand = null
result.brands.unshift({ id: null, name: '全部' })
// 2.属性
// 在外层添加选中属性,这样找prop的时候直接saleProperty.properties.find(prop=>prop.id===saleProperty.selectedProp)
result.saleProperties.forEach((item) => {
item.selectedProp = null
item.properties.unshift({ id: null, name: '全部' })
})
filterData.value = result
filterDataLoading.value = false
} catch (error) {
console.log(error)
}
}
// 监听路由变化,重新获取二级分类筛选条件
watch(
() => route.params.id,
async (newVal) => {
// 要注意路由是从二级类目变化到二级类目才重新发送请求,否则不发送
if (newVal && `/category/sub/${newVal}` === route.path) {
await getSubCateFilter(newVal)
}
},
{ immediate: true }
)
// 获取筛选参数,参考数据:{brandId: '', attrs: [{groupName: '颜色', propName: '蓝色'}]}
const getFilterParams = () => {
const filterParams = { brandId: null, attrs: [] }
// 1.获取选中的品牌ID
filterParams.brandId = filterData.value.selectedBrand
// 2.获取选中的属性
filterData.value.saleProperties.forEach((item) => {
item.properties.find((prop) => {
// 剔除 '全部'
if (prop.id === item.selectedProp && prop.id !== null) {
const obj = {
groupName: item.name,
propName: prop.name
}
filterParams.attrs.push(obj)
return true
}
})
})
if ((filterParams.attrs.length = 0)) filterParams.attrs = null
return filterParams
}
// 1.记录当前选择的品牌
const changeBrand = (brandId) => {
if (filterData.value.selectedBrand === brandId) return
filterData.value.selectedBrand = brandId
// 通知父组件,筛选条件变更
emit('filterChange', getFilterParams())
}
// 2.记录当前选择的属性
const changeProp = (item, propId) => {
if (item.selectedProp === propId) return
item.selectedProp = propId
emit('filterChange', getFilterParams())
}
</script>
2.复选框组件封装
大致步骤:
-
实现组件本身的选中和不选中的效果
-
实现组件的v-model指令
-
改造成@vueuse/core的函数写法
实现双向绑定:
-
const props = defineProps({ modelValue: { type: Boolean, default: false } }) const emits = defineEmits(['update:modelValue', 'change'])
- 原生写法:
// 这是原生写法
const checked = ref(false)
const changeChecked = () => {
checked.value = !checked.value
emits('update:modelValue', checked.value)
}
watch(() => props.modelValue, (newVal) => {
checked.value = newVal
}, {immediate: true})
组件中:
<xtx-checkbox v-model="isChecked" />
// 拆解写法
<xtx-checkbox :model-value="isChecked" @update:modelValue="handleCheck" />
@vueuse/core写法:
const checked = useVModel(props, 'modelValue', emits) // 封装父组件传来的数据
const changeChecked = () => {
// 使用封装后的数据就是在使用父组件的数据
// 这里修改值就已经通知了父组件修改数据,它会自动触发emit函数
const newVal = !checked.value
checked.value = newVal
// 这里让组件再派发一个自定义事件消息
emits('change', newVal)
}
<template>
<div class="sub-sort">
<div class="sort">
<a
@click="changeSort(null)"
:class="{ active: sortParams.sortField === null }"
href="javascript:;"
>
默认排序
</a>
<a
@click="changeSort('publishTime')"
:class="{ active: sortParams.sortField === 'publishTime' }"
href="javascript:;"
>
最新商品
</a>
<a
@click="changeSort('orderNum')"
:class="{ active: sortParams.sortField === 'orderNum' }"
href="javascript:;"
>
最高人气
</a>
<a
@click="changeSort('evaluateNum')"
:class="{ active: sortParams.sortField === 'evaluateNum' }"
href="javascript:;"
>
评论最多
</a>
<a @click="changeSort('price')" href="javascript:;">
价格排序
<i
:class="{
active:
sortParams.sortField === 'price' &&
sortParams.sortMethod === 'asc'
}"
class="arrow up"
></i>
<i
:class="{
active:
sortParams.sortField === 'price' &&
sortParams.sortMethod === 'desc'
}"
class="arrow down"
></i>
</a>
</div>
<div class="check">
<xtx-checkbox @change="changeCheck" v-model="sortParams.inventory">
仅显示有货商品
</xtx-checkbox>
<xtx-checkbox @change="changeCheck" v-model="sortParams.onlyDiscount">
仅显示优惠商品
</xtx-checkbox>
</div>
</div>
</template>
逻辑实现:
1. 数据要与后台需要的请求参数保持一致:
<script setup>
import { reactive } from 'vue'
// 交互数据:与后台参数保持一致
// sortField: publishTime, orderNum, price, evaluateNum
// sortMethod: asc, desc
const emit = defineEmits(['sortChange'])
const sortParams = reactive({
inventory: false,
onlyDiscount: false,
sortField: null,
sortMethod: null
})
// 改变排序参数
const changeSort = (sortField) => {
// 如果点击的是价格排序
if (sortField === 'price') {
sortParams.sortField = sortField
// 如果是第一次点击价格排序,给设置降序
if (sortParams.sortMethod === null) {
sortParams.sortMethod = 'desc'
} else {
sortParams.sortMethod = sortParams.sortMethod === 'desc' ? 'asc' : 'desc'
}
} else {
// 如果点击的是相同排序,就不工作
if (sortField === sortParams.sortField) {
return
}
// 如果是其他排序,重置升降序
sortParams.sortField = sortField
sortParams.sortMethod = null
}
// 更改了排序参数之后,通知父组件排序参数改变,重新发送请求
emit('sortChange', sortParams)
}
const changeCheck = () => {
emit('sortChange', sortParams)
}
</script>
<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>
核心原理:
1.使用IntersectionObserver,创建观察者对象,观察组件是否进入可视区,进入即根据数据加载状态和数据是否加载完毕状态通知父组件发送数据请求
2.父组件每次发送请求都要改变请求参数,比如page++,当数据全部请求完毕(这次请求没有数据返回)之后,设置数据加载完毕状态finished为true,这样数据无限加载组件就会显示加载完图案
<script setup>
import {useIntersectionObserver} from "@vueuse/core"
import {ref, onMounted} from "vue";
const props = defineProps({
loading: {
type: Boolean,
default: false
},
finished: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['infinite'])
// 当组件进入可视区时,根据loading状态和finished状态决定是否需要加载数据
// 触发条件:此次数据加载完毕且数据未全部加载完毕
const content = ref(null)
onMounted(() => {
useIntersectionObserver(content, ([{isIntersecting}]) => {
if(isIntersecting) {
if(!props.loading && !props.finished) {
emit('infinite')
}
}
}, {threshold: 0})
})
</script>
// 组件中使用:
<!-- 列表 -->
<ul>
<li v-for="goods in goodsList" :key="goods.id">
<goods-item :goods="goods" />
</li>
</ul>
<xtx-infinite-loading
:loading="loading"
:finished="finished"
@infinite="getData"
/>
5.筛选数据的实现
大致步骤:
-
排序组件中,当点击了排序或者复选框改变后,触发自定义事件sort-change传出排序参数
-
筛选组件中,当你改变了属性或者品牌,触发自定义事件filter-change传出筛选参数
-
// sub组件:
<script setup>
import SubBread from './sub-bread.vue'
import SubFilter from './sub-filter.vue'
import SubSort from './sub-sort.vue'
import GoodsItem from '@/views/category/goods-item'
import { findSuCategoryGoods } from '@/api/category'
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'
const route = useRoute()
const loading = ref(false)
const finished = ref(false)
const goodsList = ref([])
// 请求参数
let reqParams = {
page: 1,
pageSize: 20
}
const getData = async () => {
// 正在加载数据
loading.value = true
reqParams.categoryId = route.params.id
const { result } = await findSuCategoryGoods(reqParams)
if (result.items.length) {
// 如果还有数据加载进来,说明数据未加载完毕
goodsList.value.push(...result.items)
// 下次请求就加载下一页的数据
reqParams.page++
} else {
// 如果没有数据了,就说明数据全部加载完毕
finished.value = true
}
// 此次数据加载完毕
loading.value = false
}
watch(
() => route.params.id,
async (newVal) => {
// 要判断路由是在二级类目下变化的,才能重新发送二级类目的请求商品数据
if (newVal && `/category/sub/${newVal}` === route.path) {
// 重置请求参数,重新发送请求
finished.value = false
reqParams.page = 1
goodsList.value = []
}
},
{ immediate: true }
)
// 监听排序组件的自定义事件,获取更改后的排序参数,重新发送请求
const changeSort = (sortParams) => {
finished.value = false
// 将排序参数一并发送到后台请求商品
reqParams = { ...reqParams, ...sortParams }
reqParams.page = 1
// 置空商品列表,会进入infinite组件的可视区,自动触发发送请求
goodsList.value = []
}
// 监听筛选组件的自定义事件,获取更改后的筛选参数,重新发送请求
const changeFilter = (filterParams) => {
finished.value = false
// 将筛选参数一并发送到后台请求商品
reqParams = { ...reqParams, ...filterParams }
reqParams.page = 1
goodsList.value = []
}
</script>