Browse Source

feat: new interface pendant can be configured to display hidden

vince 8 months ago
parent
commit
a765d3bbc0
36 changed files with 256 additions and 612 deletions
  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"