若没有离开海岸的勇气,就必然无法横跨大海

序
很多时候UI设计师提供的底部tabbar是具有“设计感”,官网底部导航无法实现,如果在页面都引入自定义tabbar在点击时候就会有闪屏的情况通
过组件引入可以解决点击导航不闪屏,初次打开使用生命周期,首页上拉加载后通知组件加载第二页
1. **`mounted` 生命周期**:组件渲染时自动触发,适合请求初始数据
2. **`loadMoreData` 方法**:暴露给父页面调用,实现上拉加载
3. **状态管理**:`isLoading` 防止重复加载,`pageNum` 记录当前页码
声明:新手本文介绍了一套完整的 uni-app Tabbar 组件化方案,实际项目可下载源码 预览后测试在使用
1. **架构设计**:Tabbar 容器 + 子页面组件,实现懒加载
2. **生命周期**:`mounted` 自动触发数据请求
3. **事件通信**:`onReachBottom` 统一管理,通过 `ref` 分发
4. **状态管理**:加载状态、页码、防重复加载
截图


案例
index.vue
<template>
<view class="main-page">
<!-- 页面内容区域 -->
<view class="page-content">
<!-- 首页 -->
<home-page v-if="activeTab === 0" ref="homePage" />
<!-- 分类页 -->
<category-page v-if="activeTab === 1" ref="categoryPage" />
</view>
<!-- Tabbar 底部导航 -->
<wd-tabbar v-model="activeTab" fixed safe-area-inset-bottom @change="handleTabChange">
<wd-tabbar-item title="首页" icon="home" :name="0" />
<wd-tabbar-item title="分类" icon="apps" :name="1" />
</wd-tabbar>
</view>
</template>
<script>
import HomePage from './components/HomePage.vue'
import CategoryPage from './components/CategoryPage.vue'
export default {
components: {
HomePage,
CategoryPage
},
data() {
return {
activeTab: 0
}
},
onLoad() {
console.log('主页面加载完成')
},
// 页面滚动到底部时触发
onReachBottom() {
console.log('页面滚动到底部,当前tab:', this.activeTab)
this.handleLoadMore()
},
methods: {
handleTabChange(event) {
console.log('切换tab:', event)
const titles = ['首页', '分类']
uni.setNavigationBarTitle({
title: titles[event.value]
})
},
// 通知当前显示的组件加载更多
handleLoadMore() {
if (this.activeTab === 0) {
// 通知首页组件加载更多
this.$refs.homePage && this.$refs.homePage.loadMoreData()
} else if (this.activeTab === 1) {
// 通知分类组件加载更多
this.$refs.categoryPage && this.$refs.categoryPage.loadMoreData()
}
}
}
}
</script>
<style lang="scss" scoped>
.main-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.page-content {
padding-bottom: 100rpx;
}
</style>
组件
CategoryPage.vue
<template>
<view class="category-page">
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">商品分类</text>
</view>
<!-- 分类宫格 - 一行2个 -->
<view class="category-grid">
<wd-grid :column="2" :border="true" :gutter="0">
<wd-grid-item v-for="(item, index) in categoryList" :key="index"
@click="handleCategoryClick(item, index)" custom-class="category-item">
<template #default>
<view class="category-content">
<wd-img :src="item.image" width="80px" height="80px" radius="12px" class="category-image" />
<view class="category-info">
<text class="category-title">{{ item.title }}</text>
<text class="category-subtitle">{{ item.subtitle }}</text>
</view>
</view>
</template>
</wd-grid-item>
</wd-grid>
</view>
<!-- 分类导航标签 -->
<view class="category-tags">
<wd-cell-group title="热门分类" :border="false">
<wd-cell title="快速筛选">
<template #default>
<view class="tag-list">
<wd-tag v-for="(tag, idx) in hotTags" :key="idx" :type="tag.type"
@click="handleTagClick(tag, idx)">
{{ tag.name }}
</wd-tag>
</view>
</template>
</wd-cell>
</wd-cell-group>
</view>
<!-- 推荐商品卡片 -->
<view class="recommend-section">
<wd-divider>为你推荐</wd-divider>
<view class="recommend-list">
<wd-card v-for="(product, index) in recommendList" :key="index" :type="'rectangle'"
@click="handleProductClick(product, index)">
<template #title>
<view class="card-header">
<wd-img :src="product.image" width="100%" height="150px" radius="8px" mode="aspectFill" />
</view>
</template>
<view class="card-content">
<text class="product-name">{{ product.name }}</text>
<text class="product-desc">{{ product.desc }}</text>
<view class="product-price">
<text class="price">¥{{ product.price }}</text>
<text class="original-price">¥{{ product.originalPrice }}</text>
</view>
</view>
</wd-card>
</view>
</view>
<!-- 加载更多提示 -->
<view class="load-more-section">
<wd-loadmore :state="loadState" />
</view>
</view>
</template>
<script>
export default {
name: 'CategoryPage',
data() {
return {
// 分类列表数据 - 一行2个
categoryList: [
{
title: '数码电器',
subtitle: '手机电脑 数码配件',
image: 'https://picsum.photos/200/200?random=1',
path: '/pages/category/digital'
},
{
title: '服装服饰',
subtitle: '男装女装 潮流穿搭',
image: 'https://picsum.photos/200/200?random=2',
path: '/pages/category/clothing'
},
{
title: '美妆护肤',
subtitle: '护肤彩妆 个人护理',
image: 'https://picsum.photos/200/200?random=3',
path: '/pages/category/beauty'
},
{
title: '食品生鲜',
subtitle: '新鲜水果 休闲零食',
image: 'https://picsum.photos/200/200?random=4',
path: '/pages/category/food'
},
{
title: '家居家装',
subtitle: '家具用品 装修建材',
image: 'https://picsum.photos/200/200?random=5',
path: '/pages/category/home'
},
{
title: '运动户外',
subtitle: '运动装备 户外用品',
image: 'https://picsum.photos/200/200?random=6',
path: '/pages/category/sports'
},
{
title: '图书文具',
subtitle: '图书教材 办公文具',
image: 'https://picsum.photos/200/200?random=7',
path: '/pages/category/books'
},
{
title: '母婴用品',
subtitle: '奶粉尿裤 玩具用品',
image: 'https://picsum.photos/200/200?random=8',
path: '/pages/category/baby'
},
{
title: '汽车用品',
subtitle: '汽车配件 保养用品',
image: 'https://picsum.photos/200/200?random=9',
path: '/pages/category/car'
},
{
title: '宠物生活',
subtitle: '宠物食品 宠物用品',
image: 'https://picsum.photos/200/200?random=10',
path: '/pages/category/pet'
}
],
// 热门标签
hotTags: [
{ name: '新品', type: 'primary' },
{ name: '热销', type: 'danger' },
{ name: '特价', type: 'warning' },
{ name: '推荐', type: 'success' },
{ name: '限时', type: 'primary' },
{ name: '爆款', type: 'danger' }
],
// 推荐商品列表
recommendList: [
{
name: '无线蓝牙耳机',
desc: '高品质音效 长续航',
price: 199,
originalPrice: 299,
image: 'https://picsum.photos/300/200?random=20'
},
{
name: '智能手表',
desc: '健康监测 运动追踪',
price: 599,
originalPrice: 799,
image: 'https://picsum.photos/300/200?random=21'
},
{
name: '便携充电宝',
desc: '20000mAh 快充',
price: 89,
originalPrice: 129,
image: 'https://picsum.photos/300/200?random=22'
},
{
name: '机械键盘',
desc: 'RGB背光 青轴手感',
price: 299,
originalPrice: 399,
image: 'https://picsum.photos/300/200?random=23'
}
],
// 加载状态
loadState: 'loading',
// 当前页码
pageNum: 1,
// 是否正在加载
isLoading: false
}
},
mounted() {
// 组件打开时执行
this.handleComponentOpen()
},
methods: {
// 组件打开时执行
handleComponentOpen() {
uni.showToast({
title: '组件【分类】打开了',
icon: 'none',
duration: 2000
})
console.log('组件【分类】打开了')
},
// 加载更多数据 - 由父页面通过 ref 调用
loadMoreData() {
this.isLoading = true
this.loadState = 'loading'
// 模拟加载第*页
uni.showToast({
title: `模拟加载第${this.pageNum}页`,
icon: 'none',
duration: 1500
})
console.log(`模拟加载第${this.pageNum}页`)
// 模拟异步加载
setTimeout(() => {
// 添加新数据
const newProducts = [
{
name: `上拉加载商品 ${this.pageNum}-1`,
desc: `这是第${this.pageNum}页加载的商品描述...`,
price: 99 + this.pageNum * 10,
originalPrice: 199 + this.pageNum * 10,
image: `https://picsum.photos/300/200?random=${30 + this.pageNum * 2}`
},
{
name: `上拉加载商品 ${this.pageNum}-2`,
desc: `这是第${this.pageNum}页加载的商品描述...`,
price: 149 + this.pageNum * 10,
originalPrice: 249 + this.pageNum * 10,
image: `https://picsum.photos/300/200?random=${31 + this.pageNum * 2}`
}
]
this.recommendList.push(...newProducts)
this.pageNum++
this.isLoading = false
this.loadState = 'loading'
}, 1000)
},
// 点击分类
handleCategoryClick(item, index) {
console.log('点击分类:', item, index)
uni.showToast({
title: item.title,
icon: 'none'
})
// 可以跳转到分类详情页
// uni.navigateTo({ url: item.path })
},
// 点击标签
handleTagClick(tag, index) {
console.log('点击标签:', tag, index)
uni.showToast({
title: tag.name,
icon: 'none'
})
},
// 点击推荐商品
handleProductClick(product, index) {
console.log('点击商品:', product, index)
uni.showToast({
title: product.name,
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.category-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 20rpx;
}
.page-header {
background-color: #fff;
padding: 30rpx;
text-align: center;
border-bottom: 1rpx solid #eee;
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.category-grid {
margin: 20rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.category-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 20rpx;
}
.category-image {
margin-bottom: 16rpx;
}
.category-info {
display: flex;
flex-direction: column;
align-items: center;
}
.category-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.category-subtitle {
font-size: 24rpx;
color: #999;
}
.category-tags {
margin: 0 20rpx 20rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding: 20rpx 0;
}
.recommend-section {
margin: 0 20rpx;
}
.recommend-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 20rpx;
}
.recommend-list ::v-deep .wd-card {
width: calc(50% - 10rpx);
margin-bottom: 20rpx;
}
.card-header {
width: 100%;
}
.card-content {
padding: 16rpx;
}
.product-name {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.product-desc {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 12rpx;
}
.product-price {
display: flex;
align-items: baseline;
gap: 12rpx;
}
.price {
font-size: 32rpx;
font-weight: bold;
color: #ff4d4f;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
.load-more-section {
margin: 20rpx;
}
</style>
HomePage.vue
<template>
<view class="home-page">
<!-- 搜索栏 -->
<view class="search-section">
<wd-search v-model="searchValue" placeholder="搜索商品、文章" hide-cancel @search="handleSearch"
@click="handleSearchClick" />
</view>
<!-- 轮播图 -->
<view class="swiper-section">
<wd-swiper :list="swiperList" :height="180" :autoplay="true" :interval="3000" :duration="500" :loop="true"
indicator :indicator-props="{ type: 'dots', position: 'bottom' }" @click="handleSwiperClick" />
</view>
<!-- 通知栏 -->
<view class="notice-section">
<wd-notice-bar :text="noticeText" prefix="notification" :scrollable="true" :speed="50" closable
@click="handleNoticeClick" />
</view>
<!-- 金刚区 - 8个功能入口 -->
<view class="grid-section">
<wd-grid :column="4" :border="false" :gutter="10">
<wd-grid-item v-for="(item, index) in gridList" :key="index" :icon="item.icon" :text="item.text"
icon-size="28px" @click="handleGridClick(item, index)" />
</wd-grid>
</view>
<!-- 最新文章列表 -->
<view class="article-section">
<wd-cell-group :title="'最新文章'" :border="false">
<wd-cell v-for="(article, index) in articleList" :key="index" :title="article.title"
:label="article.desc" :value="article.date" is-link @click="handleArticleClick(article, index)">
<template #icon>
<wd-img :src="article.cover" width="60px" height="60px" radius="8px"
style="margin-right: 12px;" />
</template>
</wd-cell>
</wd-cell-group>
</view>
<!-- 加载更多提示 -->
<view class="load-more-section">
<wd-loadmore :state="loadState" />
</view>
</view>
</template>
<script>
export default {
name: 'HomePage',
data() {
return {
// 搜索相关
searchValue: '',
// 轮播图数据
swiperList: [
{ value: 'https://picsum.photos/750/360?random=1', text: '新品上市 限时优惠' },
{ value: 'https://picsum.photos/750/360?random=2', text: '品质生活 从这里开始' },
{ value: 'https://picsum.photos/750/360?random=3', text: '精选好物 等你来选' },
{ value: 'https://picsum.photos/750/360?random=4', text: '会员专享 更多优惠' }
],
// 通知栏数据
noticeText: '欢迎来到我们的App!新用户注册即送大礼包,限时活动进行中,快来参与吧!',
// 金刚区数据 - 8个功能入口(使用 wot-ui 支持的图标名称)
gridList: [
{ icon: 'gift', text: '购物车' },
{ icon: 'file', text: '我的订单' },
{ icon: 'tag', text: '优惠券' },
{ icon: 'location', text: '地址管理' },
{ icon: 'headset', text: '客服中心' },
{ icon: 'star', text: '我的收藏' },
{ icon: 'time-line', text: '浏览记录' },
{ icon: 'settings', text: '设置' }
],
// 文章列表数据
articleList: [
{
title: '如何挑选优质商品?这5个技巧你要知道',
desc: '在购物时,我们都希望能买到物美价廉的商品。本文将分享5个实用的挑选技巧...',
date: '2024-01-15',
cover: 'https://picsum.photos/120/120?random=10'
},
{
title: '春季养生指南:这些食物要多吃',
desc: '春天是万物复苏的季节,也是养生的好时机。让我们一起来看看春季应该多吃哪些食物...',
date: '2024-01-14',
cover: 'https://picsum.photos/120/120?random=11'
},
{
title: '家居收纳小妙招,让家更整洁',
desc: '整洁的家居环境能让人心情愉悦。今天分享几个实用的收纳技巧,帮助你打造整洁的家...',
date: '2024-01-13',
cover: 'https://picsum.photos/120/120?random=12'
},
{
title: '旅行必备清单,出行不再忘带东西',
desc: '每次出门旅行总是忘带东西?这份详细的旅行必备清单帮你解决烦恼...',
date: '2024-01-12',
cover: 'https://picsum.photos/120/120?random=13'
},
{
title: '数码产品选购指南:性价比之选',
desc: '面对琳琅满目的数码产品,如何选择性价比最高的?本文为你详细解析...',
date: '2024-01-11',
cover: 'https://picsum.photos/120/120?random=14'
}
],
// 加载状态
loadState: 'loading',
// 当前页码
pageNum: 1,
// 是否正在加载
isLoading: false
}
},
mounted() {
// 组件打开时执行
this.handleComponentOpen()
},
methods: {
// 组件打开时执行
handleComponentOpen() {
uni.showToast({
title: '组件【首页】打开了',
icon: 'none',
duration: 2000
})
console.log('组件【首页】打开了')
},
// 加载更多数据 - 由父页面通过 ref 调用
loadMoreData() {
this.isLoading = true
this.loadState = 'loading'
// 模拟加载第*页
uni.showToast({
title: `模拟加载第${this.pageNum}页`,
icon: 'none',
duration: 1500
})
console.log(`模拟加载第${this.pageNum}页`)
// 模拟异步加载
setTimeout(() => {
// 添加新数据
const newArticles = [
{
title: `上拉加载文章标题 ${this.pageNum}-1`,
desc: `这是第${this.pageNum}页加载的文章内容描述...`,
date: '2024-01-10',
cover: `https://picsum.photos/120/120?random=${20 + this.pageNum * 2}`
},
{
title: `上拉加载文章标题 ${this.pageNum}-2`,
desc: `这是第${this.pageNum}页加载的文章内容描述...`,
date: '2024-01-09',
cover: `https://picsum.photos/120/120?random=${21 + this.pageNum * 2}`
}
]
this.articleList.push(...newArticles)
this.pageNum++
this.isLoading = false
this.loadState = 'loading'
}, 1000)
},
// 搜索相关
handleSearch(value) {
console.log('搜索:', value)
uni.showToast({
title: `搜索: ${value}`,
icon: 'none'
})
},
handleSearchClick() {
console.log('点击搜索框')
},
// 轮播图点击
handleSwiperClick(index, item) {
console.log('点击轮播图:', index, item)
uni.showToast({
title: `查看: ${item.text}`,
icon: 'none'
})
},
// 通知栏点击
handleNoticeClick() {
console.log('点击通知栏')
uni.showToast({
title: '查看通知详情',
icon: 'none'
})
},
// 金刚区点击
handleGridClick(item, index) {
console.log('点击金刚区:', item, index)
uni.showToast({
title: item.text,
icon: 'none'
})
},
// 文章点击
handleArticleClick(article, index) {
console.log('点击文章:', article, index)
uni.showToast({
title: `查看: ${article.title.slice(0, 10)}...`,
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.home-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 20rpx;
}
.search-section {
background-color: #fff;
padding: 10rpx 20rpx;
}
.swiper-section {
margin: 20rpx;
border-radius: 16rpx;
overflow: hidden;
}
.notice-section {
margin: 0 20rpx 20rpx;
border-radius: 12rpx;
overflow: hidden;
}
.grid-section {
margin: 0 20rpx 20rpx;
padding: 20rpx;
background-color: #fff;
border-radius: 16rpx;
}
.article-section {
margin: 0 20rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.load-more-section {
margin: 20rpx;
}
</style>
源码下载
https://1815295193.share.123pan.cn/123pan/SmZtVv-5uegh
(源码案例使用uni-app的wot ui)