Files
estel_docs/app/composables/useTheme.ts
2025-07-24 23:09:39 +08:00

274 lines
9.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.

/**
* @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 } 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 initializeTheme = () => {
if (import.meta.client && !isInitialized) {
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();
isInitialized = true;
console.log('主题已初始化');
}
};
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;
}
}
// 字体设置保持不变
const fontMapping = {
'sans-serif': 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
'serif': 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
'monospace': '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]);
root.classList.remove('theme-classic', 'theme-elegant', 'theme-minimal');
root.classList.add(`theme-${selectedTheme.value}`);
}
};
/**
* 应用自定义主色调
*
* 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'; // 使用有效的颜色名称作为基础
}
};
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
};
}