更新最新代码

This commit is contained in:
luob
2025-12-24 23:48:38 +08:00
parent e728cf2c5e
commit 1fd17ef73a
1320 changed files with 83513 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest';
import { defaultPreferences } from '../src/config';
describe('defaultPreferences immutability test', () => {
// 创建快照,确保默认配置对象不被修改
it('should not modify the config object', () => {
expect(defaultPreferences).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,253 @@
import { 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;
// 模拟 window.matchMedia 方法
vi.stubGlobal(
'matchMedia',
vi.fn().mockImplementation((query) => ({
addEventListener: vi.fn(),
addListener: vi.fn(), // Deprecated
dispatchEvent: vi.fn(),
matches: query === '(prefers-color-scheme: dark)',
media: query,
onchange: null,
removeEventListener: vi.fn(),
removeListener: vi.fn(), // Deprecated
})),
);
beforeEach(() => {
preferenceManager = new PreferenceManager();
});
it('loads default preferences if no saved preferences found', () => {
const preferences = preferenceManager.getPreferences();
expect(preferences).toEqual(defaultPreferences);
});
it('initializes preferences with overrides', async () => {
const overrides: any = {
app: {
locale: 'en-US',
},
};
await preferenceManager.initPreferences({
namespace: 'testNamespace',
overrides,
});
// 等待防抖动操作完成
// await new Promise((resolve) => setTimeout(resolve, 300)); // 等待100毫秒
const expected = {
...defaultPreferences,
app: {
...defaultPreferences.app,
...overrides.app,
},
};
expect(preferenceManager.getPreferences()).toEqual(expected);
});
it('updates theme mode correctly', () => {
preferenceManager.updatePreferences({
theme: {
mode: 'light',
},
});
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
});
it('updates color modes correctly', () => {
preferenceManager.updatePreferences({
app: { colorGrayMode: true, colorWeakMode: true },
});
expect(preferenceManager.getPreferences().app.colorGrayMode).toBe(true);
expect(preferenceManager.getPreferences().app.colorWeakMode).toBe(true);
});
it('resets preferences to default', () => {
// 先更新一些偏好设置
preferenceManager.updatePreferences({
theme: {
mode: 'light',
},
});
// 然后重置偏好设置
preferenceManager.resetPreferences();
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
});
it('updates isMobile correctly', () => {
// 模拟移动端状态
vi.stubGlobal(
'matchMedia',
vi.fn().mockImplementation((query) => ({
addEventListener: vi.fn(),
addListener: vi.fn(),
dispatchEvent: vi.fn(),
matches: query === '(max-width: 768px)',
media: query,
onchange: null,
removeEventListener: vi.fn(),
removeListener: vi.fn(),
})),
);
preferenceManager.updatePreferences({
app: { isMobile: true },
});
expect(preferenceManager.getPreferences().app.isMobile).toBe(true);
});
it('updates the locale preference correctly', () => {
preferenceManager.updatePreferences({
app: { locale: 'en-US' },
});
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
});
it('updates the sidebar width correctly', () => {
preferenceManager.updatePreferences({
sidebar: { width: 200 },
});
expect(preferenceManager.getPreferences().sidebar.width).toBe(200);
});
it('updates the sidebar collapse state correctly', () => {
preferenceManager.updatePreferences({
sidebar: { collapsed: true },
});
expect(preferenceManager.getPreferences().sidebar.collapsed).toBe(true);
});
it('updates the navigation style type correctly', () => {
preferenceManager.updatePreferences({
navigation: { styleType: 'flat' },
} as any);
expect(preferenceManager.getPreferences().navigation.styleType).toBe(
'flat',
);
});
it('resets preferences to default correctly', () => {
// 先更新一些偏好设置
preferenceManager.updatePreferences({
app: { locale: 'en-US' },
sidebar: { collapsed: true, width: 200 },
theme: {
mode: 'light',
},
});
// 然后重置偏好设置
preferenceManager.resetPreferences();
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
});
it('does not update undefined preferences', () => {
const originalPreferences = preferenceManager.getPreferences();
preferenceManager.updatePreferences({
app: { nonexistentField: 'value' },
} as any);
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
});
it('reverts to default when a preference field is deleted', () => {
preferenceManager.updatePreferences({
app: { locale: 'en-US' },
});
preferenceManager.updatePreferences({
app: { locale: undefined },
});
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
});
it('ignores updates with invalid preference value types', () => {
const originalPreferences = preferenceManager.getPreferences();
preferenceManager.updatePreferences({
app: { isMobile: 'true' as unknown as boolean }, // 错误类型
});
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
});
it('merges nested preference objects correctly', () => {
preferenceManager.updatePreferences({
app: { name: 'New App Name' },
});
const expected = {
...defaultPreferences,
app: {
...defaultPreferences.app,
name: 'New App Name',
},
};
expect(preferenceManager.getPreferences()).toEqual(expected);
});
it('applies updates immediately after initialization', async () => {
const overrides: any = {
app: {
locale: 'en-US',
},
};
await preferenceManager.initPreferences(overrides);
preferenceManager.updatePreferences({
theme: { mode: 'light' },
});
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
});
});
describe('isDarkTheme', () => {
it('should return true for dark theme', () => {
expect(isDarkTheme('dark')).toBe(true);
});
it('should return false for light theme', () => {
expect(isDarkTheme('light')).toBe(false);
});
it('should return system preference for auto theme', () => {
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
addEventListener: vi.fn(),
addListener: vi.fn(), // Deprecated
dispatchEvent: vi.fn(),
matches: query === '(prefers-color-scheme: dark)',
media: query,
onchange: null,
removeEventListener: vi.fn(),
removeListener: vi.fn(), // Deprecated
}));
expect(isDarkTheme('auto')).toBe(true);
expect(window.matchMedia).toHaveBeenCalledWith(
'(prefers-color-scheme: dark)',
);
});
});

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,35 @@
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,
updatePreferences,
};
export * from './constants';
export type * from './types';
export * from './use-preferences';

View File

@@ -0,0 +1,116 @@
import type { Preferences } from './types';
import { generatorColorVariables } from '@vben-core/shared/color';
import { updateCSSVariables as executeUpdateCSSVariables } from '@vben-core/shared/utils';
import { BUILT_IN_THEME_PRESETS } from './constants';
/**
* 更新主题的 CSS 变量以及其他 CSS 变量
* @param preferences - 当前偏好设置对象,它的主题值将被用来设置文档的主题。
*/
function updateCSSVariables(preferences: Preferences) {
// 当修改到颜色变量时,更新 css 变量
const root = document.documentElement;
if (!root) {
return;
}
const theme = preferences?.theme ?? {};
const { builtinType, mode, radius } = theme;
// html 设置 dark 类
if (Reflect.has(theme, 'mode')) {
const dark = isDarkTheme(mode);
root.classList.toggle('dark', dark);
}
// html 设置 data-theme=[builtinType]
if (Reflect.has(theme, 'builtinType')) {
const rootTheme = root.dataset.theme;
if (rootTheme !== builtinType) {
root.dataset.theme = builtinType;
}
}
// 获取当前的内置主题
const currentBuiltType = [...BUILT_IN_THEME_PRESETS].find(
(item) => item.type === builtinType,
);
let builtinTypeColorPrimary: string | undefined = '';
if (currentBuiltType) {
const isDark = isDarkTheme(preferences.theme.mode);
// 设置不同主题的主要颜色
const color = isDark
? currentBuiltType.darkPrimaryColor || currentBuiltType.primaryColor
: currentBuiltType.primaryColor;
builtinTypeColorPrimary = color || currentBuiltType.color;
}
// 如果内置主题颜色和自定义颜色都不存在,则不更新主题颜色
if (
builtinTypeColorPrimary ||
Reflect.has(theme, 'colorPrimary') ||
Reflect.has(theme, 'colorDestructive') ||
Reflect.has(theme, 'colorSuccess') ||
Reflect.has(theme, 'colorWarning')
) {
// preferences.theme.colorPrimary = builtinTypeColorPrimary || colorPrimary;
updateMainColorVariables(preferences);
}
// 更新圆角
if (Reflect.has(theme, 'radius')) {
document.documentElement.style.setProperty('--radius', `${radius}rem`);
}
}
/**
* 更新主要的 CSS 变量
* @param preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。
*/
function updateMainColorVariables(preference: Preferences) {
if (!preference.theme) {
return;
}
const { colorDestructive, colorPrimary, colorSuccess, colorWarning } =
preference.theme;
const colorVariables = generatorColorVariables([
{ color: colorPrimary, name: 'primary' },
{ alias: 'warning', color: colorWarning, name: 'yellow' },
{ alias: 'success', color: colorSuccess, name: 'green' },
{ alias: 'destructive', color: colorDestructive, name: 'red' },
]);
// 要设置的 CSS 变量映射
const colorMappings = {
'--green-500': '--success',
'--primary-500': '--primary',
'--red-500': '--destructive',
'--yellow-500': '--warning',
};
// 统一处理颜色变量的更新
Object.entries(colorMappings).forEach(([sourceVar, targetVar]) => {
const colorValue = colorVariables[sourceVar];
if (colorValue) {
document.documentElement.style.setProperty(targetVar, colorValue);
}
});
executeUpdateCSSVariables(colorVariables);
}
function isDarkTheme(theme: string) {
let dark = theme === 'dark';
if (theme === 'auto') {
dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return dark;
}
export { isDarkTheme, updateCSSVariables };

View File

@@ -0,0 +1,254 @@
import { computed } from 'vue';
import { diff } from '@vben-core/shared/utils';
import { preferencesManager } from './preferences';
import { isDarkTheme } from './update-css-variables';
function usePreferences() {
const preferences = preferencesManager.getPreferences();
const initialPreferences = preferencesManager.getInitialPreferences();
/**
* @zh_CN 计算偏好设置的变化
*/
const diffPreference = computed(() => {
return diff(initialPreferences, preferences);
});
const appPreferences = computed(() => preferences.app);
const shortcutKeysPreferences = computed(() => preferences.shortcutKeys);
/**
* @zh_CN 判断是否为暗黑模式
* @param preferences - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。
* @returns 如果主题为暗黑模式,返回 true否则返回 false。
*/
const isDark = computed(() => {
return isDarkTheme(preferences.theme.mode);
});
const locale = computed(() => {
return preferences.app.locale;
});
const isMobile = computed(() => {
return appPreferences.value.isMobile;
});
const theme = computed(() => {
return isDark.value ? 'dark' : 'light';
});
/**
* @zh_CN 布局方式
*/
const layout = computed(() =>
isMobile.value ? 'sidebar-nav' : appPreferences.value.layout,
);
/**
* @zh_CN 是否显示顶栏
*/
const isShowHeaderNav = computed(() => {
return preferences.header.enable;
});
/**
* @zh_CN 是否全屏显示content不需要侧边、底部、顶部、tab区域
*/
const isFullContent = computed(
() => appPreferences.value.layout === 'full-content',
);
/**
* @zh_CN 是否侧边导航模式
*/
const isSideNav = computed(
() => appPreferences.value.layout === 'sidebar-nav',
);
/**
* @zh_CN 是否侧边混合模式
*/
const isSideMixedNav = computed(
() => appPreferences.value.layout === 'sidebar-mixed-nav',
);
/**
* @zh_CN 是否为头部导航模式
*/
const isHeaderNav = computed(
() => 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 是否为混合导航模式
*/
const isMixedNav = computed(
() => appPreferences.value.layout === 'mixed-nav',
);
/**
* @zh_CN 是否包含侧边导航模式
*/
const isSideMode = computed(() => {
return (
isMixedNav.value ||
isSideMixedNav.value ||
isSideNav.value ||
isHeaderMixedNav.value ||
isHeaderSidebarNav.value
);
});
const sidebarCollapsed = computed(() => {
return preferences.sidebar.collapsed;
});
/**
* @zh_CN 是否开启keep-alive
* 在tabs可见以及开启keep-alive的情况下才开启
*/
const keepAlive = computed(
() => preferences.tabbar.enable && preferences.tabbar.keepAlive,
);
/**
* @zh_CN 登录注册页面布局是否为左侧
*/
const authPanelLeft = computed(() => {
return appPreferences.value.authPageLayout === 'panel-left';
});
/**
* @zh_CN 登录注册页面布局是否为左侧
*/
const authPanelRight = computed(() => {
return appPreferences.value.authPageLayout === 'panel-right';
});
/**
* @zh_CN 登录注册页面布局是否为中间
*/
const authPanelCenter = computed(() => {
return appPreferences.value.authPageLayout === 'panel-center';
});
/**
* @zh_CN 内容是否已经最大化
* 排除 full-content模式
*/
const contentIsMaximize = computed(() => {
const headerIsHidden = preferences.header.hidden;
const sidebarIsHidden = preferences.sidebar.hidden;
return headerIsHidden && sidebarIsHidden && !isFullContent.value;
});
/**
* @zh_CN 是否启用全局搜索快捷键
*/
const globalSearchShortcutKey = computed(() => {
const { enable, globalSearch } = shortcutKeysPreferences.value;
return enable && globalSearch;
});
/**
* @zh_CN 是否启用全局注销快捷键
*/
const globalLogoutShortcutKey = computed(() => {
const { enable, globalLogout } = shortcutKeysPreferences.value;
return enable && globalLogout;
});
const globalLockScreenShortcutKey = computed(() => {
const { enable, globalLockScreen } = shortcutKeysPreferences.value;
return enable && globalLockScreen;
});
/**
* @zh_CN 偏好设置按钮位置
*/
const preferencesButtonPosition = computed(() => {
const { enablePreferences, preferencesButtonPosition } = preferences.app;
// 如果没有启用偏好设置按钮
if (!enablePreferences) {
return {
fixed: false,
header: false,
};
}
const { header, sidebar } = preferences;
const headerHidden = header.hidden;
const sidebarHidden = sidebar.hidden;
const contentIsMaximize = headerHidden && sidebarHidden;
const isHeaderPosition = preferencesButtonPosition === 'header';
// 如果设置了固定位置
if (preferencesButtonPosition !== 'auto') {
return {
fixed: preferencesButtonPosition === 'fixed',
header: isHeaderPosition,
};
}
// 如果是全屏模式或者没有固定在顶部,
const fixed =
contentIsMaximize ||
isFullContent.value ||
isMobile.value ||
!isShowHeaderNav.value;
return {
fixed,
header: !fixed,
};
});
return {
authPanelCenter,
authPanelLeft,
authPanelRight,
contentIsMaximize,
diffPreference,
globalLockScreenShortcutKey,
globalLogoutShortcutKey,
globalSearchShortcutKey,
isDark,
isFullContent,
isHeaderMixedNav,
isHeaderNav,
isHeaderSidebarNav,
isMixedNav,
isMobile,
isSideMixedNav,
isSideMode,
isSideNav,
keepAlive,
layout,
locale,
preferencesButtonPosition,
sidebarCollapsed,
theme,
};
}
export { usePreferences };

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src", "__tests__"],
"exclude": ["node_modules"]
}