feat(theme): implement comprehensive theme management system

Add full theme support with light, dark, and system modes, including:
- Theme store using Pinia for state management
- useTheme composable for reactive theme handling
- Theme setting UI in settings page
- Enhanced CSS variable system with Tailwind integration
- IPC communication for theme persistence
- Internationalization support for theme texts
- System theme detection and auto-switching

The implementation follows ClawX's architecture while adapting to Vue 3 and zn-ai's existing infrastructure.
This commit is contained in:
duanshuwen
2026-04-08 23:46:41 +08:00
parent 3ef3392808
commit a8bfbff0e9
12 changed files with 1450 additions and 326 deletions

182
src/stores/theme.ts Normal file
View File

@@ -0,0 +1,182 @@
/**
* 主题状态管理 store
* 采用 Vue 推荐的 Pinia 实现,功能和架构与 ClawX 的 Zustand 实现保持一致
* 集成 zn-ai 现有 IPC 通信
*/
import { defineStore } from 'pinia';
export type Theme = 'light' | 'dark' | 'system';
interface ThemeState {
// 主题状态
theme: Theme;
isDark: boolean;
systemTheme: 'light' | 'dark';
// 初始化状态
initialized: boolean;
}
// 检测系统主题
const detectSystemTheme = (): 'light' | 'dark' => {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
// 计算实际应用的主题(考虑 system 模式)
const getAppliedTheme = (theme: Theme, systemTheme: 'light' | 'dark'): 'light' | 'dark' => {
if (theme === 'system') return systemTheme;
return theme;
};
// 应用主题到 DOM通过 CSS 类)
const applyThemeToDom = (theme: 'light' | 'dark') => {
if (typeof document === 'undefined') return;
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
export const useThemeStore = defineStore('zn-ai-theme', {
state: (): ThemeState => {
// 从 localStorage 恢复缓存的主题,确保在初始化前 UI 不会闪烁
const cachedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('zn-ai-theme-cache') as Theme : null;
const initialTheme = (cachedTheme === 'light' || cachedTheme === 'dark' || cachedTheme === 'system') ? cachedTheme : 'system';
return {
theme: initialTheme,
isDark: false,
systemTheme: 'light',
initialized: false,
};
},
actions: {
async init() {
if (this.initialized) return;
try {
// 1. 检测系统主题
const systemTheme = detectSystemTheme();
// 2. 从主进程获取持久化的主题设置
let savedTheme: Theme = 'system';
try {
savedTheme = await window.api.invoke<Theme>('get-theme-mode');
} catch (error) {
console.warn('Failed to get theme from main process, using default:', error);
}
// 3. 计算实际主题和 isDark 状态
const appliedTheme = getAppliedTheme(savedTheme, systemTheme);
const isDark = appliedTheme === 'dark';
// 4. 应用主题到 DOM
applyThemeToDom(appliedTheme);
// 5. 更新 store 状态
this.theme = savedTheme;
this.isDark = isDark;
this.systemTheme = systemTheme;
this.initialized = true;
// 缓存到 localStorage
if (typeof localStorage !== 'undefined') {
localStorage.setItem('zn-ai-theme-cache', savedTheme);
}
// 6. 监听系统主题变化
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const newSystemTheme = e.matches ? 'dark' : 'light';
if (this.theme === 'system') {
const appliedTheme = getAppliedTheme('system', newSystemTheme);
const isDark = appliedTheme === 'dark';
applyThemeToDom(appliedTheme);
this.systemTheme = newSystemTheme;
this.isDark = isDark;
} else {
this.systemTheme = newSystemTheme;
}
};
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleChange);
} else {
mediaQuery.addListener(handleChange);
}
// 7. 监听主进程主题更新事件
window.api.on('theme-mode-updated', (isDarkUpdate: boolean) => {
const newIsDark = Boolean(isDarkUpdate);
const newTheme = newIsDark ? 'dark' : 'light';
if (this.theme === 'system') {
this.isDark = newIsDark;
} else {
const actualTheme = newIsDark ? 'dark' : 'light';
if (this.theme !== actualTheme) {
this.theme = actualTheme;
this.isDark = newIsDark;
}
}
});
}
console.log('Theme store initialized:', { theme: savedTheme, isDark, systemTheme });
} catch (error) {
console.error('Failed to initialize theme store:', error);
const systemTheme = detectSystemTheme();
const appliedTheme = getAppliedTheme('system', systemTheme);
const isDark = appliedTheme === 'dark';
applyThemeToDom(appliedTheme);
this.theme = 'system';
this.isDark = isDark;
this.systemTheme = systemTheme;
this.initialized = true;
}
},
async setTheme(theme: Theme) {
if (theme === this.theme) return;
try {
// 1. 保存到主进程
await window.api.invoke('set-theme-mode', theme);
// 2. 计算实际应用的主题
const appliedTheme = getAppliedTheme(theme, this.systemTheme);
const isDark = appliedTheme === 'dark';
// 3. 应用主题到 DOM
applyThemeToDom(appliedTheme);
// 4. 更新 store 状态
this.theme = theme;
this.isDark = isDark;
// 5. 缓存到 localStorage
if (typeof localStorage !== 'undefined') {
localStorage.setItem('zn-ai-theme-cache', theme);
}
console.log('Theme changed:', { theme, appliedTheme, isDark });
} catch (error) {
console.error('Failed to set theme:', error);
throw error;
}
},
detectSystemTheme,
updateIsDark() {
const appliedTheme = getAppliedTheme(this.theme, this.systemTheme);
this.isDark = appliedTheme === 'dark';
}
}
});