542 lines
12 KiB
JavaScript
542 lines
12 KiB
JavaScript
/**
|
|
* MetonaToast Themes - 主题管理
|
|
* @module themes
|
|
* @version 2.0.0
|
|
* @description 主题系统、自定义主题和主题切换
|
|
*/
|
|
|
|
import { THEMES } from './constants.js';
|
|
import { prefersDark } from './utils.js';
|
|
|
|
// 当前主题状态
|
|
let currentTheme = 'auto';
|
|
let themeListeners = new Set();
|
|
let systemThemeMediaQuery = null;
|
|
let systemThemeListener = null;
|
|
|
|
/**
|
|
* 获取系统主题
|
|
* @returns {string} 系统主题 ('light' 或 'dark')
|
|
*/
|
|
export const getSystemTheme = () => {
|
|
if (typeof window === 'undefined') return 'light';
|
|
return prefersDark() ? 'dark' : 'light';
|
|
};
|
|
|
|
/**
|
|
* 获取主题(别名)
|
|
* @param {string} theme - 主题名称
|
|
* @returns {string} 解析后的主题
|
|
*/
|
|
export const getTheme = (theme) => {
|
|
return resolveTheme(theme);
|
|
};
|
|
|
|
/**
|
|
* 解析主题
|
|
* @param {string} theme - 主题名称
|
|
* @returns {string} 解析后的主题
|
|
*/
|
|
export const resolveTheme = (theme) => {
|
|
if (theme === 'auto') {
|
|
// 优先使用用户设置的全局主题,否则使用系统主题
|
|
if (currentTheme && currentTheme !== 'auto') {
|
|
return currentTheme;
|
|
}
|
|
return getSystemTheme();
|
|
}
|
|
return theme;
|
|
};
|
|
|
|
/**
|
|
* 获取主题配置
|
|
* @param {string} theme - 主题名称
|
|
* @returns {Object} 主题配置
|
|
*/
|
|
export const getThemeConfig = (theme) => {
|
|
const resolved = resolveTheme(theme);
|
|
return THEMES[resolved] || THEMES.light;
|
|
};
|
|
|
|
/**
|
|
* 应用主题
|
|
* @param {string} theme - 主题名称
|
|
*/
|
|
export const applyTheme = (theme) => {
|
|
currentTheme = theme;
|
|
const resolved = resolveTheme(theme);
|
|
|
|
// 应用主题到文档
|
|
if (typeof document !== 'undefined') {
|
|
document.documentElement.setAttribute('data-met-theme', resolved);
|
|
document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto');
|
|
document.documentElement.classList.add(`met-theme-${resolved}`);
|
|
|
|
// 设置CSS变量
|
|
const config = getThemeConfig(theme);
|
|
setThemeVariables(config);
|
|
}
|
|
|
|
// 通知监听器
|
|
notifyThemeListeners(theme, resolved);
|
|
};
|
|
|
|
/**
|
|
* 设置主题CSS变量
|
|
* @param {Object} config - 主题配置
|
|
*/
|
|
export const setThemeVariables = (config) => {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
root.style.setProperty('--met-bg', config.bg);
|
|
root.style.setProperty('--met-text', config.text);
|
|
root.style.setProperty('--met-border', config.border);
|
|
root.style.setProperty('--met-shadow', config.shadow);
|
|
root.style.setProperty('--met-hover-shadow', config.hoverShadow);
|
|
root.style.setProperty('--met-progress-bg', config.progressBg);
|
|
root.style.setProperty('--met-close-hover-bg', config.closeHoverBg);
|
|
|
|
// 设置颜色变量
|
|
root.style.setProperty('--met-success', '#10b981');
|
|
root.style.setProperty('--met-error', '#ef4444');
|
|
root.style.setProperty('--met-warning', '#f59e0b');
|
|
root.style.setProperty('--met-info', '#3b82f6');
|
|
root.style.setProperty('--met-loading', '#6366f1');
|
|
};
|
|
|
|
/**
|
|
* 清除主题CSS变量
|
|
*/
|
|
export const clearThemeVariables = () => {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
const variables = [
|
|
'--met-bg',
|
|
'--met-text',
|
|
'--met-border',
|
|
'--met-shadow',
|
|
'--met-hover-shadow',
|
|
'--met-progress-bg',
|
|
'--met-close-hover-bg',
|
|
'--met-success',
|
|
'--met-error',
|
|
'--met-warning',
|
|
'--met-info',
|
|
'--met-loading',
|
|
];
|
|
|
|
variables.forEach((variable) => {
|
|
root.style.removeProperty(variable);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 获取当前主题
|
|
* @returns {string} 当前主题
|
|
*/
|
|
export const getCurrentTheme = () => {
|
|
return currentTheme;
|
|
};
|
|
|
|
/**
|
|
* 获取解析后的主题
|
|
* @returns {string} 解析后的主题
|
|
*/
|
|
export const getResolvedTheme = () => {
|
|
return resolveTheme(currentTheme);
|
|
};
|
|
|
|
/**
|
|
* 切换主题
|
|
* @param {string} theme - 新主题
|
|
*/
|
|
export const switchTheme = (theme) => {
|
|
applyTheme(theme);
|
|
saveTheme(theme);
|
|
};
|
|
|
|
/**
|
|
* 切换亮色/暗色主题
|
|
*/
|
|
export const toggleTheme = () => {
|
|
const resolved = getResolvedTheme();
|
|
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
|
switchTheme(newTheme);
|
|
};
|
|
|
|
/**
|
|
* 重置为自动主题
|
|
*/
|
|
export const resetToAuto = () => {
|
|
switchTheme('auto');
|
|
};
|
|
|
|
/**
|
|
* 保存主题到本地存储
|
|
* @param {string} theme - 主题名称
|
|
*/
|
|
export const saveTheme = (theme) => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
localStorage.setItem('metona-toast-theme', theme);
|
|
} catch (e) {
|
|
console.warn('Failed to save theme:', e);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 从本地存储加载主题
|
|
* @returns {string} 主题名称
|
|
*/
|
|
export const loadTheme = () => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
return localStorage.getItem('metona-toast-theme') || 'auto';
|
|
} catch (e) {
|
|
console.warn('Failed to load theme:', e);
|
|
}
|
|
}
|
|
return 'auto';
|
|
};
|
|
|
|
/**
|
|
* 初始化主题系统
|
|
*/
|
|
export const initTheme = () => {
|
|
// 加载保存的主题
|
|
const savedTheme = loadTheme();
|
|
applyTheme(savedTheme);
|
|
|
|
// 监听系统主题变化
|
|
watchSystemTheme();
|
|
};
|
|
|
|
/**
|
|
* 监听系统主题变化
|
|
*/
|
|
export const watchSystemTheme = () => {
|
|
if (typeof window === 'undefined' || !window.matchMedia) return;
|
|
|
|
// 移除旧的监听器
|
|
if (systemThemeMediaQuery && systemThemeListener) {
|
|
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
|
}
|
|
|
|
// 创建新的监听器
|
|
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
systemThemeListener = (e) => {
|
|
if (currentTheme === 'auto') {
|
|
applyTheme('auto');
|
|
}
|
|
};
|
|
|
|
systemThemeMediaQuery.addEventListener('change', systemThemeListener);
|
|
};
|
|
|
|
/**
|
|
* 停止监听系统主题变化
|
|
*/
|
|
export const unwatchSystemTheme = () => {
|
|
if (systemThemeMediaQuery && systemThemeListener) {
|
|
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
|
systemThemeMediaQuery = null;
|
|
systemThemeListener = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 添加主题监听器
|
|
* @param {Function} listener - 监听器函数
|
|
* @returns {Function} 移除监听器函数
|
|
*/
|
|
export const addThemeListener = (listener) => {
|
|
themeListeners.add(listener);
|
|
|
|
return () => {
|
|
themeListeners.delete(listener);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 移除主题监听器
|
|
* @param {Function} listener - 监听器函数
|
|
*/
|
|
export const removeThemeListener = (listener) => {
|
|
themeListeners.delete(listener);
|
|
};
|
|
|
|
/**
|
|
* 通知主题监听器
|
|
* @param {string} theme - 主题名称
|
|
* @param {string} resolved - 解析后的主题
|
|
*/
|
|
const notifyThemeListeners = (theme, resolved) => {
|
|
themeListeners.forEach((listener) => {
|
|
try {
|
|
listener(theme, resolved);
|
|
} catch (e) {
|
|
console.error('Theme listener error:', e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 清除所有主题监听器
|
|
*/
|
|
export const clearThemeListeners = () => {
|
|
themeListeners.clear();
|
|
};
|
|
|
|
/**
|
|
* 注册自定义主题
|
|
* @param {string} name - 主题名称
|
|
* @param {Object} config - 主题配置
|
|
*/
|
|
export const registerTheme = (name, config) => {
|
|
THEMES[name] = {
|
|
bg: config.bg || THEMES.light.bg,
|
|
text: config.text || THEMES.light.text,
|
|
border: config.border || THEMES.light.border,
|
|
shadow: config.shadow || THEMES.light.shadow,
|
|
hoverShadow: config.hoverShadow || THEMES.light.hoverShadow,
|
|
progressBg: config.progressBg || THEMES.light.progressBg,
|
|
closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 注销自定义主题
|
|
* @param {string} name - 主题名称
|
|
*/
|
|
export const unregisterTheme = (name) => {
|
|
if (name === 'light' || name === 'dark' || name === 'auto') {
|
|
console.warn('Cannot unregister built-in theme:', name);
|
|
return;
|
|
}
|
|
|
|
delete THEMES[name];
|
|
};
|
|
|
|
/**
|
|
* 获取所有主题
|
|
* @returns {Object} 主题映射
|
|
*/
|
|
export const getAllThemes = () => {
|
|
return { ...THEMES };
|
|
};
|
|
|
|
/**
|
|
* 获取主题名称列表
|
|
* @returns {Array} 主题名称数组
|
|
*/
|
|
export const getThemeNames = () => {
|
|
return Object.keys(THEMES);
|
|
};
|
|
|
|
/**
|
|
* 检查主题是否存在
|
|
* @param {string} name - 主题名称
|
|
* @returns {boolean} 是否存在
|
|
*/
|
|
export const hasTheme = (name) => {
|
|
return name in THEMES;
|
|
};
|
|
|
|
/**
|
|
* 获取主题预览
|
|
* @param {string} theme - 主题名称
|
|
* @returns {Object} 主题预览
|
|
*/
|
|
export const getThemePreview = (theme) => {
|
|
const config = getThemeConfig(theme);
|
|
const resolved = resolveTheme(theme);
|
|
|
|
return {
|
|
name: theme,
|
|
resolved,
|
|
colors: {
|
|
background: config.bg,
|
|
text: config.text,
|
|
border: config.border,
|
|
},
|
|
isDark: resolved === 'dark',
|
|
isLight: resolved === 'light',
|
|
isAuto: theme === 'auto',
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 生成主题CSS
|
|
* @param {string} theme - 主题名称
|
|
* @returns {string} CSS字符串
|
|
*/
|
|
export const generateThemeCSS = (theme) => {
|
|
const config = getThemeConfig(theme);
|
|
const resolved = resolveTheme(theme);
|
|
|
|
return `
|
|
.met-theme-${resolved} {
|
|
--met-bg: ${config.bg};
|
|
--met-text: ${config.text};
|
|
--met-border: ${config.border};
|
|
--met-shadow: ${config.shadow};
|
|
--met-hover-shadow: ${config.hoverShadow};
|
|
--met-progress-bg: ${config.progressBg};
|
|
--met-close-hover-bg: ${config.closeHoverBg};
|
|
}
|
|
`;
|
|
};
|
|
|
|
/**
|
|
* 应用主题CSS
|
|
* @param {string} theme - 主题名称
|
|
*/
|
|
export const applyThemeCSS = (theme) => {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const css = generateThemeCSS(theme);
|
|
const styleId = 'metona-toast-theme-css';
|
|
|
|
// 移除旧样式
|
|
const oldStyle = document.getElementById(styleId);
|
|
if (oldStyle) {
|
|
oldStyle.remove();
|
|
}
|
|
|
|
// 添加新样式
|
|
const style = document.createElement('style');
|
|
style.id = styleId;
|
|
style.textContent = css;
|
|
document.head.appendChild(style);
|
|
};
|
|
|
|
/**
|
|
* 移除主题CSS
|
|
*/
|
|
export const removeThemeCSS = () => {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const styleId = 'metona-toast-theme-css';
|
|
const style = document.getElementById(styleId);
|
|
if (style) {
|
|
style.remove();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 主题工具 — 代理层,直接引用模块函数
|
|
*/
|
|
export const themeUtils = {
|
|
getSystemTheme,
|
|
resolveTheme,
|
|
getThemeConfig,
|
|
applyTheme,
|
|
getCurrentTheme,
|
|
getResolvedTheme,
|
|
switchTheme,
|
|
toggleTheme,
|
|
resetToAuto,
|
|
initTheme,
|
|
watchSystemTheme,
|
|
unwatchSystemTheme,
|
|
addThemeListener,
|
|
removeThemeListener,
|
|
clearThemeListeners,
|
|
registerTheme,
|
|
unregisterTheme,
|
|
getAllThemes,
|
|
getThemeNames,
|
|
hasTheme,
|
|
getThemePreview,
|
|
generateThemeCSS,
|
|
applyThemeCSS,
|
|
removeThemeCSS,
|
|
saveTheme,
|
|
loadTheme,
|
|
};
|
|
|
|
/**
|
|
* 预设主题
|
|
*/
|
|
export const presetThemes = {
|
|
/**
|
|
* 浅色主题
|
|
*/
|
|
light: {
|
|
name: '浅色',
|
|
description: '明亮清晰的主题',
|
|
config: THEMES.light,
|
|
},
|
|
|
|
/**
|
|
* 深色主题
|
|
*/
|
|
dark: {
|
|
name: '深色',
|
|
description: '护眼舒适的暗色主题',
|
|
config: THEMES.dark,
|
|
},
|
|
|
|
/**
|
|
* 自动主题
|
|
*/
|
|
auto: {
|
|
name: '自动',
|
|
description: '跟随系统主题设置',
|
|
config: 'auto',
|
|
},
|
|
|
|
warm: {
|
|
name: '暖色',
|
|
description: '温馨舒适的暖色主题',
|
|
config: THEMES.warm,
|
|
},
|
|
};
|
|
|
|
// 注册预设主题
|
|
Object.entries(presetThemes).forEach(([name, theme]) => {
|
|
if (name !== 'auto' && theme.config !== 'auto') {
|
|
THEMES[name] = theme.config;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 创建主题管理器
|
|
* @returns {Object} 主题管理器
|
|
*/
|
|
export const createThemeManager = () => {
|
|
return {
|
|
getSystemTheme,
|
|
resolveTheme,
|
|
getThemeConfig,
|
|
applyTheme,
|
|
getCurrentTheme,
|
|
getResolvedTheme,
|
|
switchTheme,
|
|
toggleTheme,
|
|
resetToAuto,
|
|
initTheme,
|
|
watchSystemTheme,
|
|
unwatchSystemTheme,
|
|
addThemeListener,
|
|
removeThemeListener,
|
|
clearThemeListeners,
|
|
registerTheme,
|
|
unregisterTheme,
|
|
getAllThemes,
|
|
getThemeNames,
|
|
hasTheme,
|
|
getThemePreview,
|
|
generateThemeCSS,
|
|
applyThemeCSS,
|
|
removeThemeCSS,
|
|
saveTheme,
|
|
loadTheme,
|
|
};
|
|
};
|
|
|
|
export { themeUtils as default };
|