DESKTOP-USV654P\pc 1 anno fa
parent
commit
fe239ffbfc
100 ha cambiato i file con 952 aggiunte e 382 eliminazioni
  1. 17 0
      .github/workflows/deploy.yml
  2. 19 0
      .github/workflows/rerun.yml
  3. 15 0
      apps/backend-mock/api/system/dept/.post.ts
  4. 15 0
      apps/backend-mock/api/system/dept/[id].delete.ts
  5. 15 0
      apps/backend-mock/api/system/dept/[id].put.ts
  6. 61 0
      apps/backend-mock/api/system/dept/list.ts
  7. 13 1
      apps/backend-mock/middleware/1.api.ts
  8. 2 1
      apps/backend-mock/nitro.config.ts
  9. 1 3
      apps/backend-mock/utils/mock-data.ts
  10. 1 1
      apps/web-antd/package.json
  11. 7 1
      apps/web-antd/src/adapter/component/index.ts
  12. 15 16
      apps/web-antd/src/api/request.ts
  13. 23 0
      apps/web-antd/src/bootstrap.ts
  14. 9 1
      apps/web-antd/src/router/routes/core.ts
  15. 1 3
      apps/web-antd/src/router/routes/modules/dashboard.ts
  16. 0 2
      apps/web-antd/src/router/routes/modules/demos.ts
  17. 12 12
      apps/web-antd/src/router/routes/modules/vben.ts
  18. 1 1
      apps/web-baicai/package.json
  19. 7 1
      apps/web-baicai/src/adapter/component/index.ts
  20. 0 2
      apps/web-baicai/src/adapter/form.ts
  21. 177 19
      apps/web-baicai/src/adapter/vxe-table.ts
  22. 5 2
      apps/web-baicai/src/api/core/auth.ts
  23. 24 21
      apps/web-baicai/src/api/request.ts
  24. 23 0
      apps/web-baicai/src/bootstrap.ts
  25. 8 0
      apps/web-baicai/src/router/routes/core.ts
  26. 1 1
      apps/web-ele/package.json
  27. 15 15
      apps/web-ele/src/api/request.ts
  28. 22 0
      apps/web-ele/src/bootstrap.ts
  29. 9 1
      apps/web-ele/src/router/routes/core.ts
  30. 1 3
      apps/web-ele/src/router/routes/modules/dashboard.ts
  31. 0 2
      apps/web-ele/src/router/routes/modules/demos.ts
  32. 12 12
      apps/web-ele/src/router/routes/modules/vben.ts
  33. 1 1
      apps/web-naive/package.json
  34. 15 15
      apps/web-naive/src/api/request.ts
  35. 25 0
      apps/web-naive/src/bootstrap.ts
  36. 9 1
      apps/web-naive/src/router/routes/core.ts
  37. 1 3
      apps/web-naive/src/router/routes/modules/dashboard.ts
  38. 0 2
      apps/web-naive/src/router/routes/modules/demos.ts
  39. 12 12
      apps/web-naive/src/router/routes/modules/vben.ts
  40. 17 1
      apps/web-naive/src/views/demos/form/basic.vue
  41. 3 1
      docs/.vitepress/components/preview-group.vue
  42. 1 1
      docs/package.json
  43. 1 1
      docs/src/commercial/community.md
  44. 5 8
      docs/src/components/common-ui/vben-api-component.md
  45. 10 3
      docs/src/components/common-ui/vben-count-to-animator.md
  46. 6 4
      docs/src/components/common-ui/vben-drawer.md
  47. 16 5
      docs/src/components/common-ui/vben-form.md
  48. 19 9
      docs/src/components/common-ui/vben-modal.md
  49. 1 2
      docs/src/demos/vben-drawer/dynamic/index.vue
  50. 6 5
      docs/src/demos/vben-drawer/shared-data/index.vue
  51. 1 2
      docs/src/demos/vben-modal/dynamic/index.vue
  52. 6 5
      docs/src/demos/vben-modal/shared-data/index.vue
  53. 0 3
      docs/src/en/guide/essentials/route.md
  54. 8 6
      docs/src/en/guide/in-depth/theme.md
  55. 11 7
      docs/src/guide/essentials/route.md
  56. 11 13
      docs/src/guide/essentials/server.md
  57. 3 1
      docs/src/guide/essentials/settings.md
  58. 1 1
      docs/src/guide/in-depth/access.md
  59. 8 6
      docs/src/guide/in-depth/theme.md
  60. 1 1
      internal/lint-configs/commitlint-config/package.json
  61. 1 1
      internal/lint-configs/eslint-config/src/configs/javascript.ts
  62. 1 1
      internal/lint-configs/stylelint-config/package.json
  63. 1 1
      internal/node-utils/package.json
  64. 1 1
      internal/tailwind-config/package.json
  65. 1 1
      internal/tsconfig/package.json
  66. 1 1
      internal/vite-config/package.json
  67. 2 2
      package.json
  68. 1 1
      packages/@core/base/design/package.json
  69. 4 0
      packages/@core/base/design/src/css/ui.css
  70. 5 1
      packages/@core/base/design/src/design-tokens/dark.css
  71. 6 3
      packages/@core/base/design/src/design-tokens/default.css
  72. 1 1
      packages/@core/base/icons/package.json
  73. 3 0
      packages/@core/base/icons/src/lucide.ts
  74. 1 1
      packages/@core/base/shared/package.json
  75. 1 0
      packages/@core/base/shared/src/utils/download.ts
  76. 1 1
      packages/@core/base/typings/package.json
  77. 3 0
      packages/@core/base/typings/src/helper.d.ts
  78. 4 0
      packages/@core/base/typings/src/vue-router.d.ts
  79. 1 1
      packages/@core/composables/package.json
  80. 2 0
      packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
  81. 1 1
      packages/@core/preferences/package.json
  82. 2 0
      packages/@core/preferences/src/config.ts
  83. 4 0
      packages/@core/preferences/src/types.ts
  84. 1 1
      packages/@core/ui-kit/form-ui/package.json
  85. 3 52
      packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  86. 57 7
      packages/@core/ui-kit/form-ui/src/form-api.ts
  87. 54 44
      packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
  88. 12 3
      packages/@core/ui-kit/form-ui/src/form-render/form-label.vue
  89. 2 0
      packages/@core/ui-kit/form-ui/src/form-render/form.vue
  90. 17 7
      packages/@core/ui-kit/form-ui/src/types.ts
  91. 4 2
      packages/@core/ui-kit/form-ui/src/use-form-context.ts
  92. 4 2
      packages/@core/ui-kit/form-ui/src/vben-use-form.vue
  93. 1 1
      packages/@core/ui-kit/layout-ui/package.json
  94. 1 1
      packages/@core/ui-kit/menu-ui/package.json
  95. 2 2
      packages/@core/ui-kit/menu-ui/src/components/menu.vue
  96. 2 0
      packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
  97. 5 1
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
  98. 2 0
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
  99. 1 1
      packages/@core/ui-kit/popup-ui/src/drawer/index.ts
  100. 7 0
      packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts

+ 17 - 0
.github/workflows/deploy.yml

@@ -153,3 +153,20 @@ jobs:
           username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
           password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }}
           local-dir: ./apps/web-naive/dist/
+
+  rerun-on-failure:
+    name: Rerun on failure
+    needs:
+      - deploy-playground-ftp
+      - deploy-docs-ftp
+      - deploy-antd-ftp
+      - deploy-ele-ftp
+      - deploy-naive-ftp
+    if: failure() && fromJSON(github.run_attempt) < 10
+    runs-on: ubuntu-latest
+    steps:
+      - name: Retry ${{ fromJSON(github.run_attempt) }} of 10
+        env:
+          GH_REPO: ${{ github.repository }}
+          GH_TOKEN: ${{ github.token }}
+        run: gh workflow run rerun.yml -F run_id=${{ github.run_id }}

+ 19 - 0
.github/workflows/rerun.yml

@@ -0,0 +1,19 @@
+name: Rerun workflow
+
+on:
+  workflow_dispatch:
+    inputs:
+      run_id:
+        description: The workflow id to relanch
+        required: true
+jobs:
+  rerun:
+    runs-on: ubuntu-latest
+    steps:
+      - name: rerun ${{ inputs.run_id }}
+        env:
+          GH_REPO: ${{ github.repository }}
+          GH_TOKEN: ${{ github.token }}
+        run: |
+          gh run watch ${{ inputs.run_id }} > /dev/null 2>&1
+          gh run rerun ${{ inputs.run_id }} --failed

+ 15 - 0
apps/backend-mock/api/system/dept/.post.ts

@@ -0,0 +1,15 @@
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import {
+  sleep,
+  unAuthorizedResponse,
+  useResponseSuccess,
+} from '~/utils/response';
+
+export default eventHandler(async (event) => {
+  const userinfo = verifyAccessToken(event);
+  if (!userinfo) {
+    return unAuthorizedResponse(event);
+  }
+  await sleep(600);
+  return useResponseSuccess(null);
+});

+ 15 - 0
apps/backend-mock/api/system/dept/[id].delete.ts

@@ -0,0 +1,15 @@
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import {
+  sleep,
+  unAuthorizedResponse,
+  useResponseSuccess,
+} from '~/utils/response';
+
+export default eventHandler(async (event) => {
+  const userinfo = verifyAccessToken(event);
+  if (!userinfo) {
+    return unAuthorizedResponse(event);
+  }
+  await sleep(1000);
+  return useResponseSuccess(null);
+});

+ 15 - 0
apps/backend-mock/api/system/dept/[id].put.ts

@@ -0,0 +1,15 @@
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import {
+  sleep,
+  unAuthorizedResponse,
+  useResponseSuccess,
+} from '~/utils/response';
+
+export default eventHandler(async (event) => {
+  const userinfo = verifyAccessToken(event);
+  if (!userinfo) {
+    return unAuthorizedResponse(event);
+  }
+  await sleep(2000);
+  return useResponseSuccess(null);
+});

+ 61 - 0
apps/backend-mock/api/system/dept/list.ts

@@ -0,0 +1,61 @@
+import { faker } from '@faker-js/faker';
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
+
+const formatterCN = new Intl.DateTimeFormat('zh-CN', {
+  timeZone: 'Asia/Shanghai',
+  year: 'numeric',
+  month: '2-digit',
+  day: '2-digit',
+  hour: '2-digit',
+  minute: '2-digit',
+  second: '2-digit',
+});
+
+function generateMockDataList(count: number) {
+  const dataList = [];
+
+  for (let i = 0; i < count; i++) {
+    const dataItem: Record<string, any> = {
+      id: faker.string.uuid(),
+      pid: 0,
+      name: faker.commerce.department(),
+      status: faker.helpers.arrayElement([0, 1]),
+      createTime: formatterCN.format(
+        faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
+      ),
+      remark: faker.lorem.sentence(),
+    };
+    if (faker.datatype.boolean()) {
+      dataItem.children = Array.from(
+        { length: faker.number.int({ min: 1, max: 5 }) },
+        () => ({
+          id: faker.string.uuid(),
+          pid: dataItem.id,
+          name: faker.commerce.department(),
+          status: faker.helpers.arrayElement([0, 1]),
+          createTime: formatterCN.format(
+            faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
+          ),
+          remark: faker.lorem.sentence(),
+        }),
+      );
+    }
+    dataList.push(dataItem);
+  }
+
+  return dataList;
+}
+
+const mockData = generateMockDataList(10);
+
+export default eventHandler(async (event) => {
+  const userinfo = verifyAccessToken(event);
+  if (!userinfo) {
+    return unAuthorizedResponse(event);
+  }
+
+  const listData = structuredClone(mockData);
+
+  return useResponseSuccess(listData);
+});

+ 13 - 1
apps/backend-mock/middleware/1.api.ts

@@ -1,7 +1,19 @@
-export default defineEventHandler((event) => {
+import { forbiddenResponse, sleep } from '~/utils/response';
+
+export default defineEventHandler(async (event) => {
+  event.node.res.setHeader(
+    'Access-Control-Allow-Origin',
+    event.headers.get('Origin') ?? '*',
+  );
   if (event.method === 'OPTIONS') {
     event.node.res.statusCode = 204;
     event.node.res.statusMessage = 'No Content.';
     return 'OK';
+  } else if (
+    ['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
+    event.path.startsWith('/api/system/')
+  ) {
+    await sleep(Math.floor(Math.random() * 1000));
+    return forbiddenResponse(event, '演示环境,禁止修改');
   }
 });

+ 2 - 1
apps/backend-mock/nitro.config.ts

@@ -9,7 +9,8 @@ export default defineNitroConfig({
       cors: true,
       headers: {
         'Access-Control-Allow-Credentials': 'true',
-        'Access-Control-Allow-Headers': '*',
+        'Access-Control-Allow-Headers':
+          'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
         'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
         'Access-Control-Allow-Origin': '*',
         'Access-Control-Expose-Headers': '*',

+ 1 - 3
apps/backend-mock/utils/mock-data.ts

@@ -53,13 +53,12 @@ export const MOCK_CODES = [
 
 const dashboardMenus = [
   {
-    component: 'BasicLayout',
     meta: {
       order: -1,
       title: 'page.dashboard.title',
     },
     name: 'Dashboard',
-    path: '/',
+    path: '/dashboard',
     redirect: '/analytics',
     children: [
       {
@@ -116,7 +115,6 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
 
   return [
     {
-      component: 'BasicLayout',
       meta: {
         icon: 'ic:baseline-view-in-ar',
         keepAlive: true,

+ 1 - 1
apps/web-antd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/web-antd",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://vben.pro",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 7 - 1
apps/web-antd/src/adapter/component/index.ts

@@ -125,7 +125,13 @@ async function initComponentAdapter() {
     IconPicker: (props, { attrs, slots }) => {
       return h(
         IconPicker,
-        { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
+        {
+          iconSlot: 'addonAfter',
+          inputComponent: Input,
+          modelValueProp: 'value',
+          ...props,
+          ...attrs,
+        },
         slots,
       );
     },

+ 15 - 16
apps/web-antd/src/api/request.ts

@@ -1,12 +1,13 @@
 /**
  * 该文件可自行根据业务逻辑进行调整
  */
-import type { HttpResponse } from '@vben/request';
+import type { RequestClientOptions } from '@vben/request';
 
 import { useAppConfig } from '@vben/hooks';
 import { preferences } from '@vben/preferences';
 import {
   authenticateResponseInterceptor,
+  defaultResponseInterceptor,
   errorMessageResponseInterceptor,
   RequestClient,
 } from '@vben/request';
@@ -20,8 +21,9 @@ import { refreshTokenApi } from './core';
 
 const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
 
-function createRequestClient(baseURL: string) {
+function createRequestClient(baseURL: string, options?: RequestClientOptions) {
   const client = new RequestClient({
+    ...options,
     baseURL,
   });
 
@@ -69,19 +71,14 @@ function createRequestClient(baseURL: string) {
     },
   });
 
-  // response数据解构
-  client.addResponseInterceptor<HttpResponse>({
-    fulfilled: (response) => {
-      const { data: responseData, status } = response;
-
-      const { code, data } = responseData;
-      if (status >= 200 && status < 400 && code === 0) {
-        return data;
-      }
-
-      throw Object.assign({}, response, { response });
-    },
-  });
+  // 处理返回的响应数据格式
+  client.addResponseInterceptor(
+    defaultResponseInterceptor({
+      codeField: 'code',
+      dataField: 'data',
+      successCode: 0,
+    }),
+  );
 
   // token过期的处理
   client.addResponseInterceptor(
@@ -109,6 +106,8 @@ function createRequestClient(baseURL: string) {
   return client;
 }
 
-export const requestClient = createRequestClient(apiURL);
+export const requestClient = createRequestClient(apiURL, {
+  responseReturn: 'data',
+});
 
 export const baseRequestClient = new RequestClient({ baseURL: apiURL });

+ 23 - 0
apps/web-antd/src/bootstrap.ts

@@ -1,6 +1,8 @@
 import { createApp, watchEffect } from 'vue';
 
 import { registerAccessDirective } from '@vben/access';
+import { initTippy, registerLoadingDirective } from '@vben/common-ui';
+import { MotionPlugin } from '@vben/plugins/motion';
 import { preferences } from '@vben/preferences';
 import { initStores } from '@vben/stores';
 import '@vben/styles';
@@ -18,8 +20,23 @@ async function bootstrap(namespace: string) {
   // 初始化组件适配器
   await initComponentAdapter();
 
+  // // 设置弹窗的默认配置
+  // setDefaultModalProps({
+  //   fullscreenButton: false,
+  // });
+  // // 设置抽屉的默认配置
+  // setDefaultDrawerProps({
+  //   zIndex: 1020,
+  // });
+
   const app = createApp(App);
 
+  // 注册v-loading指令
+  registerLoadingDirective(app, {
+    loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+    spinning: 'spinning',
+  });
+
   // 国际化 i18n 配置
   await setupI18n(app);
 
@@ -29,9 +46,15 @@ async function bootstrap(namespace: string) {
   // 安装权限指令
   registerAccessDirective(app);
 
+  // 初始化 tippy
+  initTippy(app);
+
   // 配置路由及路由守卫
   app.use(router);
 
+  // 配置Motion插件
+  app.use(MotionPlugin);
+
   // 动态更新标题
   watchEffect(() => {
     if (preferences.app.dynamicTitle) {

+ 9 - 1
apps/web-antd/src/router/routes/core.ts

@@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router';
 
 import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 
-import { AuthPageLayout } from '#/layouts';
+import { AuthPageLayout, BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 import Login from '#/views/_core/authentication/login.vue';
 
@@ -21,13 +21,21 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
 
 /** 基本路由,这些路由是必须存在的 */
 const coreRoutes: RouteRecordRaw[] = [
+  /**
+   * 根路由
+   * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+   * 此路由必须存在,且不应修改
+   */
   {
+    component: BasicLayout,
     meta: {
+      hideInBreadcrumb: true,
       title: 'Root',
     },
     name: 'Root',
     path: '/',
     redirect: DEFAULT_HOME_PATH,
+    children: [],
   },
   {
     component: AuthPageLayout,

+ 1 - 3
apps/web-antd/src/router/routes/modules/dashboard.ts

@@ -1,18 +1,16 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'lucide:layout-dashboard',
       order: -1,
       title: $t('page.dashboard.title'),
     },
     name: 'Dashboard',
-    path: '/',
+    path: '/dashboard',
     children: [
       {
         name: 'Analytics',

+ 0 - 2
apps/web-antd/src/router/routes/modules/demos.ts

@@ -1,11 +1,9 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'ic:baseline-view-in-ar',
       keepAlive: true,

+ 12 - 12
apps/web-antd/src/router/routes/modules/vben.ts

@@ -8,30 +8,20 @@ import {
   VBEN_NAIVE_PREVIEW_URL,
 } from '@vben/constants';
 
-import { BasicLayout, IFrameView } from '#/layouts';
+import { IFrameView } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       badgeType: 'dot',
       icon: VBEN_LOGO_URL,
-      order: 9999,
+      order: 9998,
       title: $t('demos.vben.title'),
     },
     name: 'VbenProject',
     path: '/vben-admin',
     children: [
-      {
-        name: 'VbenAbout',
-        path: '/vben-admin/about',
-        component: () => import('#/views/_core/about/index.vue'),
-        meta: {
-          icon: 'lucide:copyright',
-          title: $t('demos.vben.about'),
-        },
-      },
       {
         name: 'VbenDocument',
         path: '/vben-admin/document',
@@ -76,6 +66,16 @@ const routes: RouteRecordRaw[] = [
       },
     ],
   },
+  {
+    name: 'VbenAbout',
+    path: '/vben-admin/about',
+    component: () => import('#/views/_core/about/index.vue'),
+    meta: {
+      icon: 'lucide:copyright',
+      title: $t('demos.vben.about'),
+      order: 9999,
+    },
+  },
 ];
 
 export default routes;

+ 1 - 1
apps/web-baicai/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/baicai",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://vben.pro",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 7 - 1
apps/web-baicai/src/adapter/component/index.ts

@@ -131,7 +131,13 @@ async function initComponentAdapter() {
     IconPicker: (props, { attrs, slots }) => {
       return h(
         IconPicker,
-        { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
+        {
+          iconSlot: 'addonAfter',
+          inputComponent: Input,
+          modelValueProp: 'value',
+          ...props,
+          ...attrs,
+        },
         slots,
       );
     },

+ 0 - 2
apps/web-baicai/src/adapter/form.ts

@@ -12,7 +12,6 @@ setupVbenForm<ComponentType>({
   config: {
     // ant design vue组件库默认都是 v-model:value
     baseModelPropName: 'value',
-
     // 一些组件是 v-model:checked 或者 v-model:fileList
     modelPropNameMap: {
       Checkbox: 'checked',
@@ -42,6 +41,5 @@ setupVbenForm<ComponentType>({
 const useVbenForm = useForm<ComponentType>;
 
 export { useVbenForm, z };
-
 export type VbenFormSchema = FormSchema<ComponentType>;
 export type { VbenFormProps };

+ 177 - 19
apps/web-baicai/src/adapter/vxe-table.ts

@@ -1,10 +1,17 @@
+import type { Recordable } from '@vben/types';
+
 import { h } from 'vue';
 
+import { IconifyIcon } from '@vben/icons';
+import { $te } from '@vben/locales';
 import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+import { isFunction, isString } from '@vben/utils';
+
+import { objectOmit } from '@vueuse/core';
+import { Button, Image, Popconfirm, Tag } from 'ant-design-vue';
 
-import { Button, Image } from 'ant-design-vue';
+import { $t } from '#/locales';
 
-// import { componentMap } from '#/components/view/component-map';
 import { useVbenForm } from './form';
 
 setupVbenVxeTable({
@@ -38,9 +45,18 @@ setupVbenVxeTable({
       },
     });
 
+    /**
+     * 解决vxeTable在热更新时可能会出错的问题
+     */
+    vxeUI.renderer.forEach((_item, key) => {
+      if (key.startsWith('Cell')) {
+        vxeUI.renderer.delete(key);
+      }
+    });
+
     // 表格配置项可以用 cellRender: { name: 'CellImage' },
     vxeUI.renderer.add('CellImage', {
-      renderDefault(_renderOpts, params) {
+      renderTableDefault(_renderOpts, params) {
         const { column, row } = params;
         return h(Image, { src: row[column.field] });
       },
@@ -48,7 +64,7 @@ setupVbenVxeTable({
 
     // 表格配置项可以用 cellRender: { name: 'CellLink' },
     vxeUI.renderer.add('CellLink', {
-      renderDefault(renderOpts) {
+      renderTableDefault(renderOpts) {
         const { props } = renderOpts;
         return h(
           Button,
@@ -57,20 +73,156 @@ setupVbenVxeTable({
         );
       },
     });
-    // 注册@/components/view/下面所有列渲染器
-    // componentMap.forEach((comp, key) => {
-    //   // 创建一个渲染器
-    //   vxeUI.renderer.add(key, {
-    //     // 默认显示模板
-    //     renderDefault(renderOpts, params) {
-    //       const { row, column } = params;
-    //       return h(comp, {
-    //         ...renderOpts.props,
-    //         value: row[column.field],
-    //       });
-    //     },
-    //   });
-    // });
+
+    // 单元格渲染: Tag
+    vxeUI.renderer.add('CellTag', {
+      renderTableDefault({ options, props }, { column, row }) {
+        const value = row[column.field];
+        const tagOptions = options || [
+          { color: 'success', label: $t('common.enabled'), value: 1 },
+          { color: 'error', label: $t('common.disabled'), value: 0 },
+        ];
+        const tagItem = tagOptions.find((item) => item.value === value);
+        return h(
+          Tag,
+          {
+            ...props,
+            ...objectOmit(tagItem, ['label']),
+          },
+          { default: () => tagItem?.label ?? value },
+        );
+      },
+    });
+
+    /**
+     * 注册表格的操作按钮渲染器
+     */
+    vxeUI.renderer.add('CellOperation', {
+      renderTableDefault({ attrs, options, props }, { column, row }) {
+        const defaultProps = { size: 'small', type: 'link', ...props };
+        let align = 'end';
+        switch (column.align) {
+          case 'center': {
+            align = 'center';
+            break;
+          }
+          case 'left': {
+            align = 'start';
+            break;
+          }
+          default: {
+            align = 'end';
+            break;
+          }
+        }
+        const presets: Recordable<Recordable<any>> = {
+          delete: {
+            danger: true,
+            text: $t('common.delete'),
+          },
+          edit: {
+            text: $t('common.edit'),
+          },
+        };
+        const operations: Array<Recordable<any>> = (
+          options || ['edit', 'delete']
+        )
+          .map((opt) => {
+            if (isString(opt)) {
+              return presets[opt]
+                ? { code: opt, ...presets[opt], ...defaultProps }
+                : {
+                    code: opt,
+                    text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
+                    ...defaultProps,
+                  };
+            } else {
+              return { ...defaultProps, ...presets[opt.code], ...opt };
+            }
+          })
+          .map((opt) => {
+            const optBtn: Recordable<any> = {};
+            Object.keys(opt).forEach((key) => {
+              optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key];
+            });
+            return optBtn;
+          })
+          .filter((opt) => opt.show !== false);
+
+        function renderBtn(opt: Recordable<any>, listen = true) {
+          return h(
+            Button,
+            {
+              ...props,
+              ...opt,
+              icon: undefined,
+              onClick: listen
+                ? () =>
+                    attrs?.onClick?.({
+                      code: opt.code,
+                      row,
+                    })
+                : undefined,
+            },
+            {
+              default: () => {
+                const content = [];
+                if (opt.icon) {
+                  content.push(
+                    h(IconifyIcon, { class: 'size-5', icon: opt.icon }),
+                  );
+                }
+                content.push(opt.text);
+                return content;
+              },
+            },
+          );
+        }
+
+        function renderConfirm(opt: Recordable<any>) {
+          return h(
+            Popconfirm,
+            {
+              placement: 'topLeft',
+              title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
+              ...props,
+              ...opt,
+              icon: undefined,
+              onConfirm: () => {
+                attrs?.onClick?.({
+                  code: opt.code,
+                  row,
+                });
+              },
+            },
+            {
+              default: () => renderBtn({ ...opt }, false),
+              description: () =>
+                h(
+                  'div',
+                  { class: 'truncate' },
+                  $t('ui.actionMessage.deleteConfirm', [
+                    row[attrs?.nameField || 'name'],
+                  ]),
+                ),
+            },
+          );
+        }
+
+        const btns = operations.map((opt) =>
+          opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt),
+        );
+        return h(
+          'div',
+          {
+            class: 'flex table-operations',
+            style: { justifyContent: align },
+          },
+          btns,
+        );
+      },
+    });
+
     // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
     // vxeUI.formats.add
   },
@@ -78,5 +230,11 @@ setupVbenVxeTable({
 });
 
 export { useVbenVxeGrid };
-
+export type OnActionClickParams<T = Recordable<any>> = {
+  code: string;
+  row: T;
+};
+export type OnActionClickFn<T = Recordable<any>> = (
+  params: OnActionClickParams<T>,
+) => void;
 export type * from '@vben/plugins/vxe-table';

+ 5 - 2
apps/web-baicai/src/api/core/auth.ts

@@ -27,7 +27,9 @@ export async function loginApi(data: AuthApi.LoginParams) {
     password: encrypt(data.password),
     username: data.username,
   };
-  return requestClient.post<AuthApi.LoginResult>('/security/login', postData);
+  return requestClient.post<AuthApi.LoginResult>('/security/login', postData, {
+    withCredentials: true,
+  });
 }
 
 /**
@@ -36,6 +38,7 @@ export async function loginApi(data: AuthApi.LoginParams) {
 export async function refreshTokenApi() {
   return baseRequestClient.post<AuthApi.RefreshTokenResult>(
     '/security/refresh-token',
+    null,
     {
       withCredentials: true,
     },
@@ -46,7 +49,7 @@ export async function refreshTokenApi() {
  * 退出登录
  */
 export async function logoutApi() {
-  return baseRequestClient.post('/security/logout', {
+  return baseRequestClient.post('/security/logout', null, {
     withCredentials: true,
   });
 }

+ 24 - 21
apps/web-baicai/src/api/request.ts

@@ -1,12 +1,13 @@
 /**
  * 该文件可自行根据业务逻辑进行调整
  */
-import type { HttpResponse } from '@vben/request';
+import type { RequestClientOptions } from '@vben/request';
 
 import { useAppConfig } from '@vben/hooks';
 import { preferences } from '@vben/preferences';
 import {
   authenticateResponseInterceptor,
+  defaultResponseInterceptor,
   errorMessageResponseInterceptor,
   RequestClient,
 } from '@vben/request';
@@ -20,8 +21,9 @@ import { refreshTokenApi } from './core';
 
 const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
 
-function createRequestClient(baseURL: string) {
+function createRequestClient(baseURL: string, options?: RequestClientOptions) {
   const client = new RequestClient({
+    ...options,
     baseURL,
   });
 
@@ -69,22 +71,14 @@ function createRequestClient(baseURL: string) {
     },
   });
 
-  // response数据解构
-  client.addResponseInterceptor<HttpResponse>({
-    fulfilled: (response) => {
-      const { data: responseData, status } = response;
-
-      const { code, data } = responseData;
-
-      if (status >= 200 && status < 400 && code === 200) {
-        return data;
-      } else if (status >= 200 && status < 400 && code !== 200) {
-        return Promise.reject(response);
-      }
-
-      throw Object.assign({}, response, { response });
-    },
-  });
+  // 处理返回的响应数据格式
+  client.addResponseInterceptor(
+    defaultResponseInterceptor({
+      codeField: 'code',
+      dataField: 'data',
+      successCode: 200,
+    }),
+  );
 
   // token过期的处理
   client.addResponseInterceptor(
@@ -99,9 +93,10 @@ function createRequestClient(baseURL: string) {
 
   // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
   client.addResponseInterceptor(
-    errorMessageResponseInterceptor((msg: string, _error) => {
+    errorMessageResponseInterceptor((msg: string, error) => {
       // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
-      const responseData = _error?.data ?? {};
+      // 当前mock接口返回的错误字段是 error 或者 message
+      const responseData = error?.response?.data ?? {};
       if (responseData?.code === 401) {
         message.error('登录已过期,请重新登录');
         doReAuthenticate();
@@ -116,6 +111,14 @@ function createRequestClient(baseURL: string) {
   return client;
 }
 
-export const requestClient = createRequestClient(apiURL);
+export const requestClient = createRequestClient(apiURL, {
+  responseReturn: 'data',
+});
 
 export const baseRequestClient = new RequestClient({ baseURL: apiURL });
+
+export interface PageFetchParams {
+  [key: string]: any;
+  pageNo?: number;
+  pageSize?: number;
+}

+ 23 - 0
apps/web-baicai/src/bootstrap.ts

@@ -1,6 +1,8 @@
 import { createApp, watchEffect } from 'vue';
 
 import { registerAccessDirective } from '@vben/access';
+import { initTippy, registerLoadingDirective } from '@vben/common-ui';
+import { MotionPlugin } from '@vben/plugins/motion';
 import { preferences } from '@vben/preferences';
 import { initStores } from '@vben/stores';
 import '@vben/styles';
@@ -19,8 +21,23 @@ async function bootstrap(namespace: string) {
   // 初始化组件适配器
   await initComponentAdapter();
 
+  // // 设置弹窗的默认配置
+  // setDefaultModalProps({
+  //   fullscreenButton: false,
+  // });
+  // // 设置抽屉的默认配置
+  // setDefaultDrawerProps({
+  //   // zIndex: 1020,
+  // });
+
   const app = createApp(App);
 
+  // 注册v-loading指令
+  registerLoadingDirective(app, {
+    loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+    spinning: 'spinning',
+  });
+
   // 国际化 i18n 配置
   await setupI18n(app);
 
@@ -30,12 +47,18 @@ async function bootstrap(namespace: string) {
   // 安装权限指令
   registerAccessDirective(app);
 
+  // 初始化 tippy
+  initTippy(app);
+
   // 配置路由及路由守卫
   app.use(router);
 
   // 配置@tanstack/vue-query
   app.use(VueQueryPlugin);
 
+  // 配置Motion插件
+  app.use(MotionPlugin);
+
   // 动态更新标题
   watchEffect(() => {
     if (preferences.app.dynamicTitle) {

+ 8 - 0
apps/web-baicai/src/router/routes/core.ts

@@ -21,13 +21,21 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
 
 /** 基本路由,这些路由是必须存在的 */
 const coreRoutes: RouteRecordRaw[] = [
+  /**
+   * 根路由
+   * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+   * 此路由必须存在,且不应修改
+   */
   {
+    component: BasicLayout,
     meta: {
+      hideInBreadcrumb: true,
       title: 'Root',
     },
     name: 'Root',
     path: '/',
     redirect: DEFAULT_HOME_PATH,
+    children: [],
   },
   {
     component: AuthPageLayout,

+ 1 - 1
apps/web-ele/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/web-ele",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://vben.pro",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 15 - 15
apps/web-ele/src/api/request.ts

@@ -1,12 +1,13 @@
 /**
  * 该文件可自行根据业务逻辑进行调整
  */
-import type { HttpResponse } from '@vben/request';
+import type { RequestClientOptions } from '@vben/request';
 
 import { useAppConfig } from '@vben/hooks';
 import { preferences } from '@vben/preferences';
 import {
   authenticateResponseInterceptor,
+  defaultResponseInterceptor,
   errorMessageResponseInterceptor,
   RequestClient,
 } from '@vben/request';
@@ -20,8 +21,9 @@ import { refreshTokenApi } from './core';
 
 const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
 
-function createRequestClient(baseURL: string) {
+function createRequestClient(baseURL: string, options?: RequestClientOptions) {
   const client = new RequestClient({
+    ...options,
     baseURL,
   });
 
@@ -69,18 +71,14 @@ function createRequestClient(baseURL: string) {
     },
   });
 
-  // response数据解构
-  client.addResponseInterceptor<HttpResponse>({
-    fulfilled: (response) => {
-      const { data: responseData, status } = response;
-
-      const { code, data } = responseData;
-      if (status >= 200 && status < 400 && code === 0) {
-        return data;
-      }
-      throw Object.assign({}, response, { response });
-    },
-  });
+  // 处理返回的响应数据格式
+  client.addResponseInterceptor(
+    defaultResponseInterceptor({
+      codeField: 'code',
+      dataField: 'data',
+      successCode: 0,
+    }),
+  );
 
   // token过期的处理
   client.addResponseInterceptor(
@@ -108,6 +106,8 @@ function createRequestClient(baseURL: string) {
   return client;
 }
 
-export const requestClient = createRequestClient(apiURL);
+export const requestClient = createRequestClient(apiURL, {
+  responseReturn: 'data',
+});
 
 export const baseRequestClient = new RequestClient({ baseURL: apiURL });

+ 22 - 0
apps/web-ele/src/bootstrap.ts

@@ -1,6 +1,8 @@
 import { createApp, watchEffect } from 'vue';
 
 import { registerAccessDirective } from '@vben/access';
+import { initTippy, registerLoadingDirective } from '@vben/common-ui';
+import { MotionPlugin } from '@vben/plugins/motion';
 import { preferences } from '@vben/preferences';
 import { initStores } from '@vben/stores';
 import '@vben/styles';
@@ -18,11 +20,25 @@ import { router } from './router';
 async function bootstrap(namespace: string) {
   // 初始化组件适配器
   await initComponentAdapter();
+  // // 设置弹窗的默认配置
+  // setDefaultModalProps({
+  //   fullscreenButton: false,
+  // });
+  // // 设置抽屉的默认配置
+  // setDefaultDrawerProps({
+  //   zIndex: 2000,
+  // });
   const app = createApp(App);
 
   // 注册Element Plus提供的v-loading指令
   app.directive('loading', ElLoading.directive);
 
+  // 注册Vben提供的v-loading和v-spinning指令
+  registerLoadingDirective(app, {
+    loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令
+    spinning: 'spinning',
+  });
+
   // 国际化 i18n 配置
   await setupI18n(app);
 
@@ -32,9 +48,15 @@ async function bootstrap(namespace: string) {
   // 安装权限指令
   registerAccessDirective(app);
 
+  // 初始化 tippy
+  initTippy(app);
+
   // 配置路由及路由守卫
   app.use(router);
 
+  // 配置Motion插件
+  app.use(MotionPlugin);
+
   // 动态更新标题
   watchEffect(() => {
     if (preferences.app.dynamicTitle) {

+ 9 - 1
apps/web-ele/src/router/routes/core.ts

@@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router';
 
 import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 
-import { AuthPageLayout } from '#/layouts';
+import { AuthPageLayout, BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 import Login from '#/views/_core/authentication/login.vue';
 
@@ -21,13 +21,21 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
 
 /** 基本路由,这些路由是必须存在的 */
 const coreRoutes: RouteRecordRaw[] = [
+  /**
+   * 根路由
+   * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+   * 此路由必须存在,且不应修改
+   */
   {
+    component: BasicLayout,
     meta: {
+      hideInBreadcrumb: true,
       title: 'Root',
     },
     name: 'Root',
     path: '/',
     redirect: DEFAULT_HOME_PATH,
+    children: [],
   },
   {
     component: AuthPageLayout,

+ 1 - 3
apps/web-ele/src/router/routes/modules/dashboard.ts

@@ -1,18 +1,16 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'lucide:layout-dashboard',
       order: -1,
       title: $t('page.dashboard.title'),
     },
     name: 'Dashboard',
-    path: '/',
+    path: '/dashboard',
     children: [
       {
         name: 'Analytics',

+ 0 - 2
apps/web-ele/src/router/routes/modules/demos.ts

@@ -1,11 +1,9 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'ic:baseline-view-in-ar',
       keepAlive: true,

+ 12 - 12
apps/web-ele/src/router/routes/modules/vben.ts

@@ -9,30 +9,20 @@ import {
 } from '@vben/constants';
 import { SvgAntdvLogoIcon } from '@vben/icons';
 
-import { BasicLayout, IFrameView } from '#/layouts';
+import { IFrameView } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       badgeType: 'dot',
       icon: VBEN_LOGO_URL,
-      order: 9999,
+      order: 9998,
       title: $t('demos.vben.title'),
     },
     name: 'VbenProject',
     path: '/vben-admin',
     children: [
-      {
-        name: 'VbenAbout',
-        path: '/vben-admin/about',
-        component: () => import('#/views/_core/about/index.vue'),
-        meta: {
-          icon: 'lucide:copyright',
-          title: $t('demos.vben.about'),
-        },
-      },
       {
         name: 'VbenDocument',
         path: '/vben-admin/document',
@@ -77,6 +67,16 @@ const routes: RouteRecordRaw[] = [
       },
     ],
   },
+  {
+    name: 'VbenAbout',
+    path: '/vben-admin/about',
+    component: () => import('#/views/_core/about/index.vue'),
+    meta: {
+      icon: 'lucide:copyright',
+      title: $t('demos.vben.about'),
+      order: 9999,
+    },
+  },
 ];
 
 export default routes;

+ 1 - 1
apps/web-naive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/web-naive",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://vben.pro",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 15 - 15
apps/web-naive/src/api/request.ts

@@ -1,12 +1,13 @@
 /**
  * 该文件可自行根据业务逻辑进行调整
  */
-import type { HttpResponse } from '@vben/request';
+import type { RequestClientOptions } from '@vben/request';
 
 import { useAppConfig } from '@vben/hooks';
 import { preferences } from '@vben/preferences';
 import {
   authenticateResponseInterceptor,
+  defaultResponseInterceptor,
   errorMessageResponseInterceptor,
   RequestClient,
 } from '@vben/request';
@@ -19,8 +20,9 @@ import { refreshTokenApi } from './core';
 
 const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
 
-function createRequestClient(baseURL: string) {
+function createRequestClient(baseURL: string, options?: RequestClientOptions) {
   const client = new RequestClient({
+    ...options,
     baseURL,
   });
 
@@ -68,18 +70,14 @@ function createRequestClient(baseURL: string) {
     },
   });
 
-  // response数据解构
-  client.addResponseInterceptor<HttpResponse>({
-    fulfilled: (response) => {
-      const { data: responseData, status } = response;
-
-      const { code, data } = responseData;
-      if (status >= 200 && status < 400 && code === 0) {
-        return data;
-      }
-      throw Object.assign({}, response, { response });
-    },
-  });
+  // 处理返回的响应数据格式
+  client.addResponseInterceptor(
+    defaultResponseInterceptor({
+      codeField: 'code',
+      dataField: 'data',
+      successCode: 0,
+    }),
+  );
 
   // token过期的处理
   client.addResponseInterceptor(
@@ -107,6 +105,8 @@ function createRequestClient(baseURL: string) {
   return client;
 }
 
-export const requestClient = createRequestClient(apiURL);
+export const requestClient = createRequestClient(apiURL, {
+  responseReturn: 'data',
+});
 
 export const baseRequestClient = new RequestClient({ baseURL: apiURL });

+ 25 - 0
apps/web-naive/src/bootstrap.ts

@@ -1,9 +1,12 @@
 import { createApp, watchEffect } from 'vue';
 
 import { registerAccessDirective } from '@vben/access';
+import { initTippy, registerLoadingDirective } from '@vben/common-ui';
+import { MotionPlugin } from '@vben/plugins/motion';
 import { preferences } from '@vben/preferences';
 import { initStores } from '@vben/stores';
 import '@vben/styles';
+import '@vben/styles/naive';
 
 import { useTitle } from '@vueuse/core';
 
@@ -16,8 +19,24 @@ import { router } from './router';
 async function bootstrap(namespace: string) {
   // 初始化组件适配器
   initComponentAdapter();
+
+  // // 设置弹窗的默认配置
+  // setDefaultModalProps({
+  //   fullscreenButton: false,
+  // });
+  // // 设置抽屉的默认配置
+  // setDefaultDrawerProps({
+  //   // zIndex: 2000,
+  // });
+
   const app = createApp(App);
 
+  // 注册v-loading指令
+  registerLoadingDirective(app, {
+    loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+    spinning: 'spinning',
+  });
+
   // 国际化 i18n 配置
   await setupI18n(app);
 
@@ -27,9 +46,15 @@ async function bootstrap(namespace: string) {
   // 安装权限指令
   registerAccessDirective(app);
 
+  // 初始化 tippy
+  initTippy(app);
+
   // 配置路由及路由守卫
   app.use(router);
 
+  // 配置Motion插件
+  app.use(MotionPlugin);
+
   // 动态更新标题
   watchEffect(() => {
     if (preferences.app.dynamicTitle) {

+ 9 - 1
apps/web-naive/src/router/routes/core.ts

@@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router';
 
 import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 
-import { AuthPageLayout } from '#/layouts';
+import { AuthPageLayout, BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 import Login from '#/views/_core/authentication/login.vue';
 
@@ -21,13 +21,21 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
 
 /** 基本路由,这些路由是必须存在的 */
 const coreRoutes: RouteRecordRaw[] = [
+  /**
+   * 根路由
+   * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+   * 此路由必须存在,且不应修改
+   */
   {
+    component: BasicLayout,
     meta: {
+      hideInBreadcrumb: true,
       title: 'Root',
     },
     name: 'Root',
     path: '/',
     redirect: DEFAULT_HOME_PATH,
+    children: [],
   },
   {
     component: AuthPageLayout,

+ 1 - 3
apps/web-naive/src/router/routes/modules/dashboard.ts

@@ -1,18 +1,16 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'lucide:layout-dashboard',
       order: -1,
       title: $t('page.dashboard.title'),
     },
     name: 'Dashboard',
-    path: '/',
+    path: '/dashboard',
     children: [
       {
         name: 'Analytics',

+ 0 - 2
apps/web-naive/src/router/routes/modules/demos.ts

@@ -1,11 +1,9 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'ic:baseline-view-in-ar',
       keepAlive: true,

+ 12 - 12
apps/web-naive/src/router/routes/modules/vben.ts

@@ -9,30 +9,20 @@ import {
 } from '@vben/constants';
 import { SvgAntdvLogoIcon } from '@vben/icons';
 
-import { BasicLayout, IFrameView } from '#/layouts';
+import { IFrameView } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       badgeType: 'dot',
       icon: VBEN_LOGO_URL,
-      order: 9999,
+      order: 9998,
       title: $t('demos.vben.title'),
     },
     name: 'VbenProject',
     path: '/vben-admin',
     children: [
-      {
-        name: 'VbenAbout',
-        path: '/vben-admin/about',
-        component: () => import('#/views/_core/about/index.vue'),
-        meta: {
-          icon: 'lucide:copyright',
-          title: $t('demos.vben.about'),
-        },
-      },
       {
         name: 'VbenDocument',
         path: '/vben-admin/document',
@@ -77,6 +67,16 @@ const routes: RouteRecordRaw[] = [
       },
     ],
   },
+  {
+    name: 'VbenAbout',
+    path: '/vben-admin/about',
+    component: () => import('#/views/_core/about/index.vue'),
+    meta: {
+      icon: 'lucide:copyright',
+      title: $t('demos.vben.about'),
+      order: 9999,
+    },
+  },
 ];
 
 export default routes;

+ 17 - 1
apps/web-naive/src/views/demos/form/basic.vue

@@ -40,6 +40,7 @@ const [Form, formApi] = useVbenForm({
       fieldName: 'api',
       // 界面显示的label
       label: 'ApiSelect',
+      rules: 'required',
     },
     {
       component: 'ApiTreeSelect',
@@ -56,16 +57,19 @@ const [Form, formApi] = useVbenForm({
       fieldName: 'apiTree',
       // 界面显示的label
       label: 'ApiTreeSelect',
+      rules: 'required',
     },
     {
       component: 'Input',
       fieldName: 'string',
       label: 'String',
+      rules: 'required',
     },
     {
       component: 'InputNumber',
       fieldName: 'number',
       label: 'Number',
+      rules: 'required',
     },
     {
       component: 'RadioGroup',
@@ -80,6 +84,7 @@ const [Form, formApi] = useVbenForm({
           { value: 'E', label: 'E' },
         ],
       },
+      rules: 'selectRequired',
     },
     {
       component: 'RadioGroup',
@@ -94,9 +99,9 @@ const [Form, formApi] = useVbenForm({
           { value: 'C', label: '选项C' },
           { value: 'D', label: '选项D' },
           { value: 'E', label: '选项E' },
-          { value: 'F', label: '选项F' },
         ],
       },
+      rules: 'selectRequired',
     },
     {
       component: 'CheckboxGroup',
@@ -109,11 +114,22 @@ const [Form, formApi] = useVbenForm({
           { value: 'C', label: '选项C' },
         ],
       },
+      rules: 'selectRequired',
     },
     {
       component: 'DatePicker',
       fieldName: 'date',
       label: 'Date',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      fieldName: 'textArea',
+      label: 'TextArea',
+      componentProps: {
+        type: 'textarea',
+      },
+      rules: 'required',
     },
   ],
 });

+ 3 - 1
docs/.vitepress/components/preview-group.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import type { SetupContext } from 'vue';
+
 import { computed, ref, useSlots } from 'vue';
 
 import { VbenTooltip } from '@vben-core/shadcn-ui';
@@ -25,7 +27,7 @@ const props = withDefaults(
 
 const open = ref(false);
 
-const slots = useSlots();
+const slots: SetupContext['slots'] = useSlots();
 
 const tabs = computed(() => {
   return props.files.map((file) => {

+ 1 - 1
docs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/docs",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "scripts": {
     "build": "vitepress build",

+ 1 - 1
docs/src/commercial/community.md

@@ -3,7 +3,7 @@
 社区交流群主要是为了方便大家交流,提问,解答问题,分享经验等。偏自助方式,如果你有问题,可以通过以下方式加入社区交流群:
 
 - [QQ频道](https://pd.qq.com/s/16p8lvvob):推荐!!!主要提供问题解答,分享经验等。
-- QQ群:[大群](https://qm.qq.com/q/MEmHoCLbG0),[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E),[4群](https://qm.qq.com/q/sCzSlm3504),主要使用者交流群。
+- QQ群:[大群](https://qm.qq.com/q/MEmHoCLbG0),[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E),[4群](https://qm.qq.com/q/sCzSlm3504),[5群](https://qm.qq.com/q/ya9XrtbS6s),主要使用者交流群。
 - [Discord](https://discord.com/invite/VU62jTecad): 主要提供问题解答,分享经验等。
 
 ::: tip

+ 5 - 8
docs/src/components/common-ui/vben-api-component.md

@@ -129,7 +129,8 @@ function fetchApi(): Promise<Record<string, any>> {
 
 | 属性名 | 描述 | 类型 | 默认值 |
 | --- | --- | --- | --- |
-| component | 欲包装的组件 | `Component` | - |
+| modelValue(v-model) | 当前值 | `any` | - |
+| component | 欲包装的组件(以下称为目标组件) | `Component` | - |
 | numberToString | 是否将value从数字转为string | `boolean` | `false` |
 | api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - |
 | params | 传递给api的参数 | `Record<string, any>` | - |
@@ -137,16 +138,12 @@ function fetchApi(): Promise<Record<string, any>> {
 | labelField | label字段名 | `string` | `label` |
 | childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` |
 | valueField | value字段名 | `string` | `value` |
-| optionsPropName | 组件接收options数据的属性名称 | `string` | `options` |
-| modelPropName | 组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` |
+| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` |
+| modelPropName | 目标组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` |
 | immediate | 是否立即调用api | `boolean` | `true` |
 | alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` |
 | beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - |
 | afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - |
 | options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
 | visibleEvent | 触发重新请求数据的事件名 | `string` | - |
-| loadingSlot | 组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
-
-```
-
-```
+| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |

+ 10 - 3
docs/src/components/common-ui/vben-count-to-animator.md

@@ -42,11 +42,18 @@ outline: deep
 | transition | 动画效果       | `string`  | `linear` |
 | decimals   | 保留小数点位数 | `number`  | `0`      |
 
-### Methods
+### Events
+
+| 事件名         | 描述           | 类型           |
+| -------------- | -------------- | -------------- |
+| started        | 动画已开始     | `()=>void`     |
+| finished       | 动画已结束     | `()=>void`     |
+| ~~onStarted~~  | ~~动画已开始~~ | ~~`()=>void`~~ |
+| ~~onFinished~~ | ~~动画已结束~~ | ~~`()=>void`~~ |
 
-以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。
+### Methods
 
-| 事件名 | 描述         | 类型       |
+| 方法名 | 描述         | 类型       |
 | ------ | ------------ | ---------- |
 | start  | 开始执行动画 | `()=>void` |
 | reset  | 重置         | `()=>void` |

+ 6 - 4
docs/src/components/common-ui/vben-drawer.md

@@ -55,6 +55,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
 - `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
 - 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
+- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
 
 :::
 
@@ -101,6 +102,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
 | footerClass | modal底部区域的class | `string` | - |
 | headerClass | modal顶部区域的class | `string` | - |
 | zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
+| overlayBlur | 遮罩模糊度 | `number` | - |
 
 ::: info appendToMain
 
@@ -133,13 +135,13 @@ const [Drawer, drawerApi] = useVbenDrawer({
 | close-icon     | 关闭按钮图标        |
 | extra          | 额外内容(标题右侧)  |
 
-### modalApi
+### drawerApi
 
-| 事件名 | 描述 | 类型 |
+| 方法 | 描述 | 类型 |
 | --- | --- | --- |
-| setState | 动态设置弹窗状态属性 | `setState(props) \| setState((prev)=>(props))` |
+| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>drawerApi` |
 | open | 打开弹窗 | `()=>void` |
 | close | 关闭弹窗 | `()=>void` |
-| setData | 设置共享数据 | `<T>(data:T)=>void` |
+| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` |
 | getData | 获取共享数据 | `<T>()=>T` |
 | useStore | 获取可响应式状态 | - |

+ 16 - 5
docs/src/components/common-ui/vben-form.md

@@ -316,12 +316,18 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
 | collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
 | collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
 | collapsedRows | 折叠时保持的行数 | `number` | `1` |
-| fieldMappingTime | 用于将表单内时间区域组件的数组值映射成 2 个字段 | `[string, [string, string], string?][]` | - |
+| fieldMappingTime | 用于将表单内的数组值映射成 2 个字段 | `[string, [string, string],Nullable<string>\|[string,string]\|((any,string)=>any)?][]` | - |
 | commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
 | schema | 表单项的每一项配置 | `FormSchema[]` | - |
 | submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
 | submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
 
+::: tip fieldMappingTime
+
+此属性用于将表单内的数组值映射成 2 个字段,它应当传入一个数组,数组的每一项是一个映射规则,规则的第一个成员是一个字符串,表示需要映射的字段名,第二个成员是一个数组,表示映射后的字段名,第三个成员是一个可选的格式掩码,用于格式化日期时间字段;也可以提供一个格式化函数(参数分别为当前值和当前字段名,返回格式化后的值)。如果明确地将格式掩码设为null,则原值映射而不进行格式化(适用于非日期时间字段)。例如:`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]`,`timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime`和`endTime`字段上。每一项的第三个参数是一个可选的格式掩码,
+
+:::
+
 ### TS 类型说明
 
 ::: details ActionButtonOptions
@@ -341,7 +347,7 @@ export interface ActionButtonOptions {
   /** 是否显示 */
   show?: boolean;
   /** 按钮文本 */
-  text?: string;
+  content?: string;
   /** 任意属性 */
   [key: string]: any;
 }
@@ -406,6 +412,11 @@ export interface FormCommonConfig {
    * 所有表单项的label宽度
    */
   labelWidth?: number;
+  /**
+   * 所有表单项的model属性名。使用自定义组件时可通过此配置指定组件的model属性名。已经在modelPropNameMap中注册的组件不受此配置影响
+   * @default "modelValue"
+   */
+  modelPropName?: string;
   /**
    * 所有表单项的wrapper样式
    */
@@ -434,9 +445,9 @@ export interface FormSchema<
   /** 字段名,也作为自定义插槽的名称 */
   fieldName: string;
   /** 帮助信息 */
-  help?: string;
-  /** 表单 */
-  label?: string;
+  help?: CustomRenderType;
+  /** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */
+  label?: CustomRenderType;
   /** 自定义组件内部渲染  */
   renderComponentContent?: RenderComponentContentType;
   /** 字段规则 */

+ 19 - 9
docs/src/components/common-ui/vben-modal.md

@@ -61,6 +61,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
 - `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
 - 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
+- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
 
 :::
 
@@ -111,6 +112,8 @@ const [Modal, modalApi] = useVbenModal({
 | headerClass | modal顶部区域的class | `string` | - |
 | bordered | 是否显示border | `boolean` | `false` |
 | zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
+| overlayBlur | 遮罩模糊度 | `number` | - |
+| submitting | 标记为提交中,锁定弹窗当前状态 | `boolean` | `false` |
 
 ::: info appendToMain
 
@@ -124,7 +127,7 @@ const [Modal, modalApi] = useVbenModal({
 
 | 事件名 | 描述 | 类型 | 版本号 |
 | --- | --- | --- | --- |
-| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |  |
+| onBeforeClose | 关闭前触发,返回 `false`或者被`reject`则禁止关闭 | `()=>Promise<boolean>\|boolean` | >5.5.2支持Promise |
 | onCancel | 点击取消按钮触发 | `()=>void` |  |
 | onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.4.3 |
 | onConfirm | 点击确认按钮触发 | `()=>void` |  |
@@ -143,11 +146,18 @@ const [Modal, modalApi] = useVbenModal({
 
 ### modalApi
 
-| 事件名 | 描述 | 类型 |
-| --- | --- | --- |
-| setState | 动态设置弹窗状态属性 | `setState(props) \| setState((prev)=>(props))` |
-| open | 打开弹窗 | `()=>void` |
-| close | 关闭弹窗 | `()=>void` |
-| setData | 设置共享数据 | `<T>(data:T)=>void` |
-| getData | 获取共享数据 | `<T>()=>T` |
-| useStore | 获取可响应式状态 | - |
+| 方法 | 描述 | 类型 | 版本 |
+| --- | --- | --- | --- |
+| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>modalApi` | - |
+| open | 打开弹窗 | `()=>void` | - |
+| close | 关闭弹窗 | `()=>void` | - |
+| setData | 设置共享数据 | `<T>(data:T)=>modalApi` | - |
+| getData | 获取共享数据 | `<T>()=>T` | - |
+| useStore | 获取可响应式状态 | - | - |
+| lock | 将弹窗标记为提交中,锁定当前状态 | `(isLock:boolean)=>modalApi` | >5.5.2 |
+
+::: info lock
+
+`lock`方法用于锁定当前弹窗的状态,一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时,弹窗的确认按钮会变为loading状态,同时禁用确认按钮、隐藏关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。
+
+:::

+ 1 - 2
docs/src/demos/vben-drawer/dynamic/index.vue

@@ -13,8 +13,7 @@ function open() {
 }
 
 function handleUpdateTitle() {
-  drawerApi.setState({ title: '外部动态标题' });
-  drawerApi.open();
+  drawerApi.setState({ title: '外部动态标题' }).open();
 }
 </script>
 

+ 6 - 5
docs/src/demos/vben-drawer/shared-data/index.vue

@@ -9,11 +9,12 @@ const [Drawer, drawerApi] = useVbenDrawer({
 });
 
 function open() {
-  drawerApi.setData({
-    content: '外部传递的数据 content',
-    payload: '外部传递的数据 payload',
-  });
-  drawerApi.open();
+  drawerApi
+    .setData({
+      content: '外部传递的数据 content',
+      payload: '外部传递的数据 payload',
+    })
+    .open();
 }
 </script>
 

+ 1 - 2
docs/src/demos/vben-modal/dynamic/index.vue

@@ -13,8 +13,7 @@ function openModal() {
 }
 
 function handleUpdateTitle() {
-  modalApi.setState({ title: '外部动态标题' });
-  modalApi.open();
+  modalApi.setState({ title: '外部动态标题' }).open();
 }
 </script>
 

+ 6 - 5
docs/src/demos/vben-modal/shared-data/index.vue

@@ -9,11 +9,12 @@ const [Modal, modalApi] = useVbenModal({
 });
 
 function openModal() {
-  modalApi.setData({
-    content: '外部传递的数据 content',
-    payload: '外部传递的数据 payload',
-  });
-  modalApi.open();
+  modalApi
+    .setData({
+      content: '外部传递的数据 content',
+      payload: '外部传递的数据 payload',
+    })
+    .open();
 }
 </script>
 

+ 0 - 3
docs/src/en/guide/essentials/route.md

@@ -73,7 +73,6 @@ import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       badgeType: 'dot',
       badgeVariants: 'destructive',
@@ -124,7 +123,6 @@ import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'ic:baseline-view-in-ar',
       keepAlive: true,
@@ -249,7 +247,6 @@ import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'mdi:home',
       title: $t('page.home.title'),

+ 8 - 6
docs/src/en/guide/in-depth/theme.md

@@ -28,9 +28,10 @@ You can check the list below to understand all the available variables.
 
 ```css
 :root {
-  --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto,
-    'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
-    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+  --font-family:
+    -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+    arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+    'Segoe UI Symbol', 'Noto Color Emoji';
 
   /* Default background color of <body />...etc */
   --background: 0 0% 100%;
@@ -322,9 +323,10 @@ type BuiltinThemeType =
 
 ```css
 :root {
-  --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto,
-    'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
-    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+  --font-family:
+    -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+    arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+    'Segoe UI Symbol', 'Noto Color Emoji';
 
   /* Default background color of <body />...etc */
   --background: 0 0% 100%;

+ 11 - 7
docs/src/guide/essentials/route.md

@@ -62,12 +62,10 @@ import type { RouteRecordRaw } from 'vue-router';
 
 import { VBEN_LOGO_URL } from '@vben/constants';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       badgeType: 'dot',
       badgeVariants: 'destructive',
@@ -103,7 +101,6 @@ export default routes;
 
 ::: tip
 
-- 多级路由的父级路由无需设置 `component` 属性,只需设置 `children` 属性即可。除非你真的需要在父级路由嵌套下显示内容。
 - 如果没有特殊情况,父级路由的 `redirect` 属性,不需要指定,默认会指向第一个子路由。
 
 :::
@@ -113,12 +110,10 @@ export default routes;
 ```ts
 import type { RouteRecordRaw } from 'vue-router';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'ic:baseline-view-in-ar',
       keepAlive: true,
@@ -238,12 +233,10 @@ import type { RouteRecordRaw } from 'vue-router';
 
 import { VBEN_LOGO_URL } from '@vben/constants';
 
-import { BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 
 const routes: RouteRecordRaw[] = [
   {
-    component: BasicLayout,
     meta: {
       icon: 'mdi:home',
       title: $t('page.home.title'),
@@ -400,6 +393,10 @@ interface RouteMeta {
    * 菜单可以看到,但是访问会被重定向到403
    */
   menuVisibleWithForbidden?: boolean;
+  /**
+   * 当前路由不使用基础布局(仅在顶级生效)
+   */
+  noBasicLayout?: boolean;
   /**
    * 在新窗口打开
    */
@@ -584,6 +581,13 @@ _注意:_ 排序仅针对一级菜单有效,二级菜单的排序需要在对
 
 用于配置页面的菜单参数,会在菜单中传递给页面。
 
+### noBasicLayout
+
+- 类型:`boolean`
+- 默认值:`false`
+
+用于配置当前路由不使用基础布局,仅在顶级时生效。默认情况下,所有的路由都会被包裹在基础布局中(包含顶部以及侧边等导航部件),如果你的页面不需要这些部件,可以设置 `noBasicLayout` 为 `true`。
+
 ## 路由刷新
 
 路由刷新方式如下:

+ 11 - 13
docs/src/guide/essentials/server.md

@@ -231,19 +231,17 @@ function createRequestClient(baseURL: string) {
     },
   });
 
-  // response数据解构
-  client.addResponseInterceptor<HttpResponse>({
-    fulfilled: (response) => {
-      const { data: responseData, status } = response;
-
-      const { code, data } = responseData;
-
-      if (status >= 200 && status < 400 && code === 0) {
-        return data;
-      }
-      throw Object.assign({}, response, { response });
-    },
-  });
+  // 处理返回的响应数据格式。会根据responseReturn指定的类型返回对应的数据
+  client.addResponseInterceptor(
+    defaultResponseInterceptor({
+      // 指定接口返回的数据中的 code 字段名
+      codeField: 'code',
+      // 指定接口返回的数据中装载了主要数据的字段名
+      dataField: 'data',
+      // 请求成功的 code 值,如果接口返回的 code 等于 successCode 则会认为是成功的请求
+      successCode: 0,
+    }),
+  );
 
   // token过期的处理
   client.addResponseInterceptor(

+ 3 - 1
docs/src/guide/essentials/settings.md

@@ -538,4 +538,6 @@ interface Preferences {
 
 - `overridesPreferences`方法只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置。
 - 任何配置项都可以覆盖,只需要在`overridesPreferences`方法内覆盖即可,不要修改默认配置文件。
-- 更改配置后请清空缓存,否则可能不生效。:::
+- 更改配置后请清空缓存,否则可能不生效。
+
+:::

+ 1 - 1
docs/src/guide/in-depth/access.md

@@ -296,7 +296,7 @@ const { hasAccessByRoles } = useAccess();
 
 #### 指令方式
 
-> 指令支持绑定单个或多个权限码。单个时可以直接传入字符串或数组中包含一个权限码,多个权限码则传入数组。
+> 指令支持绑定单个或多个角色。单个时可以直接传入字符串或数组中包含一个角色,多个角色均可访问则传入数组。
 
 ```vue
 <template>

+ 8 - 6
docs/src/guide/in-depth/theme.md

@@ -28,9 +28,10 @@ css 变量内的颜色,必须使用 `hsl` 格式,如 `0 0% 100%`,不需要
 
 ```css
 :root {
-  --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto,
-    'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
-    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+  --font-family:
+    -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+    arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+    'Segoe UI Symbol', 'Noto Color Emoji';
 
   /* Default background color of <body />...etc */
   --background: 0 0% 100%;
@@ -322,9 +323,10 @@ type BuiltinThemeType =
 
 ```css
 :root {
-  --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto,
-    'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
-    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+  --font-family:
+    -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+    arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+    'Segoe UI Symbol', 'Noto Color Emoji';
 
   /* Default background color of <body />...etc */
   --background: 0 0% 100%;

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/commitlint-config",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

+ 1 - 1
internal/lint-configs/eslint-config/src/configs/javascript.ts

@@ -174,7 +174,7 @@ export async function javascript(): Promise<Linter.Config[]> {
         ],
         'no-use-before-define': [
           'error',
-          { classes: false, functions: false, variables: true },
+          { classes: false, functions: false, variables: false },
         ],
         'no-useless-backreference': 'error',
         'no-useless-call': 'error',

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/stylelint-config",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/node-utils",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/tailwind-config",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

+ 1 - 1
internal/tsconfig/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/tsconfig",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/vite-config",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vben-admin-monorepo",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "private": true,
   "keywords": [
     "monorepo",
@@ -99,7 +99,7 @@
     "node": ">=20.10.0",
     "pnpm": ">=9.12.0"
   },
-  "packageManager": "pnpm@9.15.2",
+  "packageManager": "pnpm@9.15.5",
   "pnpm": {
     "peerDependencyRules": {
       "allowedVersions": {

+ 1 - 1
packages/@core/base/design/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/design",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 4 - 0
packages/@core/base/design/src/css/ui.css

@@ -81,3 +81,7 @@
     transform: translateY(0);
   }
 }
+
+.z-popup {
+  z-index: var(--popup-z-index);
+}

+ 5 - 1
packages/@core/base/design/src/design-tokens/dark.css

@@ -15,7 +15,11 @@
   --card-foreground: 210 40% 98%;
 
   /* Background color for popovers such as <DropdownMenu />, <HoverCard />, <Popover /> */
-  --popover: 222.82deg 8.43% 12.27%;
+
+  /* --popover: 222.82deg 8.43% 12.27%; */
+
+  /* 弹出层的背景色与主题区域背景色太过接近  */
+  --popover: 0 0 14.2%;
   --popover-foreground: 210 40% 98%;
 
   /* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */

+ 6 - 3
packages/@core/base/design/src/design-tokens/default.css

@@ -1,7 +1,10 @@
 :root {
-  --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto,
-    'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
-    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+  /** 弹出层的基础层级 **/
+  --popup-z-index: 2000;
+  --font-family:
+    -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue',
+    arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+    'Segoe UI Symbol', 'Noto Color Emoji';
 
   /* Default background color of <body />...etc */
   --background: 0 0% 100%;

+ 1 - 1
packages/@core/base/icons/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/icons",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 3 - 0
packages/@core/base/icons/src/lucide.ts

@@ -14,6 +14,8 @@ export {
   ChevronRight,
   ChevronsLeft,
   ChevronsRight,
+  Circle,
+  CircleCheckBig,
   CircleHelp,
   Copy,
   CornerDownLeft,
@@ -47,6 +49,7 @@ export {
   PanelRight,
   Pin,
   PinOff,
+  Plus,
   RotateCw,
   Search,
   SearchX,

+ 1 - 1
packages/@core/base/shared/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/shared",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 1 - 0
packages/@core/base/shared/src/utils/download.ts

@@ -31,6 +31,7 @@ export async function downloadFileFromUrl({
 
   if (isChrome || isSafari) {
     triggerDownload(source, resolveFileName(source, fileName));
+    return;
   }
   if (!source.includes('?')) {
     source += '?download';

+ 1 - 1
packages/@core/base/typings/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/typings",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 3 - 0
packages/@core/base/typings/src/helper.d.ts

@@ -109,6 +109,8 @@ type MergeAll<
 
 type EmitType = (name: Name, ...args: any[]) => void;
 
+type MaybePromise<T> = Promise<T> | T;
+
 export type {
   AnyFunction,
   AnyNormalFunction,
@@ -118,6 +120,7 @@ export type {
   EmitType,
   IntervalHandle,
   MaybeComputedRef,
+  MaybePromise,
   MaybeReadonlyRef,
   Merge,
   MergeAll,

+ 4 - 0
packages/@core/base/typings/src/vue-router.d.ts

@@ -97,6 +97,10 @@ interface RouteMeta {
    * 菜单可以看到,但是访问会被重定向到403
    */
   menuVisibleWithForbidden?: boolean;
+  /**
+   * 不使用基础布局(仅在顶级生效)
+   */
+  noBasicLayout?: boolean;
   /**
    * 在新窗口打开
    */

+ 1 - 1
packages/@core/composables/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/composables",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 2 - 0
packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap

@@ -80,6 +80,8 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "enable": true,
     "height": 38,
     "keepAlive": true,
+    "maxCount": 0,
+    "middleClickToClose": false,
     "persist": true,
     "showIcon": true,
     "showMaximize": true,

+ 1 - 1
packages/@core/preferences/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/preferences",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 2 - 0
packages/@core/preferences/src/config.ts

@@ -80,6 +80,8 @@ const defaultPreferences: Preferences = {
     enable: true,
     height: 38,
     keepAlive: true,
+    maxCount: 0,
+    middleClickToClose: false,
     persist: true,
     showIcon: true,
     showMaximize: true,

+ 4 - 0
packages/@core/preferences/src/types.ts

@@ -168,6 +168,10 @@ interface TabbarPreferences {
   height: number;
   /** 开启标签页缓存功能 */
   keepAlive: boolean;
+  /** 限制最大数量 */
+  maxCount: number;
+  /** 是否点击中键时关闭标签 */
+  middleClickToClose: boolean;
   /** 是否持久化标签 */
   persist: boolean;
   /** 是否开启多标签页图标 */

+ 1 - 1
packages/@core/ui-kit/form-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/form-ui",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 3 - 52
packages/@core/ui-kit/form-ui/src/components/form-actions.vue

@@ -3,12 +3,7 @@ import { computed, toRaw, unref, watch } from 'vue';
 
 import { useSimpleLocale } from '@vben-core/composables';
 import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
-import {
-  cn,
-  formatDate,
-  isFunction,
-  triggerWindowResize,
-} from '@vben-core/shared/utils';
+import { cn, isFunction, triggerWindowResize } from '@vben-core/shared/utils';
 
 import { COMPONENT_MAP } from '../config';
 import { injectFormProps } from '../use-form-context';
@@ -58,7 +53,7 @@ async function handleSubmit(e: Event) {
     return;
   }
 
-  const values = handleRangeTimeValue(toRaw(form.values));
+  const values = toRaw(await unref(rootProps).formApi?.getValues());
   await unref(rootProps).handleSubmit?.(values);
 }
 
@@ -67,13 +62,7 @@ async function handleReset(e: Event) {
   e?.stopPropagation();
   const props = unref(rootProps);
 
-  const values = toRaw(form.values);
-  // 清理时间字段
-  props.fieldMappingTime &&
-    props.fieldMappingTime.forEach(([_, [startTimeKey, endTimeKey]]) => {
-      delete values[startTimeKey];
-      delete values[endTimeKey];
-    });
+  const values = toRaw(props.formApi?.getValues());
 
   if (isFunction(props.handleReset)) {
     await props.handleReset?.(values);
@@ -82,44 +71,6 @@ async function handleReset(e: Event) {
   }
 }
 
-function handleRangeTimeValue(values: Record<string, any>) {
-  const fieldMappingTime = unref(rootProps).fieldMappingTime;
-
-  if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
-    return values;
-  }
-
-  fieldMappingTime.forEach(
-    ([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
-      if (startTimeKey && endTimeKey && values[field] === null) {
-        delete values[startTimeKey];
-        delete values[endTimeKey];
-      }
-
-      if (!values[field]) {
-        delete values[field];
-        return;
-      }
-
-      const [startTime, endTime] = values[field];
-      const [startTimeFormat, endTimeFormat] = Array.isArray(format)
-        ? format
-        : [format, format];
-
-      values[startTimeKey] = startTime
-        ? formatDate(startTime, startTimeFormat)
-        : undefined;
-      values[endTimeKey] = endTime
-        ? formatDate(endTime, endTimeFormat)
-        : undefined;
-
-      delete values[field];
-    },
-  );
-
-  return values;
-}
-
 watch(
   () => collapsed.value,
   () => {

+ 57 - 7
packages/@core/ui-kit/form-ui/src/form-api.ts

@@ -15,6 +15,7 @@ import { Store } from '@vben-core/shared/store';
 import {
   bindMethods,
   createMerge,
+  formatDate,
   isDate,
   isDayjsObject,
   isFunction,
@@ -94,7 +95,7 @@ export class FormApi {
 
   async getValues() {
     const form = await this.getForm();
-    return form.values;
+    return form.values ? this.handleRangeTimeValue(form.values) : {};
   }
 
   async isFieldValid(fieldName: string) {
@@ -117,12 +118,11 @@ export class FormApi {
             try {
               const results = await Promise.all(
                 chain.map(async (api) => {
-                  const form = await api.getForm();
                   const validateResult = await api.validate();
                   if (!validateResult.valid) {
                     return;
                   }
-                  const rawValues = toRaw(form.values || {});
+                  const rawValues = toRaw((await api.getValues()) || {});
                   return rawValues;
                 }),
               );
@@ -147,7 +147,9 @@ export class FormApi {
     if (!this.isMounted) {
       Object.assign(this.form, formActions);
       this.stateHandler.setConditionTrue();
-      this.setLatestSubmissionValues({ ...toRaw(this.form.values) });
+      this.setLatestSubmissionValues({
+        ...toRaw(this.handleRangeTimeValue(this.form.values)),
+      });
       this.isMounted = true;
     }
   }
@@ -253,7 +255,7 @@ export class FormApi {
     e?.stopPropagation();
     const form = await this.getForm();
     await form.submitForm();
-    const rawValues = toRaw(form.values || {});
+    const rawValues = toRaw(await this.getValues());
     await this.state?.handleSubmit?.(rawValues);
 
     return rawValues;
@@ -342,6 +344,55 @@ export class FormApi {
     return this.form;
   }
 
+  private handleRangeTimeValue = (originValues: Record<string, any>) => {
+    const values = { ...originValues };
+    const fieldMappingTime = this.state?.fieldMappingTime;
+
+    if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
+      return values;
+    }
+
+    fieldMappingTime.forEach(
+      ([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
+        if (startTimeKey && endTimeKey && values[field] === null) {
+          Reflect.deleteProperty(values, startTimeKey);
+          Reflect.deleteProperty(values, endTimeKey);
+          // delete values[startTimeKey];
+          // delete values[endTimeKey];
+        }
+
+        if (!values[field]) {
+          Reflect.deleteProperty(values, field);
+          // delete values[field];
+          return;
+        }
+
+        const [startTime, endTime] = values[field];
+        if (format === null) {
+          values[startTimeKey] = startTime;
+          values[endTimeKey] = endTime;
+        } else if (isFunction(format)) {
+          values[startTimeKey] = format(startTime, startTimeKey);
+          values[endTimeKey] = format(endTime, endTimeKey);
+        } else {
+          const [startTimeFormat, endTimeFormat] = Array.isArray(format)
+            ? format
+            : [format, format];
+
+          values[startTimeKey] = startTime
+            ? formatDate(startTime, startTimeFormat)
+            : undefined;
+          values[endTimeKey] = endTime
+            ? formatDate(endTime, endTimeFormat)
+            : undefined;
+        }
+        // delete values[field];
+        Reflect.deleteProperty(values, field);
+      },
+    );
+    return values;
+  };
+
   private updateState() {
     const currentSchema = this.state?.schema ?? [];
     const prevSchema = this.prevState?.schema ?? [];
@@ -353,9 +404,8 @@ export class FormApi {
       const deletedSchema = prevSchema.filter(
         (item) => !currentFields.has(item.fieldName),
       );
-
       for (const schema of deletedSchema) {
-        this.form?.setFieldValue(schema.fieldName, undefined);
+        this.form?.setFieldValue?.(schema.fieldName, undefined);
       }
     }
   }

+ 54 - 44
packages/@core/ui-kit/form-ui/src/form-render/form-field.vue

@@ -41,6 +41,7 @@ const {
   label,
   labelClass,
   labelWidth,
+  modelPropName,
   renderComponentContent,
   rules,
 } = defineProps<
@@ -192,7 +193,7 @@ const fieldProps = computed(() => {
   const rules = fieldRules.value;
   return {
     keepValue: true,
-    label,
+    label: isString(label) ? label : '',
     ...(rules ? { rules } : {}),
     ...(formFieldProps as Record<string, any>),
   };
@@ -202,9 +203,9 @@ function fieldBindEvent(slotProps: Record<string, any>) {
   const modelValue = slotProps.componentField.modelValue;
   const handler = slotProps.componentField['onUpdate:modelValue'];
 
-  const bindEventField = isString(component)
-    ? componentBindEventMap.value?.[component]
-    : null;
+  const bindEventField =
+    modelPropName ||
+    (isString(component) ? componentBindEventMap.value?.[component] : null);
 
   let value = modelValue;
   // antd design 的一些组件会传递一个 event 对象
@@ -284,7 +285,7 @@ function autofocus() {
         'pb-6': !compact,
         'pb-2': compact,
       }"
-      class="flex"
+      class="relative flex"
       v-bind="$attrs"
     >
       <FormLabel
@@ -300,55 +301,64 @@ function autofocus() {
           )
         "
         :help="help"
+        :colon="colon"
+        :label="label"
         :required="shouldRequired && !hideRequiredMark"
         :style="labelStyle"
       >
         <template v-if="label">
-          <span>{{ label }}</span>
-          <span v-if="colon" class="ml-[2px]">:</span>
+          <VbenRenderContent :content="label" />
         </template>
       </FormLabel>
-      <div :class="cn('relative flex w-full items-center', wrapperClass)">
-        <FormControl :class="cn(controlClass)">
-          <slot
-            v-bind="{
-              ...slotProps,
-              ...createComponentProps(slotProps),
-              disabled: shouldDisabled,
-              isInValid,
-            }"
-          >
-            <component
-              :is="FieldComponent"
-              ref="fieldComponentRef"
-              :class="{
-                'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
+      <div class="w-full overflow-hidden">
+        <div :class="cn('relative flex w-full items-center', wrapperClass)">
+          <div class="flex-auto overflow-hidden p-[2px]">
+            <FormControl :class="cn(controlClass)">
+              <slot
+                v-bind="{
+                  ...slotProps,
+                  ...createComponentProps(slotProps),
+                  disabled: shouldDisabled,
                   isInValid,
-              }"
-              v-bind="createComponentProps(slotProps)"
-              :disabled="shouldDisabled"
-            >
-              <template v-for="name in renderContentKey" :key="name" #[name]>
-                <VbenRenderContent
-                  :content="customContentRender[name]"
-                  v-bind="slotProps"
-                />
-              </template>
-              <!-- <slot></slot> -->
-            </component>
-          </slot>
-        </FormControl>
-        <!-- 自定义后缀 -->
-        <div v-if="suffix" class="ml-1">
-          <VbenRenderContent :content="suffix" />
+                }"
+              >
+                <component
+                  :is="FieldComponent"
+                  ref="fieldComponentRef"
+                  :class="{
+                    'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
+                      isInValid,
+                  }"
+                  v-bind="createComponentProps(slotProps)"
+                  :disabled="shouldDisabled"
+                >
+                  <template
+                    v-for="name in renderContentKey"
+                    :key="name"
+                    #[name]="renderSlotProps"
+                  >
+                    <VbenRenderContent
+                      :content="customContentRender[name]"
+                      v-bind="{ ...renderSlotProps, formContext: slotProps }"
+                    />
+                  </template>
+                  <!-- <slot></slot> -->
+                </component>
+              </slot>
+            </FormControl>
+          </div>
+
+          <!-- 自定义后缀 -->
+          <div v-if="suffix" class="ml-1">
+            <VbenRenderContent :content="suffix" />
+          </div>
+          <FormDescription v-if="description" class="ml-1">
+            <VbenRenderContent :content="description" />
+          </FormDescription>
         </div>
 
-        <FormDescription v-if="description">
-          <VbenRenderContent :content="description" />
-        </FormDescription>
-
         <Transition name="slide-up">
-          <FormMessage class="absolute -bottom-[22px]" />
+          <FormMessage class="absolute bottom-1" />
         </Transition>
       </div>
     </FormItem>

+ 12 - 3
packages/@core/ui-kit/form-ui/src/form-render/form-label.vue

@@ -1,10 +1,18 @@
 <script setup lang="ts">
-import { FormLabel, VbenHelpTooltip } from '@vben-core/shadcn-ui';
+import type { CustomRenderType } from '../types';
+
+import {
+  FormLabel,
+  VbenHelpTooltip,
+  VbenRenderContent,
+} from '@vben-core/shadcn-ui';
 import { cn } from '@vben-core/shared/utils';
 
 interface Props {
   class?: string;
-  help?: string;
+  colon?: boolean;
+  help?: CustomRenderType;
+  label?: CustomRenderType;
   required?: boolean;
 }
 
@@ -16,7 +24,8 @@ const props = defineProps<Props>();
     <span v-if="required" class="text-destructive mr-[2px]">*</span>
     <slot></slot>
     <VbenHelpTooltip v-if="help" trigger-class="size-3.5 ml-1">
-      {{ help }}
+      <VbenRenderContent :content="help" />
     </VbenHelpTooltip>
+    <span v-if="colon && label" class="ml-[2px]">:</span>
   </FormLabel>
 </template>

+ 2 - 0
packages/@core/ui-kit/form-ui/src/form-render/form.vue

@@ -98,6 +98,7 @@ const computedSchema = computed(
       hideRequiredMark = false,
       labelClass = '',
       labelWidth = 100,
+      modelPropName = '',
       wrapperClass = '',
     } = mergeWithArrayOverride(props.commonConfig, props.globalCommonConfig);
     return (props.schema || []).map((schema, index) => {
@@ -118,6 +119,7 @@ const computedSchema = computed(
         hideLabel,
         hideRequiredMark,
         labelWidth,
+        modelPropName,
         wrapperClass,
         ...schema,
         commonComponentProps: componentProps,

+ 17 - 7
packages/@core/ui-kit/form-ui/src/types.ts

@@ -4,7 +4,7 @@ import type { ZodTypeAny } from 'zod';
 import type { Component, HtmlHTMLAttributes, Ref } from 'vue';
 
 import type { VbenButtonProps } from '@vben-core/shadcn-ui';
-import type { ClassType } from '@vben-core/typings';
+import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
 
 import type { FormApi } from './form-api';
 
@@ -197,6 +197,11 @@ export interface FormCommonConfig {
    * 所有表单项的label宽度
    */
   labelWidth?: number;
+  /**
+   * 所有表单项的model属性名
+   * @default "modelValue"
+   */
+  modelPropName?: string;
   /**
    * 所有表单项的wrapper样式
    */
@@ -219,7 +224,12 @@ export type HandleResetFn = (
 export type FieldMappingTime = [
   string,
   [string, string],
-  ([string, string] | string)?,
+  (
+    | ((value: any, fieldName: string) => any)
+    | [string, string]
+    | null
+    | string
+  )?,
 ][];
 
 export interface FormSchema<
@@ -234,13 +244,13 @@ export interface FormSchema<
   /** 依赖 */
   dependencies?: FormItemDependencies;
   /** 描述 */
-  description?: string;
+  description?: CustomRenderType;
   /** 字段名 */
   fieldName: string;
   /** 帮助信息 */
-  help?: string;
+  help?: CustomRenderType;
   /** 表单项 */
-  label?: string;
+  label?: CustomRenderType;
   // 自定义组件内部渲染
   renderComponentContent?: RenderComponentContentType;
   /** 字段规则 */
@@ -311,7 +321,7 @@ export interface FormRenderProps<
 
 export interface ActionButtonOptions extends VbenButtonProps {
   [key: string]: any;
-  content?: string;
+  content?: MaybeComputedRef<string>;
   show?: boolean;
 }
 
@@ -330,7 +340,7 @@ export interface VbenFormProps<
    */
   actionWrapperClass?: ClassType;
   /**
-   * 表单字段映射成时间格式
+   * 表单字段映射
    */
   fieldMappingTime?: FieldMappingTime;
   /**

+ 4 - 2
packages/@core/ui-kit/form-ui/src/use-form-context.ts

@@ -2,7 +2,7 @@ import type { ZodRawShape } from 'zod';
 
 import type { ComputedRef } from 'vue';
 
-import type { FormActions, VbenFormProps } from './types';
+import type { ExtendedFormApi, FormActions, VbenFormProps } from './types';
 
 import { computed, unref, useSlots } from 'vue';
 
@@ -13,8 +13,10 @@ import { useForm } from 'vee-validate';
 import { object } from 'zod';
 import { getDefaultsForSchema } from 'zod-defaults';
 
+type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
+
 export const [injectFormProps, provideFormProps] =
-  createContext<[ComputedRef<VbenFormProps> | VbenFormProps, FormActions]>(
+  createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>(
     'VbenFormProps',
   );
 

+ 4 - 2
packages/@core/ui-kit/form-ui/src/vben-use-form.vue

@@ -53,8 +53,10 @@ function handleKeyDownEnter(event: KeyboardEvent) {
   forward.value.formApi.validateAndSubmitForm();
 }
 
-const handleValuesChangeDebounced = useDebounceFn((newVal) => {
-  forward.value.handleValuesChange?.(cloneDeep(newVal));
+const handleValuesChangeDebounced = useDebounceFn(async () => {
+  forward.value.handleValuesChange?.(
+    cloneDeep(await forward.value.formApi.getValues()),
+  );
   state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
 }, 300);
 

+ 1 - 1
packages/@core/ui-kit/layout-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/layout-ui",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 1 - 1
packages/@core/ui-kit/menu-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/menu-ui",
-  "version": "5.5.2",
+  "version": "5.5.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 2 - 2
packages/@core/ui-kit/menu-ui/src/components/menu.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { UseResizeObserverReturn } from '@vueuse/core';
 
-import type { VNodeArrayChildren } from 'vue';
+import type { SetupContext, VNodeArrayChildren } from 'vue';
 
 import type {
   MenuItemClicked,
@@ -55,7 +55,7 @@ const emit = defineEmits<{
 
 const { b, is } = useNamespace('menu');
 const menuStyle = useMenuStyle();
-const slots = useSlots();
+const slots: SetupContext['slots'] = useSlots();
 const menu = ref<HTMLUListElement>();
 const sliceIndex = ref(-1);
 const openedMenus = ref<MenuProvider['openedMenus']>(

+ 2 - 0
packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts

@@ -141,6 +141,7 @@ export class DrawerApi {
 
   setData<T>(payload: T) {
     this.sharedData.payload = payload;
+    return this;
   }
 
   setState(
@@ -153,5 +154,6 @@ export class DrawerApi {
     } else {
       this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
     }
+    return this;
   }
 }

+ 5 - 1
packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts

@@ -85,12 +85,16 @@ export interface DrawerProps {
    * 是否自动聚焦
    */
   openAutoFocus?: boolean;
+  /**
+   * 弹窗遮罩模糊效果
+   */
+  overlayBlur?: number;
+
   /**
    * 抽屉位置
    * @default right
    */
   placement?: DrawerPlacement;
-
   /**
    * 是否显示取消按钮
    * @default true

+ 2 - 0
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue

@@ -68,6 +68,7 @@ const {
   loading: showLoading,
   modal,
   openAutoFocus,
+  overlayBlur,
   placement,
   showCancelButton,
   showConfirmButton,
@@ -140,6 +141,7 @@ const getAppendTo = computed(() => {
       :open="state?.isOpen"
       :side="placement"
       :z-index="zIndex"
+      :overlay-blur="overlayBlur"
       @close-auto-focus="handleFocusOutside"
       @closed="() => drawerApi?.onClosed()"
       @escape-key-down="escapeKeyDown"

+ 1 - 1
packages/@core/ui-kit/popup-ui/src/drawer/index.ts

@@ -1,3 +1,3 @@
 export type * from './drawer';
 export { default as VbenDrawer } from './drawer.vue';
-export { useVbenDrawer } from './use-drawer';
+export { setDefaultDrawerProps, useVbenDrawer } from './use-drawer';

+ 7 - 0
packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts

@@ -21,6 +21,12 @@ import VbenDrawer from './drawer.vue';
 
 const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
 
+const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {};
+
+export function setDefaultDrawerProps(props: Partial<DrawerProps>) {
+  Object.assign(DEFAULT_DRAWER_PROPS, props);
+}
+
 export function useVbenDrawer<
   TParentDrawerProps extends DrawerProps = DrawerProps,
 >(options: DrawerApiOptions = {}) {
@@ -69,6 +75,7 @@ export function useVbenDrawer<
   const injectData = inject<any>(USER_DRAWER_INJECT_KEY, {});
 
   const mergedOptions = {
+    ...DEFAULT_DRAWER_PROPS,
     ...injectData.options,
     ...options,
   } as DrawerApiOptions;

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