uniapp vue3 ts自定义底部 tabbar菜单
组件源码
<script lang="ts" setup>
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import mIcon from '../m-icon/m-icon.vue';interface TabItem {text: string;iconPath: string;selectedIconPath: string;count?: number;isDot?: boolean;midButton?: boolean;pagePath?: string;customIcon?: boolean;
}const props = defineProps({show: { type: Boolean, default: true },modelValue: { type: [String, Number], default: 0 },bgColor: { type: String, default: '#ffffff' },height: { type: [String, Number], default: '50px' },iconSize: { type: [String, Number], default: 40 },midButtonSize: { type: [String, Number], default: 90 },activeColor: { type: String, default: '#303133' },inactiveColor: { type: String, default: '#606266' },midButton: { type: Boolean, default: false },list: { type: Array as () => TabItem[], default: () => [] },beforeSwitch: { type: Function as unknown as () => (index: number) => Promise<boolean> | boolean, default: null },borderTop: { type: Boolean, default: true },hideTabBar: { type: Boolean, default: true },
});const emit = defineEmits(['change', 'update:modelValue']);
const midButtonLeft = ref<string>('50%');
const pageUrl = ref<string>('');onMounted(() => {if (props.hideTabBar) uni.hideTabBar();const pages = getCurrentPages();pageUrl.value = pages[pages.length - 1]?.route || '';if (props.midButton) getMidButtonLeft();
});
const normalizePath = (path: string) => path.replace(/^\/+/, '').split('?')[0];const elIconPath = (index: number) => {const item = props.list[index];const pagePath = item.pagePath;if (pagePath) {const current = normalizePath(pageUrl.value);const target = normalizePath(pagePath);return current === target ? item.selectedIconPath : item.iconPath;} else {return index === props.modelValue ? item.selectedIconPath : item.iconPath;}
};const elColor = (index: number) => {const item = props.list[index];const pagePath = item.pagePath;if (pagePath) {return (pagePath === pageUrl.value || '/' + pagePath === pageUrl.value)? props.activeColor: props.inactiveColor;} else {return index === props.modelValue ? props.activeColor : props.inactiveColor;}
};const clickHandler = async (index: number) => {if (typeof props.beforeSwitch === 'function') {const result = props.beforeSwitch(index);if (result instanceof Promise) {try {const res = await result;if (res) switchTab(index);} catch {}} else if (result === true) {switchTab(index);}} else {switchTab(index);}
};const switchTab = (index: number) => {emit('change', index);const item = props.list[index];if (item.pagePath) {uni.switchTab({ url: item.pagePath });} else {emit('update:modelValue', index);}
};const getOffsetRight = (count?: number, isDot?: boolean) => {if (isDot) return -20;else if ((count || 0) > 9) return -40;else return -30;
};const getMidButtonLeft = () => {const res = uni.getSystemInfoSync();midButtonLeft.value = res.windowWidth / 2 + 'px';
};
</script><template><view v-show="show" class="u-tabbar" @touchmove.stop.prevent><viewclass="u-tabbar__content safe-area-inset-bottom":style="{height: height + (typeof height === 'number' ? 'rpx' : ''),backgroundColor: bgColor,}":class="{ 'u-border-top': borderTop }"><viewv-for="(item, index) in list":key="index"class="u-tabbar__content__item":class="{ 'u-tabbar__content__circle': midButton && item.midButton }"@tap.stop="() => clickHandler(index)":style="{ backgroundColor: bgColor }"><view:class="[midButton && item.midButton? 'u-tabbar__content__circle__button': 'u-tabbar__content__item__button']"><!-- {{ elIconPath(index) }} --><m-images mode="aspectFit" :url="elIconPath(index)" width="48rpx" height="48rpx"></m-images><!-- <m-icon:size="midButton && item.midButton ? midButtonSize : iconSize":name="elIconPath(index)"img-mode="scaleToFill":color="elColor(index)":custom-prefix="item.customIcon ? 'custom-icon' : 'uicon'"/> --><!-- <u-badgev-show="item.count || item.isDot":count="item.count":is-dot="item.isDot":offset="[-2, getOffsetRight(item.count, item.isDot)]"/> --></view><viewclass="u-tabbar__content__item__text":style="{ color: elColor(index) }"><text class="u-line-1">{{ item.text }}</text></view></view><viewv-show="midButton"class="u-tabbar__content__circle__border":class="{ 'u-border': borderTop }":style="{ backgroundColor: bgColor, left: midButtonLeft }"/></view><viewclass="u-fixed-placeholder safe-area-inset-bottom":style="{ height: `calc(${height}${typeof height === 'number' ? 'rpx' : ''} + ${midButton ? 48 : 0}rpx)` }"/></view>
</template><style scoped lang="scss">
//@import "../../libs/css/style.components.scss";
@mixin vue-flex($direction: row) {/* #ifndef APP-NVUE */display: flex;flex-direction: $direction;/* #endif */
}
.u-fixed-placeholder {box-sizing: content-box;
}
.u-tabbar {&__content {padding-bottom: env(safe-area-inset-bottom);height: calc(50px + env(safe-area-inset-bottom));@include vue-flex;align-items: center;position: fixed;bottom: 0;left: 0;width: 100%;z-index: 998;box-sizing: content-box;&__circle__border {border-radius: 100%;width: 110rpx;height: 110rpx;top: -48rpx;position: absolute;z-index: 4;background-color: #ffffff;left: 50%;transform: translateX(-50%);&:after {border-radius: 100px;}}&__item {flex: 1;justify-content: center;height: 100%;padding: 12rpx 0;@include vue-flex;flex-direction: column;align-items: center;position: relative;&__button {position: absolute;top: 14rpx;left: 50%;transform: translateX(-50%);}&__text {color: #ff7300;font-size: 26rpx;line-height: 28rpx;position: absolute;bottom: 14rpx;left: 50%;transform: translateX(-50%);width: 100%;text-align: center;}}&__circle {position: relative;@include vue-flex;flex-direction: column;justify-content: space-between;z-index: 10;height: calc(100% - 1px);&__button {width: 90rpx;height: 90rpx;border-radius: 100%;@include vue-flex;justify-content: center;align-items: center;position: absolute;background-color: #ffffff;top: -40rpx;left: 50%;z-index: 6;transform: translateX(-50%);}}}
}
</style>
说明
let list = [{// 非凸起按钮未激活的图标,可以是uView内置图标名或自定义扩展图标库的图标// 或者png图标的【绝对路径】,建议尺寸为80px * 80px// 如果是中间凸起的按钮,只能使用图片,且建议为120px * 120px的png图片iconPath: "home",// 激活(选中)的图标,同上selectedIconPath: "home-fill",// 显示的提示文字text: "首页",// 红色角标显示的数字,如果需要移除角标,配置此参数为0即可count: 2,// 如果配置此值为true,那么角标将会以红点的形式显示isDot: true,// 如果使用自定义扩展的图标库字体,需配置此值为true// 自定义字体图标库教程customIcon: false,// 如果是凸起按钮项,需配置此值为truemidButton: false,// 点击某一个item时,跳转的路径,此路径必须是pagees.json中tabBar字段中定义的路径pagePath: "", 路径需要以"/"开头},
];
代码
<view><view class="u-page"><!-- 所有内容的容器 --></view><!-- 与包裹页面所有内容的元素u-page同级,且在它的下方 --><m-tabbar v-model="current" :list="list" :mid-button="true"></m-tabbar></view>
</template><script>export default {data() {return {list: [{iconPath: "home",selectedIconPath: "home-fill",text: "首页",count: 2,isDot: true,customIcon: false,},{iconPath: "photo",selectedIconPath: "photo-fill",text: "放映厅",customIcon: false,},{iconPath: xxxx",selectedIconPath: "xxx",text: "发布",midButton: true,customIcon: false,},{iconPath: "play-right",selectedIconPath: "play-right-fill",text: "直播",customIcon: false,},{iconPath: "account",selectedIconPath: "account-fill",text: "我的",count: 23,isDot: false,customIcon: false,},],current: 0,};},};
</script>
- height配置导航栏高度,建议使用默认值即可,默认为50px,与 uni-app 自带系统导航栏高度一致
- bg-color组件的背景颜色
- active-color与inactive-color配置提示文字和图标的颜色(如果是字体图标的话),可以搭配bg-color达到自定义导航栏主题的效果
自定义 tabbar 场景,我们不建议在一个页面内通过几个组件,用v-if切换去模拟各个页面,而应该使用 uni-app 自带的 tabbar 系统,同时隐藏原生的 tabbar, 再引入自定导航栏,这样可以保证原有性能,同时又能自定义 tabbar,思路如下:在 pages.json 中正常定义 tabbar 逻辑和字段,只需配置tabBar字段list中的pagePath(需以"/"开头)属性即可
在各个 tabbar 页面引入u-tabbar组件,组件会默认自动通过uni.hideTabBar()隐藏系统 tabbar
通过vuex引用同一份 tabbar 组件的list参数,这样可以做到修改某一个页面的u-tabbar数据,其他页面的u-tabbar也能同步更新
组件内部会自动处理各种跳转的逻辑,同时需要注意以下两点:
要在list参数中配置pagePath路径,此路径为pages.json中定义的 tabbar 字段的路径
此种方式,无需通过v-model绑定活动项,内部会自动进行判断和跳转
底部菜单配置
// 底部菜单
// import { useImageAssets } from '@/utils/useImageAssets';
// const images=useImageAssets()
// "/static/tabbar/home1.png"
const listTabbar =[{iconPath:"/static/images/tabbar/home.png",selectedIconPath: "/static/images/tabbar/home1.png",text: "首页",pagePath:"/pages/index/index",customIcon: false,},{iconPath: "/static/images/tabbar/class.png",selectedIconPath: "/static/images/tabbar/class1.png",text: "分类",customIcon: false,pagePath:"/pages/classList/index",},{iconPath: "/static/images/tabbar/shapping.png",selectedIconPath: "/static/images/tabbar/shapping1.png",text: "购物车",midButton: true,pagePath:"/pages/shapping/index",customIcon: false,},{iconPath: "/static/images/tabbar/fujin.png",selectedIconPath: "/static/images/tabbar/fujin1.png",text: "附近",customIcon: false,pagePath:"/pages/near/index",},{iconPath: "/static/images/tabbar/my.png",selectedIconPath: "/static/images/tabbar/my1.png",text: "我的",pagePath:"/pages/my/my",customIcon: false,},]export {listTabbar
}
注意
- 小程序 “custom”: true,添加这个
uniap pages.json添加配置
--
"tabBar": {"color": "#666666","selectedColor": "#40AE36","custom": true,"borderStyle": "white","backgroundColor": "#ffffff","list": [{"iconPath": "static/images/tabbar/home.png","selectedIconPath": "static/images/tabbar/home1.png","pagePath": "pages/index/index","text": "首页"},{"iconPath": "static/images/tabbar/class.png","selectedIconPath": "static/images/tabbar/class1.png","pagePath": "pages/classList/index","text": "分类"},{"iconPath": "static/images/tabbar/shapping.png","selectedIconPath": "static/images/tabbar/shapping1.png","pagePath": "pages/shapping/index","text": "购物车"},{"iconPath": "static/images/tabbar/fujin.png","selectedIconPath": "static/images/tabbar/fujin1.png","pagePath": "pages/near/index","text": "附近"},{"iconPath": "static/images/tabbar/my.png","selectedIconPath": "static/images/tabbar/my1.png","pagePath": "pages/my/my","text": "我的"}]}
app.vue中隐藏
这样自定义tabbar就可以使用uni.switchTab跳转了
onLaunch(async () => {uni.hideTabBar()});
使用
index.vue
<m-tabbar v-model="current" :list="listTabbar" :show="true" :hideTabBar="true"></m-tabbar>listTabbar 菜单配置
hideTabBar 是否隐藏
current 当前的菜单示例代码
<template><view><text>哈哈哈</text><m-tabbar v-model="current" :list="listTabbar" :show="true" :hideTabBar="true"></m-tabbar></view></template>
<script lang="ts" setup>
import {listTabbar} from "@/config/tabbarConfig"
import { ref } from "vue";
const current = ref(1)
</script>