微信小程序原生的tabbar虽然配置简单,但一旦涉及徽标(角标)展示,样式可定制的能力就非常有限。当应用需要支持深色模式,或者产品要求根据会员皮肤、运营活动切换整体主题时,把tabbar徽标的样式硬编码在代码里就会变得难以维护。这篇文章介绍一种基于自定义tabbar的主题化方案,让徽标颜色、形状、字号都能跟随主题动态变化。

一、为什么需要自定义tabbar来做徽标主题化
先说原生方案的局限。在app.json中配置tabbar时,徽标只能通过uni.setTabBarBadge或wx.setTabBarBadge设置一个数字文本,颜色完全由微信客户端渲染,开发者无法控制角标背景色、字体色,更不可能随主题切换。虽然tabbar配置里提供了color和backgroundColor两个字段,但它们只作用于tab栏本身,不影响角标。
所以要做主题化的徽标,第一步就是启用自定义tabbar。做法是在app.json中声明"custom": true,同时保留list配置(list仍然是路由跳转的依据):
{
"tabBar": {
"custom": true,
"color": "#666666",
"selectedColor": "#1989fa",
"backgroundColor": "#ffffff",
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/message/message", "text": "消息" },
{ "pagePath": "pages/mine/mine", "text": "我的" }
]
}
}
接着在项目根目录创建custom-tab-bar文件夹,包含index.js、index.json、index.wxml、index.wxss四个文件。这样每次切换tab页面,微信会渲染我们自己的组件作为底部栏,徽标就成了组件里的一个普通元素,样式完全由我们掌控。
二、设计主题变量:把徽标样式从结构中抽离
主题化的核心思想是数据驱动样式。不要在wxss里写死background: red,而是把徽标的所有视觉属性整理成一份主题配置表,每个主题对应一组值。常见的可抽取属性包括:角标背景色、文字颜色、字体大小、圆点最小宽度、超过99时的省略展示、描边色(用于与tab背景区分)等。
// themes/theme.js
const themes = {
light: {
navBg: '#ffffff',
navText: '#333333',
badgeBg: '#ee0a24',
badgeColor: '#ffffff',
badgeBorder: '#ffffff',
badgeFontSize: 20, // rpx
badgeDotSize: 16 // rpx
},
dark: {
navBg: '#1f1f1f',
navText: '#e0e0e0',
badgeBg: '#ff5252',
badgeColor: '#1a1a1a',
badgeBorder: '#1f1f1f',
badgeFontSize: 20,
badgeDotSize: 18
},
blue: {
navBg: '#f5f8ff',
navText: '#333333',
badgeBg: '#1989fa',
badgeColor: '#ffffff',
badgeBorder: '#f5f8ff',
badgeFontSize: 20,
badgeDotSize: 16
}
};
module.exports = themes;
注意一个细节:深色主题下的徽标不建议直接沿用light的纯红底白字,因为深色环境下高饱和红容易刺眼,很多设计规范会改用亮橙红搭配深色文字,并让描边色与tab背景一致,形成“挖空”效果,视觉上更协调。这些差异点正是主题配置表存在的意义。
wxml部分不要直接引用主题对象,而是通过组件的data渲染样式,保证数据变化时视图自动更新:
<view class="tab-bar" style="background: {{theme.navBg}};">
<view wx:for="{{list}}" wx:key="pagePath" class="tab-item" data-path="{{item.pagePath}}" bindtap="switchTab">
<view class="tab-icon-wrap">
<image src="{{selected === item.pagePath ? item.selectedIcon : item.icon}}" class="tab-icon"/>
<view wx:if="{{item.badge > 0}}" class="badge-num" style="background: {{theme.badgeBg}}; color: {{theme.badgeColor}}; border-color: {{theme.badgeBorder}}; font-size: {{theme.badgeFontSize}}rpx;">
{{item.badge > 99 ? '99+' : item.badge}}
</view>
<view wx:elif="{{item.showDot}}" class="badge-dot" style="width: {{theme.badgeDotSize}}rpx; height: {{theme.badgeDotSize}}rpx; background: {{theme.badgeBg}};"></view>
</view>
<view class="tab-text" style="color: {{selected === item.pagePath ? item.selectedColor : theme.navText}};">{{item.text}}</view>
</view>
</view>
三、主题检测与切换:让徽标自动响应
主题来源一般有两类:一是跟随系统深色模式,二是应用内的皮肤设置。跟随系统时,先在app.json中开启"darkmode": true,然后在custom-tab-bar的index.js中通过wx.getSystemInfoSync().theme获取当前主题,并监听wx.onThemeChange事件,系统切换深浅色时实时刷新组件data中的theme字段。
// custom-tab-bar/index.js
const themes = require('../themes/theme.js');
Component({
data: {
selected: 0,
theme: themes.light,
list: [
{ pagePath: '/pages/index/index', text: '首页', icon: '/assets/home.png', selectedIcon: '/assets/home-active.png', badge: 0, showDot: false },
{ pagePath: '/pages/message/message', text: '消息', icon: '/assets/msg.png', selectedIcon: '/assets/msg-active.png', badge: 0, showDot: false },
{ pagePath: '/pages/mine/mine', text: '我的', icon: '/assets/mine.png', selectedIcon: '/assets/mine-active.png', badge: 0, showDot: false }
]
},
lifetimes: {
attached() {
const sys = wx.getSystemInfoSync();
this.applyTheme(sys.theme === 'dark' ? 'dark' : 'light');
this.themeListener = (res) => {
this.applyTheme(res.theme === 'dark' ? 'dark' : 'light');
};
wx.onThemeChange(this.themeListener);
},
detached() {
wx.offThemeChange(this.themeListener);
}
},
methods: {
applyTheme(name) {
this.setData({ theme: themes[name] || themes.light });
},
switchTab(e) {
const path = e.currentTarget.dataset.path;
wx.switchTab({ url: path });
}
}
});
这里有一个容易踩的坑:custom-tab-bar的实例在各个tab页面是独立的,每个tab页首次展示时都会新建一个组件实例。所以仅靠attached里读取一次系统主题是不够的,还要在每个tab页面的onShow里通过this.getTabBar()拿到当前实例,同步选中项和主题状态。建议把这段逻辑抽成公共mixin或behaviors:
// utils/tabbar-sync.js
const themes = require('../themes/theme.js');
module.exports {
onShow() {
const tabBar = this.getTabBar && this.getTabBar();
if (!tabBar) return;
const app = getApp();
tabBar.setData({
selected: this.data.tabIndex,
theme: themes[app.globalData.currentTheme] || themes.light
});
}
};
如果是应用内皮肤切换(比如蓝色活动皮肤),则不再依赖系统事件,而是在用户点击设置页的皮肤选项时,把选中的主题名写入全局状态和本地缓存,再通知所有tabbar实例刷新。可以借助事件总线或者简单的页面遍历来完成,关键是保证设置页和其他tab页拿到的tabbar实例都被更新,否则会出现切回某个页面时主题不一致的闪烁。
四、徽标数据更新与样式细节优化
角标数字通常来自服务端推送或本地未读逻辑。更新时同样要操作当前tabbar实例,而不是指望原生API。例如消息页拉到未读数后调用this.getTabBar().setData({'list[1].badge': count}),下标方式更新比整列表覆盖性能更好,避免频繁setData整个list数组。
样式层面还有几点值得注意。徽标定位一般用绝对定位挂在图标右上角,并设置min-width配合padding保证数字位数多时不挤压变形;超过99显示99+几乎是行业惯例;如果tab图标本身在深色主题下会换图,徽标的border描边色也要同步换成对应的tab背景色,否则会出现一圈不和谐的白边。此外,rpx和px的选择要统一,配置表里用rpx就必须在wxml里拼上rpx单位,避免混用导致不同机型上对不齐。
最后,如果项目使用了scss或less预编译,可以把徽标的基础样式(圆角、居中、定位)写在wxss中固定不变,只把颜色和尺寸这类主题相关的属性交给内联style注入。这样样式职责划分清晰:结构类样式归css,视觉类样式归主题数据,后期新增一套皮肤只需要在theme.js里加一个对象,完全不用碰wxml和wxss,维护成本降到最低。