feat(theme): 完善顶部导航栏与底部菜单浅色暖色系适配及多端Tab生命周期同步

这个提交包含在:
wikiboss
2026-09-16 20:27:07 +08:00
父节点 9641dfba47
当前提交 1a56ba2682
共有 8 个文件被更改,包括 370 次插入 和 14 次删除

查看文件

@@ -1,13 +1,15 @@
<script>
import { getCurrentThemeKey } from './common/theme.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from './common/theme.js';
export default {
onLaunch: function () {
const currentTheme = getCurrentThemeKey();
applyThemeToNativeUI(currentTheme);
console.log('App Launch: 电竞陪玩小程序初始化, 当前主题:', currentTheme);
},
onShow: function () {
console.log('App Show');
const currentTheme = getCurrentThemeKey();
applyThemeToNativeUI(currentTheme);
},
onHide: function () {
console.log('App Hide');
@@ -33,6 +35,11 @@ export default {
--text-secondary: #94A3B8;
--text-muted: #64748B;
--shadow-brand: 0 8rpx 24rpx rgba(99, 102, 241, 0.35);
--nav-bg: #0A0F1D;
--nav-color: #F8FAFC;
--tab-bg: #151D30;
--tab-color: #94A3B8;
--tab-active: #6366F1;
}
/* 浅色暖阳晨曦主题 (Light Warm Sunset Morning) */
@@ -52,6 +59,11 @@ page.theme-warm, .theme-warm {
--text-secondary: #57534E;
--text-muted: #78716C;
--shadow-brand: 0 8rpx 24rpx rgba(234, 88, 12, 0.25);
--nav-bg: #FDFBF7;
--nav-color: #1C1917;
--tab-bg: #FFFFFF;
--tab-color: #78716C;
--tab-active: #EA580C;
background-color: #FDFBF7;
color: #1C1917;
@@ -161,8 +173,52 @@ html, body {
}
uni-tabbar .uni-tabbar__bd {
background-color: var(--bg-surface, #151D30) !important;
border-top: 1px solid rgba(255, 255, 255, 0.08) !important;
background-color: var(--tab-bg, var(--bg-surface, #151D30)) !important;
border-top: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08)) !important;
transition: background-color 0.2s ease;
}
/* 顶部导航栏 (H5 端 uni-page-head 动态浅色/暗黑主题自适应) */
uni-page-head, .uni-page-head {
background-color: var(--nav-bg, var(--bg-primary, #0A0F1D)) !important;
color: var(--nav-color, var(--text-primary, #F8FAFC)) !important;
border-bottom: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08)) !important;
transition: background-color 0.2s ease, color 0.2s ease;
}
uni-page-head .uni-page-head__title {
color: var(--nav-color, var(--text-primary, #F8FAFC)) !important;
}
/* 浅色暖阳晨曦模式下强制确保 TabBar 与 PageHead 呈现高质感浅色 */
body:has(.theme-warm) uni-page-head,
page.theme-warm ~ uni-page-head {
background-color: #FDFBF7 !important;
border-bottom: 1px solid #F0ECE4 !important;
}
body:has(.theme-warm) uni-page-head .uni-page-head__title,
page.theme-warm ~ uni-page-head .uni-page-head__title {
color: #1C1917 !important;
}
body:has(.theme-warm) uni-tabbar .uni-tabbar__bd,
page.theme-warm ~ uni-tabbar .uni-tabbar__bd {
background-color: #FFFFFF !important;
border-top: 1px solid #F0ECE4 !important;
box-shadow: 0 -4rpx 20rpx rgba(28, 25, 23, 0.04) !important;
}
body:has(.theme-warm) uni-tabbar .uni-tabbar__label,
page.theme-warm ~ uni-tabbar .uni-tabbar__label {
color: #78716C !important;
}
body:has(.theme-warm) uni-tabbar .uni-tabbar-item.active .uni-tabbar__label,
page.theme-warm ~ uni-tabbar .uni-tabbar-item.active .uni-tabbar__label,
body:has(.theme-warm) uni-tabbar .uni-tabbar__item.active .uni-tabbar__label,
page.theme-warm ~ uni-tabbar .uni-tabbar__item.active .uni-tabbar__label {
color: #EA580C !important;
}
/* #endif */
</style>

查看文件

@@ -28,7 +28,17 @@ export const THEMES = {
textPrimary: '#F8FAFC',
textSecondary: '#94A3B8',
textMuted: '#64748B',
shadowBrand: '0 8rpx 24rpx rgba(99, 102, 241, 0.35)'
shadowBrand: '0 8rpx 24rpx rgba(99, 102, 241, 0.35)',
navBar: {
backgroundColor: '#0A0F1D',
frontColor: '#ffffff'
},
tabBar: {
color: '#94A3B8',
selectedColor: '#6366F1',
backgroundColor: '#151D30',
borderStyle: 'black'
}
}
},
[THEME_KEYS.WARM]: {
@@ -50,13 +60,61 @@ export const THEMES = {
textPrimary: '#1C1917',
textSecondary: '#57534E',
textMuted: '#78716C',
shadowBrand: '0 8rpx 24rpx rgba(234, 88, 12, 0.25)'
shadowBrand: '0 8rpx 24rpx rgba(234, 88, 12, 0.25)',
navBar: {
backgroundColor: '#FDFBF7',
frontColor: '#000000'
},
tabBar: {
color: '#78716C',
selectedColor: '#EA580C',
backgroundColor: '#FFFFFF',
borderStyle: 'white'
}
}
}
};
const STORAGE_KEY = 'peiwan_app_theme';
/**
* 将主题色彩同步应用至小程序与原生顶部导航栏及底部 TabBar
*/
export function applyThemeToNativeUI(themeKey) {
const theme = THEMES[themeKey] || THEMES[THEME_KEYS.COOL];
const { navBar, tabBar } = theme.tokens;
// 1. 设置顶部系统导航栏颜色
try {
if (typeof uni !== 'undefined' && uni.setNavigationBarColor) {
uni.setNavigationBarColor({
frontColor: navBar.frontColor,
backgroundColor: navBar.backgroundColor,
animation: {
duration: 200,
timingFunc: 'easeOut'
}
});
}
} catch (e) {
// 忽略特定页面无系统导航栏异常
}
// 2. 设置底部原生 TabBar 菜单颜色
try {
if (typeof uni !== 'undefined' && uni.setTabBarStyle) {
uni.setTabBarStyle({
color: tabBar.color,
selectedColor: tabBar.selectedColor,
backgroundColor: tabBar.backgroundColor,
borderStyle: tabBar.borderStyle
});
}
} catch (e) {
// 忽略非 TabBar 页面抛出的异常
}
}
/**
* 获取当前持久化的主题标识
*/
@@ -88,7 +146,7 @@ export function getCurrentTokens() {
}
/**
* 切换全局主题并持久化,广播全局事件
* 切换全局主题并持久化,广播全局事件,同步更新顶部与底部 UI
*/
export function setTheme(themeKey) {
const targetKey = THEMES[themeKey] ? themeKey : THEME_KEYS.COOL;
@@ -97,6 +155,9 @@ export function setTheme(themeKey) {
} catch (e) {
// 忽略持久化异常
}
// 同步原生顶部与底部栏色彩
applyThemeToNativeUI(targetKey);
// 广播主题变更事件
try {

查看文件

@@ -153,9 +153,10 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { ACTIVITIES_LIST } from '@/common/mock.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey } from '@/common/theme.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
const activities = ref(ACTIVITIES_LIST);
const currentTab = ref('ALL');
@@ -164,6 +165,7 @@ const currentThemeKey = ref(getCurrentThemeKey());
const onThemeChanged = (key) => {
currentThemeKey.value = key;
applyThemeToNativeUI(key);
};
onMounted(() => {
@@ -171,6 +173,11 @@ onMounted(() => {
uni.$on('theme-change', onThemeChanged);
});
onShow(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});

查看文件

@@ -143,9 +143,10 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { GAMES_LIST, BANNERS, EXPERTS_LIST } from '@/common/mock.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey } from '@/common/theme.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
const games = ref(GAMES_LIST);
const banners = ref(BANNERS);
@@ -156,13 +157,20 @@ const currentThemeKey = ref(getCurrentThemeKey());
const onThemeChanged = (key) => {
currentThemeKey.value = key;
applyThemeToNativeUI(key);
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
});
onShow(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});

查看文件

@@ -1,5 +1,5 @@
<template>
<view class="order-list-container">
<view class="order-list-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 顶部状态分类 Tab -->
<view class="tab-header">
<view
@@ -76,9 +76,32 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { ORDERS_LIST } from '@/common/mock.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
const currentThemeKey = ref(getCurrentThemeKey());
const onThemeChanged = (key) => {
currentThemeKey.value = key;
applyThemeToNativeUI(key);
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
uni.$on('theme-change', onThemeChanged);
});
onShow(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});
const tabs = [
{ key: 'ALL', name: '全部' },
@@ -435,4 +458,89 @@ const rateExpert = (id) => {
}
}
}
.order-list-container.theme-warm {
background-color: #FDFBF7;
.tab-header {
background-color: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 4rpx 16rpx rgba(28, 25, 23, 0.04);
.tab-item {
.tab-text {
color: #78716C;
}
&.active {
background: rgba(234, 88, 12, 0.1);
.tab-text {
color: #EA580C;
}
.tab-indicator {
background: #EA580C;
box-shadow: 0 0 10rpx rgba(234, 88, 12, 0.4);
}
}
}
}
.orders-wrap .order-card {
background-color: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 6rpx 20rpx rgba(28, 25, 23, 0.05);
.order-head {
border-bottom-color: #F5F2EC;
.order-sn {
color: #78716C;
}
}
.expert-block {
.expert-meta {
.name-row .expert-name {
color: #1C1917;
}
.service-name {
color: #57534E;
}
.order-time {
color: #78716C;
}
}
.amount-box .quantity-text {
color: #78716C;
}
}
.order-actions {
border-top-color: #F5F2EC;
.action-btn {
&.cancel-btn {
color: #78716C;
border-color: #E7E5E4;
}
&.primary-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
}
&.chat-btn {
background: rgba(234, 88, 12, 0.1);
color: #EA580C;
border-color: rgba(234, 88, 12, 0.25);
}
}
}
}
.empty-state {
.empty-text {
color: #78716C;
}
.empty-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.35);
}
}
}
</style>

查看文件

@@ -121,9 +121,10 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { USER_PROFILE } from '@/common/mock.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, setTheme, THEME_KEYS } from '@/common/theme.js';
import { getCurrentThemeKey, setTheme, THEME_KEYS, applyThemeToNativeUI } from '@/common/theme.js';
const user = ref(USER_PROFILE);
const currentRole = ref('PLAYER');
@@ -131,13 +132,20 @@ const currentThemeKey = ref(getCurrentThemeKey());
const onThemeChanged = (key) => {
currentThemeKey.value = key;
applyThemeToNativeUI(key);
};
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
});
onShow(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});

查看文件

@@ -10,6 +10,18 @@ export const THEME_KEYS = {
export type ThemeKey = (typeof THEME_KEYS)[keyof typeof THEME_KEYS];
export interface NavBarThemeTokens {
backgroundColor: string;
frontColor: '#ffffff' | '#000000';
}
export interface TabBarThemeTokens {
color: string;
selectedColor: string;
backgroundColor: string;
borderStyle: 'white' | 'black';
}
export interface ThemeTokens {
bgPrimary: string;
bgSurface: string;
@@ -26,6 +38,8 @@ export interface ThemeTokens {
textSecondary: string;
textMuted: string;
shadowBrand: string;
navBar: NavBarThemeTokens;
tabBar: TabBarThemeTokens;
}
export interface ThemeItem {
@@ -55,7 +69,17 @@ export const THEMES: Record<ThemeKey, ThemeItem> = {
textPrimary: '#F8FAFC',
textSecondary: '#94A3B8',
textMuted: '#64748B',
shadowBrand: '0 8rpx 24rpx rgba(99, 102, 241, 0.35)'
shadowBrand: '0 8rpx 24rpx rgba(99, 102, 241, 0.35)',
navBar: {
backgroundColor: '#0A0F1D',
frontColor: '#ffffff'
},
tabBar: {
color: '#94A3B8',
selectedColor: '#6366F1',
backgroundColor: '#151D30',
borderStyle: 'black'
}
}
},
[THEME_KEYS.WARM]: {
@@ -77,7 +101,17 @@ export const THEMES: Record<ThemeKey, ThemeItem> = {
textPrimary: '#1C1917',
textSecondary: '#57534E',
textMuted: '#78716C',
shadowBrand: '0 8rpx 24rpx rgba(234, 88, 12, 0.25)'
shadowBrand: '0 8rpx 24rpx rgba(234, 88, 12, 0.25)',
navBar: {
backgroundColor: '#FDFBF7',
frontColor: '#000000'
},
tabBar: {
color: '#78716C',
selectedColor: '#EA580C',
backgroundColor: '#FFFFFF',
borderStyle: 'white'
}
}
}
};

查看文件

@@ -158,4 +158,78 @@ describe('ThemeManager (TDD 测试套件: 暖色系与双主题设计系统)', (
}
});
});
describe('5. 顶部导航栏 (NavBar) 与底部菜单 (TabBar) 双主题规范校验', () => {
it('冷色系 NavBar 必须为深色黑底白字,TabBar 必须为深蓝卡片底', () => {
const coolTheme = THEMES[THEME_KEYS.COOL];
expect(coolTheme.tokens.navBar).toBeDefined();
expect(coolTheme.tokens.navBar.backgroundColor).toBe('#0A0F1D');
expect(coolTheme.tokens.navBar.frontColor).toBe('#ffffff');
expect(coolTheme.tokens.tabBar).toBeDefined();
expect(coolTheme.tokens.tabBar.backgroundColor).toBe('#151D30');
expect(coolTheme.tokens.tabBar.selectedColor).toBe('#6366F1');
});
it('浅色暖阳晨曦模式下,顶部 NavBar 与底部 TabBar 必须全部为浅色亮底', () => {
const warmTheme = THEMES[THEME_KEYS.WARM];
expect(warmTheme.tokens.navBar).toBeDefined();
expect(warmTheme.tokens.tabBar).toBeDefined();
// 顶部 NavBar 浅色底 (亮度 >= 0.85),文字为深黑 (#000000)
expect(getLuminance(warmTheme.tokens.navBar.backgroundColor)).toBeGreaterThanOrEqual(0.85);
expect(warmTheme.tokens.navBar.frontColor).toBe('#000000');
// 底部 TabBar 浅色底 (纯白或米白,亮度 >= 0.95),选中项高亮为曜橙
expect(getLuminance(warmTheme.tokens.tabBar.backgroundColor)).toBeGreaterThanOrEqual(0.95);
expect(warmTheme.tokens.tabBar.selectedColor).toBe('#EA580C');
expect(warmTheme.tokens.tabBar.borderStyle).toBe('white');
});
});
describe('6. 核心 Tab 页面多端生命周期与主题自适应门禁校验', () => {
const rootDir = path.resolve(__dirname, '../../');
const tabPages = [
path.join(rootDir, 'pwuniapp', 'pages', 'index', 'index.vue'),
path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'list.vue'),
path.join(rootDir, 'pwuniapp', 'pages', 'order', 'list.vue'),
path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'),
];
it('四大 Tab 页面必须全部引入并在 onShow 中调用 applyThemeToNativeUI', () => {
for (const pagePath of tabPages) {
expect(fs.existsSync(pagePath)).toBe(true);
const content = fs.readFileSync(pagePath, 'utf-8');
const relativeName = path.relative(rootDir, pagePath);
expect({
file: relativeName,
hasApplyTheme: content.includes('applyThemeToNativeUI'),
hasOnShow: content.includes('onShow')
}).toEqual({
file: relativeName,
hasApplyTheme: true,
hasOnShow: true
});
}
});
it('四大 Tab 页面必须包含 theme-warm 根容器类绑定与浅色样式适配', () => {
for (const pagePath of tabPages) {
const content = fs.readFileSync(pagePath, 'utf-8');
const relativeName = path.relative(rootDir, pagePath);
expect({
file: relativeName,
hasThemeWarmBind: content.includes('theme-warm'),
hasThemeWarmStyle: content.includes('.theme-warm')
}).toEqual({
file: relativeName,
hasThemeWarmBind: true,
hasThemeWarmStyle: true
});
}
});
});
});