Browse Source

refactor: refacotr preference

vben 9 months ago
parent
commit
fed47f5e05
100 changed files with 1675 additions and 655 deletions
  1. 3 2
      apps/antd-view/package.json
  2. 5 5
      apps/antd-view/src/app.vue
  3. 4 3
      apps/antd-view/src/bootstrap.ts
  4. 4 3
      apps/antd-view/src/layouts/basic.vue
  5. 3 3
      apps/antd-view/src/layouts/index.ts
  6. 6 4
      apps/antd-view/src/main.ts
  7. 0 7
      apps/antd-view/src/preference.ts
  8. 11 0
      apps/antd-view/src/preferences.ts
  9. 1 1
      apps/antd-view/src/router/guard/access.ts
  10. 5 5
      apps/antd-view/src/router/guard/index.ts
  11. 2 2
      apps/antd-view/src/router/routes/_essential.ts
  12. 2 2
      apps/antd-view/src/router/routes/modules/vben.ts
  13. 2 1
      apps/antd-view/src/services/request.ts
  14. 16 0
      apps/antd-view/src/store/index.ts
  15. 24 0
      apps/antd-view/src/store/modules/example.test.ts
  16. 13 0
      apps/antd-view/src/store/modules/example.ts
  17. 2 1
      apps/antd-view/src/views/_essential/authentication/login.vue
  18. 2 2
      internal/lint-configs/eslint-config/package.json
  19. 2 2
      internal/lint-configs/prettier-config/package.json
  20. 1 1
      internal/lint-configs/stylelint-config/package.json
  21. 1 1
      internal/node-utils/package.json
  22. 1 1
      internal/vite-config/package.json
  23. 1 1
      package.json
  24. 1 1
      packages/@vben-core/README.md
  25. 3 0
      packages/@vben-core/forward/README.md
  26. 0 0
      packages/@vben-core/forward/preferences/build.config.ts
  27. 4 2
      packages/@vben-core/forward/preferences/package.json
  28. 77 0
      packages/@vben-core/forward/preferences/src/config.ts
  29. 26 0
      packages/@vben-core/forward/preferences/src/constants.ts
  30. 32 0
      packages/@vben-core/forward/preferences/src/index.ts
  31. 289 0
      packages/@vben-core/forward/preferences/src/preferences.ts
  32. 189 0
      packages/@vben-core/forward/preferences/src/types.ts
  33. 25 21
      packages/@vben-core/forward/preferences/src/use-preferences.ts
  34. 0 0
      packages/@vben-core/forward/preferences/tsconfig.json
  35. 0 0
      packages/@vben-core/forward/request/.gitkeep
  36. 0 0
      packages/@vben-core/forward/stores/build.config.ts
  37. 2 2
      packages/@vben-core/forward/stores/package.json
  38. 0 0
      packages/@vben-core/forward/stores/shim-pinia.d.ts
  39. 0 0
      packages/@vben-core/forward/stores/src/index.ts
  40. 2 2
      packages/@vben-core/forward/stores/src/modules/access.test.ts
  41. 0 0
      packages/@vben-core/forward/stores/src/modules/access.ts
  42. 0 0
      packages/@vben-core/forward/stores/src/modules/index.ts
  43. 0 0
      packages/@vben-core/forward/stores/src/modules/tabs.ts
  44. 8 9
      packages/@vben-core/forward/stores/src/setup.ts
  45. 0 0
      packages/@vben-core/forward/stores/tsconfig.json
  46. 7 0
      packages/@vben-core/helpers/build.config.ts
  47. 45 0
      packages/@vben-core/helpers/package.json
  48. 1 0
      packages/@vben-core/helpers/src/index.ts
  49. 245 0
      packages/@vben-core/helpers/src/object.test.ts
  50. 164 0
      packages/@vben-core/helpers/src/object.ts
  51. 5 0
      packages/@vben-core/helpers/tsconfig.json
  52. 1 1
      packages/@vben-core/shared/chche/src/index.ts
  53. 0 104
      packages/@vben-core/shared/chche/src/storage-cache.test.ts
  54. 0 145
      packages/@vben-core/shared/chche/src/storage-cache.ts
  55. 130 0
      packages/@vben-core/shared/chche/src/storage-manager.test.ts
  56. 118 0
      packages/@vben-core/shared/chche/src/storage-manager.ts
  57. 1 1
      packages/@vben-core/shared/design/src/tailwind.css
  58. 1 0
      packages/@vben-core/shared/toolkit/src/index.ts
  59. 55 0
      packages/@vben-core/shared/toolkit/src/letter.test.ts
  60. 20 0
      packages/@vben-core/shared/toolkit/src/letter.ts
  61. 22 0
      packages/@vben-core/shared/typings/src/app.ts
  62. 40 0
      packages/@vben-core/shared/typings/src/flatten.d.ts
  63. 2 1
      packages/@vben-core/shared/typings/src/index.ts
  64. 0 144
      packages/@vben-core/shared/typings/src/preference.ts
  65. 4 4
      packages/@vben-core/uikit/layout-ui/src/vben-layout.ts
  66. 1 1
      packages/@vben-core/uikit/layout-ui/src/vben-layout.vue
  67. 2 2
      packages/@vben-core/uikit/menu-ui/src/interface/index.ts
  68. 1 1
      packages/@vben-core/uikit/shadcn-ui/src/components/ui/breadcrumb/BreadcrumbLink.vue
  69. 1 1
      packages/@vben-core/uikit/shadcn-ui/src/components/ui/popover/PopoverContent.vue
  70. 1 0
      packages/README.md
  71. 1 1
      packages/business/common-ui/package.json
  72. 13 13
      packages/business/common-ui/src/authentication/color-toggle.vue
  73. 3 12
      packages/business/common-ui/src/authentication/layout-toggle.vue
  74. 1 1
      packages/business/common-ui/src/index.ts
  75. 13 11
      packages/business/common-ui/src/language-toggle/language-toggle.vue
  76. 0 1
      packages/business/common-ui/src/preference/index.ts
  77. 0 102
      packages/business/common-ui/src/preference/preference-widget.vue
  78. 0 16
      packages/business/common-ui/src/preference/use-open-preference.ts
  79. 0 0
      packages/business/common-ui/src/preferences/blocks/block.vue
  80. 0 0
      packages/business/common-ui/src/preferences/blocks/general/animation.vue
  81. 6 7
      packages/business/common-ui/src/preferences/blocks/general/general.vue
  82. 0 0
      packages/business/common-ui/src/preferences/blocks/index.ts
  83. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/breadcrumb.vue
  84. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/content.vue
  85. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/footer.vue
  86. 2 2
      packages/business/common-ui/src/preferences/blocks/layout/header.vue
  87. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/interface-control.vue
  88. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/layout.vue
  89. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/navigation.vue
  90. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/sidebar.vue
  91. 0 0
      packages/business/common-ui/src/preferences/blocks/layout/tabs.vue
  92. 0 0
      packages/business/common-ui/src/preferences/blocks/select-item.vue
  93. 0 0
      packages/business/common-ui/src/preferences/blocks/switch-item.vue
  94. 0 0
      packages/business/common-ui/src/preferences/blocks/theme/color-mode.vue
  95. 0 0
      packages/business/common-ui/src/preferences/blocks/theme/color.vue
  96. 0 0
      packages/business/common-ui/src/preferences/blocks/theme/theme.vue
  97. 0 0
      packages/business/common-ui/src/preferences/blocks/toggle-item.vue
  98. 0 0
      packages/business/common-ui/src/preferences/icons/content-compact.vue
  99. 0 0
      packages/business/common-ui/src/preferences/icons/full-content.vue
  100. 0 0
      packages/business/common-ui/src/preferences/icons/header-nav.vue

+ 3 - 2
apps/antd-view/package.json

@@ -22,14 +22,14 @@
     "typecheck": "vue-tsc --noEmit --skipLibCheck"
   },
   "dependencies": {
+    "@vben-core/preferences": "workspace:*",
+    "@vben-core/stores": "workspace:*",
     "@vben/common-ui": "workspace:*",
     "@vben/constants": "workspace:*",
     "@vben/hooks": "workspace:*",
     "@vben/icons": "workspace:*",
     "@vben/layouts": "workspace:*",
     "@vben/locales": "workspace:*",
-    "@vben/preference": "workspace:*",
-    "@vben/stores": "workspace:*",
     "@vben/styles": "workspace:*",
     "@vben/types": "workspace:*",
     "@vben/utils": "workspace:*",
@@ -37,6 +37,7 @@
     "ant-design-vue": "^4.2.1",
     "axios": "^1.7.2",
     "dayjs": "^1.11.11",
+    "pinia": "2.1.7",
     "vue": "3.4.27",
     "vue-router": "^4.3.2"
   },

+ 5 - 5
apps/antd-view/src/app.vue

@@ -1,8 +1,9 @@
 <script lang="ts" setup>
 import 'dayjs/locale/zh-cn';
 
+import { preferences, usePreferences } from '@vben-core/preferences';
+
 import { GlobalProvider } from '@vben/common-ui';
-import { preference, usePreference } from '@vben/preference';
 import { ConfigProvider, theme } from 'ant-design-vue';
 import zhCN from 'ant-design-vue/es/locale/zh_CN';
 import dayjs from 'dayjs';
@@ -12,21 +13,20 @@ defineOptions({ name: 'App' });
 
 dayjs.locale(zhCN.locale);
 
-const { isDark } = usePreference();
+const { isDark } = usePreferences();
 
 const tokenTheme = computed(() => {
-  const { colorPrimary, compact } = preference;
   const algorithms = isDark.value
     ? [theme.darkAlgorithm]
     : [theme.defaultAlgorithm];
 
   // antd 紧凑模式算法
-  if (compact) {
+  if (preferences.app.compact) {
     algorithms.push(theme.compactAlgorithm);
   }
   return {
     algorithms,
-    token: { colorPrimary },
+    token: { colorPrimary: preferences.theme.colorPrimary },
   };
 });
 </script>

+ 4 - 3
apps/antd-view/src/bootstrap.ts

@@ -1,8 +1,9 @@
 import '@vben/styles';
 
+import { preferences } from '@vben-core/preferences';
+
+import { setupStore } from '@/store';
 import { setupI18n } from '@vben/locales';
-import { preference } from '@vben/preference';
-import { setupStore } from '@vben/stores';
 import { createApp } from 'vue';
 
 import App from './app.vue';
@@ -12,7 +13,7 @@ async function bootstrap(namespace: string) {
   const app = createApp(App);
 
   // 国际化 i18n 配置
-  await setupI18n(app, { defaultLocale: preference.locale });
+  await setupI18n(app, { defaultLocale: preferences.app.locale });
 
   // 配置 pinia-store
   await setupStore(app, { namespace });

+ 4 - 3
apps/antd-view/src/layouts/basic.vue

@@ -1,12 +1,13 @@
 <script lang="ts" setup>
 import type { NotificationItem } from '@vben/common-ui';
 
+import { preferences } from '@vben-core/preferences';
+import { useAccessStore } from '@vben-core/stores';
+
 import { Notification, UserDropdown } from '@vben/common-ui';
 import { IcRoundCreditScore, MdiDriveDocument, MdiGithub } from '@vben/icons';
 import { BasicLayout } from '@vben/layouts';
 import { $t } from '@vben/locales';
-import { preference } from '@vben/preference';
-import { useAccessStore } from '@vben/stores';
 import { openWindow } from '@vben/utils';
 import { computed, ref } from 'vue';
 import { useRouter } from 'vue-router';
@@ -93,7 +94,7 @@ function handleNoticeClear() {
   <BasicLayout>
     <template #user-dropdown>
       <UserDropdown
-        :avatar="preference.defaultAvatar"
+        :avatar="preferences.app.defaultAvatar"
         :menus="menus"
         text="Vben Admin"
         description="ann.vben@gmail.com"

+ 3 - 3
apps/antd-view/src/layouts/index.ts

@@ -2,7 +2,7 @@ const BasicLayout = () => import('./basic.vue');
 
 const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
 
-const AuthPageLayout = () =>
-  import('@vben/layouts').then((m) => m.AuthPageLayout);
+const AuthPageLayoutType = () =>
+  import('@vben/layouts').then((m) => m.AuthPageLayoutType);
 
-export { AuthPageLayout, BasicLayout, IFrameView };
+export { AuthPageLayoutType, BasicLayout, IFrameView };

+ 6 - 4
apps/antd-view/src/main.ts

@@ -1,18 +1,20 @@
-import { setupPreference } from '@vben/preference';
+import { preferencesManager } from '@vben-core/preferences';
 
-import { overridesPreference } from './preference';
+import { overridesPreferences } from './preferences';
 
 /**
  * 应用初始化完成之后再进行页面加载渲染
  */
 async function initApplication() {
+  // name用于指定项目唯一标识
+  // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
   const env = import.meta.env.PROD ? 'prod' : 'dev';
   const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${env}`;
 
   // app偏好设置初始化
-  await setupPreference({
+  await preferencesManager.initPreferences({
     namespace,
-    overrides: overridesPreference,
+    overrides: overridesPreferences,
   });
 
   import('./bootstrap').then((m) => m.bootstrap(namespace));

+ 0 - 7
apps/antd-view/src/preference.ts

@@ -1,7 +0,0 @@
-import type { Preference } from '@vben/types';
-
-/**
- * @description 项目配置文件
- * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
- */
-export const overridesPreference: Partial<Preference> = {};

+ 11 - 0
apps/antd-view/src/preferences.ts

@@ -0,0 +1,11 @@
+import type { DeepPartial, Preferences } from '@vben/types';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ */
+export const overridesPreferences: DeepPartial<Preferences> = {
+  app: {
+    name: 'Vben Admin',
+  },
+};

+ 1 - 1
apps/antd-view/src/router/guard/access.ts

@@ -1,9 +1,9 @@
 import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben/types';
 
+import { useAccessStore } from '@vben-core/stores';
 import type { RouteRecordRaw, Router } from 'vue-router';
 
 import { LOGIN_PATH } from '@vben/constants';
-import { useAccessStore } from '@vben/stores';
 import { filterTree, mapTree, traverseTreeValues } from '@vben/utils';
 
 import { dynamicRoutes } from '@/router/routes';

+ 5 - 5
apps/antd-view/src/router/guard/index.ts

@@ -1,7 +1,7 @@
+import { preferences } from '@vben-core/preferences';
 import type { Router } from 'vue-router';
 
 import { $t } from '@vben/locales';
-import { preference } from '@vben/preference';
 import { startProgress, stopProgress } from '@vben/utils';
 import { useTitle } from '@vueuse/core';
 
@@ -17,7 +17,7 @@ function configCommonGuard(router: Router) {
 
   router.beforeEach(async (to) => {
     // 页面加载进度条
-    if (preference.pageProgress) {
+    if (preferences.transition.progress) {
       startProgress();
     }
     to.meta.loaded = loadedPaths.has(to.path);
@@ -29,14 +29,14 @@ function configCommonGuard(router: Router) {
     loadedPaths.add(to.path);
 
     // 关闭页面加载进度条
-    if (preference.pageProgress) {
+    if (preferences.transition.progress) {
       stopProgress();
     }
 
     // 动态修改标题
-    if (preference.dynamicTitle) {
+    if (preferences.app.dynamicTitle) {
       const { title } = to.meta;
-      useTitle(`${$t(title)} - ${preference.appName}`);
+      useTitle(`${$t(title)} - ${preferences.app.name}`);
     }
   });
 }

+ 2 - 2
apps/antd-view/src/router/routes/_essential.ts

@@ -1,6 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { AuthPageLayout } from '@/layouts';
+import { AuthPageLayoutType } from '@/layouts';
 import { Fallback } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
@@ -9,7 +9,7 @@ import Login from '@/views/_essential/authentication/login.vue';
 /** 基本路由,这些路由是必须存在的 */
 const essentialRoutes: RouteRecordRaw[] = [
   {
-    component: AuthPageLayout,
+    component: AuthPageLayoutType,
     meta: {
       title: 'Authentication',
     },

+ 2 - 2
apps/antd-view/src/router/routes/modules/vben.ts

@@ -1,15 +1,15 @@
+import { preferences } from '@vben-core/preferences';
 import type { RouteRecordRaw } from 'vue-router';
 
 import { BasicLayout, IFrameView } from '@/layouts';
 import { VBEN_GITHUB_URL } from '@vben/constants';
 import { $t } from '@vben/locales/helper';
-import { preference } from '@vben/preference';
 
 export const vbenRoutes: RouteRecordRaw[] = [
   {
     component: BasicLayout,
     meta: {
-      icon: preference.logo,
+      icon: preferences.logo.source,
       title: 'Vben',
     },
     name: 'AboutLayout',

+ 2 - 1
apps/antd-view/src/services/request.ts

@@ -2,7 +2,8 @@
  * 该文件可自行根据业务逻辑进行调整
  */
 
-import { useAccessStore } from '@vben/stores';
+import { useAccessStore } from '@vben-core/stores';
+
 import { message } from 'ant-design-vue';
 import axios, {
   AxiosError,

+ 16 - 0
apps/antd-view/src/store/index.ts

@@ -0,0 +1,16 @@
+import type { InitStoreOptions } from '@vben-core/stores';
+
+import { initStore } from '@vben-core/stores';
+
+import type { App } from 'vue';
+
+/**
+ * @zh_CN 初始化pinia
+ * @param app vue app 实例
+ */
+async function setupStore(app: App, options: InitStoreOptions) {
+  const pinia = await initStore(options);
+  app.use(pinia);
+}
+
+export { setupStore };

+ 24 - 0
apps/antd-view/src/store/modules/example.test.ts

@@ -0,0 +1,24 @@
+import { createPinia, setActivePinia } from 'pinia';
+import {
+  //  beforeEach,
+  describe,
+  // expect,
+  it,
+} from 'vitest';
+
+// import { useAccessStore } from '../modules/access';
+
+describe('useCounterStore', () => {
+  it('app Name with test', () => {
+    setActivePinia(createPinia());
+    // let referenceStore = usePreferencesStore();
+
+    // beforeEach(() => {
+    //   referenceStore = usePreferencesStore();
+    // });
+
+    // expect(referenceStore.appName).toBe('vben-admin');
+    // referenceStore.setAppName('vbenAdmin');
+    // expect(referenceStore.getAppName).toBe('vbenAdmin');
+  });
+});

+ 13 - 0
apps/antd-view/src/store/modules/example.ts

@@ -0,0 +1,13 @@
+import { defineStore } from 'pinia';
+
+export const useCounterStore = defineStore('counter', {
+  actions: {
+    increment() {
+      this.count++;
+    },
+  },
+  getters: {
+    double: (state) => state.count * 2,
+  },
+  state: () => ({ count: 0 }),
+});

+ 2 - 1
apps/antd-view/src/views/_essential/authentication/login.vue

@@ -1,11 +1,12 @@
 <script lang="ts" setup>
 import type { LoginAndRegisterParams } from '@vben/common-ui';
 
+import { useAccessStore } from '@vben-core/stores';
+
 import { getUserInfo, userLogin } from '@/services';
 import { AuthenticationLogin } from '@vben/common-ui';
 import { useRequest } from '@vben/hooks';
 import { $t } from '@vben/locales';
-import { useAccessStore } from '@vben/stores';
 import { notification } from 'ant-design-vue';
 import { computed } from 'vue';
 import { useRouter } from 'vue-router';

+ 2 - 2
internal/lint-configs/eslint-config/package.json

@@ -33,7 +33,7 @@
     "eslint-plugin-command": "^0.2.3"
   },
   "devDependencies": {
-    "@eslint/js": "^9.3.0",
+    "@eslint/js": "^9.4.0",
     "@types/eslint": "^8.56.10",
     "@typescript-eslint/eslint-plugin": "^7.11.0",
     "@typescript-eslint/parser": "^7.11.0",
@@ -54,6 +54,6 @@
     "eslint-plugin-vue": "^9.26.0",
     "globals": "^15.3.0",
     "jsonc-eslint-parser": "^2.4.0",
-    "vue-eslint-parser": "^9.4.2"
+    "vue-eslint-parser": "^9.4.3"
   }
 }

+ 2 - 2
internal/lint-configs/prettier-config/package.json

@@ -31,7 +31,7 @@
     }
   },
   "dependencies": {
-    "prettier": "^3.2.5",
-    "prettier-plugin-tailwindcss": "^0.6.0"
+    "prettier": "3.3.0",
+    "prettier-plugin-tailwindcss": "^0.6.1"
   }
 }

+ 1 - 1
internal/lint-configs/stylelint-config/package.json

@@ -39,7 +39,7 @@
     "postcss": "^8.4.38",
     "postcss-html": "^1.7.0",
     "postcss-scss": "^4.0.9",
-    "prettier": "^3.2.5",
+    "prettier": "3.3.0",
     "stylelint": "^16.6.1",
     "stylelint-config-recommended": "^14.0.0",
     "stylelint-config-recommended-scss": "^14.0.0",

+ 1 - 1
internal/node-utils/package.json

@@ -36,7 +36,7 @@
     "consola": "^3.2.3",
     "find-up": "^7.0.0",
     "pkg-types": "^1.1.1",
-    "prettier": "^3.2.5",
+    "prettier": "3.3.0",
     "rimraf": "^5.0.7",
     "zx": "^7.2.3"
   }

+ 1 - 1
internal/vite-config/package.json

@@ -41,7 +41,7 @@
   "devDependencies": {
     "@types/html-minifier-terser": "^7.0.2",
     "@vben/node-utils": "workspace:*",
-    "@vitejs/plugin-vue": "^5.0.4",
+    "@vitejs/plugin-vue": "^5.0.5",
     "@vitejs/plugin-vue-jsx": "^4.0.0",
     "dayjs": "^1.11.11",
     "dotenv": "^16.4.5",

+ 1 - 1
package.json

@@ -47,7 +47,7 @@
     "@changesets/cli": "^2.27.5",
     "@ls-lint/ls-lint": "^2.2.3",
     "@types/jsdom": "^21.1.7",
-    "@types/node": "^20.12.13",
+    "@types/node": "^20.13.0",
     "@vben/commitlint-config": "workspace:*",
     "@vben/eslint-config": "workspace:*",
     "@vben/lint-staged-config": "workspace:*",

+ 1 - 1
packages/@vben-core/README.md

@@ -1,3 +1,3 @@
 # @vben-core
 
-系统一些比较基础的SDK和UI组件库,请勿将任何业务逻辑和业务包放在这里
+系统一些比较基础的SDK和UI组件库,该目录后续可能会迁移出去或者发布到npm,请勿将任何业务逻辑和业务包放在该目录

+ 3 - 0
packages/@vben-core/forward/README.md

@@ -0,0 +1,3 @@
+# @vben-core/forward
+
+该目录内的包,可直接被app所引用

+ 0 - 0
packages/preference/build.config.ts → packages/@vben-core/forward/preferences/build.config.ts


+ 4 - 2
packages/preference/package.json → packages/@vben-core/forward/preferences/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "@vben/preference",
+  "name": "@vben-core/preferences",
   "version": "1.0.0",
   "type": "module",
   "license": "MIT",
@@ -7,7 +7,7 @@
   "repository": {
     "type": "git",
     "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
-    "directory": "packages/preference"
+    "directory": "packages/@vben-core/preferences"
   },
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "scripts": {
@@ -32,6 +32,8 @@
     }
   },
   "dependencies": {
+    "@vben-core/cache": "workspace:*",
+    "@vben-core/helpers": "workspace:*",
     "@vben-core/toolkit": "workspace:*",
     "@vben-core/typings": "workspace:*",
     "@vueuse/core": "^10.10.0",

+ 77 - 0
packages/@vben-core/forward/preferences/src/config.ts

@@ -0,0 +1,77 @@
+import type { Preferences } from './types';
+
+const defaultPreferences: Preferences = {
+  app: {
+    authPageLayout: 'panel-right',
+    colorGrayMode: false,
+    colorWeakMode: false,
+    compact: false,
+    contentCompact: 'wide',
+    copyright: 'Copyright © 2024 Vben Admin PRO',
+    defaultAvatar:
+      'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.0/source/avatar-v1.webp',
+    dynamicTitle: true,
+    isMobile: false,
+    layout: 'side-nav',
+    locale: 'zh-CN',
+    name: 'Vben Admin Pro',
+    semiDarkMenu: true,
+    showPreference: true,
+    themeMode: 'dark',
+  },
+  breadcrumb: {
+    enable: true,
+    hideOnlyOne: false,
+    showHome: false,
+    showIcon: true,
+    styleType: 'normal',
+  },
+  footer: {
+    enable: true,
+    fixed: true,
+  },
+  header: {
+    enable: true,
+    hidden: false,
+    mode: 'fixed',
+  },
+  logo: {
+    enable: true,
+    source:
+      'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.0/source/logo-v1.webp',
+  },
+  navigation: {
+    accordion: true,
+    split: true,
+    styleType: 'rounded',
+  },
+
+  shortcutKeys: { enable: true },
+  sidebar: {
+    collapse: false,
+    collapseShowTitle: true,
+    enable: true,
+    expandOnHover: true,
+    extraCollapse: true,
+    hidden: false,
+    width: 240,
+  },
+
+  tabbar: {
+    enable: true,
+    keepAlive: true,
+    showIcon: true,
+  },
+
+  theme: {
+    colorPrimary: 'hsl(211 91% 39%)',
+  },
+
+  transition: {
+    enable: true,
+    name: 'fade-slide',
+    progress: true,
+  },
+};
+
+export { defaultPreferences };

+ 26 - 0
packages/@vben-core/forward/preferences/src/constants.ts

@@ -0,0 +1,26 @@
+import type { LocaleSupportType } from './types';
+
+interface Language {
+  key: LocaleSupportType;
+  text: string;
+}
+
+export const COLOR_PRIMARY_RESETS = [
+  'hsl(211 91% 39%)',
+  'hsl(212 100% 45%)',
+  'hsl(181 84% 32%)',
+  'hsl(230 99% 66%)',
+  'hsl(245 82% 67%)',
+  'hsl(340 100% 68%)',
+];
+
+export const SUPPORT_LANGUAGES: Language[] = [
+  {
+    key: 'zh-CN',
+    text: '简体中文',
+  },
+  {
+    key: 'en-US',
+    text: 'English',
+  },
+];

+ 32 - 0
packages/@vben-core/forward/preferences/src/index.ts

@@ -0,0 +1,32 @@
+import type { Flatten } from '@vben-core/typings';
+
+import { preferencesManager } from './preferences';
+
+import type { Preferences } from './types';
+
+// 偏好设置(带有层级关系)
+const preferences: Preferences = preferencesManager.getPreferences();
+
+// 扁平化后的偏好设置
+const flatPreferences: Flatten<Preferences> =
+  preferencesManager.getFlatPreferences();
+
+// 更新偏好设置
+const updatePreferences =
+  preferencesManager.updatePreferences.bind(preferencesManager);
+
+// 重置偏好设置
+const resetPreferences =
+  preferencesManager.resetPreferences.bind(preferencesManager);
+
+export {
+  flatPreferences,
+  preferences,
+  preferencesManager,
+  resetPreferences,
+  updatePreferences,
+};
+
+export * from './constants';
+export type * from './types';
+export * from './use-preferences';

+ 289 - 0
packages/@vben-core/forward/preferences/src/preferences.ts

@@ -0,0 +1,289 @@
+import type {
+  DeepPartial,
+  Flatten,
+  FlattenObjectKeys,
+} from '@vben-core/typings';
+
+import { StorageManager } from '@vben-core/cache';
+import { flattenObject, toNestedObject } from '@vben-core/helpers';
+import { convertToHslCssVar, merge } from '@vben-core/toolkit';
+
+import {
+  breakpointsTailwind,
+  useBreakpoints,
+  useCssVar,
+  useDebounceFn,
+} from '@vueuse/core';
+import { markRaw, reactive, watch } from 'vue';
+
+import { defaultPreferences } from './config';
+
+import type { Preferences } from './types';
+
+const STORAGE_KEY = 'preferences';
+
+interface initialOptions {
+  namespace: string;
+  overrides?: DeepPartial<Preferences>;
+}
+
+function isDarkTheme(theme: string) {
+  let dark = theme === 'dark';
+  if (theme === 'auto') {
+    dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+  }
+  return dark;
+}
+
+class PreferenceManager {
+  private cache: StorageManager<Preferences> | null = null;
+  private flattenedState: Flatten<Preferences>;
+  private initialPreferences: Preferences = defaultPreferences;
+  private isInitialized: boolean = false;
+  private savePreferences: (preference: Preferences) => void;
+  private state: Preferences = reactive<Preferences>({
+    ...this.loadPreferences(),
+  });
+  constructor() {
+    this.cache = new StorageManager();
+    this.flattenedState = reactive(flattenObject(this.state));
+
+    this.savePreferences = useDebounceFn(
+      (preference: Preferences) => this._savePreferences(preference),
+      100,
+    );
+  }
+
+  /**
+   * 保存偏好设置
+   * @param {Preferences} preference - 需要保存的偏好设置
+   */
+  private _savePreferences(preference: Preferences) {
+    this.cache?.setItem(STORAGE_KEY, preference);
+  }
+
+  /**
+   * 处理更新的键值
+   * 根据更新的键值执行相应的操作。
+   *
+   * @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
+   */
+  private handleUpdates(updates: DeepPartial<Preferences>) {
+    const themeUpdates = updates.theme || {};
+    const appUpdates = updates.app || {};
+
+    if (themeUpdates.colorPrimary) {
+      this.updateCssVar(this.state);
+    }
+
+    if (appUpdates.themeMode) {
+      this.updateTheme(this.state);
+    }
+
+    if (appUpdates.colorGrayMode || appUpdates.colorWeakMode) {
+      this.updateColorMode(this.state);
+    }
+  }
+
+  /**
+   * 加载偏好设置
+   * 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
+   * @returns {Preferences} 加载的偏好设置
+   */
+  private loadPreferences(): Preferences {
+    const savedPreferences = this.cache?.getItem(STORAGE_KEY);
+    return savedPreferences || { ...defaultPreferences };
+  }
+  /**
+   * 监听状态和系统偏好设置的变化。
+   */
+  private setupWatcher() {
+    if (this.isInitialized) {
+      return;
+    }
+
+    const debounceWaterState = useDebounceFn(() => {
+      const newFlattenedState = flattenObject(this.state);
+      for (const k in newFlattenedState) {
+        const key = k as FlattenObjectKeys<Preferences>;
+        this.flattenedState[key] = newFlattenedState[key];
+      }
+      this.savePreferences(this.state);
+    }, 16);
+
+    const debounceWaterFlattenedState = useDebounceFn(
+      (val: Flatten<Preferences>) => {
+        this.updateState(val);
+        this.savePreferences(this.state);
+      },
+      16,
+    );
+
+    // 监听 state 的变化
+    watch(this.state, debounceWaterState, { deep: true });
+
+    // 监听 flattenedState 的变化并触发 set 方法
+    watch(this.flattenedState, debounceWaterFlattenedState, { deep: true });
+
+    // 监听断点,判断是否移动端
+    const breakpoints = useBreakpoints(breakpointsTailwind);
+    const isMobile = breakpoints.smaller('md');
+    watch(
+      () => isMobile.value,
+      (val) => {
+        this.updatePreferences({
+          app: { isMobile: val },
+        });
+      },
+      { immediate: true },
+    );
+
+    // 监听系统主题偏好设置变化
+    window
+      .matchMedia('(prefers-color-scheme: dark)')
+      .addEventListener('change', ({ matches: isDark }) => {
+        this.updatePreferences({
+          app: { themeMode: isDark ? 'dark' : 'light' },
+        });
+        this.updateTheme(this.state);
+      });
+  }
+
+  /**
+   * 更新页面颜色模式(灰色、色弱)
+   * @param preference
+   */
+  private updateColorMode(preference: Preferences) {
+    if (preference.app) {
+      const { colorGrayMode, colorWeakMode } = preference.app;
+      const body = document.body;
+      const COLOR_WEAK = 'invert-mode';
+      const COLOR_GRAY = 'grayscale-mode';
+      colorWeakMode
+        ? body.classList.add(COLOR_WEAK)
+        : body.classList.remove(COLOR_WEAK);
+      colorGrayMode
+        ? body.classList.add(COLOR_GRAY)
+        : body.classList.remove(COLOR_GRAY);
+    }
+  }
+
+  /**
+   * 更新 CSS 变量
+   * @param  preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。
+   */
+  private updateCssVar(preference: Preferences) {
+    if (preference.theme) {
+      for (const [key, value] of Object.entries(preference.theme)) {
+        if (['colorPrimary'].includes(key)) {
+          const cssVarKey = key.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
+          const cssVarValue = useCssVar(`--${cssVarKey}`);
+          cssVarValue.value = convertToHslCssVar(value);
+        }
+      }
+    }
+  }
+
+  /**
+   *  更新状态
+   * 将新的扁平对象转换为嵌套对象,并与当前状态合并。
+   * @param {FlattenObject<Preferences>} newValue - 新的扁平对象
+   */
+  private updateState(newValue: Flatten<Preferences>) {
+    const nestObj = toNestedObject(newValue, 2);
+    Object.assign(this.state, merge(nestObj, this.state));
+  }
+
+  /**
+   * 更新主题
+   * @param preferences - 当前偏好设置对象,它的主题值将被用来设置文档的主题。
+   */
+  private updateTheme(preferences: Preferences) {
+    // 当修改到颜色变量时,更新 css 变量
+    const root = document.documentElement;
+    if (root) {
+      const themeMode = preferences?.app?.themeMode;
+      if (!themeMode) {
+        return;
+      }
+      const dark = isDarkTheme(themeMode);
+      root.classList.toggle('dark', dark);
+    }
+  }
+
+  public getFlatPreferences() {
+    return this.flattenedState;
+  }
+
+  public getInitialPreferences() {
+    return this.initialPreferences;
+  }
+
+  public getPreferences() {
+    return this.state;
+  }
+
+  /**
+   * 覆盖偏好设置
+   * @param overrides - 要覆盖的偏好设置
+   * @param 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({}, this.loadPreferences(), overrides);
+
+    // 更新偏好设置
+    this.updatePreferences(mergedPreference);
+
+    this.setupWatcher();
+    // 标记为已初始化
+    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);
+    // 从存储中移除偏好设置项
+    this.cache?.removeItem(STORAGE_KEY);
+  }
+
+  /**
+   * 更新偏好设置
+   * @param updates - 要更新的偏好设置
+   */
+  public updatePreferences(updates: DeepPartial<Preferences>) {
+    const mergedState = merge(updates, markRaw(this.state));
+
+    Object.assign(this.state, mergedState);
+    Object.assign(this.flattenedState, flattenObject(this.state));
+
+    // 根据更新的键值执行相应的操作
+    this.handleUpdates(updates);
+    this.savePreferences(this.state);
+  }
+}
+
+const preferencesManager = new PreferenceManager();
+export { isDarkTheme, preferencesManager };

+ 189 - 0
packages/@vben-core/forward/preferences/src/types.ts

@@ -0,0 +1,189 @@
+import type {
+  ContentCompactType,
+  LayoutHeaderModeType,
+  LayoutType,
+  LocaleSupportType,
+  ThemeModeType,
+} from '@vben-core/typings';
+
+type BreadcrumbStyleType = 'background' | 'normal';
+
+type NavigationStyleType = 'plain' | 'rounded';
+
+type PageTransitionType = 'fade-slide';
+
+type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
+
+interface AppPreferences {
+  /** 登录注册页面布局 */
+  authPageLayout: AuthPageLayoutType;
+  /** 是否开启灰色模式 */
+  colorGrayMode: boolean;
+  /** 是否开启色弱模式 */
+  colorWeakMode: boolean;
+  /** 是否开启紧凑模式 */
+  compact: boolean;
+  /** 是否开启内容紧凑模式 */
+  contentCompact: ContentCompactType;
+  /** 页脚Copyright */
+  copyright: string;
+  // /** 应用默认头像 */
+  defaultAvatar: string;
+  // /** 开启动态标题 */
+  dynamicTitle: boolean;
+  /** 是否移动端 */
+  isMobile: boolean;
+  /** 布局方式 */
+  layout: LayoutType;
+  /** 支持的语言 */
+  locale: LocaleSupportType;
+  /** 应用名 */
+  name: string;
+  /** 是否开启半深色菜单(只在theme='light'时生效) */
+  semiDarkMenu: boolean;
+  /** 是否显示偏好设置 */
+  showPreference: boolean;
+  /** 当前主题 */
+  themeMode: ThemeModeType;
+}
+
+interface BreadcrumbPreferences {
+  /** 面包屑是否启用 */
+  enable: boolean;
+  /** 面包屑是否只有一个时隐藏 */
+  hideOnlyOne: boolean;
+  /** 面包屑首页图标是否可见 */
+  showHome: boolean;
+  /** 面包屑图标是否可见 */
+  showIcon: boolean;
+  /** 面包屑风格 */
+  styleType: BreadcrumbStyleType;
+}
+
+interface FooterPreferences {
+  /** 底栏是否可见 */
+  enable: boolean;
+  /** 底栏是否固定 */
+  fixed: boolean;
+}
+
+interface HeaderPreferences {
+  /** 顶栏是否启用 */
+  enable: boolean;
+  /** 顶栏是否隐藏,css-隐藏 */
+  hidden: boolean;
+  /** header显示模式 */
+  mode: LayoutHeaderModeType;
+}
+
+interface LogoPreferences {
+  /** logo是否可见 */
+  enable: boolean;
+  /** logo地址 */
+  source: string;
+}
+
+interface NavigationPreferences {
+  /** 导航菜单手风琴模式 */
+  accordion: boolean;
+  /** 导航菜单是否切割,只在 layout=mixed-nav 生效 */
+  split: boolean;
+  /** 导航菜单风格 */
+  styleType: NavigationStyleType;
+}
+
+interface SidebarPreferences {
+  /** 侧边栏是否折叠 */
+  collapse: boolean;
+  /** 侧边栏折叠时,是否显示title */
+  collapseShowTitle: boolean;
+  /** 侧边栏是否可见 */
+  enable: boolean;
+  /** 菜单自动展开状态 */
+  expandOnHover: boolean;
+  /** 侧边栏扩展区域是否折叠 */
+  extraCollapse: boolean;
+  /** 侧边栏是否隐藏 - css */
+  hidden: boolean;
+  /** 侧边栏宽度 */
+  width: number;
+}
+
+interface ShortcutKeyPreferences {
+  /** 是否启用快捷键-全局 */
+  enable: boolean;
+}
+
+interface TabbarPreferences {
+  /** 是否开启多标签页 */
+  enable: boolean;
+  /** 开启标签页缓存功能 */
+  keepAlive: boolean;
+  /** 是否开启多标签页图标 */
+  showIcon: boolean;
+}
+
+interface ThemePreferences {
+  /** 主题色 */
+  colorPrimary: string;
+}
+
+interface TransitionPreferences {
+  /** 页面切换动画是否启用 */
+  enable: boolean;
+  /** 页面切换动画 */
+  name: PageTransitionType;
+  /** 是否开启页面加载进度动画 */
+  progress: boolean;
+}
+
+interface Preferences {
+  /** 全局配置 */
+  app: AppPreferences;
+  /** 顶栏配置 */
+  breadcrumb: BreadcrumbPreferences;
+  /** 底栏配置 */
+  footer: FooterPreferences;
+  /** 面包屑配置 */
+  header: HeaderPreferences;
+  /** logo配置 */
+  logo: LogoPreferences;
+  /** 导航配置 */
+  navigation: NavigationPreferences;
+  /** 快捷键配置 */
+  shortcutKeys: ShortcutKeyPreferences;
+  /** 侧边栏配置 */
+  sidebar: SidebarPreferences;
+  /** 标签页配置 */
+  tabbar: TabbarPreferences;
+  /** 主题配置 */
+  theme: ThemePreferences;
+  /** 动画配置 */
+  transition: TransitionPreferences;
+}
+
+type PreferencesKeys = keyof Preferences;
+
+export type {
+  AppPreferences,
+  AuthPageLayoutType,
+  BreadcrumbPreferences,
+  BreadcrumbStyleType,
+  ContentCompactType,
+  FooterPreferences,
+  HeaderPreferences,
+  LayoutHeaderModeType,
+  LayoutType,
+  LocaleSupportType,
+  LogoPreferences,
+  NavigationPreferences,
+  PageTransitionType,
+  Preferences,
+  PreferencesKeys,
+  ShortcutKeyPreferences,
+  SidebarPreferences,
+  TabbarPreferences,
+  ThemeModeType,
+  ThemePreferences,
+  TransitionPreferences,
+};

+ 25 - 21
packages/preference/src/use-preference.ts → packages/@vben-core/forward/preferences/src/use-preferences.ts

@@ -2,28 +2,26 @@ import { diff } from '@vben-core/toolkit';
 
 import { computed } from 'vue';
 
-import {
-  initialPreference,
-  isDarkTheme,
-  currentPreference as preference,
-} from './preference';
+import { isDarkTheme, preferencesManager } from './preferences';
 
-function usePreference() {
+function usePreferences() {
+  const preferences = preferencesManager.getPreferences();
+  const flatPreferences = preferencesManager.getFlatPreferences();
+  const initialPreferences = preferencesManager.getInitialPreferences();
   /**
    * @zh_CN 计算偏好设置的变化
    */
   const diffPreference = computed(() => {
-    return diff(initialPreference.value, preference);
+    return diff(initialPreferences, preferences);
   });
 
   /**
    * @zh_CN 判断是否为暗黑模式
-   * @param  preference - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。
+   * @param  preferences - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。
    * @returns 如果主题为暗黑模式,返回 true,否则返回 false。
    */
   const isDark = computed(() => {
-    const theme = preference.theme;
-    return isDarkTheme(theme);
+    return isDarkTheme(flatPreferences.appThemeMode);
   });
 
   const theme = computed(() => {
@@ -34,33 +32,39 @@ function usePreference() {
    * @zh_CN 布局方式
    */
   const layout = computed(() =>
-    preference.isMobile ? 'side-nav' : preference.layout,
+    flatPreferences.appIsMobile ? 'side-nav' : flatPreferences.appLayout,
   );
 
   /**
    * @zh_CN 是否全屏显示content,不需要侧边、底部、顶部、tab区域
    */
-  const isFullContent = computed(() => preference.layout === 'full-content');
+  const isFullContent = computed(
+    () => flatPreferences.appLayout === 'full-content',
+  );
 
   /**
    * @zh_CN 是否侧边导航模式
    */
-  const isSideNav = computed(() => preference.layout === 'side-nav');
+  const isSideNav = computed(() => flatPreferences.appLayout === 'side-nav');
 
   /**
    * @zh_CN 是否侧边混合模式
    */
-  const isSideMixedNav = computed(() => preference.layout === 'side-mixed-nav');
+  const isSideMixedNav = computed(
+    () => flatPreferences.appLayout === 'side-mixed-nav',
+  );
 
   /**
    * @zh_CN 是否为头部导航模式
    */
-  const isHeaderNav = computed(() => preference.layout === 'header-nav');
+  const isHeaderNav = computed(
+    () => flatPreferences.appLayout === 'header-nav',
+  );
 
   /**
    * @zh_CN 是否为混合导航模式
    */
-  const isMixedNav = computed(() => preference.layout === 'mixed-nav');
+  const isMixedNav = computed(() => flatPreferences.appLayout === 'mixed-nav');
 
   /**
    * @zh_CN 是否包含侧边导航模式
@@ -74,28 +78,28 @@ function usePreference() {
    * 在tabs可见以及开启keep-alive的情况下才开启
    */
   const keepAlive = computed(
-    () => preference.keepAlive && preference.tabsVisible,
+    () => flatPreferences.tabbarKeepAlive && flatPreferences.tabbarEnable,
   );
 
   /**
    * @zh_CN 登录注册页面布局是否为左侧
    */
   const authPanelLeft = computed(() => {
-    return preference.authPageLayout === 'panel-left';
+    return flatPreferences.appAuthPageLayout === 'panel-left';
   });
 
   /**
    * @zh_CN 登录注册页面布局是否为左侧
    */
   const authPanelRight = computed(() => {
-    return preference.authPageLayout === 'panel-right';
+    return flatPreferences.appAuthPageLayout === 'panel-right';
   });
 
   /**
    * @zh_CN 登录注册页面布局是否为中间
    */
   const authPanelCenter = computed(() => {
-    return preference.authPageLayout === 'panel-center';
+    return flatPreferences.appAuthPageLayout === 'panel-center';
   });
 
   return {
@@ -116,4 +120,4 @@ function usePreference() {
   };
 }
 
-export { usePreference };
+export { usePreferences };

+ 0 - 0
packages/preference/tsconfig.json → packages/@vben-core/forward/preferences/tsconfig.json


+ 0 - 0
packages/@vben-core/forward/request/.gitkeep


+ 0 - 0
packages/stores/build.config.ts → packages/@vben-core/forward/stores/build.config.ts


+ 2 - 2
packages/stores/package.json → packages/@vben-core/forward/stores/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "@vben/stores",
+  "name": "@vben-core/stores",
   "version": "1.0.0",
   "type": "module",
   "license": "MIT",
@@ -7,7 +7,7 @@
   "repository": {
     "type": "git",
     "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
-    "directory": "packages/stores"
+    "directory": "packages/@vben-core/stores"
   },
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "scripts": {

+ 0 - 0
packages/stores/shim-pinia.d.ts → packages/@vben-core/forward/stores/shim-pinia.d.ts


+ 0 - 0
packages/stores/src/index.ts → packages/@vben-core/forward/stores/src/index.ts


+ 2 - 2
packages/stores/src/modules/access.test.ts → packages/@vben-core/forward/stores/src/modules/access.test.ts

@@ -11,10 +11,10 @@ import {
 describe('useAccessStore', () => {
   it('app Name with test', () => {
     setActivePinia(createPinia());
-    // let referenceStore = usePreferenceStore();
+    // let referenceStore = usePreferencesStore();
 
     // beforeEach(() => {
-    //   referenceStore = usePreferenceStore();
+    //   referenceStore = usePreferencesStore();
     // });
 
     // expect(referenceStore.appName).toBe('vben-admin');

+ 0 - 0
packages/stores/src/modules/access.ts → packages/@vben-core/forward/stores/src/modules/access.ts


+ 0 - 0
packages/stores/src/modules/index.ts → packages/@vben-core/forward/stores/src/modules/index.ts


+ 0 - 0
packages/stores/src/modules/tabs.ts → packages/@vben-core/forward/stores/src/modules/tabs.ts


+ 8 - 9
packages/stores/src/setup.ts → packages/@vben-core/forward/stores/src/setup.ts

@@ -1,10 +1,8 @@
-import type { App } from 'vue';
-
 import { createPinia } from 'pinia';
 
-interface SetupStoreOptions {
+interface InitStoreOptions {
   /**
-   * @zh_CN 应用名,由于 @vben/stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名
+   * @zh_CN 应用名,由于 @vben-core/stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名
    * 应用名将被用于持久化的前缀
    */
   namespace: string;
@@ -12,20 +10,21 @@ interface SetupStoreOptions {
 
 /**
  * @zh_CN 初始化pinia
- * @param app vue app 实例
  */
-async function setupStore(app: App, options: SetupStoreOptions) {
+async function initStore(options: InitStoreOptions) {
   const { createPersistedState } = await import('pinia-plugin-persistedstate');
   const pinia = createPinia();
   const { namespace } = options;
   pinia.use(
     createPersistedState({
       // key $appName-$store.id
-      key: (storeKey) => `__${namespace}-${storeKey}__`,
+      key: (storeKey) => `${namespace}-${storeKey}`,
       storage: localStorage,
     }),
   );
-  app.use(pinia);
+  return pinia;
 }
 
-export { setupStore };
+export { initStore };
+
+export type { InitStoreOptions };

+ 0 - 0
packages/stores/tsconfig.json → packages/@vben-core/forward/stores/tsconfig.json


+ 7 - 0
packages/@vben-core/helpers/build.config.ts

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

+ 45 - 0
packages/@vben-core/helpers/package.json

@@ -0,0 +1,45 @@
+{
+  "name": "@vben-core/helpers",
+  "version": "1.0.0",
+  "type": "module",
+  "license": "MIT",
+  "homepage": "https://github.com/vbenjs/vue-vben-admin",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
+    "directory": "packages/@vben-core/helpers"
+  },
+  "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
+  "scripts": {
+    "build": "pnpm unbuild",
+    "stub": "pnpm unbuild --stub"
+  },
+  "files": [
+    "dist"
+  ],
+  "sideEffects": false,
+  "main": "./dist/index.mjs",
+  "module": "./dist/index.mjs",
+  "imports": {
+    "#*": "./src/*"
+  },
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "development": "./src/index.ts",
+      "default": "./dist/index.mjs"
+    }
+  },
+  "publishConfig": {
+    "exports": {
+      ".": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/index.mjs"
+      }
+    }
+  },
+  "dependencies": {
+    "@vben-core/toolkit": "workspace:*",
+    "@vben-core/typings": "workspace:*"
+  }
+}

+ 1 - 0
packages/@vben-core/helpers/src/index.ts

@@ -0,0 +1 @@
+export * from './object';

+ 245 - 0
packages/@vben-core/helpers/src/object.test.ts

@@ -0,0 +1,245 @@
+import { describe, expect, it } from 'vitest';
+
+import { flattenObject, toCamelCase, toNestedObject } from './object';
+
+describe('toCamelCase', () => {
+  it('should return the key if parentKey is empty', () => {
+    expect(toCamelCase('child', '')).toBe('child');
+  });
+
+  it('should combine parentKey and key in camel case', () => {
+    expect(toCamelCase('child', 'parent')).toBe('parentChild');
+  });
+
+  it('should handle empty key and parentKey', () => {
+    expect(toCamelCase('', '')).toBe('');
+  });
+
+  it('should handle key with capital letters', () => {
+    expect(toCamelCase('Child', 'parent')).toBe('parentChild');
+    expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');
+  });
+});
+
+describe('flattenObject', () => {
+  it('should flatten a nested object correctly', () => {
+    const nestedObject = {
+      language: 'en',
+      notifications: {
+        email: true,
+        push: {
+          sound: true,
+          vibration: false,
+        },
+      },
+      theme: 'light',
+    };
+
+    const expected = {
+      language: 'en',
+      notificationsEmail: true,
+      notificationsPushSound: true,
+      notificationsPushVibration: false,
+      theme: 'light',
+    };
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+
+  it('should handle empty objects', () => {
+    const nestedObject = {};
+    const expected = {};
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+
+  it('should handle objects with primitive values', () => {
+    const nestedObject = {
+      active: true,
+      age: 30,
+      name: 'Alice',
+    };
+
+    const expected = {
+      active: true,
+      age: 30,
+      name: 'Alice',
+    };
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+
+  it('should handle objects with null values', () => {
+    const nestedObject = {
+      user: {
+        age: null,
+        name: null,
+      },
+    };
+
+    const expected = {
+      userAge: null,
+      userName: null,
+    };
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+
+  it('should handle nested empty objects', () => {
+    const nestedObject = {
+      a: {},
+      b: { c: {} },
+    };
+
+    const expected = {};
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+
+  it('should handle arrays within objects', () => {
+    const nestedObject = {
+      hobbies: ['reading', 'gaming'],
+      name: 'Alice',
+    };
+
+    const expected = {
+      hobbies: ['reading', 'gaming'],
+      name: 'Alice',
+    };
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+  it('should flatten objects with nested arrays correctly', () => {
+    const nestedObject = {
+      person: {
+        hobbies: ['reading', 'gaming'],
+        name: 'Alice',
+      },
+    };
+
+    const expected = {
+      personHobbies: ['reading', 'gaming'],
+      personName: 'Alice',
+    };
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+
+  it('should handle objects with undefined values', () => {
+    const nestedObject = {
+      user: {
+        age: undefined,
+        name: 'Alice',
+      },
+    };
+
+    const expected = {
+      userAge: undefined,
+      userName: 'Alice',
+    };
+
+    const result = flattenObject(nestedObject);
+    expect(result).toEqual(expected);
+  });
+});
+
+describe('toNestedObject', () => {
+  it('should convert flat object to nested object with level 1', () => {
+    const flatObject = {
+      anotherKeyExample: 2,
+      commonAppName: 1,
+      someOtherKey: 3,
+    };
+
+    const expectedNestedObject = {
+      anotherKeyExample: 2,
+      commonAppName: 1,
+      someOtherKey: 3,
+    };
+
+    expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
+  });
+
+  it('should convert flat object to nested object with level 2', () => {
+    const flatObject = {
+      appAnotherKeyExample: 2,
+      appCommonName: 1,
+      appSomeOtherKey: 3,
+    };
+
+    const expectedNestedObject = {
+      app: {
+        anotherKeyExample: 2,
+        commonName: 1,
+        someOtherKey: 3,
+      },
+    };
+
+    expect(toNestedObject(flatObject, 2)).toEqual(expectedNestedObject);
+  });
+
+  it('should convert flat object to nested object with level 3', () => {
+    const flatObject = {
+      appAnotherKeyExampleValue: 2,
+      appCommonNameKey: 1,
+      appSomeOtherKeyItem: 3,
+    };
+
+    const expectedNestedObject = {
+      app: {
+        another: {
+          keyExampleValue: 2,
+        },
+        common: {
+          nameKey: 1,
+        },
+        some: {
+          otherKeyItem: 3,
+        },
+      },
+    };
+
+    expect(toNestedObject(flatObject, 3)).toEqual(expectedNestedObject);
+  });
+
+  it('should handle empty object', () => {
+    const flatObject = {};
+
+    const expectedNestedObject = {};
+
+    expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
+  });
+
+  it('should handle single key object', () => {
+    const flatObject = {
+      singleKey: 1,
+    };
+
+    const expectedNestedObject = {
+      singleKey: 1,
+    };
+
+    expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
+  });
+
+  it('should handle complex keys', () => {
+    const flatObject = {
+      anotherComplexKeyWithParts: 2,
+      complexKeyWithMultipleParts: 1,
+    };
+
+    const expectedNestedObject = {
+      anotherComplexKeyWithParts: 2,
+      complexKeyWithMultipleParts: 1,
+    };
+
+    expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
+  });
+});

+ 164 - 0
packages/@vben-core/helpers/src/object.ts

@@ -0,0 +1,164 @@
+import type { Flatten } from '@vben-core/typings';
+
+import {
+  capitalizeFirstLetter,
+  toLowerCaseFirstLetter,
+} from '@vben-core/toolkit';
+
+/**
+ *  生成驼峰命名法的键名
+ * @param key
+ * @param parentKey
+ */
+function toCamelCase(key: string, parentKey: string): string {
+  if (!parentKey) {
+    return key;
+  }
+  return parentKey + key.charAt(0).toUpperCase() + key.slice(1);
+}
+
+/**
+ * 将嵌套对象扁平化
+ * @param obj - 需要扁平化的对象
+ * @param parentKey - 父键名,用于递归时拼接键名
+ * @param result - 存储结果的对象
+ * @returns 扁平化后的对象
+ *
+ * 示例:
+ * const nestedObj = {
+ *   user: {
+ *     name: 'Alice',
+ *     address: {
+ *       city: 'Wonderland',
+ *       zip: '12345'
+ *     }
+ *   },
+ *   items: [
+ *     { id: 1, name: 'Item 1' },
+ *     { id: 2, name: 'Item 2' }
+ *   ],
+ *   active: true
+ * };
+ * const flatObj = flattenObject(nestedObj);
+ * console.log(flatObj);
+ * 输出:
+ * {
+ *   userName: 'Alice',
+ *   userAddressCity: 'Wonderland',
+ *   userAddressZip: '12345',
+ *   items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ],
+ *   active: true
+ * }
+ */
+function flattenObject<T extends Record<string, any>>(
+  obj: T,
+  parentKey: string = '',
+  result: Record<string, any> = {},
+): Flatten<T> {
+  Object.keys(obj).forEach((key) => {
+    const newKey = parentKey
+      ? `${parentKey}${capitalizeFirstLetter(key)}`
+      : key;
+    const value = obj[key];
+
+    if (value && typeof value === 'object' && !Array.isArray(value)) {
+      flattenObject(value, newKey, result);
+    } else {
+      result[newKey] = value;
+    }
+  });
+  return result as Flatten<T>;
+}
+
+/**
+ * 将扁平对象转换为嵌套对象。
+ *
+ * @template T - 输入对象值的类型
+ * @param {Record<string, T>} obj - 要转换的扁平对象
+ * @param {number} level - 嵌套的层级
+ * @returns {T} 嵌套对象
+ *
+ * @example
+ * 将扁平对象转换为嵌套对象,嵌套层级为 1
+ * const flatObject = {
+ *   'commonAppName': 1,
+ *   'anotherKeyExample': 2,
+ *   'someOtherKey': 3
+ * };
+ * const nestedObject = toNestedObject(flatObject, 1);
+ * console.log(nestedObject);
+ * 输出:
+ * {
+ *   commonAppName: 1,
+ *   anotherKeyExample: 2,
+ *   someOtherKey: 3
+ * }
+ *
+ * @example
+ * 将扁平对象转换为嵌套对象,嵌套层级为 2
+ * const flatObject = {
+ *   'appCommonName': 1,
+ *   'appAnotherKeyExample': 2,
+ *   'appSomeOtherKey': 3
+ * };
+ * const nestedObject = toNestedObject(flatObject, 2);
+ * console.log(nestedObject);
+ * 输出:
+ * {
+ *   app: {
+ *     commonName: 1,
+ *     anotherKeyExample: 2,
+ *     someOtherKey: 3
+ *   }
+ * }
+ */
+
+function toNestedObject<T>(obj: Record<string, T>, level: number): T {
+  const result: any = {};
+
+  for (const key in obj) {
+    const keys = key.split(/(?=[A-Z])/);
+    // 将驼峰式分割为数组;
+    let current = result;
+
+    for (let i = 0; i < keys.length; i++) {
+      const lowerKey = keys[i].toLowerCase();
+      if (i === level - 1) {
+        const remainingKeys = keys.slice(i).join(''); // 保留后续部分作为键的一部分
+        current[toLowerCaseFirstLetter(remainingKeys)] = obj[key];
+        break;
+      } else {
+        current[lowerKey] = current[lowerKey] || {};
+        current = current[lowerKey];
+      }
+    }
+  }
+
+  return result as T;
+}
+
+export { flattenObject, toCamelCase, toNestedObject };
+
+// 定义递归类型,用于推断扁平化后的对象类型
+// 限制递归深度的辅助类型
+// type FlattenDepth<
+//   T,
+//   Depth extends number,
+//   CurrentDepth extends number[] = [],
+// > = {
+//   [K in keyof T as CurrentDepth['length'] extends Depth
+//     ? K
+//     : T[K] extends object
+//       ? `${CurrentDepth['length'] extends 0 ? Uncapitalize<K & string> : Capitalize<K & string>}${keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]> extends string ? Capitalize<keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>> : ''}`
+//       : `${CurrentDepth['length'] extends 0 ? Uncapitalize<K & string> : Capitalize<K & string>}`]: CurrentDepth['length'] extends Depth
+//     ? T[K]
+//     : T[K] extends object
+//       ? FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>[keyof FlattenDepth<
+//           T[K],
+//           Depth,
+//           [...CurrentDepth, 1]
+//         >]
+//       : T[K];
+// };
+
+// type Flatten<T, Depth extends number = 4> = FlattenDepth<T, Depth>;

+ 5 - 0
packages/@vben-core/helpers/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/tsconfig/library.json",
+  "include": ["src"]
+}

+ 1 - 1
packages/@vben-core/shared/chche/src/index.ts

@@ -1 +1 @@
-export * from './storage-cache';
+export * from './storage-manager';

+ 0 - 104
packages/@vben-core/shared/chche/src/storage-cache.test.ts

@@ -1,104 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-
-import { StorageCache } from './storage-cache';
-
-describe('storageCache', () => {
-  let localStorageCache: StorageCache;
-  let sessionStorageCache: StorageCache;
-
-  beforeEach(() => {
-    localStorageCache = new StorageCache('prefix_', 'localStorage');
-    sessionStorageCache = new StorageCache('prefix_', 'sessionStorage');
-    localStorage.clear();
-    sessionStorage.clear();
-    vi.useFakeTimers();
-  });
-
-  afterEach(() => {
-    vi.useRealTimers();
-  });
-
-  it('should set and get an item with prefix in localStorage', () => {
-    localStorageCache.setItem('testKey', 'testValue');
-    const value = localStorageCache.getItem<string>('testKey');
-    expect(value).toBe('testValue');
-    expect(localStorage.getItem('prefix_testKey')).not.toBeNull();
-  });
-
-  it('should set and get an item with prefix in sessionStorage', () => {
-    sessionStorageCache.setItem('testKey', 'testValue');
-    const value = sessionStorageCache.getItem<string>('testKey');
-    expect(value).toBe('testValue');
-    expect(sessionStorage.getItem('prefix_testKey')).not.toBeNull();
-  });
-
-  it('should return null for expired item in localStorage', () => {
-    localStorageCache.setItem('testKey', 'testValue', 1 / 60); // 1 second expiry
-    vi.advanceTimersByTime(2000); // Fast-forward 2 seconds
-    const value = localStorageCache.getItem<string>('testKey');
-    expect(value).toBeNull();
-  });
-
-  it('should return null for expired item in sessionStorage', () => {
-    sessionStorageCache.setItem('testKey', 'testValue', 1 / 60); // 1 second expiry
-    vi.advanceTimersByTime(2000); // Fast-forward 2 seconds
-    const value = sessionStorageCache.getItem<string>('testKey');
-    expect(value).toBeNull();
-  });
-
-  it('should remove an item with prefix in localStorage', () => {
-    localStorageCache.setItem('testKey', 'testValue');
-    localStorageCache.removeItem('testKey');
-    const value = localStorageCache.getItem<string>('testKey');
-    expect(value).toBeNull();
-    expect(localStorage.getItem('prefix_testKey')).toBeNull();
-  });
-
-  it('should remove an item with prefix in sessionStorage', () => {
-    sessionStorageCache.setItem('testKey', 'testValue');
-    sessionStorageCache.removeItem('testKey');
-    const value = sessionStorageCache.getItem<string>('testKey');
-    expect(value).toBeNull();
-    expect(sessionStorage.getItem('prefix_testKey')).toBeNull();
-  });
-
-  it('should clear all items in localStorage', () => {
-    localStorageCache.setItem('testKey1', 'testValue1');
-    localStorageCache.setItem('testKey2', 'testValue2');
-    localStorageCache.clear();
-    expect(localStorageCache.length()).toBe(0);
-  });
-
-  it('should clear all items in sessionStorage', () => {
-    sessionStorageCache.setItem('testKey1', 'testValue1');
-    sessionStorageCache.setItem('testKey2', 'testValue2');
-    sessionStorageCache.clear();
-    expect(sessionStorageCache.length()).toBe(0);
-  });
-
-  it('should return correct length in localStorage', () => {
-    localStorageCache.setItem('testKey1', 'testValue1');
-    localStorageCache.setItem('testKey2', 'testValue2');
-    expect(localStorageCache.length()).toBe(2);
-  });
-
-  it('should return correct length in sessionStorage', () => {
-    sessionStorageCache.setItem('testKey1', 'testValue1');
-    sessionStorageCache.setItem('testKey2', 'testValue2');
-    expect(sessionStorageCache.length()).toBe(2);
-  });
-
-  it('should return correct key by index in localStorage', () => {
-    localStorageCache.setItem('testKey1', 'testValue1');
-    localStorageCache.setItem('testKey2', 'testValue2');
-    expect(localStorageCache.key(0)).toBe('prefix_testKey1');
-    expect(localStorageCache.key(1)).toBe('prefix_testKey2');
-  });
-
-  it('should return correct key by index in sessionStorage', () => {
-    sessionStorageCache.setItem('testKey1', 'testValue1');
-    sessionStorageCache.setItem('testKey2', 'testValue2');
-    expect(sessionStorageCache.key(0)).toBe('prefix_testKey1');
-    expect(sessionStorageCache.key(1)).toBe('prefix_testKey2');
-  });
-});

+ 0 - 145
packages/@vben-core/shared/chche/src/storage-cache.ts

@@ -1,145 +0,0 @@
-import type { IStorageCache, StorageType, StorageValue } from './types';
-
-class StorageCache implements IStorageCache {
-  protected prefix: string;
-  protected storage: Storage;
-
-  constructor(prefix: string = '', storageType: StorageType = 'localStorage') {
-    this.prefix = prefix;
-    this.storage =
-      storageType === 'localStorage' ? localStorage : sessionStorage;
-  }
-
-  // 获取带前缀的键名
-  private getFullKey(key: string): string {
-    return this.prefix + key;
-  }
-
-  // 获取项之后的钩子方法
-  protected afterGetItem<T>(_key: string, _value: T | null): void {}
-
-  // 设置项之后的钩子方法
-  protected afterSetItem<T>(
-    _key: string,
-    _value: T,
-    _expiryInMinutes?: number,
-  ): void {}
-
-  // 获取项之前的钩子方法
-  protected beforeGetItem(_key: string): void {}
-
-  // 设置项之前的钩子方法
-  protected beforeSetItem<T>(
-    _key: string,
-    _value: T,
-    _expiryInMinutes?: number,
-  ): void {}
-
-  /**
-   * 清空存储
-   */
-  clear(): void {
-    try {
-      this.storage.clear();
-    } catch (error) {
-      console.error('Error clearing storage', error);
-    }
-  }
-
-  /**
-   * 获取存储项
-   * @param key 存储键
-   * @returns 存储值或 null
-   */
-  getItem<T>(key: string): T | null {
-    const fullKey = this.getFullKey(key);
-    this.beforeGetItem(fullKey);
-
-    let value: T | null = null;
-    try {
-      const item = this.storage.getItem(fullKey);
-      if (item) {
-        const storageValue: StorageValue<T> = JSON.parse(item);
-        if (storageValue.expiry && storageValue.expiry < Date.now()) {
-          this.storage.removeItem(fullKey);
-        } else {
-          value = storageValue.data;
-        }
-      }
-    } catch (error) {
-      console.error('Error getting item from storage', error);
-    }
-
-    this.afterGetItem(fullKey, value);
-    return value;
-  }
-
-  /**
-   * 获取存储中的键
-   * @param index 键的索引
-   * @returns 存储键或 null
-   */
-  key(index: number): null | string {
-    try {
-      return this.storage.key(index);
-    } catch (error) {
-      console.error('Error getting key from storage', error);
-      return null;
-    }
-  }
-
-  /**
-   * 获取存储项的数量
-   * @returns 存储项的数量
-   */
-  length(): number {
-    try {
-      return this.storage.length;
-    } catch (error) {
-      console.error('Error getting storage length', error);
-      return 0;
-    }
-  }
-
-  /**
-   * 删除存储项
-   * @param key 存储键
-   */
-  removeItem(key: string): void {
-    const fullKey = this.getFullKey(key);
-    try {
-      this.storage.removeItem(fullKey);
-    } catch (error) {
-      console.error('Error removing item from storage', error);
-    }
-  }
-
-  /**
-   * 设置存储项
-   * @param key 存储键
-   * @param value 存储值
-   * @param expiryInMinutes 过期时间(分钟)
-   */
-  setItem<T>(key: string, value: T, expiryInMinutes?: number): void {
-    const fullKey = this.getFullKey(key);
-    this.beforeSetItem(fullKey, value, expiryInMinutes);
-
-    const now = Date.now();
-    const expiry = expiryInMinutes ? now + expiryInMinutes * 60_000 : null;
-
-    const storageValue: StorageValue<T> = {
-      data: value,
-      expiry,
-    };
-
-    try {
-      this.storage.setItem(fullKey, JSON.stringify(storageValue));
-    } catch (error) {
-      console.error('Error setting item in storage', error);
-    }
-
-    this.afterSetItem(fullKey, value, expiryInMinutes);
-  }
-}
-
-export { StorageCache };

+ 130 - 0
packages/@vben-core/shared/chche/src/storage-manager.test.ts

@@ -0,0 +1,130 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { StorageManager } from './storage-manager';
+
+describe('storageManager', () => {
+  let storageManager: StorageManager<{ age: number; name: string }>;
+
+  beforeEach(() => {
+    vi.useFakeTimers();
+    localStorage.clear();
+    storageManager = new StorageManager<{ age: number; name: string }>({
+      prefix: 'test_',
+    });
+  });
+
+  it('should set and get an item', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' });
+    const user = storageManager.getItem('user');
+    expect(user).toEqual({ age: 30, name: 'John Doe' });
+  });
+
+  it('should return default value if item does not exist', () => {
+    const user = storageManager.getItem('nonexistent', {
+      age: 0,
+      name: 'Default User',
+    });
+    expect(user).toEqual({ age: 0, name: 'Default User' });
+  });
+
+  it('should remove an item', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' });
+    storageManager.removeItem('user');
+    const user = storageManager.getItem('user');
+    expect(user).toBeNull();
+  });
+
+  it('should clear all items with the prefix', () => {
+    storageManager.setItem('user1', { age: 30, name: 'John Doe' });
+    storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
+    storageManager.clear();
+    expect(storageManager.getItem('user1')).toBeNull();
+    expect(storageManager.getItem('user2')).toBeNull();
+  });
+
+  it('should clear expired items', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
+    vi.advanceTimersByTime(1001); // 快进时间
+    storageManager.clearExpiredItems();
+    const user = storageManager.getItem('user');
+    expect(user).toBeNull();
+  });
+
+  it('should not clear non-expired items', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
+    vi.advanceTimersByTime(5000); // 快进时间
+    storageManager.clearExpiredItems();
+    const user = storageManager.getItem('user');
+    expect(user).toEqual({ age: 30, name: 'John Doe' });
+  });
+
+  it('should handle JSON parse errors gracefully', () => {
+    localStorage.setItem('test_user', '{ invalid JSON }');
+    const user = storageManager.getItem('user', {
+      age: 0,
+      name: 'Default User',
+    });
+    expect(user).toEqual({ age: 0, name: 'Default User' });
+  });
+  it('should return null for non-existent items without default value', () => {
+    const user = storageManager.getItem('nonexistent');
+    expect(user).toBeNull();
+  });
+
+  it('should overwrite existing items', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' });
+    storageManager.setItem('user', { age: 25, name: 'Jane Doe' });
+    const user = storageManager.getItem('user');
+    expect(user).toEqual({ age: 25, name: 'Jane Doe' });
+  });
+
+  it('should handle items without expiry correctly', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' });
+    vi.advanceTimersByTime(5000);
+    const user = storageManager.getItem('user');
+    expect(user).toEqual({ age: 30, name: 'John Doe' });
+  });
+
+  it('should remove expired items when accessed', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
+    vi.advanceTimersByTime(1001); // 快进时间
+    const user = storageManager.getItem('user');
+    expect(user).toBeNull();
+  });
+
+  it('should not remove non-expired items when accessed', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
+    vi.advanceTimersByTime(5000); // 快进时间
+    const user = storageManager.getItem('user');
+    expect(user).toEqual({ age: 30, name: 'John Doe' });
+  });
+
+  it('should handle multiple items with different expiry times', () => {
+    storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
+    storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
+    vi.advanceTimersByTime(1500); // 快进时间
+    storageManager.clearExpiredItems();
+    const user1 = storageManager.getItem('user1');
+    const user2 = storageManager.getItem('user2');
+    expect(user1).toBeNull();
+    expect(user2).toEqual({ age: 25, name: 'Jane Doe' });
+  });
+
+  it('should handle items with no expiry', () => {
+    storageManager.setItem('user', { age: 30, name: 'John Doe' });
+    vi.advanceTimersByTime(10_000); // 快进时间
+    storageManager.clearExpiredItems();
+    const user = storageManager.getItem('user');
+    expect(user).toEqual({ age: 30, name: 'John Doe' });
+  });
+
+  it('should clear all items correctly', () => {
+    storageManager.setItem('user1', { age: 30, name: 'John Doe' });
+    storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
+    storageManager.clear();
+    const user1 = storageManager.getItem('user1');
+    const user2 = storageManager.getItem('user2');
+    expect(user1).toBeNull();
+    expect(user2).toBeNull();
+  });
+});

+ 118 - 0
packages/@vben-core/shared/chche/src/storage-manager.ts

@@ -0,0 +1,118 @@
+type StorageType = 'localStorage' | 'sessionStorage';
+
+interface StorageManagerOptions {
+  prefix?: string;
+  storageType?: StorageType;
+}
+
+interface StorageItem<T> {
+  expiry?: number;
+  value: T;
+}
+
+class StorageManager<T> {
+  private prefix: string;
+  private storage: Storage;
+
+  constructor({
+    prefix = '',
+    storageType = 'localStorage',
+  }: StorageManagerOptions = {}) {
+    this.prefix = prefix;
+    this.storage =
+      storageType === 'localStorage'
+        ? window.localStorage
+        : window.sessionStorage;
+  }
+
+  /**
+   * 获取完整的存储键
+   * @param key 原始键
+   * @returns 带前缀的完整键
+   */
+  private getFullKey(key: string): string {
+    return `${this.prefix}-${key}`;
+  }
+
+  /**
+   * 清除所有带前缀的存储项
+   */
+  clear(): void {
+    const keysToRemove: string[] = [];
+    for (let i = 0; i < this.storage.length; i++) {
+      const key = this.storage.key(i);
+      if (key && key.startsWith(this.prefix)) {
+        keysToRemove.push(key);
+      }
+    }
+    keysToRemove.forEach((key) => this.storage.removeItem(key));
+  }
+
+  /**
+   * 清除所有过期的存储项
+   */
+  clearExpiredItems(): void {
+    for (let i = 0; i < this.storage.length; i++) {
+      const key = this.storage.key(i);
+      if (key && key.startsWith(this.prefix)) {
+        const shortKey = key.replace(this.prefix, '');
+        this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项
+      }
+    }
+  }
+
+  /**
+   * 获取存储项
+   * @param key 键
+   * @param defaultValue 当项不存在或已过期时返回的默认值
+   * @returns 值,如果项已过期或解析错误则返回默认值
+   */
+  getItem(key: string, defaultValue: T | null = null): T | null {
+    const fullKey = this.getFullKey(key);
+    const itemStr = this.storage.getItem(fullKey);
+    if (!itemStr) {
+      return defaultValue;
+    }
+
+    try {
+      const item: StorageItem<T> = JSON.parse(itemStr);
+      if (item.expiry && Date.now() > item.expiry) {
+        this.storage.removeItem(fullKey);
+        return defaultValue;
+      }
+      return item.value;
+    } catch (error) {
+      console.error(`Error parsing item with key "${fullKey}":`, error);
+      this.storage.removeItem(fullKey); // 如果解析失败,删除该项
+      return defaultValue;
+    }
+  }
+
+  /**
+   * 移除存储项
+   * @param key 键
+   */
+  removeItem(key: string): void {
+    const fullKey = this.getFullKey(key);
+    this.storage.removeItem(fullKey);
+  }
+
+  /**
+   * 设置存储项
+   * @param key 键
+   * @param value 值
+   * @param ttl 存活时间(毫秒)
+   */
+  setItem(key: string, value: T, ttl?: number): void {
+    const fullKey = this.getFullKey(key);
+    const expiry = ttl ? Date.now() + ttl : undefined;
+    const item: StorageItem<T> = { expiry, value };
+    try {
+      this.storage.setItem(fullKey, JSON.stringify(item));
+    } catch (error) {
+      console.error(`Error setting item with key "${fullKey}":`, error);
+    }
+  }
+}
+
+export { StorageManager };

+ 1 - 1
packages/@vben-core/shared/design/src/tailwind.css

@@ -18,7 +18,7 @@
   }
 
   .outline-box {
-    @apply outline-border relative  cursor-pointer rounded-md p-1 outline outline-1;
+    @apply outline-border relative cursor-pointer rounded-md p-1 outline outline-1;
 
     &::after {
       @apply absolute left-1/2 top-1/2 z-20 h-0 w-[1px] rounded-sm opacity-0 outline outline-2 outline-transparent transition-all duration-300 content-[''];

+ 1 - 0
packages/@vben-core/shared/toolkit/src/index.ts

@@ -3,6 +3,7 @@ export * from './date';
 export * from './diff';
 export * from './hash';
 export * from './inference';
+export * from './letter';
 export * from './merge';
 export * from './namespace';
 export * from './nprogress';

+ 55 - 0
packages/@vben-core/shared/toolkit/src/letter.test.ts

@@ -0,0 +1,55 @@
+import { describe, expect, it } from 'vitest';
+
+import { capitalizeFirstLetter, toLowerCaseFirstLetter } from './letter';
+
+// 编写测试用例
+describe('capitalizeFirstLetter', () => {
+  it('should capitalize the first letter of a string', () => {
+    expect(capitalizeFirstLetter('hello')).toBe('Hello');
+    expect(capitalizeFirstLetter('world')).toBe('World');
+  });
+
+  it('should handle empty strings', () => {
+    expect(capitalizeFirstLetter('')).toBe('');
+  });
+
+  it('should handle single character strings', () => {
+    expect(capitalizeFirstLetter('a')).toBe('A');
+    expect(capitalizeFirstLetter('b')).toBe('B');
+  });
+
+  it('should not change the case of other characters', () => {
+    expect(capitalizeFirstLetter('hElLo')).toBe('HElLo');
+  });
+});
+
+describe('toLowerCaseFirstLetter', () => {
+  it('should convert the first letter to lowercase', () => {
+    expect(toLowerCaseFirstLetter('CommonAppName')).toBe('commonAppName');
+    expect(toLowerCaseFirstLetter('AnotherKeyExample')).toBe(
+      'anotherKeyExample',
+    );
+  });
+
+  it('should return the same string if the first letter is already lowercase', () => {
+    expect(toLowerCaseFirstLetter('alreadyLowerCase')).toBe('alreadyLowerCase');
+  });
+
+  it('should handle empty strings', () => {
+    expect(toLowerCaseFirstLetter('')).toBe('');
+  });
+
+  it('should handle single character strings', () => {
+    expect(toLowerCaseFirstLetter('A')).toBe('a');
+    expect(toLowerCaseFirstLetter('a')).toBe('a');
+  });
+
+  it('should handle strings with only one uppercase letter', () => {
+    expect(toLowerCaseFirstLetter('A')).toBe('a');
+  });
+
+  it('should handle strings with special characters', () => {
+    expect(toLowerCaseFirstLetter('!Special')).toBe('!Special');
+    expect(toLowerCaseFirstLetter('123Number')).toBe('123Number');
+  });
+});

+ 20 - 0
packages/@vben-core/shared/toolkit/src/letter.ts

@@ -0,0 +1,20 @@
+/**
+ * 将字符串的首字母大写
+ * @param string
+ */
+function capitalizeFirstLetter(string: string): string {
+  return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+/**
+ * 将字符串的首字母转换为小写。
+ *
+ * @param str 要转换的字符串
+ * @returns 首字母小写的字符串
+ */
+function toLowerCaseFirstLetter(str: string): string {
+  if (!str) return str; // 如果字符串为空,直接返回
+  return str.charAt(0).toLowerCase() + str.slice(1);
+}
+
+export { capitalizeFirstLetter, toLowerCaseFirstLetter };

+ 22 - 0
packages/@vben-core/shared/typings/src/app.ts

@@ -0,0 +1,22 @@
+type LocaleSupportType = 'en-US' | 'zh-CN';
+
+type LayoutType =
+  | 'full-content'
+  | 'header-nav'
+  | 'mixed-nav'
+  | 'side-mixed-nav'
+  | 'side-nav';
+
+type ThemeModeType = 'auto' | 'dark' | 'light';
+
+type ContentCompactType = 'compact' | 'wide';
+
+type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
+
+export type {
+  ContentCompactType,
+  LayoutHeaderModeType,
+  LayoutType,
+  LocaleSupportType,
+  ThemeModeType,
+};

+ 40 - 0
packages/@vben-core/shared/typings/src/flatten.d.ts

@@ -0,0 +1,40 @@
+// `Prev` 类型用于表示递归深度的递减。它是一个元组,其索引代表了递归的层数,通过索引访问可以得到减少后的层数。
+// 例如,Prev[3] 等于 2,表示递归深度从 3 减少到 2。
+type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
+
+// `FlattenDepth` 类型用于将一个嵌套的对象类型“展平”,同时考虑到了递归的深度。
+// 它接受三个泛型参数:T(要处理的类型),Prefix(属性名前缀,默认为空字符串),Depth(递归深度,默认为3)。
+// 如果当前深度(Depth)为 0,则停止递归并返回 `never`。否则,如果属性值是对象类型,则递归调用 `FlattenDepth` 并递减深度。
+// 对于非对象类型的属性,将其直接映射到结果类型中,并根据前缀构造属性名。
+
+type FlattenDepth<T, Prefix extends string = '', Depth extends number = 4> = {
+  [K in keyof T]: T[K] extends object
+    ? Depth extends 0
+      ? never
+      : FlattenDepth<
+          T[K],
+          `${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`,
+          Prev[Depth]
+        >
+    : {
+        [P in `${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`]: T[K];
+      };
+}[keyof T] extends infer O
+  ? { [P in keyof O]: O[P] }
+  : never;
+
+// `UnionToIntersection` 类型用于将一个联合类型转换为交叉类型。
+// 这个类型通过条件类型和类型推断的方式来实现。它先尝试将输入类型(U)映射为一个函数类型,
+// 然后通过推断这个函数类型的返回类型(infer I),最终得到一个交叉类型。
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
+  k: infer I,
+) => void
+  ? I
+  : never;
+
+type Flatten<T> = UnionToIntersection<FlattenDepth<T>>;
+
+type FlattenObject<T> = FlattenDepth<T>;
+type FlattenObjectKeys<T> = keyof FlattenObject<T>;
+
+export type { Flatten, FlattenObject, FlattenObjectKeys, UnionToIntersection };

+ 2 - 1
packages/@vben-core/shared/typings/src/index.ts

@@ -1,5 +1,6 @@
 export type * from './access';
+export type * from './app';
+export type * from './flatten';
 export type * from './menu-record';
-export type * from './preference';
 export type * from './tabs';
 export type * from './tools';

+ 0 - 144
packages/@vben-core/shared/typings/src/preference.ts

@@ -1,144 +0,0 @@
-type LayoutType =
-  | 'full-content'
-  | 'header-nav'
-  | 'mixed-nav'
-  | 'side-mixed-nav'
-  | 'side-nav';
-
-type BreadcrumbStyle = 'background' | 'normal';
-
-type NavigationStyle = 'plain' | 'rounded';
-
-type ThemeType = 'auto' | 'dark' | 'light';
-
-type ContentCompactType = 'compact' | 'wide';
-
-type LayoutHeaderMode = 'auto' | 'auto-scroll' | 'fixed' | 'static';
-
-type PageTransitionType = 'fade-slide';
-
-type AuthPageLayout = 'panel-center' | 'panel-left' | 'panel-right';
-
-type SupportLocale = 'en-US' | 'zh-CN';
-
-interface Language {
-  key: SupportLocale;
-  text: string;
-}
-
-interface Preference {
-  /** 应用名 */
-  appName: string;
-  /** 登录注册页面布局 */
-  authPageLayout: AuthPageLayout;
-  /** 面包屑是否只有一个时隐藏 */
-  breadcrumbHideOnlyOne: boolean;
-  /** 面包屑首页图标是否可见 */
-  breadcrumbHome: boolean;
-  /** 面包屑图标是否可见 */
-  breadcrumbIcon: boolean;
-  /** 面包屑类型 */
-  breadcrumbStyle: BreadcrumbStyle;
-  /** 面包屑是否可见 */
-  breadcrumbVisible: boolean;
-  /** 是否开启灰色模式 */
-  colorGrayMode: boolean;
-  /** 主题色 */
-  colorPrimary: string;
-  /** 是否开启色弱模式 */
-  colorWeakMode: boolean;
-  /** 是否开启紧凑模式 */
-  compact: boolean;
-  /** 是否开启内容紧凑模式 */
-  contentCompact: ContentCompactType;
-  /** 页脚Copyright */
-  copyright: string;
-  /** 应用默认头像 */
-  defaultAvatar: string;
-  /** 开启动态标题 */
-  dynamicTitle: boolean;
-  /** 页脚是否固定 */
-  footerFixed: boolean;
-  /** 页脚是否可见 */
-  footerVisible: boolean;
-  /** 顶栏是否隐藏 */
-  headerHidden: boolean;
-  /** header显示模式 */
-  headerMode: LayoutHeaderMode;
-  /** 顶栏是否可见 */
-  headerVisible: boolean;
-  /** 是否移动端 */
-  isMobile: boolean;
-  /** 开启标签页缓存功能 */
-  keepAlive: boolean;
-  /** 布局方式 */
-  layout: LayoutType;
-  /** 支持的语言 */
-  locale: SupportLocale;
-  /** 应用Logo */
-  logo: string;
-  /** logo是否可见 */
-  logoVisible: boolean;
-  /** 导航菜单手风琴模式 */
-  navigationAccordion: boolean;
-  /** 导航菜单是否切割,只在 layout=mixed-nav 生效 */
-  navigationSplit: boolean;
-  /** 导航菜单风格 */
-  navigationStyle: NavigationStyle;
-  /** 是否开启页面加载进度条 */
-  pageProgress: boolean;
-  /** 页面切换动画 */
-  pageTransition: PageTransitionType;
-  /** 页面切换动画是否启用 */
-  pageTransitionEnable: boolean;
-  /** 是否开启半深色菜单(只在theme='light'时生效) */
-  semiDarkMenu: boolean;
-  /** 是否启用快捷键 */
-  shortcutKeys: boolean;
-  /** 是否显示偏好设置 */
-  showPreference: boolean;
-  /** 侧边栏是否折叠 */
-  sideCollapse: boolean;
-  /** 侧边栏折叠时,是否显示title */
-  sideCollapseShowTitle: boolean;
-  /** 菜单自动展开状态 */
-  sideExpandOnHover: boolean;
-  /** 侧边栏扩展区域是否折叠 */
-  sideExtraCollapse: boolean;
-  /** 侧边栏是否隐藏 */
-  sideHidden: boolean;
-  /** 侧边栏是否可见 */
-  sideVisible: boolean;
-  /** 侧边栏宽度 */
-  sideWidth: number;
-  /** 是否开启多标签页图标 */
-  tabsIcon: boolean;
-  /** 是否开启多标签页 */
-  tabsVisible: boolean;
-  /** 当前主题 */
-  theme: ThemeType;
-}
-
-// 这些属性是静态的,不会随着用户的操作而改变
-interface StaticPreference {
-  /** 主题色预设 */
-  colorPrimaryPresets: string[];
-  /** 支持的语言 */
-  supportLanguages: Language[];
-}
-
-type PreferenceKeys = keyof Preference;
-
-export type {
-  AuthPageLayout,
-  BreadcrumbStyle,
-  ContentCompactType,
-  LayoutHeaderMode,
-  LayoutType,
-  PageTransitionType,
-  Preference,
-  PreferenceKeys,
-  StaticPreference,
-  SupportLocale,
-  ThemeType,
-};

+ 4 - 4
packages/@vben-core/uikit/layout-ui/src/vben-layout.ts

@@ -1,8 +1,8 @@
 import type {
   ContentCompactType,
-  LayoutHeaderMode,
+  LayoutHeaderModeType,
   LayoutType,
-  ThemeType,
+  ThemeModeType,
 } from '@vben-core/typings';
 
 interface VbenLayoutProps {
@@ -86,7 +86,7 @@ interface VbenLayoutProps {
    * header 显示模式
    * @default 'fixed'
    */
-  headerMode?: LayoutHeaderMode;
+  headerMode?: LayoutHeaderModeType;
   /**
    * header是否显示
    * @default true
@@ -146,7 +146,7 @@ interface VbenLayoutProps {
    * 侧边栏
    * @default dark
    */
-  sideTheme?: ThemeType;
+  sideTheme?: ThemeModeType;
   /**
    * 侧边栏是否可见
    * @default true

+ 1 - 1
packages/@vben-core/uikit/layout-ui/src/vben-layout.vue

@@ -460,7 +460,7 @@ function handleOpenMenu() {
 
 <template>
   <div class="relative flex min-h-full w-full">
-    <slot name="preference"></slot>
+    <slot name="preferences"></slot>
     <slot name="floating-button-group"></slot>
     <LayoutSide
       v-if="sideVisibleState"

+ 2 - 2
packages/@vben-core/uikit/menu-ui/src/interface/index.ts

@@ -1,4 +1,4 @@
-import type { MenuRecordBadgeRaw, ThemeType } from '@vben-core/typings';
+import type { MenuRecordBadgeRaw, ThemeModeType } from '@vben-core/typings';
 
 import type { Ref } from 'vue';
 
@@ -46,7 +46,7 @@ interface MenuProps {
    * @zh_CN 菜单主题
    * @default dark
    */
-  theme?: ThemeType;
+  theme?: ThemeModeType;
 }
 
 interface SubMenuProps extends MenuRecordBadgeRaw {

+ 1 - 1
packages/@vben-core/uikit/shadcn-ui/src/components/ui/breadcrumb/BreadcrumbLink.vue

@@ -16,7 +16,7 @@ const props = withDefaults(
   <Primitive
     :as="as"
     :as-child="asChild"
-    :class="cn('hover:text-foreground  transition-colors', props.class)"
+    :class="cn('hover:text-foreground transition-colors', props.class)"
   >
     <slot></slot>
   </Primitive>

+ 1 - 1
packages/@vben-core/uikit/shadcn-ui/src/components/ui/popover/PopoverContent.vue

@@ -37,7 +37,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
       v-bind="{ ...forwarded, ...$attrs }"
       :class="
         cn(
-          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2  w-72 rounded-md border p-4 shadow-md outline-none',
+          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 w-72 rounded-md border p-4 shadow-md outline-none',
           props.class,
         )
       "

+ 1 - 0
packages/README.md

@@ -0,0 +1 @@
+# packages

+ 1 - 1
packages/business/common-ui/package.json

@@ -46,10 +46,10 @@
   "dependencies": {
     "@vben-core/design": "workspace:*",
     "@vben-core/iconify": "workspace:*",
+    "@vben-core/preferences": "workspace:*",
     "@vben-core/shadcn-ui": "workspace:*",
     "@vben-core/toolkit": "workspace:*",
     "@vben/locales": "workspace:*",
-    "@vben/preference": "workspace:*",
     "@vueuse/core": "^10.10.0",
     "@vueuse/integrations": "^10.10.0",
     "qrcode": "^1.5.3",

+ 13 - 13
packages/business/common-ui/src/authentication/color-toggle.vue

@@ -1,20 +1,21 @@
 <script setup lang="ts">
 import { IcRoundColorLens } from '@vben-core/iconify';
-import { VbenIconButton } from '@vben-core/shadcn-ui';
-
 import {
-  preference,
-  staticPreference,
-  updatePreference,
-} from '@vben/preference';
+  COLOR_PRIMARY_RESETS,
+  preferences,
+  updatePreferences,
+} from '@vben-core/preferences';
+import { VbenIconButton } from '@vben-core/shadcn-ui';
 
 defineOptions({
   name: 'AuthenticationColorToggle',
 });
 
 function handleUpdate(value: string) {
-  updatePreference({
-    colorPrimary: value,
+  updatePreferences({
+    theme: {
+      colorPrimary: value,
+    },
   });
 }
 </script>
@@ -24,10 +25,7 @@ function handleUpdate(value: string) {
     <div
       class="ease-ou flex w-0 overflow-hidden transition-all duration-500 group-hover:w-48"
     >
-      <template
-        v-for="color in staticPreference.colorPrimaryPresets"
-        :key="color"
-      >
+      <template v-for="color in COLOR_PRIMARY_RESETS" :key="color">
         <VbenIconButton
           class="flex-center flex-shrink-0"
           @click="handleUpdate(color)"
@@ -35,7 +33,9 @@ function handleUpdate(value: string) {
           <div
             class="relative h-3.5 w-3.5 rounded-[2px] before:absolute before:left-0.5 before:top-0.5 before:h-2.5 before:w-2.5 before:rounded-[2px] before:border before:border-gray-900 before:opacity-0 before:transition-all before:duration-150 before:content-[''] hover:scale-110"
             :class="[
-              preference.colorPrimary === color ? `before:opacity-100` : '',
+              preferences.theme.colorPrimary === color
+                ? `before:opacity-100`
+                : '',
             ]"
             :style="{ backgroundColor: color }"
           ></div>

+ 3 - 12
packages/business/common-ui/src/authentication/layout-toggle.vue

@@ -1,17 +1,15 @@
 <script setup lang="ts">
-import type { AuthPageLayout } from '@vben/types';
 import type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';
 
 import { MdiDockBottom, MdiDockLeft, MdiDockRight } from '@vben-core/iconify';
+import { preferences, usePreferences } from '@vben-core/preferences';
 import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
 
 import { $t } from '@vben/locales';
-import { preference, updatePreference, usePreference } from '@vben/preference';
 import { computed } from 'vue';
 
 defineOptions({
   name: 'AuthenticationLayoutToggle',
-  // inheritAttrs: false,
 });
 
 const menus = computed((): VbenDropdownMenuItem[] => [
@@ -32,20 +30,13 @@ const menus = computed((): VbenDropdownMenuItem[] => [
   },
 ]);
 
-function handleUpdate(value: string) {
-  updatePreference({
-    authPageLayout: value as AuthPageLayout,
-  });
-}
-
-const { authPanelCenter, authPanelLeft, authPanelRight } = usePreference();
+const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();
 </script>
 
 <template>
   <VbenDropdownRadioMenu
+    v-model="preferences.app.authPageLayout"
     :menus="menus"
-    :model-value="preference.authPageLayout"
-    @update:model-value="handleUpdate"
   >
     <VbenIconButton>
       <MdiDockRight v-if="authPanelRight" class="size-5" />

+ 1 - 1
packages/business/common-ui/src/index.ts

@@ -4,7 +4,7 @@ export * from './global-provider';
 export * from './global-search';
 export * from './language-toggle';
 export * from './notification';
-export * from './preference';
+export * from './preferences';
 export * from './spinner';
 export * from './theme-toggle';
 export * from './user-dropdown';

+ 13 - 11
packages/business/common-ui/src/language-toggle/language-toggle.vue

@@ -1,26 +1,28 @@
 <script setup lang="ts">
-import type { SupportLocale } from '@vben/types';
+import type { LocaleSupportType } from '@vben/types';
 
 import { IcBaselineLanguage } from '@vben-core/iconify';
+import {
+  SUPPORT_LANGUAGES,
+  preferences,
+  updatePreferences,
+} from '@vben-core/preferences';
 import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
 
 import { loadLocaleMessages } from '@vben/locales';
-import {
-  preference,
-  staticPreference,
-  updatePreference,
-} from '@vben/preference';
 
 defineOptions({
   name: 'LanguageToggle',
 });
 
-const menus = staticPreference.supportLanguages;
+const menus = SUPPORT_LANGUAGES;
 
 async function handleUpdate(value: string) {
-  const locale = value as SupportLocale;
-  updatePreference({
-    locale,
+  const locale = value as LocaleSupportType;
+  updatePreferences({
+    app: {
+      locale,
+    },
   });
   // 更改预览
   await loadLocaleMessages(locale);
@@ -31,7 +33,7 @@ async function handleUpdate(value: string) {
   <div>
     <VbenDropdownRadioMenu
       :menus="menus"
-      :model-value="preference.locale"
+      :model-value="preferences.app.locale"
       @update:model-value="handleUpdate"
     >
       <VbenIconButton>

+ 0 - 1
packages/business/common-ui/src/preference/index.ts

@@ -1 +0,0 @@
-export { default as PreferenceWidget } from './preference-widget.vue';

+ 0 - 102
packages/business/common-ui/src/preference/preference-widget.vue

@@ -1,102 +0,0 @@
-<script lang="ts" setup>
-import type { PreferenceKeys, SupportLocale } from '@vben/types';
-
-import { loadLocaleMessages } from '@vben/locales';
-import {
-  preference,
-  staticPreference,
-  updatePreference,
-} from '@vben/preference';
-
-import Preference from './preference.vue';
-
-function handleUpdate(key: PreferenceKeys, value: boolean | string) {
-  updatePreference({
-    [key]: value,
-  });
-}
-
-function updateLocale(value: string) {
-  const locale = value as SupportLocale;
-  updatePreference({
-    locale,
-  });
-  // 更改预览
-  loadLocaleMessages(locale);
-}
-</script>
-<template>
-  <Preference
-    :color-primary-presets="staticPreference.colorPrimaryPresets"
-    :breadcrumb-visible="preference.breadcrumbVisible"
-    :breadcrumb-style="preference.breadcrumbStyle"
-    :color-gray-mode="preference.colorGrayMode"
-    :breadcrumb-icon="preference.breadcrumbIcon"
-    :color-primary="preference.colorPrimary"
-    :color-weak-mode="preference.colorWeakMode"
-    :content-compact="preference.contentCompact"
-    :breadcrumb-home="preference.breadcrumbHome"
-    :side-collapse="preference.sideCollapse"
-    :layout="preference.layout"
-    :semi-dark-menu="preference.semiDarkMenu"
-    :side-visible="preference.sideVisible"
-    :footer-visible="preference.footerVisible"
-    :tabs-visible="preference.tabsVisible"
-    :header-visible="preference.headerVisible"
-    :footer-fixed="preference.footerFixed"
-    :header-mode="preference.headerMode"
-    :theme="preference.theme"
-    :dynamic-title="preference.dynamicTitle"
-    :breadcrumb-hide-only-one="preference.breadcrumbHideOnlyOne"
-    :page-transition="preference.pageTransition"
-    :page-progress="preference.pageProgress"
-    :tabs-icon="preference.tabsIcon"
-    :locale="preference.locale"
-    :navigation-accordion="preference.navigationAccordion"
-    :navigation-style="preference.navigationStyle"
-    :shortcut-keys="preference.shortcutKeys"
-    :navigation-split="preference.navigationSplit"
-    :side-collapse-show-title="preference.sideCollapseShowTitle"
-    :page-transition-enable="preference.pageTransitionEnable"
-    @update:shortcut-keys="(value) => handleUpdate('shortcutKeys', value)"
-    @update:navigation-style="(value) => handleUpdate('navigationStyle', value)"
-    @update:navigation-accordion="
-      (value) => handleUpdate('navigationAccordion', value)
-    "
-    @update:navigation-split="(value) => handleUpdate('navigationSplit', value)"
-    @update:dynamic-title="(value) => handleUpdate('dynamicTitle', value)"
-    @update:tabs-icon="(value) => handleUpdate('tabsIcon', value)"
-    @update:side-collapse="(value) => handleUpdate('sideCollapse', value)"
-    @update:locale="updateLocale"
-    @update:header-visible="(value) => handleUpdate('headerVisible', value)"
-    @update:side-visible="(value) => handleUpdate('sideVisible', value)"
-    @update:footer-visible="(value) => handleUpdate('footerVisible', value)"
-    @update:tabs-visible="(value) => handleUpdate('tabsVisible', value)"
-    @update:header-mode="(value) => handleUpdate('headerMode', value)"
-    @update:footer-fixed="(value) => handleUpdate('footerFixed', value)"
-    @update:breadcrumb-visible="
-      (value) => handleUpdate('breadcrumbVisible', value)
-    "
-    @update:breadcrumb-hide-only-one="
-      (value) => handleUpdate('breadcrumbHideOnlyOne', value)
-    "
-    @update:side-collapse-show-title="
-      (value) => handleUpdate('sideCollapseShowTitle', value)
-    "
-    @update:breadcrumb-home="(value) => handleUpdate('breadcrumbHome', value)"
-    @update:breadcrumb-icon="(value) => handleUpdate('breadcrumbIcon', value)"
-    @update:breadcrumb-style="(value) => handleUpdate('breadcrumbStyle', value)"
-    @update:page-transition-enable="
-      (value) => handleUpdate('pageTransitionEnable', value)
-    "
-    @update:color-gray-mode="(value) => handleUpdate('colorGrayMode', value)"
-    @update:page-transition="(value) => handleUpdate('pageTransition', value)"
-    @update:page-progress="(value) => handleUpdate('pageProgress', value)"
-    @update:color-primary="(value) => handleUpdate('colorPrimary', value)"
-    @update:color-weak-mode="(value) => handleUpdate('colorWeakMode', value)"
-    @update:content-compact="(value) => handleUpdate('contentCompact', value)"
-    @update:layout="(value) => handleUpdate('layout', value)"
-    @update:semi-dark-menu="(value) => handleUpdate('semiDarkMenu', value)"
-    @update:theme="(value) => handleUpdate('theme', value)"
-  />
-</template>

+ 0 - 16
packages/business/common-ui/src/preference/use-open-preference.ts

@@ -1,16 +0,0 @@
-import { ref } from 'vue';
-
-const openPreference = ref(false);
-
-function useOpenPreference() {
-  function handleOpenPreference() {
-    openPreference.value = true;
-  }
-
-  return {
-    handleOpenPreference,
-    openPreference,
-  };
-}
-
-export { useOpenPreference };

+ 0 - 0
packages/business/common-ui/src/preference/blocks/block.vue → packages/business/common-ui/src/preferences/blocks/block.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/general/animation.vue → packages/business/common-ui/src/preferences/blocks/general/animation.vue


+ 6 - 7
packages/business/common-ui/src/preference/blocks/general/general.vue → packages/business/common-ui/src/preferences/blocks/general/general.vue

@@ -1,8 +1,9 @@
 <script setup lang="ts">
 import type { SelectListItem } from '@vben/types';
 
+import { SUPPORT_LANGUAGES } from '@vben-core/preferences';
+
 import { $t } from '@vben/locales';
-import { staticPreference } from '@vben/preference';
 
 import SelectItem from '../select-item.vue';
 import SwitchItem from '../switch-item.vue';
@@ -15,12 +16,10 @@ const locale = defineModel<string>('locale');
 const dynamicTitle = defineModel<boolean>('dynamicTitle');
 const shortcutKeys = defineModel<boolean>('shortcutKeys');
 
-const localeItems: SelectListItem[] = staticPreference.supportLanguages.map(
-  (item) => ({
-    label: item.text,
-    value: item.key,
-  }),
-);
+const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
+  label: item.text,
+  value: item.key,
+}));
 </script>
 
 <template>

+ 0 - 0
packages/business/common-ui/src/preference/blocks/index.ts → packages/business/common-ui/src/preferences/blocks/index.ts


+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/breadcrumb.vue → packages/business/common-ui/src/preferences/blocks/layout/breadcrumb.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/content.vue → packages/business/common-ui/src/preferences/blocks/layout/content.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/footer.vue → packages/business/common-ui/src/preferences/blocks/layout/footer.vue


+ 2 - 2
packages/business/common-ui/src/preference/blocks/layout/header.vue → packages/business/common-ui/src/preferences/blocks/layout/header.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import type { LayoutHeaderMode, SelectListItem } from '@vben/types';
+import type { LayoutHeaderModeType, SelectListItem } from '@vben/types';
 
 import { $t } from '@vben/locales';
 
@@ -13,7 +13,7 @@ defineOptions({
 defineProps<{ disabled: boolean }>();
 
 const headerVisible = defineModel<boolean>('headerVisible');
-const headerMode = defineModel<LayoutHeaderMode>('headerMode');
+const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
 
 const localeItems: SelectListItem[] = [
   {

+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/interface-control.vue → packages/business/common-ui/src/preferences/blocks/layout/interface-control.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/layout.vue → packages/business/common-ui/src/preferences/blocks/layout/layout.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/navigation.vue → packages/business/common-ui/src/preferences/blocks/layout/navigation.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/sidebar.vue → packages/business/common-ui/src/preferences/blocks/layout/sidebar.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/layout/tabs.vue → packages/business/common-ui/src/preferences/blocks/layout/tabs.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/select-item.vue → packages/business/common-ui/src/preferences/blocks/select-item.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/switch-item.vue → packages/business/common-ui/src/preferences/blocks/switch-item.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/theme/color-mode.vue → packages/business/common-ui/src/preferences/blocks/theme/color-mode.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/theme/color.vue → packages/business/common-ui/src/preferences/blocks/theme/color.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/theme/theme.vue → packages/business/common-ui/src/preferences/blocks/theme/theme.vue


+ 0 - 0
packages/business/common-ui/src/preference/blocks/toggle-item.vue → packages/business/common-ui/src/preferences/blocks/toggle-item.vue


+ 0 - 0
packages/business/common-ui/src/preference/icons/content-compact.vue → packages/business/common-ui/src/preferences/icons/content-compact.vue


+ 0 - 0
packages/business/common-ui/src/preference/icons/full-content.vue → packages/business/common-ui/src/preferences/icons/full-content.vue


+ 0 - 0
packages/business/common-ui/src/preference/icons/header-nav.vue → packages/business/common-ui/src/preferences/icons/header-nav.vue


Some files were not shown because too many files changed in this diff