文章目录
零.首页最终效果
一.自定义导航栏
要求把默认的导航栏升级成自行以导航栏,并进行样式适配,做成可复用的组件
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 />
仍有区别----没有类型声明
// 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.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">
<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.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 -->
<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-u
i的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.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
})