1.项目介绍
2.不同端的效果展示图
2.1无底部安全区域:
2.2有底部安全区域:
3. 代码编写
3.1 html 代码部分:
3.2 html 核心代码:
3.3 css 代码部分:
3.4 css 核心代码:
3.5 vue2 代码编写:
3.6 vue3 代码编写:
3.7 vue2/3 核心代码:
3.8 Tabbar 组件的使用:
4. vue2 完整代码
5. vue3 完整代码
项目描述:uniapp 使用 vue2/3,自定义封装 tabbar,底部凹凸透明显示。兼容h5、微信小程序、app等。html 和 css 部分代码一样,只有 js 功能代码,分别使用 vue2 和 vue3 的写法。
兼容性:h5、微信小程序、app端兼容,其他端没有测试过,应该也兼容的。
技术栈:uniapp + vue2 /3。
在h5端,底部是没有安全区域的,效果图如下:
在微信小程序端和 app端中,苹果手机底部可能会有安全区域,效果图如下
<template>
<view class="tabbar" :style="`--repeat: ${tabbar.length}`">
<view class="tabbar-nav">
<view class="tabbar-nav-a" v-for="(item, index) in tabbar" :key="index"
:class="pageIndex == index ? 'after' : ''" @tap="tapTabbar(item, index)">
<view class="after-icon" v-if="pageIndex == index">
<uni-icons :type="item.selectedIconPath" size="60rpx" color="#fff"></uni-icons>
</view>
<uni-icons :type="item.iconPath" size="48rpx" v-else></uni-icons>
<text class="text">{{ item.text }}</text>
</view>
</view>
<view class="safeZone" :style="{ 'height': safeAreaInsetsBottom }"></view>
</view>
<view :style="{ 'height': tabbarHeight }"></view>
</template>
:style="`--repeat: ${tabbar.length}`" :tabbar的数量,根据数组的长度改变。
:style="{ 'height': safeAreaInsetsBottom }" :底部安全区域的高度。
:style="{ 'height': tabbarHeight }":tabbar导航栏的高度,防止内容被遮挡。
<style lang="scss" scoped>
.tabbar {
--width: 100rpx;
--height: 100rpx;
--size: 60rpx;
--repeat: 4;
--color: #FC536E;
--radius: 20rpx;
--rgba: rgba(255, 255, 255, 0.95);
position: fixed;
width: 100%;
bottom: 0;
z-index: 999;
.tabbar-nav {
display: grid;
grid-template-columns: repeat(var(--repeat), 1fr);
.tabbar-nav-a {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding: 4rpx 0;
position: relative;
height: var(--height);
background-color: var(--rgba);
box-sizing: border-box;
filter: drop-shadow(2rpx -2rpx 2rpx rgba(0, 0, 0, 0.1));
font-size: 24rpx;
&:nth-child(1) {
border-top-left-radius: var(--radius);
}
&:nth-last-child(1) {
border-top-right-radius: var(--radius);
}
}
.after {
background: radial-gradient(circle at center top, transparent var(--size), var(--rgba) 0)center top;
.after-icon {
width: var(--width);
height: var(--height);
background-color: var(--color);
border-radius: 50%;
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
}
text {
color: var(--color);
}
}
}
.safeZone {
background-color: var(--rgba);
}
}
</style>
background: radial-gradient(circle at center top, transparent var(--size), var(--rgba) 0)center top;凹凸透明显示,最主要就是靠这行样式代码。
--repeat: 4;:控制 tabbar 的数量
<script>
export default {
name: "Tabbar",
props: {
pageIndex: {
type: Number,
default: 0,
}
},
data() {
return {
tabbarHeight: '',
safeAreaInsetsBottom: '',
tabbar: [{
iconPath: "heart",
selectedIconPath: "heart-filled",
text: "爱吧",
pagePath: "/pages/index/index"
},
{
iconPath: "wallet",
selectedIconPath: "wallet-filled",
text: "记账",
pagePath: "/pages/bookkeeping/bookkeeping"
},
{
iconPath: "chat",
selectedIconPath: "chat-filled",
text: "社区",
pagePath: "/pages/community/community"
},
{
iconPath: "person",
selectedIconPath: "person-filled",
text: "我",
pagePath: "/pages/my/my"
}]
}
},
created() {
uni.hideTabBar({ animation: true })
let res = uni.getWindowInfo().safeAreaInsets.bottom + 50
this.safeAreaInsetsBottom = res + "rpx";
this.tabbarHeight = res * 2 + "rpx"
},
methods: {
tapTabbar(val, index) {
uni.switchTab({
url: val.pagePath
})
}
}
}
</script>
<script setup>
defineOptions({
name: "Tabbar"
});
const tabbarHeight = ref('')
const safeAreaInsetsBottom = ref(0)
const props = defineProps({
pageIndex: {
type: Number,
default: 0
}
});
const tabbar = reactive([{
iconPath: "heart",
selectedIconPath: "heart-filled",
text: "爱吧",
pagePath: "/pages/index/index"
},
{
iconPath: "wallet",
selectedIconPath: "wallet-filled",
text: "记账",
pagePath: "/pages/bookkeeping/bookkeeping"
},
{
iconPath: "chat",
selectedIconPath: "chat-filled",
text: "社区",
pagePath: "/pages/community/community"
},
{
iconPath: "person",
selectedIconPath: "person-filled",
text: "我",
pagePath: "/pages/my/my"
}
])
onMounted(() => {
uni.hideTabBar({ animation: true })
let res = uni.getWindowInfo().safeAreaInsets.bottom
safeAreaInsetsBottom.value = res * 2 + "rpx";
tabbarHeight.value = res + 50 * 2 + "rpx";
});
const tapTabbar = (val, index) => {
uni.switchTab({
url: val.pagePath
})
}
</script>
uni.hideTabBar({ animation: true }) :隐藏默认的 tabBar 。
let res = uni.getWindowInfo().safeAreaInsets.bottom :获取底部安全区域的高度,返回px单位。
this.safeAreaInsetsBottom = res * 2 + "rpx":将高度 px单位 转为 rpx单位。
this.tabbarHeight = res + 50 * 2 + "rpx" :(底部安全区域高度 + 导航栏自身高度)转 rpx单位。
:pageIndex="0":用于控制激活状态,高亮显示。
<template>
<view class="tabbar" :style="`--repeat: ${tabbar.length}`">
<view class="tabbar-nav">
<view class="tabbar-nav-a" v-for="(item, index) in tabbar" :key="index"
:class="pageIndex == index ? 'after' : ''"
@tap="tapTabbar(item, index)">
<view class="after-icon" v-if="pageIndex == index">
<uni-icons size="60rpx" color="#fff"
:type="item.selectedIconPath" ></uni-icons>
</view>
<uni-icons :type="item.iconPath" size="48rpx" v-else></uni-icons>
<text class="text">{{ item.text }}</text>
</view>
</view>
<view class="safeZone" :style="{ 'height': safeAreaInsetsBottom }"></view>
</view>
<view :style="{ 'height': tabbarHeight }"></view>
</template>
<script>
export default {
name: "Tabbar",
props: {
pageIndex: {
type: Number,
default: 0,
}
},
data() {
return {
tabbarHeight: '',
safeAreaInsetsBottom: '',
tabbar: [{
iconPath: "heart",
selectedIconPath: "heart-filled",
text: "爱吧",
pagePath: "/pages/index/index"
},
{
iconPath: "wallet",
selectedIconPath: "wallet-filled",
text: "记账",
pagePath: "/pages/bookkeeping/bookkeeping"
},
{
iconPath: "chat",
selectedIconPath: "chat-filled",
text: "社区",
pagePath: "/pages/community/community"
},
{
iconPath: "person",
selectedIconPath: "person-filled",
text: "我",
pagePath: "/pages/my/my"
}]
}
},
created() {
uni.hideTabBar({ animation: true })
let res = uni.getWindowInfo().safeAreaInsets.bottom
this.safeAreaInsetsBottom = res * 2 + "rpx";
this.tabbarHeight = res + 50 * 2 + "rpx"
},
methods: {
tapTabbar(val, index) {
uni.switchTab({
url: val.pagePath
})
}
}
}
</script>
<style lang="scss" scoped>
.tabbar {
--width: 100rpx;
--height: 100rpx;
--size: 60rpx;
--repeat: 4;
--color: #FC536E;
--radius: 20rpx;
--rgba: rgba(255, 255, 255, 0.95);
position: fixed;
width: 100%;
bottom: 0;
z-index: 999;
.tabbar-nav {
display: grid;
grid-template-columns: repeat(var(--repeat), 1fr);
.tabbar-nav-a {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding: 4rpx 0;
position: relative;
height: var(--height);
background-color: var(--rgba);
box-sizing: border-box;
filter: drop-shadow(2rpx -2rpx 2rpx rgba(0, 0, 0, 0.1));
font-size: 24rpx;
&:nth-child(1) {
border-top-left-radius: var(--radius);
}
&:nth-last-child(1) {
border-top-right-radius: var(--radius);
}
}
.after {
background: radial-gradient(circle at center top,
transparent var(--size), var(--rgba) 0)center top;
.after-icon {
width: var(--width);
height: var(--height);
background-color: var(--color);
border-radius: 50%;
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
}
text {
color: var(--color);
}
}
}
.safeZone {
background-color: var(--rgba);
}
}
</style>
<template>
<view class="tabbar" :style="`--repeat: ${tabbar.length}`">
<view class="tabbar-nav">
<view class="tabbar-nav-a" v-for="(item, index) in tabbar" :key="index"
:class="pageIndex == index ? 'after' : ''"
@tap="tapTabbar(item, index)">
<view class="after-icon" v-if="pageIndex == index">
<uni-icons size="60rpx" color="#fff"
:type="item.selectedIconPath">
</uni-icons>
</view>
<uni-icons :type="item.iconPath" size="48rpx" v-else></uni-icons>
<text class="text">{{ item.text }}</text>
</view>
</view>
<view class="safeZone" :style="{ 'height': safeAreaInsetsBottom }"></view>
</view>
<view :style="{ 'height': tabbarHeight }"></view>
</template>
<script setup>
defineOptions({
name: "Tabbar"
});
const tabbarHeight = ref('')
const safeAreaInsetsBottom = ref(0)
const props = defineProps({
pageIndex: {
type: Number,
default: 0
}
});
const tabbar = reactive([{
iconPath: "heart",
selectedIconPath: "heart-filled",
text: "爱吧",
pagePath: "/pages/index/index"
},
{
iconPath: "wallet",
selectedIconPath: "wallet-filled",
text: "记账",
pagePath: "/pages/bookkeeping/bookkeeping"
},
{
iconPath: "chat",
selectedIconPath: "chat-filled",
text: "社区",
pagePath: "/pages/community/community"
},
{
iconPath: "person",
selectedIconPath: "person-filled",
text: "我",
pagePath: "/pages/my/my"
}
])
onMounted(() => {
uni.hideTabBar({ animation: true })
let res = uni.getWindowInfo().safeAreaInsets.bottom
safeAreaInsetsBottom.value = res * 2 + "rpx";
tabbarHeight.value = res + 50 * 2 + "rpx";
});
const tapTabbar = (val, index) => {
uni.switchTab({
url: val.pagePath
})
}
</script>
<style lang="scss" scoped>
.tabbar {
--width: 100rpx;
--height: 100rpx;
--size: 60rpx;
--repeat: 4;
--color: #FC536E;
--radius: 20rpx;
--rgba: rgba(255, 255, 255, 0.95);
position: fixed;
width: 100%;
bottom: 0;
z-index: 999;
.tabbar-nav {
display: grid;
grid-template-columns: repeat(var(--repeat), 1fr);
.tabbar-nav-a {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding: 4rpx 0;
position: relative;
height: var(--height);
background-color: var(--rgba);
box-sizing: border-box;
filter: drop-shadow(2rpx -2rpx 2rpx rgba(0, 0, 0, 0.1));
font-size: 24rpx;
&:nth-child(1) {
border-top-left-radius: var(--radius);
}
&:nth-last-child(1) {
border-top-right-radius: var(--radius);
}
}
.after {
background: radial-gradient(circle at center top,
transparent var(--size),
var(--rgba) 0)center top;
.after-icon {
width: var(--width);
height: var(--height);
background-color: var(--color);
border-radius: 50%;
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
}
text {
color: var(--color);
}
}
}
.safeZone {
background-color: var(--rgba);
}
}
</style>