uniapp项目之小兔鲜儿小程序商城(二) 首页的实现:自定义导航栏,轮拨图,前台分类,热门推荐,猜你喜欢,下拉刷新,骨架屏

文章目录

零.首页最终效果

在这里插入图片描述

一.自定义导航栏

要求把默认的导航栏升级成自行以导航栏,并进行样式适配,做成可复用的组件
在这里插入图片描述

1.新建pages/index/components/CustomNavbar.vue首页子组件

并复制静态结构如下

<script setup lang="ts">
//
</script>
<template>
  <view class="navbar">
    <!-- logo文字 -->
    <view class="logo">
      <image class="logo-image" src="@/static/images/logo.png"></image>
      <text class="logo-text">新鲜 · 亲民 · 快捷</text>
    </view>
    <!-- 搜索条 -->
    <view class="search">
      <text class="icon-search">搜索商品</text>
      <text class="icon-scan"></text>
    </view>
  </view>
</template>
<style lang="scss">
/* 自定义导航条 */
.navbar {
  background-image: url(@/static/images/navigator_bg.png);
  background-size: cover;
  position: relative;
  display: flex;
  flex-direction: column;
  padding-top: 20px;
  .logo {
    display: flex;
    align-items: center;
    height: 64rpx;
    padding-left: 30rpx;
    padding-top: 20rpx;
    .logo-image {
      width: 166rpx;
      height: 39rpx;
    }
    .logo-text {
      flex: 1;
      line-height: 28rpx;
      color: #fff;
      margin: 2rpx 0 0 20rpx;
      padding-left: 20rpx;
      border-left: 1rpx solid #fff;
      font-size: 26rpx;
    }
  }
  .search {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 10rpx 0 26rpx;
    height: 64rpx;
    margin: 16rpx 20rpx;
    color: #fff;
    font-size: 28rpx;
    border-radius: 32rpx;
    background-color: rgba(255, 255, 255, 0.5);
  }
  .icon-search {
    &::before {
      margin-right: 10rpx;
    }
  }
  .icon-scan {
    font-size: 30rpx;
    padding: 15rpx;
  }
}
</style>
2.在首页pages/index/index.vue中引入
<script setup lang="ts">
//引入子组件CustomNavBar
import CustomNavbar from './components/CustomNavbar.vue'
</script>
<template>
  <!-- 使用子组件 -->
  <CustomNavbar />
  <view class="index">index我是首页</view>
</template>
3.隐藏默认导航栏+修改标题颜色
//pages.json
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页",
        "navigationStyle": "custom",//隐藏默认导航栏
        "navigationBarTextStyle": "white",//修改标题颜色
      }
    }
4.适配不同机型

对安全区域进行样式适配

使用到了uniapp的一个api:获取屏幕边界到安全区域的距离
//获取屏幕边界到安全区域的距离
const { safeAreaInsets } = uni.getSystemInfoSync()
在子组件中使用
//使用uniapp的api,获取屏幕边界到安全区域的距离
const { safeAreaInsets } = uni.getSystemInfoSync()
......
  <!-- 把该距离变量动态绑定style,可以实现导航条跟随屏幕刘海区域变化 -->
  <view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
    <!-- logo文字 -->
  </view>

二.轮拨图

轮拨图不仅在首页中使用到,在商品分类页中也有,因此也封装成一个通用组件

1.新建 src/components/XtxSwiper.vue全局子组件

并准备静态结构如下:

<script setup lang="ts">
import { ref } from 'vue'
const activeIndex = ref(0)
</script>
<template>
  <vi ew class="carousel">
使用到了小程序的标签:swiper
    <swiper :circular="true" :autoplay="false" :interval="3000">
      <swiper-item>
        点击图片跳转:
        <navigator url="/pages/index/index" hover-class="none" class="navigator">
          <image
            mode="aspectFill"
            class="image"
            src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/slider_1.jpg"
          ></image>
        </navigator>
      </swiper-item>
      <swiper-item>
        <navigator url="/pages/index/index" hover-class="none" class="navigator">
          <image
            mode="aspectFill"
            class="image"
            src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/slider_2.jpg"
          ></image>
        </navigator>
      </swiper-item>
      <swiper-item>
        <navigator url="/pages/index/index" hover-class="none" class="navigator">
          <image
            mode="aspectFill"
            class="image"
            src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/slider_3.jpg"
          ></image>
        </navigator>
      </swiper-item>
    </swiper>
    <!-- 指示点(自定义) -->
    <view class="indicator">
      <text
        v-for="(item, index) in 3"
        :key="item"
        class="dot"
        :class="{ active: index === activeIndex }"
      ></text>
    </view>
  </view>
</template>
<style lang="scss">
/* 轮播图 */
.carousel {
  height: 280rpx;
  position: relative;
  overflow: hidden;
  transform: translateY(0);
  background-color: #efefef;
  .indicator {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 16rpx;
    display: flex;
    justify-content: center;
    .dot {
      width: 30rpx;
      height: 6rpx;
      margin: 0 8rpx;
      border-radius: 6rpx;
      background-color: rgba(255, 255, 255, 0.4);
    }
    .active {
      background-color: #fff;
    }
  }
  .navigator,
  .image {
    width: 100%;
    height: 100%;
  }
}
</style>
2.自动导入通用组件的步骤

(参考之前在pages.json中对uni-ui的配置)

//pages.json
{
  //组件自动导入
  "easycom": {
    //是否开启自动扫描
    "autoscan": true,
    //以正则方式自定义组件的匹配规则(添加后需重启服务器才能生效)
    "custom": {
      // 之前的:uni-ui 规则如下配置
      "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
      //新增的:以Xtx开头的组件
      "^xtx(.*)": "@/components/xtx$1.vue"
    }
  },
  "pages": [
    ......
  ],
}

验证:在首页中不导入直接使用轮拨图组件

<script setup lang="ts">
//引入子组件CustomNavBar
import CustomNavbar from './components/CustomNavbar.vue'
</script>
<template>
  <!-- 使用子组件 -->
  <CustomNavbar />
  <!-- 不用导入,直接使用轮拨图通用子组件 -->
   <XtxSwiper />
  <view class="index">index我是首页</view>
</template>
3.添加类型声明

此时自动导入的 <XtxSwiper />和手动导入的<CustomNavbar />仍有区别----没有类型声明

知识点:为已有的js文件提供类型声明,关键字declare

// src/types/components.d.ts

//导入轮拨图组件
import XtxSwiper from './XtxSwiper.vue'
//扩展全局组件类型,声明全局组件的类型
declare module '@vue/runtime-core' {// 注意:此处更新了要写成:declare module 'vue'
  export interface GlobalComponents {
    XtxSwiper: typeof XtxSwiper//typeof拿到组件的类型,然后赋值给全局组件类型
  }
}

注:declare module '@vue/runtime-core'应为declare module 'vue'

4.轮拨图的指示点

此时的指示点仅是静态结构

    <!-- 指示点(自定义) -->
    <view class="indicator">
      <text
        v-for="(item, index) in 3"
        :key="item"
        class="dot"
        :class="{ active: index === activeIndex }"//动态绑定.active类实现高亮==>通过比较index===activeIndex
      ></text>
    </view>

最终目标是让指示点跟着轮拨图的切换而切换

step1:获取轮拨图滚动时,当前图片的索引

uniapp官网>Swiper>@change事件中event.detail.current就是下标

//当轮拨图滚动时触发
function onChange(e) {//此时提示e为any类型==>缺少类型声明
  console.log(e) //e.current为当前轮播图的索引
}
    <!-- 指示点(自定义) -->
    <view class="indicator">
      <text
        v-for="(item, index) in 3"
        @change='onChange'
        :key="item"
        class="dot"
        :class="{ active: index === activeIndex }"//动态绑定.active类实现高亮==>通过比较index===activeIndex
      ></text>
    </view>
step2:提供类型声明
const activeIndex=ref(0)
const onChange: UniHelper.SwiperOnChange = (e) => {
  console.log(e) //e.current为当前轮播图的索引 
  activeIndex.value=e.detail?.current
}
step3:把拿到的下标更新给activeIndex
activeIndex.value=e.detail?.current

此时报错:不能将"number|undefine"分配给"number"
因为detail后面加了可选符,当没有时可能为undefined

解决方式:把可选链调整为非空断言
activeIndex.value=e.detail!.current
5.获取轮拨图的数据

当前轮拨图的图片使用的是静态资源,现在优化为:从后台获取数据并动态渲染

5.1.封装api

在这里插入图片描述
新建services/home.js

import { http } from "@/utils/http"
const getHomeBannerAPI= (distributionSite=1)=>{
    //调用http中封装的发起请求的函数(基于uni.request)
    return http({
        methods:'GET',
        url:'/home/banner',
        data:{
            distributionSite
        }
    })
}
5.2.页面调用
//index.vue
import {getHomeBannerAPI} from '@/services/home.ts'
import {onLoad} from '@dcloundio/uni-app'
const bannerList=ref([])
//先封装一个调用函数
const getHomeBannerData=async()=>{
    const res=await getHomeBannerAPI()
    const bannerList=res.result//缺乏类型声明
}

onLoad(()=>{//记得这个钩子也需导入
    //页面加载时调用该函数
    getHomeBannerData()
})

此时:
bannerList和res.result都缺乏类型声明

6.轮拨图的数据类型
6.1.定义轮拨图的数据类型:res.result
  • 复制指定类型文件并粘贴到新建的types/home.d.ts如下:
/*首页-广告区域数据类型 */
export type BannerItem = {
  /** 跳转链接 */
  hrefUrl: string
  /** id */
  id: string
  /** 图片链接 */
  imgUrl: string
  /** 跳转类型 */
  type: number
}

注:code,msg,result的类型不用再声明了,已经有了
前面的代码:

interface Data<T> {
  code: string //状态码:'1"
  msg: string //提示信息:'请求成功'
  result: T //核心数据类型:{{}.{},{},{},{}}
}

在这里插入图片描述

  • service/home.ts中,导入types/home.d.ts中的BannerItem
//service/home.ts
import type {BannerItem} from '@/types/home'
import { http } from "@/utils/http"
const getHomeBannerAPI= (distributionSite=1)=>{
    //调用http中封装的发起请求的函数(基于uni.request)
    return http<BannerItem[]>({//指定类型为一个对象数组
        methods:'GET',
        url:'/home/banner',
        data:{
            distributionSite
        }
    })
}
6.2.定义轮拨图的数据类型:bannerList
//index.vue
import type {BannerItem} from '@/types/home'
const bannerList=ref<BannerItem[]>([])
7.父传子+动态渲染

此时数据是在首页中发起请求并获取的,而轮拨图是一个封装子组件,因此需要父传子

  • 父组件:pages/index/index.vue
<XtxSwiper :list='bannerList'/>
  • 子组件:src/components/XtxSwiper.vue
<script setup lang="ts">
import { ref } from 'vue'
import type { BannerItem } from '@/types/home'
// 子组件接收自定义属性list
defineProps<{
  list: BannerItem[]
}>()
const activeIndex = ref(0)
//当轮拨图滚动时触发
const onChange: UniHelper.SwiperOnChange = (e) => {
  console.log(e) //e.current为当前轮播图的索引
  activeIndex.value = e.detail!.current //非空断言:回调参数是可选链式的
}
</script>

<template>
  <view class="carousel">
    <swiper :circular="true" :autoplay="false" :interval="3000">
      <!-- 动态渲染:轮拨图 -->
      <swiper-item v-for="item in list" :key="item.id">
        <navigator url="/pages/index/index" hover-class="none" class="navigator">
          <image mode="aspectFill" class="image" :src="item.imgUrl"></image>
        </navigator>
      </swiper-item>
    </swiper>
    <!-- 指示点 -->
    <view class="indicator">
      <!-- 动态渲染:指示点 -->
      <text
        v-for="(item, index) in list"
        :key="item.id"
        @change="onChange"
        class="dot"
        :class="{ active: index === activeIndex }"
      ></text>
    </view>
  </view>
</template>

三.前台分类

在这里插入图片描述

前台分类也要封装成独立子组件,但是与轮拨图不同的是,轮拨图除了在首页中使用之外,在分类页中也用上,
但是前台分类仅仅在首页中用到,因此可以写在index/components/CategoryPanel.vue中.(类似自定义导航栏)

1.组件封装
1.1.准备组件

准备静态结构如下:

<script setup lang="ts">
//
</script>
<template>
  <view class="category">
    <navigator//导航链接
      class="category-item"
      hover-class="none"
      url="/pages/index/index"
      v-for="item in 10"//列表循环
      :key="item"
    >
    内部结构:图片+文本
      <image
        class="icon"
        src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/nav_icon_1.png"
      ></image>
      <text class="text">居家</text>
    </navigator>
  </view>
</template>
<style lang="scss">
/* 前台类目 */
.category {
  margin: 20rpx 0 0;
  padding: 10rpx 0;
  display: flex;
  flex-wrap: wrap;
  min-height: 328rpx;
  .category-item {
    width: 150rpx;
    display: flex;
    justify-content: center;
    flex-direction: column;
    align-items: center;
    box-sizing: border-box;
    .icon {
      width: 100rpx;
      height: 100rpx;
    }
    .text {
      font-size: 26rpx;
      color: #666;
    }
  }
}
</style>
1.2.导入并使用组件
//index.vue
......
//导入分类面板组件
import CategoryPanel from './components/CategoryPanel.vue'

  <!-- 自定义导航栏 -->
  <CustomNavbar />
  <!-- 轮拨图通用子组件 -->
  <XtxSwiper :list="bannerList" />
  <!-- 分类面板 -->
  <CategoryPanel />

1.3.设置首页底色
//index.vue
<style lang="scss">
//更改页面的底色
page {
  background-color: #f5f5f5;
}
</style>
2.获取数据

在这里插入图片描述

2.1.封装api
// services/home.ts
export const getHomeCategoryAPI = () => {
  return http({
    method: 'GET',
    url: '/home/category/mutli',
  })
}
2.2.页面调用
//index.vue

import { getHomeBannerAPI, getHomeCategoryAPI } from '@/services/home.ts'
const categoryList = ref([])
// 获取首页分类数据
const getHomeCategoryData = async () => {
  const res = await getHomeCategoryAPI()
  categoryList.value = res.result
}
onLoad(() => {
  getHomeBannerData()
  getHomeCategoryData()
})

*此时categoryList.valueres.result都没有类型声明

2.3.声明类型
  • 粘贴分类数据的声明类型到已有的home.d.ts文件中
/** 首页-前台类目数据类型 */
export type CategoryItem = {
  /** 图标路径 */
  icon: string
  /** id */
  id: string
  /** 分类名称 */
  name: string
}
  • res.result指定类型
// services/home.ts
import type {CategoryItem} from '@/types/home'

export const getHomeCategoryAPI = () => {
  return http<CategoryItem[]>({
    method: 'GET',
    url: '/home/category/mutli',
  })
}
  • categoryList指定类型
import type {CategoryItem} from '@/types/home'
const categoryList = ref<CategoryItem[]>([])
// 获取首页分类数据
const getHomeCategoryData = async () => {
  const res = await getHomeCategoryAPI()
  categoryList.value = res.result
}
2.4.父传子+动态渲染

父组件:index.vue

<CategoryPanel :list='categoryList' />

子组件:CategoryPanel.vue

<script setup lang="ts">
import type { CategoryItem } from '@/types/home'
// 定义 props 接收数据
defineProps<{
  list: CategoryItem[]
}>()
</script>
<template>
  <view class="category">
    <navigator
      class="category-item"
      hover-class="none"
      url="/pages/index/index"
      v-for="item in list"
      :key="item.id"
    >
      <image class="icon" :src="item.icon"></image>
      <text class="text">{{ item.name }}</text>
    </navigator>
  </view>
</template>

四.热门推荐

在这里插入图片描述
后端根据用户的消费习惯等信息向用户推荐的一系列商品,前端负责展示这些商品展示给用户

1.组件封装
1.1.准备组件

创建index/components/HotPanel.vue作为首页的子组件,并准备静态结构如下:

<script setup lang="ts">
//
</script>
<template>
  <!-- 推荐专区 -->
  <view class="panel hot">
    <view class="item" v-for="item in 4" :key="item">
      <view class="title">
        <text class="title-text">特惠推荐</text>
        <text class="title-desc">精选全攻略</text>
      </view>
      <navigator hover-class="none" url="/pages/hot/hot" class="cards">
        <image
          class="image"
          mode="aspectFit"
          src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_small_1.jpg"
        ></image>
        <image
          class="image"
          mode="aspectFit"
          src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_small_2.jpg"
        ></image>
      </navigator>
    </view>
  </view>
</template>
<style lang="scss">
/* 热门推荐 */
.hot {
  display: flex;
  flex-wrap: wrap;
  min-height: 508rpx;
  margin: 20rpx 20rpx 0;
  border-radius: 10rpx;
  background-color: #fff;
  .title {
    display: flex;
    align-items: center;
    padding: 24rpx 24rpx 0;
    font-size: 32rpx;
    color: #262626;
    position: relative;
    .title-desc {
      font-size: 24rpx;
      color: #7f7f7f;
      margin-left: 18rpx;
    }
  }
  .item {
    display: flex;
    flex-direction: column;
    width: 50%;
    height: 254rpx;
    border-right: 1rpx solid #eee;
    border-top: 1rpx solid #eee;
    .title {
      justify-content: start;
    }
    &:nth-child(2n) {
      border-right: 0 none;
    }
    &:nth-child(-n + 2) {
      border-top: 0 none;
    }
    .image {
      width: 150rpx;
      height: 150rpx;
    }
  }
  .cards {
    flex: 1;
    padding: 15rpx 20rpx;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
}
</style>
1.2.导入组件
//导入热门推荐组件
import HotPanel from './components/HotPanel.vue'

    <!-- 热门推荐 -->
    <HotPanel :list="hotList" />
2.获取数据

在这里插入图片描述

2.1.封装api
    <!-- 热门推荐 -->
    <HotPanel :list="hotList" />
2.2.页面调用
//index.vue

import type { BannerItem, CategoryItem, HotItem } from '@/types/home'
import { getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI } from '@/services/home.ts'
const hotList = ref<HotItem[]>([])
// 获取首页热门推荐数据
const getHomeHotData = async () => {
  const res = await getHomeHotAPI()
  hotList.value = res.result
}
onLoad(() => {
  getHomeBannerData()
  getHomeCategoryData()
  getHomeHotData()
})

*此时hotList.valueres.result还没有声明类型

2.3.类型声明
  • 粘贴类型声明文件到home.d.ts
//types/home.d.ts

/** 首页-热门推荐数据类型 */
export type HotItem = {
  /** 说明 */
  alt: string
  /** id */
  id: string
  /** 图片集合[ 图片路径 ] */
  pictures: string[]
  /** 跳转地址 */
  target: string
  /** 标题 */
  title: string
  /** 推荐类型 */
  type: string
}
  • res.result提供类型声明
//home.ts
import type { BannerItem, CategoryItem, HotItem, GuessItem } from '@/types/home'

const hotList = ref<HotItem[]>([])
// 获取首页热门推荐数据
export const getHomeHotAPI = () => {
  return http<HotItem[]>({
    method: 'GET',
    url: '/home/hot/mutli',
  })
}
  • hotList提供类型声明
//index.vue
import type { BannerItem, CategoryItem, HotItem } from '@/types/home'
const hotList = ref<HotItem[]>([])
2.4.父传子+动态渲染

父组件:index.vue

  <!-- 热门推荐 -->
  <HotPanel :list="hotList" />

子组件:src\pages\index\components\HotPanel.vue

<script setup lang="ts">
import type { HotItem } from '@/types/home'
// 定义 props 接收数据
defineProps<{
  list: HotItem[]
}>()
</script>
<template>
  <!-- 推荐专区 -->
  <view class="panel hot">
    <view class="item" v-for="item in list" :key="item.id">
      <view class="title">
        <text class="title-text">{{ item.title }}</text>
        <text class="title-desc">{{ item.alt }}</text>
      </view>
      <navigator hover-class="none" :url="`/pages/hot/hot?type=${item.type}`" class="cards">
<!-- 动态渲染:第二层v-for -->
        <image
          v-for="src in item.pictures"
          :key="src"
          class="image"
          mode="aspectFit"
          :src="src"
        ></image>
      </navigator>
    </view>
  </view>
</template>

五.猜你喜欢【难点】

后端根据用户的浏览记录等信息向用户随机推荐的一系列商品,前端负责把商品在多个页面中展示。

猜你喜欢要封装成全局通用子组件,因为多个页面(购物车,结算页,首页)用到该组件

1.组件封装
1.1.准备组件

新建src/components/XtxGuess.vue
并粘贴静态结构代码如下:

<script setup lang="ts">
//
</script>
<template>
  <!-- 猜你喜欢 -->
  <view class="caption">
    <text class="text">猜你喜欢</text>
  </view>
  <view class="guess">
    <navigator
      class="guess-item"
      v-for="item in 10"
      :key="item"
      :url="`/pages/goods/goods?id=4007498`"
    >
      <image
        class="image"
        mode="aspectFill"
        src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_big_1.jpg"
      ></image>
      <view class="name"> 德国THORE男表 超薄手表男士休闲简约夜光石英防水直径40毫米 </view>
      <view class="price">
        <text class="small">¥</text>
        <text>899.00</text>
      </view>
    </navigator>
  </view>
  <view class="loading-text"> 正在加载... </view>
</template>
<style lang="scss">
:host {
  display: block;
}
/* 分类标题 */
.caption {
  display: flex;
  justify-content: center;
  line-height: 1;
  padding: 36rpx 0 40rpx;
  font-size: 32rpx;
  color: #262626;
  .text {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 0 28rpx 0 30rpx;
    &::before,
    &::after {
      content: '';
      width: 20rpx;
      height: 20rpx;
      background-image: url(@/static/images/bubble.png);
      background-size: contain;
      margin: 0 10rpx;
    }
  }
}
/* 猜你喜欢 */
.guess {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  padding: 0 20rpx;
  .guess-item {
    width: 345rpx;
    padding: 24rpx 20rpx 20rpx;
    margin-bottom: 20rpx;
    border-radius: 10rpx;
    overflow: hidden;
    background-color: #fff;
  }
  .image {
    width: 304rpx;
    height: 304rpx;
  }
  .name {
    height: 75rpx;
    margin: 10rpx 0;
    font-size: 26rpx;
    color: #262626;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
  .price {
    line-height: 1;
    padding-top: 4rpx;
    color: #cf4444;
    font-size: 26rpx;
  }
  .small {
    font-size: 80%;
  }
}
// 加载提示文字
.loading-text {
  text-align: center;
  font-size: 28rpx;
  color: #666;
  padding: 20rpx 0;
}
</style>
1.2.直接使用

类似轮拨图组件,无需导入直接使用

//获取猜你喜欢的组件实例
import type { XtxGuessInstance } from '@/types/component'
const guessRef = ref<XtxGuessInstance>()

<!-- 猜你喜欢 -->
<XtxGuess />
2.定义组件类型
// types/components.d.ts
import XtxSwiper from '@/components/XtxSwiper.vue'
import XtxGuess from '@/components/XtxGuess.vue'
declare module 'vue' {
  export interface GlobalComponents {
    XtxSwiper: typeof XtxSwiper
    XtxGuess: typeof XtxGuess
  }
}
// 组件实例类型
export type XtxGuessInstance = InstanceType<typeof XtxGuess>
3.添加滚动容器scroll-view

在这里插入图片描述

3.1.滚动容器包裹需要的子组件

使用滚动容器scroll-view把需要滚动的子组件占位符(除自定义导航栏都滚动)包起来

  <!-- 自定义导航栏 -->
  <CustomNavbar />
  <scroll-view class='scroll-view' scroll-y>
    <!-- 轮拨图通用子组件 -->
    <!-- 父传子:自定义属性为list -->
    <XtxSwiper :list="bannerList" />
    <!-- 分类面板 -->
    <CategoryPanel :list="categoryList" />
    <!-- 热门推荐 -->
    <HotPanel :list="hotList" />
    <!-- 猜你喜欢 -->
    <XtxGuess />
  </scroll-view>
3.2.设置滚动的高度

在这里插入图片描述

滚动的高度=页面高度-自定义导航栏高度

  • 页面高度:
page {
  //更改页面的底色
  background-color: #f5f5f5;
  //设置页面高度为100%
  height: 100%;
  // 设置弹性布局和排列方向
  display: flex;
  flex-direction: column;
}
  • 滚动高度:
.scroll-view{
    flex:1
    //height:0//若还滚动不了就加上这句
}
4.获取数据

在这里插入图片描述

4.1.封装api
// src/services/home.ts
/**
* 猜你喜欢-小程序
*/
export const getHomeGoodsGuessLikeAPI = (data?: PageParams) => {
  return http<PageResult<GuessItem>>({
    method: 'GET',
    url: '/home/goods/guessLike',
    data,
  })
}
4.2.声明类型

猜你喜欢后台返回的数据类型中大致可以分为:列表数据,总条数,当前页数,其中:

列表数据是根据调用的页面而相应变化的,考虑将其抽离出来声明为泛型数据
分页数据在分页功能中调用,不论哪个页面调用,都是相同的数据类型,因此同样可以抽离出来并声明

  • 猜你喜欢的商品数据类型

放在已存在的src/types/home.d.ts 文件中

//src/types/home.d.ts 

/** 猜你喜欢-商品类型 */
export type GuessItem = {
  /** 商品描述 */
  desc: string
  /** 商品折扣 */
  discount: number
  /** id */
  id: string
  /** 商品名称 */
  name: string
  /** 商品已下单数量 */
  orderNum: number
  /** 商品图片 */
  picture: string
  /** 商品价格 */
  price: number
}
  • 分页:分页结果+分页参数的数据类型

新建src/types/global.d.ts

/** 通用分页结果类型 */
export type PageResult<T> = {
  /** 列表数据 */
  items: T[]
  /** 总条数 */
  counts: number
  /** 当前页数 */
  page: number
  /** 总页数 */
  pages: number
  /** 每页条数 */
  pageSize: number
}

/** 通用分页参数类型 */
export type PageParams = {
  /** 页码:默认值为 1 */
  page?: number
  /** 页大小:默认值为 10 */
  pageSize?: number
}
4.3.列表数据的页面调用
4.3.1.为何不是在首页中请求获取数据然后父传子?

因为这个组件多次被复用了,然后数据又都是一样的,
所以放在组件内部中,可以一次请求就能完成数据的展示。

4.3.2.为啥要在组件挂载完毕时调用api?

使用组件生命周期钩子而非页面生命周期函数的原因是:仅调用一次,而不是在每个用到"猜你喜欢"功能的页面中各调用一次
使用组件生命周期钩子中的onMounted的原因是:等dom树生成之后才能渲染,不然获取到的数据没有dom元素渲染

4.3.3.实现代码
//XtxGuess.vue
import type {GuessItem} from '@/type/home.d.ts'
const guessList=ref<GuessItem[]>([])
//获取猜你喜欢数据
const getHomeGoodsGuessLikeData=async()=>{
   const res=await getHomeGoodsGuessLikeAPI()
    guessList.value=res.result.items//为啥要加items:查看通用分页结果类型
}
//组件挂载完毕
onMounted(){
    getHomeGoodsGuessLikeData()
}
4.4.列表数据的动态渲染

不用父传子:用goodsList直接在子组件Xtxguess.vue中动态渲染

<!-- 猜你喜欢 -->
  <view class="caption">
    <text class="text">猜你喜欢</text>
  </view>
  <view class="guess">
    <navigator
      class="guess-item"
      v-for="item in guessList"
      :key="item.id"
      :url="`/pages/goods/goods?id=4007498`"
    >
      <image
        class="image"
        mode="aspectFill"
        :src="item.picture"
      ></image>
      <view class="name">{{item.name}}</view>
      <view class="price">
        <text class="small">¥</text>
        <text>{{item.price}}</text>
      </view>
    </navigator>
  </view>
4.5.什么时候以及如何调用分页数据?

在这里插入图片描述

4.5.1.加载分页数据的时机以及如何实现

当滚动容器scroll-view滚动触底的时候,才开始加载分页数据

此时触发"加载分页数据"的事件是绑定在首页的scroll-view上的,
数据的获取和加载是在猜你喜欢子组件XtxGuess当中的,

为了实现这业务逻辑(父组件调用子组件的方法)–需要用到模板引用(ref标识),它可以获取当前组件的DOM对象和其他组件的实例对象

4.5.2.注意事项

     1)当前是TypeScript项目,因此还需要指定组件实例的类型
     2)在setup语法糖是,所有子组件默认是封闭的,需要手动设置暴露子组件

4.5.3.实现步骤
  • step1:滚动容器绑定滚动触底事件
  • step2:在事件中调用子组件获取分页数据的方法
// pages/index/index.vue
<script setup lang="ts">
import type { XtxGuessInstance } from '@/types/components'
import { ref } from 'vue'
// 获取猜你喜欢组件实例
const guessRef = ref<XtxGuessInstance>()
// 滚动触底事件
const onScrolltolower = () => {
  guessRef.value?.getMore()
}
</script>
<template>
  <!-- 滚动容器 -->
  <scroll-view scroll-y @scrolltolower="onScrolltolower">
    ......
    <!-- 猜你喜欢 -->
    <XtxGuess ref="guessRef" />
  </scroll-view>
</template>
  • step3:给ref指定类型

使用到了TS的方法InstanceType,用于获取组件类型

//types/components.d.ts
//组件实例类型
export type XtxGuessInstance=InstanceType<typeof XtxGuess>
  • step4:暴露子组件的获取数据的方法
//XtxGuess.vue
defineExpose({
    getMore:getHomeGoodsGuessLikeData//可以不暴露原有方法名,而是自定义
})
4.6.分页数据的动态渲染

业务逻辑:
在已封装的"获取猜你喜欢"的接口api函数中getHomeGoodsGuessLikeAPI,把添加分类的参数添加进去并且为其指定类型,
然后调用该函数并传参,获得的分页数据追加到guessList数组里面
最后对页码进行累加,目的是为下一次传参时,可以传入不同的数据,即下一页的数据

4.6.1.声明类型

(用到了通用分页参数类型,上面已经声明过)

4.6.2.升级getHomeGoodsGuessLikeAPI接口

在这里插入图片描述

// src/services/home.ts
/**
* 猜你喜欢-小程序
*/
export const getHomeGoodsGuessLikeAPI = (data?: PageParams) => {
  return http<PageResult<GuessItem>>({
    method: 'GET',
    url: '/home/goods/guessLike',
    data,//导入并指定类型<==在子组件调用传参的时候定义一个分页参数pageParams
    
  })
}
4.6.3.调用子组件并传入页面参数
// 分页参数
const pageParams: Required<PageParams> = {//ts的工具函数
  page: 1,
  pageSize: 10,
}
// 获取猜你喜欢数据
const getHomeGoodsGuessLikeData = async () => {
    
  // 退出分页判断
  if (finish.value === true) {
    return uni.showToast({ icon: 'none', title: '没有更多数据~' })
  }
  const res = await getHomeGoodsGuessLikeAPI(pageParams)
  // 数组追加
  guessList.value.push(...res.result.items)//数组追加到另一个数组:拓展运算符
  // 分页条件
  if (pageParams.page < res.result.pages) {//要不要加value
    // 页码累加的条件是页码<总页数
    pageParams.page++//当前是可选的,当没有数据时会报错,把其类型改为必选Required
  } else {
    finish.value = true//标记结束
  }
}
4.6.4.什么时候退出分页?

数据总数和总页数是有限的,
由此判断:当页码小于总页数时,可以继续进行页码累加,否则标记结束

//已结束的标记
const finish=ref(false)

替换"正在加载"的文字:
<view class="loading-text"> {{finish?"没有更多数据~":"正在加载..."}} </view>

六.优化:下拉刷新

1.设置下拉刷新

下拉刷新使用到了uni-ui的scroll-view组件上的如下属性:

  • 配置 refresher-enabled 属性,开启下拉刷新交互
  • 监听 @refresherrefresh 事件,判断用户是否执行了下拉操作
  • 配置 refresher-triggered 属性,关闭下拉状态
//index .vue

<!-- 滚动容器 -->
<scroll-view
  refresher-enabled
  @refresherrefresh="onRefresherrefresh"
  :refresher-triggered="isTriggered"
  class="scroll-view"
  scroll-y
>
........
</scroll-view>
2.监听到用户的下拉行为后需要做些什么?
  • 刷新:轮拨图,前台分类,当前热卖,猜你喜欢
  • 开启和关闭下拉刷新的动画
//index.vue

//监听用户的下拉行为
const onRefreshrefresh = async () => {
  //开启动画
  isTriggered.value = true
  //先重置"猜你喜欢"组件的数据
  guessRef.value?.resetData()
  //刷新数据,重新获取数据==>确保全部加载完毕后关闭动画
  await Promise.all([
    getHomeBannerData(),
    getHomeCategoryData(),
    getHomeHotData(),
    guessRef.value?.getMore(), //再调用猜你喜欢的组件的获取更多数据的方法
  ])
  //关闭动画
  isTriggered.value = false
}
3.下拉刷新时获取猜你喜欢组件,获取之后应该做什么?
  • 重置页码
  • 重置列表
  • 重置结束标记

(以上数据都是存在子组件中,首页需要使用ref标识来调用)

  • 数据重置后再加载数据

子组件:XtxGuess.vue

const resetData = () => {
//重置数据
const resetData = () => {
pageParams.page = 1
guessList.value = []
finish.value = false
}
// 暴露方法
defineExpose({
resetData,
getMore: getHomeGoodsGuessLikeData,
})

父组件:index.vue

//index.vue

//监听用户的下拉行为
const onRefreshrefresh = async () => {
  //开启动画
  isTriggered.value = true
  //先重置"猜你喜欢"组件的数据
  guessRef.value?.resetData()
  //刷新数据,重新获取数据==>确保全部加载完毕后关闭动画
  await Promise.all([
    getHomeBannerData(),
    getHomeCategoryData(),
    getHomeHotData(),
    guessRef.value?.getMore(), //再调用猜你喜欢的组件的获取更多数据的方法
  ])
  //关闭动画
  isTriggered.value = false
}

*使用Promise.all的优势是减少等待时间:
在这里插入图片描述

七.优化:骨架屏

在这里插入图片描述

什么是骨架屏?

骨架屏是页面加载出来之前的空白页面

骨架屏显示的逻辑:数据是否在加载中

如何编写骨架屏文件?
  • 微信开发者工具可以快速生成骨架屏的结构和样式:
    微信开发者工具(模拟器)>(右上角)页面信息>生成骨架屏>确认生成index.skeleton.wxmlindex.skeleton.wxss两个文件

  • 找到这两个文件,转换为vue组件即可
    新建pages/index/components/PageSkeleton.vue

删掉其他多余代码只保留:轮拨图,前台分类,猜你喜欢
代码略
  • 首页调用子组件
<!-- 自定义导航栏 -->
  <CustomNavbar />
  <scroll-view class='scroll-view' scroll-y>
    <PageSkeleton v-if="true" />
    <template v-else>
        <!-- 轮拨图通用子组件 -->
        <!-- 父传子:自定义属性为list -->
        <XtxSwiper :list="bannerList" />
        <!-- 分类面板 -->
        <CategoryPanel :list="categoryList" />
        <!-- 热门推荐 -->
        <HotPanel :list="hotList" />
        <!-- 猜你喜欢 -->
        <XtxGuess />
    </template>
  </scroll-view>
  • 判断骨架屏的加载时机
const isLoading=ref(false)
onLoad(() => {
  isLoading.value=true
  //getHomeBannerData()
  //getHomeCategoryData()
  //getHomeHotData()
  Promise.all([
      getHomeBannerData()
      getHomeCategoryData()
      getHomeHotData()
  ])
  isLoading.value=false  
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端OnTheRun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值