uniapp01-推荐模块
|总字数:2.6k|阅读时长:12分钟|浏览量:1
小兔鲜儿 - 推荐模块
主要实现 Tabs 交互、多 Tabs 列表分页加载数据。
动态获取数据
参考效果
推荐模块的布局结构是相同的,因此我们可以复用相同的页面及交互,只是所展示的数据不同。

静态结构
新建热门推荐页面文件,并在 pages.json
中添加路由(VS Code 插件自动完成)。
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
| // /src/pages/hot/hot.vue <script setup lang="ts"> // 热门推荐页 标题和url const hotMap = [ { type: "1", title: "特惠推荐", url: "/hot/preference" }, { type: "2", title: "爆款推荐", url: "/hot/inVogue" }, { type: "3", title: "一站买全", url: "/hot/oneStop" }, { type: "4", title: "新鲜好物", url: "/hot/new" }, ]; </script>
<template> <view class="viewport"> <!-- 推荐封面图 --> <view class="cover"> <image src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-20/84abb5b1-8344-49ae-afc1-9cb932f3d593.jpg" ></image> </view> <!-- 推荐选项 --> <view class="tabs"> <text class="text active">抢先尝鲜</text> <text class="text">新品预告</text> </view> <!-- 推荐列表 --> <scroll-view scroll-y class="scroll-view"> <view class="goods"> <navigator hover-class="none" class="navigator" v-for="goods in 10" :key="goods" :url="`/pages/goods/goods?id=`" > <image class="thumb" src="https://yanxuan-item.nosdn.127.net/5e7864647286c7447eeee7f0025f8c11.png" ></image> <view class="name ellipsis">不含酒精,使用安心爽肤清洁湿巾</view> <view class="price"> <text class="symbol">¥</text> <text class="number">29.90</text> </view> </navigator> </view> <view class="loading-text">正在加载...</view> </scroll-view> </view> </template>
<style lang="scss"> page { height: 100%; background-color: #f4f4f4; } .viewport { display: flex; flex-direction: column; height: 100%; padding: 180rpx 0 0; position: relative; } .cover { width: 750rpx; height: 225rpx; border-radius: 0 0 40rpx 40rpx; overflow: hidden; position: absolute; left: 0; top: 0; } .scroll-view { flex: 1; } .tabs { display: flex; justify-content: space-evenly; height: 100rpx; line-height: 90rpx; margin: 0 20rpx; font-size: 28rpx; border-radius: 10rpx; box-shadow: 0 4rpx 5rpx rgba(200, 200, 200, 0.3); color: #333; background-color: #fff; position: relative; z-index: 9; .text { margin: 0 20rpx; position: relative; } .active { &::after { content: ""; width: 40rpx; height: 4rpx; transform: translate(-50%); background-color: #27ba9b; position: absolute; left: 50%; bottom: 24rpx; } } } .goods { display: flex; flex-wrap: wrap; justify-content: space-between; padding: 0 20rpx 20rpx; .navigator { width: 345rpx; padding: 20rpx; margin-top: 20rpx; border-radius: 10rpx; background-color: #fff; } .thumb { width: 305rpx; height: 305rpx; } .name { height: 88rpx; font-size: 26rpx; } .price { line-height: 1; color: #cf4444; font-size: 30rpx; } .symbol { font-size: 70%; } .decimal { font-size: 70%; } }
.loading-text { text-align: center; font-size: 28rpx; color: #666; padding: 20rpx 0 50rpx; } </style>
|
获取页面参数
热门推荐页要根据页面参数区分需要获取的是哪种类型的推荐列表,然后再去调用相应的接口,来获取不同的数据,再渲染到页面当中。
项目首页(传递参数)
1 2 3 4
| // src/pages/index/components/HotPanel.vue <navigator :url="`/pages/hot/hot?type=${item.type}`"> …省略 </navigator>
|
热门推荐页(获取参数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // src/pages/hot/hot.vue <script setup lang="ts"> // 热门推荐页 标题和url const hotMap = [ { type: "1", title: "特惠推荐", url: "/hot/preference" }, { type: "2", title: "爆款推荐", url: "/hot/inVogue" }, { type: "3", title: "一站买全", url: "/hot/oneStop" }, { type: "4", title: "新鲜好物", url: "/hot/new" }, ]; // uniapp 获取页面参数 const query = defineProps<{ type: string; }>(); // console.log(query) const currHot = hotMap.find((v) => v.type === query.type); // 动态设置标题 uni.setNavigationBarTitle({ title: currHot!.title }); </script>
|
传递不同的页面参数,动态设置推荐页标题。
获取数据
地址参数
不同类型的推荐,需要调用不同的 API 接口:
type |
推荐类型 |
接口路径 |
1 |
特惠推荐 |
/hot/preference |
2 |
爆款推荐 |
/hot/inVogue |
3 |
一站买全 |
/hot/oneStop |
4 |
新鲜好物 |
/hot/new |
接口调用
调用接口获取推荐商品列表的数据,然后再将这些数据渲染出来。
接口地址:见上表
请求方式:GET
请求参数:
Query:
字段名称 |
是否必须 |
默认值 |
备注 |
subType |
否 |
无 |
推荐列表 Tab 项的 id |
page |
否 |
1 |
页码 |
pageSize |
否 |
10 |
每页商品数量 |
请求封装
经过分析,尽管不同类型推荐的请求 url 不同,但请求参数及响应格式都具有一致性,因此可以将接口的调用进行封装,参考代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { http } from "@/utils/http"; import type { PageParams } from "@/types/global";
type HotParams = PageParams & { subType?: string; };
export const getHotRecommendAPI = (url: string, data?: HotParams) => { return http<HotResult>({ method: "GET", url, data, }); };
|
类型声明
电商项目较为常见商品展示,商品的类型是可复用的,封装到 src/types/global.d.ts
文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
export type GoodsItem = { desc: string; discount: number; id: string; name: string; orderNum: number; picture: string; price: number; };
|
其实猜你喜欢的商品类型也相同,可复用通用商品类型,封装到 src/services/home.ts
文件中:
1 2 3 4 5
| import type { GoodsItem } from "@/types/global";
export type GuessItem = GoodsItem;
|
热门推荐类型如下,新建 src/types/hot.d.ts
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import type { PageResult, GoodsItem } from "./global";
export type HotResult = { id: string; bannerPicture: string; title: string; subTypes: SubTypeItem[]; };
export type SubTypeItem = { id: string; title: string; goodsItems: PageResult<GoodsItem>; };
|
最后,把获取到的数据结合模板语法渲染到页面中。
多 Tabs 分页加载
需要根据当前用户选中的 Tabs 加载对应的列表数据。

Tabs 交互基础
当用户点击页面中的 Tab 后,切换展示相应的商品列表,功能相对简单,快速实现即可。
参考代码
{2,3,13,14,25}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
| <script setup lang="ts"> // 高亮的下标 const activeIndex = ref(0); </script>
<template> <!-- 推荐选项 --> <view class="tabs"> <text class="text" v-for="(item, index) in subTypes" :key="item.id" :class="{ active: index === activeIndex }" @tap="activeIndex = index" > {{ item.title }} </text> </view> <!-- 推荐列表 --> <scroll-view scroll-y class="scroll-view" v-for="(item, index) in subTypes" :key="item.id" v-show="activeIndex === index" > ...省略 </scroll-view> </template>
|
选中 Tabs 分页
根据当前用户选中的 Tabs 加载对应的列表数据。
操作流程
- 根据高亮下标,获取对应列表数据
- 提取列表的分页参数,用于发送请求
- 滚动触底事件,页码累加,数组追加,退出判断等业务和常规分页基本一致
参考代码(总)
热门推荐页
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
| <script setup lang="ts"> import { getHotRecommendAPI } from "@/services/hot"; import type { SubTypeItem } from "@/types/hot"; import { onLoad } from "@dcloudio/uni-app"; import { ref } from "vue";
// 热门推荐页 标题和url const hotMap = [ { type: "1", title: "特惠推荐", url: "/hot/preference" }, { type: "2", title: "爆款推荐", url: "/hot/inVogue" }, { type: "3", title: "一站买全", url: "/hot/oneStop" }, { type: "4", title: "新鲜好物", url: "/hot/new" }, ];
// uniapp 获取页面参数 const query = defineProps<{ type: string; }>(); // 获取当前推荐信息 const currHot = hotMap.find((v) => v.type === query.type); // 动态设置标题 uni.setNavigationBarTitle({ title: currHot!.title });
// 推荐封面图 const bannerPicture = ref(""); // 推荐选项 const subTypes = ref<(SubTypeItem & { finish?: boolean })[]>([]); // 高亮的下标 const activeIndex = ref(0); // 获取热门推荐数据 const getHotRecommendData = async () => { const res = await getHotRecommendAPI(currHot!.url, { // 技巧:环境变量,开发环境,修改初始页面方便测试分页结束 page: import.meta.env.DEV ? 30 : 1, pageSize: 10, }); // 保存封面 bannerPicture.value = res.result.bannerPicture; // 保存列表 subTypes.value = res.result.subTypes; };
// 页面加载 onLoad(() => { getHotRecommendData(); });
// 滚动触底 const onScrolltolower = async () => { // 获取当前选项 const currsubTypes = subTypes.value[activeIndex.value]; // 分页条件 if (currsubTypes.goodsItems.page < currsubTypes.goodsItems.pages) { // 当前页码累加 currsubTypes.goodsItems.page++; } else { // 标记已结束 currsubTypes.finish = true; // 退出并轻提示 return uni.showToast({ icon: "none", title: "没有更多数据了~" }); }
// 调用API传参 const res = await getHotRecommendAPI(currHot!.url, { subType: currsubTypes.id, page: currsubTypes.goodsItems.page, pageSize: currsubTypes.goodsItems.pageSize, }); // 新的列表选项 const newsubTypes = res.result.subTypes[activeIndex.value]; // 数组追加 currsubTypes.goodsItems.items.push(...newsubTypes.goodsItems.items); }; </script>
<template> <view class="viewport"> <!-- 推荐封面图 --> <view class="cover"> <image :src="bannerPicture"></image> </view> <!-- 推荐选项 --> <view class="tabs"> <text v-for="(item, index) in subTypes" :key="item.id" class="text" :class="{ active: index === activeIndex }" @tap="activeIndex = index" >{{ item.title }}</text > </view> <!-- 推荐列表 --> <scroll-view v-for="(item, index) in subTypes" :key="item.id" v-show="activeIndex === index" scroll-y class="scroll-view" @scrolltolower="onScrolltolower" > <view class="goods"> <navigator hover-class="none" class="navigator" v-for="goods in item.goodsItems.items" :key="goods.id" :url="`/pages/goods/goods?id=${goods.id}`" > <image class="thumb" :src="goods.picture"></image> <view class="name ellipsis">{{ goods.name }}</view> <view class="price"> <text class="symbol">¥</text> <text class="number">{{ goods.price }}</text> </view> </navigator> </view> <view class="loading-text"> {{ item.finish ? "没有更多数据了~" : "正在加载..." }} </view> </scroll-view> </view> </template>
|