1
0
Эх сурвалжийг харах

feat: add vxe-table component (#4563)

* chore: wip vxe-table

* feat: add table demo

* chore: follow ci recommendations to adjust details

* chore: add custom-cell demo

* feat: add custom-cell table demo

* feat: add table from demo
Vben 7 сар өмнө
parent
commit
4173264805
80 өөрчлөгдсөн 2426 нэмэгдсэн , 80 устгасан
  1. 48 0
      apps/backend-mock/api/table/list.ts
  2. 1 0
      apps/backend-mock/package.json
  3. 36 0
      apps/backend-mock/utils/response.ts
  4. 1 0
      apps/web-antd/src/adapter/index.ts
  5. 59 0
      apps/web-antd/src/adapter/vxe-table.ts
  6. 1 0
      apps/web-ele/src/adapter/index.ts
  7. 60 0
      apps/web-ele/src/adapter/vxe-table.ts
  8. 1 0
      apps/web-naive/src/adapter/index.ts
  9. 59 0
      apps/web-naive/src/adapter/vxe-table.ts
  10. 1 0
      cspell.json
  11. 0 2
      docs/package.json
  12. 1 0
      docs/src/components/common-ui/vben-form.md
  13. 4 1
      internal/tailwind-config/src/index.ts
  14. 2 1
      internal/vite-config/package.json
  15. 1 0
      internal/vite-config/src/config/application.ts
  16. 8 0
      internal/vite-config/src/plugins/index.ts
  17. 20 0
      internal/vite-config/src/plugins/vxe-table.ts
  18. 2 0
      internal/vite-config/src/typing.ts
  19. 1 0
      packages/@core/base/design/src/design-tokens/dark/index.css
  20. 1 1
      packages/@core/base/design/src/design-tokens/default/index.css
  21. 15 1
      packages/@core/base/icons/build.config.ts
  22. 27 0
      packages/@core/base/icons/src/components/empty.vue
  23. 2 1
      packages/@core/base/icons/src/index.ts
  24. 6 4
      packages/@core/base/shared/src/constants/globals.ts
  25. 8 0
      packages/@core/base/shared/src/utils/dom.ts
  26. 1 1
      packages/@core/composables/src/index.ts
  27. 29 3
      packages/@core/composables/src/use-layout-style.ts
  28. 1 1
      packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
  29. 1 1
      packages/@core/preferences/src/config.ts
  30. 5 0
      packages/@core/preferences/src/use-preferences.ts
  31. 1 1
      packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts
  32. 12 2
      packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  33. 3 1
      packages/@core/ui-kit/form-ui/src/form-api.ts
  34. 9 0
      packages/@core/ui-kit/form-ui/src/types.ts
  35. 1 1
      packages/@core/ui-kit/form-ui/src/use-vben-form.ts
  36. 15 0
      packages/@core/ui-kit/form-ui/src/vben-use-form.vue
  37. 2 2
      packages/@core/ui-kit/layout-ui/src/components/layout-content.vue
  38. 28 1
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  39. 0 7
      packages/@core/ui-kit/menu-ui/build.config.ts
  40. 0 7
      packages/@core/ui-kit/shadcn-ui/build.config.ts
  41. 12 2
      packages/@core/ui-kit/shadcn-ui/src/components/pagination/pagination.vue
  42. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue
  43. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue
  44. 1 1
      packages/effects/common-ui/src/components/page/__tests__/page.test.ts
  45. 64 17
      packages/effects/common-ui/src/components/page/page.vue
  46. 15 1
      packages/effects/plugins/package.json
  47. 1 0
      packages/effects/plugins/postcss.config.mjs
  48. 111 0
      packages/effects/plugins/src/vxe-table/api.ts
  49. 4 0
      packages/effects/plugins/src/vxe-table/index.ts
  50. 122 0
      packages/effects/plugins/src/vxe-table/init.ts
  51. 78 0
      packages/effects/plugins/src/vxe-table/theme.css
  52. 53 0
      packages/effects/plugins/src/vxe-table/types.ts
  53. 42 0
      packages/effects/plugins/src/vxe-table/use-vxe-grid.ts
  54. 264 0
      packages/effects/plugins/src/vxe-table/use-vxe-grid.vue
  55. 1 0
      packages/effects/plugins/tailwind.config.mjs
  56. 2 1
      packages/locales/src/langs/en-US.json
  57. 2 1
      packages/locales/src/langs/zh-CN.json
  58. 1 0
      playground/src/adapter/index.ts
  59. 59 0
      playground/src/adapter/vxe-table.ts
  60. 1 0
      playground/src/api/examples/index.ts
  61. 0 0
      playground/src/api/examples/status.ts
  62. 18 0
      playground/src/api/examples/table.ts
  63. 1 1
      playground/src/api/index.ts
  64. 12 0
      playground/src/locales/langs/en-US.json
  65. 12 0
      playground/src/locales/langs/zh-CN.json
  66. 83 1
      playground/src/router/routes/modules/examples.ts
  67. 1 1
      playground/src/views/examples/doc-button.vue
  68. 5 0
      playground/src/views/examples/form/basic.vue
  69. 93 0
      playground/src/views/examples/vxe-table/basic.vue
  70. 110 0
      playground/src/views/examples/vxe-table/custom-cell.vue
  71. 57 0
      playground/src/views/examples/vxe-table/edit-cell.vue
  72. 94 0
      playground/src/views/examples/vxe-table/edit-row.vue
  73. 64 0
      playground/src/views/examples/vxe-table/fixed.vue
  74. 102 0
      playground/src/views/examples/vxe-table/form.vue
  75. 65 0
      playground/src/views/examples/vxe-table/remote.vue
  76. 172 0
      playground/src/views/examples/vxe-table/table-data.ts
  77. 59 0
      playground/src/views/examples/vxe-table/tree.vue
  78. 63 0
      playground/src/views/examples/vxe-table/virtual.vue
  79. 102 12
      pnpm-lock.yaml
  80. 5 1
      pnpm-workspace.yaml

+ 48 - 0
apps/backend-mock/api/table/list.ts

@@ -0,0 +1,48 @@
+import { faker } from '@faker-js/faker';
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { unAuthorizedResponse } from '~/utils/response';
+
+function generateMockDataList(count: number) {
+  const dataList = [];
+
+  for (let i = 0; i < count; i++) {
+    const dataItem = {
+      id: faker.string.uuid(),
+      imageUrl: faker.image.avatar(),
+      imageUrl2: faker.image.avatar(),
+      open: faker.datatype.boolean(),
+      status: faker.helpers.arrayElement(['success', 'error', 'warning']),
+      productName: faker.commerce.productName(),
+      price: faker.commerce.price(),
+      currency: faker.finance.currencyCode(),
+      quantity: faker.number.int({ min: 1, max: 100 }),
+      available: faker.datatype.boolean(),
+      category: faker.commerce.department(),
+      releaseDate: faker.date.past(),
+      rating: faker.number.float({ min: 1, max: 5 }),
+      description: faker.commerce.productDescription(),
+      weight: faker.number.float({ min: 0.1, max: 10 }),
+      color: faker.color.human(),
+      inProduction: faker.datatype.boolean(),
+      tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
+    };
+
+    dataList.push(dataItem);
+  }
+
+  return dataList;
+}
+
+const mockData = generateMockDataList(100);
+
+export default eventHandler(async (event) => {
+  const userinfo = verifyAccessToken(event);
+  if (!userinfo) {
+    return unAuthorizedResponse(event);
+  }
+
+  await sleep(600);
+
+  const { page, pageSize } = getQuery(event);
+  return usePageResponseSuccess(page as string, pageSize as string, mockData);
+});

+ 1 - 0
apps/backend-mock/package.json

@@ -10,6 +10,7 @@
     "start": "nitro dev"
   },
   "dependencies": {
+    "@faker-js/faker": "catalog:",
     "jsonwebtoken": "catalog:",
     "nitropack": "catalog:"
   },

+ 36 - 0
apps/backend-mock/utils/response.ts

@@ -9,6 +9,27 @@ export function useResponseSuccess<T = any>(data: T) {
   };
 }
 
+export function usePageResponseSuccess<T = any>(
+  page: number | string,
+  pageSize: number | string,
+  list: T[],
+  { message = 'ok' } = {},
+) {
+  const pageData = pagination(
+    Number.parseInt(`${page}`),
+    Number.parseInt(`${pageSize}`),
+    list,
+  );
+
+  return {
+    ...useResponseSuccess({
+      items: pageData,
+      total: list.length,
+    }),
+    message,
+  };
+}
+
 export function useResponseError(message: string, error: any = null) {
   return {
     code: -1,
@@ -27,3 +48,18 @@ export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
   setResponseStatus(event, 401);
   return useResponseError('UnauthorizedException', 'Unauthorized Exception');
 }
+
+export function sleep(ms: number) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export function pagination<T = any>(
+  pageNo: number,
+  pageSize: number,
+  array: T[],
+): T[] {
+  const offset = (pageNo - 1) * Number(pageSize);
+  return offset + Number(pageSize) >= array.length
+    ? array.slice(offset)
+    : array.slice(offset, offset + Number(pageSize));
+}

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

@@ -1 +1,2 @@
 export * from './form';
+export * from './vxe-table';

+ 59 - 0
apps/web-antd/src/adapter/vxe-table.ts

@@ -0,0 +1,59 @@
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { Button, Image } from 'ant-design-vue';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+  configVxeTable: (vxeUI) => {
+    vxeUI.setConfig({
+      grid: {
+        align: 'center',
+        border: true,
+        minHeight: 180,
+        proxyConfig: {
+          autoLoad: true,
+          response: {
+            result: 'items',
+            total: 'total',
+            list: 'items',
+          },
+          showActiveMsg: true,
+          showResponseMsg: false,
+        },
+        round: true,
+        size: 'small',
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellImage' },
+    vxeUI.renderer.add('CellImage', {
+      renderDefault(_renderOpts, params) {
+        const { column, row } = params;
+        return h(Image, { src: row[column.field] });
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellLink' },
+    vxeUI.renderer.add('CellLink', {
+      renderDefault(renderOpts) {
+        const { props } = renderOpts;
+        return h(
+          Button,
+          { size: 'small', type: 'link' },
+          { default: () => props?.text },
+        );
+      },
+    });
+
+    // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+    // vxeUI.formats.add
+  },
+  useVbenForm,
+});
+
+export { useVbenVxeGrid };
+
+export type * from '@vben/plugins/vxe-table';

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

@@ -1 +1,2 @@
 export * from './form';
+export * from './vxe-table';

+ 60 - 0
apps/web-ele/src/adapter/vxe-table.ts

@@ -0,0 +1,60 @@
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { ElButton, ElImage } from 'element-plus';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+  configVxeTable: (vxeUI) => {
+    vxeUI.setConfig({
+      grid: {
+        align: 'center',
+        border: true,
+        minHeight: 180,
+        proxyConfig: {
+          autoLoad: true,
+          response: {
+            result: 'items',
+            total: 'total',
+            list: 'items',
+          },
+          showActiveMsg: true,
+          showResponseMsg: false,
+        },
+        round: true,
+        size: 'small',
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellImage' },
+    vxeUI.renderer.add('CellImage', {
+      renderDefault(_renderOpts, params) {
+        const { column, row } = params;
+        const src = row[column.field];
+        return h(ElImage, { src, previewSrcList: [src] });
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellLink' },
+    vxeUI.renderer.add('CellLink', {
+      renderDefault(renderOpts) {
+        const { props } = renderOpts;
+        return h(
+          ElButton,
+          { size: 'small', link: true },
+          { default: () => props?.text },
+        );
+      },
+    });
+
+    // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+    // vxeUI.formats.add
+  },
+  useVbenForm,
+});
+
+export { useVbenVxeGrid };
+
+export type * from '@vben/plugins/vxe-table';

+ 1 - 0
apps/web-naive/src/adapter/index.ts

@@ -1,2 +1,3 @@
 export * from './form';
 export * from './naive';
+export * from './vxe-table';

+ 59 - 0
apps/web-naive/src/adapter/vxe-table.ts

@@ -0,0 +1,59 @@
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { NButton, NImage } from 'naive-ui';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+  configVxeTable: (vxeUI) => {
+    vxeUI.setConfig({
+      grid: {
+        align: 'center',
+        border: true,
+        minHeight: 180,
+        proxyConfig: {
+          autoLoad: true,
+          response: {
+            result: 'items',
+            total: 'total',
+            list: 'items',
+          },
+          showActiveMsg: true,
+          showResponseMsg: false,
+        },
+        round: true,
+        size: 'small',
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellImage' },
+    vxeUI.renderer.add('CellImage', {
+      renderDefault(_renderOpts, params) {
+        const { column, row } = params;
+        return h(NImage, { src: row[column.field] });
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellLink' },
+    vxeUI.renderer.add('CellLink', {
+      renderDefault(renderOpts) {
+        const { props } = renderOpts;
+        return h(
+          NButton,
+          { size: 'small', type: 'primary', quaternary: true },
+          { default: () => props?.text },
+        );
+      },
+    });
+
+    // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+    // vxeUI.formats.add
+  },
+  useVbenForm,
+});
+
+export { useVbenVxeGrid };
+
+export type * from '@vben/plugins/vxe-table';

+ 1 - 0
cspell.json

@@ -19,6 +19,7 @@
     "intlify",
     "mkdist",
     "mockjs",
+    "vitejs",
     "noopener",
     "noreferrer",
     "nprogress",

+ 0 - 2
docs/package.json

@@ -13,9 +13,7 @@
   "dependencies": {
     "@vben-core/shadcn-ui": "workspace:*",
     "@vben/common-ui": "workspace:*",
-    "@vben/hooks": "workspace:*",
     "@vben/locales": "workspace:*",
-    "@vben/preferences": "workspace:*",
     "@vben/styles": "workspace:*",
     "ant-design-vue": "catalog:",
     "lucide-vue-next": "catalog:",

+ 1 - 0
docs/src/components/common-ui/vben-form.md

@@ -267,6 +267,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
 | submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
 | showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
 | collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
+| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
 | collapsedRows | 折叠时保持的行数 | `number` | `1` |
 | commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
 | schema | 表单项的每一项配置 | `FormSchema` | - |

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

@@ -91,7 +91,10 @@ const customColors = {
   main: {
     DEFAULT: 'hsl(var(--main))',
   },
-  overlay: 'hsl(var(--overlay))',
+  overlay: {
+    content: 'hsl(var(--overlay-content))',
+    DEFAULT: 'hsl(var(--overlay))',
+  },
   red: {
     ...createColorsPalette('red'),
     foreground: 'hsl(var(--destructive-foreground))',

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

@@ -53,6 +53,7 @@
     "vite": "catalog:",
     "vite-plugin-compression": "catalog:",
     "vite-plugin-dts": "catalog:",
-    "vite-plugin-html": "catalog:"
+    "vite-plugin-html": "catalog:",
+    "vite-plugin-lazy-import": "catalog:"
   }
 }

+ 1 - 0
internal/vite-config/src/config/application.ts

@@ -47,6 +47,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
       },
       pwa: true,
       pwaOptions: getDefaultPwaOptions(appTitle),
+      vxeTableLazyImport: true,
       ...envConfig,
       ...application,
     });

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

@@ -26,6 +26,7 @@ import { viteMetadataPlugin } from './inject-metadata';
 import { viteLicensePlugin } from './license';
 import { viteNitroMockPlugin } from './nitro-mock';
 import { vitePrintPlugin } from './print';
+import { viteVxeTableImportsPlugin } from './vxe-table';
 
 /**
  * 获取条件成立的 vite 插件
@@ -110,6 +111,7 @@ async function loadApplicationPlugins(
     printInfoMap,
     pwa,
     pwaOptions,
+    vxeTableLazyImport,
     ...commonOptions
   } = options;
 
@@ -135,6 +137,12 @@ async function loadApplicationPlugins(
         return [await vitePrintPlugin({ infoMap: printInfoMap })];
       },
     },
+    {
+      condition: vxeTableLazyImport,
+      plugins: async () => {
+        return [await viteVxeTableImportsPlugin()];
+      },
+    },
     {
       condition: nitroMock,
       plugins: async () => {

+ 20 - 0
internal/vite-config/src/plugins/vxe-table.ts

@@ -0,0 +1,20 @@
+import type { PluginOption } from 'vite';
+
+import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import';
+
+async function viteVxeTableImportsPlugin(): Promise<PluginOption> {
+  return [
+    lazyImport({
+      resolvers: [
+        VxeResolver({
+          libraryName: 'vxe-table',
+        }),
+        VxeResolver({
+          libraryName: 'vxe-pc-ui',
+        }),
+      ],
+    }),
+  ];
+}
+
+export { viteVxeTableImportsPlugin };

+ 2 - 0
internal/vite-config/src/typing.ts

@@ -123,6 +123,8 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
   pwa?: boolean;
   /** pwa 插件配置 */
   pwaOptions?: Partial<PwaPluginOptions>;
+  /** 是否开启vxe-table懒加载 */
+  vxeTableLazyImport?: boolean;
 }
 
 interface LibraryPluginOptions extends CommonPluginOptions {

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

@@ -79,6 +79,7 @@
 
   /* 遮罩颜色 */
   --overlay: 0deg 0% 0% / 40%;
+  --overlay-content: 0deg 0% 0% / 40%;
 
   /* 基本文字大小 */
   --font-size-base: 16px;

+ 1 - 1
packages/@core/base/design/src/design-tokens/default/index.css

@@ -79,7 +79,7 @@
 
   /* 遮罩颜色 */
   --overlay: 0 0% 0% / 45%;
-  --overlay-light: 0 0% 95% / 45%;
+  --overlay-content: 0 0% 95% / 45%;
 
   /* 基本文字大小 */
   --font-size-base: 16px;

+ 15 - 1
packages/@core/base/icons/build.config.ts

@@ -3,5 +3,19 @@ import { defineBuildConfig } from 'unbuild';
 export default defineBuildConfig({
   clean: true,
   declaration: true,
-  entries: ['src/index'],
+  entries: [
+    {
+      builder: 'mkdist',
+      input: './src',
+      loaders: ['vue'],
+      pattern: ['**/*.vue'],
+    },
+    {
+      builder: 'mkdist',
+      format: 'esm',
+      input: './src',
+      loaders: ['js'],
+      pattern: ['**/*.ts'],
+    },
+  ],
 });

+ 27 - 0
packages/@core/base/icons/src/components/empty.vue

@@ -0,0 +1,27 @@
+<template>
+  <svg
+    height="41"
+    viewBox="0 0 64 41"
+    width="64"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <g fill="none" fill-rule="evenodd" transform="translate(0 1)">
+      <ellipse
+        cx="32"
+        cy="33"
+        fill="hsl(var(--background-deep))"
+        rx="32"
+        ry="7"
+      />
+      <g fill-rule="nonzero" stroke="hsl(var(--heavy))">
+        <path
+          d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
+        />
+        <path
+          d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
+          fill="hsl(var(--accent))"
+        />
+      </g>
+    </g>
+  </svg>
+</template>

+ 2 - 1
packages/@core/base/icons/src/index.ts

@@ -1,4 +1,5 @@
+export { default as EmptyIcon } from './components/empty.vue';
 export * from './create-icon';
-export * from './lucide';
 
+export * from './lucide';
 export * from '@iconify/vue';

+ 6 - 4
packages/@core/base/shared/src/constants/globals.ts

@@ -1,9 +1,11 @@
-/**
- * @zh_CN 布局内容高度 css变量
- * @en_US Layout content height
- */
+/** layout content 组件的高度 */
 export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
+/** layout content 组件的宽度 */
 export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
+/** layout header 组件的高度 */
+export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
+/** layout footer 组件的高度 */
+export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
 
 /**
  * @zh_CN 默认命名空间

+ 8 - 0
packages/@core/base/shared/src/utils/dom.ts

@@ -85,3 +85,11 @@ export function needsScrollbar() {
   // 在其他情况下,根据 scrollHeight 和 innerHeight 比较判断
   return doc.scrollHeight > window.innerHeight;
 }
+
+export function triggerWindowResize(): void {
+  // 创建一个新的 resize 事件
+  const resizeEvent = new Event('resize');
+
+  // 触发 window 的 resize 事件
+  window.dispatchEvent(resizeEvent);
+}

+ 1 - 1
packages/@core/composables/src/index.ts

@@ -1,5 +1,5 @@
-export * from './use-content-style';
 export * from './use-is-mobile';
+export * from './use-layout-style';
 export * from './use-namespace';
 export * from './use-priority-value';
 export * from './use-scroll-lock';

+ 29 - 3
packages/@core/composables/src/use-content-style.ts → packages/@core/composables/src/use-layout-style.ts

@@ -4,6 +4,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
 import {
   CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
   CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
+  CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT,
+  CSS_VARIABLE_LAYOUT_HEADER_HEIGHT,
 } from '@vben-core/shared/constants';
 import {
   getElementVisibleRect,
@@ -15,7 +17,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
 /**
  * @zh_CN content style
  */
-function useContentStyle() {
+export function useLayoutContentStyle() {
   let resizeObserver: null | ResizeObserver = null;
   const contentElement = ref<HTMLDivElement | null>(null);
   const visibleDomRect = ref<null | VisibleDomRect>(null);
@@ -40,7 +42,7 @@ function useContentStyle() {
       contentHeight.value = `${visibleDomRect.value.height}px`;
       contentWidth.value = `${visibleDomRect.value.width}px`;
     },
-    100,
+    16,
   );
 
   onMounted(() => {
@@ -58,4 +60,28 @@ function useContentStyle() {
   return { contentElement, overlayStyle, visibleDomRect };
 }
 
-export { useContentStyle };
+export function useLayoutHeaderStyle() {
+  const headerHeight = useCssVar(CSS_VARIABLE_LAYOUT_HEADER_HEIGHT);
+
+  return {
+    getLayoutHeaderHeight: () => {
+      return Number.parseInt(`${headerHeight.value}`, 10);
+    },
+    setLayoutHeaderHeight: (height: number) => {
+      headerHeight.value = `${height}px`;
+    },
+  };
+}
+
+export function useLayoutFooterStyle() {
+  const footerHeight = useCssVar(CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT);
+
+  return {
+    getLayoutFooterHeight: () => {
+      return Number.parseInt(`${footerHeight.value}`, 10);
+    },
+    setLayoutFooterHeight: (height: number) => {
+      footerHeight.value = `${height}px`;
+    },
+  };
+}

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

@@ -39,7 +39,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "icpLink": "",
   },
   "footer": {
-    "enable": true,
+    "enable": false,
     "fixed": false,
   },
   "header": {

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

@@ -39,7 +39,7 @@ const defaultPreferences: Preferences = {
     icpLink: '',
   },
   footer: {
-    enable: true,
+    enable: false,
     fixed: false,
   },
   header: {

+ 5 - 0
packages/@core/preferences/src/use-preferences.ts

@@ -28,6 +28,10 @@ function usePreferences() {
     return isDarkTheme(preferences.theme.mode);
   });
 
+  const locale = computed(() => {
+    return preferences.app.locale;
+  });
+
   const isMobile = computed(() => {
     return appPreferences.value.isMobile;
   });
@@ -218,6 +222,7 @@ function usePreferences() {
     isSideNav,
     keepAlive,
     layout,
+    locale,
     preferencesButtonPosition,
     sidebarCollapsed,
     theme,

+ 1 - 1
packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts

@@ -109,7 +109,7 @@ describe('formApi', () => {
   });
 
   it('should unmount form and reset state', () => {
-    formApi.unmounted();
+    formApi.unmount();
     expect(formApi.isMounted).toBe(false);
   });
 

+ 12 - 2
packages/@core/ui-kit/form-ui/src/components/form-actions.vue

@@ -1,9 +1,9 @@
 <script setup lang="ts">
-import { computed, toRaw, unref } from 'vue';
+import { computed, toRaw, unref, watch } from 'vue';
 
 import { useSimpleLocale } from '@vben-core/composables';
 import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
-import { cn, isFunction } from '@vben-core/shared/utils';
+import { cn, isFunction, triggerWindowResize } from '@vben-core/shared/utils';
 
 import { COMPONENT_MAP } from '../config';
 import { injectFormProps } from '../use-form-context';
@@ -65,6 +65,16 @@ async function handleReset(e: Event) {
     form.resetForm();
   }
 }
+
+watch(
+  () => collapsed.value,
+  () => {
+    const props = unref(rootProps);
+    if (props.collapseTriggerResize) {
+      triggerWindowResize();
+    }
+  },
+);
 </script>
 <template>
   <div

+ 3 - 1
packages/@core/ui-kit/form-ui/src/form-api.ts

@@ -24,9 +24,11 @@ function getDefaultState(): VbenFormProps {
     actionWrapperClass: '',
     collapsed: false,
     collapsedRows: 1,
+    collapseTriggerResize: false,
     commonConfig: {},
     handleReset: undefined,
     handleSubmit: undefined,
+    handleValuesChange: undefined,
     layout: 'horizontal',
     resetButtonOptions: {},
     schema: [],
@@ -249,7 +251,7 @@ export class FormApi {
     return rawValues;
   }
 
-  unmounted() {
+  unmount() {
     // this.state = null;
     this.isMounted = false;
     this.stateHandler.reset();

+ 9 - 0
packages/@core/ui-kit/form-ui/src/types.ts

@@ -244,6 +244,11 @@ export interface FormRenderProps<
    * @default 1
    */
   collapsedRows?: number;
+  /**
+   * 是否触发resize事件
+   * @default false
+   */
+  collapseTriggerResize?: boolean;
   /**
    * 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
    */
@@ -302,6 +307,10 @@ export interface VbenFormProps<
    * 表单提交回调
    */
   handleSubmit?: HandleSubmitFn;
+  /**
+   * 表单值变化回调
+   */
+  handleValuesChange?: (values: Record<string, any>) => void;
   /**
    * 重置按钮参数
    */

+ 1 - 1
packages/@core/ui-kit/form-ui/src/use-vben-form.ts

@@ -24,7 +24,7 @@ export function useVbenForm<
   const Form = defineComponent(
     (props: VbenFormProps, { attrs, slots }) => {
       onBeforeUnmount(() => {
-        api.unmounted();
+        api.unmount();
       });
       return () =>
         h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);

+ 15 - 0
packages/@core/ui-kit/form-ui/src/vben-use-form.vue

@@ -1,7 +1,10 @@
 <script setup lang="ts">
 import type { ExtendedFormApi, VbenFormProps } from './types';
 
+// import { toRaw, watch } from 'vue';
+
 import { useForwardPriorityValues } from '@vben-core/composables';
+// import { isFunction } from '@vben-core/shared/utils';
 
 import FormActions from './components/form-actions.vue';
 import {
@@ -31,6 +34,18 @@ props.formApi?.mount?.(form);
 const handleUpdateCollapsed = (value: boolean) => {
   props.formApi?.setState({ collapsed: !!value });
 };
+// if (isFunction(forward.value.handleValuesChange)) {
+//   watch(
+//     () => form.values,
+//     (val) => {
+//       forward.value.handleValuesChange?.(toRaw(val));
+//     },
+//     {
+//       deep: true,
+//       immediate: true,
+//     },
+//   );
+// }
 </script>
 
 <template>

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

@@ -4,7 +4,7 @@ import type { ContentCompactType } from '@vben-core/typings';
 import type { CSSProperties } from 'vue';
 import { computed } from 'vue';
 
-import { useContentStyle } from '@vben-core/composables';
+import { useLayoutContentStyle } from '@vben-core/composables';
 import { Slot } from '@vben-core/shadcn-ui';
 
 interface Props {
@@ -25,7 +25,7 @@ interface Props {
 
 const props = withDefaults(defineProps<Props>(), {});
 
-const { contentElement, overlayStyle } = useContentStyle();
+const { contentElement, overlayStyle } = useLayoutContentStyle();
 
 const style = computed((): CSSProperties => {
   const {

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

@@ -4,7 +4,11 @@ import type { VbenLayoutProps } from './vben-layout';
 import type { CSSProperties } from 'vue';
 import { computed, ref, watch } from 'vue';
 
-import { SCROLL_FIXED_CLASS } from '@vben-core/composables';
+import {
+  SCROLL_FIXED_CLASS,
+  useLayoutFooterStyle,
+  useLayoutHeaderStyle,
+} from '@vben-core/composables';
 import { Menu } from '@vben-core/icons';
 import { VbenIconButton } from '@vben-core/shadcn-ui';
 
@@ -74,6 +78,9 @@ const {
   y: scrollY,
 } = useScroll(document);
 
+const { setLayoutHeaderHeight } = useLayoutHeaderStyle();
+const { setLayoutFooterHeight } = useLayoutFooterStyle();
+
 const { y: mouseY } = useMouse({ target: contentRef, type: 'client' });
 
 const {
@@ -356,6 +363,26 @@ watch(
   },
 );
 
+watch(
+  [() => headerWrapperHeight.value, () => isFullContent.value],
+  ([height]) => {
+    setLayoutHeaderHeight(isFullContent.value ? 0 : height);
+  },
+  {
+    immediate: true,
+  },
+);
+
+watch(
+  () => props.footerHeight,
+  (height: number) => {
+    setLayoutFooterHeight(height);
+  },
+  {
+    immediate: true,
+  },
+);
+
 {
   const mouseMove = () => {
     mouseY.value > headerWrapperHeight.value

+ 0 - 7
packages/@core/ui-kit/menu-ui/build.config.ts

@@ -15,13 +15,6 @@ export default defineBuildConfig({
       loaders: ['vue'],
       pattern: ['**/*.vue'],
     },
-    // {
-    //   builder: 'mkdist',
-    //   format: 'cjs',
-    //   input: './src',
-    //   loaders: ['js'],
-    //   pattern: ['**/*.ts'],
-    // },
     {
       builder: 'mkdist',
       format: 'esm',

+ 0 - 7
packages/@core/ui-kit/shadcn-ui/build.config.ts

@@ -16,13 +16,6 @@ export default defineBuildConfig({
       loaders: ['vue'],
       pattern: ['**/*.vue'],
     },
-    // {
-    //   builder: 'mkdist',
-    //   format: 'cjs',
-    //   input: './src',
-    //   loaders: ['js'],
-    //   pattern: ['**/*.ts'],
-    // },
     {
       builder: 'mkdist',
       format: 'esm',

+ 12 - 2
packages/@core/ui-kit/shadcn-ui/src/components/pagination/pagination.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, watch } from 'vue';
 
 import { cn } from '@vben-core/shared/utils';
 
@@ -32,10 +32,13 @@ const {
   showRowsPerPage = true,
   showTotalText = true,
   siblingCount = 1,
-  size = 'default',
+  size = 'small',
   total = 500,
 } = defineProps<Props>();
 
+const emit = defineEmits<{
+  pageChange: [currentPage: number, pageSize: number];
+}>();
 const currentPage = defineModel<number>('currentPage', { default: 1 });
 const itemPerPage = defineModel<number>('itemPerPage', { default: 20 });
 
@@ -53,6 +56,13 @@ const options = computed(() => {
 function handleUpdateModelValue(value: string) {
   itemPerPage.value = Number(value);
 }
+
+watch(
+  [() => itemPerPage.value, () => currentPage.value],
+  ([itemPerPage, currentPage]) => {
+    emit('pageChange', currentPage, itemPerPage);
+  },
+);
 </script>
 
 <template>

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue

@@ -69,7 +69,7 @@ function onTransitionEnd() {
   <div
     :class="
       cn(
-        'z-100 dark:bg-overlay pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center bg-[hsl(var(--overlay-light))] transition-all duration-500',
+        'z-100 dark:bg-overlay bg-overlay-content pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center transition-all duration-500',
         {
           'invisible opacity-0': !showSpinner,
         },

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue

@@ -63,7 +63,7 @@ function onTransitionEnd() {
   <div
     :class="
       cn(
-        'flex-center z-100 dark:bg-overlay absolute left-0 top-0 size-full bg-[hsl(var(--overlay-light))] backdrop-blur-sm transition-all duration-500',
+        'flex-center z-100 bg-overlay-content absolute left-0 top-0 size-full backdrop-blur-sm transition-all duration-500',
         {
           'invisible opacity-0': !showSpinner,
         },

+ 1 - 1
packages/effects/common-ui/src/components/page/__tests__/page.test.ts

@@ -54,7 +54,7 @@ describe('page.vue', () => {
       },
     });
 
-    const contentDiv = wrapper.find('.m-4');
+    const contentDiv = wrapper.find('.p-4');
     expect(contentDiv.classes()).toContain('custom-class');
   });
 

+ 64 - 17
packages/effects/common-ui/src/components/page/page.vue

@@ -1,37 +1,79 @@
 <script setup lang="ts">
+import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
+
 interface Props {
   title?: string;
   description?: string;
   contentClass?: string;
-  showFooter?: boolean;
+  /**
+   * 根据content可见高度自适应
+   */
+  autoContentHeight?: boolean;
 }
 
 defineOptions({
   name: 'Page',
 });
 
-const props = withDefaults(defineProps<Props>(), {
-  contentClass: '',
-  description: '',
-  showFooter: false,
-  title: '',
+const {
+  contentClass = '',
+  description = '',
+  autoContentHeight = false,
+  title = '',
+} = defineProps<Props>();
+
+const headerHeight = ref(0);
+const footerHeight = ref(0);
+const shouldAutoHeight = ref(false);
+
+const headerRef = useTemplateRef<HTMLDivElement>('headerRef');
+const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
+
+const contentStyle = computed(() => {
+  if (autoContentHeight) {
+    return {
+      height: shouldAutoHeight.value
+        ? `calc(var(--vben-content-height) - ${headerHeight.value}px - ${footerHeight.value}px)`
+        : '0',
+      // 'overflow-y': shouldAutoHeight.value?'auto':'unset',
+    };
+  }
+  return {};
+});
+
+async function calcContentHeight() {
+  if (!autoContentHeight) {
+    return;
+  }
+  await nextTick();
+  headerHeight.value = headerRef.value?.offsetHeight || 0;
+  footerHeight.value = footerRef.value?.offsetHeight || 0;
+  setTimeout(() => {
+    shouldAutoHeight.value = true;
+  }, 30);
+}
+
+onMounted(() => {
+  calcContentHeight();
 });
 </script>
 
 <template>
-  <div class="relative h-full">
+  <div class="relative">
     <div
-      v-if="description || $slots.description || title"
-      class="bg-card px-6 py-4"
+      v-if="
+        description ||
+        $slots.description ||
+        title ||
+        $slots.title ||
+        $slots.extra
+      "
+      ref="headerRef"
+      class="bg-card relative px-6 py-4"
     >
       <slot name="title">
-        <div
-          v-if="title"
-          class="mb-2 flex justify-between text-lg font-semibold"
-        >
+        <div v-if="title" class="mb-2 flex text-lg font-semibold">
           {{ title }}
-
-          <slot name="extra"></slot>
         </div>
       </slot>
 
@@ -40,14 +82,19 @@ const props = withDefaults(defineProps<Props>(), {
           {{ description }}
         </p>
       </slot>
+
+      <div v-if="$slots.extra" class="absolute bottom-4 right-4">
+        <slot name="extra"></slot>
+      </div>
     </div>
 
-    <div :class="contentClass" class="m-4">
+    <div :class="contentClass" :style="contentStyle" class="h-full p-4">
       <slot></slot>
     </div>
 
     <div
-      v-if="props.showFooter"
+      v-if="$slots.footer"
+      ref="footerRef"
       class="bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4"
     >
       <slot name="footer"></slot>

+ 15 - 1
packages/effects/plugins/package.json

@@ -17,12 +17,26 @@
     "./echarts": {
       "types": "./src/echarts/index.ts",
       "default": "./src/echarts/index.ts"
+    },
+    "./vxe-table": {
+      "types": "./src/vxe-table/index.ts",
+      "default": "./src/vxe-table/index.ts"
     }
   },
   "dependencies": {
+    "@vben-core/form-ui": "workspace:*",
+    "@vben-core/shadcn-ui": "workspace:*",
+    "@vben-core/shared": "workspace:*",
+    "@vben/hooks": "workspace:*",
+    "@vben/icons": "workspace:*",
+    "@vben/locales": "workspace:*",
     "@vben/preferences": "workspace:*",
+    "@vben/types": "workspace:*",
+    "@vben/utils": "workspace:*",
     "@vueuse/core": "catalog:",
     "echarts": "catalog:",
-    "vue": "catalog:"
+    "vue": "catalog:",
+    "vxe-pc-ui": "catalog:",
+    "vxe-table": "catalog:"
   }
 }

+ 1 - 0
packages/effects/plugins/postcss.config.mjs

@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';

+ 111 - 0
packages/effects/plugins/src/vxe-table/api.ts

@@ -0,0 +1,111 @@
+import type { VxeGridInstance } from 'vxe-table';
+
+import type { VxeGridProps } from './types';
+
+import { toRaw } from 'vue';
+
+import { Store } from '@vben-core/shared/store';
+import {
+  bindMethods,
+  isFunction,
+  mergeWithArrayOverride,
+  StateHandler,
+} from '@vben-core/shared/utils';
+
+function getDefaultState(): VxeGridProps {
+  return {
+    class: '',
+    gridClass: '',
+    gridOptions: {},
+    gridEvents: {},
+    formOptions: undefined,
+  };
+}
+
+export class VxeGridApi {
+  // private prevState: null | VxeGridProps = null;
+  public grid = {} as VxeGridInstance;
+
+  isMounted = false;
+  public state: null | VxeGridProps = null;
+
+  stateHandler: StateHandler;
+
+  public store: Store<VxeGridProps>;
+
+  constructor(options: VxeGridProps = {}) {
+    const storeState = { ...options };
+
+    const defaultState = getDefaultState();
+    this.store = new Store<VxeGridProps>(
+      mergeWithArrayOverride(storeState, defaultState),
+      {
+        onUpdate: () => {
+          // this.prevState = this.state;
+          this.state = this.store.state;
+        },
+      },
+    );
+
+    this.state = this.store.state;
+    this.stateHandler = new StateHandler();
+    bindMethods(this);
+  }
+
+  mount(instance: null | VxeGridInstance) {
+    if (!this.isMounted && instance) {
+      this.grid = instance;
+      this.stateHandler.setConditionTrue();
+      this.isMounted = true;
+    }
+  }
+
+  async query(params: Record<string, any> = {}) {
+    try {
+      await this.grid.commitProxy('query', toRaw(params));
+    } catch (error) {
+      console.error('Error occurred while querying:', error);
+    }
+  }
+
+  async reload(params: Record<string, any> = {}) {
+    try {
+      await this.grid.commitProxy('reload', toRaw(params));
+    } catch (error) {
+      console.error('Error occurred while reloading:', error);
+    }
+  }
+
+  setGridOptions(options: Partial<VxeGridProps['gridOptions']>) {
+    this.setState({
+      gridOptions: options,
+    });
+  }
+
+  setLoading(isLoading: boolean) {
+    this.setState({
+      gridOptions: {
+        loading: isLoading,
+      },
+    });
+  }
+
+  setState(
+    stateOrFn:
+      | ((prev: VxeGridProps) => Partial<VxeGridProps>)
+      | Partial<VxeGridProps>,
+  ) {
+    if (isFunction(stateOrFn)) {
+      this.store.setState((prev) => {
+        return mergeWithArrayOverride(stateOrFn(prev), prev);
+      });
+    } else {
+      this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev));
+    }
+  }
+
+  unmount() {
+    this.isMounted = false;
+    this.stateHandler.reset();
+  }
+}

+ 4 - 0
packages/effects/plugins/src/vxe-table/index.ts

@@ -0,0 +1,4 @@
+export { setupVbenVxeTable } from './init';
+export * from './use-vxe-grid';
+export { default as VbenVxeGrid } from './use-vxe-grid.vue';
+export type { VxeGridListeners, VxeGridProps } from 'vxe-table';

+ 122 - 0
packages/effects/plugins/src/vxe-table/init.ts

@@ -0,0 +1,122 @@
+import type { SetupVxeTable } from './types';
+
+import { defineComponent, watch } from 'vue';
+
+import { usePreferences } from '@vben/preferences';
+import { useVbenForm } from '@vben-core/form-ui';
+
+import {
+  VxeButton,
+  VxeButtonGroup,
+  // VxeFormGather,
+  // VxeForm,
+  // VxeFormItem,
+  VxeIcon,
+  VxeInput,
+  VxeLoading,
+  VxePager,
+  // VxeList,
+  // VxeModal,
+  // VxeOptgroup,
+  // VxeOption,
+  // VxePulldown,
+  // VxeRadio,
+  // VxeRadioButton,
+  // VxeRadioGroup,
+  VxeSelect,
+  VxeTooltip,
+  VxeUI,
+  // VxeSwitch,
+  // VxeTextarea,
+} from 'vxe-pc-ui';
+import enUS from 'vxe-pc-ui/lib/language/en-US';
+
+// 导入默认的语言
+import zhCN from 'vxe-pc-ui/lib/language/zh-CN';
+import {
+  VxeColgroup,
+  VxeColumn,
+  VxeGrid,
+  VxeTable,
+  VxeToolbar,
+} from 'vxe-table';
+
+// 是否加载过
+let isInit = false;
+
+// eslint-disable-next-line import/no-mutable-exports
+export let useTableForm: typeof useVbenForm;
+
+// 部分组件,如果没注册,vxe-table 会报错,这里实际没用组件,只是为了不报错,同时可以减少打包体积
+const createVirtualComponent = (name = '') => {
+  return defineComponent({
+    name,
+  });
+};
+
+export function initVxeTable() {
+  if (isInit) {
+    return;
+  }
+
+  VxeUI.component(VxeTable);
+  VxeUI.component(VxeColumn);
+  VxeUI.component(VxeColgroup);
+  VxeUI.component(VxeLoading);
+  VxeUI.component(VxeGrid);
+  VxeUI.component(VxeToolbar);
+
+  VxeUI.component(VxeButton);
+  VxeUI.component(VxeButtonGroup);
+  // VxeUI.component(VxeCheckbox);
+  // VxeUI.component(VxeCheckboxGroup);
+  VxeUI.component(createVirtualComponent('VxeForm'));
+  // VxeUI.component(VxeFormGather);
+  // VxeUI.component(VxeFormItem);
+  VxeUI.component(VxeIcon);
+  VxeUI.component(VxeInput);
+  // VxeUI.component(VxeList);
+  VxeUI.component(VxeLoading);
+  // VxeUI.component(VxeModal);
+  // VxeUI.component(VxeOptgroup);
+  // VxeUI.component(VxeOption);
+  VxeUI.component(VxePager);
+  // VxeUI.component(VxePulldown);
+  // VxeUI.component(VxeRadio);
+  // VxeUI.component(VxeRadioButton);
+  // VxeUI.component(VxeRadioGroup);
+  VxeUI.component(VxeSelect);
+  // VxeUI.component(VxeSwitch);
+  // VxeUI.component(VxeTextarea);
+  VxeUI.component(VxeTooltip);
+
+  isInit = true;
+}
+
+export function setupVbenVxeTable(setupOptions: SetupVxeTable) {
+  const { configVxeTable, useVbenForm } = setupOptions;
+
+  initVxeTable();
+  useTableForm = useVbenForm;
+
+  const preference = usePreferences();
+
+  const localMap = {
+    'zh-CN': zhCN,
+    'en-US': enUS,
+  };
+
+  watch(
+    [() => preference.theme.value, () => preference.locale.value],
+    ([theme, locale]) => {
+      VxeUI.setTheme(theme === 'dark' ? 'dark' : 'light');
+      VxeUI.setI18n(locale, localMap[locale]);
+      VxeUI.setLanguage(locale);
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  configVxeTable(VxeUI);
+}

+ 78 - 0
packages/effects/plugins/src/vxe-table/theme.css

@@ -0,0 +1,78 @@
+:root {
+  --vxe-ui-font-color: hsl(var(--foreground));
+  --vxe-ui-font-primary-color: hsl(var(--primary));
+
+  /* --vxe-ui-font-lighten-color: #babdc0;
+  --vxe-ui-font-darken-color: #86898e; */
+  --vxe-ui-font-disabled-color: hsl(var(--foreground) / 50%);
+
+  /* base */
+  --vxe-ui-base-popup-border-color: hsl(var(--border));
+
+  /* --vxe-ui-base-popup-box-shadow: 0px 12px 30px 8px rgb(0 0 0 / 50%); */
+
+  /* layout */
+  --vxe-ui-layout-background-color: hsl(var(--background));
+  --vxe-ui-table-resizable-line-color: hsl(var(--border));
+
+  /* --vxe-ui-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px hsl(var(--accent));
+  --vxe-ui-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px hsl(var(--accent)); */
+
+  /* input */
+  --vxe-ui-input-border-color: hsl(var(--border));
+
+  /* --vxe-ui-input-placeholder-color: #8d9095; */
+
+  /* --vxe-ui-input-disabled-background-color: #262727; */
+
+  /* loading */
+  --vxe-ui-loading-background-color: hsl(var(--overlay-content));
+
+  /* table */
+  --vxe-ui-table-header-background-color: hsl(var(--accent));
+  --vxe-ui-table-border-color: hsl(var(--border));
+  --vxe-ui-table-row-hover-background-color: hsl(var(--accent-hover));
+  --vxe-ui-table-row-striped-background-color: hsl(var(--accent) / 60%);
+  --vxe-ui-table-row-hover-striped-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-radio-checked-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-hover-radio-checked-background-color: hsl(
+    var(--accent-hover)
+  );
+  --vxe-ui-table-row-checkbox-checked-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-hover-checkbox-checked-background-color: hsl(
+    var(--accent-hover)
+  );
+  --vxe-ui-table-row-current-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover));
+
+  /* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */
+}
+
+.vxe-pager {
+  .vxe-pager--prev-btn:not(.is--disabled):active,
+  .vxe-pager--next-btn:not(.is--disabled):active,
+  .vxe-pager--num-btn:not(.is--disabled):active,
+  .vxe-pager--jump-prev:not(.is--disabled):active,
+  .vxe-pager--jump-next:not(.is--disabled):active,
+  .vxe-pager--prev-btn:not(.is--disabled):focus,
+  .vxe-pager--next-btn:not(.is--disabled):focus,
+  .vxe-pager--num-btn:not(.is--disabled):focus,
+  .vxe-pager--jump-prev:not(.is--disabled):focus,
+  .vxe-pager--jump-next:not(.is--disabled):focus {
+    color: hsl(var(--accent-foreground));
+    background-color: hsl(var(--accent));
+    border: 1px solid hsl(var(--border));
+    box-shadow: 0 0 0 1px hsl(var(--border));
+  }
+
+  .vxe-pager {
+    &--wrapper {
+      display: flex;
+      align-items: center;
+    }
+
+    &--sizes {
+      margin-right: auto;
+    }
+  }
+}

+ 53 - 0
packages/effects/plugins/src/vxe-table/types.ts

@@ -0,0 +1,53 @@
+import type { DeepPartial } from '@vben/types';
+import type { VbenFormProps } from '@vben-core/form-ui';
+import type {
+  VxeGridListeners,
+  VxeGridProps as VxeTableGridProps,
+  VxeUIExport,
+} from 'vxe-table';
+
+import type { VxeGridApi } from './api';
+
+import type { Ref } from 'vue';
+
+import { useVbenForm } from '@vben-core/form-ui';
+
+export interface VxePaginationInfo {
+  currentPage: number;
+  pageSize: number;
+  total: number;
+}
+
+export interface VxeGridProps {
+  /**
+   * 组件class
+   */
+  class?: any;
+  /**
+   * vxe-grid class
+   */
+  gridClass?: any;
+  /**
+   * vxe-grid 配置
+   */
+  gridOptions?: DeepPartial<VxeTableGridProps>;
+  /**
+   * vxe-grid 事件
+   */
+  gridEvents?: DeepPartial<VxeGridListeners>;
+  /**
+   * 表单配置
+   */
+  formOptions?: VbenFormProps;
+}
+
+export type ExtendedVxeGridApi = {
+  useStore: <T = NoInfer<VxeGridProps>>(
+    selector?: (state: NoInfer<VxeGridProps>) => T,
+  ) => Readonly<Ref<T>>;
+} & VxeGridApi;
+
+export interface SetupVxeTable {
+  configVxeTable: (ui: VxeUIExport) => void;
+  useVbenForm: typeof useVbenForm;
+}

+ 42 - 0
packages/effects/plugins/src/vxe-table/use-vxe-grid.ts

@@ -0,0 +1,42 @@
+import type { ExtendedVxeGridApi, VxeGridProps } from './types';
+
+import { defineComponent, h, onBeforeUnmount } from 'vue';
+
+import { useStore } from '@vben-core/shared/store';
+
+import { VxeGridApi } from './api';
+import VxeGrid from './use-vxe-grid.vue';
+
+export function useVbenVxeGrid(options: VxeGridProps) {
+  // const IS_REACTIVE = isReactive(options);
+  const api = new VxeGridApi(options);
+  const extendedApi: ExtendedVxeGridApi = api as ExtendedVxeGridApi;
+  extendedApi.useStore = (selector) => {
+    return useStore(api.store, selector);
+  };
+
+  const Grid = defineComponent(
+    (props: VxeGridProps, { attrs, slots }) => {
+      onBeforeUnmount(() => {
+        api.unmount();
+      });
+      return () => h(VxeGrid, { ...props, ...attrs, api: extendedApi }, slots);
+    },
+    {
+      inheritAttrs: false,
+      name: 'VbenVxeGrid',
+    },
+  );
+  // Add reactivity support
+  // if (IS_REACTIVE) {
+  //   watch(
+  //     () => options,
+  //     () => {
+  //       api.setState(options);
+  //     },
+  //     { immediate: true },
+  //   );
+  // }
+
+  return [Grid, extendedApi] as const;
+}

+ 264 - 0
packages/effects/plugins/src/vxe-table/use-vxe-grid.vue

@@ -0,0 +1,264 @@
+<script lang="ts" setup>
+import type { VbenFormProps } from '@vben-core/form-ui';
+import type {
+  VxeGridInstance,
+  VxeGridProps as VxeTableGridProps,
+} from 'vxe-table';
+
+import type { ExtendedVxeGridApi, VxeGridProps } from './types';
+
+import {
+  computed,
+  nextTick,
+  onMounted,
+  toRaw,
+  useSlots,
+  useTemplateRef,
+} from 'vue';
+
+import { usePriorityValues } from '@vben/hooks';
+import { EmptyIcon } from '@vben/icons';
+import { $t } from '@vben/locales';
+import { cloneDeep, cn, mergeWithArrayOverride } from '@vben/utils';
+import { VbenLoading } from '@vben-core/shadcn-ui';
+
+import { VxeGrid, VxeUI } from 'vxe-table';
+
+import { useTableForm } from './init';
+
+import 'vxe-table/styles/cssvar.scss';
+import 'vxe-pc-ui/styles/cssvar.scss';
+import './theme.css';
+
+interface Props extends VxeGridProps {
+  api: ExtendedVxeGridApi;
+}
+
+const props = withDefaults(defineProps<Props>(), {});
+
+const gridRef = useTemplateRef<VxeGridInstance>('gridRef');
+
+const state = props.api?.useStore?.();
+
+const {
+  gridOptions,
+  class: className,
+  gridClass,
+  gridEvents,
+  formOptions,
+} = usePriorityValues(props, state);
+
+const slots = useSlots();
+
+const [Form, formApi] = useTableForm({});
+
+const showToolbar = computed(() => {
+  return !!slots['toolbar-actions']?.() || !!slots['toolbar-tools']?.();
+});
+
+const options = computed(() => {
+  const slotActions = slots['toolbar-actions']?.();
+  const slotTools = slots['toolbar-tools']?.();
+
+  const forceUseToolbarOptions = showToolbar.value
+    ? {
+        toolbarConfig: {
+          slots: {
+            ...(slotActions ? { buttons: 'toolbar-actions' } : {}),
+            ...(slotTools ? { tools: 'toolbar-tools' } : {}),
+          },
+        },
+      }
+    : {};
+
+  const mergedOptions: VxeTableGridProps = cloneDeep(
+    mergeWithArrayOverride(
+      {},
+      forceUseToolbarOptions,
+      toRaw(gridOptions.value),
+    ),
+  );
+
+  if (mergedOptions.proxyConfig) {
+    const { ajax } = mergedOptions.proxyConfig;
+    mergedOptions.proxyConfig.enabled = !!ajax;
+    // 不自动加载数据, 由组件控制
+    mergedOptions.proxyConfig.autoLoad = false;
+  }
+
+  if (!showToolbar.value && mergedOptions.toolbarConfig) {
+    mergedOptions.toolbarConfig.enabled = false;
+  }
+
+  if (mergedOptions.pagerConfig) {
+    mergedOptions.pagerConfig = mergeWithArrayOverride(
+      {},
+      mergedOptions.pagerConfig,
+      {
+        pageSize: 20,
+        background: true,
+        pageSizes: [10, 20, 30, 50, 100, 200],
+        className: 'mt-2 w-full',
+        layouts: [
+          'Total',
+          'Sizes',
+          'Home',
+          'PrevJump',
+          'PrevPage',
+          'Number',
+          'NextPage',
+          'NextJump',
+          'End',
+          // 'FullJump',
+        ] as any[],
+        size: 'mini' as const,
+      },
+    );
+  }
+  if (mergedOptions.formConfig) {
+    mergedOptions.formConfig.enabled = false;
+  }
+  return mergedOptions;
+});
+
+const events = computed(() => {
+  return {
+    ...gridEvents.value,
+  };
+});
+
+const vbenFormOptions = computed(() => {
+  const defaultFormProps: VbenFormProps = {
+    handleSubmit: async () => {
+      const formValues = formApi.form.values;
+      props.api.reload(formValues);
+    },
+    handleReset: async () => {
+      formApi.resetForm();
+      const formValues = formApi.form.values;
+      props.api.reload(formValues);
+    },
+    collapseTriggerResize: true,
+    commonConfig: {
+      componentProps: {
+        class: 'w-full',
+      },
+    },
+    showCollapseButton: true,
+    submitButtonOptions: {
+      text: $t('common.query'),
+    },
+    wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
+  };
+  return {
+    ...mergeWithArrayOverride({}, formOptions.value, defaultFormProps),
+  };
+});
+
+const delegatedSlots = computed(() => {
+  const resultSlots: string[] = [];
+
+  for (const key of Object.keys(slots)) {
+    if (!['empty', 'form', 'loading'].includes(key)) {
+      resultSlots.push(key);
+    }
+  }
+  return resultSlots;
+});
+
+const delegatedFormSlots = computed(() => {
+  const resultSlots: string[] = [];
+
+  for (const key of Object.keys(slots)) {
+    if (key.startsWith('form-')) {
+      resultSlots.push(key);
+    }
+  }
+  return resultSlots;
+});
+
+async function init() {
+  await nextTick();
+  const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
+  const defaultGridOptions: VxeTableGridProps = mergeWithArrayOverride(
+    {},
+    toRaw(gridOptions.value),
+    toRaw(globalGridConfig),
+  );
+  // 内部主动加载数据,防止form的默认值影响
+  const autoLoad = defaultGridOptions.proxyConfig?.autoLoad;
+  const enableProxyConfig = options.value.proxyConfig?.enabled;
+  if (enableProxyConfig && autoLoad) {
+    props.api.reload(formApi.form.values);
+  }
+
+  // form 由 vben-form代替,所以不适配formConfig,这里给出警告
+  const formConfig = defaultGridOptions.formConfig;
+  if (formConfig?.enabled) {
+    console.warn(
+      '[Vben Vxe Table]: The formConfig in the grid is not supported, please use the `formOptions` props',
+    );
+  }
+}
+
+onMounted(() => {
+  props.api?.mount?.(gridRef.value);
+  init();
+});
+</script>
+
+<template>
+  <div :class="cn('bg-card h-full rounded-md', className)">
+    <VxeGrid
+      ref="gridRef"
+      :class="
+        cn(
+          'p-2',
+          {
+            'pt-0': showToolbar && !formOptions,
+          },
+          gridClass,
+        )
+      "
+      v-bind="options"
+      v-on="events"
+    >
+      <template
+        v-for="slotName in delegatedSlots"
+        :key="slotName"
+        #[slotName]="slotProps"
+      >
+        <slot :name="slotName" v-bind="slotProps"></slot>
+      </template>
+      <template #form>
+        <div v-if="formOptions" class="relative rounded py-3 pb-6">
+          <slot name="form">
+            <Form v-bind="vbenFormOptions">
+              <template
+                v-for="slotName in delegatedFormSlots"
+                :key="slotName"
+                #[slotName]="slotProps"
+              >
+                <slot :name="slotName" v-bind="slotProps"></slot>
+              </template>
+            </Form>
+          </slot>
+          <div
+            class="bg-background-deep z-100 absolute -left-2 bottom-2 h-4 w-[calc(100%+1rem)] overflow-hidden"
+          ></div>
+        </div>
+      </template>
+      <template #loading>
+        <slot name="loading">
+          <VbenLoading :spinning="true" />
+        </slot>
+      </template>
+      <template #empty>
+        <slot name="empty">
+          <EmptyIcon class="mx-auto" />
+          <div class="mt-2">{{ $t('common.noData') }}</div>
+        </slot>
+      </template>
+    </VxeGrid>
+  </div>
+</template>

+ 1 - 0
packages/effects/plugins/tailwind.config.mjs

@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config';

+ 2 - 1
packages/locales/src/langs/en-US.json

@@ -31,7 +31,8 @@
     "confirm": "Comfirm",
     "noData": "No Data",
     "refresh": "Refresh",
-    "loadingMenu": "Loading Menu"
+    "loadingMenu": "Loading Menu",
+    "query": "Search"
   },
   "fallback": {
     "pageNotFound": "Oops! Page Not Found",

+ 2 - 1
packages/locales/src/langs/zh-CN.json

@@ -31,7 +31,8 @@
     "confirm": "确认",
     "noData": "暂无数据",
     "refresh": "刷新",
-    "loadingMenu": "加载菜单中"
+    "loadingMenu": "加载菜单中",
+    "query": "查询"
   },
   "fallback": {
     "pageNotFound": "哎呀!未找到页面",

+ 1 - 0
playground/src/adapter/index.ts

@@ -1 +1,2 @@
 export * from './form';
+export * from './vxe-table';

+ 59 - 0
playground/src/adapter/vxe-table.ts

@@ -0,0 +1,59 @@
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { Button, Image } from 'ant-design-vue';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+  configVxeTable: (vxeUI) => {
+    vxeUI.setConfig({
+      grid: {
+        align: 'center',
+        border: true,
+        minHeight: 180,
+        proxyConfig: {
+          autoLoad: true,
+          response: {
+            result: 'items',
+            total: 'total',
+            list: 'items',
+          },
+          showActiveMsg: true,
+          showResponseMsg: false,
+        },
+        round: true,
+        size: 'small',
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellImage' },
+    vxeUI.renderer.add('CellImage', {
+      renderDefault(_renderOpts, params) {
+        const { column, row } = params;
+        return h(Image, { src: row[column.field] });
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellLink' },
+    vxeUI.renderer.add('CellLink', {
+      renderDefault(renderOpts) {
+        const { props } = renderOpts;
+        return h(
+          Button,
+          { size: 'small', type: 'link' },
+          { default: () => props?.text },
+        );
+      },
+    });
+
+    // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+    // vxeUI.formats.add
+  },
+  useVbenForm,
+});
+
+export { useVbenVxeGrid };
+
+export type * from '@vben/plugins/vxe-table';

+ 1 - 0
playground/src/api/demos/index.ts → playground/src/api/examples/index.ts

@@ -1 +1,2 @@
 export * from './status';
+export * from './table';

+ 0 - 0
playground/src/api/demos/status.ts → playground/src/api/examples/status.ts


+ 18 - 0
playground/src/api/examples/table.ts

@@ -0,0 +1,18 @@
+import { requestClient } from '#/api/request';
+
+export namespace DemoTableApi {
+  export interface PageFetchParams {
+    [key: string]: any;
+    page: number;
+    pageSize: number;
+  }
+}
+
+/**
+ * 获取示例表格数据
+ */
+async function getExampleTableApi(params: DemoTableApi.PageFetchParams) {
+  return requestClient.get('/table/list', { params });
+}
+
+export { getExampleTableApi };

+ 1 - 1
playground/src/api/index.ts

@@ -1,2 +1,2 @@
 export * from './core';
-export * from './demos';
+export * from './examples';

+ 12 - 0
playground/src/locales/langs/en-US.json

@@ -82,6 +82,18 @@
         "api": "Api",
         "merge": "Merge Form"
       },
+      "vxeTable": {
+        "title": "Vxe Table",
+        "basic": "Basic Table",
+        "remote": "Remote Load",
+        "tree": "Tree Table",
+        "fixed": "Fixed Header/Column",
+        "virtual": "Virtual Scroll",
+        "editCell": "Edit Cell",
+        "editRow": "Edit Row",
+        "custom-cell": "Custom Cell",
+        "form": "Form Table"
+      },
       "captcha": {
         "title": "Captcha",
         "pointSelection": "Point Selection Captcha",

+ 12 - 0
playground/src/locales/langs/zh-CN.json

@@ -82,6 +82,18 @@
         "api": "Api",
         "merge": "合并表单"
       },
+      "vxeTable": {
+        "title": "Vxe 表格",
+        "basic": "基础表格",
+        "remote": "远程加载",
+        "tree": "树形表格",
+        "fixed": "固定表头/列",
+        "virtual": "虚拟滚动",
+        "editCell": "单元格编辑",
+        "editRow": "行编辑",
+        "custom-cell": "自定义单元格",
+        "form": "开启搜索表单"
+      },
       "captcha": {
         "title": "验证码",
         "pointSelection": "点选验证",

+ 83 - 1
playground/src/router/routes/modules/examples.ts

@@ -42,7 +42,6 @@ const routes: RouteRecordRaw[] = [
           title: $t('page.examples.ellipsis.title'),
         },
       },
-
       {
         name: 'FormExample',
         path: '/examples/form',
@@ -109,6 +108,89 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      {
+        name: 'VxeTableExample',
+        path: '/examples/vxe-table',
+        meta: {
+          icon: 'lucide:table',
+          title: $t('page.examples.vxeTable.title'),
+        },
+        children: [
+          {
+            name: 'VxeTableBasicExample',
+            path: '/examples/vxe-table/basic',
+            component: () => import('#/views/examples/vxe-table/basic.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.basic'),
+            },
+          },
+          {
+            name: 'VxeTableRemoteExample',
+            path: '/examples/vxe-table/remote',
+            component: () => import('#/views/examples/vxe-table/remote.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.remote'),
+            },
+          },
+          {
+            name: 'VxeTableTreeExample',
+            path: '/examples/vxe-table/tree',
+            component: () => import('#/views/examples/vxe-table/tree.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.tree'),
+            },
+          },
+          {
+            name: 'VxeTableFixedExample',
+            path: '/examples/vxe-table/fixed',
+            component: () => import('#/views/examples/vxe-table/fixed.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.fixed'),
+            },
+          },
+          {
+            name: 'VxeTableCustomCellExample',
+            path: '/examples/vxe-table/custom-cell',
+            component: () =>
+              import('#/views/examples/vxe-table/custom-cell.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.custom-cell'),
+            },
+          },
+          {
+            name: 'VxeTableFormExample',
+            path: '/examples/vxe-table/form',
+            component: () => import('#/views/examples/vxe-table/form.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.form'),
+            },
+          },
+          {
+            name: 'VxeTableEditCellExample',
+            path: '/examples/vxe-table/edit-cell',
+            component: () => import('#/views/examples/vxe-table/edit-cell.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.editCell'),
+            },
+          },
+          {
+            name: 'VxeTableEditRowExample',
+            path: '/examples/vxe-table/edit-row',
+            component: () => import('#/views/examples/vxe-table/edit-row.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.editRow'),
+            },
+          },
+          {
+            name: 'VxeTableVirtualExample',
+            path: '/examples/vxe-table/virtual',
+            component: () => import('#/views/examples/vxe-table/virtual.vue'),
+            meta: {
+              title: $t('page.examples.vxeTable.virtual'),
+            },
+          },
+        ],
+      },
       {
         name: 'CaptchaExample',
         path: '/examples/captcha',

+ 1 - 1
playground/src/views/examples/doc-button.vue

@@ -12,5 +12,5 @@ function handleClick() {
 </script>
 
 <template>
-  <Button type="link" @click="handleClick">查看组件文档</Button>
+  <Button @click="handleClick">查看组件文档</Button>
 </template>

+ 5 - 0
playground/src/views/examples/form/basic.vue

@@ -6,6 +6,8 @@ import dayjs from 'dayjs';
 
 import { useVbenForm } from '#/adapter';
 
+import DocButton from '../doc-button.vue';
+
 const [BaseForm, baseFormApi] = useVbenForm({
   // 所有表单项共用,可单独在表单内覆盖
   commonConfig: {
@@ -329,6 +331,9 @@ function handleSetFormValue() {
     description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。"
     title="表单组件"
   >
+    <template #extra>
+      <DocButton path="/components/common-ui/vben-form" />
+    </template>
     <Card title="基础示例">
       <template #extra>
         <Button type="primary" @click="handleSetFormValue">设置表单值</Button>

+ 93 - 0
playground/src/views/examples/vxe-table/basic.vue

@@ -0,0 +1,93 @@
+<script lang="ts" setup>
+import type { VxeGridListeners, VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { Button, message } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+
+import DocButton from '../doc-button.vue';
+import { MOCK_TABLE_DATA } from './table-data';
+
+interface RowType {
+  address: string;
+  age: number;
+  id: number;
+  name: string;
+  nickname: string;
+  role: string;
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { field: 'name', title: 'Name' },
+    { field: 'age', sortable: true, title: 'Age' },
+    { field: 'nickname', title: 'Nickname' },
+    { field: 'role', title: 'Role' },
+    { field: 'address', showOverflow: true, title: 'Address' },
+  ],
+  data: MOCK_TABLE_DATA,
+  sortConfig: {
+    multiple: true,
+  },
+};
+
+const gridEvents: VxeGridListeners<RowType> = {
+  cellClick: ({ row }) => {
+    message.info(`cell-click: ${row.name}`);
+  },
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({ gridEvents, gridOptions });
+
+const showBorder = gridApi.useStore((state) => state.gridOptions?.border);
+const showStripe = gridApi.useStore((state) => state.gridOptions?.stripe);
+
+function changeBorder() {
+  gridApi.setGridOptions({
+    border: !showBorder.value,
+  });
+}
+
+function changeStripe() {
+  gridApi.setGridOptions({
+    stripe: !showStripe.value,
+  });
+}
+
+function changeLoading() {
+  gridApi.setLoading(true);
+  setTimeout(() => {
+    gridApi.setLoading(false);
+  }, 2000);
+}
+</script>
+
+<template>
+  <Page
+    description="表格组件常用于快速开发数据展示与交互界面,示例数据为静态数据。该组件是对vxe-table进行简单的二次封装,大部分属性与方法与vxe-table保持一致。"
+    title="表格基础示例"
+  >
+    <template #extra>
+      <DocButton path="/components/common-ui/vben-vxe-table" />
+    </template>
+    <Grid>
+      <template #toolbar-actions>
+        <Button class="mr-2" type="primary">左右按钮插槽</Button>
+      </template>
+      <template #toolbar-tools>
+        <Button class="mr-2" type="primary" @click="changeBorder">
+          {{ showBorder ? '隐藏' : '显示' }}边框
+        </Button>
+        <Button class="mr-2" type="primary" @click="changeLoading">
+          显示loading
+        </Button>
+        <Button class="mr-2" type="primary" @click="changeStripe">
+          {{ showStripe ? '隐藏' : '显示' }}斑马纹
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 110 - 0
playground/src/views/examples/vxe-table/custom-cell.vue

@@ -0,0 +1,110 @@
+<script lang="ts" setup>
+import type { VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { Button, Image, Switch, Tag } from 'ant-design-vue';
+import dayjs from 'dayjs';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { getExampleTableApi } from '#/api';
+
+interface RowType {
+  category: string;
+  color: string;
+  id: string;
+  imageUrl: string;
+  open: boolean;
+  price: string;
+  productName: string;
+  releaseDate: string;
+  status: 'error' | 'success' | 'warning';
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  checkboxConfig: {
+    highlight: true,
+    labelField: 'name',
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { field: 'category', title: 'Category', width: 100 },
+    {
+      field: 'imageUrl',
+      slots: { default: 'image-url' },
+      title: 'Image',
+      width: 100,
+    },
+    {
+      cellRender: { name: 'CellImage' },
+      field: 'imageUrl2',
+      title: 'Render Image',
+      width: 130,
+    },
+    {
+      field: 'open',
+      slots: { default: 'open' },
+      title: 'Open',
+      width: 100,
+    },
+    {
+      field: 'status',
+      slots: { default: 'status' },
+      title: 'Status',
+      width: 100,
+    },
+    { field: 'color', title: 'Color', width: 100 },
+    { field: 'productName', title: 'Product Name', width: 200 },
+    { field: 'price', title: 'Price', width: 100 },
+    {
+      field: 'releaseDate',
+      formatter: ({ cellValue }) => {
+        return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
+      },
+      title: 'Date',
+      width: 200,
+    },
+    {
+      cellRender: { name: 'CellLink', props: { text: '编辑' } },
+      field: 'action',
+      fixed: 'right',
+      title: '操作',
+      width: 120,
+    },
+  ],
+  height: 'auto',
+  keepSource: true,
+  pagerConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }) => {
+        return await getExampleTableApi({
+          page: page.currentPage,
+          pageSize: page.pageSize,
+        });
+      },
+    },
+  },
+};
+
+const [Grid] = useVbenVxeGrid({ gridOptions });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid>
+      <template #image-url="{ row }">
+        <Image :src="row.imageUrl" height="30" width="30" />
+      </template>
+      <template #open="{ row }">
+        <Switch v-model:checked="row.open" />
+      </template>
+      <template #status="{ row }">
+        <Tag :color="row.color">{{ row.status }}</Tag>
+      </template>
+      <template #action>
+        <Button type="link">编辑</Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 57 - 0
playground/src/views/examples/vxe-table/edit-cell.vue

@@ -0,0 +1,57 @@
+<script lang="ts" setup>
+import type { VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { getExampleTableApi } from '#/api';
+
+interface RowType {
+  category: string;
+  color: string;
+  id: string;
+  price: string;
+  productName: string;
+  releaseDate: string;
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { editRender: { name: 'input' }, field: 'category', title: 'Category' },
+    { editRender: { name: 'input' }, field: 'color', title: 'Color' },
+    {
+      editRender: { name: 'input' },
+      field: 'productName',
+      title: 'Product Name',
+    },
+    { field: 'price', title: 'Price' },
+    { field: 'releaseDate', title: 'Date' },
+  ],
+  editConfig: {
+    mode: 'cell',
+    trigger: 'click',
+  },
+  height: 'auto',
+  pagerConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }) => {
+        return await getExampleTableApi({
+          page: page.currentPage,
+          pageSize: page.pageSize,
+        });
+      },
+    },
+  },
+  showOverflow: true,
+};
+
+const [Grid] = useVbenVxeGrid({ gridOptions });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid />
+  </Page>
+</template>

+ 94 - 0
playground/src/views/examples/vxe-table/edit-row.vue

@@ -0,0 +1,94 @@
+<script lang="ts" setup>
+import type { VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { Button, message } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { getExampleTableApi } from '#/api';
+
+interface RowType {
+  category: string;
+  color: string;
+  id: string;
+  price: string;
+  productName: string;
+  releaseDate: string;
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { editRender: { name: 'input' }, field: 'category', title: 'Category' },
+    { editRender: { name: 'input' }, field: 'color', title: 'Color' },
+    {
+      editRender: { name: 'input' },
+      field: 'productName',
+      title: 'Product Name',
+    },
+    { field: 'price', title: 'Price' },
+    { field: 'releaseDate', title: 'Date' },
+    { slots: { default: 'action' }, title: '操作' },
+  ],
+  editConfig: {
+    mode: 'row',
+    trigger: 'click',
+  },
+  height: 'auto',
+  pagerConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }) => {
+        return await getExampleTableApi({
+          page: page.currentPage,
+          pageSize: page.pageSize,
+        });
+      },
+    },
+  },
+  showOverflow: true,
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
+
+function hasEditStatus(row: RowType) {
+  return gridApi.grid?.isEditByRow(row);
+}
+
+function editRowEvent(row: RowType) {
+  gridApi.grid?.setEditRow(row);
+}
+
+async function saveRowEvent(row: RowType) {
+  await gridApi.grid?.clearEdit();
+
+  gridApi.setLoading(true);
+  setTimeout(() => {
+    gridApi.setLoading(false);
+    message.success({
+      content: `保存成功!category=${row.category}`,
+    });
+  }, 600);
+}
+
+const cancelRowEvent = (_row: RowType) => {
+  gridApi.grid?.clearEdit();
+};
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid>
+      <template #action="{ row }">
+        <template v-if="hasEditStatus(row)">
+          <Button type="link" @click="saveRowEvent(row)">保存</Button>
+          <Button type="link" @click="cancelRowEvent(row)">取消</Button>
+        </template>
+        <template v-else>
+          <Button type="link" @click="editRowEvent(row)">编辑</Button>
+        </template>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 64 - 0
playground/src/views/examples/vxe-table/fixed.vue

@@ -0,0 +1,64 @@
+<script lang="ts" setup>
+import type { VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { getExampleTableApi } from '#/api';
+
+interface RowType {
+  category: string;
+  color: string;
+  id: string;
+  price: string;
+  productName: string;
+  releaseDate: string;
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  columns: [
+    { fixed: 'left', title: '序号', type: 'seq', width: 50 },
+    { field: 'category', title: 'Category', width: 300 },
+    { field: 'color', title: 'Color', width: 300 },
+    { field: 'productName', title: 'Product Name', width: 300 },
+    { field: 'price', title: 'Price', width: 300 },
+    { field: 'releaseDate', title: 'Date', width: 500 },
+    {
+      field: 'action',
+      fixed: 'right',
+      slots: { default: 'action' },
+      title: '操作',
+      width: 120,
+    },
+  ],
+  height: 'auto',
+  pagerConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }) => {
+        return await getExampleTableApi({
+          page: page.currentPage,
+          pageSize: page.pageSize,
+        });
+      },
+    },
+  },
+  rowConfig: {
+    isHover: true,
+  },
+};
+
+const [Grid] = useVbenVxeGrid({ gridOptions });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid>
+      <template #action>
+        <Button type="link">编辑</Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 102 - 0
playground/src/views/examples/vxe-table/form.vue

@@ -0,0 +1,102 @@
+<script lang="ts" setup>
+import type { VbenFormProps, VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { getExampleTableApi } from '#/api';
+
+interface RowType {
+  category: string;
+  color: string;
+  id: string;
+  price: string;
+  productName: string;
+  releaseDate: string;
+}
+
+const formOptions: VbenFormProps = {
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'category',
+      label: 'Category',
+    },
+    {
+      component: 'Input',
+      fieldName: 'productName',
+      label: 'ProductName',
+    },
+    {
+      component: 'Input',
+      fieldName: 'price',
+      label: 'Price',
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          {
+            label: 'Color1',
+            value: '1',
+          },
+          {
+            label: 'Color2',
+            value: '2',
+          },
+        ],
+        placeholder: '请选择',
+      },
+      fieldName: 'color',
+      label: 'Color',
+    },
+    {
+      component: 'DatePicker',
+      fieldName: 'datePicker',
+      label: 'Date',
+    },
+  ],
+};
+
+const gridOptions: VxeGridProps<RowType> = {
+  checkboxConfig: {
+    highlight: true,
+    labelField: 'name',
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { align: 'left', title: 'Name', type: 'checkbox', width: 100 },
+    { field: 'category', title: 'Category' },
+    { field: 'color', title: 'Color' },
+    { field: 'productName', title: 'Product Name' },
+    { field: 'price', title: 'Price' },
+    { field: 'releaseDate', title: 'Date' },
+  ],
+  height: 'auto',
+  keepSource: true,
+  pagerConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }, formValues) => {
+        message.success(`Query params: ${JSON.stringify(formValues)}`);
+        return await getExampleTableApi({
+          page: page.currentPage,
+          pageSize: page.pageSize,
+          ...formValues,
+        });
+      },
+    },
+  },
+};
+
+const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid />
+  </Page>
+</template>

+ 65 - 0
playground/src/views/examples/vxe-table/remote.vue

@@ -0,0 +1,65 @@
+<script lang="ts" setup>
+import type { VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { getExampleTableApi } from '#/api';
+
+interface RowType {
+  category: string;
+  color: string;
+  id: string;
+  price: string;
+  productName: string;
+  releaseDate: string;
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  checkboxConfig: {
+    highlight: true,
+    labelField: 'name',
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { align: 'left', title: 'Name', type: 'checkbox', width: 100 },
+    { field: 'category', title: 'Category' },
+    { field: 'color', title: 'Color' },
+    { field: 'productName', title: 'Product Name' },
+    { field: 'price', title: 'Price' },
+    { field: 'releaseDate', title: 'Date' },
+  ],
+  height: 'auto',
+  keepSource: true,
+  pagerConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }) => {
+        return await getExampleTableApi({
+          page: page.currentPage,
+          pageSize: page.pageSize,
+        });
+      },
+    },
+  },
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid>
+      <template #toolbar-tools>
+        <Button class="mr-2" type="primary" @click="() => gridApi.query()">
+          刷新当前页面
+        </Button>
+        <Button type="primary" @click="() => gridApi.reload()">
+          刷新并返回第一页
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 172 - 0
playground/src/views/examples/vxe-table/table-data.ts

@@ -0,0 +1,172 @@
+interface TableRowData {
+  address: string;
+  age: number;
+  id: number;
+  name: string;
+  nickname: string;
+  role: string;
+}
+
+const roles = ['User', 'Admin', 'Manager', 'Guest'];
+
+export const MOCK_TABLE_DATA: TableRowData[] = (() => {
+  const data: TableRowData[] = [];
+  for (let i = 0; i < 40; i++) {
+    data.push({
+      address: `New York${i}`,
+      age: i + 1,
+      id: i,
+      name: `Test${i}`,
+      nickname: `Test${i}`,
+      role: roles[Math.floor(Math.random() * roles.length)] as string,
+    });
+  }
+  return data;
+})();
+
+export const MOCK_TREE_TABLE_DATA = [
+  {
+    date: '2020-08-01',
+    id: 10_000,
+    name: 'Test1',
+    parentId: null,
+    size: 1024,
+    type: 'mp3',
+  },
+  {
+    date: '2021-04-01',
+    id: 10_050,
+    name: 'Test2',
+    parentId: null,
+    size: 0,
+    type: 'mp4',
+  },
+  {
+    date: '2020-03-01',
+    id: 24_300,
+    name: 'Test3',
+    parentId: 10_050,
+    size: 1024,
+    type: 'avi',
+  },
+  {
+    date: '2021-04-01',
+    id: 20_045,
+    name: 'Test4',
+    parentId: 24_300,
+    size: 600,
+    type: 'html',
+  },
+  {
+    date: '2021-04-01',
+    id: 10_053,
+    name: 'Test5',
+    parentId: 24_300,
+    size: 0,
+    type: 'avi',
+  },
+  {
+    date: '2021-10-01',
+    id: 24_330,
+    name: 'Test6',
+    parentId: 10_053,
+    size: 25,
+    type: 'txt',
+  },
+  {
+    date: '2020-01-01',
+    id: 21_011,
+    name: 'Test7',
+    parentId: 10_053,
+    size: 512,
+    type: 'pdf',
+  },
+  {
+    date: '2021-06-01',
+    id: 22_200,
+    name: 'Test8',
+    parentId: 10_053,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2020-11-01',
+    id: 23_666,
+    name: 'Test9',
+    parentId: null,
+    size: 2048,
+    type: 'xlsx',
+  },
+  {
+    date: '2021-06-01',
+    id: 23_677,
+    name: 'Test10',
+    parentId: 23_666,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2021-06-01',
+    id: 23_671,
+    name: 'Test11',
+    parentId: 23_677,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2021-06-01',
+    id: 23_672,
+    name: 'Test12',
+    parentId: 23_677,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2021-06-01',
+    id: 23_688,
+    name: 'Test13',
+    parentId: 23_666,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2021-06-01',
+    id: 23_681,
+    name: 'Test14',
+    parentId: 23_688,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2021-06-01',
+    id: 23_682,
+    name: 'Test15',
+    parentId: 23_688,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2020-10-01',
+    id: 24_555,
+    name: 'Test16',
+    parentId: null,
+    size: 224,
+    type: 'avi',
+  },
+  {
+    date: '2021-06-01',
+    id: 24_566,
+    name: 'Test17',
+    parentId: 24_555,
+    size: 1024,
+    type: 'js',
+  },
+  {
+    date: '2021-06-01',
+    id: 24_577,
+    name: 'Test18',
+    parentId: 24_555,
+    size: 1024,
+    type: 'js',
+  },
+];

+ 59 - 0
playground/src/views/examples/vxe-table/tree.vue

@@ -0,0 +1,59 @@
+<script lang="ts" setup>
+import type { VxeGridProps } from '#/adapter';
+
+import { Page } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+
+import { MOCK_TREE_TABLE_DATA } from './table-data';
+
+interface RowType {
+  date: string;
+  id: number;
+  name: string;
+  parentId: null | number;
+  size: number;
+  type: string;
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  columns: [
+    { type: 'seq', width: 70 },
+    { field: 'name', minWidth: 300, title: 'Name', treeNode: true },
+    { field: 'size', title: 'Size' },
+    { field: 'type', title: 'Type' },
+    { field: 'date', title: 'Date' },
+  ],
+  data: MOCK_TREE_TABLE_DATA,
+  treeConfig: {
+    parentField: 'parentId',
+    rowField: 'id',
+    transform: true,
+  },
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
+
+const expandAll = () => {
+  gridApi.grid?.setAllTreeExpand(true);
+};
+
+const collapseAll = () => {
+  gridApi.grid?.setAllTreeExpand(false);
+};
+</script>
+
+<template>
+  <Page>
+    <Grid>
+      <template #toolbar-tools>
+        <Button class="mr-2" type="primary" @click="expandAll">
+          展开全部
+        </Button>
+        <Button type="primary" @click="collapseAll"> 折叠全部 </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 63 - 0
playground/src/views/examples/vxe-table/virtual.vue

@@ -0,0 +1,63 @@
+<script lang="ts" setup>
+import type { VxeGridProps } from '#/adapter';
+
+import { onMounted } from 'vue';
+
+import { Page } from '@vben/common-ui';
+
+import { useVbenVxeGrid } from '#/adapter';
+
+interface RowType {
+  id: number;
+  name: string;
+  role: string;
+  sex: string;
+}
+
+const gridOptions: VxeGridProps<RowType> = {
+  columns: [
+    { type: 'seq', width: 70 },
+    { field: 'name', title: 'Name' },
+    { field: 'role', title: 'Role' },
+    { field: 'sex', title: 'Sex' },
+  ],
+  data: [],
+  height: 'auto',
+  scrollY: {
+    enabled: true,
+    gt: 0,
+  },
+  showOverflow: true,
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
+
+// 模拟行数据
+const loadList = (size = 200) => {
+  try {
+    const dataList: RowType[] = [];
+    for (let i = 0; i < size; i++) {
+      dataList.push({
+        id: 10_000 + i,
+        name: `Test${i}`,
+        role: 'Developer',
+        sex: '男',
+      });
+    }
+    gridApi.setGridOptions({ data: dataList });
+  } catch (error) {
+    console.error('Failed to load data:', error);
+    // Implement user-friendly error handling
+  }
+};
+
+onMounted(() => {
+  loadList(1000);
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Grid />
+  </Page>
+</template>

+ 102 - 12
pnpm-lock.yaml

@@ -27,9 +27,12 @@ catalogs:
     '@eslint/js':
       specifier: ^9.11.1
       version: 9.11.1
+    '@faker-js/faker':
+      specifier: ^9.0.3
+      version: 9.0.3
     '@iconify/json':
-      specifier: ^2.2.255
-      version: 2.2.255
+      specifier: ^2.2.256
+      version: 2.2.256
     '@iconify/tailwind':
       specifier: ^1.1.3
       version: 1.1.3
@@ -438,6 +441,9 @@ catalogs:
     vite-plugin-html:
       specifier: ^3.2.2
       version: 3.2.2
+    vite-plugin-lazy-import:
+      specifier: ^1.0.7
+      version: 1.0.7
     vite-plugin-lib-inject-css:
       specifier: ^2.1.1
       version: 2.1.1
@@ -468,6 +474,12 @@ catalogs:
     vue-tsc:
       specifier: ^2.1.6
       version: 2.1.6
+    vxe-pc-ui:
+      specifier: ^4.2.9
+      version: 4.2.12
+    vxe-table:
+      specifier: ^4.7.84
+      version: 4.7.85
     watermark-js-plus:
       specifier: ^1.5.7
       version: 1.5.7
@@ -590,6 +602,9 @@ importers:
 
   apps/backend-mock:
     dependencies:
+      '@faker-js/faker':
+        specifier: 'catalog:'
+        version: 9.0.3
       jsonwebtoken:
         specifier: 'catalog:'
         version: 9.0.2
@@ -802,15 +817,9 @@ importers:
       '@vben/common-ui':
         specifier: workspace:*
         version: link:../packages/effects/common-ui
-      '@vben/hooks':
-        specifier: workspace:*
-        version: link:../packages/effects/hooks
       '@vben/locales':
         specifier: workspace:*
         version: link:../packages/locales
-      '@vben/preferences':
-        specifier: workspace:*
-        version: link:../packages/preferences
       '@vben/styles':
         specifier: workspace:*
         version: link:../packages/styles
@@ -1042,7 +1051,7 @@ importers:
     dependencies:
       '@iconify/json':
         specifier: 'catalog:'
-        version: 2.2.255
+        version: 2.2.256
       '@iconify/tailwind':
         specifier: 'catalog:'
         version: 1.1.3
@@ -1171,6 +1180,9 @@ importers:
       vite-plugin-html:
         specifier: 'catalog:'
         version: 3.2.2(vite@5.4.8(@types/node@22.7.4)(less@4.2.0)(sass@1.79.4)(terser@5.33.0))
+      vite-plugin-lazy-import:
+        specifier: 'catalog:'
+        version: 1.0.7
 
   packages/@core/base/design: {}
 
@@ -1584,9 +1596,33 @@ importers:
 
   packages/effects/plugins:
     dependencies:
+      '@vben-core/form-ui':
+        specifier: workspace:*
+        version: link:../../@core/ui-kit/form-ui
+      '@vben-core/shadcn-ui':
+        specifier: workspace:*
+        version: link:../../@core/ui-kit/shadcn-ui
+      '@vben-core/shared':
+        specifier: workspace:*
+        version: link:../../@core/base/shared
+      '@vben/hooks':
+        specifier: workspace:*
+        version: link:../hooks
+      '@vben/icons':
+        specifier: workspace:*
+        version: link:../../icons
+      '@vben/locales':
+        specifier: workspace:*
+        version: link:../../locales
       '@vben/preferences':
         specifier: workspace:*
         version: link:../../preferences
+      '@vben/types':
+        specifier: workspace:*
+        version: link:../../types
+      '@vben/utils':
+        specifier: workspace:*
+        version: link:../../utils
       '@vueuse/core':
         specifier: 'catalog:'
         version: 11.1.0(vue@3.5.10(typescript@5.6.2))
@@ -1596,6 +1632,12 @@ importers:
       vue:
         specifier: 3.5.10
         version: 3.5.10(typescript@5.6.2)
+      vxe-pc-ui:
+        specifier: 'catalog:'
+        version: 4.2.12
+      vxe-table:
+        specifier: 'catalog:'
+        version: 4.7.85
 
   packages/effects/request:
     dependencies:
@@ -3959,6 +4001,10 @@ packages:
     resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  '@faker-js/faker@9.0.3':
+    resolution: {integrity: sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==}
+    engines: {node: '>=18.0.0', npm: '>=9.0.0'}
+
   '@fastify/busboy@2.1.1':
     resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
     engines: {node: '>=14'}
@@ -3995,8 +4041,8 @@ packages:
   '@iconify-json/vscode-icons@1.2.2':
     resolution: {integrity: sha512-bTpT0HJDRqGkxQv8oiETNHLEnBZpnA1QaRD35CQyO7M7qgWVLx2xwn/lK6e4waojmlPC3ckMBx3WFIUUn0/Jdg==}
 
-  '@iconify/json@2.2.255':
-    resolution: {integrity: sha512-wtBKGYrKHOmRlbai6cd4yTfHakLQ4lLD68w5pb1RDf1+6o0QcvLdun1sWZNcqkOvZOZAUOUPVISqKkYDlWY6YA==}
+  '@iconify/json@2.2.256':
+    resolution: {integrity: sha512-u2RwfBUuDE3A8qx3vnXdcJMtirHc9QrRRULfGY6Il6/K76Odfrm4yVqS/fYIh+wXwWl/fZdAZEozqxpZftfnIQ==}
 
   '@iconify/tailwind@1.1.3':
     resolution: {integrity: sha512-SfyeT+2b/aKWA6DjwdevXdLUqaEqJ5xWTegD92KItaWc47IYsGuqrt/GOz4dJCPcTVCrsUjlvMpy8cNd+uV5nQ==}
@@ -5177,6 +5223,9 @@ packages:
   '@vueuse/shared@9.13.0':
     resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
 
+  '@vxe-ui/core@4.0.12':
+    resolution: {integrity: sha512-ft8f874eQSv4N9+oulFKeg8APgd8RMHeFeUUUTNckIRJ/cNi0dbR0Fe2+ZZpRl3BwRtbE2hHb2FKWmL2oyZkfw==}
+
   JSONStream@1.3.5:
     resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
     hasBin: true
@@ -6221,6 +6270,9 @@ packages:
   dom-serializer@2.0.0:
     resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
 
+  dom-zindex@1.0.6:
+    resolution: {integrity: sha512-FKWIhiU96bi3xpP9ewRMgANsoVmMUBnMnmpCT6dPMZOunVYJQmJhSRruoI0XSPoHeIif3kyEuiHbFrOJwEJaEA==}
+
   domelementtype@2.3.0:
     resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
 
@@ -10251,6 +10303,9 @@ packages:
       '@nuxt/kit':
         optional: true
 
+  vite-plugin-lazy-import@1.0.7:
+    resolution: {integrity: sha512-mE6oAObOb4wqso4AoUGi9cLjdR+4vay1RCaKJvziBuFPlziZl7J0aw2hsqRTokLVRx3bli0a0VyjMOwsNDv58A==}
+
   vite-plugin-lib-inject-css@2.1.1:
     resolution: {integrity: sha512-RIMeVnqBK/8I0E9nnQWzws6pdj5ilRMPJSnXYb6nWxNR4EmDPnksnb/ACoR5Fy7QfzULqS4gtQMrjwnNCC9zoA==}
     peerDependencies:
@@ -10417,6 +10472,12 @@ packages:
     peerDependencies:
       vue: 3.5.10
 
+  vxe-pc-ui@4.2.12:
+    resolution: {integrity: sha512-zJ7sJLCtMahW5KNgiqQE+qDuBMoiOCIc0kl/W6ByoPgX5E1KzQTE3qvRc+v7pU/4GW//Vr3No/x1RwHMJix6Kg==}
+
+  vxe-table@4.7.85:
+    resolution: {integrity: sha512-sNQ4jKnU6vZkStTK2JDDKgIz5kKCCWtTtOVl7dpNsLJ16NYWMCDlNby5m/DJC+xa0dPvSdr7+AH4TXfD1vpRFg==}
+
   warning@4.0.3:
     resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
 
@@ -10569,6 +10630,9 @@ packages:
     resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
     engines: {node: '>=12'}
 
+  xe-utils@3.5.30:
+    resolution: {integrity: sha512-5Ez6JUANpMakduiTLxrNObzqMebnM4697KvHW5okedkUjXvYgGvkbg0tABTkvwDW/Pb09v7vT68dzBOeAuOu0g==}
+
   xml-name-validator@4.0.0:
     resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
     engines: {node: '>=12'}
@@ -12914,6 +12978,8 @@ snapshots:
     dependencies:
       levn: 0.4.1
 
+  '@faker-js/faker@9.0.3': {}
+
   '@fastify/busboy@2.1.1': {}
 
   '@floating-ui/core@1.6.8':
@@ -12954,7 +13020,7 @@ snapshots:
     dependencies:
       '@iconify/types': 2.0.0
 
-  '@iconify/json@2.2.255':
+  '@iconify/json@2.2.256':
     dependencies:
       '@iconify/types': 2.0.0
       pathe: 1.1.2
@@ -14463,6 +14529,11 @@ snapshots:
       - '@vue/composition-api'
       - vue
 
+  '@vxe-ui/core@4.0.12':
+    dependencies:
+      dom-zindex: 1.0.6
+      xe-utils: 3.5.30
+
   JSONStream@1.3.5:
     dependencies:
       jsonparse: 1.3.1
@@ -15620,6 +15691,8 @@ snapshots:
       domhandler: 5.0.3
       entities: 4.5.0
 
+  dom-zindex@1.0.6: {}
+
   domelementtype@2.3.0: {}
 
   domhandler@4.3.1:
@@ -20137,6 +20210,13 @@ snapshots:
       - rollup
       - supports-color
 
+  vite-plugin-lazy-import@1.0.7:
+    dependencies:
+      '@rollup/pluginutils': 5.1.2(rollup@4.24.0)
+      es-module-lexer: 1.5.4
+      rollup: 4.24.0
+      xe-utils: 3.5.30
+
   vite-plugin-lib-inject-css@2.1.1(vite@5.4.8(@types/node@22.7.4)(less@4.2.0)(sass@1.79.4)(terser@5.33.0)):
     dependencies:
       '@ast-grep/napi': 0.22.6
@@ -20362,6 +20442,14 @@ snapshots:
       vooks: 0.2.12(vue@3.5.10(typescript@5.6.2))
       vue: 3.5.10(typescript@5.6.2)
 
+  vxe-pc-ui@4.2.12:
+    dependencies:
+      '@vxe-ui/core': 4.0.12
+
+  vxe-table@4.7.85:
+    dependencies:
+      vxe-pc-ui: 4.2.12
+
   warning@4.0.3:
     dependencies:
       loose-envify: 1.4.0
@@ -20584,6 +20672,8 @@ snapshots:
 
   xdg-basedir@5.1.0: {}
 
+  xe-utils@3.5.30: {}
+
   xml-name-validator@4.0.0: {}
 
   y18n@4.0.3: {}

+ 5 - 1
pnpm-workspace.yaml

@@ -21,7 +21,8 @@ catalog:
   '@commitlint/config-conventional': ^19.5.0
   '@ctrl/tinycolor': ^4.1.0
   '@eslint/js': ^9.11.1
-  '@iconify/json': ^2.2.255
+  '@faker-js/faker': ^9.0.3
+  '@iconify/json': ^2.2.256
   '@iconify/tailwind': ^1.1.3
   '@iconify/vue': ^4.1.2
   '@intlify/core-base': ^10.0.3
@@ -160,6 +161,7 @@ catalog:
   vite-plugin-compression: ^0.5.1
   vite-plugin-dts: 4.2.1
   vite-plugin-html: ^3.2.2
+  vite-plugin-lazy-import: ^1.0.7
   vite-plugin-lib-inject-css: ^2.1.1
   vite-plugin-pwa: ^0.20.5
   vite-plugin-vue-devtools: ^7.4.6
@@ -171,6 +173,8 @@ catalog:
   vue-i18n: ^10.0.3
   vue-router: ^4.4.5
   vue-tsc: ^2.1.6
+  vxe-pc-ui: ^4.2.9
+  vxe-table: ^4.7.84
   watermark-js-plus: ^1.5.7
   zod: ^3.23.8
   zod-defaults: ^0.1.3