瀏覽代碼

feat: 合并

DESKTOP-USV654P\pc 11 月之前
父節點
當前提交
ddaaf01e77
共有 100 個文件被更改,包括 1484 次插入291 次删除
  1. 9 9
      .github/CODEOWNERS
  2. 7 7
      .lintstagedrc.mjs
  3. 4 4
      .vscode/launch.json
  4. 27 2
      apps/backend-mock/api/table/list.ts
  5. 3 0
      apps/backend-mock/utils/mock-data.ts
  6. 1 1
      apps/web-antd/package.json
  7. 45 3
      apps/web-antd/src/adapter/component/index.ts
  8. 3 1
      apps/web-antd/src/locales/index.ts
  9. 11 3
      apps/web-antd/src/router/guard.ts
  10. 5 1
      apps/web-antd/src/views/_core/authentication/code-login.vue
  11. 3 5
      apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue
  12. 3 5
      apps/web-antd/src/views/dashboard/analytics/analytics-visits-data.vue
  13. 3 5
      apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
  14. 3 5
      apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue
  15. 3 5
      apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue
  16. 1 1
      apps/web-antd/src/views/dashboard/analytics/index.vue
  17. 2 1
      apps/web-baicai/package.json
  18. 45 2
      apps/web-baicai/src/adapter/component/index.ts
  19. 1 2
      apps/web-baicai/src/adapter/vxe-table.ts
  20. 4 4
      apps/web-baicai/src/components/form/component-map.ts
  21. 3 0
      apps/web-baicai/src/components/tree/index.vue
  22. 3 0
      apps/web-baicai/src/components/tree/src/tree.vue
  23. 1 0
      apps/web-baicai/src/layouts/basic.vue
  24. 11 3
      apps/web-baicai/src/router/guard.ts
  25. 6 0
      apps/web-baicai/src/utils/utils.ts
  26. 50 5
      apps/web-baicai/src/views/_core/authentication/code-login.vue
  27. 1 1
      apps/web-ele/package.json
  28. 138 9
      apps/web-ele/src/adapter/component/index.ts
  29. 1 0
      apps/web-ele/src/adapter/form.ts
  30. 4 0
      apps/web-ele/src/bootstrap.ts
  31. 3 1
      apps/web-ele/src/locales/index.ts
  32. 1 0
      apps/web-ele/src/locales/langs/en-US/demos.json
  33. 1 0
      apps/web-ele/src/locales/langs/zh-CN/demos.json
  34. 11 3
      apps/web-ele/src/router/guard.ts
  35. 8 0
      apps/web-ele/src/router/routes/modules/demos.ts
  36. 5 1
      apps/web-ele/src/views/_core/authentication/code-login.vue
  37. 3 5
      apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue
  38. 3 5
      apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue
  39. 3 5
      apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
  40. 3 5
      apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue
  41. 3 5
      apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue
  42. 1 1
      apps/web-ele/src/views/dashboard/analytics/index.vue
  43. 52 44
      apps/web-ele/src/views/demos/element/index.vue
  44. 181 0
      apps/web-ele/src/views/demos/form/basic.vue
  45. 1 1
      apps/web-naive/package.json
  46. 84 4
      apps/web-naive/src/adapter/component/index.ts
  47. 0 2
      apps/web-naive/src/adapter/form.ts
  48. 2 2
      apps/web-naive/src/locales/index.ts
  49. 1 0
      apps/web-naive/src/locales/langs/en-US/demos.json
  50. 1 0
      apps/web-naive/src/locales/langs/zh-CN/demos.json
  51. 11 3
      apps/web-naive/src/router/guard.ts
  52. 8 0
      apps/web-naive/src/router/routes/modules/demos.ts
  53. 5 1
      apps/web-naive/src/views/_core/authentication/code-login.vue
  54. 3 5
      apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue
  55. 3 5
      apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue
  56. 3 5
      apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
  57. 3 5
      apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue
  58. 3 5
      apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue
  59. 1 1
      apps/web-naive/src/views/dashboard/analytics/index.vue
  60. 143 0
      apps/web-naive/src/views/demos/form/basic.vue
  61. 2 1
      apps/web-naive/src/views/demos/naive/index.vue
  62. 3 1
      docs/.vitepress/config/en.mts
  63. 21 1
      docs/.vitepress/config/zh.mts
  64. 0 1
      docs/.vitepress/theme/components/site-layout.vue
  65. 1 1
      docs/package.json
  66. 2 1
      docs/src/_env/adapter/component.ts
  67. 0 2
      docs/src/_env/adapter/form.ts
  68. 152 0
      docs/src/components/common-ui/vben-api-component.md
  69. 22 6
      docs/src/components/common-ui/vben-drawer.md
  70. 56 0
      docs/src/components/common-ui/vben-ellipsis-text.md
  71. 33 10
      docs/src/components/common-ui/vben-form.md
  72. 11 0
      docs/src/components/common-ui/vben-modal.md
  73. 12 8
      docs/src/components/common-ui/vben-vxe-table.md
  74. 4 0
      docs/src/components/introduction.md
  75. 44 0
      docs/src/components/layout-ui/page.md
  76. 100 0
      docs/src/demos/vben-api-component/cascader/index.vue
  77. 4 0
      docs/src/demos/vben-ellipsis-text/expand/index.vue
  78. 4 0
      docs/src/demos/vben-ellipsis-text/line/index.vue
  79. 14 0
      docs/src/demos/vben-ellipsis-text/tooltip/index.vue
  80. 7 0
      docs/src/demos/vben-vxe-table/form/index.vue
  81. 1 0
      docs/src/en/guide/essentials/settings.md
  82. 1 0
      docs/src/guide/essentials/settings.md
  83. 1 1
      docs/src/guide/in-depth/ui-framework.md
  84. 1 1
      docs/src/guide/introduction/quick-start.md
  85. 1 1
      internal/lint-configs/commitlint-config/package.json
  86. 1 0
      internal/lint-configs/eslint-config/src/configs/import.ts
  87. 0 1
      internal/lint-configs/eslint-config/src/configs/javascript.ts
  88. 19 42
      internal/lint-configs/eslint-config/src/configs/perfectionist.ts
  89. 0 1
      internal/lint-configs/eslint-config/src/configs/vue.ts
  90. 1 1
      internal/lint-configs/stylelint-config/package.json
  91. 1 1
      internal/node-utils/package.json
  92. 1 1
      internal/node-utils/src/index.ts
  93. 3 1
      internal/node-utils/src/spinner.ts
  94. 1 1
      internal/tailwind-config/package.json
  95. 1 1
      internal/tailwind-config/src/index.ts
  96. 1 1
      internal/tsconfig/package.json
  97. 1 1
      internal/vite-config/package.json
  98. 2 2
      internal/vite-config/src/config/application.ts
  99. 2 2
      internal/vite-config/src/plugins/importmap.ts
  100. 2 2
      internal/vite-config/src/plugins/inject-app-loading/index.ts

+ 9 - 9
.github/CODEOWNERS

@@ -1,14 +1,14 @@
 # default onwer
-* anncwb@126.com vince292007@gmail.com
+* anncwb@126.com vince292007@gmail.com netfan@foxmail.com
 
 # vben core onwer
-/.github/ anncwb@126.com vince292007@gmail.com
-/.vscode/ anncwb@126.com vince292007@gmail.com
-/packages/ anncwb@126.com vince292007@gmail.com
-/packages/@core/ anncwb@126.com vince292007@gmail.com
-/internal/ anncwb@126.com vince292007@gmail.com
-/scripts/ anncwb@126.com vince292007@gmail.com
+/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
+/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
+/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
+/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
+/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
+/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
 
 # vben team onwer
-apps/ anncwb@126.com vince292007@gmail.com @vbenjs/team-v5
-docs/ anncwb@126.com vince292007@gmail.com @vbenjs/team-v5
+apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5
+docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5

+ 7 - 7
.lintstagedrc.mjs

@@ -1,4 +1,10 @@
 export default {
+  '*.md': ['prettier --cache --ignore-unknown --write'],
+  '*.vue': [
+    'prettier --write',
+    'eslint --cache --fix',
+    'stylelint --fix --allow-empty-input',
+  ],
   '*.{js,jsx,ts,tsx}': [
     'prettier --cache --ignore-unknown  --write',
     'eslint --cache --fix',
@@ -7,14 +13,8 @@ export default {
     'prettier --cache --ignore-unknown --write',
     'stylelint --fix --allow-empty-input',
   ],
-  '*.md': ['prettier --cache --ignore-unknown --write'],
-  '*.vue': [
-    'prettier --write',
-    'eslint --cache --fix',
-    'stylelint --fix --allow-empty-input',
-  ],
+  'package.json': ['prettier --cache --write'],
   '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
     'prettier --cache --write--parser json',
   ],
-  'package.json': ['prettier --cache --write'],
 };

+ 4 - 4
.vscode/launch.json

@@ -9,7 +9,7 @@
       "url": "http://localhost:5555",
       "env": { "NODE_ENV": "development" },
       "sourceMaps": true,
-      "webRoot": "${workspaceFolder}"
+      "webRoot": "${workspaceFolder}/playground"
     },
     {
       "type": "chrome",
@@ -18,7 +18,7 @@
       "url": "http://localhost:5666",
       "env": { "NODE_ENV": "development" },
       "sourceMaps": true,
-      "webRoot": "${workspaceFolder}"
+      "webRoot": "${workspaceFolder}/apps/web-antd"
     },
     {
       "type": "chrome",
@@ -27,7 +27,7 @@
       "url": "http://localhost:5777",
       "env": { "NODE_ENV": "development" },
       "sourceMaps": true,
-      "webRoot": "${workspaceFolder}"
+      "webRoot": "${workspaceFolder}/apps/web-ele"
     },
     {
       "type": "chrome",
@@ -36,7 +36,7 @@
       "url": "http://localhost:5888",
       "env": { "NODE_ENV": "development" },
       "sourceMaps": true,
-      "webRoot": "${workspaceFolder}"
+      "webRoot": "${workspaceFolder}/apps/web-naive"
     }
   ]
 }

+ 27 - 2
apps/backend-mock/api/table/list.ts

@@ -43,6 +43,31 @@ export default eventHandler(async (event) => {
 
   await sleep(600);
 
-  const { page, pageSize } = getQuery(event);
-  return usePageResponseSuccess(page as string, pageSize as string, mockData);
+  const { page, pageSize, sortBy, sortOrder } = getQuery(event);
+  const listData = structuredClone(mockData);
+  if (sortBy && Reflect.has(listData[0], sortBy as string)) {
+    listData.sort((a, b) => {
+      if (sortOrder === 'asc') {
+        if (sortBy === 'price') {
+          return (
+            Number.parseFloat(a[sortBy as string]) -
+            Number.parseFloat(b[sortBy as string])
+          );
+        } else {
+          return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
+        }
+      } else {
+        if (sortBy === 'price') {
+          return (
+            Number.parseFloat(b[sortBy as string]) -
+            Number.parseFloat(a[sortBy as string])
+          );
+        } else {
+          return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
+        }
+      }
+    });
+  }
+
+  return usePageResponseSuccess(page as string, pageSize as string, listData);
 });

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

@@ -4,6 +4,7 @@ export interface UserInfo {
   realName: string;
   roles: string[];
   username: string;
+  homePath?: string;
 }
 
 export const MOCK_USERS: UserInfo[] = [
@@ -20,6 +21,7 @@ export const MOCK_USERS: UserInfo[] = [
     realName: 'Admin',
     roles: ['admin'],
     username: 'admin',
+    homePath: '/workspace',
   },
   {
     id: 2,
@@ -27,6 +29,7 @@ export const MOCK_USERS: UserInfo[] = [
     realName: 'Jack',
     roles: ['user'],
     username: 'jack',
+    homePath: '/analytics',
   },
 ];
 

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

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

+ 45 - 3
apps/web-antd/src/adapter/component/index.ts

@@ -3,12 +3,13 @@
  * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
  */
 
+import type { Component, SetupContext } from 'vue';
+
 import type { BaseFormComponentType } from '@vben/common-ui';
 
-import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { globalShareState } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -48,12 +49,15 @@ const withDefaultPlaceholder = <T extends Component>(
 
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
+  | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'AutoComplete'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
   | 'DefaultButton'
   | 'Divider'
+  | 'IconPicker'
   | 'Input'
   | 'InputNumber'
   | 'InputPassword'
@@ -77,7 +81,38 @@ async function initComponentAdapter() {
     // 如果你的组件体积比较大,可以使用异步加载
     // Button: () =>
     // import('xxx').then((res) => res.Button),
-
+    ApiSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: Select,
+          loadingSlot: 'suffixIcon',
+          visibleEvent: 'onDropdownVisibleChange',
+          modelPropName: 'value',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: TreeSelect,
+          fieldNames: { label: 'label', value: 'value', children: 'children' },
+          loadingSlot: 'suffixIcon',
+          modelPropName: 'value',
+          optionsPropName: 'treeData',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
     AutoComplete,
     Checkbox,
     CheckboxGroup,
@@ -87,6 +122,13 @@ async function initComponentAdapter() {
       return h(Button, { ...props, attrs, type: 'default' }, slots);
     },
     Divider,
+    IconPicker: (props, { attrs, slots }) => {
+      return h(
+        IconPicker,
+        { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
+        slots,
+      );
+    },
     Input: withDefaultPlaceholder(Input, 'input'),
     InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
     InputPassword: withDefaultPlaceholder(InputPassword, 'input'),

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

@@ -1,7 +1,9 @@
-import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
 import type { Locale } from 'ant-design-vue/es/locale';
 
 import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
 import { ref } from 'vue';
 
 import {

+ 11 - 3
apps/web-antd/src/router/guard.ts

@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
     if (coreRouteNames.includes(to.name as string)) {
       if (to.path === LOGIN_PATH && accessStore.accessToken) {
         return decodeURIComponent(
-          (to.query?.redirect as string) || DEFAULT_HOME_PATH,
+          (to.query?.redirect as string) ||
+            userStore.userInfo?.homePath ||
+            DEFAULT_HOME_PATH,
         );
       }
       return true;
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
         return {
           path: LOGIN_PATH,
           // 如不需要,直接删除 query
-          query: { redirect: encodeURIComponent(to.fullPath) },
+          query:
+            to.fullPath === DEFAULT_HOME_PATH
+              ? {}
+              : { redirect: encodeURIComponent(to.fullPath) },
           // 携带当前跳转的页面,登录后重新跳转该页面
           replace: true,
         };
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
     accessStore.setAccessMenus(accessibleMenus);
     accessStore.setAccessRoutes(accessibleRoutes);
     accessStore.setIsAccessChecked(true);
-    const redirectPath = (from.query.redirect ?? to.fullPath) as string;
+    const redirectPath = (from.query.redirect ??
+      (to.path === DEFAULT_HOME_PATH
+        ? userInfo.homePath || DEFAULT_HOME_PATH
+        : to.fullPath)) as string;
 
     return {
       ...router.resolve(decodeURIComponent(redirectPath)),

+ 5 - 1
apps/web-antd/src/views/_core/authentication/code-login.vue

@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
 defineOptions({ name: 'CodeLogin' });
 
 const loading = ref(false);
+const CODE_LENGTH = 6;
 
 const formSchema = computed((): VbenFormSchema[] => {
   return [
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
     {
       component: 'VbenPinInput',
       componentProps: {
+        codeLength: CODE_LENGTH,
         createText: (countdown: number) => {
           const text =
             countdown > 0
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
       },
       fieldName: 'code',
       label: $t('authentication.code'),
-      rules: z.string().min(1, { message: $t('authentication.codeTip') }),
+      rules: z.string().length(CODE_LENGTH, {
+        message: $t('authentication.codeTip', [CODE_LENGTH]),
+      }),
     },
   ];
 });

+ 3 - 5
apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-antd/src/views/dashboard/analytics/analytics-visits-data.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 1 - 1
apps/web-antd/src/views/dashboard/analytics/index.vue

@@ -15,10 +15,10 @@ import {
 } from '@vben/icons';
 
 import AnalyticsTrends from './analytics-trends.vue';
-import AnalyticsVisits from './analytics-visits.vue';
 import AnalyticsVisitsData from './analytics-visits-data.vue';
 import AnalyticsVisitsSales from './analytics-visits-sales.vue';
 import AnalyticsVisitsSource from './analytics-visits-source.vue';
+import AnalyticsVisits from './analytics-visits.vue';
 
 const overviewItems: AnalysisOverviewItem[] = [
   {

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/baicai",
-  "version": "5.4.8",
+  "version": "5.5.2",
   "homepage": "https://vben.pro",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {
@@ -30,6 +30,7 @@
   },
   "dependencies": {
     "@tanstack/vue-query": "catalog:",
+    "@vben-core/menu-ui": "workspace:*",
     "@vben/access": "workspace:*",
     "@vben/common-ui": "workspace:*",
     "@vben/constants": "workspace:*",

+ 45 - 2
apps/web-baicai/src/adapter/component/index.ts

@@ -3,14 +3,15 @@
  * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
  */
 
+import type { Component, SetupContext } from 'vue';
+
 import type { BaseFormComponentType } from '@vben/common-ui';
 
 import type { CustomComponentType } from '#/components/form/types';
 
-import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { globalShareState } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -52,12 +53,15 @@ const withDefaultPlaceholder = <T extends Component>(
 
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
+  | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'AutoComplete'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
   | 'DefaultButton'
   | 'Divider'
+  | 'IconPicker'
   | 'Input'
   | 'InputNumber'
   | 'InputPassword'
@@ -83,6 +87,38 @@ async function initComponentAdapter() {
     // Button: () =>
     // import('xxx').then((res) => res.Button),
 
+    ApiSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: Select,
+          loadingSlot: 'suffixIcon',
+          modelPropName: 'value',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: TreeSelect,
+          fieldNames: { label: 'label', value: 'value', children: 'children' },
+          loadingSlot: 'suffixIcon',
+          modelPropName: 'value',
+          optionsPropName: 'treeData',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
     AutoComplete,
     Checkbox,
     CheckboxGroup,
@@ -92,6 +128,13 @@ async function initComponentAdapter() {
       return h(Button, { ...props, attrs, type: 'default' }, slots);
     },
     Divider,
+    IconPicker: (props, { attrs, slots }) => {
+      return h(
+        IconPicker,
+        { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
+        slots,
+      );
+    },
     Input: withDefaultPlaceholder(Input, 'input'),
     InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
     InputPassword: withDefaultPlaceholder(InputPassword, 'input'),

+ 1 - 2
apps/web-baicai/src/adapter/vxe-table.ts

@@ -5,7 +5,6 @@ import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
 import { Button, Image } from 'ant-design-vue';
 
 // import { componentMap } from '#/components/view/component-map';
-
 import { useVbenForm } from './form';
 
 setupVbenVxeTable({
@@ -18,11 +17,11 @@ setupVbenVxeTable({
         columnConfig: {
           resizable: true,
         },
-        minHeight: 180,
         formConfig: {
           // 全局禁用vxe-table的表单配置,使用formOptions
           enabled: false,
         },
+        minHeight: 180,
         proxyConfig: {
           autoLoad: true,
           response: {

+ 4 - 4
apps/web-baicai/src/components/form/component-map.ts

@@ -1,8 +1,8 @@
-import type { CustomComponentType } from './types';
-
 import type { Component } from 'vue';
 
-import { toPascalCase } from '#/utils';
+import type { CustomComponentType } from './types';
+
+import { getFileNameWithoutExtension, toPascalCase } from '#/utils';
 
 const componentMap = new Map<CustomComponentType | string, Component>();
 // import.meta.glob() 直接引入所有的模块 Vite 独有的功能
@@ -11,7 +11,7 @@ const modules = import.meta.glob('./components/**/*.vue', { eager: true });
 Object.keys(modules).forEach((key) => {
   if (!key.includes('-ignore')) {
     const mod = (modules as any)[key].default || {};
-    const compName = key.replace('./components/', '').replace('.vue', '');
+    const compName = getFileNameWithoutExtension(key);
     componentMap.set(toPascalCase(compName), mod);
   }
 });

+ 3 - 0
apps/web-baicai/src/components/tree/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div>TRee</div>
+</template>

+ 3 - 0
apps/web-baicai/src/components/tree/src/tree.vue

@@ -0,0 +1,3 @@
+<template>
+  <div>TRee</div>
+</template>

+ 1 - 0
apps/web-baicai/src/layouts/basic.vue

@@ -143,6 +143,7 @@ watch(
         :text="userStore.userInfo?.realName"
         description="ann.vben@gmail.com"
         tag-text="Pro"
+        trigger="both"
         @logout="handleLogout"
       />
     </template>

+ 11 - 3
apps/web-baicai/src/router/guard.ts

@@ -52,7 +52,9 @@ function setupAccessGuard(router: Router) {
     if (coreRouteNames.includes(to.name as string)) {
       if (to.path === LOGIN_PATH && accessStore.accessToken) {
         return decodeURIComponent(
-          (to.query?.redirect as string) || DEFAULT_HOME_PATH,
+          (to.query?.redirect as string) ||
+            userStore.userInfo?.homePath ||
+            DEFAULT_HOME_PATH,
         );
       }
       return true;
@@ -70,7 +72,10 @@ function setupAccessGuard(router: Router) {
         return {
           path: LOGIN_PATH,
           // 如不需要,直接删除 query
-          query: { redirect: encodeURIComponent(to.fullPath) },
+          query:
+            to.fullPath === DEFAULT_HOME_PATH
+              ? {}
+              : { redirect: encodeURIComponent(to.fullPath) },
           // 携带当前跳转的页面,登录后重新跳转该页面
           replace: true,
         };
@@ -100,7 +105,10 @@ function setupAccessGuard(router: Router) {
     accessStore.setAccessMenus(accessibleMenus);
     accessStore.setAccessRoutes(accessibleRoutes);
     accessStore.setIsAccessChecked(true);
-    const redirectPath = (from.query.redirect ?? to.fullPath) as string;
+    const redirectPath = (from.query.redirect ??
+      (to.path === DEFAULT_HOME_PATH
+        ? userInfo.homePath || DEFAULT_HOME_PATH
+        : to.fullPath)) as string;
 
     return {
       ...router.resolve(decodeURIComponent(redirectPath)),

+ 6 - 0
apps/web-baicai/src/utils/utils.ts

@@ -93,3 +93,9 @@ export const getLeafNodeIds = (treeData: any) => {
   }
   return leafNodeIds;
 };
+
+export function getFileNameWithoutExtension(path: string) {
+  // 使用正则表达式匹配最后一个 / 后面的所有字符,但不包括最后一个点(.)之后的内容
+  const match = path.match(/[^/]+(?=\.[^./]+$|$)/);
+  return match ? match[0].replace(/\.[^/.]+$/, '') : null;
+}

+ 50 - 5
apps/web-baicai/src/views/_core/authentication/code-login.vue

@@ -1,15 +1,37 @@
 <script lang="ts" setup>
-import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui';
+import type { VbenFormSchema } from '@vben/common-ui';
+import type { Recordable } from '@vben/types';
 
-import { computed, ref } from 'vue';
+import { computed, ref, useTemplateRef } from 'vue';
 
 import { AuthenticationCodeLogin, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
+import { message } from 'ant-design-vue';
+
 defineOptions({ name: 'CodeLogin' });
 
 const loading = ref(false);
-
+const CODE_LENGTH = 6;
+const loginRef =
+  useTemplateRef<InstanceType<typeof AuthenticationCodeLogin>>('loginRef');
+function sendCodeApi(phoneNumber: string) {
+  message.loading({
+    content: $t('page.auth.sendingCode'),
+    duration: 0,
+    key: 'sending-code',
+  });
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      message.success({
+        content: $t('page.auth.codeSentTo', [phoneNumber]),
+        duration: 3,
+        key: 'sending-code',
+      });
+      resolve({ code: '123456', phoneNumber });
+    }, 3000);
+  });
+}
 const formSchema = computed((): VbenFormSchema[] => {
   return [
     {
@@ -29,6 +51,7 @@ const formSchema = computed((): VbenFormSchema[] => {
     {
       component: 'VbenPinInput',
       componentProps: {
+        codeLength: CODE_LENGTH,
         createText: (countdown: number) => {
           const text =
             countdown > 0
@@ -36,11 +59,32 @@ const formSchema = computed((): VbenFormSchema[] => {
               : $t('authentication.sendCode');
           return text;
         },
+        handleSendCode: async () => {
+          // 模拟发送验证码
+          // Simulate sending verification code
+          loading.value = true;
+          const formApi = loginRef.value?.getFormApi();
+          if (!formApi) {
+            loading.value = false;
+            throw new Error('formApi is not ready');
+          }
+          await formApi.validateField('phoneNumber');
+          const isPhoneReady = await formApi.isFieldValid('phoneNumber');
+          if (!isPhoneReady) {
+            loading.value = false;
+            throw new Error('Phone number is not Ready');
+          }
+          const { phoneNumber } = await formApi.getValues();
+          await sendCodeApi(phoneNumber);
+          loading.value = false;
+        },
         placeholder: $t('authentication.code'),
       },
       fieldName: 'code',
       label: $t('authentication.code'),
-      rules: z.string().min(1, { message: $t('authentication.codeTip') }),
+      rules: z.string().length(CODE_LENGTH, {
+        message: $t('authentication.codeTip', [CODE_LENGTH]),
+      }),
     },
   ];
 });
@@ -49,7 +93,7 @@ const formSchema = computed((): VbenFormSchema[] => {
  * Asynchronously handle the login process
  * @param values 登录表单数据
  */
-async function handleLogin(values: LoginCodeParams) {
+async function handleLogin(values: Recordable<any>) {
   // eslint-disable-next-line no-console
   console.log(values);
 }
@@ -57,6 +101,7 @@ async function handleLogin(values: LoginCodeParams) {
 
 <template>
   <AuthenticationCodeLogin
+    ref="loginRef"
     :form-schema="formSchema"
     :loading="loading"
     @submit="handleLogin"

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

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

+ 138 - 9
apps/web-ele/src/adapter/component/index.ts

@@ -3,25 +3,30 @@
  * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
  */
 
+import type { Component, SetupContext } from 'vue';
+
 import type { BaseFormComponentType } from '@vben/common-ui';
+import type { Recordable } from '@vben/types';
 
-import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { globalShareState } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
   ElButton,
   ElCheckbox,
+  ElCheckboxButton,
   ElCheckboxGroup,
   ElDatePicker,
   ElDivider,
   ElInput,
   ElInputNumber,
   ElNotification,
+  ElRadio,
+  ElRadioButton,
   ElRadioGroup,
-  ElSelect,
+  ElSelectV2,
   ElSpace,
   ElSwitch,
   ElTimePicker,
@@ -41,10 +46,13 @@ const withDefaultPlaceholder = <T extends Component>(
 
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
+  | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
   | 'Divider'
+  | 'IconPicker'
   | 'Input'
   | 'InputNumber'
   | 'RadioGroup'
@@ -61,9 +69,57 @@ async function initComponentAdapter() {
     // 如果你的组件体积比较大,可以使用异步加载
     // Button: () =>
     // import('xxx').then((res) => res.Button),
-
+    ApiSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: ElSelectV2,
+          loadingSlot: 'loading',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: ElTreeSelect,
+          props: { label: 'label', children: 'children' },
+          nodeKey: 'value',
+          loadingSlot: 'loading',
+          optionsPropName: 'data',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
     Checkbox: ElCheckbox,
-    CheckboxGroup: ElCheckboxGroup,
+    CheckboxGroup: (props, { attrs, slots }) => {
+      let defaultSlot;
+      if (Reflect.has(slots, 'default')) {
+        defaultSlot = slots.default;
+      } else {
+        const { options, isButton } = attrs;
+        if (Array.isArray(options)) {
+          defaultSlot = () =>
+            options.map((option) =>
+              h(isButton ? ElCheckboxButton : ElCheckbox, option),
+            );
+        }
+      }
+      return h(
+        ElCheckboxGroup,
+        { ...props, ...attrs },
+        { ...slots, default: defaultSlot },
+      );
+    },
     // 自定义默认按钮
     DefaultButton: (props, { attrs, slots }) => {
       return h(ElButton, { ...props, attrs, type: 'info' }, slots);
@@ -73,14 +129,87 @@ async function initComponentAdapter() {
       return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
     },
     Divider: ElDivider,
+    IconPicker: (props, { attrs, slots }) => {
+      return h(
+        IconPicker,
+        {
+          iconSlot: 'append',
+          modelValueProp: 'model-value',
+          inputComponent: ElInput,
+          ...props,
+          ...attrs,
+        },
+        slots,
+      );
+    },
     Input: withDefaultPlaceholder(ElInput, 'input'),
     InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
-    RadioGroup: ElRadioGroup,
-    Select: withDefaultPlaceholder(ElSelect, 'select'),
+    RadioGroup: (props, { attrs, slots }) => {
+      let defaultSlot;
+      if (Reflect.has(slots, 'default')) {
+        defaultSlot = slots.default;
+      } else {
+        const { options } = attrs;
+        if (Array.isArray(options)) {
+          defaultSlot = () =>
+            options.map((option) =>
+              h(attrs.isButton ? ElRadioButton : ElRadio, option),
+            );
+        }
+      }
+      return h(
+        ElRadioGroup,
+        { ...props, ...attrs },
+        { ...slots, default: defaultSlot },
+      );
+    },
+    Select: (props, { attrs, slots }) => {
+      return h(ElSelectV2, { ...props, attrs }, slots);
+    },
     Space: ElSpace,
     Switch: ElSwitch,
-    TimePicker: ElTimePicker,
-    DatePicker: ElDatePicker,
+    TimePicker: (props, { attrs, slots }) => {
+      const { name, id, isRange } = props;
+      const extraProps: Recordable<any> = {};
+      if (isRange) {
+        if (name && !Array.isArray(name)) {
+          extraProps.name = [name, `${name}_end`];
+        }
+        if (id && !Array.isArray(id)) {
+          extraProps.id = [id, `${id}_end`];
+        }
+      }
+      return h(
+        ElTimePicker,
+        {
+          ...props,
+          ...attrs,
+          ...extraProps,
+        },
+        slots,
+      );
+    },
+    DatePicker: (props, { attrs, slots }) => {
+      const { name, id, type } = props;
+      const extraProps: Recordable<any> = {};
+      if (type && type.includes('range')) {
+        if (name && !Array.isArray(name)) {
+          extraProps.name = [name, `${name}_end`];
+        }
+        if (id && !Array.isArray(id)) {
+          extraProps.id = [id, `${id}_end`];
+        }
+      }
+      return h(
+        ElDatePicker,
+        {
+          ...props,
+          ...attrs,
+          ...extraProps,
+        },
+        slots,
+      );
+    },
     TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
     Upload: ElUpload,
   };

+ 1 - 0
apps/web-ele/src/adapter/form.ts

@@ -12,6 +12,7 @@ setupVbenForm<ComponentType>({
   config: {
     modelPropNameMap: {
       Upload: 'fileList',
+      CheckboxGroup: 'model-value',
     },
   },
   defineRules: {

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

@@ -7,6 +7,7 @@ import '@vben/styles';
 import '@vben/styles/ele';
 
 import { useTitle } from '@vueuse/core';
+import { ElLoading } from 'element-plus';
 
 import { $t, setupI18n } from '#/locales';
 
@@ -19,6 +20,9 @@ async function bootstrap(namespace: string) {
   await initComponentAdapter();
   const app = createApp(App);
 
+  // 注册Element Plus提供的v-loading指令
+  app.directive('loading', ElLoading.directive);
+
   // 国际化 i18n 配置
   await setupI18n(app);
 

+ 3 - 1
apps/web-ele/src/locales/index.ts

@@ -1,7 +1,9 @@
-import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
 import type { Language } from 'element-plus/es/locale';
 
 import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
 import { ref } from 'vue';
 
 import {

+ 1 - 0
apps/web-ele/src/locales/langs/en-US/demos.json

@@ -1,6 +1,7 @@
 {
   "title": "Demos",
   "elementPlus": "Element Plus",
+  "form": "Form",
   "vben": {
     "title": "Project",
     "about": "About",

+ 1 - 0
apps/web-ele/src/locales/langs/zh-CN/demos.json

@@ -1,6 +1,7 @@
 {
   "title": "演示",
   "elementPlus": "Element Plus",
+  "form": "表单演示",
   "vben": {
     "title": "项目",
     "about": "关于",

+ 11 - 3
apps/web-ele/src/router/guard.ts

@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
     if (coreRouteNames.includes(to.name as string)) {
       if (to.path === LOGIN_PATH && accessStore.accessToken) {
         return decodeURIComponent(
-          (to.query?.redirect as string) || DEFAULT_HOME_PATH,
+          (to.query?.redirect as string) ||
+            userStore.userInfo?.homePath ||
+            DEFAULT_HOME_PATH,
         );
       }
       return true;
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
         return {
           path: LOGIN_PATH,
           // 如不需要,直接删除 query
-          query: { redirect: encodeURIComponent(to.fullPath) },
+          query:
+            to.fullPath === DEFAULT_HOME_PATH
+              ? {}
+              : { redirect: encodeURIComponent(to.fullPath) },
           // 携带当前跳转的页面,登录后重新跳转该页面
           replace: true,
         };
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
     accessStore.setAccessMenus(accessibleMenus);
     accessStore.setAccessRoutes(accessibleRoutes);
     accessStore.setIsAccessChecked(true);
-    const redirectPath = (from.query.redirect ?? to.fullPath) as string;
+    const redirectPath = (from.query.redirect ??
+      (to.path === DEFAULT_HOME_PATH
+        ? userInfo.homePath || DEFAULT_HOME_PATH
+        : to.fullPath)) as string;
 
     return {
       ...router.resolve(decodeURIComponent(redirectPath)),

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

@@ -23,6 +23,14 @@ const routes: RouteRecordRaw[] = [
         path: '/demos/element',
         component: () => import('#/views/demos/element/index.vue'),
       },
+      {
+        meta: {
+          title: $t('demos.form'),
+        },
+        name: 'BasicForm',
+        path: '/demos/form',
+        component: () => import('#/views/demos/form/basic.vue'),
+      },
     ],
   },
 ];

+ 5 - 1
apps/web-ele/src/views/_core/authentication/code-login.vue

@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
 defineOptions({ name: 'CodeLogin' });
 
 const loading = ref(false);
+const CODE_LENGTH = 6;
 
 const formSchema = computed((): VbenFormSchema[] => {
   return [
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
     {
       component: 'VbenPinInput',
       componentProps: {
+        codeLength: CODE_LENGTH,
         createText: (countdown: number) => {
           const text =
             countdown > 0
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
       },
       fieldName: 'code',
       label: $t('authentication.code'),
-      rules: z.string().min(1, { message: $t('authentication.codeTip') }),
+      rules: z.string().length(CODE_LENGTH, {
+        message: $t('authentication.codeTip', [CODE_LENGTH]),
+      }),
     },
   ];
 });

+ 3 - 5
apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 1 - 1
apps/web-ele/src/views/dashboard/analytics/index.vue

@@ -15,10 +15,10 @@ import {
 } from '@vben/icons';
 
 import AnalyticsTrends from './analytics-trends.vue';
-import AnalyticsVisits from './analytics-visits.vue';
 import AnalyticsVisitsData from './analytics-visits-data.vue';
 import AnalyticsVisitsSales from './analytics-visits-sales.vue';
 import AnalyticsVisitsSource from './analytics-visits-source.vue';
+import AnalyticsVisits from './analytics-visits.vue';
 
 const overviewItems: AnalysisOverviewItem[] = [
   {

+ 52 - 44
apps/web-ele/src/views/demos/element/index.vue

@@ -61,49 +61,57 @@ const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
     description="支持多语言,主题功能集成切换等"
     title="Element Plus组件使用演示"
   >
-    <ElCard class="mb-5">
-      <template #header> 按钮 </template>
-      <ElSpace>
-        <ElButton text>Text</ElButton>
-        <ElButton>Default</ElButton>
-        <ElButton type="primary"> Primary </ElButton>
-        <ElButton type="info"> Info </ElButton>
-        <ElButton type="success"> Success </ElButton>
-        <ElButton type="warning"> Warning </ElButton>
-        <ElButton type="danger"> Error </ElButton>
-      </ElSpace>
-    </ElCard>
-    <ElCard class="mb-5">
-      <template #header> Message </template>
-      <ElSpace>
-        <ElButton type="info" @click="info"> 信息 </ElButton>
-        <ElButton type="danger" @click="error"> 错误 </ElButton>
-        <ElButton type="warning" @click="warning"> 警告 </ElButton>
-        <ElButton type="success" @click="success"> 成功 </ElButton>
-      </ElSpace>
-    </ElCard>
-    <ElCard class="mb-5">
-      <template #header> Notification </template>
-      <ElSpace>
-        <ElButton type="info" @click="notify('info')"> 信息 </ElButton>
-        <ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
-        <ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
-        <ElButton type="success" @click="notify('success')"> 成功 </ElButton>
-      </ElSpace>
-    </ElCard>
-    <ElCard class="mb-5">
-      <template #header> Segmented </template>
-      <ElSegmented
-        v-model="segmentedValue"
-        :options="segmentedOptions"
-        size="large"
-      />
-    </ElCard>
-    <ElCard class="mb-5">
-      <ElTable :data="tableData" stripe>
-        <ElTable.TableColumn label="测试列1" prop="prop1" />
-        <ElTable.TableColumn label="测试列2" prop="prop2" />
-      </ElTable>
-    </ElCard>
+    <div class="flex flex-wrap gap-5">
+      <ElCard class="mb-5 w-auto">
+        <template #header> 按钮 </template>
+        <ElSpace>
+          <ElButton text>Text</ElButton>
+          <ElButton>Default</ElButton>
+          <ElButton type="primary"> Primary </ElButton>
+          <ElButton type="info"> Info </ElButton>
+          <ElButton type="success"> Success </ElButton>
+          <ElButton type="warning"> Warning </ElButton>
+          <ElButton type="danger"> Error </ElButton>
+        </ElSpace>
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <template #header> Message </template>
+        <ElSpace>
+          <ElButton type="info" @click="info"> 信息 </ElButton>
+          <ElButton type="danger" @click="error"> 错误 </ElButton>
+          <ElButton type="warning" @click="warning"> 警告 </ElButton>
+          <ElButton type="success" @click="success"> 成功 </ElButton>
+        </ElSpace>
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <template #header> Notification </template>
+        <ElSpace>
+          <ElButton type="info" @click="notify('info')"> 信息 </ElButton>
+          <ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
+          <ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
+          <ElButton type="success" @click="notify('success')"> 成功 </ElButton>
+        </ElSpace>
+      </ElCard>
+      <ElCard class="mb-5 w-auto">
+        <template #header> Segmented </template>
+        <ElSegmented
+          v-model="segmentedValue"
+          :options="segmentedOptions"
+          size="large"
+        />
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <template #header> V-Loading </template>
+        <div class="flex size-72 items-center justify-center" v-loading="true">
+          一些演示的内容
+        </div>
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <ElTable :data="tableData" stripe>
+          <ElTable.TableColumn label="测试列1" prop="prop1" />
+          <ElTable.TableColumn label="测试列2" prop="prop2" />
+        </ElTable>
+      </ElCard>
+    </div>
   </Page>
 </template>

+ 181 - 0
apps/web-ele/src/views/demos/form/basic.vue

@@ -0,0 +1,181 @@
+<script lang="ts" setup>
+import { h } from 'vue';
+
+import { Page } from '@vben/common-ui';
+
+import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
+
+import { useVbenForm } from '#/adapter/form';
+import { getAllMenusApi } from '#/api';
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    // 所有表单项
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  layout: 'horizontal',
+  // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
+  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
+  handleSubmit: (values) => {
+    ElMessage.success(`表单数据:${JSON.stringify(values)}`);
+  },
+  schema: [
+    {
+      // 组件需要在 #/adapter.ts内注册,并加上类型
+      component: 'ApiSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口转options格式
+        afterFetch: (data: { name: string; path: string }[]) => {
+          return data.map((item: any) => ({
+            label: item.name,
+            value: item.path,
+          }));
+        },
+        // 菜单接口
+        api: getAllMenusApi,
+      },
+      // 字段名
+      fieldName: 'api',
+      // 界面显示的label
+      label: 'ApiSelect',
+    },
+    {
+      component: 'ApiTreeSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口
+        api: getAllMenusApi,
+        childrenField: 'children',
+        // 菜单接口转options格式
+        labelField: 'name',
+        valueField: 'path',
+      },
+      // 字段名
+      fieldName: 'apiTree',
+      // 界面显示的label
+      label: 'ApiTreeSelect',
+    },
+    {
+      component: 'Input',
+      fieldName: 'string',
+      label: 'String',
+    },
+    {
+      component: 'InputNumber',
+      fieldName: 'number',
+      label: 'Number',
+    },
+    {
+      component: 'RadioGroup',
+      fieldName: 'radio',
+      label: 'Radio',
+      componentProps: {
+        options: [
+          { value: 'A', label: 'A' },
+          { value: 'B', label: 'B' },
+          { value: 'C', label: 'C' },
+          { value: 'D', label: 'D' },
+          { value: 'E', label: 'E' },
+        ],
+      },
+    },
+    {
+      component: 'RadioGroup',
+      fieldName: 'radioButton',
+      label: 'RadioButton',
+      componentProps: {
+        isButton: true,
+        options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
+          value: v,
+          label: `选项${v}`,
+        })),
+      },
+    },
+    {
+      component: 'CheckboxGroup',
+      fieldName: 'checkbox',
+      label: 'Checkbox',
+      componentProps: {
+        options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
+      },
+    },
+    {
+      component: 'CheckboxGroup',
+      fieldName: 'checkbox1',
+      label: 'Checkbox1',
+      renderComponentContent: () => {
+        return {
+          default: () => {
+            return ['A', 'B', 'C', 'D'].map((v) =>
+              h(ElCheckbox, { label: v, value: v }),
+            );
+          },
+        };
+      },
+    },
+    {
+      component: 'CheckboxGroup',
+      fieldName: 'checkbotton',
+      label: 'CheckBotton',
+      componentProps: {
+        isButton: true,
+        options: [
+          { value: 'A', label: '选项A' },
+          { value: 'B', label: '选项B' },
+          { value: 'C', label: '选项C' },
+        ],
+      },
+    },
+    {
+      component: 'DatePicker',
+      fieldName: 'date',
+      label: 'Date',
+    },
+    {
+      component: 'Select',
+      fieldName: 'select',
+      label: 'Select',
+      componentProps: {
+        filterable: true,
+        options: [
+          { value: 'A', label: '选项A' },
+          { value: 'B', label: '选项B' },
+          { value: 'C', label: '选项C' },
+        ],
+      },
+    },
+  ],
+});
+function setFormValues() {
+  formApi.setValues({
+    string: 'string',
+    number: 123,
+    radio: 'B',
+    radioButton: 'C',
+    checkbox: ['A', 'C'],
+    checkbotton: ['B', 'C'],
+    checkbox1: ['A', 'B'],
+    date: new Date(),
+    select: 'B',
+  });
+}
+</script>
+<template>
+  <Page
+    description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
+    title="表单演示"
+  >
+    <ElCard>
+      <template #header>
+        <div class="flex items-center">
+          <span class="flex-auto">基础表单演示</span>
+          <ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
+        </div>
+      </template>
+      <Form />
+    </ElCard>
+  </Page>
+</template>

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

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

+ 84 - 4
apps/web-naive/src/adapter/component/index.ts

@@ -3,12 +3,13 @@
  * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
  */
 
+import type { Component, SetupContext } from 'vue';
+
 import type { BaseFormComponentType } from '@vben/common-ui';
 
-import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { globalShareState } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -19,6 +20,8 @@ import {
   NDivider,
   NInput,
   NInputNumber,
+  NRadio,
+  NRadioButton,
   NRadioGroup,
   NSelect,
   NSpace,
@@ -42,10 +45,13 @@ const withDefaultPlaceholder = <T extends Component>(
 
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
+  | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
   | 'Divider'
+  | 'IconPicker'
   | 'Input'
   | 'InputNumber'
   | 'RadioGroup'
@@ -63,8 +69,54 @@ async function initComponentAdapter() {
     // Button: () =>
     // import('xxx').then((res) => res.Button),
 
+    ApiSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: NSelect,
+          modelPropName: 'value',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiComponent,
+        {
+          placeholder: $t('ui.placeholder.select'),
+          ...props,
+          ...attrs,
+          component: NTreeSelect,
+          nodeKey: 'value',
+          loadingSlot: 'arrow',
+          keyField: 'value',
+          modelPropName: 'value',
+          optionsPropName: 'options',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
     Checkbox: NCheckbox,
-    CheckboxGroup: NCheckboxGroup,
+    CheckboxGroup: (props, { attrs, slots }) => {
+      let defaultSlot;
+      if (Reflect.has(slots, 'default')) {
+        defaultSlot = slots.default;
+      } else {
+        const { options } = attrs;
+        if (Array.isArray(options)) {
+          defaultSlot = () => options.map((option) => h(NCheckbox, option));
+        }
+      }
+      return h(
+        NCheckboxGroup,
+        { ...props, ...attrs },
+        { default: defaultSlot },
+      );
+    },
     DatePicker: NDatePicker,
     // 自定义默认按钮
     DefaultButton: (props, { attrs, slots }) => {
@@ -75,9 +127,37 @@ async function initComponentAdapter() {
       return h(NButton, { ...props, attrs, type: 'primary' }, slots);
     },
     Divider: NDivider,
+    IconPicker: (props, { attrs, slots }) => {
+      return h(
+        IconPicker,
+        { iconSlot: 'suffix', inputComponent: NInput, ...props, ...attrs },
+        slots,
+      );
+    },
     Input: withDefaultPlaceholder(NInput, 'input'),
     InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
-    RadioGroup: NRadioGroup,
+    RadioGroup: (props, { attrs, slots }) => {
+      let defaultSlot;
+      if (Reflect.has(slots, 'default')) {
+        defaultSlot = slots.default;
+      } else {
+        const { options } = attrs;
+        if (Array.isArray(options)) {
+          defaultSlot = () =>
+            options.map((option) =>
+              h(attrs.isButton ? NRadioButton : NRadio, option),
+            );
+        }
+      }
+      const groupRender = h(
+        NRadioGroup,
+        { ...props, ...attrs },
+        { default: defaultSlot },
+      );
+      return attrs.isButton
+        ? h(NSpace, { vertical: true }, () => groupRender)
+        : groupRender;
+    },
     Select: withDefaultPlaceholder(NSelect, 'select'),
     Space: NSpace,
     Switch: NSwitch,

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

@@ -10,8 +10,6 @@ import { $t } from '@vben/locales';
 
 setupVbenForm<ComponentType>({
   config: {
-    // naive-ui组件不接受onChang事件,所以需要禁用
-    disabledOnChangeListener: true,
     // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
     emptyStateValue: null,
     baseModelPropName: 'value',

+ 2 - 2
apps/web-naive/src/locales/index.ts

@@ -1,7 +1,7 @@
-import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
-
 import type { App } from 'vue';
 
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
 import {
   $t,
   setupI18n as coreSetup,

+ 1 - 0
apps/web-naive/src/locales/langs/en-US/demos.json

@@ -2,6 +2,7 @@
   "title": "Demos",
   "naive": "Naive UI",
   "table": "Table",
+  "form": "Form",
   "vben": {
     "title": "Project",
     "about": "About",

+ 1 - 0
apps/web-naive/src/locales/langs/zh-CN/demos.json

@@ -2,6 +2,7 @@
   "title": "演示",
   "naive": "Naive UI",
   "table": "Table",
+  "form": "表单",
   "vben": {
     "title": "项目",
     "about": "关于",

+ 11 - 3
apps/web-naive/src/router/guard.ts

@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
     if (coreRouteNames.includes(to.name as string)) {
       if (to.path === LOGIN_PATH && accessStore.accessToken) {
         return decodeURIComponent(
-          (to.query?.redirect as string) || DEFAULT_HOME_PATH,
+          (to.query?.redirect as string) ||
+            userStore.userInfo?.homePath ||
+            DEFAULT_HOME_PATH,
         );
       }
       return true;
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
         return {
           path: LOGIN_PATH,
           // 如不需要,直接删除 query
-          query: { redirect: encodeURIComponent(to.fullPath) },
+          query:
+            to.fullPath === DEFAULT_HOME_PATH
+              ? {}
+              : { redirect: encodeURIComponent(to.fullPath) },
           // 携带当前跳转的页面,登录后重新跳转该页面
           replace: true,
         };
@@ -101,7 +106,10 @@ function setupAccessGuard(router: Router) {
     accessStore.setAccessMenus(accessibleMenus);
     accessStore.setAccessRoutes(accessibleRoutes);
     accessStore.setIsAccessChecked(true);
-    const redirectPath = (from.query.redirect ?? to.fullPath) as string;
+    const redirectPath = (from.query.redirect ??
+      (to.path === DEFAULT_HOME_PATH
+        ? userInfo.homePath || DEFAULT_HOME_PATH
+        : to.fullPath)) as string;
 
     return {
       ...router.resolve(decodeURIComponent(redirectPath)),

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

@@ -31,6 +31,14 @@ const routes: RouteRecordRaw[] = [
         path: '/demos/table',
         component: () => import('#/views/demos/table/index.vue'),
       },
+      {
+        meta: {
+          title: $t('demos.form'),
+        },
+        name: 'Form',
+        path: '/demos/form',
+        component: () => import('#/views/demos/form/basic.vue'),
+      },
     ],
   },
 ];

+ 5 - 1
apps/web-naive/src/views/_core/authentication/code-login.vue

@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
 defineOptions({ name: 'CodeLogin' });
 
 const loading = ref(false);
+const CODE_LENGTH = 6;
 
 const formSchema = computed((): VbenFormSchema[] => {
   return [
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
     {
       component: 'VbenPinInput',
       componentProps: {
+        codeLength: CODE_LENGTH,
         createText: (countdown: number) => {
           const text =
             countdown > 0
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
       },
       fieldName: 'code',
       label: $t('authentication.code'),
-      rules: z.string().min(1, { message: $t('authentication.codeTip') }),
+      rules: z.string().length(CODE_LENGTH, {
+        message: $t('authentication.codeTip', [CODE_LENGTH]),
+      }),
     },
   ];
 });

+ 3 - 5
apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 3 - 5
apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue

@@ -1,11 +1,9 @@
 <script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
 import { onMounted, ref } from 'vue';
 
-import {
-  EchartsUI,
-  type EchartsUIType,
-  useEcharts,
-} from '@vben/plugins/echarts';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 
 const chartRef = ref<EchartsUIType>();
 const { renderEcharts } = useEcharts(chartRef);

+ 1 - 1
apps/web-naive/src/views/dashboard/analytics/index.vue

@@ -15,10 +15,10 @@ import {
 } from '@vben/icons';
 
 import AnalyticsTrends from './analytics-trends.vue';
-import AnalyticsVisits from './analytics-visits.vue';
 import AnalyticsVisitsData from './analytics-visits-data.vue';
 import AnalyticsVisitsSales from './analytics-visits-sales.vue';
 import AnalyticsVisitsSource from './analytics-visits-source.vue';
+import AnalyticsVisits from './analytics-visits.vue';
 
 const overviewItems: AnalysisOverviewItem[] = [
   {

+ 143 - 0
apps/web-naive/src/views/demos/form/basic.vue

@@ -0,0 +1,143 @@
+<script lang="ts" setup>
+import { Page } from '@vben/common-ui';
+
+import { NButton, NCard, useMessage } from 'naive-ui';
+
+import { useVbenForm } from '#/adapter/form';
+import { getAllMenusApi } from '#/api';
+
+const message = useMessage();
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    // 所有表单项
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  layout: 'horizontal',
+  // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
+  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
+  handleSubmit: (values) => {
+    message.success(`表单数据:${JSON.stringify(values)}`);
+  },
+  schema: [
+    {
+      // 组件需要在 #/adapter.ts内注册,并加上类型
+      component: 'ApiSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口转options格式
+        afterFetch: (data: { name: string; path: string }[]) => {
+          return data.map((item: any) => ({
+            label: item.name,
+            value: item.path,
+          }));
+        },
+        // 菜单接口
+        api: getAllMenusApi,
+      },
+      // 字段名
+      fieldName: 'api',
+      // 界面显示的label
+      label: 'ApiSelect',
+    },
+    {
+      component: 'ApiTreeSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口
+        api: getAllMenusApi,
+        childrenField: 'children',
+        // 菜单接口转options格式
+        labelField: 'name',
+        valueField: 'path',
+      },
+      // 字段名
+      fieldName: 'apiTree',
+      // 界面显示的label
+      label: 'ApiTreeSelect',
+    },
+    {
+      component: 'Input',
+      fieldName: 'string',
+      label: 'String',
+    },
+    {
+      component: 'InputNumber',
+      fieldName: 'number',
+      label: 'Number',
+    },
+    {
+      component: 'RadioGroup',
+      fieldName: 'radio',
+      label: 'Radio',
+      componentProps: {
+        options: [
+          { value: 'A', label: 'A' },
+          { value: 'B', label: 'B' },
+          { value: 'C', label: 'C' },
+          { value: 'D', label: 'D' },
+          { value: 'E', label: 'E' },
+        ],
+      },
+    },
+    {
+      component: 'RadioGroup',
+      fieldName: 'radioButton',
+      label: 'RadioButton',
+      componentProps: {
+        isButton: true,
+        class: 'flex flex-wrap', // 如果选项过多,可以添加class来自动折叠
+        options: [
+          { value: 'A', label: '选项A' },
+          { value: 'B', label: '选项B' },
+          { value: 'C', label: '选项C' },
+          { value: 'D', label: '选项D' },
+          { value: 'E', label: '选项E' },
+          { value: 'F', label: '选项F' },
+        ],
+      },
+    },
+    {
+      component: 'CheckboxGroup',
+      fieldName: 'checkbox',
+      label: 'Checkbox',
+      componentProps: {
+        options: [
+          { value: 'A', label: '选项A' },
+          { value: 'B', label: '选项B' },
+          { value: 'C', label: '选项C' },
+        ],
+      },
+    },
+    {
+      component: 'DatePicker',
+      fieldName: 'date',
+      label: 'Date',
+    },
+  ],
+});
+function setFormValues() {
+  formApi.setValues({
+    string: 'string',
+    number: 123,
+    radio: 'B',
+    radioButton: 'C',
+    checkbox: ['A', 'C'],
+    date: Date.now(),
+  });
+}
+</script>
+<template>
+  <Page
+    description="表单适配器重新包装了CheckboxGroup和RadioGroup,可以通过options属性传递选项数据(选项数据将作为子组件的属性)"
+    title="表单演示"
+  >
+    <NCard title="基础表单">
+      <template #header-extra>
+        <NButton type="primary" @click="setFormValues">设置表单值</NButton>
+      </template>
+      <Form />
+    </NCard>
+  </Page>
+</template>

+ 2 - 1
apps/web-naive/src/views/demos/naive/index.vue

@@ -1,7 +1,8 @@
 <script lang="ts" setup>
+import type { NotificationType } from 'naive-ui';
+
 import { Page } from '@vben/common-ui';
 
-import { type NotificationType } from 'naive-ui';
 import { NButton, NCard, NSpace, useMessage, useNotification } from 'naive-ui';
 
 const notification = useNotification();

+ 3 - 1
docs/.vitepress/config/en.mts

@@ -1,4 +1,6 @@
-import { type DefaultTheme, defineConfig } from 'vitepress';
+import type { DefaultTheme } from 'vitepress';
+
+import { defineConfig } from 'vitepress';
 
 import { version } from '../../../package.json';
 

+ 21 - 1
docs/.vitepress/config/zh.mts

@@ -1,4 +1,6 @@
-import { type DefaultTheme, defineConfig } from 'vitepress';
+import type { DefaultTheme } from 'vitepress';
+
+import { defineConfig } from 'vitepress';
 
 import { version } from '../../../package.json';
 
@@ -148,10 +150,24 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
         },
       ],
     },
+    {
+      collapsed: false,
+      text: '布局组件',
+      items: [
+        {
+          link: 'layout-ui/page',
+          text: 'Page 页面',
+        },
+      ],
+    },
     {
       collapsed: false,
       text: '通用组件',
       items: [
+        {
+          link: 'common-ui/vben-api-component',
+          text: 'ApiComponent Api组件包装器',
+        },
         {
           link: 'common-ui/vben-modal',
           text: 'Modal 模态框',
@@ -172,6 +188,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
           link: 'common-ui/vben-count-to-animator',
           text: 'CountToAnimator 数字动画',
         },
+        {
+          link: 'common-ui/vben-ellipsis-text',
+          text: 'EllipsisText 省略文本',
+        },
       ],
     },
   ];

+ 0 - 1
docs/.vitepress/theme/components/site-layout.vue

@@ -10,7 +10,6 @@ import {
 
 // import { useAntdDesignTokens } from '@vben/hooks';
 // import { initPreferences } from '@vben/preferences';
-
 import { ConfigProvider, theme } from 'ant-design-vue';
 import mediumZoom from 'medium-zoom';
 import { useRoute } from 'vitepress';

+ 1 - 1
docs/package.json

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

+ 2 - 1
docs/src/_env/adapter/component.ts

@@ -3,9 +3,10 @@
  * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
  */
 
+import type { Component, SetupContext } from 'vue';
+
 import type { BaseFormComponentType } from '@vben/common-ui';
 
-import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
 import { globalShareState } from '@vben/common-ui';

+ 0 - 2
docs/src/_env/adapter/form.ts

@@ -14,8 +14,6 @@ initComponentAdapter();
 setupVbenForm<ComponentType>({
   config: {
     baseModelPropName: 'value',
-    // naive-ui组件不接受onChang事件,所以需要禁用
-    disabledOnChangeListener: true,
     // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
     emptyStateValue: null,
     modelPropNameMap: {

+ 152 - 0
docs/src/components/common-ui/vben-api-component.md

@@ -0,0 +1,152 @@
+---
+outline: deep
+---
+
+# Vben ApiComponent Api组件包装器
+
+框架提供的API“包装器”,它一般不独立使用,主要用于包装其它组件,为目标组件提供自动获取远程数据的能力,但仍然保持了目标组件的原始用法。
+
+::: info 写在前面
+
+我们在各个应用的组件适配器中,使用ApiComponent包装了Select、TreeSelect组件,使得这些组件可以自动获取远程数据并生成选项。其它类似的组件(比如Cascader)如有需要也可以参考示例代码自行进行包装。
+
+:::
+
+## 基础用法
+
+通过 `component` 传入其它组件的定义,并配置相关的其它属性(主要是一些名称映射)。包装组件将通过`api`获取数据(`beforerFetch`、`afterFetch`将分别在`api`运行前、运行后被调用),使用`resultField`从中提取数组,使用`valueField`、`labelField`等来从数据中提取value和label(如果提供了`childrenField`,会将其作为树形结构递归处理每一级数据),之后将处理好的数据通过`optionsPropName`指定的属性传递给目标组件。
+
+::: details 包装级联选择器,点击下拉时开始加载远程数据
+
+```vue
+<script lang="ts" setup>
+import { ApiComponent } from '@vben/common-ui';
+
+import { Cascader } from 'ant-design-vue';
+
+const treeData: Record<string, any> = [
+  {
+    label: '浙江',
+    value: 'zhejiang',
+    children: [
+      {
+        value: 'hangzhou',
+        label: '杭州',
+        children: [
+          {
+            value: 'xihu',
+            label: '西湖',
+          },
+          {
+            value: 'sudi',
+            label: '苏堤',
+          },
+        ],
+      },
+      {
+        value: 'jiaxing',
+        label: '嘉兴',
+        children: [
+          {
+            value: 'wuzhen',
+            label: '乌镇',
+          },
+          {
+            value: 'meihuazhou',
+            label: '梅花洲',
+          },
+        ],
+      },
+      {
+        value: 'zhoushan',
+        label: '舟山',
+        children: [
+          {
+            value: 'putuoshan',
+            label: '普陀山',
+          },
+          {
+            value: 'taohuadao',
+            label: '桃花岛',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label: '江苏',
+    value: 'jiangsu',
+    children: [
+      {
+        value: 'nanjing',
+        label: '南京',
+        children: [
+          {
+            value: 'zhonghuamen',
+            label: '中华门',
+          },
+          {
+            value: 'zijinshan',
+            label: '紫金山',
+          },
+          {
+            value: 'yuhuatai',
+            label: '雨花台',
+          },
+        ],
+      },
+    ],
+  },
+];
+/**
+ * 模拟请求接口
+ */
+function fetchApi(): Promise<Record<string, any>> {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(treeData);
+    }, 1000);
+  });
+}
+</script>
+<template>
+  <ApiComponent
+    :api="fetchApi"
+    :component="Cascader"
+    :immediate="false"
+    children-field="children"
+    loading-slot="suffixIcon"
+    visible-event="onDropdownVisibleChange"
+  />
+</template>
+```
+
+:::
+
+## API
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| component | 欲包装的组件 | `Component` | - |
+| numberToString | 是否将value从数字转为string | `boolean` | `false` |
+| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - |
+| params | 传递给api的参数 | `Record<string, any>` | - |
+| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - |
+| labelField | label字段名 | `string` | `label` |
+| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` |
+| valueField | value字段名 | `string` | `value` |
+| 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` | - |
+
+```
+
+```

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

@@ -54,6 +54,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`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
 
 :::
 
@@ -74,12 +75,16 @@ const [Drawer, drawerApi] = useVbenDrawer({
 
 | 属性名 | 描述 | 类型 | 默认值 |
 | --- | --- | --- | --- |
+| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
+| connectedComponent | 连接另一个Modal组件 | `Component` | - |
+| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
 | title | 标题 | `string\|slot` | - |
 | titleTooltip | 标题提示信息 | `string\|slot` | - |
 | description | 描述信息 | `string\|slot` | - |
 | isOpen | 弹窗打开状态 | `boolean` | `false` |
 | loading | 弹窗加载状态 | `boolean` | `false` |
 | closable | 显示关闭按钮 | `boolean` | `true` |
+| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` |
 | modal | 显示遮罩 | `boolean` | `true` |
 | header | 显示header | `boolean` | `true` |
 | footer | 显示footer | `boolean\|slot` | `true` |
@@ -95,17 +100,26 @@ const [Drawer, drawerApi] = useVbenDrawer({
 | contentClass | modal内容区域的class | `string` | - |
 | footerClass | modal底部区域的class | `string` | - |
 | headerClass | modal顶部区域的class | `string` | - |
+| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
+
+::: info appendToMain
+
+`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
+
+:::
 
 ### Event
 
 以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
 
-| 事件名 | 描述 | 类型 |
-| --- | --- | --- |
-| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
-| onCancel | 点击取消按钮触发 | `()=>void` |
-| onConfirm | 点击确认按钮触发 | `()=>void` |
-| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
+| 事件名 | 描述 | 类型 | 版本限制 |
+| --- | --- | --- | --- |
+| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- |
+| onCancel | 点击取消按钮触发 | `()=>void` | --- |
+| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
+| onConfirm | 点击确认按钮触发 | `()=>void` | --- |
+| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
+| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |
 
 ### Slots
 
@@ -116,6 +130,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
 | default        | 默认插槽 - 弹窗内容 |
 | prepend-footer | 取消按钮左侧        |
 | append-footer  | 取消按钮右侧        |
+| close-icon     | 关闭按钮图标        |
+| extra          | 额外内容(标题右侧)  |
 
 ### modalApi
 

+ 56 - 0
docs/src/components/common-ui/vben-ellipsis-text.md

@@ -0,0 +1,56 @@
+---
+outline: deep
+---
+
+# Vben EllipsisText 省略文本
+
+框架提供的文本展示组件,可配置超长省略、tooltip提示、展开收起等功能。
+
+> 如果文档内没有参数说明,可以尝试在在线示例内寻找
+
+## 基础用法
+
+通过默认插槽设置文本内容,`maxWidth`属性设置最大宽度。
+
+<DemoPreview dir="demos/vben-ellipsis-text/line" />
+
+## 可折叠的文本块
+
+通过`line`设置折叠后的行数,`expand`属性设置是否支持展开收起。
+
+<DemoPreview dir="demos/vben-ellipsis-text/expand" />
+
+## 自定义提示浮层
+
+通过名为`tooltip`的插槽定制提示信息。
+
+<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
+
+## API
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| expand | 支持点击展开或收起 | `boolean` | `false` |
+| line | 文本最大行数 | `number` | `1` |
+| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
+| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
+| tooltip | 启用文本提示 | `boolean` | `true` |
+| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
+| tooltipColor | 提示文本的颜色 | `string` | - |
+| tooltipFontSize | 提示文本的大小 | `string` | - |
+| tooltipMaxWidth | 提示浮层的最大宽度。如不设置则保持与文本宽度一致 | `number` | - |
+| tooltipOverlayStyle | 提示框内容区域样式 | `CSSProperties` | `{ textAlign: 'justify' }` |
+
+### Events
+
+| 事件名       | 描述         | 类型                       |
+| ------------ | ------------ | -------------------------- |
+| expandChange | 展开状态改变 | `(isExpand:boolean)=>void` |
+
+### Slots
+
+| 插槽名  | 描述                             |
+| ------- | -------------------------------- |
+| tooltip | 启用文本提示时,用来定制提示内容 |

+ 33 - 10
docs/src/components/common-ui/vben-form.md

@@ -87,7 +87,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
 import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { globalShareState } from '@vben/common-ui';
+import { globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -149,6 +149,7 @@ export type ComponentType =
   | 'TimePicker'
   | 'TreeSelect'
   | 'Upload'
+  | 'IconPicker';
   | BaseFormComponentType;
 
 async function initComponentAdapter() {
@@ -166,6 +167,7 @@ async function initComponentAdapter() {
       return h(Button, { ...props, attrs, type: 'default' }, slots);
     },
     Divider,
+    IconPicker,
     Input: withDefaultPlaceholder(Input, 'input'),
     InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
     InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
@@ -285,6 +287,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
 | setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
 | getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
 | validate | 表单校验 | `()=>Promise<void>` |
+| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
+| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
 | resetValidate | 重置表单校验 | `()=>Promise<void>` |
 | updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
 | setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
@@ -304,16 +308,19 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
 | actionWrapperClass | 表单操作区域class | `any` | - |
 | handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
 | handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
+| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>,) => void` | - |
+| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
 | resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
 | submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
 | showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
-| collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
+| collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
 | collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
 | collapsedRows | 折叠时保持的行数 | `number` | `1` |
-| fieldMappingTime | 用于将表单内时间区域的应设成 2 个字段 | `[string, [string, string], string?][]` | - |
+| fieldMappingTime | 用于将表单内时间区域组件的数组值映射成 2 个字段 | `[string, [string, string], string?][]` | - |
 | commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
-| schema | 表单项的每一项配置 | `FormSchema` | - |
+| schema | 表单项的每一项配置 | `FormSchema[]` | - |
 | submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
+| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
 
 ### TS 类型说明
 
@@ -350,10 +357,21 @@ export interface FormCommonConfig {
    * 所有表单项的props
    */
   componentProps?: ComponentProps;
+  /**
+   * 是否紧凑模式(移除表单底部为显示校验错误信息所预留的空间)。
+   * 在有设置校验规则的场景下,建议不要将其设置为true
+   * 默认为false。但用作表格的搜索表单时,默认为true
+   * @default false
+   */
+  compact?: boolean;
   /**
    * 所有表单项的控件样式
    */
   controlClass?: string;
+  /**
+   * 在表单项的Label后显示一个冒号
+   */
+  colon?: boolean;
   /**
    * 所有表单项的禁用状态
    * @default false
@@ -413,7 +431,7 @@ export interface FormSchema<
   dependencies?: FormItemDependencies;
   /** 描述 */
   description?: string;
-  /** 字段名 */
+  /** 字段名,也作为自定义插槽的名称 */
   fieldName: string;
   /** 帮助信息 */
   help?: string;
@@ -436,7 +454,7 @@ export interface FormSchema<
 
 ```ts
 dependencies: {
-  // 只有当 name 字段的值变化时,才会触发联动
+  // 触发字段。只有这些字段值变动时,联动才会触发
   triggerFields: ['name'],
   // 动态判断当前字段是否需要显示,不显示则直接销毁
   if(values,formApi){},
@@ -457,11 +475,11 @@ dependencies: {
 
 ### 表单校验
 
-表单联动需要通过 schema 内的 `rules` 属性进行配置。
+表单校验需要通过 schema 内的 `rules` 属性进行配置。
 
-rules的值可以是一个字符串,也可以是一个zod的schema。
+rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。
 
-#### 字符串
+#### 预定义的校验规则
 
 ```ts
 // 表示字段必填,默认会根据适配器的required进行国际化
@@ -487,11 +505,16 @@ import { z } from '#/adapter/form';
   rules: z.string().min(1, { message: '请输入字符串' });
 }
 
-// 可选,并且携带默认值
+// 可选(可以是undefined),并且携带默认值。注意zod的optional不包括空字符串''
 {
    rules: z.string().default('默认值').optional(),
 }
 
+// 可以是空字符串、undefined或者一个邮箱地址
+{
+  rules: z.union(z.string().email().optional(), z.literal(""))
+}
+
 // 复杂校验
 {
    z.string().min(1, { message: "请输入" })

+ 11 - 0
docs/src/components/common-ui/vben-modal.md

@@ -60,6 +60,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`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
 
 :::
 
@@ -80,6 +81,9 @@ const [Modal, modalApi] = useVbenModal({
 
 | 属性名 | 描述 | 类型 | 默认值 |
 | --- | --- | --- | --- |
+| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
+| connectedComponent | 连接另一个Modal组件 | `Component` | - |
+| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
 | title | 标题 | `string\|slot` | - |
 | titleTooltip | 标题提示信息 | `string\|slot` | - |
 | description | 描述信息 | `string\|slot` | - |
@@ -106,6 +110,13 @@ const [Modal, modalApi] = useVbenModal({
 | footerClass | modal底部区域的class | `string` | - |
 | headerClass | modal顶部区域的class | `string` | - |
 | bordered | 是否显示border | `boolean` | `false` |
+| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
+
+::: info appendToMain
+
+`appendToMain`可以指定将弹窗挂载到内容区域,打开这种弹窗时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,弹窗会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。
+
+:::
 
 ### Event
 

+ 12 - 8
docs/src/components/common-ui/vben-vxe-table.md

@@ -165,6 +165,8 @@ vxeUI.renderer.add('CellLink', {
 
 **表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。
 
+当启用了表单搜索时,可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。
+
 <DemoPreview dir="demos/vben-vxe-table/form" />
 
 ## 单元格编辑
@@ -215,14 +217,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
 
 useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表单的方法。
 
-| 方法名 | 描述 | 类型 |
-| --- | --- | --- |
-| setLoading | 设置loading状态 | `(loading)=>void` |
-| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` |
-| reload | 重载表格,会进行初始化 | `(params:any)=>void` |
-| query | 重载表格,会保留当前分页 | `(params:any)=>void` |
-| grid | vxe-table grid实例 | `VxeGridInstance` |
-| formApi | vbenForm api实例 | `FormApi` |
+| 方法名 | 描述 | 类型 | 说明 |
+| --- | --- | --- | --- |
+| setLoading | 设置loading状态 | `(loading)=>void` | - |
+| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` | - |
+| reload | 重载表格,会进行初始化 | `(params:any)=>void` | - |
+| query | 重载表格,会保留当前分页 | `(params:any)=>void` | - |
+| grid | vxe-table grid实例 | `VxeGridInstance` | - |
+| formApi | vbenForm api实例 | `FormApi` | - |
+| toggleSearchForm | 设置搜索表单显示状态 | `(show?: boolean)=>boolean` | 当省略参数时,则将表单在显示和隐藏两种状态之间切换 |
 
 ## Props
 
@@ -236,3 +239,4 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
 | gridOptions    | grid组件的参数     | `VxeTableGridProps` |
 | gridEvents     | grid组件的触发的⌚️ | `VxeGridListeners`  |
 | formOptions    | 表单参数           | `VbenFormProps`     |
+| showSearchForm | 是否显示搜索表单   | `boolean`           |

+ 4 - 0
docs/src/components/introduction.md

@@ -6,6 +6,10 @@
 
 :::
 
+## 布局组件
+
+布局组件一般在页面内容区域用作顶层容器组件,提供一些统一的布局样式和基本功能。
+
 ## 通用组件
 
 通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。

+ 44 - 0
docs/src/components/layout-ui/page.md

@@ -0,0 +1,44 @@
+---
+outline: deep
+---
+
+# Page 常规页面组件
+
+提供一个常规页面布局的组件,包括头部、内容区域、底部三个部分。
+
+::: info 写在前面
+
+本组件是一个基本布局组件。如果有更多的通用页面布局需求(比如双列布局等),可以根据实际需求自行封装。
+
+:::
+
+## 基础用法
+
+将`Page`作为你的业务页面的根组件即可。
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 | 说明 |
+| --- | --- | --- | --- | --- |
+| title | 页面标题 | `string\|slot` | - | - |
+| description | 页面描述(标题下的内容) | `string\|slot` | - | - |
+| contentClass | 内容区域的class | `string` | - | - |
+| headerClass | 头部区域的class | `string` | - | - |
+| footerClass | 底部区域的class | `string` | - | - |
+| autoContentHeight | 自动调整内容区域的高度 | `boolean` | `false` | - |
+
+::: tip 注意
+
+如果`title`、`description`、`extra`三者均未提供有效内容(通过`props`或者`slots`均可),则页面头部区域不会渲染。
+
+:::
+
+### Slots
+
+| 插槽名称    | 描述         |
+| ----------- | ------------ |
+| default     | 页面内容     |
+| title       | 页面标题     |
+| description | 页面描述     |
+| extra       | 页面头部右侧 |
+| footer      | 页面底部     |

+ 100 - 0
docs/src/demos/vben-api-component/cascader/index.vue

@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+import { ApiComponent } from '@vben/common-ui';
+
+import { Cascader } from 'ant-design-vue';
+
+const treeData: Record<string, any> = [
+  {
+    label: '浙江',
+    value: 'zhejiang',
+    children: [
+      {
+        value: 'hangzhou',
+        label: '杭州',
+        children: [
+          {
+            value: 'xihu',
+            label: '西湖',
+          },
+          {
+            value: 'sudi',
+            label: '苏堤',
+          },
+        ],
+      },
+      {
+        value: 'jiaxing',
+        label: '嘉兴',
+        children: [
+          {
+            value: 'wuzhen',
+            label: '乌镇',
+          },
+          {
+            value: 'meihuazhou',
+            label: '梅花洲',
+          },
+        ],
+      },
+      {
+        value: 'zhoushan',
+        label: '舟山',
+        children: [
+          {
+            value: 'putuoshan',
+            label: '普陀山',
+          },
+          {
+            value: 'taohuadao',
+            label: '桃花岛',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label: '江苏',
+    value: 'jiangsu',
+    children: [
+      {
+        value: 'nanjing',
+        label: '南京',
+        children: [
+          {
+            value: 'zhonghuamen',
+            label: '中华门',
+          },
+          {
+            value: 'zijinshan',
+            label: '紫金山',
+          },
+          {
+            value: 'yuhuatai',
+            label: '雨花台',
+          },
+        ],
+      },
+    ],
+  },
+];
+/**
+ * 模拟请求接口
+ */
+function fetchApi(): Promise<Record<string, any>> {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(treeData);
+    }, 1000);
+  });
+}
+</script>
+<template>
+  <ApiComponent
+    :api="fetchApi"
+    :component="Cascader"
+    :immediate="false"
+    children-field="children"
+    loading-slot="suffixIcon"
+    visible-event="onDropdownVisibleChange"
+  />
+</template>

文件差異過大導致無法顯示
+ 4 - 0
docs/src/demos/vben-ellipsis-text/expand/index.vue


文件差異過大導致無法顯示
+ 4 - 0
docs/src/demos/vben-ellipsis-text/line/index.vue


+ 14 - 0
docs/src/demos/vben-ellipsis-text/tooltip/index.vue

@@ -0,0 +1,14 @@
+<script lang="ts" setup>
+import { EllipsisText } from '@vben/common-ui';
+</script>
+<template>
+  <EllipsisText :max-width="240">
+    住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
+    <template #tooltip>
+      <div style="text-align: center">
+        《秦皇岛》<br />住在我心里孤独的<br />孤独的海怪 痛苦之王<br />开始厌倦
+        深海的光 停滞的海浪
+      </div>
+    </template>
+  </EllipsisText>
+</template>

+ 7 - 0
docs/src/demos/vben-vxe-table/form/index.vue

@@ -76,6 +76,8 @@ const formOptions: VbenFormProps = {
   submitButtonOptions: {
     content: '查询',
   },
+  // 是否在字段值改变时提交表单
+  submitOnChange: false,
   // 按下回车时是否提交表单
   submitOnEnter: false,
 };
@@ -108,6 +110,11 @@ const gridOptions: VxeGridProps<RowType> = {
       },
     },
   },
+  toolbarConfig: {
+    // 是否显示搜索表单控制按钮
+    // @ts-ignore 正式环境时有完整的类型声明
+    search: true,
+  },
 };
 
 const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });

+ 1 - 0
docs/src/en/guide/essentials/settings.md

@@ -217,6 +217,7 @@ const defaultPreferences: Preferences = {
     globalSearch: true,
   },
   sidebar: {
+    autoActivateChild: false,
     collapsed: false,
     collapsedShowTitle: false,
     enable: true,

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

@@ -240,6 +240,7 @@ const defaultPreferences: Preferences = {
     globalSearch: true,
   },
   sidebar: {
+    autoActivateChild: false,
     collapsed: false,
     collapsedShowTitle: false,
     enable: true,

+ 1 - 1
docs/src/guide/in-depth/ui-framework.md

@@ -4,7 +4,7 @@
 
 ## 新增组件库应用
 
-如果你想用其他别的组件库,你只需要按下步骤进行操作:
+如果你想用其他别的组件库,你只需要按下步骤进行操作:
 
 1. 在`apps`内创建一个新的文件夹,例如`apps/web-xxx`。
 2. 更改`apps/web-xxx/package.json`的`name`字段为`web-xxx`。

+ 1 - 1
docs/src/guide/introduction/quick-start.md

@@ -67,7 +67,7 @@ pnpm install
 ::: tip 注意
 
 - 项目只支持使用 `pnpm` 进行依赖安装,默认会使用 `corepack` 来安装指定版本的 `pnpm`。:
-- 如果你的网络环境无法访问npm源,你可以设置系统的环境变量`COREPACK_REGISTRY=https://registry.npmmirror.com`,然后再执行`pnpm install`。
+- 如果你的网络环境无法访问npm源,你可以设置系统的环境变量`COREPACK_NPM_REGISTRY=https://registry.npmmirror.com`,然后再执行`pnpm install`。
 - 如果你不想使用`corepack`,你需要禁用`corepack`,然后使用你自己的`pnpm`进行安装。
 
 :::

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

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

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

@@ -10,6 +10,7 @@ export async function importPluginConfig(): Promise<Linter.Config[]> {
         import: pluginImport,
       },
       rules: {
+        'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
         'import/first': 'error',
         'import/newline-after-import': 'error',
         'import/no-duplicates': 'error',

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

@@ -1,6 +1,5 @@
 import type { Linter } from 'eslint';
 
-// @ts-expect-error - no types
 import js from '@eslint/js';
 import pluginUnusedImports from 'eslint-plugin-unused-imports';
 import globals from 'globals';

+ 19 - 42
internal/lint-configs/eslint-config/src/configs/perfectionist.ts

@@ -1,8 +1,13 @@
 import type { Linter } from 'eslint';
 
-import perfectionistPlugin from 'eslint-plugin-perfectionist';
+import { interopDefault } from '../util';
 
 export async function perfectionist(): Promise<Linter.Config[]> {
+  const perfectionistPlugin = await interopDefault(
+    // @ts-expect-error - no types
+    import('eslint-plugin-perfectionist'),
+  );
+
   return [
     perfectionistPlugin.configs['recommended-natural'],
     {
@@ -19,21 +24,28 @@ export async function perfectionist(): Promise<Linter.Config[]> {
           {
             customGroups: {
               type: {
-                vben: 'vben',
-                vue: 'vue',
+                'vben-core-type': ['^@vben-core/.+'],
+                'vben-type': ['^@vben/.+'],
+                'vue-type': ['^vue$', '^vue-.+', '^@vue/.+'],
               },
               value: {
-                vben: ['@vben*', '@vben/**/**', '@vben-core/**/**'],
-                vue: ['vue', 'vue-*', '@vue*'],
+                vben: ['^@vben/.+'],
+                'vben-core': ['^@vben-core/.+'],
+                vue: ['^vue$', '^vue-.+', '^@vue/.+'],
               },
             },
+            environment: 'node',
             groups: [
               ['external-type', 'builtin-type', 'type'],
+              'vue-type',
+              'vben-type',
+              'vben-core-type',
               ['parent-type', 'sibling-type', 'index-type'],
               ['internal-type'],
               'builtin',
               'vue',
               'vben',
+              'vben-core',
               'external',
               'internal',
               ['parent', 'sibling', 'index'],
@@ -43,12 +55,13 @@ export async function perfectionist(): Promise<Linter.Config[]> {
               'object',
               'unknown',
             ],
-            internalPattern: ['#*', '#*/**'],
+            internalPattern: ['^#/.+'],
             newlinesBetween: 'always',
             order: 'asc',
             type: 'natural',
           },
         ],
+        'perfectionist/sort-modules': 'off',
         'perfectionist/sort-named-exports': [
           'error',
           {
@@ -67,42 +80,6 @@ export async function perfectionist(): Promise<Linter.Config[]> {
             groups: ['unknown', 'items', 'list', 'children'],
             ignorePattern: ['children'],
             order: 'asc',
-            partitionByComment: 'Part:**',
-            type: 'natural',
-          },
-        ],
-        'perfectionist/sort-vue-attributes': [
-          'error',
-          {
-            // Based on: https://vuejs.org/style-guide/rules-recommended.html#element-attribute-order
-            customGroups: {
-              /* eslint-disable perfectionist/sort-objects */
-              DEFINITION: '*(is|:is|v-is)',
-              LIST_RENDERING: 'v-for',
-              CONDITIONALS: 'v-*(else-if|if|else|show|cloak)',
-              RENDER_MODIFIERS: 'v-*(pre|once)',
-              GLOBAL: '*(:id|id)',
-              UNIQUE: '*(ref|key|:ref|:key)',
-              SLOT: '*(v-slot|slot)',
-              TWO_WAY_BINDING: '*(v-model|v-model:*)',
-              // OTHER_DIRECTIVES e.g. 'v-custom-directive'
-              EVENTS: '*(v-on|@*)',
-              CONTENT: 'v-*(html|text)',
-              /* eslint-enable perfectionist/sort-objects */
-            },
-            groups: [
-              'DEFINITION',
-              'LIST_RENDERING',
-              'CONDITIONALS',
-              'RENDER_MODIFIERS',
-              'GLOBAL',
-              'UNIQUE',
-              'SLOT',
-              'TWO_WAY_BINDING',
-              'unknown',
-              'EVENTS',
-              'CONTENT',
-            ],
             type: 'natural',
           },
         ],

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

@@ -4,7 +4,6 @@ import { interopDefault } from '../util';
 
 export async function vue(): Promise<Linter.Config[]> {
   const [pluginVue, parserVue, parserTs] = await Promise.all([
-    // @ts-expect-error missing types
     interopDefault(import('eslint-plugin-vue')),
     interopDefault(import('vue-eslint-parser')),
     // @ts-expect-error missing types

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/stylelint-config",
-  "version": "5.4.8",
+  "version": "5.5.2",
   "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.4.8",
+  "version": "5.5.2",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

+ 1 - 1
internal/node-utils/src/index.ts

@@ -2,7 +2,7 @@ export * from './constants';
 export * from './date';
 export * from './fs';
 export * from './git';
-export { add as gitAdd, getStagedFiles } from './git';
+export { getStagedFiles, add as gitAdd } from './git';
 export { generatorContentHash } from './hash';
 export * from './monorepo';
 export { toPosixPath } from './path';

+ 3 - 1
internal/node-utils/src/spinner.ts

@@ -1,4 +1,6 @@
-import ora, { type Ora } from 'ora';
+import type { Ora } from 'ora';
+
+import ora from 'ora';
 
 interface SpinnerOptions {
   failedText?: string;

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

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

+ 1 - 1
internal/tailwind-config/src/index.ts

@@ -130,7 +130,6 @@ export default {
     enterAnimationPlugin,
   ],
   prefix: '',
-  safelist: ['dark'],
   theme: {
     container: {
       center: true,
@@ -202,6 +201,7 @@ export default {
       },
     },
   },
+  safelist: ['dark'],
 } as Config;
 
 function createColorsPalette(name: string) {

+ 1 - 1
internal/tsconfig/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/tsconfig",
-  "version": "5.4.8",
+  "version": "5.5.2",
   "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.4.8",
+  "version": "5.5.2",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

+ 2 - 2
internal/vite-config/src/config/application.ts

@@ -1,4 +1,4 @@
-import type { UserConfig } from 'vite';
+import type { CSSOptions, UserConfig } from 'vite';
 
 import type { DefineApplicationOptions } from '../typing';
 
@@ -100,7 +100,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
   });
 }
 
-function createCssOptions(injectGlobalScss = true) {
+function createCssOptions(injectGlobalScss = true): CSSOptions {
   const root = findMonorepoRoot();
   return {
     preprocessorOptions: injectGlobalScss

+ 2 - 2
internal/vite-config/src/plugins/importmap.ts

@@ -10,11 +10,11 @@ import { minify } from 'html-minifier-terser';
 
 const DEFAULT_PROVIDER = 'jspm.io';
 
-type pluginOptions = {
+type pluginOptions = GeneratorOptions & {
   debug?: boolean;
   defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io';
   importmap?: Array<{ name: string; range?: string }>;
-} & GeneratorOptions;
+};
 
 // async function getLatestVersionOfShims() {
 //   const result = await fetch('https://ga.jspm.io/npm:es-module-shims');

+ 2 - 2
internal/vite-config/src/plugins/inject-app-loading/index.ts

@@ -1,3 +1,5 @@
+import type { PluginOption } from 'vite';
+
 import fs from 'node:fs';
 import fsp from 'node:fs/promises';
 import { join } from 'node:path';
@@ -5,8 +7,6 @@ import { fileURLToPath } from 'node:url';
 
 import { readPackageJSON } from '@vben/node-utils';
 
-import { type PluginOption } from 'vite';
-
 /**
  * 用于生成将loading样式注入到项目中
  * 为多app提供loading样式,无需在每个 app -> index.html单独引入

部分文件因文件數量過多而無法顯示