Merge branch 'main' into main
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { defaultPreferences } from '../src/config';
|
||||
import { PreferenceManager } from '../src/preferences';
|
||||
import { isDarkTheme } from '../src/update-css-variables';
|
||||
|
||||
describe('preferences', () => {
|
||||
let preferenceManager: PreferenceManager;
|
||||
let PreferenceManager: typeof import('../src/preferences').PreferenceManager;
|
||||
let preferenceManager: InstanceType<
|
||||
typeof import('../src/preferences').PreferenceManager
|
||||
>;
|
||||
|
||||
// 模拟 window.matchMedia 方法
|
||||
vi.stubGlobal(
|
||||
@@ -21,7 +23,36 @@ describe('preferences', () => {
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
})),
|
||||
);
|
||||
|
||||
vi.stubGlobal('localStorage', {
|
||||
clear: vi.fn(),
|
||||
getItem: vi.fn(() => null),
|
||||
key: vi.fn(() => null),
|
||||
length: 0,
|
||||
removeItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
});
|
||||
|
||||
vi.stubGlobal('sessionStorage', {
|
||||
clear: vi.fn(),
|
||||
getItem: vi.fn(() => null),
|
||||
key: vi.fn(() => null),
|
||||
length: 0,
|
||||
removeItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
({ PreferenceManager } = await import('../src/preferences'));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(localStorage.getItem).mockImplementation(() => null);
|
||||
vi.mocked(localStorage.removeItem).mockReset();
|
||||
vi.mocked(localStorage.setItem).mockReset();
|
||||
vi.mocked(sessionStorage.getItem).mockImplementation(() => null);
|
||||
vi.mocked(sessionStorage.removeItem).mockReset();
|
||||
vi.mocked(sessionStorage.setItem).mockReset();
|
||||
preferenceManager = new PreferenceManager();
|
||||
});
|
||||
|
||||
@@ -214,7 +245,10 @@ describe('preferences', () => {
|
||||
},
|
||||
};
|
||||
|
||||
await preferenceManager.initPreferences(overrides);
|
||||
await preferenceManager.initPreferences({
|
||||
namespace: 'apply-updates',
|
||||
overrides,
|
||||
});
|
||||
|
||||
preferenceManager.updatePreferences({
|
||||
theme: { mode: 'light' },
|
||||
@@ -222,6 +256,265 @@ describe('preferences', () => {
|
||||
|
||||
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
|
||||
});
|
||||
|
||||
it('initializes custom preferences extension with default values', async () => {
|
||||
const extension = {
|
||||
fields: [
|
||||
{
|
||||
component: 'switch',
|
||||
defaultValue: true,
|
||||
key: 'enableWorkbench',
|
||||
label: '启用工作台',
|
||||
},
|
||||
{
|
||||
component: 'select',
|
||||
defaultValue: 'single',
|
||||
key: 'tenantMode',
|
||||
label: '租户模式',
|
||||
options: [
|
||||
{ label: '单租户', value: 'single' },
|
||||
{ label: '多租户', value: 'multi' },
|
||||
],
|
||||
},
|
||||
],
|
||||
tabLabel: '扩展',
|
||||
title: '业务偏好',
|
||||
} as const;
|
||||
|
||||
await preferenceManager.initPreferences({
|
||||
extension,
|
||||
namespace: 'custom-defaults',
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferencesExtension()).toEqual(extension);
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
enableWorkbench: true,
|
||||
tenantMode: 'single',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not expose mutable custom preference baselines or extension schema', async () => {
|
||||
const extension = {
|
||||
fields: [
|
||||
{
|
||||
component: 'number',
|
||||
componentProps: {
|
||||
max: 10,
|
||||
min: 2,
|
||||
step: 2,
|
||||
},
|
||||
defaultValue: 4,
|
||||
key: 'pageSize',
|
||||
label: '分页大小',
|
||||
},
|
||||
],
|
||||
tabLabel: '扩展',
|
||||
title: '业务偏好',
|
||||
} as const;
|
||||
|
||||
await preferenceManager.initPreferences({
|
||||
extension,
|
||||
namespace: 'custom-readonly',
|
||||
});
|
||||
|
||||
const initialCustomPreferences =
|
||||
preferenceManager.getInitialCustomPreferences<{
|
||||
pageSize: number;
|
||||
}>() as { pageSize: number };
|
||||
const preferencesExtension = preferenceManager.getPreferencesExtension<{
|
||||
pageSize: number;
|
||||
}>() as {
|
||||
fields: Array<{ componentProps?: { max?: number }; label: string }>;
|
||||
};
|
||||
const [firstField] = preferencesExtension.fields;
|
||||
|
||||
initialCustomPreferences.pageSize = 8;
|
||||
expect(firstField).toBeDefined();
|
||||
expect(firstField?.componentProps).toBeDefined();
|
||||
|
||||
if (!firstField || !firstField.componentProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstField.label = '已修改';
|
||||
firstField.componentProps.max = 20;
|
||||
|
||||
expect(preferenceManager.getInitialCustomPreferences()).toEqual({
|
||||
pageSize: 4,
|
||||
});
|
||||
expect(preferenceManager.getPreferencesExtension()).toEqual(extension);
|
||||
});
|
||||
|
||||
it('updates and resets custom preferences correctly', async () => {
|
||||
await preferenceManager.initPreferences({
|
||||
extension: {
|
||||
fields: [
|
||||
{
|
||||
component: 'number',
|
||||
defaultValue: 20,
|
||||
key: 'pageSize',
|
||||
label: '分页大小',
|
||||
},
|
||||
{
|
||||
component: 'input',
|
||||
defaultValue: '日报',
|
||||
key: 'reportTitle',
|
||||
label: '报表标题',
|
||||
},
|
||||
],
|
||||
tabLabel: '扩展',
|
||||
},
|
||||
namespace: 'custom-reset',
|
||||
});
|
||||
|
||||
preferenceManager.updateCustomPreferences({
|
||||
pageSize: 50,
|
||||
reportTitle: '月报',
|
||||
});
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
pageSize: 50,
|
||||
reportTitle: '月报',
|
||||
});
|
||||
|
||||
preferenceManager.resetPreferences();
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
pageSize: 20,
|
||||
reportTitle: '日报',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores invalid custom preferences updates', async () => {
|
||||
await preferenceManager.initPreferences({
|
||||
extension: {
|
||||
fields: [
|
||||
{
|
||||
component: 'switch',
|
||||
defaultValue: true,
|
||||
key: 'enableWorkbench',
|
||||
label: '启用工作台',
|
||||
},
|
||||
{
|
||||
component: 'select',
|
||||
defaultValue: 'single',
|
||||
key: 'tenantMode',
|
||||
label: '租户模式',
|
||||
options: [
|
||||
{ label: '单租户', value: 'single' },
|
||||
{ label: '多租户', value: 'multi' },
|
||||
],
|
||||
},
|
||||
],
|
||||
tabLabel: '扩展',
|
||||
},
|
||||
namespace: 'custom-invalid',
|
||||
});
|
||||
|
||||
const originalCustomPreferences = preferenceManager.getCustomPreferences();
|
||||
|
||||
preferenceManager.updateCustomPreferences({
|
||||
enableWorkbench: 'true' as unknown as boolean,
|
||||
tenantMode: 'unknown',
|
||||
unknownField: 'value',
|
||||
} as any);
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual(
|
||||
originalCustomPreferences,
|
||||
);
|
||||
});
|
||||
|
||||
it('enforces custom number field min max and step constraints', async () => {
|
||||
await preferenceManager.initPreferences({
|
||||
extension: {
|
||||
fields: [
|
||||
{
|
||||
component: 'number',
|
||||
componentProps: {
|
||||
max: 10,
|
||||
min: 2,
|
||||
step: 2,
|
||||
},
|
||||
defaultValue: 4,
|
||||
key: 'pageSize',
|
||||
label: '分页大小',
|
||||
},
|
||||
],
|
||||
tabLabel: '扩展',
|
||||
},
|
||||
namespace: 'custom-number-constraints',
|
||||
});
|
||||
|
||||
preferenceManager.updateCustomPreferences({
|
||||
pageSize: 8,
|
||||
});
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
pageSize: 8,
|
||||
});
|
||||
|
||||
preferenceManager.updateCustomPreferences({
|
||||
pageSize: 1,
|
||||
});
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
pageSize: 8,
|
||||
});
|
||||
|
||||
preferenceManager.updateCustomPreferences({
|
||||
pageSize: 12,
|
||||
});
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
pageSize: 8,
|
||||
});
|
||||
|
||||
preferenceManager.updateCustomPreferences({
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
pageSize: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters cached custom number values that violate field constraints', async () => {
|
||||
vi.mocked(localStorage.getItem).mockImplementation((key) => {
|
||||
if (key.endsWith('cache-preferences-custom')) {
|
||||
return JSON.stringify({
|
||||
value: {
|
||||
pageSize: 5,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
await preferenceManager.initPreferences({
|
||||
extension: {
|
||||
fields: [
|
||||
{
|
||||
component: 'number',
|
||||
componentProps: {
|
||||
max: 10,
|
||||
min: 2,
|
||||
step: 2,
|
||||
},
|
||||
defaultValue: 4,
|
||||
key: 'pageSize',
|
||||
label: '分页大小',
|
||||
},
|
||||
],
|
||||
tabLabel: '扩展',
|
||||
},
|
||||
namespace: 'custom-number-cache',
|
||||
});
|
||||
|
||||
expect(preferenceManager.getCustomPreferences()).toEqual({
|
||||
pageSize: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDarkTheme', () => {
|
||||
|
||||
@@ -4,7 +4,11 @@ import { preferencesManager } from './preferences';
|
||||
|
||||
export const {
|
||||
getPreferences,
|
||||
getCustomPreferences,
|
||||
getInitialCustomPreferences,
|
||||
getPreferencesExtension,
|
||||
updatePreferences,
|
||||
updateCustomPreferences,
|
||||
resetPreferences,
|
||||
clearCache,
|
||||
initPreferences,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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';
|
||||
|
||||
@@ -17,6 +23,7 @@ import { defaultPreferences } from './config';
|
||||
import { updateCSSVariables } from './update-css-variables';
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
CUSTOM: 'preferences-custom',
|
||||
MAIN: 'preferences',
|
||||
LOCALE: 'preferences-locale',
|
||||
THEME: 'preferences-theme',
|
||||
@@ -24,7 +31,10 @@ const STORAGE_KEYS = {
|
||||
|
||||
class PreferenceManager {
|
||||
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 isInitialized = false;
|
||||
private state: Preferences;
|
||||
@@ -34,10 +44,7 @@ class PreferenceManager {
|
||||
this.state = reactive<Preferences>(
|
||||
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));
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取扩展偏好设置
|
||||
*/
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取扩展偏好设置配置
|
||||
*/
|
||||
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 ({ namespace, overrides }: InitialOptions) => {
|
||||
initPreferences = async <
|
||||
TCustomPreferences extends object = CustomPreferencesRecord,
|
||||
>({
|
||||
namespace,
|
||||
overrides,
|
||||
extension,
|
||||
}: InitialOptions<TCustomPreferences>) => {
|
||||
// 防止重复初始化
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
@@ -78,6 +124,10 @@ class PreferenceManager {
|
||||
|
||||
// 合并初始偏好设置
|
||||
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
||||
this.customPreferencesExtension = extension ?? null;
|
||||
this.initialCustomPreferences = this.resolveCustomPreferencesDefaults(
|
||||
this.customPreferencesExtension,
|
||||
);
|
||||
|
||||
// 加载缓存的偏好设置并与初始配置合并
|
||||
const cachedPreferences = this.loadFromCache() || {};
|
||||
@@ -89,6 +139,14 @@ class PreferenceManager {
|
||||
|
||||
// 更新偏好设置
|
||||
this.updatePreferences(mergedPreference);
|
||||
this.replaceCustomPreferences(
|
||||
merge(
|
||||
{},
|
||||
this.sanitizeCustomPreferences(this.loadCustomFromCache() || {}),
|
||||
this.initialCustomPreferences,
|
||||
),
|
||||
);
|
||||
this.saveToCache();
|
||||
|
||||
// 设置监听器
|
||||
this.setupWatcher();
|
||||
@@ -105,14 +163,42 @@ class PreferenceManager {
|
||||
resetPreferences = () => {
|
||||
// 将状态重置为初始偏好设置
|
||||
Object.assign(this.state, this.initialPreferences);
|
||||
this.replaceCustomPreferences(this.initialCustomPreferences);
|
||||
|
||||
// 保存偏好设置至缓存
|
||||
this.saveToCache(this.state);
|
||||
this.saveToCache();
|
||||
|
||||
// 直接触发 UI 更新
|
||||
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 - 要更新的偏好设置
|
||||
@@ -126,9 +212,25 @@ class PreferenceManager {
|
||||
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 - 更新的偏好设置
|
||||
@@ -158,6 +260,70 @@ class PreferenceManager {
|
||||
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
|
||||
@@ -166,14 +332,72 @@ class PreferenceManager {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,84 @@ import type {
|
||||
} from '@vben-core/typings';
|
||||
|
||||
type SupportedLanguagesType = 'en-US' | 'zh-CN';
|
||||
type CustomPreferencesValue = boolean | number | string;
|
||||
|
||||
interface CustomPreferencesOption<TValue extends string = string> {
|
||||
label: string;
|
||||
value: TValue;
|
||||
}
|
||||
|
||||
interface BaseCustomPreferencesField<
|
||||
TKey extends string = string,
|
||||
TValue extends CustomPreferencesValue = CustomPreferencesValue,
|
||||
> {
|
||||
componentProps?: Record<string, any>;
|
||||
defaultValue: TValue;
|
||||
disabled?: boolean;
|
||||
key: TKey;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
tip?: string;
|
||||
}
|
||||
|
||||
interface CustomPreferencesInputField<
|
||||
TKey extends string = string,
|
||||
> extends BaseCustomPreferencesField<TKey, string> {
|
||||
component: 'input';
|
||||
}
|
||||
|
||||
interface CustomPreferencesNumberField<
|
||||
TKey extends string = string,
|
||||
> extends BaseCustomPreferencesField<TKey, number> {
|
||||
component: 'number';
|
||||
}
|
||||
|
||||
interface CustomPreferencesSelectField<
|
||||
TKey extends string = string,
|
||||
> extends BaseCustomPreferencesField<TKey, string> {
|
||||
component: 'select';
|
||||
options: CustomPreferencesOption[];
|
||||
}
|
||||
|
||||
interface CustomPreferencesSwitchField<
|
||||
TKey extends string = string,
|
||||
> extends BaseCustomPreferencesField<TKey, boolean> {
|
||||
component: 'switch';
|
||||
}
|
||||
|
||||
type CustomPreferencesRecord = Record<string, CustomPreferencesValue>;
|
||||
|
||||
type AnyCustomPreferencesField =
|
||||
| CustomPreferencesInputField
|
||||
| CustomPreferencesNumberField
|
||||
| CustomPreferencesSelectField
|
||||
| CustomPreferencesSwitchField;
|
||||
|
||||
type CustomPreferencesField<
|
||||
TCustomPreferences extends object = CustomPreferencesRecord,
|
||||
> =
|
||||
string extends Extract<keyof TCustomPreferences, string>
|
||||
? AnyCustomPreferencesField
|
||||
: {
|
||||
[K in Extract<
|
||||
keyof TCustomPreferences,
|
||||
string
|
||||
>]: TCustomPreferences[K] extends boolean
|
||||
? CustomPreferencesSwitchField<K>
|
||||
: TCustomPreferences[K] extends number
|
||||
? CustomPreferencesNumberField<K>
|
||||
: TCustomPreferences[K] extends string
|
||||
? CustomPreferencesInputField<K> | CustomPreferencesSelectField<K>
|
||||
: never;
|
||||
}[Extract<keyof TCustomPreferences, string>];
|
||||
|
||||
interface PreferencesExtension<
|
||||
TCustomPreferences extends object = CustomPreferencesRecord,
|
||||
> {
|
||||
fields: Array<CustomPreferencesField<TCustomPreferences>>;
|
||||
tabLabel: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface AppPreferences {
|
||||
/** 权限模式 */
|
||||
@@ -324,19 +402,33 @@ interface Preferences {
|
||||
|
||||
type PreferencesKeys = keyof Preferences;
|
||||
|
||||
interface InitialOptions {
|
||||
interface InitialOptions<
|
||||
TCustomPreferences extends object = CustomPreferencesRecord,
|
||||
> {
|
||||
extension?: PreferencesExtension<TCustomPreferences>;
|
||||
namespace: string;
|
||||
overrides?: DeepPartial<Preferences>;
|
||||
}
|
||||
export type {
|
||||
AnyCustomPreferencesField,
|
||||
AppPreferences,
|
||||
BaseCustomPreferencesField,
|
||||
BreadcrumbPreferences,
|
||||
CustomPreferencesField,
|
||||
CustomPreferencesInputField,
|
||||
CustomPreferencesNumberField,
|
||||
CustomPreferencesOption,
|
||||
CustomPreferencesRecord,
|
||||
CustomPreferencesSelectField,
|
||||
CustomPreferencesSwitchField,
|
||||
CustomPreferencesValue,
|
||||
FooterPreferences,
|
||||
HeaderPreferences,
|
||||
InitialOptions,
|
||||
LogoPreferences,
|
||||
NavigationPreferences,
|
||||
Preferences,
|
||||
PreferencesExtension,
|
||||
PreferencesKeys,
|
||||
ShortcutKeyPreferences,
|
||||
SidebarPreferences,
|
||||
|
||||
@@ -7,7 +7,13 @@ import { isDarkTheme } from './update-css-variables';
|
||||
|
||||
function usePreferences() {
|
||||
const preferences = preferencesManager.getPreferences();
|
||||
const customPreferences = preferencesManager.getCustomPreferences();
|
||||
const initialPreferences = preferencesManager.getInitialPreferences();
|
||||
const initialCustomPreferences =
|
||||
preferencesManager.getInitialCustomPreferences();
|
||||
const preferencesExtension = computed(() =>
|
||||
preferencesManager.getPreferencesExtension(),
|
||||
);
|
||||
/**
|
||||
* @zh_CN 计算偏好设置的变化
|
||||
*/
|
||||
@@ -15,6 +21,10 @@ function usePreferences() {
|
||||
return diff(initialPreferences, preferences);
|
||||
});
|
||||
|
||||
const diffCustomPreference = computed(() => {
|
||||
return diff(initialCustomPreferences, customPreferences);
|
||||
});
|
||||
|
||||
const appPreferences = computed(() => preferences.app);
|
||||
|
||||
const shortcutKeysPreferences = computed(() => preferences.shortcutKeys);
|
||||
@@ -228,7 +238,9 @@ function usePreferences() {
|
||||
authPanelLeft,
|
||||
authPanelRight,
|
||||
contentIsMaximize,
|
||||
customPreferences,
|
||||
diffPreference,
|
||||
diffCustomPreference,
|
||||
globalLockScreenShortcutKey,
|
||||
globalLogoutShortcutKey,
|
||||
globalSearchShortcutKey,
|
||||
@@ -245,6 +257,7 @@ function usePreferences() {
|
||||
keepAlive,
|
||||
layout,
|
||||
locale,
|
||||
preferencesExtension,
|
||||
preferencesButtonPosition,
|
||||
sidebarCollapsed,
|
||||
theme,
|
||||
|
||||
Reference in New Issue
Block a user