| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- import type { DeepPartial } from '@vben-core/typings';
- import type {
- CustomPreferencesField,
- CustomPreferencesRecord,
- InitialOptions,
- Preferences,
- PreferencesExtension,
- } from './types';
- import { markRaw, reactive, readonly, watch } from 'vue';
- import { StorageManager } from '@vben-core/shared/cache';
- import { isMacOs, merge } from '@vben-core/shared/utils';
- import {
- breakpointsTailwind,
- useBreakpoints,
- useDebounceFn,
- } from '@vueuse/core';
- import { defaultPreferences } from './config';
- import { updateCSSVariables } from './update-css-variables';
- const STORAGE_KEYS = {
- CUSTOM: 'preferences-custom',
- MAIN: 'preferences',
- LOCALE: 'preferences-locale',
- THEME: 'preferences-theme',
- } as const;
- class PreferenceManager {
- private cache: StorageManager;
- private customPreferencesExtension: null | PreferencesExtension<any> = null;
- private customState = reactive<CustomPreferencesRecord>({});
- private debouncedSave: () => void;
- private initialCustomPreferences: CustomPreferencesRecord = {};
- private initialPreferences: Preferences = defaultPreferences;
- private isInitialized = false;
- private state: Preferences;
- constructor() {
- this.cache = new StorageManager();
- // 构造函数不再同步读取缓存,使用默认值初始化
- // 真正的缓存加载在 initPreferences 中完成(已经是 async)
- this.state = reactive<Preferences>({ ...defaultPreferences });
- this.debouncedSave = useDebounceFn(() => this.saveToCache(), 150);
- }
- /**
- * 清除所有缓存的偏好设置
- */
- clearCache = async () => {
- await Promise.all(
- Object.values(STORAGE_KEYS).map((key) => this.cache.removeItem(key)),
- );
- };
- /**
- * 获取扩展偏好设置
- */
- getCustomPreferences = <
- TCustomPreferences extends object = CustomPreferencesRecord,
- >() => {
- return readonly(this.customState) as Readonly<TCustomPreferences>;
- };
- /**
- * 获取初始化扩展偏好设置
- */
- getInitialCustomPreferences = <
- TCustomPreferences extends object = CustomPreferencesRecord,
- >() => {
- return this.cloneValue(
- this.initialCustomPreferences,
- ) as Readonly<TCustomPreferences>;
- };
- /**
- * 获取初始化偏好设置
- */
- getInitialPreferences = () => {
- return this.initialPreferences;
- };
- /**
- * 获取当前偏好设置(只读)
- */
- getPreferences = () => {
- return readonly(this.state);
- };
- /**
- * 获取扩展偏好设置配置
- */
- getPreferencesExtension = <
- TCustomPreferences extends object = CustomPreferencesRecord,
- >() => {
- return this.customPreferencesExtension
- ? (this.cloneValue(this.customPreferencesExtension) as Readonly<
- PreferencesExtension<TCustomPreferences>
- >)
- : null;
- };
- /**
- * 初始化偏好设置
- * @param options - 初始化配置项
- * @param options.namespace - 命名空间,用于隔离不同应用的配置
- * @param options.overrides - 要覆盖的偏好设置
- */
- initPreferences = async <
- TCustomPreferences extends object = CustomPreferencesRecord,
- >({
- namespace,
- overrides,
- extension,
- }: InitialOptions<TCustomPreferences>) => {
- // 防止重复初始化
- if (this.isInitialized) {
- return;
- }
- // 使用命名空间初始化存储管理器
- this.cache = new StorageManager({ prefix: namespace });
- // 合并初始偏好设置:前面的对象优先,后面的对象仅补齐缺失字段
- this.initialPreferences = merge({}, overrides, defaultPreferences);
- this.customPreferencesExtension = extension ?? null;
- this.initialCustomPreferences = this.resolveCustomPreferencesDefaults(
- this.customPreferencesExtension,
- );
- // 加载缓存的偏好设置,并仅用缓存补齐初始化配置中未显式设置的字段
- const cachedPreferences = (await this.loadFromCache()) || {};
- const mergedPreference = merge(
- {},
- this.initialPreferences, // 初始化配置优先,缓存仅补齐缺失字段
- cachedPreferences,
- );
- // 更新偏好设置
- this.updatePreferences(mergedPreference);
- const cachedCustom = (await this.loadCustomFromCache()) || {};
- this.replaceCustomPreferences(
- merge(
- {},
- this.sanitizeCustomPreferences(cachedCustom),
- this.initialCustomPreferences,
- ),
- );
- await this.saveToCache();
- // 设置监听器
- this.setupWatcher();
- // 初始化平台标识
- this.initPlatform();
- this.isInitialized = true;
- };
- /**
- * 重置偏好设置到初始状态
- */
- resetPreferences = async () => {
- // 将状态重置为初始偏好设置
- Object.assign(this.state, this.initialPreferences);
- this.replaceCustomPreferences(this.initialCustomPreferences);
- // 保存偏好设置至缓存
- await this.saveToCache();
- // 直接触发 UI 更新
- this.handleUpdates(this.state);
- };
- /**
- * 更新扩展偏好设置
- * @param updates - 要更新的扩展偏好设置
- */
- updateCustomPreferences = (updates: DeepPartial<object>) => {
- if (!this.customPreferencesExtension) {
- return;
- }
- const sanitizedUpdates = this.sanitizeCustomPreferences(
- updates as DeepPartial<CustomPreferencesRecord>,
- );
- if (Object.keys(sanitizedUpdates).length === 0) {
- return;
- }
- this.replaceCustomPreferences(
- merge({}, sanitizedUpdates, markRaw(this.customState)),
- );
- this.debouncedSave();
- };
- /**
- * 更新偏好设置
- * @param updates - 要更新的偏好设置
- */
- updatePreferences = (updates: DeepPartial<Preferences>) => {
- // 深度合并更新内容和当前状态
- const mergedState = merge({}, updates, markRaw(this.state));
- Object.assign(this.state, mergedState);
- // 根据更新的值执行更新
- this.handleUpdates(updates);
- // 保存到缓存(fire-and-forget,通过 debounce 控制频率)
- this.debouncedSave();
- };
- private cloneValue<T>(value: T): T {
- if (Array.isArray(value)) {
- return value.map((item) => this.cloneValue(item)) as T;
- }
- if (value && typeof value === 'object') {
- return Object.fromEntries(
- Object.entries(value as Record<string, unknown>).map(
- ([key, nestedValue]) => [key, this.cloneValue(nestedValue)],
- ),
- ) as T;
- }
- return value;
- }
- /**
- * 处理更新
- * @param updates - 更新的偏好设置
- */
- private handleUpdates(updates: DeepPartial<Preferences>) {
- const { theme, app } = updates;
- if (
- theme &&
- (Object.keys(theme).length > 0 || Reflect.has(theme, 'fontSize'))
- ) {
- updateCSSVariables(this.state);
- }
- if (
- app &&
- (Reflect.has(app, 'colorGrayMode') || Reflect.has(app, 'colorWeakMode'))
- ) {
- this.updateColorMode(this.state);
- }
- }
- /**
- * 初始化平台标识
- */
- private initPlatform() {
- document.documentElement.dataset.platform = isMacOs() ? 'macOs' : 'window';
- }
- private isAlmostInteger(value: number, epsilon = Number.EPSILON * 10) {
- return Math.abs(value - Math.round(value)) < epsilon;
- }
- private isValidCustomPreferenceValue(
- field: CustomPreferencesField,
- value: unknown,
- ) {
- switch (field.component) {
- case 'number': {
- if (typeof value !== 'number' || !Number.isFinite(value)) {
- return false;
- }
- const max = this.resolveNumericConstraint(field.componentProps?.max);
- const min = this.resolveNumericConstraint(field.componentProps?.min);
- const step = this.resolveNumericConstraint(field.componentProps?.step);
- if (min !== undefined && value < min) {
- return false;
- }
- if (max !== undefined && value > max) {
- return false;
- }
- if (step !== undefined) {
- if (step <= 0) {
- return false;
- }
- const stepBase = min ?? 0;
- const stepCount = (value - stepBase) / step;
- if (!this.isAlmostInteger(stepCount)) {
- return false;
- }
- }
- return true;
- }
- case 'select': {
- return (
- typeof value === 'string' &&
- field.options.some((option) => option.value === value)
- );
- }
- case 'switch': {
- return typeof value === 'boolean';
- }
- default: {
- return typeof value === 'string';
- }
- }
- }
- /**
- * 从缓存加载扩展偏好设置
- * @returns 缓存的扩展偏好设置,如果不存在则返回 null
- */
- private async loadCustomFromCache(): Promise<CustomPreferencesRecord | null> {
- return this.cache.getItem<CustomPreferencesRecord>(STORAGE_KEYS.CUSTOM);
- }
- /**
- * 从缓存加载偏好设置
- * @returns 缓存的偏好设置,如果不存在则返回 null
- */
- private async loadFromCache(): Promise<null | Preferences> {
- return this.cache.getItem<Preferences>(STORAGE_KEYS.MAIN);
- }
- private replaceCustomPreferences(preferences: CustomPreferencesRecord) {
- Object.keys(this.customState).forEach((key) => {
- Reflect.deleteProperty(this.customState, key);
- });
- Object.assign(this.customState, preferences);
- }
- private resolveCustomPreferencesDefaults(
- extension: null | PreferencesExtension<any>,
- ) {
- if (!extension) {
- return {};
- }
- const result: CustomPreferencesRecord = {};
- for (const field of extension.fields) {
- result[field.key] = field.defaultValue;
- }
- return result;
- }
- private resolveNumericConstraint(value: unknown) {
- return typeof value === 'number' && Number.isFinite(value)
- ? value
- : undefined;
- }
- private sanitizeCustomPreferences(
- updates: DeepPartial<CustomPreferencesRecord>,
- ) {
- if (!this.customPreferencesExtension) {
- return {};
- }
- const result: CustomPreferencesRecord = {};
- for (const field of this.customPreferencesExtension.fields) {
- const value = updates[field.key];
- if (
- value !== undefined &&
- this.isValidCustomPreferenceValue(field, value)
- ) {
- result[field.key] = value;
- }
- }
- return result;
- }
- /**
- * 保存偏好设置到缓存
- */
- private async saveToCache() {
- try {
- await this.cache.setItem(STORAGE_KEYS.MAIN, this.state);
- await this.cache.setItem(STORAGE_KEYS.LOCALE, this.state.app.locale);
- await this.cache.setItem(STORAGE_KEYS.THEME, this.state.theme.mode);
- if (this.customPreferencesExtension) {
- await this.cache.setItem(STORAGE_KEYS.CUSTOM, {
- ...this.customState,
- });
- return;
- }
- await this.cache.removeItem(STORAGE_KEYS.CUSTOM);
- } catch (error) {
- console.error('Failed to save preferences to cache:', error);
- }
- }
- /**
- * 监听状态和系统偏好设置的变化
- */
- 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 }) => {
- // 仅在自动模式下跟随系统主题
- if (this.state.theme.mode === 'auto') {
- // 先应用实际的主题
- this.updatePreferences({
- theme: { mode: isDark ? 'dark' : 'light' },
- });
- // 再恢复为 auto 模式,保持跟随系统的状态
- this.updatePreferences({
- theme: { mode: 'auto' },
- });
- }
- });
- }
- /**
- * 更新页面颜色模式(灰色、色弱)
- * @param preference - 偏好设置
- */
- private updateColorMode(preference: Preferences) {
- const { colorGrayMode, colorWeakMode } = preference.app;
- const dom = document.documentElement;
- dom.classList.toggle('invert-mode', colorWeakMode);
- dom.classList.toggle('grayscale-mode', colorGrayMode);
- }
- }
- const preferencesManager = new PreferenceManager();
- export { PreferenceManager, preferencesManager };
|