|
|
|
@ -16,168 +16,167 @@ import { |
|
|
|
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`; |
|
|
|
const STORAGE_KEYS = { |
|
|
|
MAIN: 'preferences', |
|
|
|
LOCALE: 'preferences-locale', |
|
|
|
THEME: 'preferences-theme', |
|
|
|
} as const; |
|
|
|
|
|
|
|
class PreferenceManager { |
|
|
|
private cache: null | StorageManager = null; |
|
|
|
// private flattenedState: Flatten<Preferences>;
|
|
|
|
private cache: StorageManager; |
|
|
|
private debouncedSave: (preference: Preferences) => void; |
|
|
|
private initialPreferences: Preferences = defaultPreferences; |
|
|
|
private isInitialized: boolean = false; |
|
|
|
private savePreferences: (preference: Preferences) => void; |
|
|
|
private state: Preferences = reactive<Preferences>({ |
|
|
|
...this.loadPreferences(), |
|
|
|
}); |
|
|
|
private isInitialized = false; |
|
|
|
private state: Preferences; |
|
|
|
|
|
|
|
constructor() { |
|
|
|
this.cache = new StorageManager(); |
|
|
|
|
|
|
|
// 避免频繁的操作缓存
|
|
|
|
this.savePreferences = useDebounceFn( |
|
|
|
(preference: Preferences) => this._savePreferences(preference), |
|
|
|
this.state = reactive<Preferences>( |
|
|
|
this.loadFromCache() || { ...defaultPreferences }, |
|
|
|
); |
|
|
|
this.debouncedSave = useDebounceFn( |
|
|
|
(preference) => this.saveToCache(preference), |
|
|
|
150, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
clearCache() { |
|
|
|
[STORAGE_KEY, STORAGE_KEY_LOCALE, STORAGE_KEY_THEME].forEach((key) => { |
|
|
|
this.cache?.removeItem(key); |
|
|
|
}); |
|
|
|
} |
|
|
|
/** |
|
|
|
* 清除所有缓存的偏好设置 |
|
|
|
*/ |
|
|
|
clearCache = () => { |
|
|
|
Object.values(STORAGE_KEYS).forEach((key) => this.cache.removeItem(key)); |
|
|
|
}; |
|
|
|
|
|
|
|
public getInitialPreferences() { |
|
|
|
/** |
|
|
|
* 获取初始化偏好设置 |
|
|
|
*/ |
|
|
|
getInitialPreferences = () => { |
|
|
|
return this.initialPreferences; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
public getPreferences() { |
|
|
|
/** |
|
|
|
* 获取当前偏好设置(只读) |
|
|
|
*/ |
|
|
|
getPreferences = () => { |
|
|
|
return readonly(this.state); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* 覆盖偏好设置 |
|
|
|
* overrides 要覆盖的偏好设置 |
|
|
|
* namespace 命名空间 |
|
|
|
* 初始化偏好设置 |
|
|
|
* @param namespace - 命名空间,用于隔离不同应用的配置 |
|
|
|
* @param overrides - 要覆盖的偏好设置 |
|
|
|
*/ |
|
|
|
public async initPreferences({ namespace, overrides }: InitialOptions) { |
|
|
|
// 是否初始化过
|
|
|
|
initPreferences = async ({ namespace, overrides }: InitialOptions) => { |
|
|
|
// 防止重复初始化
|
|
|
|
if (this.isInitialized) { |
|
|
|
return; |
|
|
|
} |
|
|
|
// 初始化存储管理器
|
|
|
|
|
|
|
|
// 使用命名空间初始化存储管理器
|
|
|
|
this.cache = new StorageManager({ prefix: namespace }); |
|
|
|
|
|
|
|
// 合并初始偏好设置
|
|
|
|
this.initialPreferences = merge({}, overrides, defaultPreferences); |
|
|
|
|
|
|
|
// 加载并合并当前存储的偏好设置
|
|
|
|
// 加载缓存的偏好设置并与初始配置合并
|
|
|
|
const cachedPreferences = this.loadFromCache() || {}; |
|
|
|
const mergedPreference = merge( |
|
|
|
{}, |
|
|
|
// overrides,
|
|
|
|
this.loadCachedPreferences() || {}, |
|
|
|
cachedPreferences, |
|
|
|
this.initialPreferences, |
|
|
|
); |
|
|
|
|
|
|
|
// 更新偏好设置
|
|
|
|
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() { |
|
|
|
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); |
|
|
|
} |
|
|
|
|
|
|
|
// 保存偏好设置至缓存
|
|
|
|
this.saveToCache(this.state); |
|
|
|
|
|
|
|
// 直接触发 UI 更新
|
|
|
|
this.handleUpdates(this.state); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* 更新偏好设置 |
|
|
|
* @param updates - 要更新的偏好设置 |
|
|
|
*/ |
|
|
|
public updatePreferences(updates: DeepPartial<Preferences>) { |
|
|
|
updatePreferences = (updates: DeepPartial<Preferences>) => { |
|
|
|
// 深度合并更新内容和当前状态
|
|
|
|
const mergedState = merge({}, updates, markRaw(this.state)); |
|
|
|
|
|
|
|
Object.assign(this.state, mergedState); |
|
|
|
|
|
|
|
// 根据更新的键值执行相应的操作
|
|
|
|
// 根据更新的值执行更新
|
|
|
|
this.handleUpdates(updates); |
|
|
|
this.savePreferences(this.state); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 保存偏好设置 |
|
|
|
* @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); |
|
|
|
} |
|
|
|
// 保存到缓存
|
|
|
|
this.debouncedSave(this.state); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理更新的键值 |
|
|
|
* 根据更新的键值执行相应的操作。 |
|
|
|
* @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置 |
|
|
|
* 处理更新 |
|
|
|
* @param updates - 更新的偏好设置 |
|
|
|
*/ |
|
|
|
private handleUpdates(updates: DeepPartial<Preferences>) { |
|
|
|
const themeUpdates = updates.theme || {}; |
|
|
|
const appUpdates = updates.app || {}; |
|
|
|
const { theme, app } = updates; |
|
|
|
|
|
|
|
if ( |
|
|
|
(themeUpdates && Object.keys(themeUpdates).length > 0) || |
|
|
|
Reflect.has(themeUpdates, 'fontSize') |
|
|
|
theme && |
|
|
|
(Object.keys(theme).length > 0 || Reflect.has(theme, 'fontSize')) |
|
|
|
) { |
|
|
|
updateCSSVariables(this.state); |
|
|
|
} |
|
|
|
|
|
|
|
if ( |
|
|
|
Reflect.has(appUpdates, 'colorGrayMode') || |
|
|
|
Reflect.has(appUpdates, 'colorWeakMode') |
|
|
|
app && |
|
|
|
(Reflect.has(app, 'colorGrayMode') || Reflect.has(app, 'colorWeakMode')) |
|
|
|
) { |
|
|
|
this.updateColorMode(this.state); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 初始化平台标识 |
|
|
|
*/ |
|
|
|
private initPlatform() { |
|
|
|
const dom = document.documentElement; |
|
|
|
dom.dataset.platform = isMacOs() ? 'macOs' : 'window'; |
|
|
|
document.documentElement.dataset.platform = isMacOs() ? 'macOs' : 'window'; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。 |
|
|
|
* 从缓存加载偏好设置 |
|
|
|
* @returns 缓存的偏好设置,如果不存在则返回 null |
|
|
|
*/ |
|
|
|
private loadCachedPreferences() { |
|
|
|
return this.cache?.getItem<Preferences>(STORAGE_KEY); |
|
|
|
private loadFromCache(): null | Preferences { |
|
|
|
return this.cache.getItem<Preferences>(STORAGE_KEYS.MAIN); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 加载偏好设置 |
|
|
|
* @returns {Preferences} 加载的偏好设置 |
|
|
|
* 保存偏好设置到缓存 |
|
|
|
* @param preference - 要保存的偏好设置 |
|
|
|
*/ |
|
|
|
private loadPreferences(): Preferences { |
|
|
|
return this.loadCachedPreferences() || { ...defaultPreferences }; |
|
|
|
private saveToCache(preference: Preferences) { |
|
|
|
this.cache.setItem(STORAGE_KEYS.MAIN, preference); |
|
|
|
this.cache.setItem(STORAGE_KEYS.LOCALE, preference.app.locale); |
|
|
|
this.cache.setItem(STORAGE_KEYS.THEME, preference.theme.mode); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 监听状态和系统偏好设置的变化。 |
|
|
|
* 监听状态和系统偏好设置的变化 |
|
|
|
*/ |
|
|
|
private setupWatcher() { |
|
|
|
if (this.isInitialized) { |
|
|
|
@ -187,6 +186,7 @@ class PreferenceManager { |
|
|
|
// 监听断点,判断是否移动端
|
|
|
|
const breakpoints = useBreakpoints(breakpointsTailwind); |
|
|
|
const isMobile = breakpoints.smaller('md'); |
|
|
|
|
|
|
|
watch( |
|
|
|
() => isMobile.value, |
|
|
|
(val) => { |
|
|
|
@ -201,12 +201,13 @@ class PreferenceManager { |
|
|
|
window |
|
|
|
.matchMedia('(prefers-color-scheme: dark)') |
|
|
|
.addEventListener('change', ({ matches: isDark }) => { |
|
|
|
// 如果偏好设置中主题模式为auto,则跟随系统更新
|
|
|
|
// 仅在自动模式下跟随系统主题
|
|
|
|
if (this.state.theme.mode === 'auto') { |
|
|
|
// 先应用实际的主题
|
|
|
|
this.updatePreferences({ |
|
|
|
theme: { mode: isDark ? 'dark' : 'light' }, |
|
|
|
}); |
|
|
|
// 恢复为auto模式
|
|
|
|
// 再恢复为 auto 模式,保持跟随系统的状态
|
|
|
|
this.updatePreferences({ |
|
|
|
theme: { mode: 'auto' }, |
|
|
|
}); |
|
|
|
@ -216,19 +217,17 @@ class PreferenceManager { |
|
|
|
|
|
|
|
/** |
|
|
|
* 更新页面颜色模式(灰色、色弱) |
|
|
|
* @param preference |
|
|
|
* @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'; |
|
|
|
dom.classList.toggle(COLOR_WEAK, colorWeakMode); |
|
|
|
dom.classList.toggle(COLOR_GRAY, colorGrayMode); |
|
|
|
} |
|
|
|
|
|
|
|
dom.classList.toggle('invert-mode', colorWeakMode); |
|
|
|
dom.classList.toggle('grayscale-mode', colorGrayMode); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const preferencesManager = new PreferenceManager(); |
|
|
|
|
|
|
|
export { PreferenceManager, preferencesManager }; |
|
|
|
|