Files
Aether/frontend/src/composables/useDarkMode.ts
2025-12-10 20:52:44 +08:00

133 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { effectScope, ref, watch, computed } from 'vue'
const THEME_STORAGE_KEY = 'theme'
// 主题模式类型
export type ThemeMode = 'system' | 'light' | 'dark'
// 全局共享的状态
const themeMode = ref<ThemeMode>('system')
const isDark = ref(false)
let initialized = false
let scope: ReturnType<typeof effectScope> | null = null
let mediaQuery: MediaQueryList | null = null
const applyDarkMode = (value: boolean) => {
if (typeof document === 'undefined') {
return
}
document.documentElement.classList.toggle('dark', value)
if (document.body) {
document.body.setAttribute('theme-mode', value ? 'dark' : 'light')
}
}
const getSystemPreference = (): boolean => {
if (typeof window === 'undefined') {
return false
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
const updateDarkMode = () => {
if (themeMode.value === 'system') {
isDark.value = getSystemPreference()
} else {
isDark.value = themeMode.value === 'dark'
}
applyDarkMode(isDark.value)
}
const handleSystemChange = (e: MediaQueryListEvent) => {
if (themeMode.value === 'system') {
isDark.value = e.matches
applyDarkMode(isDark.value)
}
}
const ensureWatcher = () => {
if (scope) {
return
}
scope = effectScope(true)
scope.run(() => {
watch(
themeMode,
(value) => {
updateDarkMode()
if (typeof window !== 'undefined') {
localStorage.setItem(THEME_STORAGE_KEY, value)
}
},
{ flush: 'post' }
)
})
}
const initialize = () => {
if (initialized) {
return
}
initialized = true
ensureWatcher()
if (typeof window !== 'undefined') {
const storedTheme = localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode | null
if (storedTheme === 'dark' || storedTheme === 'light' || storedTheme === 'system') {
themeMode.value = storedTheme
} else {
// 兼容旧版本存储格式,旧版本直接存储 'dark' 或 'light'
themeMode.value = 'system'
}
// 监听系统主题变化
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', handleSystemChange)
}
updateDarkMode()
}
export function useDarkMode() {
initialize()
ensureWatcher()
applyDarkMode(isDark.value)
const setDarkMode = (value: boolean) => {
themeMode.value = value ? 'dark' : 'light'
}
const setThemeMode = (mode: ThemeMode) => {
themeMode.value = mode
}
const toggleDarkMode = () => {
// 循环切换system -> light -> dark -> system
if (themeMode.value === 'system') {
themeMode.value = 'light'
} else if (themeMode.value === 'light') {
themeMode.value = 'dark'
} else {
themeMode.value = 'system'
}
}
// 是否为跟随系统模式
const isSystemMode = computed(() => themeMode.value === 'system')
return {
isDark,
themeMode,
isSystemMode,
toggleDarkMode,
setDarkMode,
setThemeMode
}
}