首页模块

image.png

首页主体-左侧分类-结构渲染

目的: 实现首页主体内容-左侧分类

大致步骤:

  • 准备左侧分类组件和基础布局
  • 从vuex中拿出9个分类数据,且值需要两个子分类,但是左侧是10个,需要补充一个品牌数据。
    • 使用计算属性完成上面逻辑
  • 渲染组件

落地代码:
src/api/constants.js

  1. const category = [
  2. '居家',
  3. '美食',
  4. '服饰',
  5. '母婴',
  6. '个护',
  7. '严选',
  8. '运动',
  9. '杂项',
  10. '品牌',
  11. ]
  • 准备组件:src/views/home/components/home-categroy.vue
  1. <template>
  2. <div class='home-category'>
  3. <ul class="menu">
  4. <li v-for="i in 10" :key="i">
  5. <RouterLink to="/">居家</RouterLink>
  6. <RouterLink to="/">洗漱</RouterLink>
  7. <RouterLink to="/">清洁</RouterLink>
  8. </li>
  9. </ul>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. name: 'HomeCategory'
  15. }
  16. </script>
  17. <style scoped lang='less'></style>
  • 预览组件:src/views/home/index.vue
  1. <template>
  2. <div class="page-home">
  3. <div class="home-entry">
  4. <div class="container">
  5. <!-- 左侧分类 -->
  6. <HomeCategory />
  7. </div>
  8. </div>
  9. </div>
  10. </template>
  11. <script>
  12. import HomeCategory from './components/home-category'
  13. export default {
  14. name: 'PageHome',
  15. components: { HomeCategory }
  16. }
  17. </script>
  18. <style scoped lang="less"></style>
  • 从vuex中拿出分类,取出子分类中的前两项。给一级分类追加一项品牌,进行渲染。
  1. <template>
  2. <div class="home-category">
  3. <ul class="menu">
  4. <li v-for="item in list" :key="item.id">
  5. <!-- 一级分类 -->
  6. <RouterLink to="/">{{ item.name }}</RouterLink>
  7. <!-- 二级分类 -->
  8. <template v-if="item.children">
  9. <RouterLink
  10. v-for="sub in item.children"
  11. :key="sub.id"
  12. :to="`/category/sub/${sub.id}`"
  13. >
  14. {{ sub.name }}
  15. </RouterLink>
  16. </template>
  17. </li>
  18. </ul>
  19. </div>
  20. </template>
  21. <script>
  22. import { useStore } from 'vuex'
  23. import { computed } from 'vue'
  24. export default {
  25. name: 'HomeCategory',
  26. setup () {
  27. // 左侧分类底部添加一个品牌
  28. const brand = {
  29. id: 'brand',
  30. name: '品牌',
  31. children: [{ id: 'brand-chilren', name: '品牌推荐' }]
  32. }
  33. const store = useStore()
  34. const list = computed(() => {
  35. // 获取原始的分类数据
  36. const cates = store.state.cate.list
  37. // 左侧二级分类仅仅需要两个
  38. const result = cates.map(item => {
  39. // 获取数组的前两项数据
  40. item.children = item.children.slice(0, 2)
  41. return item
  42. })
  43. result.push(brand)
  44. return result
  45. })
  46. return { list }
  47. }
  48. }
  49. </script>
  50. <style scoped lang="less">
  51. .home-category {
  52. width: 250px;
  53. height: 500px;
  54. background: rgba(0, 0, 0, 0.8);
  55. position: relative;
  56. z-index: 99;
  57. .menu {
  58. li {
  59. padding-left: 40px;
  60. height: 50px;
  61. line-height: 50px;
  62. &:hover {
  63. background: @xtxColor;
  64. }
  65. a {
  66. margin-right: 4px;
  67. color: #fff;
  68. &:first-child {
  69. font-size: 16px;
  70. }
  71. }
  72. }
  73. }
  74. }
  75. </style>
  76. <template>
  77. <div class='home-category'>
  78. <ul class="menu">
  79. <li v-for="item in menuList" :key="item.id">
  80. <RouterLink :to="`/category/${item.id}`">{{item.name}}</RouterLink>
  81. <template v-if="item.children">
  82. <RouterLink
  83. v-for="sub in item.children"
  84. :key="sub.id"
  85. :to="`/category/sub/${sub.id}`">
  86. {{sub.name}}
  87. </RouterLink>
  88. </template>
  89. </li>
  90. </ul>
  91. </div>
  92. </template>
  93. <script>
  94. import { useStore } from 'vuex'
  95. import { reactive, computed } from 'vue'
  96. export default {
  97. name: 'HomeCategory',
  98. // 1. 获取vuex的一级分类,并且只需要两个二级分类
  99. // 2. 需要在组件内部,定义一个品牌数据
  100. // 3. 根据vuex的分类数据和组件中定义品牌数据,得到左侧分类完整数据(9分类+1品牌)数组
  101. // 4. 进行渲染即可
  102. setup () {
  103. const brand = reactive({
  104. id: 'brand',
  105. name: '品牌',
  106. children: [{ id: 'brand-chilren', name: '品牌推荐' }]
  107. })
  108. const store = useStore()
  109. const menuList = computed(() => {
  110. const list = store.state.cate.list.map(item => {
  111. return {
  112. id: item.id,
  113. name: item.name,
  114. // 防止初始化没有children的时候调用slice函数报错
  115. children: item.children && item.children.slice(0, 2)
  116. }
  117. })
  118. list.push(brand)
  119. return list
  120. })
  121. return { menuList }
  122. }
  123. }
  124. </script>
  125. <style scoped lang='less'>
  126. .home-category {
  127. width: 250px;
  128. height: 500px;
  129. background: rgba(0,0,0,0.8);
  130. position: relative;
  131. z-index: 99;
  132. .menu {
  133. li {
  134. padding-left: 40px;
  135. height: 50px;
  136. line-height: 50px;
  137. &:hover {
  138. background: @xtxColor;
  139. }
  140. a {
  141. margin-right: 4px;
  142. color: #fff;
  143. &:first-child {
  144. font-size: 16px;
  145. }
  146. }
  147. }
  148. }
  149. }
  150. </style>

首页主体-左侧分类-弹层展示

目的: 实现首页主体内容-左侧分类-鼠标进入弹出

大致步骤:

  • 准备布局
  • 得到数据

    • 鼠标经过记录ID
    • 通过ID得到分类推荐商品,使用计算属性
    • 完成渲染


    落地代码:

  1. 准备布局:src/views/home/components/home-categroy.vue
  1. <!-- 弹层 -->
  2. <div class="layer">
  3. <h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>
  4. <ul>
  5. <li v-for="i in 9" :key="i">
  6. <RouterLink to="/">
  7. <img src="https://yanxuan-item.nosdn.127.net/5a115da8f2f6489d8c71925de69fe7b8.png" alt="">
  8. <div class="info">
  9. <p class="name ellipsis-2">【定金购】严选零食大礼包(12件)</p>
  10. <p class="desc ellipsis">超值组合装,满足馋嘴欲</p>
  11. <p class="price"><i>¥</i>100.00</p>
  12. </div>
  13. </RouterLink>
  14. </li>
  15. </ul>
  16. </div>
  1. .layer {
  2. width: 990px;
  3. height: 500px;
  4. background: rgba(255,255,255,0.8);
  5. position: absolute;
  6. left: 250px;
  7. top: 0;
  8. display: none;
  9. padding: 0 15px;
  10. h4 {
  11. font-size: 20px;
  12. font-weight: normal;
  13. line-height: 80px;
  14. small {
  15. font-size: 16px;
  16. color: #666;
  17. }
  18. }
  19. ul {
  20. display: flex;
  21. flex-wrap: wrap;
  22. li {
  23. width: 310px;
  24. height: 120px;
  25. margin-right: 15px;
  26. margin-bottom: 15px;
  27. border: 1px solid #eee;
  28. border-radius: 4px;
  29. background: #fff;
  30. &:nth-child(3n) {
  31. margin-right: 0;
  32. }
  33. a {
  34. display: flex;
  35. width: 100%;
  36. height: 100%;
  37. align-items: center;
  38. padding: 10px;
  39. &:hover {
  40. background: #e3f9f4;
  41. }
  42. img {
  43. width: 95px;
  44. height: 95px;
  45. }
  46. .info {
  47. padding-left: 10px;
  48. line-height: 24px;
  49. width: 190px;
  50. .name {
  51. font-size: 16px;
  52. color: #666;
  53. }
  54. .desc {
  55. color: #999;
  56. }
  57. .price {
  58. font-size: 22px;
  59. color: @priceColor;
  60. i {
  61. font-size: 16px;
  62. }
  63. }
  64. }
  65. }
  66. }
  67. }
  68. }
  69. &:hover {
  70. .layer {
  71. display: block;
  72. }
  73. }
  1. 渲染逻辑:src/views/home/components/home-categroy.vue
  • 定义一个数据记录当前鼠标经过分类的ID,使用计算属性得到当前的分类推荐商品数据
  1. <ul class="menu">
  2. + <li v-for="item in list" :key="item.id" @mouseenter="categoryId=item.id">
  1. + // 获取当前分类逻辑
  2. + const categoryId = ref(null)
  3. + const currCategory = computed(()=>{
  4. + return list.value.find(item => item.id === categoryId.value)
  5. + })
  6. + return { list, categoryId, currCategory }
  • 渲染模版
  1. <!-- 弹层 -->
  2. <div class="layer">
  3. <h4>{{currCategory&&currCategory.id==='brand'?'品牌':'分类'}}推荐 <small>根据您的购买或浏览记录推荐</small></h4>
  4. <ul v-if="currCategory && currCategory.goods && currCategory.goods.length">
  5. <li v-for="item in currCategory.goods" :key="item.id">
  6. <RouterLink to="/">
  7. <img :src="item.picture" alt="">
  8. <div class="info">
  9. <p class="name ellipsis-2">{{item.name}}</p>
  10. <p class="desc ellipsis">{{item.desc}}</p>
  11. <p class="price"><i>¥</i>{{item.price}}</p>
  12. </div>
  13. </RouterLink>
  14. </li>
  15. </ul>
  16. </div>

首页主体-左侧分类-处理品牌

目的: 品牌展示特殊,需要额外获取数据和额外的布局。

大致步骤:

  • 定义API接口,在 home-category.vue 组件获取数据。
  • 完成基础布局,根据数据进行渲染。
  • 处理左侧分类激活显示。

落地代码:

  1. 定义API接口,在 home-category.vue 组件获取数据。

src/api/home.js

  1. export const findBrand = (limit) => {
  2. return request('/home/brand', 'get', {limit})
  3. }

src/views/home/components/home-category.vue

  1. const brand = reactive({
  2. id: 'brand',
  3. name: '品牌',
  4. children: [{ id: 'brand-children', name: '品牌推荐' }],
  5. + brands: []
  6. })
  1. +import { findBrand } from '@/api/home.js'
  2. // ... 省略代码
  3. setup () {
  4. // ... 省略代码
  5. + findBrand().then(data=>{
  6. + brand.brandsgoods = data.result
  7. + })
  8. return { list, categoryId, currCategory }
  9. }
  1. 进行渲染:src/views/home/components/home-category.vue
  • 布局样式
  1. <ul>
  2. <li class="brand" v-for="i in 6" :key="i">
  3. <RouterLink to="/">
  4. <img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/brand_goods_1.jpg" alt="">
  5. <div class="info">
  6. <p class="place"><i class="iconfont icon-dingwei"></i>北京</p>
  7. <p class="name ellipsis">DW</p>
  8. <p class="desc ellipsis-2">DW品牌闪购</p>
  9. </div>
  10. </RouterLink>
  11. </li>
  12. </ul>
  1. li.brand {
  2. height: 180px;
  3. a {
  4. align-items: flex-start;
  5. img {
  6. width: 120px;
  7. height: 160px;
  8. }
  9. .info {
  10. p {
  11. margin-top: 8px;
  12. }
  13. .place {
  14. color: #999;
  15. }
  16. }
  17. }
  18. }
  • 进行渲染
  1. + <ul v-if="currCategory && currCategory.brands && currCategory.brands.length">
  2. + <li class="brand" v-for="item in currCategory.brands" :key="item.id">
  3. + <RouterLink to="/">
  4. + <img :src="item.picture" alt="">
  5. + <div class="info">
  6. + <p class="place"><i class="iconfont icon-dingwei"></i>{{item.place}}</p>
  7. + <p class="name ellipsis">{{item.name}}</p>
  8. + <p class="desc ellipsis-2">{{item.desc}}</p>
  9. + </div>
  10. + </RouterLink>
  11. + </li>
  12. + </ul>
  1. 处理左侧分类激活显示 src/views/home/components/home-category.vue
  • 激活类active
  1. .menu {
  2. li {
  3. padding-left: 40px;
  4. height: 50px;
  5. line-height: 50px;
  6. + &:hover,&.active {
  7. background: @xtxColor;
  8. }
  • 绑定类
  1. <ul class="menu">
  2. + <li :class="{active:categoryId===item.id}"
  • 移除类
  1. + <div class='home-category' @mouseleave="categoryId=null">
  2. <ul class="menu">

总结: 品牌数据需要请求后台,再汇总到所有数据中,然后渲染,然后激活当前的分类。

首页主体-左侧分类-骨架效果

目的: 为了在加载的过程中等待效果更好,封装一个骨架屏组件。

大致步骤:

  • 需要一个组件,做占位使用。这个占位组件有个专业术语:骨架屏组件。
    • 暴露一些属性:高,宽,背景,是否有闪动画。
  • 这是一个公用组件,需要全局注册,将来这样的组件建议再vue插件中定义。
  • 使用组件完成左侧分类骨架效果。

落的代码:

  1. 封装组件:src/components/lib/xtx-skeleton.vue
  1. <template>
  2. <div class="xtx-skeleton" :style="{width,height}" :class="{shan:animated}">
  3. <!-- 1 盒子-->
  4. <div class="block" :style="{backgroundColor:bg}"></div>
  5. <!-- 2 闪效果 xtx-skeleton 伪元素 --->
  6. </div>
  7. </template>
  8. <script>
  9. export default {
  10. name: 'XtxSkeleton',
  11. // 使用的时候需要动态设置 高度,宽度,背景颜色,是否闪下
  12. props: {
  13. bg: {
  14. type: String,
  15. default: '#efefef'
  16. },
  17. width: {
  18. type: String,
  19. default: '100px'
  20. },
  21. height: {
  22. type: String,
  23. default: '100px'
  24. },
  25. animated: {
  26. type: Boolean,
  27. default: false
  28. }
  29. }
  30. }
  31. </script>
  32. <style scoped lang="less">
  33. .xtx-skeleton {
  34. display: inline-block;
  35. position: relative;
  36. overflow: hidden;
  37. vertical-align: middle;
  38. .block {
  39. width: 100%;
  40. height: 100%;
  41. border-radius: 2px;
  42. }
  43. }
  44. .shan {
  45. &::after {
  46. content: "";
  47. position: absolute;
  48. animation: shan 1.5s ease 0s infinite;
  49. top: 0;
  50. width: 50%;
  51. height: 100%;
  52. background: linear-gradient(
  53. to left,
  54. rgba(255, 255, 255, 0) 0,
  55. rgba(255, 255, 255, 0.3) 50%,
  56. rgba(255, 255, 255, 0) 100%
  57. );
  58. transform: skewX(-45deg);
  59. }
  60. }
  61. @keyframes shan {
  62. 0% {
  63. left: -100%;
  64. }
  65. 100% {
  66. left: 120%;
  67. }
  68. }
  69. </style>
  1. 封装插件:插件定义 src/componets/library/index.js 使用插件 src/main.js
  1. // 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法,注意:没有全局过滤器。
  2. // 这就是插件
  3. // vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
  4. // vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展
  5. import XtxSkeleton from './xtx-skeleton.vue'
  6. export default {
  7. install (app) {
  8. // 在app上进行扩展,app提供 component directive 函数
  9. // 如果要挂载原型 app.config.globalProperties 方式
  10. app.component(XtxSkeleton.name, XtxSkeleton)
  11. }
  12. }
  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. import router from './router'
  4. import store from './store'
  5. import './mock'
  6. +import ui from './components/library'
  7. import 'normalize.css'
  8. import '@/assets/styles/common.less'
  9. +// 插件的使用,在main.js使用app.use(插件)
  10. +createApp(App).use(store).use(router).use(ui).mount('#app')
  1. 最后使用组件完成左侧分类骨架效果: src/views/home/components/home-category.vue
  1. <ul class="menu">
  2. <li :class="{active:categoryId===item.id}" v-for="item in menuList" :key="item.id" @mouseenter="categoryId=item.id">
  3. <RouterLink to="/">{{item.name}}</RouterLink>
  4. <template v-if="item.children">
  5. <RouterLink to="/" v-for="sub in item.children" :key="sub.id">{{sub.name}}</RouterLink>
  6. </template>
  7. + <span v-else>
  8. + <XtxSkeleton width="60px" height="18px" style="margin-right:5px" bg="rgba(255,255,255,0.2)" />
  9. + <XtxSkeleton width="50px" height="18px" bg="rgba(255,255,255,0.2)" />
  10. + </span>
  11. </li>
  12. </ul>
  1. .xtx-skeleton {
  2. animation: fade 1s linear infinite alternate;
  3. }
  4. @keyframes fade {
  5. from {
  6. opacity: 0.2;
  7. }
  8. to {
  9. opacity: 1;
  10. }
  11. }

首页主体-轮播图-基础布局

目的: 封装小兔鲜轮播图组件,第一步:基础结构的使用。

大致步骤:

  • 准备xtx-carousel组件基础布局,全局注册
  • 准备home-banner组件,使用xtx-carousel组件,再首页注册使用。
  • 深度作用xtx-carousel组件的默认样式

落的代码:

  • 轮播图基础结构 src/components/library/xtx-carousel.vue
  1. <template>
  2. <div class='xtx-carousel'>
  3. <ul class="carousel-body">
  4. <li class="carousel-item fade">
  5. <RouterLink to="/">
  6. <img src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-15/1ba86bcc-ae71-42a3-bc3e-37b662f7f07e.jpg" alt="">
  7. </RouterLink>
  8. </li>
  9. </ul>
  10. <a href="javascript:;" class="carousel-btn prev"><i class="iconfont icon-angle-left"></i></a>
  11. <a href="javascript:;" class="carousel-btn next"><i class="iconfont icon-angle-right"></i></a>
  12. <div class="carousel-indicator">
  13. <span v-for="i in 5" :key="i"></span>
  14. </div>
  15. </div>
  16. </template>
  17. <script>
  18. export default {
  19. name: 'XtxCarousel'
  20. }
  21. </script>
  22. <style scoped lang="less">
  23. .xtx-carousel{
  24. width: 100%;
  25. height: 100%;
  26. min-width: 300px;
  27. min-height: 150px;
  28. position: relative;
  29. .carousel{
  30. &-body {
  31. width: 100%;
  32. height: 100%;
  33. }
  34. &-item {
  35. width: 100%;
  36. height: 100%;
  37. position: absolute;
  38. left: 0;
  39. top: 0;
  40. opacity: 0;
  41. transition: opacity 0.5s linear;
  42. &.fade {
  43. opacity: 1;
  44. z-index: 1;
  45. }
  46. img {
  47. width: 100%;
  48. height: 100%;
  49. }
  50. }
  51. &-indicator {
  52. position: absolute;
  53. left: 0;
  54. bottom: 20px;
  55. z-index: 2;
  56. width: 100%;
  57. text-align: center;
  58. span {
  59. display: inline-block;
  60. width: 12px;
  61. height: 12px;
  62. background: rgba(0,0,0,0.2);
  63. border-radius: 50%;
  64. cursor: pointer;
  65. ~ span {
  66. margin-left: 12px;
  67. }
  68. &.active {
  69. background: #fff;
  70. }
  71. }
  72. }
  73. &-btn {
  74. width: 44px;
  75. height: 44px;
  76. background: rgba(0,0,0,.2);
  77. color: #fff;
  78. border-radius: 50%;
  79. position: absolute;
  80. top: 228px;
  81. z-index: 2;
  82. text-align: center;
  83. line-height: 44px;
  84. opacity: 0;
  85. transition: all 0.5s;
  86. &.prev{
  87. left: 20px;
  88. }
  89. &.next{
  90. right: 20px;
  91. }
  92. }
  93. }
  94. &:hover {
  95. .carousel-btn {
  96. opacity: 1;
  97. }
  98. }
  99. }
  100. </style>
  • 全局注册轮播图 src/components/library/index.js
  1. import XtxSkeleton from './xtx-skeleton.vue'
  2. +import XtxCarousel from './xtx-carousel.vue'
  3. export default {
  4. install (app) {
  5. app.component(XtxSkeleton.name, XtxSkeleton)
  6. + app.component(XtxCarousel.name, XtxCarousel)
  7. }
  8. }
  • 首页广告组件基础结构 src/views/home/components/home-banner.vue
  1. <template>
  2. <div class="home-banner">
  3. <XtxCarousel />
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: 'HomeBanner'
  9. }
  10. </script>
  11. <style scoped lang="less">
  12. .home-banner {
  13. width: 1240px;
  14. height: 500px;
  15. position: absolute;
  16. left: 0;
  17. top: 0;
  18. z-index: 98
  19. }
  20. </style>
  • 首页使用广告组件
  1. <template>
  2. + <!-- 首页入口 -->
  3. + <div class="home-entry">
  4. + <div class="container">
  5. <!-- 左侧分类 -->
  6. <HomeCategory />
  7. <!-- 轮播图 -->
  8. <HomeBanner />
  9. </div>
  10. </div>
  11. </template>
  12. <script>
  13. import HomeCategory from './components/home-category'
  14. +import HomeBanner from './components/home-banner'
  15. export default {
  16. name: 'HomePage',
  17. components: {
  18. + HomeCategory,
  19. HomeBanner
  20. }
  21. }
  22. </script>
  23. <style scoped lang="less"></style>
  • 覆盖轮播图组件样式 src/views/home/components/home-banner.vue
  1. .xtx-carousel {
  2. ::v-deep .carousel-btn.prev {
  3. left: 270px;
  4. }
  5. ::v-deep .carousel-indicator {
  6. padding-left: 250px;
  7. }
  8. }

总结: 需要注意要覆盖样式,首页轮播图特殊些。

首页主体-轮播图-渲染结构

目的: 封装小兔鲜轮播图组件,第二步:动态渲染结构。

大致步骤:

  • 定义获取广告图API函数
  • 在home-banner组件获取轮播图数据,传递给xtx-carousel组件
  • 在xtx-carousel组件完成渲染

落的代码:

  • API函数 src/api/home.js
  1. /**
  2. * 获取广告图
  3. * @returns Promise
  4. */
  5. export const findBanner = () => {
  6. return request('/home/banner', 'get')
  7. }
  • 广告组件获取数据,传给轮播图 src/views/home/components/home-banner.vue
  1. <template>
  2. <div class="home-banner">
  3. + <XtxCarousel :sliders="sliders" />
  4. </div>
  5. </template>
  6. <script>
  7. import { ref } from 'vue'
  8. import { findBanner } from '@/api/home'
  9. export default {
  10. name: 'HomeBanner',
  11. + setup () {
  12. + const sliders = ref([])
  13. + findBanner().then(data => {
  14. + sliders.value = data.result
  15. + })
  16. + return { sliders }
  17. + }
  18. }
  19. </script>
  • 完成轮播图结构渲染 src/components/library/xtx-carousel.vue
  1. <template>
  2. <div class='xtx-carousel'>
  3. <ul class="carousel-body">
  4. + <li class="carousel-item" v-for="(item,i) in sliders" :key="i" :class="{fade:index===i}">
  5. <RouterLink to="/">
  6. + <img :src="item.imgUrl" alt="">
  7. </RouterLink>
  8. </li>
  9. </ul>
  10. <a href="javascript:;" class="carousel-btn prev"><i class="iconfont icon-angle-left"></i></a>
  11. <a href="javascript:;" class="carousel-btn next"><i class="iconfont icon-angle-right"></i></a>
  12. <div class="carousel-indicator">
  13. + <span v-for="(item,i) in sliders" :key="i" :class="{active:index===i}"></span>
  14. </div>
  15. </div>
  16. </template>
  17. <script>
  18. +import { ref } from 'vue'
  19. export default {
  20. name: 'XtxCarousel',
  21. + props: {
  22. + sliders: {
  23. + type: Array,
  24. + default: () => []
  25. + }
  26. + },
  27. + setup () {
  28. + // 默认显示的图片的索引
  29. + const index = ref(0)
  30. + return { index }
  31. + }
  32. }
  33. </script>

总结: fade是控制显示那张图片的,需要一个默认索引数据,渲染第一张图和激活第一个点。

首页主体-轮播图-逻辑封装

目的: 封装小兔鲜轮播图组件,第三步:逻辑功能实现。

大致步骤:

  • 自动播放,暴露自动轮播属性,设置了就自动轮播
  • 如果有自动播放,鼠标进入离开,暂停,开启
  • 指示器切换,上一张,下一张
  • 销毁组件,清理定时器

落地代码: src/components/library/xtx-carousel.vue

  • 自动轮播实现
  1. +import { ref, watch } from 'vue'
  2. export default {
  3. name: 'XtxCarousel',
  4. props: {
  5. sliders: {
  6. type: Array,
  7. default: () => []
  8. },
  9. + duration: {
  10. + type: Number,
  11. + default: 3000
  12. + },
  13. + autoPlay: {
  14. + type: Boolean,
  15. + default: false
  16. + }
  17. },
  18. setup (props) {
  19. // 默认显示的图片的索引
  20. const index = ref(0)
  21. + // 自动播放
  22. + let timer = null
  23. + const autoPlayFn = () => {
  24. + clearInterval(timer)
  25. + timer = setInterval(() => {
  26. + index.value++
  27. + if (index.value >= props.sliders.length) {
  28. + index.value = 0
  29. + }
  30. + }, props.duration)
  31. + }
  32. + watch(() => props.sliders, (newVal) => {
  33. + // 有数据&开启自动播放,才调用自动播放函数
  34. + if (newVal.length && props.autoPlay) {
  35. + index.value = 0
  36. + autoPlayFn()
  37. + }
  38. + }, { immediate: true })
  39. +
  40. return { index }
  41. }
  42. }
  • 如果有自动播放,鼠标进入离开,暂停,开启
  1. // 鼠标进入停止,移出开启自动,前提条件:autoPlay为true
  2. const stop = () => {
  3. if (timer) clearInterval(timer)
  4. }
  5. const start = () => {
  6. if (props.sliders.length && props.autoPlay) {
  7. autoPlayFn()
  8. }
  9. }
  10. return { index, stop, start }
  1. + <div class='xtx-carousel' @mouseenter="stop()" @mouseleave="start()">

使用需要加 auto-play <XtxCarousel auto-play :sliders="sliders" />

  • 指示器切换,上一张,下一张
  1. // 上一张下一张
  2. const toggle = (step) => {
  3. const newIndex = index.value + step
  4. if (newIndex >= props.sliders.length) {
  5. index.value = 0
  6. return
  7. }
  8. if (newIndex < 0) {
  9. index.value = props.sliders.length - 1
  10. return
  11. }
  12. index.value = newIndex
  13. }
  14. return { index, stop, start, toggle }
  • 销毁组件,清理定时器
  1. // 组件消耗,清理定时器
  2. onUnmounted(() => {
  3. clearInterval(timer)
  4. })

总结: 按照思路步骤,一步步实现即可。

首页主体-面板封装

目的: 提取首页的公用面板进行复用

大致思路:

  • 头部
    • 标题和副标题由props传入
    • 右侧内容由插槽传入
      • 查看更多使用次数多封装成全局组件
  • 主体

    • 全部由插槽传入


    1606296832501.png

实现步骤:

  • 查看更多全局组件实现

src/components/library/xtx-more.vue 定义

  1. <template>
  2. <RouterLink :to="path" class="xtx-more">
  3. <span>查看全部</span>
  4. <i class="iconfont icon-angle-right"></i>
  5. </RouterLink>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'XtxMore',
  10. props: {
  11. path: {
  12. type: String,
  13. default: '/'
  14. }
  15. }
  16. }
  17. </script>
  18. <style scoped lang='less'>
  19. .xtx-more {
  20. margin-bottom: 2px;
  21. span {
  22. font-size: 16px;
  23. vertical-align: middle;
  24. margin-right: 4px;
  25. color: #999;
  26. }
  27. i {
  28. font-size: 14px;
  29. vertical-align: middle;
  30. position: relative;
  31. top: 2px;
  32. color: #ccc;
  33. }
  34. &:hover {
  35. span,i {
  36. color: @xtxColor;
  37. }
  38. }
  39. }
  40. </style>

src/components/library/index.js 注册

  1. import XtxSkeleton from './xtx-skeleton.vue'
  2. import XtxCarousel from './xtx-carousel.vue'
  3. +import XtxMore from './xtx-more.vue'
  4. export default {
  5. install (app) {
  6. app.component(XtxSkeleton.name, XtxSkeleton)
  7. app.component(XtxCarousel.name, XtxCarousel)
  8. + app.component(XtxMore.name, XtxMore)
  9. }
  10. }
  • 定义首页需要的面板组件
  • src/views/home/components/home-pannel.vue 定义
  1. <template>
  2. <div class="home-panel">
  3. <div class="container">
  4. <div class="head">
  5. <h3>{{ title }}<small>{{ subTitle }}</small></h3>
  6. <slot name="right" />
  7. </div>
  8. <slot />
  9. </div>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. name: 'HomePanel',
  15. props: {
  16. title: {
  17. type: String,
  18. default: ''
  19. },
  20. subTitle: {
  21. type: String,
  22. default: ''
  23. }
  24. }
  25. }
  26. </script>
  27. <style scoped lang='less'>
  28. .home-panel {
  29. background-color: #fff;
  30. .head {
  31. padding: 40px 0;
  32. display: flex;
  33. align-items: flex-end;
  34. h3 {
  35. flex: 1;
  36. font-size: 32px;
  37. font-weight: normal;
  38. margin-left: 6px;
  39. height: 35px;
  40. line-height: 35px;
  41. small {
  42. font-size: 16px;
  43. color: #999;
  44. margin-left: 20px;
  45. }
  46. }
  47. }
  48. }
  49. </style>

首页主体-新鲜好物

目的: 使用面板组件完成新鲜好物模块。

大致步骤:

  • 封装API调用接口
  • 进行组件基础布局
  • 调用接口渲染组件

落地代码:

src/api/home.js

  1. // 获取新鲜好物接口数据
  2. export const findNew = () => {
  3. return request({
  4. method: 'get',
  5. url: 'home/new'
  6. })
  7. }
  • src/views/home/components/home-new.vue 定义
  1. <template>
  2. <div class="home-new">
  3. <HomePanel title="新鲜好物" sub-title="新鲜出炉 品质靠谱">
  4. <!-- 具名插槽注入右侧内容 -->
  5. <template #right>
  6. <XtxMore path="/" />
  7. </template>
  8. <!-- 面板内容:注入默认插槽中 -->
  9. <ul class="goods-list">
  10. <li v-for="item in goods" :key="item.id">
  11. <RouterLink :to="`/product/${item.id}`">
  12. <img :src="item.picture" alt="" />
  13. <p class="name ellipsis">{{ item.name }}</p>
  14. <p class="price">&yen;{{ item.price }}</p>
  15. </RouterLink>
  16. </li>
  17. </ul>
  18. </HomePanel>
  19. </div>
  20. </template>
  21. <script>
  22. import { ref } from 'vue'
  23. import HomePanel from './home-pannel'
  24. import { findNew } from '@/api/home'
  25. export default {
  26. name: 'HomeNew',
  27. components: { HomePanel },
  28. setup () {
  29. const goods = ref([])
  30. findNew().then(data => {
  31. goods.value = data.result
  32. })
  33. return { goods }
  34. }
  35. }
  36. </script>
  37. <style scoped lang="less">
  38. // 鼠标经过上移阴影动画
  39. .hoverShadow () {
  40. transition: all 0.5s;
  41. &:hover {
  42. transform: translate3d(0, -3px, 0);
  43. box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
  44. }
  45. }
  46. .goods-list {
  47. display: flex;
  48. justify-content: space-between;
  49. height: 406px;
  50. li {
  51. width: 306px;
  52. height: 406px;
  53. background: #f0f9f4;
  54. .hoverShadow();
  55. img {
  56. width: 306px;
  57. height: 306px;
  58. }
  59. p {
  60. font-size: 22px;
  61. padding: 12px 30px 0 30px;
  62. text-align: center;
  63. }
  64. .price {
  65. color: @priceColor;
  66. }
  67. }
  68. }
  69. </style>

src/views/home/index.vue

  1. <!-- 新鲜好物 -->
  2. + <HomeNew />
  3. </div>
  4. </template>
  5. <script>
  6. import HomeCategory from './components/home-category'
  7. import HomeBanner from './components/home-banner'
  8. +import HomeNew from './components/home-new'
  9. export default {
  10. name: 'xtx-home-page',
  11. + components: { HomeCategory, HomeBanner, HomeNew }
  12. }
  13. </script>

总结: vue3.0中 只支持v-slot指令,所以需要配合template来使用。

首页主体-人气推荐

目的: 完成人气推荐模块

大致步骤:

  • 定义API函数
  • 定义组件且完成渲染
  • 在首页组件中导入使用

落地代码:

src/api/home.js

  1. export const findHot = () => {
  2. return request('home/hot', 'get')
  3. }

src/views/home/components/home-hot.vue

  1. <template>
  2. <HomePanel title="人气推荐" sub-title="人气爆款 不容错过">
  3. <ul ref="pannel" class="goods-list">
  4. <li v-for="item in goods" :key="item.id">
  5. <RouterLink to="/">
  6. <img :src="item.picture" alt="">
  7. <p class="name">{{item.title}}</p>
  8. <p class="desc">{{item.alt}}</p>
  9. </RouterLink>
  10. </li>
  11. </ul>
  12. </HomePanel>
  13. </template>
  14. <script>
  15. import { ref } from 'vue'
  16. import HomePanel from './home-panel'
  17. import { findHot } from '@/api/home'
  18. export default {
  19. name: 'HomeNew',
  20. components: { HomePanel },
  21. setup () {
  22. const goods = ref([])
  23. findHot().then(data => {
  24. goods.value = data.result
  25. })
  26. return { goods }
  27. }
  28. }
  29. </script>
  30. <style scoped lang='less'>
  31. .goods-list {
  32. display: flex;
  33. justify-content: space-between;
  34. height: 426px;
  35. li {
  36. width: 306px;
  37. height: 406px;
  38. .hoverShadow();
  39. img {
  40. width: 306px;
  41. height: 306px;
  42. }
  43. p {
  44. font-size: 22px;
  45. padding-top: 12px;
  46. text-align: center;
  47. }
  48. .desc {
  49. color: #999;
  50. font-size: 18px;
  51. }
  52. }
  53. }
  54. </style>

src/views/home/index.vue

  1. <!-- 新鲜好物 -->
  2. <HomeNew />
  3. <!-- 人气推荐 -->
  4. + <HomeHot />
  5. </div>
  6. </template>
  7. <script>
  8. import HomeCategory from './components/home-category'
  9. import HomeBanner from './components/home-banner'
  10. import HomeNew from './components/home-new'
  11. +import HomeHot from './components/home-hot'
  12. export default {
  13. name: 'xtx-home-page',
  14. + components: { HomeCategory, HomeBanner, HomeNew, HomeHot }
  15. }
  16. </script>

首页主体-补充-vue动画

目标: 知道vue中如何使用动画,知道Transition组件使用。

当vue中,显示隐藏,创建移除,一个元素或者一个组件的时候,可以通过transition实现动画。

1616576876892.png

如果元素或组件离开,完成一个淡出效果:

  1. <transition name="fade">
  2. <p v-if="show">100</p>
  3. </transition>
  1. .fade-leave {
  2. opacity: 1
  3. }
  4. .fade-leave-active {
  5. transition: all 1s;
  6. }
  7. .fade-leave-to {
  8. opcaity: 0
  9. }
  • 进入(显示,创建)
    • v-enter 进入前 (vue3.0 v-enter-from)
    • v-enter-active 进入中
    • v-enter-to 进入后
  • 离开(隐藏,移除)

    • v-leave 进入前 (vue3.0 v-leave-from)
    • v-leave-active 进入中
    • v-leave-to 进入后


    多个transition使用不同动画,可以添加nam属性,name属性的值替换v即可。

首页主体-面板骨架效果

目的: 加上面板的骨架加载效果

定义一个骨架布局组件:

src/views/home/components/home-skeleton.vue

  1. <template>
  2. <div class='home-skeleton'>
  3. <div class="item" v-for="i in 4" :key="i" :style="{backgroundColor:bg}">
  4. <XtxSkeleton bg="#e4e4e4" width="306px" height="306px" animated />
  5. <XtxSkeleton bg="#e4e4e4" width="160px" height="24px" animated />
  6. <XtxSkeleton bg="#e4e4e4" width="120px" height="24px" animated />
  7. </div>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. name: 'HomeSkeleton',
  13. props: {
  14. bg: {
  15. type: String,
  16. default: '#fff'
  17. }
  18. }
  19. }
  20. </script>
  21. <style scoped lang='less'>
  22. .home-skeleton {
  23. width: 1240px;
  24. height: 406px;
  25. display: flex;
  26. justify-content: space-between;
  27. .item {
  28. width: 306px;
  29. .xtx-skeleton ~ .xtx-skeleton{
  30. display: block;
  31. margin: 16px auto 0;
  32. }
  33. }
  34. }
  35. </style>
  • home-hot 组件分别使用
  1. <HomePanel title="人气推荐" sub-title="人气爆款 不容错过">
  2. + <div style="position: relative;height: 426px;">
  3. + <Transition name="fade">
  4. + <ul v-if="goods.length" ref="pannel" class="goods-list">
  5. <li v-for="item in goods" :key="item.id">
  6. <RouterLink to="/">
  7. <img :src="item.picture" alt="">
  8. <p class="name">{{item.title}}</p>
  9. <p class="desc">{{item.alt}}</p>
  10. </RouterLink>
  11. </li>
  12. </ul>
  13. + <HomeSkeleton v-else />
  14. + </Transition>
  15. + </div>
  16. </HomePanel>
  17. <script>
  18. + import HomeSkeleton from './home-skeleton.vue'
  19. </script>
  • home-new 组件分别使用
  1. <template>
  2. <HomePanel title="新鲜好物" sub-title="新鲜出炉 品质靠谱">
  3. <template v-slot:right><XtxMore /></template>
  4. + <div style="position: relative;height: 406px;">
  5. + <Transition name="fade">
  6. + <ul v-if="goods.length" ref="pannel" class="goods-list">
  7. <li v-for="item in goods" :key="item.id">
  8. <RouterLink to="/">
  9. <img :src="item.picture" alt="">
  10. <p class="name">{{item.name}}</p>
  11. <p class="price">&yen;{{item.price}}</p>
  12. </RouterLink>
  13. </li>
  14. </ul>
  15. + <HomeSkeleton bg="#f0f9f4" v-else />
  16. + </Transition>
  17. + </div>
  18. </HomePanel>
  19. </template>
  20. <script>
  21. + import HomeSkeleton from './home-skeleton.vue'
  22. </script>

src/assets/styles/common.less 定义动画

  1. .fade{
  2. &-leave {
  3. &-active {
  4. position: absolute;
  5. width: 100%;
  6. transition: opacity .5s .2s;
  7. z-index: 1;
  8. }
  9. &-to {
  10. opacity: 0;
  11. }
  12. }
  13. }

注意:

  • 动画的父容器需要是定位,防止定位跑偏。

首页主体-组件数据懒加载

目的: 实现当组件进入可视区域在加载数据。

我们可以使用 @vueuse/core 中的 useIntersectionObserver 来实现监听进入可视区域行为,但是必须配合vue3.0的组合API的方式才能实现。

大致步骤:

  • 理解 useIntersectionObserver 的使用,各个参数的含义
  • 改造 home-new 组件成为数据懒加载,掌握 useIntersectionObserver 函数的用法
  • 封装 useLazyData 函数,作为数据懒加载公用函数
  • home-newhome-hot 改造成懒加载方式

落的代码:

  1. 先分析下这个useIntersectionObserver 函数:
  1. // stop 是停止观察是否进入或移出可视区域的行为
  2. + // 1. 参数一target表示被监听的DOM元素
  3. + // 2.参数二是回调函数,用于通知监听的动作(回调函数的第一个形参isIntersecting表示被监听的元素已经进入可视区域)
  4. const { stop } = useIntersectionObserver(
  5. // target 是观察的目标dom容器,必须是dom容器,而且是vue3.0方式绑定的dom对象
  6. target,
  7. // isIntersecting 是否进入可视区域,true是进入 false是移出
  8. // observerElement 被观察的dom
  9. ([{ isIntersecting }], observerElement) => {
  10. // 在此处可根据isIntersecting来判断,然后做业务
  11. },
  12. )
  1. 开始改造 home-new 组件:rc/views/home/components/home-new.vue
  • 进入可视区后获取数据
  1. <div ref="box" style="position: relative;height: 406px;">
  2. // 省略。。。
  3. <script>
  4. import HomePanel from './home-panel'
  5. import HomeSkeleton from './home-skeleton'
  6. import { findNew } from '@/api/home'
  7. import { ref } from 'vue'
  8. import { useIntersectionObserver } from '@vueuse/core'
  9. export default {
  10. name: 'HomeNew',
  11. components: { HomePanel, HomeSkeleton },
  12. setup () {
  13. const goods = ref([])
  14. const box = ref(null)
  15. const { stop } = useIntersectionObserver(
  16. box,
  17. ([{ isIntersecting }]) => {
  18. if (isIntersecting) {
  19. stop()
  20. findNew().then(data => {
  21. goods.value = data.result
  22. })
  23. }
  24. }
  25. )
  26. return { goods, box }
  27. }
  28. }
  29. </script>
  1. 由于首页面板数据加载都需要实现懒数据加载,所以封装一个钩子函数,得到数据。

src/hooks/index.js

  1. // hooks 封装逻辑,提供响应式数据。
  2. import { useIntersectionObserver } from '@vueuse/core'
  3. import { ref } from 'vue'
  4. // 数据懒加载函数
  5. export const useLazyData = (apiFn) => {
  6. // 需要
  7. // 1. 被观察的对象
  8. // 2. 不同的API函数
  9. const target = ref(null)
  10. const result = ref([])
  11. const { stop } = useIntersectionObserver(
  12. target,
  13. ([{ isIntersecting }], observerElement) => {
  14. if (isIntersecting) {
  15. stop()
  16. // 调用API获取数据
  17. apiFn().then(data => {
  18. result.value = data.result
  19. })
  20. }
  21. }
  22. )
  23. // 返回--->数据(dom,后台数据)
  24. return { target, result }
  25. }
  1. 再次改造 home-new 组件:rc/views/home/components/home-new.vue
  1. import { findNew } from '@/api/home'
  2. +import { useLazyData } from '@/hooks'
  3. export default {
  4. name: 'HomeNew',
  5. components: { HomePanel, HomeSkeleton },
  6. setup () {
  7. + const { target, result } = useLazyData(findNew)
  8. + return { goods: result, target }
  9. }
  10. }
  1. 然后改造 home-hot 组件:src/views/home/components/home-hot.vue
  1. + <div ref="target" style="position: relative;height: 426px;">
  1. import { findHot } from '@/api/home'
  2. import HomePanel from './home-panel'
  3. import HomeSkeleton from './home-skeleton'
  4. +import { useLazyData } from '@/hooks'
  5. export default {
  6. name: 'HomeHot',
  7. components: { HomePanel, HomeSkeleton },
  8. setup () {
  9. + const { target, result } = useLazyData(findHot)
  10. + return { target, list: result }
  11. }
  12. }

首页主体-热门品牌

目的: 实现品牌的展示,和切换品牌效果。

基本步骤:

  • 准备基础布局组件
  • 获取数据实现渲染,完成切换效果
  • 加上骨架效果和数据懒加载

落的代码:

  1. 基础结构:src/views/home/components/home-brand.vue
  1. <template>
  2. <HomePanel title="热门品牌" sub-title="国际经典 品质保证">
  3. <template v-slot:right>
  4. <a href="javascript:;" class="iconfont icon-angle-left prev"></a>
  5. <a href="javascript:;" class="iconfont icon-angle-right next"></a>
  6. </template>
  7. <div class="box" ref="box">
  8. <ul class="list" >
  9. <li v-for="i in 10" :key="i">
  10. <RouterLink to="/">
  11. <img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/brand_goods_1.jpg" alt="">
  12. </RouterLink>
  13. </li>
  14. </ul>
  15. </div>
  16. </HomePanel>
  17. </template>
  18. <script>
  19. import HomePanel from './home-panel'
  20. export default {
  21. name: 'HomeBrand',
  22. components: { HomePanel }
  23. }
  24. </script>
  25. <style scoped lang='less'>
  26. .home-panel {
  27. background:#f5f5f5
  28. }
  29. .iconfont {
  30. width: 20px;
  31. height: 20px;
  32. background: #ccc;
  33. color: #fff;
  34. display: inline-block;
  35. text-align: center;
  36. margin-left: 5px;
  37. background: @xtxColor;
  38. &::before {
  39. font-size: 12px;
  40. position: relative;
  41. top: -2px
  42. }
  43. &.disabled {
  44. background: #ccc;
  45. cursor: not-allowed;
  46. }
  47. }
  48. .box {
  49. display: flex;
  50. width: 100%;
  51. height: 345px;
  52. overflow: hidden;
  53. padding-bottom: 40px;
  54. .list {
  55. width: 200%;
  56. display: flex;
  57. transition: all 1s;
  58. li {
  59. margin-right: 10px;
  60. width: 240px;
  61. &:nth-child(5n) {
  62. margin-right: 0;
  63. }
  64. img {
  65. width: 240px;
  66. height: 305px;
  67. }
  68. }
  69. }
  70. }
  71. </style>
  • 使用组件:src/views/home/index.vue
  1. <!-- 人气推荐 -->
  2. <HomeHot />
  3. <!-- 热门品牌 -->
  4. + <HomeBrand />
  1. +import HomeBrand from './components/home-brand'
  2. export default {
  3. name: 'xtx-home-page',
  4. + components: { HomeCategory, HomeBanner, HomeNew, HomeHot, HomeBrand }
  5. }
  1. 获取数据和切换效果:
  • 由于最后会使用到数据懒加载,那么我们也会使用组合API实现。
  • 业务上,只有两页数据切换,0—->1 或者 1—->0 的方式。
  1. <template>
  2. <HomePanel title="热门品牌" sub-title="国际经典 品质保证">
  3. <template v-slot:right>
  4. <a @click="toggle(-1)" :class="{disabled:index===0}" href="javascript:;" class="iconfont icon-angle-left prev"></a>
  5. <a @click="toggle(1)" :class="{disabled:index===1}" href="javascript:;" class="iconfont icon-angle-right next"></a>
  6. </template>
  7. <div class="box">
  8. <ul v-if="brands.length" class="list" :style="{transform:`translateX(${-index*1240}px)`}">
  9. <li v-for="item in brands" :key="item.id">
  10. <RouterLink to="/">
  11. <img :src="item.picture" alt="">
  12. </RouterLink>
  13. </li>
  14. </ul>
  15. </div>
  16. </HomePanel>
  17. </template>
  18. <script>
  19. import { ref } from 'vue'
  20. import HomePanel from './home-panel'
  21. import { findBrand } from '@/api/home'
  22. import { useLazyData } from '@/hooks'
  23. export default {
  24. name: 'HomeBrand',
  25. components: { HomePanel },
  26. setup () {
  27. // 获取数据
  28. const brands = ref([])
  29. findBrand(10).then(data => {
  30. brands.value = data.result
  31. })
  32. // 切换效果,前提只有 0 1 两页
  33. const index = ref(0)
  34. // 1. 点击上一页
  35. // 2. 点击下一页
  36. const toggle = (step) => {
  37. const newIndex = index.value + step
  38. if (newIndex < 0 || newIndex > 1) return
  39. index.value = newIndex
  40. }
  41. return { brands, toggle, index }
  42. }
  43. }
  44. </script>
  1. 加上数据懒加载和骨架效果
  1. <template>
  2. <HomePanel title="热门品牌" sub-title="国际经典 品质保证">
  3. <template v-slot:right>
  4. <a @click="toggle(-1)" :class="{disabled:index===0}" href="javascript:;" class="iconfont icon-angle-left prev"></a>
  5. <a @click="toggle(1)" :class="{disabled:index===1}" href="javascript:;" class="iconfont icon-angle-right next"></a>
  6. </template>
  7. + <div ref="target" class="box">
  8. + <Transition name="fade">
  9. + <ul v-if="brands.length" class="list" :style="{transform:`translateX(${-index*1240}px)`}">
  10. <li v-for="item in brands" :key="item.id">
  11. <RouterLink to="/">
  12. <img :src="item.picture" alt="">
  13. </RouterLink>
  14. </li>
  15. </ul>
  16. + <div v-else class="skeleton">
  17. + <XtxSkeleton class="item" v-for="i in 5" :key="i" animated bg="#e4e4e4" width="240px" height="305px"/>
  18. + </div>
  19. + </Transition>
  20. </div>
  21. </HomePanel>
  22. </template>
  23. <script>
  24. import { ref } from 'vue'
  25. import HomePanel from './home-panel'
  26. import { findBrand } from '@/api/home'
  27. +import { useLazyData } from '@/hooks'
  28. export default {
  29. name: 'HomeBrand',
  30. components: { HomePanel },
  31. setup () {
  32. // 获取数据
  33. // const brands = ref([])
  34. // findBrand(10).then(data => {
  35. // brands.value = data.result
  36. // })
  37. + // 注意:useLazyData需要的是API函数,如果遇到要传参的情况,自己写函数再函数中调用API
  38. + const { target, result } = useLazyData(() => findBrand(10))
  39. // 切换效果,前提只有 0 1 两页
  40. const index = ref(0)
  41. // 1. 点击上一页
  42. // 2. 点击下一页
  43. const toggle = (step) => {
  44. const newIndex = index.value + step
  45. if (newIndex < 0 || newIndex > 1) return
  46. index.value = newIndex
  47. }
  48. + return { brands: result, toggle, index, target }
  49. }
  50. }
  51. </script>
  1. .skeleton {
  2. width: 100%;
  3. display: flex;
  4. .item {
  5. margin-right: 10px;
  6. &:nth-child(5n) {
  7. margin-right: 0;
  8. }
  9. }
  10. }

总结: 注意下useLazyData传参的情况。

首页主体-商品区块

目的: 完成商品区域展示。

大致步骤:

  • 准备一个商品盒子组件 home-goods 展示单个商品
  • 定义产品区块组件 home-product 使用 home-goods 完成基础布局
  • 在首页中使用 home-product 组件
  • 定义API函数,获取数据,进行渲染
  • 处理板块需要进入可视区太多内容才能加载数据问题。

落地代码:

  1. 单个商品组件:src/views/home/components/home-goods.vue
  1. <template>
  2. <div class="goods-item">
  3. <RouterLink to="/" class="image">
  4. <img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/fresh_goods_1.jpg" alt="" />
  5. </RouterLink>
  6. <p class="name ellipsis-2">美威 智利原味三文鱼排 240g/袋 4片装</p>
  7. <p class="desc">海鲜年货</p>
  8. <p class="price">&yen;108.00</p>
  9. <div class="extra">
  10. <RouterLink to="/">
  11. <span>找相似</span>
  12. <span>发现现多宝贝 &gt;</span>
  13. </RouterLink>
  14. </div>
  15. </div>
  16. </template>
  17. <script>
  18. export default {
  19. name: 'HomeGoods'
  20. }
  21. </script>
  22. <style scoped lang='less'>
  23. .goods-item {
  24. width: 240px;
  25. height: 300px;
  26. padding: 10px 30px;
  27. position: relative;
  28. overflow: hidden;
  29. border: 1px solid transparent;
  30. transition: all .5s;
  31. .image {
  32. display: block;
  33. width: 160px;
  34. height: 160px;
  35. margin: 0 auto;
  36. img {
  37. width: 100%;
  38. height: 100%;
  39. }
  40. }
  41. p {
  42. margin-top: 6px;
  43. font-size: 16px;
  44. &.name {
  45. height: 44px;
  46. }
  47. &.desc {
  48. color: #666;
  49. height: 22px;
  50. }
  51. &.price {
  52. margin-top: 10px;
  53. font-size: 20px;
  54. color: @priceColor;
  55. }
  56. }
  57. .extra {
  58. position: absolute;
  59. left: 0;
  60. bottom: 0;
  61. height: 86px;
  62. width: 100%;
  63. background: @xtxColor;
  64. text-align: center;
  65. transform: translate3d(0,100%,0);
  66. transition: all .5s;
  67. span {
  68. display: block;
  69. color: #fff;
  70. width: 120px;
  71. margin: 0 auto;
  72. line-height: 30px;
  73. &:first-child {
  74. font-size: 18px;
  75. border-bottom:1px solid #fff;
  76. line-height: 40px;
  77. margin-top: 5px;
  78. }
  79. }
  80. }
  81. &:hover {
  82. border-color: @xtxColor;
  83. .extra {
  84. transform: none;
  85. }
  86. }
  87. }
  88. </style>
  1. 产品区块组件:src/views/home/components/home-product.vue
  1. <template>
  2. <div class="home-product">
  3. <HomePanel title="生鲜" v-for="i in 4" :key="i">
  4. <template v-slot:right>
  5. <div class="sub">
  6. <RouterLink to="/">海鲜</RouterLink>
  7. <RouterLink to="/">水果</RouterLink>
  8. <RouterLink to="/">蔬菜</RouterLink>
  9. <RouterLink to="/">水产</RouterLink>
  10. <RouterLink to="/">禽肉</RouterLink>
  11. </div>
  12. <XtxMore />
  13. </template>
  14. <div class="box">
  15. <RouterLink class="cover" to="/">
  16. <img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/fresh_goods_cover.jpg" alt="">
  17. <strong class="label">
  18. <span>生鲜馆</span>
  19. <span>全场3件7折</span>
  20. </strong>
  21. </RouterLink>
  22. <ul class="goods-list">
  23. <li v-for="i in 8" :key="i">
  24. <HomeGoods />
  25. </li>
  26. </ul>
  27. </div>
  28. </HomePanel>
  29. </div>
  30. </template>
  31. <script>
  32. import HomePanel from './home-panel'
  33. import HomeGoods from './home-goods'
  34. export default {
  35. name: 'HomeProduct',
  36. components: { HomePanel, HomeGoods }
  37. }
  38. </script>
  39. <style scoped lang='less'>
  40. .home-product {
  41. background: #fff;
  42. height: 2900px;
  43. .sub {
  44. margin-bottom: 2px;
  45. a {
  46. padding: 2px 12px;
  47. font-size: 16px;
  48. border-radius: 4px;
  49. &:hover {
  50. background: @xtxColor;
  51. color: #fff;
  52. }
  53. &:last-child {
  54. margin-right: 80px;
  55. }
  56. }
  57. }
  58. .box {
  59. display: flex;
  60. .cover {
  61. width: 240px;
  62. height: 610px;
  63. margin-right: 10px;
  64. position: relative;
  65. img {
  66. width: 100%;
  67. height: 100%;
  68. }
  69. .label {
  70. width: 188px;
  71. height: 66px;
  72. display: flex;
  73. font-size: 18px;
  74. color: #fff;
  75. line-height: 66px;
  76. font-weight: normal;
  77. position: absolute;
  78. left: 0;
  79. top: 50%;
  80. transform: translate3d(0,-50%,0);
  81. span {
  82. text-align: center;
  83. &:first-child {
  84. width: 76px;
  85. background: rgba(0,0,0,.9);
  86. }
  87. &:last-child {
  88. flex: 1;
  89. background: rgba(0,0,0,.7);
  90. }
  91. }
  92. }
  93. }
  94. .goods-list {
  95. width: 990px;
  96. display: flex;
  97. flex-wrap: wrap;
  98. li {
  99. width: 240px;
  100. height: 300px;
  101. margin-right: 10px;
  102. margin-bottom: 10px;
  103. &:nth-last-child(-n+4) {
  104. margin-bottom: 0;
  105. }
  106. &:nth-child(4n) {
  107. margin-right: 0;
  108. }
  109. }
  110. }
  111. }
  112. }
  113. </style>
  1. 使用组件:src/views/home/index.vue
  1. <!-- 人气推荐 -->
  2. <HomeHot />
  3. <!-- 热门品牌 -->
  4. <HomeBrand />
  5. <!-- 商品区域 -->
  6. + <HomeProduct />
  1. +import HomeProduct from './components/home-product'
  2. export default {
  3. name: 'xtx-home-page',
  4. + components: { HomeCategory, HomeBanner, HomeNew, HomeHot, HomeBrand, HomeProduct }
  5. }
  1. 获取数据渲染:
  • 定义API src/api/home.js
  1. // 获取商品信息数据
  2. export const findGoods = () => {
  3. return request({
  4. method: 'get',
  5. url: 'home/goods'
  6. })
  7. }
  • 进行渲染

src/views/home/components/home-product.vue

  1. <template>
  2. <div class="home-product" ref="homeProduct">
  3. + <HomePanel :title="cate.name" v-for="cate in data" :key="cate.id">
  4. <template v-slot:right>
  5. <div class="sub">
  6. + <RouterLink v-for="sub in cate.children" :key="sub.id" to="/">{{sub.name}}</RouterLink>
  7. </div>
  8. <XtxMore />
  9. </template>
  10. <div class="box">
  11. <RouterLink class="cover" to="/">
  12. + <img :src="cate.picture" alt="">
  13. <strong class="label">
  14. + <span>{{cate.name}}馆</span>
  15. + <span>{{cate.saleInfo}}</span>
  16. </strong>
  17. </RouterLink>
  18. <ul class="goods-list">
  19. + <li v-for="item in cate.goods" :key="item.id">
  20. + <HomeGoods :goods="item" />
  21. </li>
  22. </ul>
  23. </div>
  24. </HomePanel>
  25. </div>
  26. </template>
  27. <script>
  28. import HomePanel from './home-panel'
  29. import HomeGoods from './home-goods'
  30. +import { findGoods } from '@/api/home'
  31. +import { useLazyData } from '@/hooks'
  32. export default {
  33. name: 'HomeProduct',
  34. components: { HomePanel, HomeGoods },
  35. + setup () {
  36. + const { container, data } = useLazyData(findGoods)
  37. + return { homeProduct: container, data }
  38. + }
  39. }
  40. </script>

src/views/home/components/home-goods.vue

  1. <template>
  2. <div class="goods-item">
  3. <RouterLink to="/" class="image">
  4. + <img :src="goods.picture" alt="" />
  5. </RouterLink>
  6. + <p class="name ellipsis-2">{{goods.name}}</p>
  7. + <p class="desc">{{goods.tag}}</p>
  8. + <p class="price">&yen;{{goods.price}}</p>
  9. <div class="extra">
  10. <RouterLink to="/">
  11. <span>找相似</span>
  12. <span>发现现多宝贝 &gt;</span>
  13. </RouterLink>
  14. </div>
  15. </div>
  16. </template>
  17. <script>
  18. export default {
  19. name: 'HomeGoods',
  20. + props: {
  21. + goods: {
  22. + type: Object,
  23. + default: () => {}
  24. + }
  25. + }
  26. }
  27. </script>
  1. 处理问题:
  • 产品区域需要滚动比较多才能去加载数据。
  1. const { stop } = useIntersectionObserver(
  2. container,
  3. ([{ isIntersecting }], dom) => {
  4. if (isIntersecting) {
  5. stop()
  6. apiFn && apiFn().then(({ result }) => {
  7. data.value = result
  8. })
  9. }
  10. + }, {
  11. + threshold: 0
  12. + }
  13. )
  • threshold 容器和可视区交叉的占比(进入的面积/容器完整面试) 取值,0-1 之间,默认比0大,所以需要滚动较多才能触发进入可视区域事件。

首页主体-最新专题

目的: 完成最新专题展示。

基础布局:src/views/home/components/home-special.vue

  1. <template>
  2. <HomePanel title="最新专题">
  3. <template v-slot:right><XtxMore /></template>
  4. <div class="special-list" ref="homeSpecial">
  5. <div class="special-item" v-for="i in 3" :key="i">
  6. <RouterLink to="/">
  7. <img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/topic_goods_1.jpg" alt />
  8. <div class="meta">
  9. <p class="title">
  10. <span class="top ellipsis">看到撒娇的撒娇的凯撒就</span>
  11. <span class="sub ellipsis">倒萨倒萨倒萨</span>
  12. </p>
  13. <span class="price">&yen;19.99起</span>
  14. </div>
  15. </RouterLink>
  16. <div class="foot">
  17. <span class="like"><i class="iconfont icon-hart1"></i>100</span>
  18. <span class="view"><i class="iconfont icon-see"></i>100</span>
  19. <span class="reply"><i class="iconfont icon-message"></i>100</span>
  20. </div>
  21. </div>
  22. </div>
  23. </HomePanel>
  24. </template>
  25. <script>
  26. import HomePanel from './home-panel'
  27. export default {
  28. name: 'HomeSpecial',
  29. components: { HomePanel }
  30. }
  31. </script>
  32. <style scoped lang='less'>
  33. .home-panel {
  34. background: #f5f5f5;
  35. }
  36. .special-list {
  37. height: 380px;
  38. padding-bottom: 20px;
  39. display: flex;
  40. justify-content: space-between;
  41. .special-item {
  42. width: 404px;
  43. background: #fff;
  44. .hoverShadow();
  45. a {
  46. display: block;
  47. width: 100%;
  48. height: 288px;
  49. position: relative;
  50. img {
  51. width: 100%;
  52. height: 100%;
  53. }
  54. .meta {
  55. background-image: linear-gradient(to top,rgba(0, 0, 0, 0.8),transparent 50%);
  56. position: absolute;
  57. left: 0;
  58. top: 0;
  59. width: 100%;
  60. height: 288px;
  61. .title {
  62. position: absolute;
  63. bottom: 0px;
  64. left: 0;
  65. padding-left: 16px;
  66. width: 70%;
  67. height: 70px;
  68. .top {
  69. color: #fff;
  70. font-size: 22px;
  71. display: block;
  72. }
  73. .sub {
  74. display: block;
  75. font-size: 19px;
  76. color: #999;
  77. }
  78. }
  79. .price {
  80. position: absolute;
  81. bottom: 25px;
  82. right: 16px;
  83. line-height: 1;
  84. padding: 4px 8px 4px 7px;
  85. color: @priceColor;
  86. font-size: 17px;
  87. background-color: #fff;
  88. border-radius: 2px;
  89. }
  90. }
  91. }
  92. .foot {
  93. height: 72px;
  94. line-height: 72px;
  95. padding: 0 20px;
  96. font-size: 16px;
  97. i {
  98. display: inline-block;
  99. width: 15px;
  100. height: 14px;
  101. margin-right: 5px;
  102. color: #999;
  103. }
  104. .like,
  105. .view {
  106. float: left;
  107. margin-right: 25px;
  108. vertical-align: middle;
  109. }
  110. .reply {
  111. float: right;
  112. vertical-align: middle;
  113. }
  114. }
  115. }
  116. }
  117. </style>

使用组件:src/views/home/index.vue

  1. <!-- 商品区域 -->
  2. <HomeProduct />
  3. <!-- 最新专题 -->
  4. + <HomeSpecial />
  1. +import HomeSpecial from './components/home-special'
  2. export default {
  3. name: 'xtx-home-page',
  4. + components: { HomeCategory, HomeBanner, HomeNew, HomeHot, HomeBrand, HomeProduct, HomeSpecial }
  5. }

获取数据:

  • 定义API src/api/home.js
  1. export const findSpecial = () => {
  2. return request('home/special', 'get')
  3. }
  • 渲染组件 src/views/home/components/home-speical.vue
  1. <template>
  2. <HomePanel title="最新专题">
  3. <template v-slot:right><XtxMore /></template>
  4. <div class="special-list" ref="homeSpecial">
  5. + <div class="special-item" v-for="item in list" :key="item.id">
  6. <RouterLink to="/">
  7. + <img :src="item.cover" alt />
  8. <div class="meta">
  9. + <p class="title">{{item.title}}<small>{{item.summary}}</small></p>
  10. + <span class="price">&yen;{{item.lowestPrice}}起</span>
  11. </div>
  12. </RouterLink>
  13. <div class="foot">
  14. + <span class="like"><i class="iconfont icon-hart1"></i>{{item.collectNum}}</span>
  15. + <span class="view"><i class="iconfont icon-see"></i>{{item.viewNum}}</span>
  16. + <span class="reply"><i class="iconfont icon-message"></i>{{item.replyNum}}</span>
  17. </div>
  18. </div>
  19. </div>
  20. </HomePanel>
  21. </template>
  22. <script>
  23. import HomePanel from './home-panel'
  24. +import { findSpecial } from '@/api/home'
  25. +import { useLazyData } from '@/hooks'
  26. export default {
  27. name: 'HomeSpecial',
  28. components: { HomePanel },
  29. + setup () {
  30. + const { container, data } = useLazyData(findSpecial)
  31. + return { homeSpecial: container, list: data }
  32. + }
  33. }
  34. </script>

首页主体-图片懒加载

目的: 当图片进入可视区域内去加载图片,且处理加载失败,封装成指令。

介绍一个webAPI:IntersectionObserver

  1. // 创建观察对象实例
  2. const observer = new IntersectionObserver(callback[, options])
  3. // callback 被观察dom进入可视区离开可视区都会触发
  4. // - 两个回调参数 entries , observer
  5. // - entries 被观察的元素信息对象的数组 [{元素信息},{}],信息中isIntersecting判断进入或离开
  6. // - observer 就是观察实例
  7. // options 配置参数
  8. // - 三个配置属性 root rootMargin threshold
  9. // - root 基于的滚动容器,默认是document
  10. // - rootMargin 容器有没有外边距
  11. // - threshold 交叉的比例
  12. // 实例提供两个方法
  13. // observe(dom) 观察哪个dom
  14. // unobserve(dom) 停止观察那个dom

基于vue3.0和IntersectionObserver封装懒加载指令

src/components/library/index.js

  1. export default {
  2. install (app) {
  3. app.component(XtxSkeleton.name, XtxSkeleton)
  4. app.component(XtxCarousel.name, XtxCarousel)
  5. app.component(XtxMore.name, XtxMore)
  6. + defineDirective(app)
  7. }
  8. }
  1. import defaultImg from '@/assets/images/200.png'
  2. // 指令
  3. const defineDirective = (app) => {
  4. // 图片懒加载指令
  5. app.directive('lazyload', {
  6. mounted (el, binding) {
  7. const observer = new IntersectionObserver(([{ isIntersecting }]) => {
  8. if (isIntersecting) {
  9. observer.unobserve(el)
  10. el.onerror = () => {
  11. el.src = defaultImg
  12. }
  13. el.src = binding.value
  14. }
  15. }, {
  16. threshold: 0.01
  17. })
  18. observer.observe(el)
  19. }
  20. })
  21. }

使用指令:

src/views/home/component/home-product.vue

  1. <RouterLink class="cover" to="/">
  2. + <img alt="" v-lazyload="cate.picture">
  3. <strong class="label">
  4. <span>{{cate.name}}馆</span>
  5. <span>{{cate.saleInfo}}</span>
  6. </strong>
  7. </RouterLink>

src/views/home/component/home-goods.vue

  1. <RouterLink to="/" class="image">
  2. + <img alt="" v-lazyload="goods.picture" />
  3. </RouterLink>

`src/views/home/component/home-product.vue

  1. <RouterLink class="cover" to="/">
  2. + <img v-lazyload="item.picture" alt="">
  3. <strong class="label">
  4. <span>{{item.name}}馆</span>
  5. <span>{{item.saleInfo}}</span>
  6. </strong>
  7. </RouterLink>

总结:

  • 在img上使用使用v-lazyload值为图片地址,不设置src属性。