今日目标
✔ 掌握商品一级和二级分类的处理。
面包屑封装
- 拷贝面包屑组件 Bread 和 BreadItem。
src/components/bread/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <script lang="ts" setup name="XtxBread"> import { provide } from 'vue'
const props = defineProps({ separator: { type: String, default: '', }, })
provide('separator', props.separator) </script> <template> <div class="xtx-bread"> <slot /> </div> </template> <style scoped lang="less"> .xtx-bread { display: flex; padding: 25px 10px; &-item { a { color: #666; transition: all 0.4s; &:hover { color: @xtxColor; } } } i { font-size: 12px; margin-left: 5px; margin-right: 5px; line-height: 22px; } } </style>
|
src/components/bread/item.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <script lang="ts" setup name="XtxBreadItem"> import { inject } from 'vue'
defineProps({ to: { type: String, }, })
const separator = inject('separator') </script> <template> <div class="xtx-bread-item">
<router-link v-if="to" :to="to"><slot /></router-link> <span v-else><slot /></span> <i v-if="separator">{{ separator }}</i> <i v-else class="iconfont icon-angle-right"></i> </div> </template>
<style lang="less" scoped> .xtx-bread-item { i { margin: 0 6px; font-size: 10px; } // 最后一个i隐藏 &:nth-last-of-type(1) { i { display: none; } } } </style>
|
- 注册成全局组件应用。
1 2 3 4 5 6 7 8 9
| import Bread from './bread/index.vue' import BreadItem from './bread/item.vue'
export default { install(app) { app.component('XtxBread', Bread) app.component('XtxBreadItem', BreadItem) }, }
|
- 提供类型声明,
globale.d.ts
。
1 2 3 4 5 6 7 8
| import XtxBread from '@/components/bread/index.vue' import XtxBreadItem from '@/components/bread/item.vue' declare module 'vue' { export interface GlobalComponents { XtxBread: typeof XtxBread XtxBreadItem: typeof XtxBreadItem } }
|
- 测试,
views/playground/index.vue
。
1 2 3 4 5 6
| <template> <XtxBread> <XtxBreadItem to="/">首页</XtxBreadItem> <XtxBreadItem>美食</XtxBreadItem> </XtxBread> </template>
|
顶级分类
获取面包屑数据并渲染
- 在
types/data.d.ts
中定义分类的数据类型。
1 2 3 4 5 6
| export type TopCategory = { id: string name: string picture: string children: CategoryItem[] }
|
- 准备数据
store/modules/category.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { defineStore } from 'pinia' import request from '@/utils/request' import { ApiRes, CategoryItem, TopCategory } from '@/types/data' import { topCategory } from '../constants' const defaultCategory = topCategory.map((item) => { return { name: item, } }) export default defineStore('category', { state: () => ({ topCategory: {} as TopCategory, }), actions: { async getTopCategory(id: string) { const res = await request.get<ApiRes<TopCategory>>('/category', { params: { id, }, }) this.topCategory = res.data.result }, }, })
|
- 组件中渲染,
views/category/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <script lang="ts" setup name="TopCategory"> import useStore from '@/store' import { useRoute } from 'vue-router' const { category } = useStore() const route = useRoute() category.getTopCategory(route.params.id as string) </script>
<template> <div class="top-category"> <div class="container"> <XtxBread> <XtxBreadItem to="/">首页</XtxBreadItem> <XtxBreadItem>{{category.topCategory.name}}</XtxBreadItem> </XtxBread> </div> </div> </template>
|
监听路由变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { watch } from 'vue' import useStore from '@/store' import { useRoute } from 'vue-router' const { category } = useStore() const route = useRoute() watch( () => route.params.id, (newValue) => { category.getTopCategory(newValue as string) }, { immediate: true, } )
|
- 报错:从分类页跳转到首页会报错,需要判断路由地址。
1 2 3 4 5 6 7 8 9 10
| watch( () => route.params.id, (newValue) => { if (!newValue) return category.getTopCategory(newValue as string) }, { immediate: true, } )
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| watch( () => route.params.id, (newValue) => { if (!newValue) return if (route.fullPath !== `/category/${newValue}`) return category.getTopCategory(newValue as string)
}, { immediate: true, } )
|
watchEffect 的使用
使用 watchEffect 监听路由的变化,参考文档:https://staging-cn.vuejs.org/guide/essentials/watchers.html#watcheffect。
1 2 3 4 5 6 7
| watchEffect(() => { const id = route.params.id as string if (route.fullPath === `/category/${id}`) { category.getTopCategory(id) } })
|
watch
vs watchEffect
,相同点:都给我们提供了创建副作用的能力;不同点:追踪响应式依赖的方式。
渲染轮播图
- 获取轮播图数据并渲染,
views/category/index.vue
。
1 2 3 4 5 6
| <script lang="ts" setup name="TopCategory"> import useStore from '@/store' const { category, home } = useStore() home.getBannerList() </script>
|
1 2 3
| <XtxBread></XtxBread>
<XtxCarousel :slides="home.bannerList" style="height: 500px" auto-play />
|
二级分类渲染
- 基本结构和样式,
views/category/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <XtxCarousel></XtxCarousel>
<div class="sub-list"> <h3>全部分类</h3> <ul> <li v-for="i in 8" :key="i"> <a href="javascript:;"> <img src="https://yanxuan.nosdn.127.net/3102b963e7a3c74b9d2ae90e4380da65.png?quality=95&imageView" /> <p>空调</p> </a> </li> </ul> </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <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>
|
- 动态渲染数据,
views/category/index.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script lang="ts" name="TopCategory" setup> const { topCategory } = storeToRefs(category) </script>
<div class="sub-list"> <h3>全部分类</h3> <ul> <li v-for="item in topCategory.children" :key="item.id"> <RouterLink :to="`/category/sub/${item.id}`"> <img :src="item.picture" /> <p>{{ item.name }}</p> </RouterLink> </li> </ul> </div>
|
商品展示
基本结构
- 封装商品信息组件
src/views/category/components/goods-item.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <RouterLink to="/" class="goods-item"> <img src="https://yanxuan-item.nosdn.127.net/46898c7fa475dbfc89dac2f7e7c2c16f.jpg" alt="" /> <p class="name ellipsis">红功夫 麻辣小龙虾 19.99/500g 实惠到家</p> <p class="desc ellipsis">火锅食材</p> <p class="price">¥19.99</p> </RouterLink> </template>
<script lang="ts" setup 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>
|
- 导入 goodsItem 组件并渲染,
src/views/category/index.vue
。
1 2 3
| <script lang="ts" setup name="TopCategory"> import GoodsItem from './components/goods-item.vue' </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <div class="sub-list"></div>
<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>
|
- 样式处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| .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; } }
|
动态渲染
src/views/category/index.vue
。
1 2 3 4 5 6 7 8 9 10 11
| <div class="ref-goods" v-for="item in category.topCategory.children"> <div class="head"> <h3>- {{ item.name }} -</h3> <p class="tag">温暖柔软,品质之选</p> <XtxMore /> </div> <div class="body"> <GoodsItem v-for="goods in item.goods" :key="goods.id" :goods="goods" /> </div> </div>
|
src/views/category/components/goods-item.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script lang="ts" setup name="GoodsItem"> import { GoodItem } from '@/types/data' import { PropType } from 'vue' defineProps({ goods: { type: Object as PropType<GoodItem>, default: () => ({}), }, }) </script>
<template> <RouterLink to="/" class="goods-item"> <img v-lazy="goods.picture" alt="" /> <p class="name ellipsis">{{ goods.name }}</p> <p class="desc ellipsis">{{ goods.desc }}</p> <p class="price">¥{{ goods.price }}</p> </RouterLink> </template>
|
处理跳转
- 在路由切换的时候,需要滚动到页面最顶部。
文档,router/index.ts
。
1 2 3 4 5 6 7 8
| const router = createRouter({ history: createWebHashHistory(), scrollBehavior: () => { return { top: 0, } }, })
|
- 鼠标滑过一级类目
母婴
,再移动到二级类目连体衣/礼盒
,滚动窗口,点击二级类名连体衣/礼盒
,发现二级类名不会被隐藏。
views/layout/components/app-header-nav.vue
1 2 3
| <li class="home"><RouterLink to="/">首页</RouterLink></li>
<li @mousemove="show(item)"></li>
|
二级类目
展示面包屑
- 基本结构
src/views/category/sub.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script lang="ts" setup name="SubCategory"></script>
<template> <div class="sub-category"> <div class="container"> <XtxBread> <XtxBreadItem to="/">首页</XtxBreadItem> <XtxBreadItem to="/">居家</XtxBreadItem> <XtxBreadItem>水壶</XtxBreadItem> </XtxBread> </div> </div> </template>
|
- 定义二级分类的数据类型,
types/data.d.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export type SaleProperty = { id: string name: string properties: { id: string name: string }[] }
export type SubCategory = { id: string name: string picture?: any parentId: string parentName: string brands: { id: string name: string nameEn: string logo: string picture: string type?: any desc: string place: string }[] saleProperties: SaleProperty[] }
|
- 准备二级分类的数据,
store/modules/category.ts
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const useCategoryStore = defineStore('category', { state: () => ({ subCategory: {} as SubCategory, }), actions: { async getSubFilter(id: string) { const res = await request.get<ApiRes<SubCategory>>('/category/sub/filter', { params: { id, }, }) this.subCategory = res.data.result }, }, })
|
- 渲染面包屑,
views/category/sub.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <script lang="ts" setup name="SubCategory"> import useStore from '@/store' import { watchEffect } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const { category } = useStore() watchEffect(() => { const id = route.params.id as string if (route.fullPath !== `/category/sub/${id}`) return category.getSubFilter(id) }) </script>
<template> <div class="sub-category"> <div class="container"> <XtxBread> <XtxBreadItem to="/">首页</XtxBreadItem> <XtxBreadItem :to="`/category/${category.subCategory.parentId}`"> {{ category.subCategory.parentName }} </XtxBreadItem> <XtxBreadItem>{{ category.subCategory.name }}</XtxBreadItem> </XtxBread> </div> </div> </template>
|
筛选区展示
- 基础布局和样式,
src/views/category/sub.vue
。
1 2 3 4 5 6 7 8 9 10 11
| <XtxBread></XtxBread>
<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>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <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 0.3s; display: inline-block; &.active, &:hover { color: @xtxColor; } } } } } </style>
|
- 渲染真实数据,
views/category/sub.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <div class="sub-filter"> <div class="item"> <div class="head">品牌:</div> <div class="body"> <a href="javascript:;">全部</a> <a href="javascript:;" v-for="item in category.subCategory.brands" :key="item.id"> {{ item.name }} </a> </div> </div> <div class="item" v-for="item in category.subCategory.saleProperties" :key="item.id"> <div class="head">{{ item.name }}:</div> <div class="body"> <a href="javascript:;">全部</a> <a href="javascript:;" v-for="sub in item.properties" :key="sub.id">{{ sub.name }}</a> </div> </div> </div>
|
商品列表结构
- 基本结构
src/views/category/sub.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script lang="ts" setup name="SubCategory"> import GoodsItem from './components/goods-item.vue' </script> <template> <div class="goods-list"> <div class="sub-sort"> <div class="sort"> <a href="javascript:;" class="active">默认排序</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> <ul> <li v-for="goods in 20" :key="goods"> <GoodsItem /> </li> </ul> </div> </template>
|
- 样式,
src/views/category/sub.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| .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; } } } .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; } } } }
|
商品基本渲染
types/data.d.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export type SubCategory = { id: string name: string picture?: any parentId: string parentName: string brands: { id: string name: string nameEn: string logo: string picture: string type?: any desc: string place: string }[] saleProperties: SaleProperty[] goods: GoodItem[] }
|
views/category/sub.vue
1 2 3 4 5 6
| <ul> <li v-for="goods in category.subCategory.goods" :key="goods.id"> <GoodsItem :goods="goods" /> </li> </ul>
|