01-顶级类目-面包屑组件-初级

目的: 封装一个简易的面包屑组件,适用于两级场景。
大致步骤:

  • 准备静态的 xtx-bread.vue 组件
  • 定义 props 暴露 parentPath parentName 属性,默认插槽,动态渲染组件
  • 在 library/index.js 注册组件,使用验证效果。

落的代码:

  • 基础结构 src/components/library/xtx-bread.vue ```vue

  1. - 定义props进行渲染 src/components/library/xtx-bread.vue
  2. ```vue
  3. <template>
  4. <div class='xtx-bread'>
  5. <div class="xtx-bread-item">
  6. <RouterLink to="/">首页</RouterLink>
  7. </div>
  8. <i class="iconfont icon-angle-right"></i>
  9. + <div class="xtx-bread-item" v-if="parentName">
  10. + <RouterLink v-if="parentPath" :to="parentPath">{{parentName}}</RouterLink>
  11. + <span v-else>{{parentName}}</span>
  12. + </div>
  13. + <i v-if="parentName" class="iconfont icon-angle-right"></i>
  14. <div class="xtx-bread-item">
  15. + <span><slot /></span>
  16. </div>
  17. </div>
  18. </template>
  19. <script>
  20. export default {
  21. name: 'XtxBread',
  22. + props: {
  23. + parentName: {
  24. + type: String,
  25. + default: ''
  26. + },
  27. + parentPath: {
  28. + type: String,
  29. + default: ''
  30. + }
  31. + }
  32. }
  33. </script>
  • 注册使用 src/components/library/index.js ```javascript +import XtxBread from ‘./xtx-bread.vue’

export default { install (app) {

  • app.component(XtxBread.name, XtxBread) ``` 使用: 空调
    总结: 采用基本的封装手法,灵活度不是很高。

    02-顶级类目-面包屑组件-高级

    目的: 封装一个高复用的面包屑组件,适用于多级场景。认识 render 选项和 h 函数。
    参考element-ui的面包屑组件:
    image.png
    大致步骤:
  • 使用插槽和封装选项组件完成面包屑组件基本功能,但是最后一项多一个图标。
  • 学习 render 选项,h 函数 的基本使用。
  • 通过 render 渲染,h 函数封装面包屑功能。

落的代码:

  • 我们需要两个组件,xtx-bread 和 xtx-bread-item 才能完成动态展示。

定义单项面包屑组件 src/components/library/xtx-bread-item.vue

  1. <template>
  2. <div class="xtx-bread-item">
  3. <RouterLink v-if="to" :to="to"><slot /></RouterLink>
  4. <span v-else><slot /></span>
  5. <i class="iconfont icon-angle-right"></i>
  6. </div>
  7. </template>
  8. <script>
  9. export default {
  10. name: 'XtxBreadItem',
  11. props: {
  12. to: {
  13. type: [String, Object]
  14. }
  15. }
  16. }
  17. </script>

在 library/index.js注册

  1. +import 'XtxBreadItem' from './xtx-bread-item.vue'
  2. export default {
  3. install (app) {
  4. + app.component(XtxBreadItem.name, XtxBread)
  • 过渡版,你发现结构缺少风格图标,如果在item中加上话都会有图标,但是最后一个是不需要的。 ```vue
```
  • 2)交互效果:

    1. <template>
    2. <div class='sub-sort'>
    3. <div class="sort">
    4. <a :class="{active:sortParams.sortField===null}" @click="changeSort(null)" href="javascript:;">默认排序</a>
    5. <a :class="{active:sortParams.sortField==='publishTime'}" @click="changeSort('publishTime')" href="javascript:;">最新商品</a>
    6. <a :class="{active:sortParams.sortField==='orderNum'}" @click="changeSort('orderNum')" href="javascript:;">最高人气</a>
    7. <a :class="{active:sortParams.sortField==='evaluateNum'}" @click="changeSort('evaluateNum')" href="javascript:;">评论最多</a>
    8. <a @click="changeSort('price')" href="javascript:;">
    9. 价格排序
    10. <i class="arrow up" :class="{active:sortParams.sortField==='price'&&sortParams.sortMethod=='asc'}" />
    11. <i class="arrow down" :class="{active:sortParams.sortField==='price'&&sortParams.sortMethod=='desc'}" />
    12. </a>
    13. </div>
    14. <div class="check">
    15. <XtxCheckbox v-model="sortParams.inventory">仅显示有货商品</XtxCheckbox>
    16. <XtxCheckbox v-model="sortParams.onlyDiscount">仅显示特惠商品</XtxCheckbox>
    17. </div>
    18. </div>
    19. </template>
    20. <script>
    21. import { reactive } from 'vue'
    22. export default {
    23. name: 'SubSort',
    24. setup () {
    25. // 1. 根据后台需要的参数定义数据对象
    26. // 2. 根据数据对象,绑定组件(复选框,排序按钮)
    27. // 3. 在操作排序组件的时候,需要反馈给数据对象
    28. // sortField====>publishTime,orderNum,price,evaluateNum
    29. // sortMethod====>asc为正序 desc为倒序
    30. const sortParams = reactive({
    31. inventory: false,
    32. onlyDiscount: false,
    33. sortField: null,
    34. sortMethod: null
    35. })
    36. // 改变排序
    37. const changeSort = (sortField) => {
    38. if (sortField === 'price') {
    39. sortParams.sortField = sortField
    40. if (sortParams.sortMethod === null) {
    41. // 第一次点击,默认是降序
    42. sortParams.sortMethod = 'desc'
    43. } else {
    44. // 其他情况根据当前排序取反
    45. sortParams.sortMethod = sortParams.sortMethod === 'desc' ? 'asc' : 'desc'
    46. }
    47. } else {
    48. // 如果排序未改变停止逻辑
    49. if (sortParams.sortField === sortField) return
    50. sortParams.sortField = sortField
    51. sortParams.sortMethod = null
    52. }
    53. }
    54. return { sortParams, changeSort }
    55. }
    56. }
    57. </script>

    13-二级类目-结果区-数据加载

    目的:实现结果区域商品展示。
    大致步骤:

  • 完成结果区域商品布局

  • 完成 xtx-infinite-loading 组件封装
  • 使用 xtx-infinite-loading 完成数据加载和渲染

落地代码:src/views/category/sub.vue

  1. 基础布局 ```vue

  1. 1. 无限列表加载组件 src/components/xtx-infinite-loading.vue
  2. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27272831/1659191785158-2f032917-e8ef-4480-9fb2-8539e7bfb2ee.png#clientId=ufe842d9c-86a4-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u3cc80d42&margin=%5Bobject%20Object%5D&name=image.png&originHeight=567&originWidth=1222&originalType=url&ratio=1&rotation=0&showTitle=false&size=29947&status=done&style=none&taskId=ud2c00746-1475-4488-ba77-8a87e2a15a9&title=)
  3. ```vue
  4. <template>
  5. <div class="xtx-infinite-loading" ref="container">
  6. <div class="loading" v-if="loading">
  7. <span class="img"></span>
  8. <span class="text">正在加载...</span>
  9. </div>
  10. <div class="none" v-if="finished">
  11. <span class="img"></span>
  12. <span class="text">亲,没有更多了</span>
  13. </div>
  14. </div>
  15. </template>
  16. <script>
  17. import { ref } from 'vue'
  18. import { useIntersectionObserver } from '@vueuse/core'
  19. export default {
  20. name: 'XtxInfiniteLoading',
  21. props: {
  22. loading: {
  23. type: Boolean,
  24. default: false
  25. },
  26. finished: {
  27. type: Boolean,
  28. default: false
  29. }
  30. },
  31. setup (props, { emit }) {
  32. const container = ref(null)
  33. useIntersectionObserver(
  34. container,
  35. ([{ isIntersecting }], dom) => {
  36. if (isIntersecting) {
  37. if (props.loading === false && props.finished === false) {
  38. emit('infinite')
  39. }
  40. }
  41. },
  42. {
  43. threshold: 0
  44. }
  45. )
  46. return { container }
  47. }
  48. }
  49. </script>
  50. <style scoped lang='less'>
  51. .xtx-infinite-loading {
  52. .loading {
  53. display: flex;
  54. align-items: center;
  55. justify-content: center;
  56. height: 200px;
  57. .img {
  58. width: 50px;
  59. height: 50px;
  60. background: url(../../assets/images/load.gif) no-repeat center / contain;
  61. }
  62. .text {
  63. color: #999;
  64. font-size: 16px;
  65. }
  66. }
  67. .none {
  68. display: flex;
  69. align-items: center;
  70. justify-content: center;
  71. height: 200px;
  72. .img {
  73. width: 200px;
  74. height: 134px;
  75. background: url(../../assets/images/none.png) no-repeat center / contain;
  76. }
  77. .text {
  78. color: #999;
  79. font-size: 16px;
  80. }
  81. }
  82. }
  83. </style>
  1. 定义获取数据的API src/api/category.js ```javascript /**
    • 获取分类下的商品(带筛选条件)
    • @param {Object} params - 可参考接口文档 */ export const findSubCategoryGoods = (params) => { return request(‘/category/goods/temporary’, ‘post’, params) }
  1. 1. src/views/category/sub.vue 使用组件
  2. ```vue
  3. <!-- 结果区域 -->
  4. <div class="goods-list">
  5. <!-- 排序 -->
  6. <GoodsSort />
  7. <!-- 列表 -->
  8. <ul>
  9. <li v-for="item in list" :key="item.id" >
  10. <GoodsItem :goods="item" />
  11. </li>
  12. </ul>
  13. <!-- 加载 -->
  14. + <XtxInfiniteLoading :loading="loading" :finished="finished" @infinite="getData" />
  15. </div>
  1. <script>
  2. import SubBread from './components/sub-bread'
  3. import SubFilter from './components/sub-filter'
  4. import SubSort from './components/sub-sort'
  5. import GoodsItem from './components/goods-item'
  6. import { ref, watch } from 'vue'
  7. import { findSubCategoryGoods } from '@/api/category'
  8. import { useRoute } from 'vue-router'
  9. export default {
  10. name: 'SubCategory',
  11. components: { SubBread, SubFilter, SubSort, GoodsItem },
  12. setup () {
  13. // 1. 基础布局
  14. // 2. 无限加载组件
  15. // 3. 动态加载数据且渲染
  16. // 4. 任何筛选条件变化需要更新列表
  17. const route = useRoute()
  18. const loading = ref(false)
  19. const finished = ref(false)
  20. const goodsList = ref([])
  21. // 查询参数
  22. let reqParams = {
  23. page: 1,
  24. pageSize: 20
  25. }
  26. // 获取数据函数
  27. const getData = () => {
  28. loading.value = true
  29. reqParams.categoryId = route.params.id
  30. findSubCategoryGoods(reqParams).then(({ result }) => {
  31. if (result.items.length) {
  32. goodsList.value.push(...result.items)
  33. reqParams.page++
  34. } else {
  35. // 加载完毕
  36. finished.value = true
  37. }
  38. // 请求结束
  39. loading.value = false
  40. })
  41. }
  42. // 切换二级分类重新加载
  43. watch(() => route.params.id, (newVal) => {
  44. if (newVal && route.path === ('/category/sub/' + newVal)) {
  45. goodsList.value = []
  46. reqParams = {
  47. page: 1,
  48. pageSize: 20
  49. }
  50. finished.value = false
  51. }
  52. })
  53. return { loading, finished, goodsList, getData }
  54. }
  55. }
  56. </script>

14-二级类目-结果区-进行筛选

目的:在做了筛选和排序的时候从新渲染商品列表。
大致步骤:

  • 排序组件,当你点击了排序后 或者 复选框改变后 触发自定义事件 sort-change 传出排序参数
  • 筛选组件,当你改了品牌,或者其他筛选添加,触发自定义事件 filter-change 传出筛选参数
  • 在sub组件,分别绑定 sort-change filter-change 得到参数和当前参数合并,回到第一页,清空数据,设置未加载完成,触发加载。

落地代码:

  • 组件:src/views/category/components/sub-sort.vue ```typescript // 改变排序 const changeSort = (sortField) => {
    1. // 省略代码....
  • emit(‘sort-change’, sortParams) }
    1. ```vue
    2. <div class="check">
    3. <XtxCheckbox @change="changeCheck" v-model="sortParams.inventory">仅显示有货商品</XtxCheckbox>
    4. <XtxCheckbox @change="changeCheck" v-model="sortParams.onlyDiscount">仅显示特惠商品</XtxCheckbox>
    5. </div>
    1. const changeCheck = (sortField) => {
    2. emit('sort-change', sortParams)
    3. }
  • 组件 src/views/category/components/sub-filter.vue

    1. // 获取筛选参数
    2. const getFilterParams = () => {
    3. const filterParams = {}
    4. const attrs = []
    5. filterParams.brandId = filterData.value.selectedBrand
    6. filterData.value.saleProperties.forEach(p => {
    7. const attr = p.properties.find(attr => attr.id === p.selectedProp)
    8. if (attr && attr.id !== undefined) {
    9. attrs.push({ groupName: p.name, propertyName: attr.name })
    10. }
    11. })
    12. if (attrs.length) filterParams.attrs = attrs
    13. return filterParams
    14. }
    15. // 选择品牌
    16. const changeBrand = (brandId) => {
    17. if (filterData.value.selectedBrand === brandId) return
    18. filterData.value.selectedBrand = brandId
    19. emit('filter-change', getFilterParams())
    20. }
    21. // 选中属性
    22. const changeAttr = (p, attrId) => {
    23. if (p.selectedProp === attrId) return
    24. p.selectedProp = attrId
    25. emit('filter-change', getFilterParams())
    26. }
    27. return { filterData, filterLoading, changeBrand, changeAttr }
  • 组件 src/views/category/sub.vue ```vue

  1. ```javascript
  2. // 监听筛选区改变
  3. const changeFilter = (filterParams) => {
  4. reqParams = { ...reqParams, ...filterParams }
  5. reqParams.page = 1
  6. goodsList.value = []
  7. finished.value = false
  8. }
  9. // 监听排序改变
  10. const changeSort = (sortParams) => {
  11. reqParams = { ...reqParams, ...sortParams }
  12. reqParams.page = 1
  13. goodsList.value = []
  14. finished.value = false
  15. }
  16. return { loading, finished, goodsList, getData, changeFilter, changeSort }