文章目录
- 零.首页最终效果
- 一.自定义导航栏
- 1.新建`pages/index/components/CustomNavbar.vue`首页子组件
- 2.在首页`pages/index/index.vue`中引入
- 3.隐藏默认导航栏+修改标题颜色
- 4.适配不同机型
- 使用到了uniapp的一个api:获取屏幕边界到安全区域的距离
- 在子组件中使用
- 二.轮拨图
- 1.新建 `src/components/XtxSwiper.vue`全局子组件
- 2.自动导入通用组件的步骤
- 3.添加类型声明
- 4.轮拨图的指示点
- step1:获取轮拨图滚动时,当前图片的索引
- step2:提供类型声明
- step3:把拿到的下标更新给activeIndex
- 5.获取轮拨图的数据
- 5.1.封装api
- 5.2.页面调用
- 6.轮拨图的数据类型
- 6.1.定义轮拨图的数据类型:`res.result`
- 6.2.定义轮拨图的数据类型:`bannerList`
- 7.父传子+动态渲染
- 三.前台分类
- 1.组件封装
- 1.1.准备组件
- 1.2.导入并使用组件
- 1.3.设置首页底色
- 2.获取数据
- 2.1.封装api
- 2.2.页面调用
- 2.3.声明类型
- 2.4.父传子+动态渲染
- 四.热门推荐
- 1.组件封装
- 1.1.准备组件
- 1.2.导入组件
- 2.获取数据
- 2.1.封装api
- 2.2.页面调用
- 2.3.类型声明
- 2.4.父传子+动态渲染
- 五.猜你喜欢【难点】
- 1.组件封装
- 1.1.准备组件
- 1.2.直接使用
- 2.定义组件类型
- 3.添加滚动容器`scroll-view`
- 3.1.滚动容器包裹需要的子组件
- 3.2.设置滚动的高度
- 4.获取数据
- 4.1.封装api
- 4.2.声明类型
- 4.3.列表数据的页面调用
- 4.3.1.为何不是在首页中请求获取数据然后父传子?
- 4.3.2.为啥要在组件挂载完毕时调用api?
- 4.3.3.实现代码
- 4.4.列表数据的动态渲染
- 4.5.什么时候以及如何调用分页数据?
- 4.5.1.加载分页数据的时机以及如何实现
- 4.5.2.注意事项
- 4.5.3.实现步骤
- 4.6.分页数据的动态渲染
- 4.6.1.声明类型
- 4.6.2.升级`getHomeGoodsGuessLikeAPI`接口
- 4.6.3.调用子组件并传入页面参数
- 4.6.4.什么时候退出分页?
- 六.优化:下拉刷新
- 1.设置下拉刷新
- 2.监听到用户的下拉行为后需要做些什么?
- 3.下拉刷新时获取猜你喜欢组件,获取之后应该做什么?
- 七.优化:骨架屏
- 什么是骨架屏?
- 如何编写骨架屏文件?
零.首页最终效果
一.自定义导航栏
要求把默认的导航栏升级成自行以导航栏,并进行样式适配,做成可复用的组件
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"><imagemode="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"><imagemode="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"><imagemode="aspectFill"class="image"src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/slider_3.jpg"></image></navigator></swiper-item></swiper><!-- 指示点(自定义) --><view class="indicator"><textv-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"><textv-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"><textv-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"><!-- 动态渲染:指示点 --><textv-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">内部结构:图片+文本<imageclass="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.vueimport { getHomeBannerAPI, getHomeCategoryAPI } from '@/services/home.ts'
const categoryList = ref([])
// 获取首页分类数据
const getHomeCategoryData = async () => {const res = await getHomeCategoryAPI()categoryList.value = res.result
}
onLoad(() => {getHomeBannerData()getHomeCategoryData()
})
*此时categoryList.value
和 res.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"><navigatorclass="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"><imageclass="image"mode="aspectFit"src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_small_1.jpg"></image><imageclass="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.vueimport 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.value
和res.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 --><imagev-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"><navigatorclass="guess-item"v-for="item in 10":key="item":url="`/pages/goods/goods?id=4007498`"><imageclass="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 XtxSwiperXtxGuess: 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"><navigatorclass="guess-item"v-for="item in guessList":key="item.id":url="`/pages/goods/goods?id=4007498`"><imageclass="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-u
i的scroll-view
组件上的如下属性:
- 配置
refresher-enabled
属性,开启下拉刷新交互 - 监听
@refresherrefresh
事件,判断用户是否执行了下拉操作 - 配置
refresher-triggered
属性,关闭下拉状态
//index .vue<!-- 滚动容器 -->
<scroll-viewrefresher-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.wxml
和index.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
})