import { ref, computed, type Ref } from 'vue' /** 主题类型('System' = 跟随系统,'ClassicDark' = 经典深色,'ClassicLight' = 经典浅色,'ModernLight' = 现代浅色)*/ export type Theme = 'System' | 'ClassicDark' | 'ClassicLight' | 'ModernLight' /** 可用主题列表(System 置顶) */ export const THEMES: Theme[] = ['System', 'ClassicDark', 'ClassicLight', 'ModernLight'] // [跟随系统, 经典深色, 经典浅色, 现代浅色] /** 跟随系统时按系统配色偏好解析为经典深色/经典浅色 */ function resolveSystemTheme(): Theme { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'ClassicDark' : 'ClassicLight' } let current: Ref | null = null // 实际生效主题(始终为具体主题) let selected: Ref | null = null // 用户选择主题(可为 System) let systemQuery: MediaQueryList | null = null let systemChangeHandler: ((event: MediaQueryListEvent) => void) | null = null const enabledThemes = ref([...THEMES]) let siteDefault: Theme = 'System' function applyTheme(t: Theme) { current!.value = t document.documentElement.setAttribute('data-theme', t) } /** 注册系统配色偏好监听:系统切换时自动应用对应经典主题 */ function watchSystemTheme() { if (systemQuery) return systemQuery = window.matchMedia('(prefers-color-scheme: dark)') systemChangeHandler = () => applyTheme(resolveSystemTheme()) systemQuery.addEventListener('change', systemChangeHandler) } function unwatchSystemTheme() { if (systemQuery && systemChangeHandler) { systemQuery.removeEventListener('change', systemChangeHandler) } systemQuery = null systemChangeHandler = null } /** 读取本地保存的主题,读取失败返回 null */ function readSavedTheme(): Theme | null { try { return localStorage.getItem('voicehub-theme') as Theme | null } catch { return null } } /** * 获取全局主题状态引用。 * 在组件 composable 中使用,服务端返回初始值但不订阅。 */ export function useTheme() { // SSR 环境返回只读 stub,避免模块级单例在 Node 进程中跨请求共享可变状态 if (import.meta.server) { const currentTheme = computed(() => 'ClassicDark' as Theme) const selectedTheme = computed(() => 'ClassicDark' as Theme) const isDark = computed(() => true) return { currentTheme, selectedTheme, isDark, themes: computed(() => enabledThemes.value), setTheme: () => {}, toggleTheme: () => {} } } if (!current) { const saved: Theme | null = readSavedTheme() // 用户保存的主题被禁用时回退到站点默认;回退结果不写回 localStorage,避免把系统代选固化为用户偏好 const chosen: Theme = (THEMES.includes(saved as Theme) && enabledThemes.value.includes(saved as Theme) ? saved : null) ?? siteDefault selected = ref(chosen) current = ref(chosen === 'System' ? resolveSystemTheme() : chosen) document.documentElement.setAttribute('data-theme', current.value) if (chosen === 'System') watchSystemTheme() } const theme = current! const chosen = selected! const currentTheme = computed(() => theme.value) const selectedTheme = computed(() => chosen.value) const isDark = computed(() => theme.value === 'ClassicDark') const setTheme = (t: Theme) => { if (!enabledThemes.value.includes(t)) return chosen.value = t if (t === 'System') { watchSystemTheme() applyTheme(resolveSystemTheme()) } else { unwatchSystemTheme() applyTheme(t) } try { localStorage.setItem('voicehub-theme', t) } catch { /* localStorage 写入失败(如配额满或被禁用),静默忽略 */ } } /** 切换主题:按顺序循环切换 */ const toggleTheme = () => { const availableThemes = enabledThemes.value const nextIndex = (availableThemes.indexOf(chosen.value) + 1) % availableThemes.length setTheme(availableThemes[nextIndex]!) } return { currentTheme, selectedTheme, isDark, themes: enabledThemes, setTheme, toggleTheme } } /** 应用服务端主题策略;用户已保存的主题优先于站点默认主题。 */ export function applyThemeConfig(defaultTheme: unknown, configuredThemes: unknown) { if (import.meta.server) return if (!current || !selected) { console.warn('applyThemeConfig: useTheme 尚未初始化,主题配置暂不生效') return } const nextEnabled = Array.isArray(configuredThemes) ? THEMES.filter((theme) => configuredThemes.includes(theme)) : [...THEMES] enabledThemes.value = nextEnabled.length > 0 ? nextEnabled : [...THEMES] if (enabledThemes.value.includes('System') && (!enabledThemes.value.includes('ClassicDark') || !enabledThemes.value.includes('ClassicLight'))) { enabledThemes.value = enabledThemes.value.filter((theme) => theme !== 'System') } if (enabledThemes.value.length === 0) { enabledThemes.value = [...THEMES] } siteDefault = THEMES.includes(defaultTheme as Theme) && enabledThemes.value.includes(defaultTheme as Theme) ? defaultTheme as Theme : enabledThemes.value.includes('System') ? 'System' : enabledThemes.value[0]! const saved = readSavedTheme() const next = THEMES.includes(saved as Theme) && enabledThemes.value.includes(saved as Theme) ? saved as Theme : siteDefault // 用户保存的主题被禁用时回退到站点默认;回退结果不写回 localStorage,避免把系统代选固化为用户偏好 selected.value = next if (next === 'System') { watchSystemTheme() applyTheme(resolveSystemTheme()) } else { unwatchSystemTheme() applyTheme(next) } }