Merge remote-tracking branch 'vben/main' into vben-dev
# Conflicts: # .gitignore # .husky/pre-commit # .vscode/extensions.json # .vscode/settings.json # README.ja-JP.md # README.md # README.zh-CN.md # apps/backend-mock/api/auth/codes.ts # apps/backend-mock/api/auth/login.post.ts # apps/backend-mock/api/auth/logout.post.ts # apps/backend-mock/api/auth/refresh.post.ts # apps/backend-mock/api/menu/all.ts # apps/backend-mock/api/status.ts # apps/backend-mock/api/table/list.ts # apps/backend-mock/api/test.get.ts # apps/backend-mock/api/test.post.ts # apps/backend-mock/api/user/info.ts # apps/backend-mock/middleware/1.api.ts # apps/backend-mock/nitro.config.ts # apps/backend-mock/package.json # apps/backend-mock/routes/[...].ts # apps/backend-mock/tsconfig.json # apps/backend-mock/utils/cookie-utils.ts # apps/backend-mock/utils/jwt-utils.ts # apps/backend-mock/utils/mock-data.ts # apps/backend-mock/utils/response.ts # apps/web-antd/.env # apps/web-antd/src/adapter/component/index.ts # apps/web-antd/src/adapter/form.ts # apps/web-antd/src/api/request.ts # apps/web-antd/src/layouts/auth.vue # apps/web-antd/src/layouts/basic.vue # apps/web-antd/src/preferences.ts # apps/web-antd/src/router/guard.ts # apps/web-antd/src/router/routes/modules/demos.ts # apps/web-antd/src/store/auth.ts # apps/web-antd/src/views/demos/antd/index.vue # apps/web-antdv-next/vite.config.ts # apps/web-ele/.env # apps/web-ele/index.html # apps/web-ele/package.json # apps/web-ele/src/adapter/component/index.ts # apps/web-ele/src/adapter/form.ts # apps/web-ele/src/adapter/vxe-table.ts # apps/web-ele/src/api/request.ts # apps/web-ele/src/bootstrap.ts # apps/web-ele/src/layouts/auth.vue # apps/web-ele/src/layouts/basic.vue # apps/web-ele/src/locales/index.ts # apps/web-ele/src/locales/langs/en-US/demos.json # apps/web-ele/src/locales/langs/en-US/page.json # apps/web-ele/src/locales/langs/zh-CN/demos.json # apps/web-ele/src/locales/langs/zh-CN/page.json # apps/web-ele/src/preferences.ts # apps/web-ele/src/router/guard.ts # apps/web-ele/src/router/routes/core.ts # apps/web-ele/src/router/routes/modules/dashboard.ts # apps/web-ele/src/router/routes/modules/demos.ts # apps/web-ele/src/router/routes/modules/vben.ts # apps/web-ele/src/store/auth.ts # apps/web-ele/src/views/_core/authentication/code-login.vue # apps/web-ele/src/views/_core/authentication/forget-password.vue # apps/web-ele/src/views/_core/authentication/register.vue # apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue # apps/web-ele/src/views/dashboard/analytics/index.vue # apps/web-ele/src/views/demos/element/index.vue # apps/web-ele/src/views/demos/form/basic.vue # apps/web-ele/tsconfig.json # apps/web-ele/tsconfig.node.json # apps/web-naive/.env # apps/web-naive/index.html # apps/web-naive/package.json # apps/web-naive/src/adapter/component/index.ts # apps/web-naive/src/adapter/form.ts # apps/web-naive/src/adapter/vxe-table.ts # apps/web-naive/src/api/request.ts # apps/web-naive/src/bootstrap.ts # apps/web-naive/src/layouts/auth.vue # apps/web-naive/src/layouts/basic.vue # apps/web-naive/src/locales/index.ts # apps/web-naive/src/locales/langs/en-US/demos.json # apps/web-naive/src/locales/langs/en-US/page.json # apps/web-naive/src/locales/langs/zh-CN/demos.json # apps/web-naive/src/locales/langs/zh-CN/page.json # apps/web-naive/src/preferences.ts # apps/web-naive/src/router/guard.ts # apps/web-naive/src/router/routes/core.ts # apps/web-naive/src/router/routes/modules/dashboard.ts # apps/web-naive/src/router/routes/modules/demos.ts # apps/web-naive/src/router/routes/modules/vben.ts # apps/web-naive/src/store/auth.ts # apps/web-naive/src/views/_core/authentication/code-login.vue # apps/web-naive/src/views/_core/authentication/forget-password.vue # apps/web-naive/src/views/_core/authentication/register.vue # apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue # apps/web-naive/src/views/dashboard/analytics/index.vue # apps/web-naive/src/views/demos/form/basic.vue # apps/web-naive/src/views/demos/naive/index.vue # apps/web-naive/tsconfig.json # apps/web-naive/tsconfig.node.json # apps/web-naive/vite.config.ts # docs/.vitepress/components/demo-preview.vue # docs/.vitepress/components/preview-group.vue # docs/.vitepress/config/en.mts # docs/.vitepress/config/index.mts # docs/.vitepress/config/plugins/demo-preview.ts # docs/.vitepress/config/shared.mts # docs/.vitepress/config/zh.mts # docs/.vitepress/theme/components/site-layout.vue # docs/.vitepress/theme/components/vben-contributors.vue # docs/.vitepress/theme/styles/variables.css # docs/package.json # docs/src/_env/adapter/component.ts # docs/src/_env/adapter/form.ts # docs/src/_env/adapter/vxe-table.ts # docs/src/commercial/community.md # docs/src/components/common-ui/vben-count-to-animator.md # docs/src/components/common-ui/vben-drawer.md # docs/src/components/common-ui/vben-form.md # docs/src/components/common-ui/vben-modal.md # docs/src/components/common-ui/vben-vxe-table.md # docs/src/components/layout-ui/page.md # docs/src/demos/vben-drawer/auto-height/drawer.vue # docs/src/demos/vben-drawer/basic/index.vue # docs/src/demos/vben-drawer/dynamic/index.vue # docs/src/demos/vben-drawer/shared-data/drawer.vue # docs/src/demos/vben-drawer/shared-data/index.vue # docs/src/demos/vben-form/api/index.vue # docs/src/demos/vben-form/basic/index.vue # docs/src/demos/vben-form/custom/index.vue # docs/src/demos/vben-form/dynamic/index.vue # docs/src/demos/vben-form/query/index.vue # docs/src/demos/vben-form/rules/index.vue # docs/src/demos/vben-modal/auto-height/modal.vue # docs/src/demos/vben-modal/basic/index.vue # docs/src/demos/vben-modal/dynamic/index.vue # docs/src/demos/vben-modal/shared-data/index.vue # docs/src/demos/vben-modal/shared-data/modal.vue # docs/src/demos/vben-vxe-table/basic/index.vue # docs/src/demos/vben-vxe-table/custom-cell/index.vue # docs/src/demos/vben-vxe-table/edit-row/index.vue # docs/src/demos/vben-vxe-table/fixed/index.vue # docs/src/demos/vben-vxe-table/form/index.vue # docs/src/demos/vben-vxe-table/remote/index.vue # docs/src/demos/vben-vxe-table/tree/index.vue # docs/src/demos/vben-vxe-table/virtual/index.vue # docs/src/en/guide/essentials/development.md # docs/src/en/guide/essentials/external-module.md # docs/src/en/guide/essentials/route.md # docs/src/en/guide/essentials/server.md # docs/src/en/guide/essentials/settings.md # docs/src/en/guide/in-depth/access.md # docs/src/en/guide/in-depth/loading.md # docs/src/en/guide/in-depth/locale.md # docs/src/en/guide/in-depth/theme.md # docs/src/en/guide/introduction/quick-start.md # docs/src/en/guide/introduction/thin.md # docs/src/en/guide/introduction/vben.md # docs/src/en/guide/other/faq.md # docs/src/en/guide/project/dir.md # docs/src/en/guide/project/standard.md # docs/src/en/guide/project/tailwindcss.md # docs/src/en/index.md # docs/src/friend-links/index.md # docs/src/guide/essentials/build.md # docs/src/guide/essentials/development.md # docs/src/guide/essentials/external-module.md # docs/src/guide/essentials/route.md # docs/src/guide/essentials/server.md # docs/src/guide/essentials/settings.md # docs/src/guide/in-depth/access.md # docs/src/guide/in-depth/check-updates.md # docs/src/guide/in-depth/loading.md # docs/src/guide/in-depth/locale.md # docs/src/guide/in-depth/theme.md # docs/src/guide/introduction/quick-start.md # docs/src/guide/introduction/thin.md # docs/src/guide/introduction/vben.md # docs/src/guide/introduction/why.md # docs/src/guide/other/faq.md # docs/src/guide/project/dir.md # docs/src/guide/project/standard.md # docs/src/guide/project/tailwindcss.md # docs/src/index.md # docs/tsconfig.json # package.json # packages/@core/ui-kit/shadcn-ui/src/components/logo/logo.vue # packages/constants/src/core.ts # packages/effects/common-ui/src/components/index.ts # packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue # packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue # packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue # packages/effects/layouts/src/authentication/authentication.vue # packages/effects/layouts/src/basic/layout.vue # packages/effects/layouts/src/widgets/notification/notification.vue # packages/effects/layouts/src/widgets/notification/types.ts # packages/icons/src/iconify/index.ts # packages/icons/src/svg/index.ts # packages/stores/src/modules/user.ts # playground/src/adapter/component/index.ts # playground/src/store/auth.ts # pnpm-lock.yaml # vben-admin.code-workspace
This commit is contained in:
@@ -9,19 +9,31 @@ const defaultPreferences: Preferences = {
|
||||
colorWeakMode: false,
|
||||
compact: false,
|
||||
contentCompact: 'wide',
|
||||
contentCompactWidth: 1200,
|
||||
contentPadding: 0,
|
||||
contentPaddingBottom: 0,
|
||||
contentPaddingLeft: 0,
|
||||
contentPaddingRight: 0,
|
||||
contentPaddingTop: 0,
|
||||
defaultAvatar:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||
defaultHomePath: '/dashboard',
|
||||
dynamicTitle: true,
|
||||
enableCheckUpdates: true,
|
||||
enableCopyPreferences: true,
|
||||
enablePreferences: true,
|
||||
enableRefreshToken: false,
|
||||
enableStickyPreferencesNavigationBar: true,
|
||||
isMobile: false,
|
||||
layout: 'sidebar-nav',
|
||||
locale: 'zh-CN',
|
||||
loginExpiredMode: 'page',
|
||||
name: 'Vben Admin',
|
||||
preferencesButtonPosition: 'auto',
|
||||
timezone: 'Asia/Shanghai',
|
||||
watermark: false,
|
||||
watermarkContent: '',
|
||||
zIndex: 200,
|
||||
},
|
||||
breadcrumb: {
|
||||
enable: true,
|
||||
@@ -35,22 +47,29 @@ const defaultPreferences: Preferences = {
|
||||
companySiteLink: 'https://www.vben.pro',
|
||||
date: '2024',
|
||||
enable: true,
|
||||
icp: '',
|
||||
icpLink: '',
|
||||
icp: '闽ICP备19024351号',
|
||||
icpLink: 'https://beian.miit.gov.cn/',
|
||||
settingShow: true,
|
||||
},
|
||||
footer: {
|
||||
enable: false,
|
||||
fixed: false,
|
||||
height: 32,
|
||||
},
|
||||
header: {
|
||||
enable: true,
|
||||
height: 50,
|
||||
hidden: false,
|
||||
menuAlign: 'start',
|
||||
mode: 'fixed',
|
||||
},
|
||||
|
||||
logo: {
|
||||
enable: true,
|
||||
fit: 'contain',
|
||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
showText: true,
|
||||
logoMode: 'icon',
|
||||
},
|
||||
navigation: {
|
||||
accordion: true,
|
||||
@@ -59,6 +78,7 @@ const defaultPreferences: Preferences = {
|
||||
},
|
||||
shortcutKeys: {
|
||||
enable: true,
|
||||
globalEscape: false,
|
||||
globalLockScreen: true,
|
||||
globalLogout: true,
|
||||
globalPreferences: true,
|
||||
@@ -67,11 +87,17 @@ const defaultPreferences: Preferences = {
|
||||
sidebar: {
|
||||
autoActivateChild: false,
|
||||
collapsed: false,
|
||||
collapsedButton: true,
|
||||
collapsedShowTitle: false,
|
||||
collapseWidth: 60,
|
||||
draggable: true,
|
||||
enable: true,
|
||||
expandOnHover: true,
|
||||
extraCollapse: true,
|
||||
extraCollapse: false,
|
||||
extraCollapsedWidth: 60,
|
||||
fixedButton: true,
|
||||
hidden: false,
|
||||
mixedWidth: 80,
|
||||
width: 224,
|
||||
},
|
||||
tabbar: {
|
||||
@@ -79,11 +105,15 @@ const defaultPreferences: Preferences = {
|
||||
enable: true,
|
||||
height: 38,
|
||||
keepAlive: true,
|
||||
maxCount: 0,
|
||||
middleClickToClose: false,
|
||||
persist: true,
|
||||
showIcon: true,
|
||||
showMaximize: true,
|
||||
showMore: true,
|
||||
showRefresh: true,
|
||||
styleType: 'chrome',
|
||||
visitHistory: true,
|
||||
wheelable: true,
|
||||
},
|
||||
theme: {
|
||||
@@ -94,8 +124,10 @@ const defaultPreferences: Preferences = {
|
||||
colorWarning: 'hsl(42 84% 61%)',
|
||||
mode: 'dark',
|
||||
radius: '0.5',
|
||||
fontSize: 16,
|
||||
semiDarkHeader: false,
|
||||
semiDarkSidebar: false,
|
||||
semiDarkSidebarSub: false,
|
||||
},
|
||||
transition: {
|
||||
enable: true,
|
||||
@@ -105,13 +137,35 @@ const defaultPreferences: Preferences = {
|
||||
},
|
||||
widget: {
|
||||
fullscreen: true,
|
||||
fullscreenButtonPosition: 'header',
|
||||
globalSearch: true,
|
||||
globalSearchButtonPosition: 'header',
|
||||
languageToggle: true,
|
||||
languageToggleButtonPosition: 'header',
|
||||
lockScreen: true,
|
||||
lockScreenButtonPosition: 'header',
|
||||
logoutButtonPosition: 'header',
|
||||
notification: true,
|
||||
notificationButtonPosition: 'header',
|
||||
refresh: true,
|
||||
refreshButtonPosition: 'header',
|
||||
sidebarToggle: true,
|
||||
themeToggle: true,
|
||||
themeToggleButtonPosition: 'header',
|
||||
timezone: true,
|
||||
timezoneButtonPosition: 'header',
|
||||
order: [
|
||||
'globalSearch',
|
||||
'preferences',
|
||||
'themeToggle',
|
||||
'languageToggle',
|
||||
'timezone',
|
||||
'fullscreen',
|
||||
'refresh',
|
||||
'notification',
|
||||
'lockScreenBtn',
|
||||
'logoutBtn',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BuiltinThemeType } from '@vben-core/typings';
|
||||
import type { BuiltinThemeType, TimezoneOption } from '@vben-core/typings';
|
||||
|
||||
interface BuiltinThemePreset {
|
||||
color: string;
|
||||
@@ -38,12 +38,10 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
|
||||
primaryColor: 'hsl(240 5.9% 10%)',
|
||||
type: 'zinc',
|
||||
},
|
||||
|
||||
{
|
||||
color: 'hsl(181 84% 32%)',
|
||||
type: 'deep-green',
|
||||
},
|
||||
|
||||
{
|
||||
color: 'hsl(211 91% 39%)',
|
||||
type: 'deep-blue',
|
||||
@@ -56,7 +54,6 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
|
||||
color: 'hsl(0 75% 42%)',
|
||||
type: 'rose',
|
||||
},
|
||||
|
||||
{
|
||||
color: 'hsl(0 0% 25%)',
|
||||
darkPrimaryColor: 'hsl(0 0% 98%)',
|
||||
@@ -81,8 +78,39 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 时区选项
|
||||
*/
|
||||
const DEFAULT_TIME_ZONE_OPTIONS: TimezoneOption[] = [
|
||||
{
|
||||
offset: -5,
|
||||
timezone: 'America/New_York',
|
||||
label: 'America/New_York(GMT-5)',
|
||||
},
|
||||
{
|
||||
offset: 0,
|
||||
timezone: 'Europe/London',
|
||||
label: 'Europe/London(GMT0)',
|
||||
},
|
||||
{
|
||||
offset: 8,
|
||||
timezone: 'Asia/Shanghai',
|
||||
label: 'Asia/Shanghai(GMT+8)',
|
||||
},
|
||||
{
|
||||
offset: 9,
|
||||
timezone: 'Asia/Tokyo',
|
||||
label: 'Asia/Tokyo(GMT+9)',
|
||||
},
|
||||
{
|
||||
offset: 9,
|
||||
timezone: 'Asia/Seoul',
|
||||
label: 'Asia/Seoul(GMT+9)',
|
||||
},
|
||||
];
|
||||
|
||||
export const COLOR_PRESETS = [...BUILT_IN_THEME_PRESETS].slice(0, 7);
|
||||
|
||||
export { BUILT_IN_THEME_PRESETS };
|
||||
export { BUILT_IN_THEME_PRESETS, DEFAULT_TIME_ZONE_OPTIONS };
|
||||
|
||||
export type { BuiltinThemePreset };
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
/* oxlint-disable unicorn/prefer-export-from */
|
||||
import type { Preferences } from './types';
|
||||
|
||||
import { preferencesManager } from './preferences';
|
||||
|
||||
// 偏好设置(带有层级关系)
|
||||
const preferences: Preferences =
|
||||
preferencesManager.getPreferences.apply(preferencesManager);
|
||||
|
||||
// 更新偏好设置
|
||||
const updatePreferences =
|
||||
preferencesManager.updatePreferences.bind(preferencesManager);
|
||||
|
||||
// 重置偏好设置
|
||||
const resetPreferences =
|
||||
preferencesManager.resetPreferences.bind(preferencesManager);
|
||||
|
||||
const clearPreferencesCache =
|
||||
preferencesManager.clearCache.bind(preferencesManager);
|
||||
|
||||
// 初始化偏好设置
|
||||
const initPreferences =
|
||||
preferencesManager.initPreferences.bind(preferencesManager);
|
||||
|
||||
export {
|
||||
clearPreferencesCache,
|
||||
initPreferences,
|
||||
preferences,
|
||||
preferencesManager,
|
||||
resetPreferences,
|
||||
export const {
|
||||
getPreferences,
|
||||
getCustomPreferences,
|
||||
getInitialCustomPreferences,
|
||||
getPreferencesExtension,
|
||||
updatePreferences,
|
||||
};
|
||||
updateCustomPreferences,
|
||||
resetPreferences,
|
||||
clearCache,
|
||||
initPreferences,
|
||||
} = preferencesManager;
|
||||
|
||||
export const preferences: Preferences = getPreferences();
|
||||
|
||||
export { preferencesManager };
|
||||
|
||||
export * from './constants';
|
||||
export type * from './types';
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
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 { StorageManager } from '@vben-core/shared/cache';
|
||||
import { isMacOs, merge } from '@vben-core/shared/utils';
|
||||
import {
|
||||
isMacOs,
|
||||
merge,
|
||||
mergeWithArrayOverride,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
import {
|
||||
breakpointsTailwind,
|
||||
@@ -16,81 +26,425 @@ 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 = {
|
||||
CUSTOM: 'preferences-custom',
|
||||
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 customPreferencesExtension: null | PreferencesExtension<any> = null;
|
||||
private customState = reactive<CustomPreferencesRecord>({});
|
||||
private debouncedSave: () => void;
|
||||
private initialCustomPreferences: CustomPreferencesRecord = {};
|
||||
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),
|
||||
150,
|
||||
);
|
||||
// 构造函数不再同步读取缓存,使用默认值初始化
|
||||
// 真正的缓存加载在 initPreferences 中完成(已经是 async)
|
||||
this.state = reactive<Preferences>({ ...defaultPreferences });
|
||||
this.debouncedSave = useDebounceFn(() => this.saveToCache(), 150);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存偏好设置
|
||||
* @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);
|
||||
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()) || {};
|
||||
// 修复历史脏数据:早期版本用 defu(merge) 合并数组,导致 widget.order
|
||||
// 在每次 updatePreferences 时被 concat 追加,膨胀到几十/上百条重复 key。
|
||||
// 这里在加载缓存后做一次去重,保留首次出现的 key。
|
||||
this.sanitizeCachedArray(cachedPreferences, 'widget', 'order');
|
||||
const mergedPreference = mergeWithArrayOverride(
|
||||
{},
|
||||
cachedPreferences, // 用户缓存的设置优先
|
||||
this.initialPreferences, // 初始设置仅补齐缺失字段
|
||||
);
|
||||
|
||||
// 更新偏好设置
|
||||
this.updatePreferences(mergedPreference);
|
||||
|
||||
const cachedCustom = (await this.loadCustomFromCache()) || {};
|
||||
this.replaceCustomPreferences(
|
||||
mergeWithArrayOverride(
|
||||
{},
|
||||
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(
|
||||
mergeWithArrayOverride({}, sanitizedUpdates, markRaw(this.customState)),
|
||||
);
|
||||
this.debouncedSave();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新偏好设置
|
||||
* @param updates - 要更新的偏好设置
|
||||
*/
|
||||
updatePreferences = (updates: DeepPartial<Preferences>) => {
|
||||
// 深度合并更新内容和当前状态
|
||||
// 注意:必须用 mergeWithArrayOverride 而不是 merge。
|
||||
// defu(merge) 对数组的语义是 concat 追加,会导致 widget.order 等
|
||||
// 数组字段在每次 updatePreferences 时被重复追加,指数级膨胀。
|
||||
const mergedState = mergeWithArrayOverride(
|
||||
{},
|
||||
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 {DeepPartial<Preferences>} updates - 部分更新的偏好设置
|
||||
* 处理更新
|
||||
* @param updates - 更新的偏好设置
|
||||
*/
|
||||
private handleUpdates(updates: DeepPartial<Preferences>) {
|
||||
const themeUpdates = updates.theme || {};
|
||||
const appUpdates = updates.app || {};
|
||||
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
|
||||
const { theme, app } = updates;
|
||||
|
||||
if (
|
||||
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';
|
||||
}
|
||||
|
||||
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 loadCachedPreferences() {
|
||||
return this.cache?.getItem<Preferences>(STORAGE_KEY);
|
||||
private async loadCustomFromCache(): Promise<CustomPreferencesRecord | null> {
|
||||
return this.cache.getItem<CustomPreferencesRecord>(STORAGE_KEYS.CUSTOM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载偏好设置
|
||||
* @returns {Preferences} 加载的偏好设置
|
||||
* 从缓存加载偏好设置
|
||||
* @returns 缓存的偏好设置,如果不存在则返回 null
|
||||
*/
|
||||
private loadPreferences(): Preferences {
|
||||
return this.loadCachedPreferences() || { ...defaultPreferences };
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听状态和系统偏好设置的变化。
|
||||
* 清理缓存中膨胀的数组字段(保留首次出现的元素)
|
||||
* 用于修复早期版本 defu 合并数组时 concat 追加导致的脏数据
|
||||
*/
|
||||
private sanitizeCachedArray(
|
||||
cached: Record<string, any>,
|
||||
group: string,
|
||||
field: string,
|
||||
) {
|
||||
const node = cached?.[group]?.[field];
|
||||
if (!Array.isArray(node) || node.length <= 1) return;
|
||||
const seen = new Set<unknown>();
|
||||
const deduped = node.filter((item) => {
|
||||
if (seen.has(item)) return false;
|
||||
seen.add(item);
|
||||
return true;
|
||||
});
|
||||
if (deduped.length !== node.length) {
|
||||
cached[group][field] = deduped;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -100,6 +454,7 @@ class PreferenceManager {
|
||||
// 监听断点,判断是否移动端
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller('md');
|
||||
|
||||
watch(
|
||||
() => isMobile.value,
|
||||
(val) => {
|
||||
@@ -114,115 +469,33 @@ class PreferenceManager {
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', ({ matches: isDark }) => {
|
||||
this.updatePreferences({
|
||||
theme: { mode: isDark ? 'dark' : 'light' },
|
||||
});
|
||||
// 仅在自动模式下跟随系统主题
|
||||
if (this.state.theme.mode === 'auto') {
|
||||
// 先应用实际的主题
|
||||
this.updatePreferences({
|
||||
theme: { mode: isDark ? 'dark' : 'light' },
|
||||
});
|
||||
// 再恢复为 auto 模式,保持跟随系统的状态
|
||||
this.updatePreferences({
|
||||
theme: { mode: 'auto' },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新页面颜色模式(灰色、色弱)
|
||||
* @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';
|
||||
colorWeakMode
|
||||
? dom.classList.add(COLOR_WEAK)
|
||||
: dom.classList.remove(COLOR_WEAK);
|
||||
colorGrayMode
|
||||
? dom.classList.add(COLOR_GRAY)
|
||||
: dom.classList.remove(COLOR_GRAY);
|
||||
}
|
||||
}
|
||||
const { colorGrayMode, colorWeakMode } = preference.app;
|
||||
const dom = document.documentElement;
|
||||
|
||||
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() || {},
|
||||
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() {
|
||||
// 将状态重置为初始偏好设置
|
||||
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<Preferences>) {
|
||||
const mergedState = merge({}, updates, markRaw(this.state));
|
||||
|
||||
Object.assign(this.state, mergedState);
|
||||
|
||||
// 根据更新的键值执行相应的操作
|
||||
this.handleUpdates(updates);
|
||||
this.savePreferences(this.state);
|
||||
dom.classList.toggle('invert-mode', colorWeakMode);
|
||||
dom.classList.toggle('grayscale-mode', colorGrayMode);
|
||||
}
|
||||
}
|
||||
|
||||
const preferencesManager = new PreferenceManager();
|
||||
|
||||
export { PreferenceManager, preferencesManager };
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
BuiltinThemeType,
|
||||
ContentCompactType,
|
||||
DeepPartial,
|
||||
LayoutHeaderMenuAlignType,
|
||||
LayoutHeaderModeType,
|
||||
LayoutType,
|
||||
LoginExpiredModeType,
|
||||
@@ -16,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 {
|
||||
/** 权限模式 */
|
||||
@@ -32,18 +111,38 @@ interface AppPreferences {
|
||||
compact: boolean;
|
||||
/** 是否开启内容紧凑模式 */
|
||||
contentCompact: ContentCompactType;
|
||||
// /** 应用默认头像 */
|
||||
/** 内容紧凑宽度 */
|
||||
contentCompactWidth: number;
|
||||
/** 内容内边距 */
|
||||
contentPadding: number;
|
||||
/** 内容底部内边距 */
|
||||
contentPaddingBottom: number;
|
||||
/** 内容左侧内边距 */
|
||||
contentPaddingLeft: number;
|
||||
/** 内容右侧内边距 */
|
||||
contentPaddingRight: number;
|
||||
/** 内容顶部内边距 */
|
||||
contentPaddingTop: number;
|
||||
/** 应用默认头像 */
|
||||
defaultAvatar: string;
|
||||
// /** 开启动态标题 */
|
||||
/** 默认首页地址 */
|
||||
defaultHomePath: string;
|
||||
/** 开启动态标题 */
|
||||
dynamicTitle: boolean;
|
||||
/** 是否开启检查更新 */
|
||||
enableCheckUpdates: boolean;
|
||||
/** 是否显示复制偏好设置按钮 */
|
||||
enableCopyPreferences: boolean;
|
||||
/** 是否显示偏好设置 */
|
||||
enablePreferences: boolean;
|
||||
/**
|
||||
* @zh_CN 是否开启refreshToken
|
||||
*/
|
||||
enableRefreshToken: boolean;
|
||||
/**
|
||||
* @zh_CN 是否开启首选项导航栏吸顶效果
|
||||
*/
|
||||
enableStickyPreferencesNavigationBar: boolean;
|
||||
/** 是否移动端 */
|
||||
isMobile: boolean;
|
||||
/** 布局方式 */
|
||||
@@ -58,10 +157,20 @@ interface AppPreferences {
|
||||
preferencesButtonPosition: PreferencesButtonPositionType;
|
||||
/** 版本信息 */
|
||||
version: string;
|
||||
/**
|
||||
* @zh_CN 应用时区
|
||||
*/
|
||||
timezone: string;
|
||||
/**
|
||||
* @zh_CN 是否开启水印
|
||||
*/
|
||||
watermark: boolean;
|
||||
/**
|
||||
* @zh_CN 水印文案
|
||||
*/
|
||||
watermarkContent: string;
|
||||
/** z-index */
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
interface BreadcrumbPreferences {
|
||||
@@ -99,13 +208,19 @@ interface FooterPreferences {
|
||||
enable: boolean;
|
||||
/** 底栏是否固定 */
|
||||
fixed: boolean;
|
||||
/** 底栏高度 */
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface HeaderPreferences {
|
||||
/** 顶栏是否启用 */
|
||||
enable: boolean;
|
||||
/** 顶栏高度 */
|
||||
height: number;
|
||||
/** 顶栏是否隐藏,css-隐藏 */
|
||||
hidden: boolean;
|
||||
/** 顶栏菜单位置 */
|
||||
menuAlign: LayoutHeaderMenuAlignType;
|
||||
/** header显示模式 */
|
||||
mode: LayoutHeaderModeType;
|
||||
}
|
||||
@@ -113,8 +228,18 @@ interface HeaderPreferences {
|
||||
interface LogoPreferences {
|
||||
/** logo是否可见 */
|
||||
enable: boolean;
|
||||
/** logo图片适应方式 */
|
||||
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
/** logo高度, 只在 logoMode=full时生效 */
|
||||
fullLogoHeight?: number | string;
|
||||
/** logo 展示类型,icon 图标模式, full 铺满logo区域 */
|
||||
logoMode: 'full' | 'icon';
|
||||
/** logo text是否展示 */
|
||||
showText: boolean;
|
||||
/** logo地址 */
|
||||
source: string;
|
||||
/** 暗色主题logo地址 (可选,若不设置则使用 source) */
|
||||
sourceDark?: string;
|
||||
}
|
||||
|
||||
interface NavigationPreferences {
|
||||
@@ -131,16 +256,28 @@ interface SidebarPreferences {
|
||||
autoActivateChild: boolean;
|
||||
/** 侧边栏是否折叠 */
|
||||
collapsed: boolean;
|
||||
/** 侧边栏折叠按钮是否可见 */
|
||||
collapsedButton: boolean;
|
||||
/** 侧边栏折叠时,是否显示title */
|
||||
collapsedShowTitle: boolean;
|
||||
/** 侧边栏折叠宽度 */
|
||||
collapseWidth: number;
|
||||
/** 侧边栏菜单拖拽 */
|
||||
draggable: boolean;
|
||||
/** 侧边栏是否可见 */
|
||||
enable: boolean;
|
||||
/** 菜单自动展开状态 */
|
||||
expandOnHover: boolean;
|
||||
/** 侧边栏扩展区域是否折叠 */
|
||||
extraCollapse: boolean;
|
||||
/** 侧边栏扩展区域折叠宽度 */
|
||||
extraCollapsedWidth: number;
|
||||
/** 侧边栏固定按钮是否可见 */
|
||||
fixedButton: boolean;
|
||||
/** 侧边栏是否隐藏 - css */
|
||||
hidden: boolean;
|
||||
/** 混合侧边栏宽度 */
|
||||
mixedWidth: number;
|
||||
/** 侧边栏宽度 */
|
||||
width: number;
|
||||
}
|
||||
@@ -148,6 +285,8 @@ interface SidebarPreferences {
|
||||
interface ShortcutKeyPreferences {
|
||||
/** 是否启用快捷键-全局 */
|
||||
enable: boolean;
|
||||
/** 是否启用全局关闭窗口快捷键 */
|
||||
globalEscape: boolean;
|
||||
/** 是否启用全局锁屏快捷键 */
|
||||
globalLockScreen: boolean;
|
||||
/** 是否启用全局注销快捷键 */
|
||||
@@ -167,6 +306,10 @@ interface TabbarPreferences {
|
||||
height: number;
|
||||
/** 开启标签页缓存功能 */
|
||||
keepAlive: boolean;
|
||||
/** 限制最大数量 */
|
||||
maxCount: number;
|
||||
/** 是否点击中键时关闭标签 */
|
||||
middleClickToClose: boolean;
|
||||
/** 是否持久化标签 */
|
||||
persist: boolean;
|
||||
/** 是否开启多标签页图标 */
|
||||
@@ -175,8 +318,12 @@ interface TabbarPreferences {
|
||||
showMaximize: boolean;
|
||||
/** 显示更多按钮 */
|
||||
showMore: boolean;
|
||||
/** 显示刷新按钮 */
|
||||
showRefresh: boolean;
|
||||
/** 标签页风格 */
|
||||
styleType: TabsStyleType;
|
||||
/** 是否开启访问历史记录 */
|
||||
visitHistory: boolean;
|
||||
/** 是否开启鼠标滚轮响应 */
|
||||
wheelable: boolean;
|
||||
}
|
||||
@@ -192,6 +339,8 @@ interface ThemePreferences {
|
||||
colorSuccess: string;
|
||||
/** 警告色 */
|
||||
colorWarning: string;
|
||||
/** 字体大小(单位:px) */
|
||||
fontSize: number;
|
||||
/** 当前主题 */
|
||||
mode: ThemeModeType;
|
||||
/** 圆角 */
|
||||
@@ -200,6 +349,8 @@ interface ThemePreferences {
|
||||
semiDarkHeader: boolean;
|
||||
/** 是否开启半深色菜单(只在theme='light'时生效) */
|
||||
semiDarkSidebar: boolean;
|
||||
/** 是否开启半深色子菜单(只在theme='light'时生效) */
|
||||
semiDarkSidebarSub: boolean;
|
||||
}
|
||||
|
||||
interface TransitionPreferences {
|
||||
@@ -216,20 +367,42 @@ interface TransitionPreferences {
|
||||
interface WidgetPreferences {
|
||||
/** 是否启用全屏部件 */
|
||||
fullscreen: boolean;
|
||||
/** 全屏按钮位置 */
|
||||
fullscreenButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否启用全局搜索部件 */
|
||||
globalSearch: boolean;
|
||||
/** 全局搜索按钮位置 */
|
||||
globalSearchButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否启用语言切换部件 */
|
||||
languageToggle: boolean;
|
||||
/** 语言切换按钮位置 */
|
||||
languageToggleButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否开启锁屏功能 */
|
||||
lockScreen: boolean;
|
||||
/** 锁屏按钮位置 */
|
||||
lockScreenButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 退出登录按钮位置 */
|
||||
logoutButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否显示通知部件 */
|
||||
notification: boolean;
|
||||
/** 通知按钮位置 */
|
||||
notificationButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 小部件排序 */
|
||||
order: readonly string[];
|
||||
/** 显示刷新按钮 */
|
||||
refresh: boolean;
|
||||
/** 刷新按钮位置 */
|
||||
refreshButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否显示侧边栏显示/隐藏部件 */
|
||||
sidebarToggle: boolean;
|
||||
/** 是否显示主题切换部件 */
|
||||
themeToggle: boolean;
|
||||
/** 主题切换按钮位置 */
|
||||
themeToggleButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
/** 是否显示时区部件 */
|
||||
timezone: boolean;
|
||||
/** 时区按钮位置 */
|
||||
timezoneButtonPosition: 'header' | 'none' | 'user-dropdown';
|
||||
}
|
||||
|
||||
interface Preferences {
|
||||
@@ -263,19 +436,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,
|
||||
|
||||
@@ -66,6 +66,19 @@ function updateCSSVariables(preferences: Preferences) {
|
||||
if (Reflect.has(theme, 'radius')) {
|
||||
document.documentElement.style.setProperty('--radius', `${radius}rem`);
|
||||
}
|
||||
|
||||
// 更新字体大小
|
||||
if (Reflect.has(theme, 'fontSize')) {
|
||||
const fontSize = theme.fontSize;
|
||||
document.documentElement.style.setProperty(
|
||||
'--font-size-base',
|
||||
`${fontSize}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--menu-font-size',
|
||||
`calc(${fontSize}px * 0.875)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { diff } from '@vben-core/shared/utils';
|
||||
import { diff, diffStrict } from '@vben-core/shared/utils';
|
||||
|
||||
import { preferencesManager } from './preferences';
|
||||
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 计算偏好设置的变化
|
||||
* @zh_CN 使用 diffStrict:图标排序等数组字段需顺序敏感比较
|
||||
*/
|
||||
const diffPreference = computed(() => {
|
||||
return diff(initialPreferences, preferences);
|
||||
return diffStrict(initialPreferences, preferences);
|
||||
});
|
||||
|
||||
const diffCustomPreference = computed(() => {
|
||||
return diff(initialCustomPreferences, customPreferences);
|
||||
});
|
||||
|
||||
const appPreferences = computed(() => preferences.app);
|
||||
@@ -29,7 +40,7 @@ function usePreferences() {
|
||||
});
|
||||
|
||||
const locale = computed(() => {
|
||||
return preferences.app.locale;
|
||||
return appPreferences.value.locale;
|
||||
});
|
||||
|
||||
const isMobile = computed(() => {
|
||||
@@ -82,6 +93,20 @@ function usePreferences() {
|
||||
() => appPreferences.value.layout === 'header-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否为头部混合导航模式
|
||||
*/
|
||||
const isHeaderMixedNav = computed(
|
||||
() => appPreferences.value.layout === 'header-mixed-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否为顶部通栏+侧边导航模式
|
||||
*/
|
||||
const isHeaderSidebarNav = computed(
|
||||
() => appPreferences.value.layout === 'header-sidebar-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否为混合导航模式
|
||||
*/
|
||||
@@ -93,7 +118,13 @@ function usePreferences() {
|
||||
* @zh_CN 是否包含侧边导航模式
|
||||
*/
|
||||
const isSideMode = computed(() => {
|
||||
return isMixedNav.value || isSideMixedNav.value || isSideNav.value;
|
||||
return (
|
||||
isMixedNav.value ||
|
||||
isSideMixedNav.value ||
|
||||
isSideNav.value ||
|
||||
isHeaderMixedNav.value ||
|
||||
isHeaderSidebarNav.value
|
||||
);
|
||||
});
|
||||
|
||||
const sidebarCollapsed = computed(() => {
|
||||
@@ -116,7 +147,7 @@ function usePreferences() {
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 登录注册页面布局是否为左侧
|
||||
* @zh_CN 登录注册页面布局是否为右侧
|
||||
*/
|
||||
const authPanelRight = computed(() => {
|
||||
return appPreferences.value.authPageLayout === 'panel-right';
|
||||
@@ -155,6 +186,14 @@ function usePreferences() {
|
||||
return enable && globalLogout;
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 是否启用全局注销快捷键
|
||||
*/
|
||||
const globalEscapeShortcutKey = computed(() => {
|
||||
const { enable, globalEscape } = shortcutKeysPreferences.value;
|
||||
return enable && globalEscape;
|
||||
});
|
||||
|
||||
const globalLockScreenShortcutKey = computed(() => {
|
||||
const { enable, globalLockScreen } = shortcutKeysPreferences.value;
|
||||
return enable && globalLockScreen;
|
||||
@@ -165,12 +204,12 @@ function usePreferences() {
|
||||
*/
|
||||
const preferencesButtonPosition = computed(() => {
|
||||
const { enablePreferences, preferencesButtonPosition } = preferences.app;
|
||||
|
||||
// 如果没有启用偏好设置按钮
|
||||
if (!enablePreferences) {
|
||||
return {
|
||||
fixed: false,
|
||||
header: false,
|
||||
userDropdown: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,12 +220,15 @@ function usePreferences() {
|
||||
const contentIsMaximize = headerHidden && sidebarHidden;
|
||||
|
||||
const isHeaderPosition = preferencesButtonPosition === 'header';
|
||||
const isUserDropdownPosition =
|
||||
preferencesButtonPosition === 'user-dropdown';
|
||||
|
||||
// 如果设置了固定位置
|
||||
if (preferencesButtonPosition !== 'auto') {
|
||||
return {
|
||||
fixed: preferencesButtonPosition === 'fixed',
|
||||
header: isHeaderPosition,
|
||||
userDropdown: isUserDropdownPosition,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,6 +242,7 @@ function usePreferences() {
|
||||
return {
|
||||
fixed,
|
||||
header: !fixed,
|
||||
userDropdown: !fixed && isUserDropdownPosition,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -208,13 +251,18 @@ function usePreferences() {
|
||||
authPanelLeft,
|
||||
authPanelRight,
|
||||
contentIsMaximize,
|
||||
customPreferences,
|
||||
diffPreference,
|
||||
diffCustomPreference,
|
||||
globalLockScreenShortcutKey,
|
||||
globalLogoutShortcutKey,
|
||||
globalEscapeShortcutKey,
|
||||
globalSearchShortcutKey,
|
||||
isDark,
|
||||
isFullContent,
|
||||
isHeaderMixedNav,
|
||||
isHeaderNav,
|
||||
isHeaderSidebarNav,
|
||||
isMixedNav,
|
||||
isMobile,
|
||||
isSideMixedNav,
|
||||
@@ -223,9 +271,11 @@ function usePreferences() {
|
||||
keepAlive,
|
||||
layout,
|
||||
locale,
|
||||
preferencesExtension,
|
||||
preferencesButtonPosition,
|
||||
sidebarCollapsed,
|
||||
theme,
|
||||
app: appPreferences.value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user