小兔鲜电商项目(二):一级分类、二级分类制作
目录
1、一级分类
路由配置
一级分类 URL:http://127.0.0.1:5173/categopry/1005000,一级分类页是通过 /category/categoryId 的方式控制跳转,所以我们需要让路由接收 id 参数,修改 src\router\index.js 文件:
routes: [
{
path: '/',
component: Layout,
children: [
{
path: '',
component: Home
},
{
path: 'category/:id', //动态接收分类id参数
component: Category
}
]
},
修改导航区域
LayoutHeader.vue文件中:
<RouterLink :to="`/category/${ item.id }`">{{ item.name }}</RouterLink>
吸顶导航LayoutFixed.vue文件中:
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink :to="`/category/${ item.id }`">{{ item.name }}</RouterLink>
</li>
面包屑导航渲染
模板代码
在views/Category/index.vue目录下,添加以下代码:
<script setup>
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>居家</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.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;
}
}
}
}
}
.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: space-around;
padding: 0 40px 30px;
}
}
.bread-container {
padding: 25px 0;
}
}
</style>
封装接口
在apis文件下创建category.js文件:
import http from '@/utils/http'
export function getCategoryAPI(id) {
return http({
url: '/category',
params: {
id
}
})
}
数据渲染
在views/Category/index.vue目录下,修改代码:
<script setup>
import {getCategoryAPI} from '@/apis/category'
import {onMounted,onUpdated,ref} from 'vue'
import {useRoute} from 'vue-router'
const categoryData=ref({})
const router=useRoute() //获取路由参数使用useRoute
const getCategory=async ()=>{
const res=await getCategoryAPI(router.params.id) // 获取参数与定义路由是相关的,params.id / query.id
categoryData.value=res.result
}
onMounted(()=>getCategory())
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</div>
</template>
banner轮播图
分类轮播图和首页轮播图的区别只有一个,接口参数不同,其余逻辑完成一致。
获取 Banner 数据的接口参数如下,我们可以通过传递不同的参数来区分分类轮播图和首页轮播图:
| 参数名 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| distributionSite | Query | String | 否 | 广告区域展示位置(投放位置 投放位置,1为首页,2为分类商品页) 默认是1 |
完善接口
在home.js中修改接口
export function getBannerAPI(params = {}) {
// 默认为1 商品为w
const { distributionSite = '1' } = params;
return http({
url: 'home/banner',
params: {
distributionSite
}
})
}
数据渲染
在src/views/Category/index.vue目录下,完善代码:
<script setup>
import {getCategoryAPI} from '@/apis/category'
import {onMounted,onUpdated,ref} from 'vue'
import {useRoute} from 'vue-router'
import { getBannerAPI } from '@/apis/home'
const categoryData=ref({})
const router=useRoute() //获取路由参数使用useRoute
const getCategory=async ()=>{
const res=await getCategoryAPI(router.params.id) // 获取参数与定义路由是相关的,params.id / query.id
categoryData.value=res.result
}
onMounted(()=>getCategory())
// 获取banner
const bannerList = ref([])
const getBanner = async () => {
const res = await getBannerAPI({
distributionSite: '2'
})
bannerList.value = res.result
}
onMounted(() => getBanner())
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 轮播图 -->
<div class="home-banner">
<!--使用 ElementPlus 的轮播图组件-->
<el-carousel height="500px">
<el-carousel-item v-for="item in bannerList" :key="item.id">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.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;
}
}
}
}
}
.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: space-around;
padding: 0 40px 30px;
}
}
.bread-container {
padding: 25px 0;
}
}
.home-banner {
width: 1240px;
height: 500px;
margin:0 auto;
img {
width: 100%;
height: 500px;
}
}
</style>
激活状态显示
RouterLink组件默认支持激活样式显示的类名,只需要给active-class属性设置对应的类名即可。
在LayoutHeader.vue文件中修改代码:
<ul class="app-header-nav">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink active-class="active" :to="`/category/${ item.id }`">{{ item.name }}</RouterLink>
</li>
</ul>
分类列表渲染
修改 src\views\Category\index.vue 文件,在【Banner 轮播图】模块下添加分类数据模板:
<!-- 分类数据 -->
<div class="sub-list">
<h3>全部分类</h3>
<ul>
<li v-for="i in categoryData.children" :key="i.id">
<RouterLink to="/">
<img :src="i.picture" />
<p>{{ i.name }}</p>
</RouterLink>
</li>
</ul>
</div>
<div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
<div class="head">
<h3>- {{ item.name }}-</h3>
</div>
<div class="body">
<GoodsItem v-for="good in item.goods" :good="good" :key="good.id" />
</div>
</div>
解决路由缓存问题
解决方案
在 Vue 中使用带有参数的路由时需要注意的是,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。这也意味着组件的生命周期钩子不会被调用。
缓存问题:当路由 path 一样,参数不同的时候会选择直接复用路由对应的组件导致数据无法更新,解决方案:
1、让组件实例不复用,强制销毁重建:给 router-view 添加 key,破坏缓存
<!-- 在Layout文件的index.vue中修改 --> <!-- 添加key 破坏复用机制 强制销毁创建 --> <router-view :key="$route.fullPath"/>这种方式会将所有的接口重新发送请求,整个实例都被销毁重建了。
2、监听路由变化,变化之后执行数据更新操作:beforeRouteUpdate:钩子函数可以在每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可
import {getCategoryAPI} from '@/apis/category' import {onMounted,ref} from 'vue' import {useRoute} from 'vue-router' import { getBannerAPI } from '@/apis/home' import GoodsItem from '@/views/Home/components/GoodsItem.vue' import { onBeforeRouteUpdate } from 'vue-router' // 获取数据 const categoryData=ref({}) const router=useRoute() const getCategory=async (id=router.params.id)=>{ // router.params.id存在滞后性 const res=await getCategoryAPI(id) // 获取参数与定义路由是相关的,params.id / query.id categoryData.value=res.result } onMounted(()=>getCategory()) // 目标:路由参数变化的时候,可以把分类数据接口重新发送 onBeforeRouteUpdate((to)=>{ // 我们需要使用最新的分类数据 console.log(to) getCategory(to.params.id) })
使用逻辑函数拆分业务
把同一个组件中独立的业务代码通过函数做封装处理,提升代码的可维护性。
基本思想:把组件内独立的业务逻辑通过 useXXX 函数做封装处理,在组件中做组合使用。
步骤如下:
- 按照业务声明以
use打头的逻辑函数- 把独立的业务逻辑封装到各个函数内部
- 函数内部把组件中需要用到的数据或者方法return出去
- 在组件中调用函数把数据或者方法组合回来使用
在Category文件夹下创建composables文件夹,然后创建useBanner.js、useCategory.js文件
封装轮播图
import { onMounted, ref } from 'vue'
import { getBannerAPI } from '@/apis/home'
export function useBanner() {
// 获取banner
const bannerList = ref([])
const getBanner = async () => {
const res = await getBannerAPI({
distributionSite: '2'
})
bannerList.value = res.result
}
onMounted(() => getBanner())
return {
bannerList
}
}
封装分类商品列表
import { getCategoryAPI } from '@/apis/category'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { onBeforeRouteUpdate } from 'vue-router'
export function useCategory() {
// 获取数据
const categoryData = ref({})
const router = useRoute() //获取路由参数使用useRoute
const getCategory = async (id = router.params.id) => {
// router.params.id存在滞后性
const res = await getCategoryAPI(id) // 获取参数与定义路由是相关的,params.id / query.id
categoryData.value = res.result
}
onMounted(() => getCategory())
// 目标:路由参数变化的时候,可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
// 我们需要使用最新的分类数据
console.log(to)
getCategory(to.params.id)
})
return {
categoryData
}
}
修改index.vue
<script setup>
import GoodsItem from '@/views/Home/components/GoodsItem.vue'
import {useBanner} from '@/views/Category/composables/useBanner'
import { useCategory } from './composables/useCategory'
const {bannerList}=useBanner()
const {categoryData}=useCategory()
</script>
2、二级分类
路由配置
在views下创建SubCategory文件夹,并创建index.vue文件
<script setup>
</script>
<template>
<div class="container ">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/' }">居家
</el-breadcrumb-item>
<el-breadcrumb-item>居家生活用品</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="sub-container">
<el-tabs>
<el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
<el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
<el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
</el-tabs>
<div class="body">
<!-- 商品列表-->
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.bread-container {
padding: 25px 0;
color: #666;
}
.sub-container {
padding: 20px 10px;
background-color: #fff;
.body {
display: flex;
flex-wrap: wrap;
padding: 0 10px;
}
.goods-item {
display: block;
width: 220px;
margin-right: 20px;
padding: 20px 30px;
text-align: center;
img {
width: 160px;
height: 160px;
}
p {
padding-top: 10px;
}
.name {
font-size: 16px;
}
.desc {
color: #999;
height: 29px;
}
.price {
color: $priceColor;
font-size: 20px;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
}
</style>
在router/index.js中补充代码:
import SubCategory from '@/views/SubCategory/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: Layout,
children: [
{
path: '',
component: Home
},
{
path: 'category/:id', //动态接收分类id参数 :id用来占位
component: Category
},
{
path: 'category/sub/:id',
component: SubCategory
}
]
},
{
path: '/login',
component: Login
}
],
})
在views/Category/index.vue中完善代码:
<h3>全部分类</h3>
<ul>
<li v-for="i in categoryData.children" :key="i.id">
<RouterLink :to="`/category/sub/${i.id}`">
<img :src="i.picture" />
<p>{{ i.name }}</p>
</RouterLink>
</li>
</ul>
面包屑导航
封装接口
在 src\apis\category.js 文件中封装接口,用于获取二级分类列表数据:
/**
* @description: 获取二级分类列表数据
* @param {*} id 分类id
* @return {*}
*/
export const getCategoryFilterAPI = (id) => {
return http({
url: '/category/sub/filter',
params: {
id
}
})
}
数据渲染
<script setup>
import {getCategoryFilterAPI} from '@/apis/category'
import {onMounted, ref} from 'vue'
import {useRoute} from 'vue-router'
const categoryData=ref({})
const route=useRoute()
const getCategoryData=async()=>{
const res=await getCategoryFilterAPI(route.params.id)
categoryData.value=res.result
}
onMounted(()=>getCategoryData())
</script>
<template>
<div class="container ">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: `/category/${categoryData.parentId}` }">{{ categoryData.parentName }}
</el-breadcrumb-item>
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="sub-container">
<el-tabs>
<el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
<el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
<el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
</el-tabs>
<div class="body">
<!-- 商品列表-->
</div>
</div>
</div>
</template>
基础商品列表
封装接口
在src/apis/categoty.js中:
/**
* @description: 获取导航数据
* @data {
categoryId: 1005000 ,
page: 1,
pageSize: 20,
sortField: 'publishTime' | 'orderNum' | 'evaluateNum'
}
* @return {*}
*/
export const getSubCategoryAPI = (data) => {
return http({
url: '/category/goods/temporary',
method: 'POST',
data
})
}
数据渲染
在src/views/SubCategory/index.vue中:
<script setup>
import GoodsItem from '@/views/Home/components/GoodsItem.vue'
// 基础列表
const goodList=ref([])
const reqData=ref({
categoryId:route.params.id,
page:1,
pageSize:20,
sortField:'publishTime'
})
const getGoodList =async()=>{
const res=await getSubCategoryAPI(reqData.value)
// console.log(res)
goodList.value=res.result.items
}
onMounted(()=>getGoodList())
</script>
<template>
<div class="body">
<!-- 商品列表-->
<GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id"/>
</div>
</template>
列表筛选功能
<div class="sub-container" >
<el-tabs v-model="reqData.sortField" @tab-change="tabChange">
<el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
<el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
<el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
</el-tabs>
<div class="body">
<!-- 商品列表-->
<GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id"/>
</div>
</div>
const reqData=ref({
categoryId:route.params.id,
page:1,
pageSize:20,
sortField:'publishTime'
})
// tab 切换回调
const tabChange =()=>{
reqData.value.page=1
getGoodList()
}
列表无限加载
核心实现逻辑:使用elementPlus提供的v-infinite-scroll指令监听是否满足触底条件,满加载条件时,让页数参数+1获取数据,做新老数据拼接渲染。

步骤:
1. 配置v-infinite-scroll
<div class="body" v-infinite-scroll="load" :infinite-scroll-disabled="disabled">
<!-- 商品列表-->
<GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id"/>
</div>
2. 页数加一 ---> 获取下一页数据
3. 新老数据拼接
4. 加载完毕结束监听
const disabled=ref(false)
const load =async ()=>{
// 页数++
reqData.value.page++
// 获取列表
const res=await getSubCategoryAPI(reqData.value)
// 数据拼接
goodList.value=[...goodList.value,...res.result.items]
//加载完毕 如果后端明确告诉我们哪个字段用于结束,那就使用它,否则使用下面的
if(res.result.items.length===0){
disabled.value=true
}
}
定制路由滚动行为
在router/index.js目录中,在routes后面
scrollBehavior() {
return {
top: 0
}
}更多推荐




所有评论(0)