多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

uni-app跨端开发:从零实现自定义凸起TabBar的完整实战指南

uni-app跨端开发:从零实现自定义凸起TabBar的完整实战指南 1. 从“平”到“凸”为什么我们需要一个凸起的TabBar在移动端应用开发中底部导航栏TabBar是用户交互的核心枢纽。无论是微信、支付宝还是抖音、淘宝它们都采用了这种经典的导航模式。然而随着应用设计的演进一个“平平无奇”的TabBar已经很难满足产品对视觉冲击力和核心功能引导的需求。于是“凸起式TabBar”应运而生。这种设计通常也被称为“悬浮按钮式导航”或“凸起标签栏”其核心特征是将中间的某个导航项通常是核心功能入口如发布、拍照、首页设计得比其他项更大、更高并悬浮于底部导航栏之上。它不仅仅是一个视觉上的“凸起”更是一种精妙的产品策略。从用户体验角度看它通过视觉上的“突出”和物理空间上的“前置”极大地提升了核心功能的可发现性和操作便捷性。用户的手指可以更自然、更快速地触及这个被强调的按钮从而引导用户完成应用最希望他们进行的操作——发布内容、开始创作或进入核心主页。对于使用 uni-app 进行跨端开发的开发者而言实现这样一个效果意味着需要跨越不同平台iOS、Android、小程序的UI渲染差异和交互规范。原生开发中iOS和Android都有成熟的组件库或自定义View方案来实现凸起TabBar但在uni-app的跨端语境下我们无法直接使用这些原生组件。因此我们的挑战在于如何利用uni-app提供的Vue.js语法和跨端API构建一个在H5、各端小程序以及App端都能表现一致且性能良好的凸起TabBar这不仅是CSS样式的堆砌更涉及到布局计算、事件穿透、平台适配等一系列工程化问题。接下来我将以一个完整的实战项目为例手把手带你从零构建一个功能完善、体验流畅的凸起TabBar。我们会深入每一个技术细节解释“为什么这么做”并分享我在多个项目中踩过的坑和总结出的最佳实践。2. 技术方案选型为何放弃uni.setTabBarItem选择完全自定义在uni-app中官方提供了uni.setTabBarItemAPI可以动态设置某个Tab项的内容包括图标、文字等。乍一看似乎可以通过设置一个更大的图标来模拟“凸起”效果。但经过实践这条路基本走不通原因如下2.1 官方TabBar的局限性分析首先uni.setTabBarItem无法修改Tab项的背景、边框、阴影等样式更无法改变其布局位置和层级。它本质上是对原生TabBar组件的有限配置。其次各平台特别是小程序对原生TabBar的样式限制非常严格自定义能力极其有限。例如微信小程序的TabBar高度、图标尺寸都有明确规范超出部分会被裁剪。最后凸起效果往往需要按钮“悬浮”在TabBar之上并可能遮挡一部分页面内容这涉及到复杂的z-index层级管理和页面布局调整原生TabBar根本无法实现。2.2 完全自定义视图Custom TabBar的优势因此业内通用的、也是唯一可行的方案是完全隐藏官方的TabBar在每一个页面的底部自己用view组件绘制一个自定义的导航栏。这样我们就获得了完全的UI控制权样式自由可以任意设置背景、圆角、阴影、凸起形状。布局自由凸起按钮可以任意定位甚至可以超出导航栏容器。交互自由可以自定义点击效果、添加动画、实现更复杂的交互逻辑。跨端一致由于是我们自己用View绘制的在所有平台上的表现基本一致避免了原生差异。2.3 实现路径规划我们的实现将分为几个核心步骤页面结构搭建创建包含页面内容和底部自定义TabBar的页面骨架。TabBar组件化将TabBar抽离为独立的Vue组件实现高内聚、低耦合。凸起按钮实现核心难点解决定位、层级、事件和样式问题。路由与状态管理实现点击Tab切换页面并高亮当前选中项。多端适配与优化处理不同平台的安全区域如iPhone的“刘海”和底部Home条、性能优化等。注意隐藏官方TabBar需要在pages.json中为每个需要自定义TabBar的页面进行配置这是一个容易遗漏的步骤。3. 实战第一步搭建基础页面结构与隐藏原生TabBar让我们从最基础的工程配置开始。假设我们的应用有四个页面首页、分类、发布凸起按钮、消息、我的。3.1 配置 pages.json首先我们需要在pages.json中定义页面路由并关闭所有页面的原生导航栏和TabBar为我们的自定义视图腾出空间。// pages.json { pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页, navigationStyle: custom, // 隐藏原生导航栏如果需要自定义导航栏也需此项 enablePullDownRefresh: false, app-plus: { titleNView: false // App端隐藏原生导航栏 } } }, { path: pages/category/category, style: { navigationBarTitleText: 分类, navigationStyle: custom, app-plus: { titleNView: false } } }, // “发布”页面可能是一个独立的页面点击凸起按钮后跳转过去 { path: pages/publish/publish, style: { navigationBarTitleText: 发布, navigationStyle: custom, app-plus: { titleNView: false } } }, { path: pages/message/message, style: { navigationBarTitleText: 消息, navigationStyle: custom, app-plus: { titleNView: false } } }, { path: pages/profile/profile, style: { navigationBarTitleText: 我的, navigationStyle: custom, app-plus: { titleNView: false } } } ], globalStyle: { navigationBarTextStyle: black, navigationBarTitleText: UniApp, navigationBarBackgroundColor: #F8F8F8, backgroundColor: #F8F8F8, // 关键全局隐藏原生TabBar tabBar: { borderStyle: black, backgroundColor: #ffffff, color: #8a8a8a, selectedColor: #007AFF, list: [] // 一个空数组表示不显示原生TabBar } } }3.2 创建基础页面模板每个页面的.vue文件结构需要保持一致底部为自定义TabBar预留位置。我们创建一个tabbar组件并在每个页面中引入。以pages/index/index.vue为例!-- pages/index/index.vue -- template view classpage-container !-- 页面主要内容区域需要为底部的TabBar预留padding-bottom -- scroll-view classcontent scroll-y :style{ paddingBottom: tabBarHeight px } !-- 你的页面内容在这里 -- text这里是首页内容/text /scroll-view !-- 引入自定义TabBar组件 -- custom-tabbar :currentcurrentTab tab-changeonTabChange / /view /template script import CustomTabbar from /components/custom-tabbar/custom-tabbar.vue export default { components: { CustomTabbar }, data() { return { currentTab: 0, // 当前选中的Tab索引首页对应0 tabBarHeight: 60 // 这是一个预估高度实际高度应由组件提供或通过计算获取 } }, methods: { onTabChange(index) { if (this.currentTab index) return; // 点击当前选中项不做处理 const pages [/pages/index/index, /pages/category/category, /pages/message/message, /pages/profile/profile]; // 凸起按钮索引2通常对应一个独立页面如发布页 if (index 2) { uni.navigateTo({ url: /pages/publish/publish }); // 注意跳转到发布页后当前页面的TabBar高亮状态可能需特殊处理 return; } // 其他常规Tab使用switchTab进行切换注意switchTab只能跳转到pages.json中tabBar.list配置的页面我们已隐藏原生TabBar所以这里用reLaunch或redirectTo更合适但为了保持页面栈清晰通常用redirectTo uni.redirectTo({ url: pages[index] }); // 在实际项目中更推荐使用Vuex或Pinia管理全局状态来同步更新所有页面的currentTab } } } /script style scoped .page-container { width: 100vw; height: 100vh; display: flex; flex-direction: column; } .content { flex: 1; width: 100%; box-sizing: border-box; } /style这里有几个关键点scroll-view的padding-bottom必须设置且值要等于自定义TabBar的高度否则内容会被TabBar遮挡。页面跳转逻辑常规Tab使用uni.redirectTo替换当前页面避免页面栈过深。凸起按钮对应的“发布”功能通常使用uni.navigateTo打开一个新页面。状态管理上述例子中currentTab由各自页面管理这会导致状态不同步。例如从“首页”切换到“分类”页后“首页”组件内的currentTab还是0。因此在生产环境中强烈建议使用Vuex或Pinia来管理当前激活的Tab索引确保所有页面引用的TabBar组件状态一致。4. 核心实现构建CustomTabbar组件与凸起按钮现在我们来创建最核心的custom-tabbar组件。4.1 组件结构与样式首先在/components/custom-tabbar/目录下创建组件。!-- components/custom-tabbar/custom-tabbar.vue -- template view classcustom-tabbar :styletabBarStyle !-- 左侧第一个Tab -- view classtab-item :class{ active: current 0 } tapswitchTab(0) view classicon-wrapper image classicon :srccurrent 0 ? /static/tabbar/home_active.png : /static/tabbar/home.png modeaspectFit / /view text classtext :class{ active-text: current 0 }首页/text /view !-- 左侧第二个Tab -- view classtab-item :class{ active: current 1 } tapswitchTab(1) view classicon-wrapper image classicon :srccurrent 1 ? /static/tabbar/category_active.png : /static/tabbar/category.png modeaspectFit / /view text classtext :class{ active-text: current 1 }分类/text /view !-- 中间的凸起按钮 -- view classtab-center-placeholder !-- 这个占位view用于平衡布局宽度与凸起按钮相同 -- /view view class凸起按钮 tapswitchTab(2) view class凸起按钮-icon-wrapper !-- 这里可以使用图标字体或者一个更大的图片 -- text class凸起按钮-icon/text !-- 或者 image class凸起按钮-icon-img src/static/tabbar/publish.png modeaspectFit / -- /view !-- 凸起按钮通常不需要文字 -- /view !-- 右侧两个Tab -- view classtab-item :class{ active: current 3 } tapswitchTab(3) view classicon-wrapper image classicon :srccurrent 3 ? /static/tabbar/msg_active.png : /static/tabbar/msg.png modeaspectFit / /view text classtext :class{ active-text: current 3 }消息/text /view view classtab-item :class{ active: current 4 } tapswitchTab(4) view classicon-wrapper image classicon :srccurrent 4 ? /static/tabbar/profile_active.png : /static/tabbar/profile.png modeaspectFit / /view text classtext :class{ active-text: current 4 }我的/text /view /view /template script export default { name: CustomTabbar, props: { current: { type: Number, default: 0 } }, computed: { // 动态计算TabBar样式主要用于适配iPhone等设备的底部安全区域 tabBarStyle() { let style {}; // 获取系统信息判断是否为iOS const systemInfo uni.getSystemInfoSync(); const isIOS systemInfo.platform ios; // 计算底部安全区域高度 const safeAreaInsets systemInfo.safeAreaInsets; const bottomSafeHeight safeAreaInsets ? safeAreaInsets.bottom : 0; // 基础高度 iOS安全区域高度 const totalHeight 60 (isIOS ? bottomSafeHeight : 0); style.height ${totalHeight}px; style.paddingBottom isIOS ? ${bottomSafeHeight}px : 0px; return style; } }, methods: { switchTab(index) { // 触发父组件的事件 this.$emit(tab-change, index); } } } /script style scoped .custom-tabbar { position: fixed; bottom: 0; left: 0; width: 100%; background-color: #ffffff; display: flex; align-items: center; justify-content: space-around; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); z-index: 9999; /* 确保在最上层 */ box-sizing: border-box; } .tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 60px; /* 与TabBar基础高度一致 */ transition: all 0.2s ease; } .tab-item.active .icon { transform: scale(1.1); } .tab-item.active .text { font-weight: bold; } .icon-wrapper { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-bottom: 4px; } .icon { width: 22px; height: 22px; transition: transform 0.2s ease; } .text { font-size: 10px; color: #666; transition: color 0.2s ease; } .active-text { color: #007AFF; /* 激活色 */ } /* 凸起按钮相关样式 */ .tab-center-placeholder { flex: 1; /* 占位使左右两侧的TabItem均匀分布 */ visibility: hidden; /* 不可见但占位 */ } .凸起按钮 { position: absolute; bottom: 20px; /* 凸起的高度可以调整 */ left: 50%; transform: translateX(-50%); width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #007AFF, #00C6FF); /* 渐变背景 */ display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(0, 122, 255, 0.3); /* 阴影增强立体感 */ z-index: 10000; /* 比TabBar更高 */ } .凸起按钮-icon-wrapper { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } .凸起按钮-icon { color: #ffffff; font-size: 28px; font-weight: 300; } /* 如果使用图片 */ .凸起按钮-icon-img { width: 28px; height: 28px; } /* 点击效果 */ .凸起按钮:active { transform: translateX(-50%) scale(0.95); box-shadow: 0 2px 8px rgba(0, 122, 255, 0.4); } /style4.2 布局原理深度解析这段代码是实现凸起效果的核心其布局思想非常巧妙Flex布局与占位符.custom-tabbar采用display: flex; justify-content: space-around;使得其直接子元素5个在水平方向上均匀分布。我们有4个常规Tab和1个凸起按钮但凸起按钮需要绝对定位脱离文档流。如果直接只有4个tab-item它们会平均分宽度。为了让左右各两个tab-item的位置对称我们引入了第五个元素——.tab-center-placeholder。它是一个占位view拥有flex: 1视觉上隐藏但占据了一个Flex项的空间。这样左右两侧各两个tab-item加上中间一个占位符正好5个元素实现了完美的视觉平衡。绝对定位的凸起按钮.凸起按钮使用position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);实现居中且凸起。bottom: 20px决定了它“凸起”的高度。left: 50%配合transform: translateX(-50%)是水平居中的经典写法。它的z-index要高于.custom-tabbar确保悬浮在上层。安全区域适配在tabBarStyle计算属性中我们通过uni.getSystemInfoSync()获取设备信息特别是safeAreaInsets。对于有底部Home条的iPhone我们需要在TabBar的底部增加内边距paddingBottom并将总高度增加以防止内容被Home条遮挡。这是实现沉浸式体验的关键一步很多初学者会忽略导致在iPhone上布局错乱。实操心得bottom: 20px这个值需要根据UI设计稿和实际视觉效果微调。太大会导致按钮过于突兀且容易误触上方内容太小则凸起感不足。建议在真机上多测试几种尺寸。5. 状态同步与路由跳转的工程化处理在基础版本中每个页面管理自己的current状态并通过事件通知TabBar组件这会导致状态不一致。我们来引入Vuex进行全局状态管理。5.1 创建Vuex Store// store/index.js import Vue from vue import Vuex from vuex Vue.use(Vuex) const store new Vuex.Store({ state: { tabBarCurrent: 0 // 当前选中的Tab索引 }, mutations: { UPDATE_TABBAR_CURRENT(state, index) { state.tabBarCurrent index } }, actions: { setTabBarCurrent({ commit }, index) { commit(UPDATE_TABBAR_CURRENT, index) } } }) export default store在main.js中引入store。5.2 改造CustomTabbar组件组件通过mapState获取全局状态点击时提交action。!-- components/custom-tabbar/custom-tabbar.vue (部分修改) -- script import { mapState, mapActions } from vuex export default { name: CustomTabbar, computed: { ...mapState([tabBarCurrent]), // ... 原有的tabBarStyle计算属性 }, methods: { ...mapActions([setTabBarCurrent]), switchTab(index) { if (this.tabBarCurrent index) return; this.setTabBarCurrent(index); // 更新全局状态 this.$emit(tab-change, index); // 仍可触发事件供页面处理特殊逻辑如发布页跳转 } } } /script template !-- 模板中使用 tabBarCurrent 替代 current prop -- view classcustom-tabbar :styletabBarStyle view classtab-item :class{ active: tabBarCurrent 0 } tapswitchTab(0) !-- ... -- /view !-- ... 其他tab-item同理 ... -- /view /template5.3 改造页面逻辑页面不再需要维护自己的current状态直接从store获取并在onShow生命周期中同步状态因为使用redirectTo跳转后原页面卸载新页面加载需要根据路由设置正确的激活状态。!-- pages/index/index.vue (部分修改) -- script import { mapState, mapActions } from vuex export default { computed: { ...mapState([tabBarCurrent]) }, onShow() { // 当页面显示时如果全局状态与当前页面不符则更新。 // 例如从“我的”页面切回“首页”需要确保TabBar高亮首页。 // 更常见的做法是在路由跳转时在switchTab方法或全局路由守卫中就更新好状态。 // 这里我们假设在switchTab的action中已经更新所以可能不需要此步骤。 // 但对于浏览器刷新或App冷启动可能需要根据初始路由路径来设置状态。 }, methods: { onTabChange(index) { if (this.tabBarCurrent index) return; const pages [/pages/index/index, /pages/category/category, /pages/message/message, /pages/profile/profile]; if (index 2) { uni.navigateTo({ url: /pages/publish/publish }); // 跳转到发布页通常不希望改变底部TabBar的高亮状态仍保持之前选中的Tab。 // 因此这里不调用 setTabBarCurrent。 return; } // 更新全局状态 this.setTabBarCurrent(index); // 执行页面跳转 uni.redirectTo({ url: pages[index] }); } } } /script5.4 处理凸起按钮跳转后的状态这是一个常见的产品细节。点击凸起按钮跳转到发布页后底部的TabBar应该高亮哪个项通常有两种方案不高亮任何项发布页是一个临时模态或独立页面不属于主Tab流。此时在发布页隐藏自定义TabBar或者让TabBar所有项都显示为未激活状态。高亮上一个活动Tab保持用户进入发布页前所在的Tab项为高亮状态。方案1更清晰。我们可以在发布页不引入CustomTabbar组件或者通过一个全局状态控制TabBar的显示隐藏。方案2需要记录上一次的Tab状态。我们采用方案1修改发布页和路由逻辑!-- pages/publish/publish.vue -- template view classpublish-container !-- 发布页内容 -- view发布页面/view !-- 没有引入CustomTabbar -- /view /template script export default { onLoad() { // 可以在这里设置一个全局状态让主页的TabBar隐藏如果需要的话 // 或者不做处理因为发布页根本没有TabBar。 }, onUnload() { // 页面卸载时如果需要恢复TabBar可以在这里操作 } } /script同时修改主页面onTabChange方法中关于发布页的跳转不再阻止状态更新或者跳转前记录当前状态// 在Vuex state中增加一个字段记录跳转发布页前的Tab state: { tabBarCurrent: 0, lastTabBeforePublish: 0 }, mutations: { UPDATE_LAST_TAB(state, index) { state.lastTabBeforePublish index; } } // 在点击凸起按钮时 if (index 2) { // 记录当前Tab this.$store.commit(UPDATE_LAST_TAB, this.tabBarCurrent); uni.navigateTo({ url: /pages/publish/publish }); return; }这样从发布页返回时可以根据lastTabBeforePublish恢复高亮状态。6. 高级优化与多端踩坑实录实现基本功能后我们还需要处理一些细节和平台差异问题这些才是体现工程能力的地方。6.1 解决滚动穿透与点击穿透凸起按钮是绝对定位覆盖在页面内容之上。在iOS的WebView即App端和H5端中如果页面可滚动可能会发生“滚动穿透”当你手指在凸起按钮上开始滑动然后移动到页面上时页面会意外滚动。此外凸起按钮下方的页面元素可能无法被点击点击穿透问题处理不当则相反。解决方案阻止凸起按钮的默认触摸行为给.凸起按钮添加CSS属性touch-action: none;和pointer-events: auto;(默认就是auto)。在uni-app的view上可以尝试使用touchstart和touchend事件并在touchstart中调用event.preventDefault()来阻止默认滚动但需谨慎使用可能影响按钮自身的点击反馈。更稳健的方案确保凸起按钮的区域不会与页面中可交互元素重叠。在设计时就要规划好页面底部内容的安全区域。6.2 适配不同屏幕与安全区域我们之前用uni.getSystemInfoSync()处理了底部安全区。但对于顶部刘海屏和状态栏如果页面使用了navigationStyle: custom也需要手动处理。可以在App.vue或一个全局混入中计算一个可用于所有页面的安全区域高度变量。!-- App.vue -- script export default { onLaunch() { // 获取状态栏高度 const systemInfo uni.getSystemInfoSync(); const statusBarHeight systemInfo.statusBarHeight || 0; const safeAreaInsets systemInfo.safeAreaInsets; const bottomSafeHeight safeAreaInsets ? safeAreaInsets.bottom : 0; // 挂载到全局方便使用 uni.$systemInfo { statusBarHeight, bottomSafeHeight, // 可以计算一个包含底部TabBar和安全区的总高度 tabBarTotalHeight: 60 bottomSafeHeight }; } } /script然后在页面或组件中可以直接使用uni.$systemInfo.tabBarTotalHeight来设置padding-bottom。6.3 性能优化避免重复渲染CustomTabbar组件在每个页面都被引入如果组件内部有复杂的计算或监听可能影响性能。确保tabBarStyle计算属性依赖的systemInfo是稳定的不会在每次渲染时都重新计算实际上设备信息不会变所以没问题。可以考虑在created生命周期获取一次并存到data中。图标路径等如果使用计算属性确保其依赖的响应式数据变化频率低。6.4 小程序端的特殊处理在微信小程序等平台position: fixed的底部元素在键盘弹出时可能会被顶起。需要监听键盘高度变化动态调整样式。// 在CustomTabbar组件中 data() { return { keyboardHeight: 0 }; }, onLoad() { // 微信小程序监听键盘高度变化 // #ifdef MP-WEIXIN wx.onKeyboardHeightChange(res { this.keyboardHeight res.height; }) // #endif }, computed: { tabBarStyle() { let style {}; // ... 原有计算逻辑 // 如果键盘弹出TabBar需要上移 const bottomValue this.keyboardHeight 0 ? this.keyboardHeight px : 0px; // 注意这里不能简单用bottom因为我们是fixed在底部。键盘弹出时页面整体会被推高。 // 更常见的做法是在页面容器上设置padding-bottom当键盘弹出时TabBar其实会被键盘遮挡一部分这是符合预期的。 // 如果非要让TabBar始终在可视区逻辑会非常复杂通常不建议。 return style; } }6.5 图标与字体优化图标方案建议使用图标字体如UniApp推荐的iconfont或SVG而非纯图片。这样可以方便地改变颜色和大小且不会失真。将图标字体文件放在static目录在App.vue中全局引入CSS。点击反馈为每个tab-item和凸起按钮添加:active样式或微小的缩放、透明度变化动画提升交互感。7. 扩展思考从“凸起”到“灵动岛”现在的凸起按钮是静态的。我们可以进一步赋予它更多动态功能提升用户体验。7.1 添加动画效果例如点击凸起按钮时可以有一个轻微的放大再缩小的弹性动画或者图标旋转。.凸起按钮 { /* ... 原有样式 ... */ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .凸起按钮:active { transform: translateX(-50%) scale(0.9); }7.2 实现拖拽发布模仿一些社交应用长按凸起按钮可以拖拽出一个发布菜单。view class凸起按钮 touchstartonTouchStart touchmoveonTouchMove touchendonTouchEnd :style{ transform: translate(${translateX}px, ${translateY}px) } /view script export default { data() { return { translateX: 0, translateY: 0, startX: 0, startY: 0, isDragging: false } }, methods: { onTouchStart(e) { this.startX e.touches[0].clientX; this.startY e.touches[0].clientY; this.isDragging false; // 可以设置一个定时器长按超过一定时间后触发拖拽模式 this.longPressTimer setTimeout(() { this.isDragging true; // 触发拖拽UI变化例如显示一个菜单 }, 500); }, onTouchMove(e) { if (!this.isDragging) return; const currentX e.touches[0].clientX; const currentY e.touches[0].clientY; this.translateX currentX - this.startX; this.translateY currentY - this.startY; }, onTouchEnd() { clearTimeout(this.longPressTimer); if (this.isDragging) { // 拖拽结束处理释放逻辑例如判断释放位置打开对应功能 this.isDragging false; this.translateX 0; this.translateY 0; } else { // 普通点击执行原来的跳转逻辑 this.switchTab(2); } } } } /script7.3 集成徽标Badge在“消息”Tab上显示未读数量这是一个常见需求。可以在Vuex中维护一个未读消息数在TabBar组件中动态渲染一个小红点或数字。!-- 在tab-item内 -- view classicon-wrapper image classicon :srciconPath / view v-ifindex 3 unreadCount 0 classbadge text classbadge-text{{ unreadCount 99 ? 99 : unreadCount }}/text /view /view style .badge { position: absolute; top: -5px; right: -5px; min-width: 16px; height: 16px; border-radius: 8px; background-color: #ff3b30; display: flex; align-items: center; justify-content: center; padding: 0 4px; } .badge-text { color: white; font-size: 10px; line-height: 1; } /style实现一个体验良好的凸起TabBar远不止CSS样式那么简单。它涉及跨端兼容、状态管理、交互细节、性能考量等多个方面。从隐藏原生组件到完全自主绘制从静态布局到动态交互每一步都需要仔细权衡。本文提供的方案是一个生产可用的起点你可以根据自身产品的具体设计在此基础上调整样式、丰富动画、优化性能。记住好的组件是“磨”出来的多在不同真机上进行测试收集用户反馈持续迭代才能打造出真正让用户感到愉悦的导航体验。
返回列表