Browse Source

feat: new interface pendant can be configured to display hidden

vince 10 tháng trước cách đây
mục cha
commit
a765d3bbc0
36 tập tin đã thay đổi với 256 bổ sung612 xóa
  1. 1 1
      apps/backend-mock/README.md
  2. 1 1
      apps/web-antd/index.html
  3. 3 2
      apps/web-antd/src/forward/access.ts
  4. 1 1
      apps/web-antd/src/locales/index.ts
  5. 34 0
      apps/web-antd/src/locales/langs/en-US.json
  6. 0 28
      apps/web-antd/src/locales/langs/en-US.yaml
  7. 35 0
      apps/web-antd/src/locales/langs/zh-CN.json
  8. 0 27
      apps/web-antd/src/locales/langs/zh-CN.yaml
  9. 4 3
      apps/web-antd/src/preferences.ts
  10. 9 9
      apps/web-antd/src/views/demos/access/backend/button-control.vue
  11. 17 17
      apps/web-antd/src/views/demos/access/frontend/button-control.vue
  12. 0 22
      internal/vite-config/src/plugins/index.ts
  13. 0 132
      packages/@core/forward/helpers/src/flatten-object.test.ts
  14. 0 82
      packages/@core/forward/helpers/src/flatten-object.ts
  15. 0 2
      packages/@core/forward/helpers/src/index.ts
  16. 0 115
      packages/@core/forward/helpers/src/nested-object.test.ts
  17. 0 70
      packages/@core/forward/helpers/src/nested-object.ts
  18. 10 1
      packages/@core/forward/preferences/src/config.ts
  19. 7 5
      packages/@core/forward/preferences/src/index.ts
  20. 22 2
      packages/@core/forward/preferences/src/types.ts
  21. 10 1
      packages/@core/locales/src/langs/en-US.json
  22. 10 1
      packages/@core/locales/src/langs/zh-CN.json
  23. 0 40
      packages/@core/shared/typings/src/flatten.d.ts
  24. 0 1
      packages/@core/shared/typings/src/index.ts
  25. 11 6
      packages/@core/ui-kit/layout-ui/src/vben-layout.ts
  26. 5 4
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  27. 1 1
      packages/business/access/src/code-access.vue
  28. 2 2
      packages/business/access/src/index.ts
  29. 1 1
      packages/business/access/src/role-access.vue
  30. 6 5
      packages/business/layouts/src/basic/header/header.vue
  31. 4 3
      packages/business/layouts/src/basic/layout.vue
  32. 0 4
      packages/business/layouts/src/widgets/preferences/blocks/general/general.vue
  33. 1 0
      packages/business/layouts/src/widgets/preferences/blocks/index.ts
  34. 0 21
      packages/business/layouts/src/widgets/preferences/blocks/layout/interface-control.vue
  35. 41 0
      packages/business/layouts/src/widgets/preferences/blocks/layout/widget.vue
  36. 20 2
      packages/business/layouts/src/widgets/preferences/preferences-sheet.vue

+ 1 - 1
apps/backend-mock/README.md

@@ -2,7 +2,7 @@
 
 ## Description
 
-Vben Admin Pro 数据mock服务
+Vben Admin Pro 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。由于 sqlite 安装需要在本地进行编译,所以这里接口是直接返回的。线上环境不再提供mock集成,可自行部署服务或者对接真实数据,同步 mock.js等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了 真是的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。
 
 ## Running the app
 

+ 1 - 1
apps/web-antd/index.html

@@ -11,7 +11,7 @@
       name="viewport"
       content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
     />
-    <!-- 由 vite 注入 VITE_GLOB_APP_TITLE 变量,在 . env 内配置 -->
+    <!-- 由 vite 注入 VITE_GLOB_APP_TITLE 变量,在 .env 文件内配置 -->
     <title><%= VITE_GLOB_APP_TITLE %></title>
     <link rel="icon" href="/favicon.ico" />
   </head>

+ 3 - 2
apps/web-antd/src/forward/access.ts

@@ -10,7 +10,8 @@ import { getAllMenus } from '#/apis';
 import { BasicLayout, IFrameView } from '#/layouts';
 import { $t } from '#/locales';
 
-const forbiddenPage = () => import('#/views/_essential/fallback/forbidden.vue');
+const forbiddenComponent = () =>
+  import('#/views/_essential/fallback/forbidden.vue');
 
 async function generateAccess(options: GeneratorMenuAndRoutesOptions) {
   const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
@@ -30,7 +31,7 @@ async function generateAccess(options: GeneratorMenuAndRoutesOptions) {
       return await getAllMenus();
     },
     // 可以指定没有权限跳转403页面
-    forbiddenComponent: forbiddenPage,
+    forbiddenComponent,
     // 如果 route.meta.menuVisibleWithForbidden = true
     layoutMap,
     pageMap,

+ 1 - 1
apps/web-antd/src/locales/index.ts

@@ -10,7 +10,7 @@ import dayjs from 'dayjs';
 
 const antdLocale = ref<Locale>(defaultLocale);
 
-const modules = import.meta.glob('./langs/*.y(a)?ml');
+const modules = import.meta.glob('./langs/*.json');
 
 const localesMap = loadLocalesMap(modules);
 

+ 34 - 0
apps/web-antd/src/locales/langs/en-US.json

@@ -0,0 +1,34 @@
+{
+  "page": {
+    "demos": {
+      "title": "Demos",
+      "access": {
+        "title": "Access Control",
+        "frontend-control": "Front-end Control",
+        "backend-control": "Backend Control",
+        "page": "Page visit",
+        "button": "Button control",
+        "loading-menu": "In the loading menu",
+        "access-test-1": "Super visit",
+        "access-test-2": "Admin visit",
+        "access-test-3": "User visit"
+      },
+      "nested": {
+        "title": "Nested Menu",
+        "menu1": "Menu 1",
+        "menu2": "Menu 2",
+        "menu21": "Menu 2-1",
+        "menu3": "Menu 3",
+        "menu31": "Menu 3-1",
+        "menu32": "Menu 3-2",
+        "menu321": "Menu 3-2-1"
+      },
+      "outside": {
+        "title": "External Page",
+        "embedded": "embedded Page",
+        "external-link": "External Link"
+      },
+      "fallback": { "title": "Fallback Page" }
+    }
+  }
+}

+ 0 - 28
apps/web-antd/src/locales/langs/en-US.yaml

@@ -1,28 +0,0 @@
-page:
-  demos:
-    title: Demos
-    access:
-      title: Access Control
-      frontend-control: Front-end Control
-      backend-control: Backend Control
-      page: Page visit
-      button: Button control
-      loading-menu: In the loading menu
-      access-test-1: Super visit
-      access-test-2: Admin visit
-      access-test-3: User visit
-    nested:
-      title: Nested Menu
-      menu1: Menu 1
-      menu2: Menu 2
-      menu21: Menu 2-1
-      menu3: Menu 3
-      menu31: Menu 3-1
-      menu32: Menu 3-2
-      menu321: Menu 3-2-1
-    outside:
-      title: External Page
-      embedded: embedded Page
-      external-link: External Link
-    fallback:
-      title: Fallback Page

+ 35 - 0
apps/web-antd/src/locales/langs/zh-CN.json

@@ -0,0 +1,35 @@
+{
+  "page": {
+    "demos": {
+      "title": "演示",
+      "access": {
+        "title": "访问控制",
+        "frontend-control": "前端控制",
+        "backend-control": "后端控制",
+        "page": "页面访问",
+        "button": "按钮控制",
+        "access-test-1": "Super 可见",
+        "access-test-2": "Admin 可见",
+        "access-test-3": "User 可见"
+      },
+      "nested": {
+        "title": "嵌套菜单",
+        "menu1": "菜单 1",
+        "menu2": "菜单 2",
+        "menu21": "菜单 2-1",
+        "menu3": "菜单 3",
+        "menu31": "菜单 3-1",
+        "menu32": "菜单 3-2",
+        "menu321": "菜单 3-2-1"
+      },
+      "outside": {
+        "title": "外部页面",
+        "embedded": "内嵌",
+        "external-link": "外链"
+      },
+      "fallback": {
+        "title": "缺省页"
+      }
+    }
+  }
+}

+ 0 - 27
apps/web-antd/src/locales/langs/zh-CN.yaml

@@ -1,27 +0,0 @@
-page:
-  demos:
-    title: 演示
-    access:
-      title: 访问控制
-      frontend-control: 前端控制
-      backend-control: 后端控制
-      page: 页面访问
-      button: 按钮控制
-      access-test-1: Super 可见
-      access-test-2: Admin 可见
-      access-test-3: User 可见
-    nested:
-      title: 嵌套菜单
-      menu1: 菜单 1
-      menu2: 菜单 2
-      menu21: 菜单 2-1
-      menu3: 菜单 3
-      menu31: 菜单 3-1
-      menu32: 菜单 3-2
-      menu321: 菜单 3-2-1
-    outside:
-      title: 外部页面
-      embedded: 内嵌
-      external-link: 外链
-    fallback:
-      title: 缺省页

+ 4 - 3
apps/web-antd/src/preferences.ts

@@ -1,8 +1,9 @@
-import type { DeepPartial } from '@vben/types';
-import type { Preferences } from '@vben-core/preferences';
+import { defineOverridesPreferences } from '@vben-core/preferences';
 
 /**
  * @description 项目配置文件
  * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
  */
-export const overridesPreferences: DeepPartial<Preferences> = {};
+export const overridesPreferences = defineOverridesPreferences({
+  // overrides
+});

+ 9 - 9
apps/web-antd/src/views/demos/access/backend/button-control.vue

@@ -3,7 +3,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
 
 import { useRouter } from 'vue-router';
 
-import { CodeAuthority, useAccess } from '@vben/access';
+import { CodeAccess, useAccess } from '@vben/access';
 
 import { Button } from 'ant-design-vue';
 
@@ -82,20 +82,20 @@ async function changeAccount(role: string) {
 
       <div class="card-box mt-5 p-5 font-semibold">
         <div class="mb-3 text-lg">组件形式控制</div>
-        <CodeAuthority :value="['AC_100100']">
+        <CodeAccess :value="['AC_100100']">
           <Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
-        </CodeAuthority>
-        <CodeAuthority :value="['AC_100030']">
+        </CodeAccess>
+        <CodeAccess :value="['AC_100030']">
           <Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
-        </CodeAuthority>
-        <CodeAuthority :value="['AC_1000001']">
+        </CodeAccess>
+        <CodeAccess :value="['AC_1000001']">
           <Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
-        </CodeAuthority>
-        <CodeAuthority :value="['AC_100100', 'AC_100010']">
+        </CodeAccess>
+        <CodeAccess :value="['AC_100100', 'AC_100010']">
           <Button class="mr-4">
             Super & Admin 账号可见 ["AC_100100","AC_1000001"]
           </Button>
-        </CodeAuthority>
+        </CodeAccess>
       </div>
 
       <div class="card-box mt-5 p-5 font-semibold">

+ 17 - 17
apps/web-antd/src/views/demos/access/frontend/button-control.vue

@@ -3,7 +3,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
 
 import { useRouter } from 'vue-router';
 
-import { CodeAuthority, RoleAuthority, useAccess } from '@vben/access';
+import { CodeAccess, RoleAccess, useAccess } from '@vben/access';
 
 import { Button } from 'ant-design-vue';
 
@@ -81,18 +81,18 @@ async function changeAccount(role: string) {
       </div>
       <div class="card-box mt-5 p-5 font-semibold">
         <div class="mb-3 text-lg">角色 - 组件形式控制</div>
-        <RoleAuthority :value="['super']">
+        <RoleAccess :value="['super']">
           <Button class="mr-4"> Super 角色可见 </Button>
-        </RoleAuthority>
-        <RoleAuthority :value="['admin']">
+        </RoleAccess>
+        <RoleAccess :value="['admin']">
           <Button class="mr-4"> Admin 角色可见 </Button>
-        </RoleAuthority>
-        <RoleAuthority :value="['user']">
+        </RoleAccess>
+        <RoleAccess :value="['user']">
           <Button class="mr-4"> User 角色可见 </Button>
-        </RoleAuthority>
-        <RoleAuthority :value="['super', 'admin']">
+        </RoleAccess>
+        <RoleAccess :value="['super', 'admin']">
           <Button class="mr-4"> Super & Admin 角色都可见 </Button>
-        </RoleAuthority>
+        </RoleAccess>
       </div>
 
       <div class="card-box mt-5 p-5 font-semibold">
@@ -113,20 +113,20 @@ async function changeAccount(role: string) {
 
       <div class="card-box mt-5 p-5 font-semibold">
         <div class="mb-3 text-lg">权限码 - 组件形式控制</div>
-        <CodeAuthority :value="['AC_100100']">
+        <CodeAccess :value="['AC_100100']">
           <Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
-        </CodeAuthority>
-        <CodeAuthority :value="['AC_100030']">
+        </CodeAccess>
+        <CodeAccess :value="['AC_100030']">
           <Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
-        </CodeAuthority>
-        <CodeAuthority :value="['AC_1000001']">
+        </CodeAccess>
+        <CodeAccess :value="['AC_1000001']">
           <Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
-        </CodeAuthority>
-        <CodeAuthority :value="['AC_100100', 'AC_100010']">
+        </CodeAccess>
+        <CodeAccess :value="['AC_100100', 'AC_100010']">
           <Button class="mr-4">
             Super & Admin 账号可见 ["AC_100100","AC_1000001"]
           </Button>
-        </CodeAuthority>
+        </CodeAccess>
       </div>
 
       <div class="card-box mt-5 p-5 font-semibold">

+ 0 - 22
internal/vite-config/src/plugins/index.ts

@@ -7,10 +7,6 @@ import type {
   LibraryPluginOptions,
 } from '../typing';
 
-import { join } from 'node:path';
-
-import { getPackages } from '@vben/node-utils';
-
 import viteVueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
 import viteVue from '@vitejs/plugin-vue';
 import viteVueJsx from '@vitejs/plugin-vue-jsx';
@@ -117,28 +113,10 @@ async function loadApplicationPlugins(
     {
       condition: i18n,
       plugins: async () => {
-        const { packages } = await getPackages();
-
-        const include: string[] = [];
-
-        // 加载所有应用的国际化文件
-        for (const { dir, relativeDir } of packages) {
-          if (
-            // 排除非应用目录
-            !relativeDir.startsWith('apps') ||
-            // 排除mock目录
-            relativeDir.includes('backend-mock')
-          ) {
-            continue;
-          }
-          include.push(`${join(dir, 'src', 'locales', 'langs')}/*.yaml`);
-        }
-
         return [
           viteVueI18nPlugin({
             compositionOnly: true,
             fullInstall: true,
-            include,
             runtimeOnly: true,
           }),
         ];

+ 0 - 132
packages/@core/forward/helpers/src/flatten-object.test.ts

@@ -1,132 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { flattenObject } from './flatten-object';
-
-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);
-  });
-});

+ 0 - 82
packages/@core/forward/helpers/src/flatten-object.ts

@@ -1,82 +0,0 @@
-import type { Flatten } from '@vben-core/typings';
-
-import { capitalizeFirstLetter } from '@vben-core/toolkit';
-
-/**
- * 将嵌套对象扁平化
- * @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>;
-}
-
-export { flattenObject };
-
-// 定义递归类型,用于推断扁平化后的对象类型
-// 限制递归深度的辅助类型
-// 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>;

+ 0 - 2
packages/@core/forward/helpers/src/index.ts

@@ -1,4 +1,2 @@
 export * from './find-menu-by-path';
-export * from './flatten-object';
 export * from './merge-route-modules';
-export * from './nested-object';

+ 0 - 115
packages/@core/forward/helpers/src/nested-object.test.ts

@@ -1,115 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { nestedObject } from './nested-object';
-
-describe('nestedObject', () => {
-  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(nestedObject(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(nestedObject(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(nestedObject(flatObject, 3)).toEqual(expectedNestedObject);
-  });
-
-  it('should handle empty object', () => {
-    const flatObject = {};
-
-    const expectedNestedObject = {};
-
-    expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
-  });
-
-  it('should handle single key object', () => {
-    const flatObject = {
-      singleKey: 1,
-    };
-
-    const expectedNestedObject = {
-      singleKey: 1,
-    };
-
-    expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
-  });
-
-  it('should handle complex keys', () => {
-    const flatObject = {
-      anotherComplexKeyWithParts: 2,
-      complexKeyWithMultipleParts: 1,
-    };
-
-    const expectedNestedObject = {
-      anotherComplexKeyWithParts: 2,
-      complexKeyWithMultipleParts: 1,
-    };
-
-    expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
-  });
-
-  it('should correctly nest an object based on the specified level', () => {
-    const obj = {
-      oneFiveSix: 'Value156',
-      oneTwoFour: 'Value124',
-      oneTwoThree: 'Value123',
-    };
-
-    const nested = nestedObject(obj, 2);
-
-    expect(nested).toEqual({
-      one: {
-        fiveSix: 'Value156',
-        twoFour: 'Value124',
-        twoThree: 'Value123',
-      },
-    });
-  });
-});

+ 0 - 70
packages/@core/forward/helpers/src/nested-object.ts

@@ -1,70 +0,0 @@
-import { toLowerCaseFirstLetter } from '@vben-core/toolkit';
-
-/**
- * 将扁平对象转换为嵌套对象。
- *
- * @template T - 输入对象值的类型
- * @param {Record<string, T>} obj - 要转换的扁平对象
- * @param {number} level - 嵌套的层级
- * @returns {T} 嵌套对象
- *
- * @example
- * 将扁平对象转换为嵌套对象,嵌套层级为 1
- * const flatObject = {
- *   'commonAppName': 1,
- *   'anotherKeyExample': 2,
- *   'someOtherKey': 3
- * };
- * const nestedObject = nestedObject(flatObject, 1);
- * console.log(nestedObject);
- * 输出:
- * {
- *   commonAppName: 1,
- *   anotherKeyExample: 2,
- *   someOtherKey: 3
- * }
- *
- * @example
- * 将扁平对象转换为嵌套对象,嵌套层级为 2
- * const flatObject = {
- *   'appCommonName': 1,
- *   'appAnotherKeyExample': 2,
- *   'appSomeOtherKey': 3
- * };
- * const nestedObject = nestedObject(flatObject, 2);
- * console.log(nestedObject);
- * 输出:
- * {
- *   app: {
- *     commonName: 1,
- *     anotherKeyExample: 2,
- *     someOtherKey: 3
- *   }
- * }
- */
-
-function nestedObject<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 { nestedObject };

+ 10 - 1
packages/@core/forward/preferences/src/config.ts

@@ -17,7 +17,6 @@ const defaultPreferences: Preferences = {
     layout: 'sidebar-nav',
     locale: 'zh-CN',
     name: 'Vben Admin Pro',
-    semiDarkMenu: true,
   },
   breadcrumb: {
     enable: true,
@@ -82,6 +81,7 @@ const defaultPreferences: Preferences = {
     colorWarning: 'hsl(42 84% 61%)',
     mode: 'dark',
     radius: '0.5',
+    semiDarkMenu: true,
   },
   transition: {
     enable: true,
@@ -89,6 +89,15 @@ const defaultPreferences: Preferences = {
     name: 'fade-slide',
     progress: true,
   },
+  widget: {
+    aiAssistant: true,
+    fullscreen: true,
+    globalSearch: true,
+    languageToggle: true,
+    notification: true,
+    sidebarToggle: true,
+    themeToggle: true,
+  },
 };
 
 export { defaultPreferences };

+ 7 - 5
packages/@core/forward/preferences/src/index.ts

@@ -1,3 +1,5 @@
+import type { DeepPartial } from '@vben-core/typings';
+
 import type { Preferences } from './types';
 
 import { preferencesManager } from './preferences';
@@ -5,10 +7,6 @@ import { preferencesManager } from './preferences';
 // 偏好设置(带有层级关系)
 const preferences: Preferences = preferencesManager.getPreferences();
 
-// 扁平化后的偏好设置
-// const flatPreferences: Flatten<Preferences> =
-//   preferencesManager.getFlatPreferences();
-
 // 更新偏好设置
 const updatePreferences =
   preferencesManager.updatePreferences.bind(preferencesManager);
@@ -20,9 +18,13 @@ const resetPreferences =
 const clearPreferencesCache =
   preferencesManager.clearCache.bind(preferencesManager);
 
+function defineOverridesPreferences(preferences: DeepPartial<Preferences>) {
+  return preferences;
+}
+
 export {
   clearPreferencesCache,
-  // flatPreferences,
+  defineOverridesPreferences,
   preferences,
   preferencesManager,
   resetPreferences,

+ 22 - 2
packages/@core/forward/preferences/src/types.ts

@@ -46,8 +46,6 @@ interface AppPreferences {
   locale: SupportedLanguagesType;
   /** 应用名 */
   name: string;
-  /** 是否开启半深色菜单(只在theme='light'时生效) */
-  semiDarkMenu: boolean;
 }
 
 interface BreadcrumbPreferences {
@@ -164,6 +162,8 @@ interface ThemePreferences {
   mode: ThemeModeType;
   /** 圆角 */
   radius: string;
+  /** 是否开启半深色菜单(只在theme='light'时生效) */
+  semiDarkMenu: boolean;
 }
 
 interface TransitionPreferences {
@@ -177,6 +177,23 @@ interface TransitionPreferences {
   progress: boolean;
 }
 
+interface WidgetPreferences {
+  /** 是否开启vben助手部件 */
+  aiAssistant: boolean;
+  /** 是否启用全屏部件 */
+  fullscreen: boolean;
+  /** 是否启用全局搜索部件 */
+  globalSearch: boolean;
+  /** 是否启用语言切换部件 */
+  languageToggle: boolean;
+  /** 是否显示通知部件 */
+  notification: boolean;
+  /** 是否显示侧边栏显示/隐藏部件 */
+  sidebarToggle: boolean;
+  /** 是否显示主题切换部件 */
+  themeToggle: boolean;
+}
+
 interface Preferences {
   /** 全局配置 */
   app: AppPreferences;
@@ -202,6 +219,8 @@ interface Preferences {
   theme: ThemePreferences;
   /** 动画配置 */
   transition: TransitionPreferences;
+  /** 功能配置 */
+  widget: WidgetPreferences;
 }
 
 type PreferencesKeys = keyof Preferences;
@@ -230,4 +249,5 @@ export type {
   ThemeModeType,
   ThemePreferences,
   TransitionPreferences,
+  WidgetPreferences,
 };

+ 10 - 1
packages/@core/locales/src/langs/en-US.json

@@ -147,7 +147,6 @@
     "general": "General",
     "language": "Language",
     "dynamic-title": "Dynamic Title",
-    "ai-assistant": "Ai Assistant",
     "sidebar": {
       "title": "Sidebar",
       "width": "Width",
@@ -248,6 +247,16 @@
       "search": "Global Search",
       "logout": "Logout",
       "preferences": "Preferences"
+    },
+    "widget": {
+      "title": "Widget",
+      "global-search": "Enable Global Search",
+      "fullscreen": "Enable Fullscreen",
+      "theme-toggle": "Enable Theme Toggle",
+      "language-toggle": "Enable Language Toggle",
+      "notification": "Enable Notification",
+      "sidebar-toggle": "Enable Sidebar Toggle",
+      "ai-assistant": "Enable AI Assistant"
     }
   }
 }

+ 10 - 1
packages/@core/locales/src/langs/zh-CN.json

@@ -146,7 +146,6 @@
     "general": "通用",
     "language": "语言",
     "dynamic-title": "动态标题",
-    "ai-assistant": "Ai 助手",
     "sidebar": {
       "title": "侧边栏",
       "width": "宽度",
@@ -247,6 +246,16 @@
       "search": "全局搜索",
       "logout": "退出登录",
       "preferences": "偏好设置"
+    },
+    "widget": {
+      "title": "小部件",
+      "global-search": "启用全局搜索",
+      "fullscreen": "启用全屏",
+      "theme-toggle": "启用主题切换",
+      "language-toggle": "启用语言切换",
+      "notification": "启用通知",
+      "sidebar-toggle": "启用侧边栏切换",
+      "ai-assistant": "启用 AI 助手"
     }
   }
 }

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

@@ -1,40 +0,0 @@
-// `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 };

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

@@ -1,5 +1,4 @@
 export type * from './app';
-export type * from './flatten';
 export type * from './helper';
 export type * from './menu-record';
 export type * from './tabs';

+ 11 - 6
packages/@core/ui-kit/layout-ui/src/vben-layout.ts

@@ -87,6 +87,11 @@ interface VbenLayoutProps {
    * @default 'fixed'
    */
   headerMode?: LayoutHeaderModeType;
+  /**
+   * 是否显示header切换侧边栏按钮
+   * @default
+   */
+  headerToggleSidebarButton?: boolean;
   /**
    * header是否显示
    * @default true
@@ -152,21 +157,21 @@ interface VbenLayoutProps {
    * @default 210
    */
   sidebarWidth?: number;
+  /**
+   * footer背景颜色
+   * @default #fff
+   */
+  tabbarBackgroundColor?: string;
   /**
    * tab是否可见
    * @default true
    */
   tabbarEnable?: boolean;
-  /**
-   * footer背景颜色
-   * @default #fff
-   */
-  tabsBackgroundColor?: string;
   /**
    * tab高度
    * @default 30
    */
-  tabsHeight?: number;
+  tabbarHeight?: number;
   /**
    * zIndex
    * @default 100

+ 5 - 4
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@@ -32,8 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
   headerHeight: 50,
   headerHeightOffset: 10,
   headerHidden: false,
-
   headerMode: 'fixed',
+  headerToggleSidebarButton: true,
   headerVisible: true,
   isMobile: false,
   layout: 'sidebar-nav',
@@ -45,7 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
   sidebarTheme: 'dark',
   sidebarWidth: 180,
   tabbarEnable: true,
-  tabsHeight: 36,
+  tabbarHeight: 36,
   zIndex: 200,
 });
 
@@ -122,7 +122,7 @@ const headerWrapperHeight = computed(() => {
     height += getHeaderHeight.value;
   }
   if (props.tabbarEnable) {
-    height += props.tabsHeight;
+    height += props.tabbarHeight;
   }
   return height;
 });
@@ -364,6 +364,7 @@ const maskStyle = computed((): CSSProperties => {
 
 const showHeaderToggleButton = computed(() => {
   return (
+    props.headerToggleSidebarButton &&
     isSideMode.value &&
     !isSidebarMixedNav.value &&
     !isMixedNav.value &&
@@ -528,7 +529,7 @@ function handleOpenMenu() {
 
         <LayoutTabbar
           v-if="tabbarEnable"
-          :height="tabsHeight"
+          :height="tabbarHeight"
           :style="tabbarStyle"
         >
           <slot name="tabbar"></slot>

+ 1 - 1
packages/business/access/src/code-authority.vue → packages/business/access/src/code-access.vue

@@ -13,7 +13,7 @@ interface Props {
 }
 
 defineOptions({
-  name: 'CodeAuthority',
+  name: 'CodeAccess',
 });
 
 withDefaults(defineProps<Props>(), {

+ 2 - 2
packages/business/access/src/index.ts

@@ -1,5 +1,5 @@
-export { default as CodeAuthority } from './code-authority.vue';
+export { default as CodeAccess } from './code-access.vue';
 export * from './generate-menu-and-routes';
-export { default as RoleAuthority } from './role-authority.vue';
+export { default as RoleAccess } from './role-access.vue';
 export type * from './types';
 export * from './use-access';

+ 1 - 1
packages/business/access/src/role-authority.vue → packages/business/access/src/role-access.vue

@@ -13,7 +13,7 @@ interface Props {
 }
 
 defineOptions({
-  name: 'RoleAuthority',
+  name: 'RoleAccess',
 });
 
 withDefaults(defineProps<Props>(), {

+ 6 - 5
packages/business/layouts/src/basic/header/header.vue

@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import { usePreferences } from '@vben-core/preferences';
+import { preferences, usePreferences } from '@vben-core/preferences';
 import { VbenFullScreen } from '@vben-core/shadcn-ui';
 import { useCoreAccessStore } from '@vben-core/stores';
 
@@ -33,14 +33,15 @@ const { globalSearchShortcutKey } = usePreferences();
   </div>
   <div class="flex h-full min-w-0 flex-shrink-0 items-center">
     <GlobalSearch
+      v-if="preferences.widget.globalSearch"
       :enable-shortcut-key="globalSearchShortcutKey"
       :menus="accessStore.accessMenus"
       class="mr-4"
     />
-    <ThemeToggle class="mr-2" />
-    <LanguageToggle class="mr-2" />
-    <VbenFullScreen class="mr-2" />
-    <slot name="notification"></slot>
+    <ThemeToggle v-if="preferences.widget.themeToggle" class="mr-2" />
+    <LanguageToggle v-if="preferences.widget.languageToggle" class="mr-2" />
+    <VbenFullScreen v-if="preferences.widget.fullscreen" class="mr-2" />
+    <slot v-if="preferences.widget.notification" name="notification"></slot>
     <slot name="user-dropdown"></slot>
   </div>
 </template>

+ 4 - 3
packages/business/layouts/src/basic/layout.vue

@@ -38,7 +38,7 @@ const headerMenuTheme = computed(() => {
 });
 
 const theme = computed(() => {
-  const dark = isDark.value || preferences.app.semiDarkMenu;
+  const dark = isDark.value || preferences.theme.semiDarkMenu;
   return dark ? 'dark' : 'light';
 });
 
@@ -122,6 +122,7 @@ function clearPreferencesAndLogout() {
     :footer-fixed="preferences.footer.fixed"
     :header-hidden="preferences.header.hidden"
     :header-mode="preferences.header.mode"
+    :header-toggle-sidebar-button="preferences.widget.sidebarToggle"
     :header-visible="preferences.header.enable"
     :is-mobile="preferences.app.isMobile"
     :layout="layout"
@@ -131,7 +132,7 @@ function clearPreferencesAndLogout() {
     :sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
     :sidebar-extra-collapse="preferences.sidebar.extraCollapse"
     :sidebar-hidden="preferences.sidebar.hidden"
-    :sidebar-semi-dark="preferences.app.semiDarkMenu"
+    :sidebar-semi-dark="preferences.theme.semiDarkMenu"
     :sidebar-theme="theme"
     :sidebar-width="preferences.sidebar.width"
     :tabbar-enable="preferences.tabbar.enable"
@@ -158,7 +159,7 @@ function clearPreferencesAndLogout() {
 
     <template #floating-groups>
       <CozeAssistant
-        v-if="preferences.app.aiAssistant"
+        v-if="preferences.widget.aiAssistant"
         :is-mobile="preferences.app.isMobile"
       />
       <VbenBackTop />

+ 0 - 4
packages/business/layouts/src/widgets/preferences/blocks/general/general.vue

@@ -13,7 +13,6 @@ defineOptions({
 
 const appLocale = defineModel<string>('appLocale');
 const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
-const appAiAssistant = defineModel<boolean>('appAiAssistant');
 
 const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
   label: item.text,
@@ -28,7 +27,4 @@ const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
   <SwitchItem v-model="appDynamicTitle">
     {{ $t('preferences.dynamic-title') }}
   </SwitchItem>
-  <SwitchItem v-model="appAiAssistant">
-    {{ $t('preferences.ai-assistant') }}
-  </SwitchItem>
 </template>

+ 1 - 0
packages/business/layouts/src/widgets/preferences/blocks/index.ts

@@ -10,6 +10,7 @@ export { default as Layout } from './layout/layout.vue';
 export { default as Navigation } from './layout/navigation.vue';
 export { default as Sidebar } from './layout/sidebar.vue';
 export { default as Tabbar } from './layout/tabbar.vue';
+export { default as Widget } from './layout/widget.vue';
 export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';
 export { default as SwitchItem } from './switch-item.vue';
 export { default as BuiltinTheme } from './theme/builtin.vue';

+ 0 - 21
packages/business/layouts/src/widgets/preferences/blocks/layout/interface-control.vue

@@ -1,21 +0,0 @@
-<script setup lang="ts">
-import { $t } from '@vben-core/locales';
-
-import SwitchItem from '../switch-item.vue';
-
-defineOptions({
-  name: 'PreferenceInterfaceControl',
-});
-
-const tabsVisible = defineModel<boolean>('tabsVisible');
-const logoVisible = defineModel<boolean>('logoVisible');
-</script>
-
-<template>
-  <SwitchItem v-model="tabsVisible">
-    {{ $t('preferences.tabbar.enable') }}
-  </SwitchItem>
-  <SwitchItem v-model="logoVisible">
-    {{ $t('preferences.logo-visible') }}
-  </SwitchItem>
-</template>

+ 41 - 0
packages/business/layouts/src/widgets/preferences/blocks/layout/widget.vue

@@ -0,0 +1,41 @@
+<script setup lang="ts">
+import { $t } from '@vben-core/locales';
+
+import SwitchItem from '../switch-item.vue';
+
+defineOptions({
+  name: 'PreferenceInterfaceControl',
+});
+
+const widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');
+const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
+const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
+const widgetNotification = defineModel<boolean>('widgetNotification');
+const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
+const widgetAiAssistant = defineModel<boolean>('widgetAiAssistant');
+const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
+</script>
+
+<template>
+  <SwitchItem v-model="widgetGlobalSearch">
+    {{ $t('preferences.widget.global-search') }}
+  </SwitchItem>
+  <SwitchItem v-model="widgetThemeToggle">
+    {{ $t('preferences.widget.theme-toggle') }}
+  </SwitchItem>
+  <SwitchItem v-model="widgetLanguageToggle">
+    {{ $t('preferences.widget.language-toggle') }}
+  </SwitchItem>
+  <SwitchItem v-model="widgetFullscreen">
+    {{ $t('preferences.widget.fullscreen') }}
+  </SwitchItem>
+  <SwitchItem v-model="widgetNotification">
+    {{ $t('preferences.widget.notification') }}
+  </SwitchItem>
+  <SwitchItem v-model="widgetAiAssistant">
+    {{ $t('preferences.widget.ai-assistant') }}
+  </SwitchItem>
+  <SwitchItem v-model="widgetSidebarToggle">
+    {{ $t('preferences.widget.sidebar-toggle') }}
+  </SwitchItem>
+</template>

+ 20 - 2
packages/business/layouts/src/widgets/preferences/preferences-sheet.vue

@@ -51,6 +51,7 @@ import {
   Sidebar,
   Tabbar,
   Theme,
+  Widget,
 } from './blocks';
 import IconSetting from './icons/setting.vue';
 import { useOpenPreferences } from './use-open-preferences';
@@ -59,7 +60,6 @@ const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
 const { toast } = useToast();
 const appLocale = defineModel<SupportedLanguagesType>('appLocale');
 const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
-const appAiAssistant = defineModel<boolean>('appAiAssistant');
 const appLayout = defineModel<LayoutType>('appLayout');
 const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
 const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
@@ -129,6 +129,14 @@ const shortcutKeysGlobalPreferences = defineModel<boolean>(
   'shortcutKeysGlobalPreferences',
 );
 
+const widgetGlobalSearch = defineModel<boolean>('widgetGlobalSearch');
+const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
+const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
+const widgetNotification = defineModel<boolean>('widgetNotification');
+const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
+const widgetAiAssistant = defineModel<boolean>('widgetAiAssistant');
+const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
+
 const {
   diffPreference,
   isDark,
@@ -245,7 +253,6 @@ async function handleReset() {
           <template #general>
             <Block :title="$t('preferences.general')">
               <General
-                v-model:app-ai-assistant="appAiAssistant"
                 v-model:app-dynamic-title="appDynamicTitle"
                 v-model:app-locale="appLocale"
               />
@@ -346,6 +353,17 @@ async function handleReset() {
                 v-model:tabbar-show-icon="tabbarShowIcon"
               />
             </Block>
+            <Block :title="$t('preferences.widget.title')">
+              <Widget
+                v-model:widget-ai-assistant="widgetAiAssistant"
+                v-model:widget-fullscreen="widgetFullscreen"
+                v-model:widget-global-search="widgetGlobalSearch"
+                v-model:widget-language-toggle="widgetLanguageToggle"
+                v-model:widget-notification="widgetNotification"
+                v-model:widget-sidebar-toggle="widgetSidebarToggle"
+                v-model:widget-theme-toggle="widgetThemeToggle"
+              />
+            </Block>
             <Block :title="$t('preferences.footer.title')">
               <Footer
                 v-model:footer-enable="footerEnable"