uniapp + vue3 自定义TabBar

162 阅读2分钟

20250307_154931.gif

图标使用阿里矢量图标

    <template>
    <view class="content-box">
        <view class="tab-card">
            <view :class="['item', current == index ? 'active' : '']" v-for="(item, index) in tabbar.list" @click="tabbarChange(item.pagePath, index)">
                <view :style="{ '--icon-color': tabbar.iconColor, '--selected-icon-color': tabbar.selectedIconColor }" class="icon iconfont" :class="index == current ? item.selectedIconPath : item.iconPath"></view>
                <view class="tab-text">{{ item.text }}</view>
            </view>
            <view class="slidingBlock" :style="slidingBlockStyle"> </view>
        </view>
    </view>
</template>
<script setup lang="ts">
import { ref, reactive, inject, onMounted, getCurrentInstance, defineProps, defineEmits } from 'vue';
import { getdeviceInfo } from '@/utils/deviceInfo';
const imgUrl = inject('imgUrl');
const { globalData } = reactive(getdeviceInfo());
const emit = defineEmits(['upDataTab']);
const current: any = ref(0);
const slidingBlockStyle: any = ref({});
const instance: any = getCurrentInstance(); // 获取组件实例
const tabbar: any = reactive({
    color: '#ccc', // 未选中字体颜色
    selectedColor: '#4468ff', // 选择字体颜色
    iconColor: '#fff', // 图标默认颜色
    selectedIconColor: '#1c1c1c', // 选中图标颜色
    backgroundColor: '#fff', // 背景颜色
    selectedBackground: '#fff',
    list: [
        {
            text: '首页', // 底部文字
            iconPath: 'icon-home', // 未选择icon图标
            selectedIconPath: 'icon-home', // 选中icon图标
            pagePath: '/pages/tabBar/index', //
        },
        {
            text: '客户',
            iconPath: 'icon-client',
            selectedIconPath: 'icon-client',
            pagePath: '/pages/tabBar/classify',
        },
        {
            text: '我的',
            iconPath: 'icon-my',
            selectedIconPath: 'icon-my',
            pagePath: '/pages/tabBar/my',
        },
    ],
});
onMounted(() => {
    getClientRect(0);
});

function tabbarChange(path: any, ind: any) {
    console.log(path, ind);
    if (ind == current.value) return;
    current.value = ind;
    // this.$emit('change', path);
    emit('upDataTab', path);
    getClientRect(ind);
}
function getClientRect(ind: any) {
    uni.createSelectorQuery()
        .in(instance.proxy)
        // 获取所有标签的位置信息
        .selectAll('.item')
        .boundingClientRect((rects: any) => {
            if (!rects || rects.length === 0) return; // 安全判断

            // 获取当前选中的标签位置
            const targetRect = rects[ind];
            if (!targetRect) return;

            // 获取父容器(tabs-container)的位置信息
            const containerRect = rects[0].top !== undefined ? rects[0] : { left: 0 }; // 如果无法获取到父容器的具体位置,则默认为0

            // 计算目标标签相对于父容器左侧的距离占整个视口宽度的比例
            const viewportWidth = getdeviceInfo().windowWidth;
            const relativeLeft = ((targetRect.left - containerRect.left) / viewportWidth) * 100;

            slidingBlockStyle.value = {
                width: `${targetRect.width}px`,
                height: `${targetRect.height}px`,
                transform: `translateX(calc(${relativeLeft}vw + 13rpx))`, // 使用vw单位表示相对视口宽度的百分比
            };
        })
        // 确保执行查询
        .exec();
}
defineExpose({
    tabbarChange,
    getClientRect,
});
</script>
<style lang="scss" scoped>
page {
    background-color: #f1f1f1;
    font-size: 32rpx;
}
.tab-card {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    width: 400rpx;
    // background-color: #1c1c1c;
    border-radius: 100rpx;
    display: flex;
    justify-content: space-between;
    align-items: center;
    // padding: 16rpx;
    padding: 8rpx 13rpx;
    margin: auto;
    // box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
    .item {
        // transform: all 1s;
        display: flex;
        align-items: center;
        border-radius: 100rpx;
        padding: 10rpx 32rpx;
        height: 100%;
        align-items: center;
        // background: rebeccapurple;

        > view {
            color: #000;
        }

        .icon {
            // color: var(--icon-color);
            color: #000;
            font-size: 13rpx;
        }
    }

    .active .icon {
        // color: var(--selected-icon-color);
        color: #fff;
        // transition: color 0s ease 0.5s;
        transition: all 0.5s;
    }
    .tab-text {
        margin-left: 5rpx;
        font-size: 10rpx;
    }
    .active .tab-text {
        color: #fff;
        transition: all 0.5s;
    }

    .slidingBlock {
        // background-color: #fff;
        background: linear-gradient(90deg, #ff6d3d 25%, #ff413f 75%);
        border-radius: 100rpx;
        position: absolute;
        left: 0;
        transition: all 0.5s;
        z-index: -1;
    }
}
</style>

使用方式

<template>
    <view>
        <view class="container">
            <home v-if="current == '/pages/tabBar/index'" />
            <classify v-if="current == '/pages/tabBar/classify'" />
            <user v-if="current == '/pages/tabBar/my'" />
        </view>
        <!-- 组件 -->
        <my-tab-bar ref="mytabbar" @upDataTab="upDataTab"></my-tab-bar>
    </view>
</template>
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app';
import { ref, inject, reactive } from 'vue';
import home from '@/pages/tabBar/index.vue';
import classify from '@/pages/tabBar/classify.vue';
import user from '@/pages/tabBar/my.vue';
import myTabBar from '@/components/myTabBar.vue';
const imgUrl = inject('imgUrl');
const mytabbar: any = ref();
const current: any = ref('/pages/tabBar/index');
function upDataTab(params: any) {
    current.value = params;
}
</script>
<style lang="scss" scoped>
.container {
    width: 100%;
    height: 100vh;
    background: linear-gradient(90deg, #f9cdbd, #fde7d9);
}
</style>