|
@@ -1,6 +1,12 @@
|
|
|
import type { DeepPartial } from '@vben-core/typings';
|
|
import type { DeepPartial } from '@vben-core/typings';
|
|
|
|
|
|
|
|
-import type { InitialOptions, Preferences } from './types';
|
|
|
|
|
|
|
+import type {
|
|
|
|
|
+ CustomPreferencesField,
|
|
|
|
|
+ CustomPreferencesRecord,
|
|
|
|
|
+ InitialOptions,
|
|
|
|
|
+ Preferences,
|
|
|
|
|
+ PreferencesExtension,
|
|
|
|
|
+} from './types';
|
|
|
|
|
|
|
|
import { markRaw, reactive, readonly, watch } from 'vue';
|
|
import { markRaw, reactive, readonly, watch } from 'vue';
|
|
|
|
|
|
|
@@ -17,6 +23,7 @@ import { defaultPreferences } from './config';
|
|
|
import { updateCSSVariables } from './update-css-variables';
|
|
import { updateCSSVariables } from './update-css-variables';
|
|
|
|
|
|
|
|
const STORAGE_KEYS = {
|
|
const STORAGE_KEYS = {
|
|
|
|
|
+ CUSTOM: 'preferences-custom',
|
|
|
MAIN: 'preferences',
|
|
MAIN: 'preferences',
|
|
|
LOCALE: 'preferences-locale',
|
|
LOCALE: 'preferences-locale',
|
|
|
THEME: 'preferences-theme',
|
|
THEME: 'preferences-theme',
|
|
@@ -24,7 +31,10 @@ const STORAGE_KEYS = {
|
|
|
|
|
|
|
|
class PreferenceManager {
|
|
class PreferenceManager {
|
|
|
private cache: StorageManager;
|
|
private cache: StorageManager;
|
|
|
- private debouncedSave: (preference: Preferences) => void;
|
|
|
|
|
|
|
+ private customPreferencesExtension: null | PreferencesExtension<any> = null;
|
|
|
|
|
+ private customState = reactive<CustomPreferencesRecord>({});
|
|
|
|
|
+ private debouncedSave: () => void;
|
|
|
|
|
+ private initialCustomPreferences: CustomPreferencesRecord = {};
|
|
|
private initialPreferences: Preferences = defaultPreferences;
|
|
private initialPreferences: Preferences = defaultPreferences;
|
|
|
private isInitialized = false;
|
|
private isInitialized = false;
|
|
|
private state: Preferences;
|
|
private state: Preferences;
|
|
@@ -34,10 +44,7 @@ class PreferenceManager {
|
|
|
this.state = reactive<Preferences>(
|
|
this.state = reactive<Preferences>(
|
|
|
this.loadFromCache() || { ...defaultPreferences },
|
|
this.loadFromCache() || { ...defaultPreferences },
|
|
|
);
|
|
);
|
|
|
- this.debouncedSave = useDebounceFn(
|
|
|
|
|
- (preference) => this.saveToCache(preference),
|
|
|
|
|
- 150,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ this.debouncedSave = useDebounceFn(() => this.saveToCache(), 150);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -47,6 +54,26 @@ class PreferenceManager {
|
|
|
Object.values(STORAGE_KEYS).forEach((key) => this.cache.removeItem(key));
|
|
Object.values(STORAGE_KEYS).forEach((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>;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取初始化偏好设置
|
|
* 获取初始化偏好设置
|
|
|
*/
|
|
*/
|
|
@@ -61,13 +88,32 @@ class PreferenceManager {
|
|
|
return readonly(this.state);
|
|
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 - 初始化配置项
|
|
|
* @param options.namespace - 命名空间,用于隔离不同应用的配置
|
|
* @param options.namespace - 命名空间,用于隔离不同应用的配置
|
|
|
* @param options.overrides - 要覆盖的偏好设置
|
|
* @param options.overrides - 要覆盖的偏好设置
|
|
|
*/
|
|
*/
|
|
|
- initPreferences = async ({ namespace, overrides }: InitialOptions) => {
|
|
|
|
|
|
|
+ initPreferences = async <
|
|
|
|
|
+ TCustomPreferences extends object = CustomPreferencesRecord,
|
|
|
|
|
+ >({
|
|
|
|
|
+ namespace,
|
|
|
|
|
+ overrides,
|
|
|
|
|
+ extension,
|
|
|
|
|
+ }: InitialOptions<TCustomPreferences>) => {
|
|
|
// 防止重复初始化
|
|
// 防止重复初始化
|
|
|
if (this.isInitialized) {
|
|
if (this.isInitialized) {
|
|
|
return;
|
|
return;
|
|
@@ -78,6 +124,10 @@ class PreferenceManager {
|
|
|
|
|
|
|
|
// 合并初始偏好设置
|
|
// 合并初始偏好设置
|
|
|
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
|
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
|
|
|
|
+ this.customPreferencesExtension = extension ?? null;
|
|
|
|
|
+ this.initialCustomPreferences = this.resolveCustomPreferencesDefaults(
|
|
|
|
|
+ this.customPreferencesExtension,
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
// 加载缓存的偏好设置并与初始配置合并
|
|
// 加载缓存的偏好设置并与初始配置合并
|
|
|
const cachedPreferences = this.loadFromCache() || {};
|
|
const cachedPreferences = this.loadFromCache() || {};
|
|
@@ -89,6 +139,14 @@ class PreferenceManager {
|
|
|
|
|
|
|
|
// 更新偏好设置
|
|
// 更新偏好设置
|
|
|
this.updatePreferences(mergedPreference);
|
|
this.updatePreferences(mergedPreference);
|
|
|
|
|
+ this.replaceCustomPreferences(
|
|
|
|
|
+ merge(
|
|
|
|
|
+ {},
|
|
|
|
|
+ this.sanitizeCustomPreferences(this.loadCustomFromCache() || {}),
|
|
|
|
|
+ this.initialCustomPreferences,
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ this.saveToCache();
|
|
|
|
|
|
|
|
// 设置监听器
|
|
// 设置监听器
|
|
|
this.setupWatcher();
|
|
this.setupWatcher();
|
|
@@ -105,14 +163,42 @@ class PreferenceManager {
|
|
|
resetPreferences = () => {
|
|
resetPreferences = () => {
|
|
|
// 将状态重置为初始偏好设置
|
|
// 将状态重置为初始偏好设置
|
|
|
Object.assign(this.state, this.initialPreferences);
|
|
Object.assign(this.state, this.initialPreferences);
|
|
|
|
|
+ this.replaceCustomPreferences(this.initialCustomPreferences);
|
|
|
|
|
|
|
|
// 保存偏好设置至缓存
|
|
// 保存偏好设置至缓存
|
|
|
- this.saveToCache(this.state);
|
|
|
|
|
|
|
+ this.saveToCache();
|
|
|
|
|
|
|
|
// 直接触发 UI 更新
|
|
// 直接触发 UI 更新
|
|
|
this.handleUpdates(this.state);
|
|
this.handleUpdates(this.state);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 更新扩展偏好设置
|
|
|
|
|
+ * @param updates - 要更新的扩展偏好设置
|
|
|
|
|
+ */
|
|
|
|
|
+ updateCustomPreferences = <
|
|
|
|
|
+ TCustomPreferences extends object = CustomPreferencesRecord,
|
|
|
|
|
+ >(
|
|
|
|
|
+ updates: DeepPartial<TCustomPreferences>,
|
|
|
|
|
+ ) => {
|
|
|
|
|
+ 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 - 要更新的偏好设置
|
|
* @param updates - 要更新的偏好设置
|
|
@@ -126,9 +212,25 @@ class PreferenceManager {
|
|
|
this.handleUpdates(updates);
|
|
this.handleUpdates(updates);
|
|
|
|
|
|
|
|
// 保存到缓存
|
|
// 保存到缓存
|
|
|
- this.debouncedSave(this.state);
|
|
|
|
|
|
|
+ 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 - 更新的偏好设置
|
|
* @param updates - 更新的偏好设置
|
|
@@ -158,6 +260,70 @@ class PreferenceManager {
|
|
|
document.documentElement.dataset.platform = isMacOs() ? 'macOs' : 'window';
|
|
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 loadCustomFromCache(): CustomPreferencesRecord | null {
|
|
|
|
|
+ return this.cache.getItem<CustomPreferencesRecord>(STORAGE_KEYS.CUSTOM);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 从缓存加载偏好设置
|
|
* 从缓存加载偏好设置
|
|
|
* @returns 缓存的偏好设置,如果不存在则返回 null
|
|
* @returns 缓存的偏好设置,如果不存在则返回 null
|
|
@@ -166,14 +332,72 @@ class PreferenceManager {
|
|
|
return this.cache.getItem<Preferences>(STORAGE_KEYS.MAIN);
|
|
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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 保存偏好设置到缓存
|
|
* 保存偏好设置到缓存
|
|
|
- * @param preference - 要保存的偏好设置
|
|
|
|
|
*/
|
|
*/
|
|
|
- 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 saveToCache() {
|
|
|
|
|
+ this.cache.setItem(STORAGE_KEYS.MAIN, this.state);
|
|
|
|
|
+ this.cache.setItem(STORAGE_KEYS.LOCALE, this.state.app.locale);
|
|
|
|
|
+ this.cache.setItem(STORAGE_KEYS.THEME, this.state.theme.mode);
|
|
|
|
|
+
|
|
|
|
|
+ if (this.customPreferencesExtension) {
|
|
|
|
|
+ this.cache.setItem(STORAGE_KEYS.CUSTOM, { ...this.customState });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.cache.removeItem(STORAGE_KEYS.CUSTOM);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|