import type { DeepPartial } from '@vben-core/typings'; import type { InitialOptions, Preferences } from './types'; import { markRaw, reactive, readonly, watch } from 'vue'; import { isMacOs, merge, StorageManager } from '@vben-core/toolkit'; import { breakpointsTailwind, useBreakpoints, useDebounceFn, } from '@vueuse/core'; import { defaultPreferences } from './config'; import { updateCSSVariables } from './update-css-variables'; const STORAGE_KEY = 'preferences'; const STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`; const STORAGE_KEY_THEME = `${STORAGE_KEY}-theme`; function isDarkTheme(theme: string) { let dark = theme === 'dark'; if (theme === 'auto') { dark = window.matchMedia('(prefers-color-scheme: dark)').matches; } return dark; } class PreferenceManager { private cache: null | StorageManager = null; // private flattenedState: Flatten; private initialPreferences: Preferences = defaultPreferences; private isInitialized: boolean = false; private savePreferences: (preference: Preferences) => void; private state: Preferences = reactive({ ...this.loadPreferences(), }); constructor() { this.cache = new StorageManager(); this.savePreferences = useDebounceFn( (preference: Preferences) => this._savePreferences(preference), 100, ); } /** * 保存偏好设置 * @param {Preferences} preference - 需要保存的偏好设置 */ private _savePreferences(preference: Preferences) { this.cache?.setItem(STORAGE_KEY, preference); this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale); this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode); } /** * 处理更新的键值 * 根据更新的键值执行相应的操作。 * * @param {DeepPartial} updates - 部分更新的偏好设置 */ private handleUpdates(updates: DeepPartial) { const themeUpdates = updates.theme || {}; const appUpdates = updates.app || {}; if (themeUpdates && Object.keys(themeUpdates).length > 0) { updateCSSVariables(this.state); } if ( Reflect.has(appUpdates, 'colorGrayMode') || Reflect.has(appUpdates, 'colorWeakMode') ) { this.updateColorMode(this.state); } } private initPlatform() { const dom = document.documentElement; dom.dataset.platform = isMacOs() ? 'macOs' : 'window'; } /** * 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。 */ private loadCachedPreferences() { return this.cache?.getItem(STORAGE_KEY); } /** * 加载偏好设置 * @returns {Preferences} 加载的偏好设置 */ private loadPreferences(): Preferences { return this.loadCachedPreferences() || { ...defaultPreferences }; } /** * 监听状态和系统偏好设置的变化。 */ private setupWatcher() { if (this.isInitialized) { return; } // 监听断点,判断是否移动端 const breakpoints = useBreakpoints(breakpointsTailwind); const isMobile = breakpoints.smaller('md'); watch( () => isMobile.value, (val) => { this.updatePreferences({ app: { isMobile: val }, }); }, { immediate: true }, ); // 监听系统主题偏好设置变化 window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', ({ matches: isDark }) => { this.updatePreferences({ theme: { mode: isDark ? 'dark' : 'light' }, }); updateCSSVariables(this.state); }); } /** * 更新页面颜色模式(灰色、色弱) * @param preference */ private updateColorMode(preference: Preferences) { if (preference.app) { const { colorGrayMode, colorWeakMode } = preference.app; const dom = document.documentElement; const COLOR_WEAK = 'invert-mode'; const COLOR_GRAY = 'grayscale-mode'; colorWeakMode ? dom.classList.add(COLOR_WEAK) : dom.classList.remove(COLOR_WEAK); colorGrayMode ? dom.classList.add(COLOR_GRAY) : dom.classList.remove(COLOR_GRAY); } } clearCache() { [STORAGE_KEY, STORAGE_KEY_LOCALE, STORAGE_KEY_THEME].forEach((key) => { this.cache?.removeItem(key); }); } public getInitialPreferences() { return this.initialPreferences; } public getPreferences() { return readonly(this.state); } /** * 覆盖偏好设置 * overrides 要覆盖的偏好设置 * namespace 命名空间 */ public async initPreferences({ namespace, overrides }: InitialOptions) { // 是否初始化过 if (this.isInitialized) { return; } // 初始化存储管理器 this.cache = new StorageManager({ prefix: namespace }); // 合并初始偏好设置 this.initialPreferences = merge({}, overrides, defaultPreferences); // 加载并合并当前存储的偏好设置 const mergedPreference = merge( {}, overrides, this.loadCachedPreferences() || defaultPreferences, ); // 更新偏好设置 this.updatePreferences(mergedPreference); this.setupWatcher(); this.initPlatform(); // 标记为已初始化 this.isInitialized = true; } /** * 重置偏好设置 * 偏好设置将被重置为初始值,并从 localStorage 中移除。 * * @example * 假设 initialPreferences 为 { theme: 'light', language: 'en' } * 当前 state 为 { theme: 'dark', language: 'fr' } * this.resetPreferences(); * 调用后,state 将被重置为 { theme: 'light', language: 'en' } * 并且 localStorage 中的对应项将被移除 */ resetPreferences() { // 将状态重置为初始偏好设置 Object.assign(this.state, this.initialPreferences); // 保存重置后的偏好设置 this.savePreferences(this.state); // 从存储中移除偏好设置项 [STORAGE_KEY, STORAGE_KEY_THEME, STORAGE_KEY_LOCALE].forEach((key) => { this.cache?.removeItem(key); }); this.updatePreferences(this.state); } /** * 更新偏好设置 * @param updates - 要更新的偏好设置 */ public updatePreferences(updates: DeepPartial) { const mergedState = merge({}, updates, markRaw(this.state)); Object.assign(this.state, mergedState); // 根据更新的键值执行相应的操作 this.handleUpdates(updates); this.savePreferences(this.state); } } const preferencesManager = new PreferenceManager(); export { isDarkTheme, PreferenceManager, preferencesManager };