评论
踩坑上N次,终于搞定 uni-app 自定义 TabBar 闪屏

踩坑上N次,终于搞定 uni-app 自定义 TabBar 闪屏

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

很多时候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)