diff --git a/src/components/ThemeToggle.vue b/src/components/ThemeToggle.vue new file mode 100644 index 0000000..2daad6f --- /dev/null +++ b/src/components/ThemeToggle.vue @@ -0,0 +1,33 @@ + + + + + \ No newline at end of file diff --git a/src/composables/useTheme.js b/src/composables/useTheme.js new file mode 100644 index 0000000..111b355 --- /dev/null +++ b/src/composables/useTheme.js @@ -0,0 +1,141 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +// 全局响应式主题状态 +const isDark = ref(false) + +// 检查当前主题状态 +const checkCurrentTheme = () => { + return document.documentElement.classList.contains('dark') || + document.documentElement.getAttribute('data-theme') === 'dark' +} + +// 更新响应式状态 +const updateReactiveState = () => { + isDark.value = checkCurrentTheme() +} + +// 主题管理逻辑 +export function useTheme() { + let mutationObserver = null + + // 初始化主题 + const initTheme = () => { + const savedTheme = localStorage.getItem('theme') + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + + if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { + setDarkTheme() + } else { + setLightTheme() + } + + // 立即更新响应式状态 + updateReactiveState() + } + + // 设置深色主题 + const setDarkTheme = () => { + document.documentElement.classList.add('dark') + document.documentElement.setAttribute('data-theme', 'dark') + document.body.classList.add('dark') + document.body.setAttribute('data-theme', 'dark') + localStorage.setItem('theme', 'dark') + + // 立即更新响应式状态 + isDark.value = true + + // 触发主题更新事件 + window.dispatchEvent(new CustomEvent('theme-change', { detail: { isDark: true } })) + } + + // 设置浅色主题 + const setLightTheme = () => { + document.documentElement.classList.remove('dark') + document.documentElement.removeAttribute('data-theme') + document.body.classList.remove('dark') + document.body.removeAttribute('data-theme') + localStorage.setItem('theme', 'light') + + // 立即更新响应式状态 + isDark.value = false + + // 触发主题更新事件 + window.dispatchEvent(new CustomEvent('theme-change', { detail: { isDark: false } })) + } + + // 切换主题 + const toggleTheme = () => { + if (isDark.value) { + setLightTheme() + } else { + setDarkTheme() + } + } + + // 监听系统主题变化 + const setupSystemThemeListener = () => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + mediaQuery.addListener(() => { + const savedTheme = localStorage.getItem('theme') + // 只有在用户没有手动设置主题时才跟随系统 + if (!savedTheme) { + initTheme() + } + }) + } + + // 设置DOM变化监听器(确保响应式状态同步) + const setupDOMObserver = () => { + if (typeof window !== 'undefined') { + mutationObserver = new MutationObserver(() => { + updateReactiveState() + }) + + // 监听documentElement和body的变化 + mutationObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class', 'data-theme'] + }) + + mutationObserver.observe(document.body, { + attributes: true, + attributeFilter: ['class', 'data-theme'] + }) + } + } + + // 清理监听器 + const cleanup = () => { + if (mutationObserver) { + mutationObserver.disconnect() + mutationObserver = null + } + } + + // 获取当前主题 + const getCurrentTheme = () => { + return isDark.value ? 'dark' : 'light' + } + + // 组件挂载时初始化 + onMounted(() => { + setupDOMObserver() + updateReactiveState() + }) + + // 组件卸载时清理 + onUnmounted(() => { + cleanup() + }) + + return { + isDark, + initTheme, + toggleTheme, + setDarkTheme, + setLightTheme, + setupSystemThemeListener, + getCurrentTheme, + updateReactiveState + } +} \ No newline at end of file