320 lines
11 KiB
TypeScript
320 lines
11 KiB
TypeScript
/**
|
||
* @file app/composables/useTheme.ts
|
||
* @description
|
||
*
|
||
* `useTheme` 是一个全局的主题管理"管家"(Composable)。它采用单例模式,确保在整个应用中只有一份主题状态。
|
||
* 这个模块负责:
|
||
* 1. **状态管理**:集中管理所有与主题相关的响应式状态,如主题模式(经典、优雅、简洁)、主题色、字体、字号等。
|
||
* 2. **持久化**:将用户的设置保存到 localStorage,以便在下次访问时恢复。
|
||
* 3. **应用样式**:动态地将主题设置应用到 `<html>` 根元素上,通过 CSS 变量和类名来控制全局样式。
|
||
* 4. **提供接口**:向外暴露所有状态和方法,方便任何组件读取和修改主题设置。
|
||
*
|
||
* ---
|
||
*
|
||
* @usage
|
||
*
|
||
* **自动导入**:
|
||
* 由于这个文件位于 `composables/` 目录下,Nuxt 会自动导入 `useTheme` 函数。
|
||
* 你可以在任何 `.vue` 组件的 `<script setup>` 中直接使用,无需手动 `import`。
|
||
*
|
||
* **初始化**:
|
||
* 应用启动时,`plugins/theme.client.ts` 插件会自动调用 `initializeTheme()` 方法,
|
||
* 从 localStorage 加载用户设置并应用。
|
||
*
|
||
* **在组件中使用**:
|
||
* ```vue
|
||
* <script setup lang="ts">
|
||
* // 直接解构获取需要的数据和方法
|
||
* const { selectedTheme, selectedThemeColor, resetSettings } = useTheme();
|
||
*
|
||
* function handleReset() {
|
||
* // 调用方法来修改主题
|
||
* resetSettings();
|
||
* }
|
||
* </script>
|
||
*
|
||
* <template>
|
||
* <div>
|
||
* <p>当前主题: {{ selectedTheme }}</p>
|
||
* <button @click="handleReset">重置设置</button>
|
||
* </div>
|
||
* </template>
|
||
* ```
|
||
*/
|
||
import { ref, watch, getCurrentInstance, onMounted } from 'vue'
|
||
|
||
// 定义各种选项
|
||
const themes = [
|
||
{ label: '经典', value: 'classic' },
|
||
{ label: '优雅', value: 'elegant' },
|
||
{ label: '简洁', value: 'minimal' }
|
||
]
|
||
|
||
const fonts = [
|
||
{ label: '无衬线', value: 'sans-serif' },
|
||
{ label: '衬线', value: 'serif' },
|
||
{ label: '等宽', value: 'monospace' }
|
||
]
|
||
|
||
const fontSizes = [
|
||
{ label: '更小', value: 'xs' },
|
||
{ label: '稍小', value: 'sm' },
|
||
{ label: '推荐', value: 'base' },
|
||
{ label: '稍大', value: 'lg' },
|
||
{ label: '更大', value: 'xl' }
|
||
]
|
||
|
||
const themeColors = [
|
||
{ label: '经典蓝', value: 'blue', color: '#3B82F6' },
|
||
{ label: '翡翠绿', value: 'emerald', color: '#10B981' },
|
||
{ label: '活力橙', value: 'orange', color: '#F97316' },
|
||
{ label: '柠檬黄', value: 'yellow', color: '#EAB308' },
|
||
{ label: '薰衣紫', value: 'violet', color: '#8B5CF6' },
|
||
{ label: '天空蓝', value: 'sky', color: '#0EA5E9' },
|
||
{ label: '玫瑰金', value: 'rose', color: '#F43F5E' },
|
||
{ label: '橄榄绿', value: 'lime', color: '#84CC16' },
|
||
{ label: '石墨黑', value: 'gray', color: '#6B7280' },
|
||
{ label: '雾烟灰', value: 'slate', color: '#64748B' },
|
||
{ label: '樱花粉', value: 'pink', color: '#EC4899' }
|
||
]
|
||
|
||
const codeThemes = [
|
||
{ label: 'dark', value: 'dark' },
|
||
{ label: 'light', value: 'light' },
|
||
{ label: 'github-dark', value: 'github-dark' },
|
||
{ label: 'github-light', value: 'github-light' },
|
||
{ label: 'one-dark-pro', value: 'one-dark-pro' },
|
||
{ label: 'material-theme', value: 'material-theme' }
|
||
]
|
||
|
||
const captionFormats = [
|
||
{ label: 'title 优先', value: 'title' },
|
||
{ label: 'alt 优先', value: 'alt' },
|
||
{ label: '只显示 title', value: 'title-only' },
|
||
{ label: '只显示 alt', value: 'alt-only' }
|
||
]
|
||
|
||
// 默认设置
|
||
const defaultSettings = {
|
||
theme: 'elegant',
|
||
font: 'sans-serif',
|
||
fontSize: 'base',
|
||
themeColor: 'blue',
|
||
codeTheme: 'dark',
|
||
captionFormat: 'none'
|
||
}
|
||
|
||
// 响应式状态
|
||
const selectedTheme = ref<string>(defaultSettings.theme)
|
||
const selectedFont = ref<string>(defaultSettings.font)
|
||
const selectedFontSize = ref<string>(defaultSettings.fontSize)
|
||
const selectedThemeColor = ref<string>(defaultSettings.themeColor)
|
||
const customColor = ref<string>('#3B82F6')
|
||
const selectedCodeTheme = ref<string>(defaultSettings.codeTheme)
|
||
const selectedCaptionFormat = ref<string>(defaultSettings.captionFormat)
|
||
|
||
// 这是一个单例,确保在整个应用中只有一个主题状态实例
|
||
let isInitialized = false
|
||
|
||
export function useTheme() {
|
||
const colorMode = useColorMode()
|
||
|
||
const applyThemeVariables = () => {
|
||
if (import.meta.client) {
|
||
const root = document.documentElement
|
||
|
||
// 颜色映射到 @nuxt/ui 支持的颜色
|
||
const colorMapping: { [key: string]: string } = {
|
||
blue: 'blue',
|
||
emerald: 'emerald',
|
||
orange: 'orange',
|
||
yellow: 'yellow',
|
||
violet: 'violet',
|
||
sky: 'sky',
|
||
rose: 'rose',
|
||
lime: 'lime',
|
||
gray: 'gray',
|
||
slate: 'slate',
|
||
pink: 'pink'
|
||
}
|
||
|
||
// 使用 @nuxt/ui 的主题系统
|
||
if (selectedThemeColor.value !== 'custom') {
|
||
const mappedColor = colorMapping[selectedThemeColor.value]
|
||
if (mappedColor) {
|
||
const appConfig = useAppConfig()
|
||
if (!appConfig.ui.colors) appConfig.ui.colors = {}
|
||
appConfig.ui.colors.primary = mappedColor
|
||
}
|
||
} else {
|
||
// 处理自定义颜色
|
||
root.style.setProperty('--ui-primary', customColor.value)
|
||
const appConfig = useAppConfig()
|
||
if (!appConfig.ui.colors) appConfig.ui.colors = {}
|
||
appConfig.ui.colors.primary = 'blue'
|
||
}
|
||
|
||
// 字体设置保持不变
|
||
const fontMapping = {
|
||
'sans-serif': 'Roboto, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, "Noto Sans", sans-serif',
|
||
'serif': 'Merriweather, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
|
||
'monospace': 'JetBrains Mono, ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
|
||
}
|
||
root.style.setProperty('--font-family', fontMapping[selectedFont.value as keyof typeof fontMapping])
|
||
|
||
const fontSizeMapping = {
|
||
xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.25rem'
|
||
}
|
||
root.style.setProperty('--font-size-base', fontSizeMapping[selectedFontSize.value as keyof typeof fontSizeMapping])
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用自定义主色调
|
||
*
|
||
* Nuxt UI v3 不支持运行时动态自定义颜色,因此我们使用 CSS 变量覆盖的方法:
|
||
* 1. 保持 Nuxt UI 使用有效的颜色系统(这里使用 'blue' 作为基础)
|
||
* 2. 通过 CSS 变量 --ui-primary 覆盖实际显示的颜色
|
||
* 3. 这样既保证了 Nuxt UI 系统正常工作,又实现了自定义颜色功能
|
||
*/
|
||
const applyCustomColor = () => {
|
||
if (import.meta.client) {
|
||
selectedThemeColor.value = 'custom'
|
||
const root = document.documentElement
|
||
|
||
// 使用 Nuxt UI v3 推荐的 CSS 变量覆盖方法
|
||
// 设置主色调的 CSS 变量,这会影响所有使用 primary 颜色的组件
|
||
root.style.setProperty('--ui-primary', customColor.value)
|
||
|
||
// 保持 Nuxt UI 使用有效的颜色系统
|
||
const appConfig = useAppConfig()
|
||
if (!appConfig.ui.colors) appConfig.ui.colors = {}
|
||
appConfig.ui.colors.primary = 'blue' // 使用有效的颜色名称作为基础
|
||
|
||
// 保存到localStorage
|
||
localStorage.setItem('app-theme-color', 'custom')
|
||
localStorage.setItem('app-custom-color', customColor.value)
|
||
}
|
||
}
|
||
|
||
const initializeTheme = () => {
|
||
if (import.meta.client && !isInitialized) {
|
||
// 从localStorage读取设置
|
||
selectedTheme.value = localStorage.getItem('app-theme') || defaultSettings.theme
|
||
selectedFont.value = localStorage.getItem('app-font') || defaultSettings.font
|
||
selectedFontSize.value = localStorage.getItem('app-font-size') || defaultSettings.fontSize
|
||
selectedThemeColor.value = localStorage.getItem('app-theme-color') || defaultSettings.themeColor
|
||
selectedCodeTheme.value = localStorage.getItem('app-code-theme') || defaultSettings.codeTheme
|
||
selectedCaptionFormat.value = localStorage.getItem('app-caption-format') || defaultSettings.captionFormat
|
||
|
||
// 找到自定义颜色对应的主题色值
|
||
const initialColor = themeColors.find(c => c.value === selectedThemeColor.value)?.color || '#3B82F6'
|
||
customColor.value = localStorage.getItem('app-custom-color') || initialColor
|
||
|
||
// 立即应用主题设置
|
||
applyThemeVariables()
|
||
|
||
// 如果是自定义颜色,需要特殊处理
|
||
if (selectedThemeColor.value === 'custom') {
|
||
applyCustomColor()
|
||
}
|
||
|
||
isInitialized = true
|
||
console.log('主题已初始化')
|
||
}
|
||
}
|
||
|
||
// 确保在客户端时初始化
|
||
if (import.meta.client) {
|
||
// 立即初始化,避免闪烁
|
||
initializeTheme()
|
||
|
||
const instance = getCurrentInstance()
|
||
if (instance) {
|
||
// 在组件 setup 上下文中,安全使用 onMounted
|
||
onMounted(() => {
|
||
applyThemeVariables()
|
||
if (selectedThemeColor.value === 'custom') {
|
||
applyCustomColor()
|
||
}
|
||
})
|
||
} else {
|
||
// 非组件上下文(如插件)中,等待文档就绪后应用
|
||
const run = () => {
|
||
applyThemeVariables()
|
||
if (selectedThemeColor.value === 'custom') {
|
||
applyCustomColor()
|
||
}
|
||
}
|
||
if (document.readyState === 'loading') {
|
||
window.addEventListener('DOMContentLoaded', () => run(), { once: true })
|
||
} else {
|
||
run()
|
||
}
|
||
}
|
||
}
|
||
|
||
const resetSettings = () => {
|
||
selectedTheme.value = defaultSettings.theme
|
||
selectedFont.value = defaultSettings.font
|
||
selectedFontSize.value = defaultSettings.fontSize
|
||
selectedThemeColor.value = defaultSettings.themeColor
|
||
const defaultColor = themeColors.find(c => c.value === defaultSettings.themeColor)?.color || '#3B82F6'
|
||
customColor.value = defaultColor
|
||
selectedCodeTheme.value = defaultSettings.codeTheme
|
||
selectedCaptionFormat.value = defaultSettings.captionFormat
|
||
|
||
colorMode.preference = 'system'
|
||
|
||
applyThemeVariables()
|
||
}
|
||
|
||
// 监听预设主题色的变化,并同步更新自定义颜色选择器的值
|
||
watch(selectedThemeColor, (newThemeColor) => {
|
||
if (newThemeColor !== 'custom') {
|
||
const colorObject = themeColors.find(c => c.value === newThemeColor)
|
||
if (colorObject) {
|
||
customColor.value = colorObject.color
|
||
// 应用颜色
|
||
applyCustomColor()
|
||
}
|
||
}
|
||
})
|
||
|
||
watch([selectedTheme, selectedFont, selectedFontSize, selectedThemeColor, selectedCodeTheme, selectedCaptionFormat, customColor], () => {
|
||
applyThemeVariables()
|
||
|
||
if (import.meta.client) {
|
||
localStorage.setItem('app-theme', selectedTheme.value)
|
||
localStorage.setItem('app-font', selectedFont.value)
|
||
localStorage.setItem('app-font-size', selectedFontSize.value)
|
||
localStorage.setItem('app-theme-color', selectedThemeColor.value)
|
||
localStorage.setItem('app-code-theme', selectedCodeTheme.value)
|
||
localStorage.setItem('app-caption-format', selectedCaptionFormat.value)
|
||
if (selectedThemeColor.value === 'custom') {
|
||
localStorage.setItem('app-custom-color', customColor.value)
|
||
}
|
||
}
|
||
})
|
||
|
||
return {
|
||
initializeTheme,
|
||
themes,
|
||
fonts,
|
||
fontSizes,
|
||
themeColors,
|
||
codeThemes,
|
||
captionFormats,
|
||
selectedTheme,
|
||
selectedFont,
|
||
selectedFontSize,
|
||
selectedThemeColor,
|
||
customColor,
|
||
selectedCodeTheme,
|
||
selectedCaptionFormat,
|
||
applyCustomColor,
|
||
resetSettings,
|
||
colorMode
|
||
}
|
||
}
|