uni-app 全端动态换肤方案 (Vue2 + uView 1.0)
1. 方案简介
本方案实现了 H5、App、微信小程序 的全端主题切换功能。
- 页面元素 :使用 CSS 变量 (
var(--xxx)) 实现秒级无感切换。 - 原生控件 :通过 JS API 动态修改
NavigationBar(导航栏)、TabBar(底部菜单样式)及TabBar Icon(图标)。 - 兼容性:解决了 H5 背景色不同步、App 端 API 不兼容报错、Webview 重置等问题。
2. 目录结构
Plaintext
project-root
├── common
│ └── theme.js // 核心配置:定义颜色变量、原生配置、图标映射
├── store
│ └── index.js // 核心逻辑:Vuex 状态管理、调用原生 API
├── mixins
│ └── themeMixin.js // 注入逻辑:CSS 变量注入、onShow 补救措施
├── static
│ └── tabbar // 图标资源:需按命名规范存放
├── App.vue // H5 body 背景处理、初始化
├── main.js // 全局 Mixin 注册
└── pages/index/index.vue // 使用示例
3. 核心代码实现
3.1 图片资源准备 (static/tabbar/)
文件命名规范 :{iconKey}_{themeName}.png 和 {iconKey}_{themeName}_sel.png
示例:
-
home_default.png/home_default_sel.png -
home_red.png/home_red_sel.png -
user_default.png/ ...static/
└── tabbar/
├── home_default.png // 默认主题-未选中
├── home_default_sel.png // 默认主题-选中
├── user_default.png
├── user_default_sel.png
│
├── home_red.png // 红色主题-未选中
├── home_red_sel.png // 红色主题-选中
├── user_red.png
├── user_red_sel.png
│
├── home_dark.png // 暗黑主题-未选中
└── ...
3.2 主题配置文件 (common/theme.js)
JavaScript
// Tabbar 结构配置 (需与 pages.json 保持一致)
export const tabbarConfig = [
{ index: 0, text: '首页', iconKey: 'home' },
{ index: 1, text: '我的', iconKey: 'user' }
]
// 主题色值定义
export default {
// === 默认主题 (蓝色) ===
default: {
// CSS 变量 (用于页面)
'--bg-color': '#f3f4f6',
'--box-bg': '#ffffff',
'--text-color': '#333333',
'--primary-color': '#2979ff',
// 原生控件配置
navBg: '#2979ff',
navTxt: '#ffffff', // 仅支持 #ffffff 或 #000000
tabBg: '#ffffff',
tabTxtColor: '#999999',
tabSelColor: '#2979ff',
tabBorder: 'black'
},
// === 红色主题 ===
red: {
'--bg-color': '#fff1f1',
'--box-bg': '#ffffff',
'--text-color': '#4a0a0a',
'--primary-color': '#fa3534',
navBg: '#fa3534',
navTxt: '#ffffff',
tabBg: '#fff0f0',
tabTxtColor: '#ffadad',
tabSelColor: '#fa3534',
tabBorder: 'white'
}
}
3.3 Vuex 状态管理 (store/index.js)
import Vue from 'vue'
import Vuex from 'vuex'
import themes, { tabbarConfig } from '@/common/theme.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
themeName: 'default',
},
getters: {
// 生成 CSS 变量字符串
themeStyle(state) {
const theme = themes[state.themeName] || themes['default'];
let styleStr = '';
for (let key in theme) {
if (key.startsWith('--')) {
styleStr += `${key}: ${theme[key]};`;
}
}
return styleStr;
},
// 获取当前主题对象 (供 JS 使用)
currentThemeColor(state) {
return themes[state.themeName] || themes['default'];
}
},
mutations: {
SET_THEME_STATE(state, themeName) {
state.themeName = themeName;
uni.setStorageSync('uview_theme', themeName);
}
},
actions: {
// 对外调用的切换方法
setTheme({ commit, dispatch }, themeName) {
if (!themes[themeName]) return;
commit('SET_THEME_STATE', themeName);
dispatch('updateNativeUI');
},
// 核心:更新原生 UI (Nav, Tabbar)
updateNativeUI({ state }) {
const themeName = state.themeName;
const theme = themes[themeName] || themes['default'];
// 1. 设置导航栏
try {
uni.setNavigationBarColor({
frontColor: theme.navTxt,
backgroundColor: theme.navBg,
animation: { duration: 0 },
fail: () => {}
});
} catch (e) {}
// 2. 设置 Tabbar 样式
try {
uni.setTabBarStyle({
backgroundColor: theme.tabBg,
color: theme.tabTxtColor,
selectedColor: theme.tabSelColor,
borderStyle: theme.tabBorder,
fail: () => {}
});
} catch (e) {}
// 3. 设置 Tabbar 图标
if (typeof tabbarConfig !== 'undefined' && tabbarConfig.length) {
tabbarConfig.forEach(item => {
try {
uni.setTabBarItem({
index: item.index,
text: item.text,
iconPath: `static/tabbar/${item.iconKey}_${themeName}.png`,
selectedIconPath: `static/tabbar/${item.iconKey}_${themeName}_sel.png`,
fail: () => {} // 非 Tabbar 页面调用会失败,必须捕获
});
} catch (e) {}
});
}
// 4. 设置窗口背景 (仅小程序) - 防止 App/H5 报错
// #ifdef MP
if (theme['--bg-color']) {
uni.setBackgroundColor({
backgroundColor: theme['--bg-color'],
backgroundColorTop: theme['--bg-color'],
backgroundColorBottom: theme['--bg-color'],
fail: () => {}
});
}
// #endif
}
}
})
3.4 全局 Mixin (mixins/themeMixin.js)
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['themeStyle', 'currentThemeColor']),
// 供页面根节点绑定的 style
themeVars() {
return this.themeStyle || '';
}
},
// 每次页面显示时,强制刷新一次原生 UI
// 解决 WebView 重置、返回页面导航栏颜色丢失等问题
onShow() {
this.$store.dispatch('updateNativeUI');
// 延迟 50ms 确保页面容器已准备好
// 如果在某些极端的 支付宝小程序 或 老旧 Android 机型上, onShow 执行过快, DOM 还没准备好, 导致设置失败。 可以使用 this.$nextTick 或 setTimeout 包裹一下:
// setTimeout(() => {
// this.$store.dispatch('updateNativeUI');
// }, 50);
},
methods: {
setTheme(name) {
this.$store.dispatch('setTheme', name);
}
}
}
3.5 入口配置 (main.js)
注意 :Vue.mixin 必须在 new Vue 之前。
import Vue from 'vue'
import App from './App'
import store from './store'
import uView from 'uview-ui'
import themeMixin from './mixins/themeMixin.js'
Vue.use(uView)
// 【关键】注册全局 Mixin
Vue.mixin(themeMixin)
const app = new Vue({
store,
...App
})
app.$mount()
3.6 App.vue 配置 (解决 H5 Body 背景)
<script>
export default {
onLaunch: function() {
const savedTheme = uni.getStorageSync('uview_theme') || 'default';
this.$store.dispatch('setTheme', savedTheme);
},
onShow: function() {
// #ifdef H5
this.updateH5BodyBg();
// #endif
},
methods: {
updateH5BodyBg() {
if (!this.$store || !this.$store.getters.currentThemeColor) return;
const themeConfig = this.$store.getters.currentThemeColor;
if (themeConfig['--bg-color']) {
document.body.style.backgroundColor = themeConfig['--bg-color'];
}
}
},
watch: {
// 监听主题变化
'$store.state.themeName': {
handler(val) {
// #ifdef H5
this.updateH5BodyBg();
// #endif
}
}
}
}
</script>
<style lang="scss">
@import "uview-ui/index.scss";
// 强制覆盖 page 背景,使 App 端生效
page {
background-color: var(--bg-color) !important;
transition: background-color 0.3s;
}
</style>
4. 页面使用指南
在任何 .vue 页面中,必须遵守以下规则:
- 根节点绑定 :最外层
<view>必须绑定:style="themeVars"。 - 样式使用 :
- CSS 中 :使用
var(--primary-color)。 - uView 组件中 :支持
custom-style的直接传var(...);不支持的属性用currentThemeColor['--primary-color']。
- CSS 中 :使用
示例代码 (pages/index/index.vue):
<template>
<view class="content" :style="themeVars">
<view class="box">我是跟随主题的盒子</view>
<u-button
:custom-style="{ backgroundColor: 'var(--primary-color)', color: '#fff' }"
@click="setTheme('red')">
切换红色主题
</u-button>
<u-icon name="star" :color="currentThemeColor['--primary-color']"></u-icon>
</view>
</template>
<style lang="scss" scoped>
.content {
// 使用变量
background-color: var(--bg-color);
min-height: 100vh;
}
.box {
background-color: var(--box-bg);
color: var(--text-color);
}
</style>
5. 常见问题 (FAQ)
- Q: 为什么 H5 报错 setBackgroundColor is not yet implemented?
- A: 可以在
store/index.js中使用了条件编译// #ifdef MP包裹该 API,确保只在小程序环境执行。
- A: 可以在
- Q: 为什么报错 themeVars is not defined?
- A: 检查
main.js,Vue.mixin(themeMixin)是否写在了new Vue()之前。
- A: 检查
- Q: 导航栏文字颜色为什么没变?
- A:
uni.setNavigationBarColor的frontColor属性非常严格,只能是#ffffff或#000000。请检查theme.js配置。
- A:
- Q: 为什么切换主题时 Tabbar 图标会闪一下?
- A: 这是因为原生 API 替换图片需要加载时间。建议压缩图标体积(推荐 PNG, 81x81px 以内)。
如果对您有帮忙感谢一箭三连支持,如果有错误的地方欢迎指正,大家一起学习进步!