初次提交
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const STORAGE_KEY = 'voicehub:theme'
|
||||
type ThemeName = string & {}
|
||||
|
||||
const currentTheme = ref<string | null>(null)
|
||||
const initialized = ref(false)
|
||||
|
||||
function isBrowser() {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
}
|
||||
|
||||
function getMetaThemeColor() {
|
||||
if (!isBrowser()) return null
|
||||
return document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
|
||||
}
|
||||
|
||||
function applyTheme(theme: string) {
|
||||
if (!isBrowser()) return
|
||||
|
||||
const root = document.documentElement
|
||||
const appRoot = document.querySelector<HTMLElement>('div.app')
|
||||
|
||||
root.setAttribute('data-theme', theme)
|
||||
if (appRoot) appRoot.setAttribute('data-theme', theme)
|
||||
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark')
|
||||
root.classList.remove('light')
|
||||
root.style.colorScheme = 'dark'
|
||||
} else if (theme === 'light') {
|
||||
root.classList.add('light')
|
||||
root.classList.remove('dark')
|
||||
root.style.colorScheme = 'light'
|
||||
} else {
|
||||
root.classList.remove('dark', 'light')
|
||||
root.style.colorScheme = 'light dark'
|
||||
}
|
||||
|
||||
const meta = getMetaThemeColor()
|
||||
if (meta) {
|
||||
if (theme === 'light') {
|
||||
meta.content = '#f8fafc'
|
||||
} else {
|
||||
meta.content = '#111111'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, theme)
|
||||
} catch {
|
||||
/* ignore storage errors */
|
||||
}
|
||||
}
|
||||
|
||||
function detectSystemTheme(): 'dark' | 'light' {
|
||||
if (!isBrowser()) return 'dark'
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
return prefersDark ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const enabledThemes = computed<string[]>(() => {
|
||||
const list = (config.public?.enabledThemes as unknown) as string[] | undefined
|
||||
return Array.isArray(list) && list.length > 0 ? list : ['dark', 'light']
|
||||
})
|
||||
|
||||
const defaultTheme = computed<string>(() => {
|
||||
const value = config.public?.defaultTheme as string | undefined
|
||||
if (value && enabledThemes.value.includes(value)) return value
|
||||
return enabledThemes.value.includes('dark') ? 'dark' : enabledThemes.value[0] || 'dark'
|
||||
})
|
||||
|
||||
const theme = computed<string>({
|
||||
get: () => currentTheme.value ?? defaultTheme.value,
|
||||
set: (next: string) => setTheme(next)
|
||||
})
|
||||
|
||||
const isDark = computed(() => {
|
||||
const t = theme.value
|
||||
if (t === 'dark') return true
|
||||
if (t === 'light') return false
|
||||
return detectSystemTheme() === 'dark'
|
||||
})
|
||||
|
||||
function isThemeEnabled(name: string) {
|
||||
return enabledThemes.value.includes(name)
|
||||
}
|
||||
|
||||
function setTheme(next: string, { persist = true } = {}) {
|
||||
let candidate = next
|
||||
if (!enabledThemes.value.includes(candidate)) {
|
||||
candidate = defaultTheme.value
|
||||
}
|
||||
|
||||
if (currentTheme.value === candidate && initialized.value) return
|
||||
|
||||
currentTheme.value = candidate
|
||||
applyTheme(candidate)
|
||||
|
||||
if (!persist && isBrowser()) {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const order = enabledThemes.value.length >= 2 ? enabledThemes.value : ['dark', 'light']
|
||||
const current = theme.value
|
||||
const idx = order.indexOf(current)
|
||||
const next = order[(idx + 1) % order.length]
|
||||
setTheme(next)
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
setTheme(defaultTheme.value, { persist: false })
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (initialized.value || !isBrowser()) return
|
||||
initialized.value = true
|
||||
|
||||
let stored: string | null = null
|
||||
try {
|
||||
stored = localStorage.getItem(STORAGE_KEY)
|
||||
} catch {
|
||||
stored = null
|
||||
}
|
||||
|
||||
if (stored && enabledThemes.value.includes(stored)) {
|
||||
setTheme(stored)
|
||||
return
|
||||
}
|
||||
|
||||
setTheme(defaultTheme.value, { persist: false })
|
||||
|
||||
if (window.matchMedia) {
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const onChange = () => {
|
||||
let s: string | null = null
|
||||
try {
|
||||
s = localStorage.getItem(STORAGE_KEY)
|
||||
} catch {
|
||||
s = null
|
||||
}
|
||||
if (!s) {
|
||||
setTheme(defaultTheme.value, { persist: false })
|
||||
}
|
||||
}
|
||||
if (typeof mql.addEventListener === 'function') {
|
||||
mql.addEventListener('change', onChange)
|
||||
} else if (typeof mql.addListener === 'function') {
|
||||
mql.addListener(onChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
currentTheme,
|
||||
defaultTheme,
|
||||
enabledThemes,
|
||||
isDark,
|
||||
isThemeEnabled,
|
||||
setTheme,
|
||||
toggleTheme,
|
||||
resetToDefault,
|
||||
init
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user