feat(theme): 添加主题切换功能- 新增 ThemeToggle 组件,用于在界面上显示主题切换按钮
- 实现 useTheme钩子,提供主题管理逻辑- 添加暗黑模式和亮色模式的切换支持 - 实现本地存储和系统主题的自动检测 - 添加主题切换时的事件广播
This commit is contained in:
33
src/components/ThemeToggle.vue
Normal file
33
src/components/ThemeToggle.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<n-button
|
||||||
|
circle
|
||||||
|
size="medium"
|
||||||
|
class="theme-toggle"
|
||||||
|
@click="toggleTheme"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon v-if="isDark">
|
||||||
|
<Sunny />
|
||||||
|
</n-icon>
|
||||||
|
<n-icon v-else>
|
||||||
|
<Moon />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Moon, Sunny } from '@vicons/ionicons5'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
|
||||||
|
const { isDark, toggleTheme } = useTheme()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.theme-toggle {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}</style>
|
||||||
141
src/composables/useTheme.js
Normal file
141
src/composables/useTheme.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user