危险

为之则易,不为则难

0%

02_小兔鲜

✔ 掌握自定义导航栏的方法。

✔ 掌握 http 模块的使用。

✔ 掌握上拉分页加载和下拉刷新的实现。

自定义导航栏

参考效果

自定义导航栏的样式需要适配不同的机型。

自定义导航栏

操作步骤

  1. 准备组件。

  2. 隐藏默认导航栏,修改文字颜色。

  3. 样式适配 -> 安全区域。

静态结构

  1. 新建业务组件:pages/index/componets/CustomNavbar.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<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;
.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>
  1. 引入组件,pages/index/index.vue
1
2
3
4
5
6
7
8
<script setup lang="ts">
import CustomNavbar from './components/CustomNavbar.vue'
</script>

<template>
<custom-navbar />
<view class="index"> </view>
</template>
  1. 隐藏默认的导航栏(自定义),pages.json
1
2
3
4
5
6
7
8
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom", // 隐藏默认导航
"navigationBarTextStyle": "white",
"navigationBarTitleText": "首页"
}
}

安全区域

不同手机的安全区域不同,适配安全区域能防止页面重要内容被遮挡,可通过 uni.getSystemInfoSync() 获取屏幕边界到安全区的距离。

安全区域

组件安全区适配,pages/index/componets/CustomNavbar.vue

1
2
3
4
5
6
7
8
9
10
11
<script>
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
</script>

<template>
<!-- 顶部占位 -->
<view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<!-- ...省略 -->
</view>
</template>

通用轮播组件

参考效果

小兔鲜儿项目中总共有两处广告位,分别位于【首页】和【商品分类页】,需要封装成通用组件。

轮播组件

静态结构

首页广告布局为独立的组件 XtxSwiper ,位于的 src/components/XtxSwiper.vue 目录中,静态结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<script setup lang="ts">
import { ref } from 'vue'

const activeIndex = ref(0)
</script>

<template>
<view class="carousel">
<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">
:host {
display: block;
height: 280rpx;
}
/* 轮播图 */
.carousel {
height: 100%;
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>

自动导入全局组件

1
2
3
4
5
6
7
8
9
10
11
12
{
// ...
"easycom": {
"autoscan": true,
// 组件自动引入规则
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
// 以 Xtx 开头的组件,在 components 目录中查找
"^Xtx(.*)": "@/components/Xtx$1.vue"
}
}
}

首页测试,pages/index/index.vue

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
import CustomNavbar from './components/CustomNavbar.vue'
</script>

<template>
<custom-navbar />
<XtxSwiper />
<view class="index"> </view>
</template>

全局组件类型声明

Volor 插件说明:Vue Language Toolssrc/types/components.d.ts

1
2
3
4
5
6
import XtxSwiper from '@/components/XtxSwiper.vue'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
XtxSwiper: typeof XtxSwiper
}
}

指示功能

给 swiper 绑定 @change 事件。

1
<swiper @change="onChange"></swiper>
1
2
3
4
const activeIndex = ref(0)
const onChange: UniHelper.SwiperOnChange = (ev) => {
activeIndex.value = ev.detail!.current
}

获取数据

接口地址

  1. 封装请求函数,src/services/home.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { http } from '@/utils/http'

/**
* 首页-广告区域-小程序
* @param distributionSite 广告区域展示位置(投放位置 投放位置,1为首页,2为分类商品页) 默认是1
*/
export const getHomeBannerAPI = (distributionSite = 1) => {
return http({
method: 'GET',
url: '/home/banner',
data: {
distributionSite
}
})
}
  1. 调用请求函数,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
import { getHomeBannerAPI } from '@/services/home'
import CustomNavbar from './components/CustomNavbar.vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'

const bannerList = ref([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
onLoad(() => {
getHomeBannerData()
})

类型声明

  1. 准备类型,src/types/home.d.ts
1
2
3
4
5
6
7
8
9
10
11
// 首页-广告区域数据类型
export type BannerItem = {
/** 跳转链接 */
hrefUrl: string
/** id */
id: string
/** 图片链接 */
imgUrl: string
/** 跳转类型 */
type: number
}
  1. 指定后端返回数据的类型,src/services/home.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import type { BannerItem } from '@/types/home'
import { http } from '@/utils/http'

/**
* 首页-广告区域-小程序
* @param distributionSite 广告区域展示位置(投放位置 投放位置,1为首页,2为分类商品页) 默认是1
*/
export const getHomeBannerAPI = (distributionSite = 1) => {
return http<BannerItem[]>({
method: 'GET',
url: '/home/banner',
data: {
distributionSite
}
})
}
  1. 指定 bannerList 数据的类型,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { getHomeBannerAPI } from '@/services/home'
import CustomNavbar from './components/CustomNavbar.vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
// #1
import type { BannerItem } from '@/types/home'

// #2
const bannerList = ref<BannerItem[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
onLoad(() => {
getHomeBannerData()
})
  1. 渲染 bannerList。

父组件传递数据,pages/index/index.vue

1
2
3
4
5
6
<template>
<custom-navbar />
<!-- #1 -->
<XtxSwiper :list="bannerList" />
<view class="index"> </view>
</template>

子组件接收数据,src/components/XtxSwiper.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script setup lang="ts">
import type { BannerItem } from '@/types/home'
import { ref } from 'vue'

const activeIndex = ref(0)

const onChange: UniHelper.SwiperOnChange = (ev) => {
activeIndex.value = ev.detail.current
}

// #2
defineProps<{
list: BannerItem[]
}>()
</script>

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

小结一下

首页分类渲染

参考效果

1677150782440

静态结构

  1. 前台类目布局为独立的属于首页的业务组件,因此存放到首页的 components 目录中,pages/index/components/CategoryPanel.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<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. 使用组件,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script setup lang="ts">
import { getHomeBannerAPI } from '@/services/home'
import CustomNavbar from './components/CustomNavbar.vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import type { BannerItem } from '@/types/home'
// #1
import CategoryPanel from './components/CategoryPanel.vue'

const bannerList = ref<BannerItem[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
onLoad(() => {
getHomeBannerData()
});
</script>

<template>
<custom-navbar />
<XtxSwiper :list="bannerList" />
<!-- #2 -->
<CategoryPanel />
<view class="index"> </view>
</template>
<style lang="scss">
/* #3 */
page {
background-color: #f7f7f7;
}
</style>

获取数据

接口地址

  1. 定义类型,types/home.d.ts
1
2
3
4
5
6
7
8
9
/** 首页-前台类目数据类型 */
export type CategoryItem = {
/** 图标路径 */
icon: string
/** id */
id: string
/** 分类名称 */
name: string
}
  1. 封装请求函数,services/home.ts
1
2
3
4
5
6
7
8
9
10
import type { CategoryItem } from '@/types/home'
/**
* 首页-前台分类-小程序
*/
export const getHomeCategoryAPI = () => {
return http<CategoryItem[]>({
method: 'GET',
url: '/home/category/mutli',
})
}
  1. 获取数据,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { getHomeBannerAPI, getHomeCategoryAPI } from '@/services/home'
import CustomNavbar from './components/CustomNavbar.vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
// #0
import type { BannerItem, CategoryItem } from '@/types/home'
import CategoryPanel from './components/CategoryPanel.vue'

const bannerList = ref<BannerItem[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
// #1
const categoryList = ref<CategoryItem[]>([])
// #2
const getHomeCategoryData = async () => {
const res = await getHomeCategoryAPI()
categoryList.value = res.result
}
onLoad(() => {
getHomeBannerData()
// #3
getHomeCategoryData()
})

渲染数据

  1. 父组件传递数据,pages/index/index.vue
1
<CategoryPanel :list="categoryList" />
  1. 子组件接收数据并循环,pages/index/components/CategoryPanel.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup lang="ts">
import type { CategoryItem } from '@/types/home'

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. 热门推荐布局为独立的属于首页的业务组件,因此存放到 pages/index/components/HotPanel.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<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. 首页中引入组件,pages/index/index.vue
1
2
3
4
<!-- ... -->
<CategoryPanel :list="categoryList" />
<HotPanel />
<!-- ... -->

获取数据

接口地址

  1. 声明类型,types/home.d.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 首页-热门推荐数据类型 */
export type HotItem = {
/** 说明 */
alt: string
/** id */
id: string
/** 图片集合[ 图片路径 ] */
pictures: string[]
/** 跳转地址 */
target: string
/** 标题 */
title: string
/** 推荐类型 */
type: string
}
  1. 接口封装,services/home.ts
1
2
3
4
5
6
7
8
9
10
import type { HotItem } from '@/types/home'
/**
* 首页-热门推荐-小程序
*/
export const getHomeHotAPI = () => {
return http<HotItem[]>({
method: 'GET',
url: '/home/hot/mutli',
})
}
  1. 获取数据,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// #1
import {
getHomeBannerAPI,
getHomeCategoryAPI,
getHomeHotAPI
} from '@/services/home'
import CustomNavbar from './components/CustomNavbar.vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
// #2
import type { BannerItem, CategoryItem, HotItem } from '@/types/home'
import CategoryPanel from './components/CategoryPanel.vue'
import HotPanel from './components/HotPanel.vue'

const bannerList = ref<BannerItem[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
const categoryList = ref<CategoryItem[]>([])
const getHomeCategoryData = async () => {
const res = await getHomeCategoryAPI()
categoryList.value = res.result
}
// #3
const hotList = ref<HotItem[]>([])
// #4
const getHomeHotData = async () => {
const res = await getHomeHotAPI()
hotList.value = res.result
}
onLoad(() => {
getHomeBannerData()
getHomeCategoryData()
// #5
getHomeHotData()
})

渲染数据

  1. 父组件传递数据,pages/index/index.vue
1
<HotPanel :list="hotList" />
  1. 子组件接收数据并渲染,src/pages/index/components/HotPanel.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script lang="ts" setup>
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"
>
<image
v-for="src in item.pictures"
:key="src"
class="image"
mode="aspectFit"
:src="src"
></image>
</navigator>
</view>
</view>
</template>

小结一下

猜你喜欢模块

参考效果

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

猜你喜欢

静态结构

  1. 猜你喜欢是一个通用组件,多个页面会用到,存放到 src/components/XtxGuess.vue 目录中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<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. 定义全局组件类型,types/components.d.ts
1
2
3
4
5
6
7
8
9
10
import XtxSwiper from '@/components/XtxSwiper.vue'
import XtxGuess from '@/components/XtxGuess.vue'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
XtxSwiper: typeof XtxSwiper
XtxGuess: typeof XtxGuess
}
}
// 组件实例类型
export type XtxGuessInstance = InstanceType<typeof XtxGuess>
  1. 使用组件,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<custom-navbar />
<scroll-view scroll-y class="scroll-view">
<XtxSwiper :list="bannerList" />
<CategoryPanel :list="categoryList" />
<HotPanel :list="hotList" />
<XtxGuess />
</scroll-view>
</template>
<style lang="scss">
page {
background-color: #f7f7f7;
height: 100%;
display: flex;
flex-direction: column;
}

.scroll-view {
flex: 1;
}
</style>

获取数据

接口地址

  1. 定义类型。

通用分页参数类型,src/types/global.d.ts

1
2
3
4
5
6
7
/** 通用分页参数类型 */
export type PageParams = {
/** 页码:默认值为 1 */
page?: number
/** 页大小:默认值为 10 */
pageSize?: number
}

通用分页结果类型如下,src/types/global.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
/** 通用分页结果类型 */
export type PageResult<T> = {
/** 列表数据 */
items: T[]
/** 总条数 */
counts: number
/** 当前页数 */
page: number
/** 总页数 */
pages: number
/** 每页条数 */
pageSize: number
}

猜你喜欢-商品类型,src/types/home.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 猜你喜欢-商品类型 */
export type GuessItem = {
/** 商品描述 */
desc: string
/** 商品折扣 */
discount: number
/** id */
id: string
/** 商品名称 */
name: string
/** 商品已下单数量 */
orderNum: number
/** 商品图片 */
picture: string
/** 商品价格 */
price: number
}
  1. 封装请求函数,src/services/home.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
import type { PageParams, PageResult } from '@/types/global'
import type { GuessItem } from '@/types/home'
import { http } from '@/utils/http'
/**
* 猜你喜欢-小程序
*/
export const getHomeGoodsGuessLikeAPI = (data?: PageParams) => {
return http<PageResult<GuessItem[]>>({
method: 'GET',
url: '/home/goods/guessLike',
data,
})
}
  1. 调用接口,src/components/XtxGuess.vue
1
2
3
4
5
6
7
8
9
10
11
import { getHomeGoodsGuessLikeAPI } from '@/services/home'
import { onMounted } from 'vue'

const getHomeGoodsGuessLikeData = async () => {
const res = await getHomeGoodsGuessLikeAPI()
console.log(res)
}

onMounted(() => {
getHomeGoodsGuessLikeData()
})

小结一下

列表渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<script lang="ts" setup>
import { getHomeGoodsGuessLikeAPI } from '@/services/home'
// #1
import type { GuessItem } from '@/types/home'
import { onMounted, ref } from 'vue'

// #2
const guessList = ref<GuessItem[]>()
const getHomeGoodsGuessLikeData = async () => {
const res = await getHomeGoodsGuessLikeAPI()
// #3
guessList.value = res.result.items
}

onMounted(() => {
getHomeGoodsGuessLikeData()
});
</script>
<template>
<!-- 猜你喜欢 -->
<view class="caption">
<text class="text">猜你喜欢</text>
</view>
<view class="guess">
<!-- #4 -->
<navigator
class="guess-item"
v-for="item in guessList"
:key="item.id"
:url="`/pages/goods/goods?id=${item.id}`"
>
<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>
<view class="loading-text"> 正在加载... </view>
</template>

小结一下

分页准备

  1. 监听滚动触底事件,pages/index/index.vue
1
<scroll-view scroll-y class="scroll-view" @scrolltolower="onScrolltolower"></scroll-view>
  1. 给 XtxGuess 组件绑定 ref,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
<script>
import type { XtxGuessInstance } from '@/types/components'
// #3
const guessRef = ref<XtxGuessInstance>()
const onScrolltolower = () => {
// #5
guessRef.value?.getMore()
}
</script>
<XtxGuess ref="guessRef" />
  1. 子组件暴露获取数据的方法,src/components/XtxGuess.vue
1
2
3
4
// #4
defineExpose({
getMore: getHomeGoodsGuessLikeData
})

分页加载

src/components/XtxGuess.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { getHomeGoodsGuessLikeAPI } from '@/services/home'
import type { PageParams } from '@/types/global'
import type { GuessItem } from '@/types/home'
import { onMounted, ref } from 'vue'

// #1
const pageParams: Required<PageParams> = {
page: 1,
pageSize: 10
}

const guessList = ref<GuessItem[]>([])
const getHomeGoodsGuessLikeData = async () => {
// #2
const res = await getHomeGoodsGuessLikeAPI(pageParams)

// #3
guessList.value.push(...res.result.items)

// #4
pageParams.page++
}

onMounted(() => {
getHomeGoodsGuessLikeData()
})

defineExpose({
getMore: getHomeGoodsGuessLikeData
})

分页条件

src/components/XtxGuess.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// #2
const finish = ref(false)
const guessList = ref<GuessItem[]>([])
const getHomeGoodsGuessLikeData = async () => {
// #4
if (finish.value === true) {
return uni.showToast({ icon: 'none', title: '没有更多数据~' })
}
const res = await getHomeGoodsGuessLikeAPI(pageParams)
guessList.value.push(...res.result.items)
// #1
if (pageParams.page < res.result.pages) {
pageParams.page++
} else {
// #3
finish.value = true
}
}
1
2
3
4
<!-- #5 -->
<view class="loading-text">
{{ finish ? '没有更多数据~' : '正在加载...' }}
</view>

下拉刷新

1
2
3
<!-- #1 开启下拉刷新 refresher-enabled 并绑定 @refresherrefresh -->
<!-- #2 绑定 refresher-triggered -->
<scroll-view refresher-enabled @refresherrefresh="onRefresherrefresh" :refresher-triggered="isTriggered" scroll-y class="scroll-view" @scrolltolower="onScrolltolower"></scroll-view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// #3
const isTriggered = ref(false)
const onRefresherrefresh = async () => {
isTriggered.value = true
// 加载数据
/* await getHomeBannerData()
await getHomeCategoryData()
await getHomeHotData() */
await Promise.all([
getHomeBannerData(),
getHomeCategoryData(),
getHomeHotData()
])
// 关闭动画
isTriggered.value = false
}

小结一下

猜你喜欢

下拉刷新时,重置猜你喜欢的数据。

  1. src/components/XtxGuess.vue
1
2
3
4
5
6
7
8
9
const resetData = () => {
pageParams.page = 1
guessList.value = []
finish.value = false
}
defineExpose({
getMore: getHomeGoodsGuessLikeData,
resetData
})
  1. pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
const onRefresherrefresh = async () => {
isTriggered.value = true
// #1
guessRef.value?.resetData()
await Promise.all([
getHomeBannerData(),
getHomeCategoryData(),
getHomeHotData(),
// #2
guessRef.value?.getMore()
])
isTriggered.value = false
}

骨架屏的优化

骨架屏是页面的一个空白版本,通常会在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。

参考效果

骨架屏作用是缓解用户等待时的焦虑情绪,属于用户体验优化方案。

骨架屏

生成骨架

微信开发者工具提供了自动生成骨架屏代码的能力,使用时需要把自动生成的 xxx.skeleton.vuexxx.skeleton.wxss 封装成 vue 组件。

骨架屏

具体代码

  1. 准备骨架屏,pages/index/components/PageSkeleton.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
<template name="skeleton">
<!-- 轮播图 -->
<view is="components/XtxSwiper">
<view class="carousel XtxSwiper--carousel">
<swiper :circular="true" :interval="3000" :current="0" :autoplay="false">
<swiper-item
style="
position: absolute;
width: 100%;
height: 100%;
transform: translate(0%, 0px) translateZ(0px);
"
>
<navigator class="navigator XtxSwiper--navigator" hover-class="none">
<image
class="image XtxSwiper--image sk-image"
mode="aspectFill"
></image>
</navigator>
</swiper-item>
</swiper>
<view class="indicator XtxSwiper--indicator">
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot active XtxSwiper--active"></text>
</view>
</view>
</view>
<!-- 分类 -->
<view is="pages/index/components/CategoryPanel">
<view class="category CategoryPanel--category">
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-293 sk-text"
>居家</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-384 sk-text"
>锦鲤</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-289 sk-text"
>服饰</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-295 sk-text"
>母婴</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-201 sk-text"
>个护</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-35 sk-text"
>严选</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-352 sk-text"
>数码</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-178 sk-text"
>运动</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-79 sk-text"
>杂项</text
>
</navigator>
<navigator
class="category-item CategoryPanel--category-item"
hover-class="none"
>
<image class="icon CategoryPanel--icon sk-image"></image>
<text
class="text CategoryPanel--text sk-transparent sk-text-14-2857-284 sk-text"
>品牌</text
>
</navigator>
</view>
</view>
<!-- 热门推荐 -->
<view is="pages/index/components/HotPanel">
<view class="panel HotPanel--panel hot HotPanel--hot">
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text
class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-184 sk-text"
>特惠推荐</text
>
<text
class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-542 sk-text"
>精选全攻略</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text
class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-521 sk-text"
>爆款推荐</text
>
<text
class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-696 sk-text"
>最受欢迎</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text
class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-954 sk-text"
>一站买全</text
>
<text
class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-808 sk-text"
>精心优选</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text
class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-961 sk-text"
>新鲜好物</text
>
<text
class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-730 sk-text"
>生活加分项</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
<image
class="image HotPanel--image sk-image"
mode="aspectFit"
></image>
</navigator>
</view>
</view>
</view>
<!-- 猜你喜欢 -->
<view is="components/XtxGuess" class="r">
<view class="caption XtxGuess--caption">
<text
class="text XtxGuess--text sk-transparent sk-text-0-0000-512 sk-text sk-pseudo sk-pseudo-circle"
>猜你喜欢</text
>
</view>
</view>
</template>

<style>
.sk-transparent {
color: transparent !important;
}
.sk-text-3-5714-491 {
background-image: linear-gradient(
transparent 3.5714%,
#eeeeee 0%,
#eeeeee 96.4286%,
transparent 0%
) !important;
background-size: 100% 28rpx;
position: relative !important;
}
.sk-text {
background-origin: content-box !important;
background-clip: content-box !important;
background-color: transparent !important;
color: transparent !important;
background-repeat: repeat-y !important;
}
.sk-text-14-2857-741 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 39.2rpx;
position: relative !important;
}
.sk-text-14-2857-293 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-384 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-289 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-295 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-201 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-35 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-352 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-178 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-79 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-284 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-184 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-542 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-521 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-696 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-954 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-808 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-961 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-730 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-0-0000-512 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 32rpx;
position: relative !important;
}
.sk-image {
background: #efefef !important;
}
.sk-pseudo::before,
.sk-pseudo::after {
background: #efefef !important;
background-image: none !important;
color: transparent !important;
border-color: transparent !important;
}
.sk-pseudo-rect::before,
.sk-pseudo-rect::after {
border-radius: 0 !important;
}
.sk-pseudo-circle::before,
.sk-pseudo-circle::after {
border-radius: 50% !important;
}
.sk-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
</style>
  1. 应用骨架屏,pages/index/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// #1
const isLoading = ref(false)
onLoad(async () => {
// #2
isLoading.value = true
// #3
await Promise.all([
getHomeBannerData(),
getHomeCategoryData(),
getHomeHotData()
])
// #4
isLoading.value = false
})
1
2
3
4
5
6
7
<PageSkeleton v-if="isLoading" />
<template v-else>
<XtxSwiper :list="bannerList" />
<CategoryPanel :list="categoryList" />
<HotPanel :list="hotList" />
<XtxGuess ref="guessRef" />
</template>

小结一下