最近给我们的小程序加了一个暗色模式切换
整体思路:两套颜色 + 两种触发方式
核心原理:用 CSS 变量(--theme-bg-primary 这种)来定义所有颜色,亮色和暗色分别给这些变量赋不同的值,组件本身只用变量,不写死颜色
两种触发方式
方式一:跟随系统(自动)
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) { /* 切换变量值 */ }
}
这个按照手机系统来
方式二:用户手动切换
// store/modules/theme.js
document.documentElement.setAttribute('data-theme', 'dark')
在 <html> 根节点上加 data-theme="dark" 属性,CSS 里 [data-theme='dark'] 选择器就生效了,优先级比媒体查询高,所以能覆盖系统设置。
储存到 storage 里面
文件分工
| 文件 | 干啥的 |
|---|---|
styles/theme.scss | 定义所有 CSS 变量,亮色/暗色两套值 |
styles/dark-override.scss | 把具体 class 名和变量绑定(大量 !important) |
store/modules/theme.js | JS 层管理状态,读/写本地存储,监听系统主题变化 |
App.vue | 启动时初始化主题,监听系统 onThemeChange 事件 |
代码
app.vue
<!-- 全局主题 CSS 自定义属性(非 scoped) -->
<style lang="scss">
@import './styles/theme.scss';
</style>
<!-- 全局暗色模式覆盖(非 scoped):覆盖手动切换 [data-theme='dark'] -->
<style lang="scss">
@import './styles/dark-override.scss';
</style>
//theme.scss
/* 亮色(默认) */
:root, [data-theme='light'] {
--theme-bg-primary: #ffffff;
--theme-bg-card: #ffffff;
--theme-text-primary: #101114;
--theme-input-bg: #ffffff;
}
/* 暗色 — 跟随系统 */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--theme-bg-primary: #15171f;
--theme-bg-card: #1c1e28;
--theme-text-primary: #f0f0f0;
--theme-input-bg: #222222;
}
}
/* 暗色 — 手动切换(优先级更高) */
[data-theme='dark'] {
--theme-bg-primary: #15171f;
--theme-bg-card: #1c1e28;
--theme-text-primary: #f0f0f0;
}
// dark-override.scss
@mixin dark-overrides {
.page-wrap, .publish-page { background: var(--theme-bg-primary) !important; }
.info-card, .menu-card { background: var(--theme-bg-card) !important; }
.card-title, .user-name { color: var(--theme-text-primary) !important; }
.title-input { background: var(--theme-input-bg) !important; }
}
/* 系统自动 */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) { @include dark-overrides; }
}
/* 手动切换 */
[data-theme='dark'] { @include dark-overrides; }
// 切换主题
toggleThemeMode({ commit, state }) {
const newMode = state.themeMode === 'light' ? 'dark' : 'light'
commit('SET_THEME_MODE', newMode)
commit('SET_THEME_OVERRIDE', true)
uni.setStorageSync('themeMode', newMode) // 持久化
uni.setStorageSync('themeOverride', 'true')
this.dispatch('theme/applyThemeMode')
},
// 应用到 DOM
applyThemeMode({ state }) {
if (typeof document !== 'undefined') {
if (state.themeOverride) {
// 手动模式:加 data-theme 属性,覆盖媒体查询
document.documentElement.setAttribute('data-theme', state.themeMode)
} else {
// 跟随系统:移除属性,让 @media 自动响应
document.documentElement.removeAttribute('data-theme')
}
}
},
接下来是比较重点的,上面那些是网页常做的,而小程序要考虑导航栏这些东西的颜色
//app.vue
onShow() {
// 每次页面显示时恢复导航栏颜色(纯跟随系统时 @navBgColor 自动生效)
try {
const store = this.$store
const themeMode = store.getters['theme/themeMode']
const themeOverride = store.getters['theme/themeOverride']
if (themeOverride && (themeMode === 'dark' || themeMode === 'light')) {
uni.setNavigationBarColor({
frontColor: themeMode === 'dark' ? '#ffffff' : '#000000',
backgroundColor: themeMode === 'dark' ? '#1a1a1a' : '#ffffff',
})
}
} catch (e) {
// 低版本可能不支持
}
},
onHide() {
// App hidden
},
// 监听系统主题切换(微信小程序 2.11.0+ 支持)
onThemeChange(res) {
this.handleSystemThemeChange(res.theme)
},
手动切换的时候要使用setNavigationBarColor强制修改导航栏、tabBar、页面背景的颜色
{
"pages": [
{
"root": "modules/pages/DiscoverSearch",
"name": "search",
"pages": [
{
"path": "DiscoverSearch",
"style": {
"navigationBarTitleText": "搜索",
"navigationBarBackgroundColor": "@navBgColor",
"navigationStyle": "default"
}
}
]
},
],
"globalStyle": {
"navigationBarTextStyle": "@navTxtStyle",
"navigationBarTitleText": "gallery",
"navigationBarBackgroundColor": "@navBgColor",
"backgroundColor": "@bgColor",
"backgroundTextStyle": "@bgTxtStyle",
"navigationStyle": "custom",
"app-plus": {
"softinputmode": "adjustPan"
},
}
}
这里要用@navBgColor这样的用 @变量名 语法引用 theme.json 里的 key
这套方案是 CSS 变量 + 媒体查询 + 属性选择器 的组合