Browse Source

feat: update data

DESKTOP-USV654P\pc 1 năm trước cách đây
mục cha
commit
e3f67df61a
100 tập tin đã thay đổi với 1712 bổ sung208 xóa
  1. 1 0
      .commitlintrc.js
  2. 1 0
      apps/backend-mock/nitro.config.ts
  3. 1 1
      apps/web-antd/package.json
  4. 9 1
      apps/web-antd/src/adapter/vxe-table.ts
  5. 2 2
      apps/web-antd/src/router/guard.ts
  6. 2 1
      apps/web-antd/src/router/routes/core.ts
  7. 7 2
      apps/web-antd/src/router/routes/index.ts
  8. 1 1
      apps/web-baicai/package.json
  9. 9 0
      apps/web-baicai/src/adapter/vxe-table.ts
  10. 2 2
      apps/web-baicai/src/api/system/config.ts
  11. 4 4
      apps/web-baicai/src/api/system/dictionary.ts
  12. 1 0
      apps/web-baicai/src/api/system/index.ts
  13. 6 6
      apps/web-baicai/src/api/system/log.ts
  14. 2 2
      apps/web-baicai/src/api/system/post.ts
  15. 5 5
      apps/web-baicai/src/api/system/query.ts
  16. 2 2
      apps/web-baicai/src/api/system/role.ts
  17. 45 0
      apps/web-baicai/src/api/system/table.ts
  18. 2 2
      apps/web-baicai/src/api/system/tenant.ts
  19. 2 2
      apps/web-baicai/src/api/system/user.ts
  20. 3 16
      apps/web-baicai/src/components/form/components/api-checkbox.vue
  21. 122 0
      apps/web-baicai/src/components/form/components/api-popup.vue
  22. 3 16
      apps/web-baicai/src/components/form/components/api-radio.vue
  23. 3 16
      apps/web-baicai/src/components/form/components/api-select.vue
  24. 3 17
      apps/web-baicai/src/components/form/components/api-tree-select.vue
  25. 30 0
      apps/web-baicai/src/components/form/helper.ts
  26. 1 1
      apps/web-baicai/src/components/select-card/index.ts
  27. 4 4
      apps/web-baicai/src/components/select-card/src/select-card-item.vue
  28. 1 1
      apps/web-baicai/src/components/select-card/src/select-card.vue
  29. 2 2
      apps/web-baicai/src/components/table-action/index.ts
  30. 0 0
      apps/web-baicai/src/components/table-action/src/table-action.vue
  31. 0 0
      apps/web-baicai/src/components/table-action/src/types.d.ts
  32. 2 2
      apps/web-baicai/src/router/guard.ts
  33. 8 6
      apps/web-baicai/src/router/routes/core.ts
  34. 8 2
      apps/web-baicai/src/router/routes/index.ts
  35. 5 5
      apps/web-baicai/src/views/system/design/query/components/edit.vue
  36. 0 0
      apps/web-baicai/src/views/system/design/query/components/step/baseConfig.vue
  37. 0 0
      apps/web-baicai/src/views/system/design/query/components/step/detailConfig.vue
  38. 186 0
      apps/web-baicai/src/views/system/design/table/components/edit.vue
  39. 129 0
      apps/web-baicai/src/views/system/design/table/components/page.vue
  40. 52 0
      apps/web-baicai/src/views/system/design/table/components/preview.vue
  41. 90 0
      apps/web-baicai/src/views/system/design/table/components/step/baseConfig.vue
  42. 176 0
      apps/web-baicai/src/views/system/design/table/components/step/buttonConfig.vue
  43. 184 0
      apps/web-baicai/src/views/system/design/table/components/step/columnConfig.vue
  44. 221 0
      apps/web-baicai/src/views/system/design/table/components/step/searchConfig.vue
  45. 53 0
      apps/web-baicai/src/views/system/design/table/data.config.ts
  46. 109 0
      apps/web-baicai/src/views/system/design/table/index.vue
  47. 1 1
      apps/web-ele/package.json
  48. 9 1
      apps/web-ele/src/adapter/vxe-table.ts
  49. 2 2
      apps/web-ele/src/router/guard.ts
  50. 2 1
      apps/web-ele/src/router/routes/core.ts
  51. 7 2
      apps/web-ele/src/router/routes/index.ts
  52. 1 1
      apps/web-naive/package.json
  53. 9 1
      apps/web-naive/src/adapter/vxe-table.ts
  54. 2 2
      apps/web-naive/src/router/guard.ts
  55. 2 1
      apps/web-naive/src/router/routes/core.ts
  56. 7 2
      apps/web-naive/src/router/routes/index.ts
  57. 1 1
      docs/package.json
  58. 8 6
      docs/src/components/common-ui/vben-modal.md
  59. 4 0
      docs/src/en/guide/essentials/route.md
  60. 1 1
      docs/src/en/guide/introduction/quick-start.md
  61. 1 1
      docs/src/guide/essentials/concept.md
  62. 11 0
      docs/src/guide/essentials/route.md
  63. 1 1
      docs/src/guide/in-depth/loading.md
  64. 2 2
      docs/src/guide/introduction/quick-start.md
  65. 1 1
      internal/lint-configs/commitlint-config/package.json
  66. 1 1
      internal/lint-configs/stylelint-config/package.json
  67. 1 5
      internal/node-utils/package.json
  68. 0 2
      internal/node-utils/src/index.ts
  69. 0 1
      internal/tailwind-config/build.config.ts
  70. 4 4
      internal/tailwind-config/package.json
  71. 2 3
      internal/tailwind-config/src/index.ts
  72. 1 1
      internal/tsconfig/package.json
  73. 1 2
      internal/vite-config/package.json
  74. 1 1
      internal/vite-config/src/config/common.ts
  75. 0 1
      internal/vite-config/src/config/library.ts
  76. 1 6
      internal/vite-config/src/plugins/index.ts
  77. 0 3
      internal/vite-config/src/typing.ts
  78. 3 2
      package.json
  79. 2 2
      packages/@core/base/design/package.json
  80. 1 1
      packages/@core/base/icons/package.json
  81. 1 1
      packages/@core/base/shared/package.json
  82. 12 1
      packages/@core/base/shared/src/utils/window.ts
  83. 1 1
      packages/@core/base/typings/package.json
  84. 4 0
      packages/@core/base/typings/src/vue-router.d.ts
  85. 1 1
      packages/@core/composables/package.json
  86. 1 1
      packages/@core/preferences/package.json
  87. 4 1
      packages/@core/ui-kit/form-ui/src/form-render/form.vue
  88. 1 1
      packages/@core/ui-kit/layout-ui/package.json
  89. 1 1
      packages/@core/ui-kit/menu-ui/package.json
  90. 7 1
      packages/@core/ui-kit/menu-ui/src/components/menu.vue
  91. 6 2
      packages/@core/ui-kit/menu-ui/src/sub-menu.vue
  92. 15 0
      packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts
  93. 29 2
      packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
  94. 10 0
      packages/@core/ui-kit/popup-ui/src/modal/modal.ts
  95. 9 1
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  96. 1 1
      packages/@core/ui-kit/shadcn-ui/package.json
  97. 7 6
      packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-background.vue
  98. 11 2
      packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue
  99. 1 1
      packages/@core/ui-kit/tabs-ui/package.json
  100. 1 1
      packages/constants/package.json

+ 1 - 0
.commitlintrc.js

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

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

@@ -1,5 +1,6 @@
 import errorHandler from './error';
 
+process.env.COMPATIBILITY_DATE = new Date().toISOString();
 export default defineNitroConfig({
   devErrorHandler: errorHandler,
   errorHandler: '~/error',

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

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

+ 9 - 1
apps/web-antd/src/adapter/vxe-table.ts

@@ -11,8 +11,15 @@ setupVbenVxeTable({
     vxeUI.setConfig({
       grid: {
         align: 'center',
-        border: true,
+        border: false,
+        columnConfig: {
+          resizable: true,
+        },
         minHeight: 180,
+        formConfig: {
+          // 全局禁用vxe-table的表单配置,使用formOptions
+          enabled: false,
+        },
         proxyConfig: {
           autoLoad: true,
           response: {
@@ -24,6 +31,7 @@ setupVbenVxeTable({
           showResponseMsg: false,
         },
         round: true,
+        showOverflow: true,
         size: 'small',
       },
     });

+ 2 - 2
apps/web-antd/src/router/guard.ts

@@ -8,7 +8,7 @@ import { startProgress, stopProgress } from '@vben/utils';
 import { useTitle } from '@vueuse/core';
 
 import { $t } from '#/locales';
-import { coreRouteNames, dynamicRoutes } from '#/router/routes';
+import { accessRoutes, coreRouteNames } from '#/router/routes';
 import { useAuthStore } from '#/store';
 
 import { generateAccess } from './access';
@@ -105,7 +105,7 @@ function setupAccessGuard(router: Router) {
       roles: userRoles,
       router,
       // 则会在菜单中显示,但是访问会被重定向到403
-      routes: dynamicRoutes,
+      routes: accessRoutes,
     });
 
     // 保存菜单信息和路由信息

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

@@ -1,6 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { DEFAULT_HOME_PATH } from '@vben/constants';
+import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 
 import { AuthPageLayout } from '#/layouts';
 import { $t } from '#/locales';
@@ -37,6 +37,7 @@ const coreRoutes: RouteRecordRaw[] = [
     },
     name: 'Authentication',
     path: '/auth',
+    redirect: LOGIN_PATH,
     children: [
       {
         name: 'Login',

+ 7 - 2
apps/web-antd/src/router/routes/index.ts

@@ -10,12 +10,16 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
 
 // 有需要可以自行打开注释,并创建文件夹
 // const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
 
 /** 动态路由 */
 const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
-/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统 */
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+/** 不需要权限的菜单列表(会显示在菜单中) */
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
 /** 路由列表,由基本路由+静态路由组成 */
@@ -28,4 +32,5 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
-export { coreRouteNames, dynamicRoutes, routes };
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };

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

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

+ 9 - 0
apps/web-baicai/src/adapter/vxe-table.ts

@@ -12,6 +12,14 @@ setupVbenVxeTable({
       grid: {
         align: 'center',
         border: true,
+        columnConfig: {
+          resizable: true,
+        },
+
+        formConfig: {
+          // 全局禁用vxe-table的表单配置,使用formOptions
+          enabled: false,
+        },
         minHeight: 180,
         proxyConfig: {
           autoLoad: true,
@@ -24,6 +32,7 @@ setupVbenVxeTable({
           showResponseMsg: false,
         },
         round: true,
+        showOverflow: true,
         size: 'small',
       },
     });

+ 2 - 2
apps/web-baicai/src/api/system/config.ts

@@ -17,10 +17,10 @@ export namespace ConfigApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
   export const getPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/config/page', { params });
+    requestClient.get<PageResult>('/config/page', { params });
 
   export const getDetail = (id: number) =>
     requestClient.get<RecordItem>('/config/entity', {

+ 4 - 4
apps/web-baicai/src/api/system/dictionary.ts

@@ -20,10 +20,10 @@ export namespace DictionaryApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
   export const getTypePage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/dictionary/type/page', { params });
+    requestClient.get<PageResult>('/dictionary/type/page', { params });
 
   export const getTypeDetail = (id: number) =>
     requestClient.get<RecordItem>('/dictionary/type/entity', {
@@ -57,7 +57,7 @@ export namespace DictionaryApi {
     requestClient.delete('/dictionary/item', { data: { id } });
 
   export const getItemList = (code: string) =>
-    requestClient.get<RecordItem>('/dictionary/item/list-code', {
-      params: code,
+    requestClient.get<RecordItem[]>('/dictionary/item/list-code', {
+      params: { code },
     });
 }

+ 1 - 0
apps/web-baicai/src/api/system/index.ts

@@ -8,5 +8,6 @@ export * from './menu';
 export * from './post';
 export * from './query';
 export * from './role';
+export * from './table';
 export * from './tenant';
 export * from './user';

+ 6 - 6
apps/web-baicai/src/api/system/log.ts

@@ -15,18 +15,18 @@ export namespace LogApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
   export const getAuditPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/logAudit/page', { params });
+    requestClient.get<PageResult>('/logAudit/page', { params });
   export const getOperatePage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/logOperate/page', { params });
+    requestClient.get<PageResult>('/logOperate/page', { params });
   export const getExceptionPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/logException/page', { params });
+    requestClient.get<PageResult>('/logException/page', { params });
   export const getDifferencePage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/logDifference/page', { params });
+    requestClient.get<PageResult>('/logDifference/page', { params });
   export const getVisitPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/logVisit/page', { params });
+    requestClient.get<PageResult>('/logVisit/page', { params });
 
   export const clearLog = (type: string) => requestClient.delete(`/${type}`);
 }

+ 2 - 2
apps/web-baicai/src/api/system/post.ts

@@ -21,10 +21,10 @@ export namespace PostApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
   export const getPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/post/page', { params });
+    requestClient.get<PageResult>('/post/page', { params });
 
   export const getDetail = (id: number) =>
     requestClient.get<RecordItem>('/post/entity', {

+ 5 - 5
apps/web-baicai/src/api/system/query.ts

@@ -24,9 +24,9 @@ export namespace QueryApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
-  export interface Columns {
+  export interface Column {
     tableAlias: string;
     alias: string;
     name: string;
@@ -38,11 +38,11 @@ export namespace QueryApi {
     name: string;
     alias: string;
     isMain: boolean;
-    columns: Columns[];
+    columns: Column[];
     columnKeys: string[];
   }
   export const getPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/query/page', { params });
+    requestClient.get<PageResult>('/query/page', { params });
 
   export const getDetail = (id: number) =>
     requestClient.get<RecordItem>('/query/entity', {
@@ -59,7 +59,7 @@ export namespace QueryApi {
     requestClient.delete('/query', { data: { id } });
 
   export const getColumns = (id: number) =>
-    requestClient.get('/query/column-list', { params: { id } });
+    requestClient.get<Column[]>('/query/column-list', { params: { id } });
 
   export const postExecute = (data: Record<string, any>) =>
     requestClient.post('/query/execute', data);

+ 2 - 2
apps/web-baicai/src/api/system/role.ts

@@ -22,7 +22,7 @@ export namespace RoleApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
   export interface StatisticsItem {
     id: number;
@@ -31,7 +31,7 @@ export namespace RoleApi {
   }
 
   export const getPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/role/page', { params });
+    requestClient.get<PageResult>('/role/page', { params });
 
   export const getDetail = (id: number) =>
     requestClient.get<RecordItem>('/role/entity', {

+ 45 - 0
apps/web-baicai/src/api/system/table.ts

@@ -0,0 +1,45 @@
+import type { BasicFetchResult, BasicPageParams } from '#/api/model';
+
+import { requestClient } from '#/api/request';
+
+export namespace TableApi {
+  export interface PageParams extends BasicPageParams {
+    name?: string;
+  }
+
+  export interface BasicRecordItem {
+    tableType: string;
+    name: string;
+    config: {
+      buttons: any[];
+      columns: any[];
+      search: any[];
+    };
+    databaseQueryId: number;
+  }
+
+  export interface RecordItem extends BasicRecordItem {
+    id: number;
+  }
+
+  export type PageResult = BasicFetchResult<RecordItem>;
+
+  export const getPage = (params: PageParams) =>
+    requestClient.get<PageResult>('/table/query/page', { params });
+
+  export const getDetail = (id: number) =>
+    requestClient.get<RecordItem>('/table/query/entity', {
+      params: { id },
+    });
+
+  export const addDetail = (data: BasicRecordItem) =>
+    requestClient.post('/table/query', data);
+
+  export const editDetail = (data: RecordItem) =>
+    requestClient.put('/table/query', data);
+
+  export const deleteDetail = (id: number) =>
+    requestClient.delete('/table/query', { data: { id } });
+  export const postExecute = (data: Record<string, any>) =>
+    requestClient.post('/table/query/execute', data);
+}

+ 2 - 2
apps/web-baicai/src/api/system/tenant.ts

@@ -29,10 +29,10 @@ export namespace TenantApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
   export const getPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/tenant/page', { params });
+    requestClient.get<PageResult>('/tenant/page', { params });
 
   export const getOptions = () =>
     requestClient.get<BasicOptionResult[]>('/tenant/options');

+ 2 - 2
apps/web-baicai/src/api/system/user.ts

@@ -26,10 +26,10 @@ export namespace UserApi {
     id: number;
   }
 
-  export type AppPageResult = BasicFetchResult<RecordItem>;
+  export type PageResult = BasicFetchResult<RecordItem>;
 
   export const getPage = (params: PageParams) =>
-    requestClient.get<AppPageResult>('/user/page', { params });
+    requestClient.get<PageResult>('/user/page', { params });
 
   export const getDetail = (id: number) =>
     requestClient.get<RecordItem>('/user/entity', {

+ 3 - 16
apps/web-baicai/src/components/form/components/api-checkbox.vue

@@ -12,10 +12,10 @@ import { isFunction } from '@vben/utils';
 import { useVModel } from '@vueuse/core';
 import { CheckboxGroup, Spin } from 'ant-design-vue';
 
-import { EnumApi, QueryApi } from '#/api';
-import { requestClient } from '#/api/request';
 import { get, omit } from '#/utils';
 
+import { createApiFunction } from '../helper';
+
 const props = defineProps({
   value: {
     type: [Array] as PropType<CheckboxValueType[]>,
@@ -72,20 +72,7 @@ const emitChange = () => {
 };
 
 const fetch = async () => {
-  const api: any =
-    typeof props.api.url === 'string' || !props.api.url
-      ? (params: any) => {
-          if (props.api.type === 'enum') {
-            return EnumApi.getList(params);
-          } else if (props.api.type === 'api') {
-            return QueryApi.postOptions(params);
-          } else
-            return (requestClient as any)[props.api.method](
-              props.api.url as any,
-              props.api.method === 'get' ? { params } : params,
-            );
-        }
-      : props.api.url;
+  const api = createApiFunction({ ...props.api });
   if (!api || !isFunction(api)) return;
   try {
     loading.value = true;

+ 122 - 0
apps/web-baicai/src/components/form/components/api-popup.vue

@@ -0,0 +1,122 @@
+<script setup lang="ts">
+import type { SelectValue } from 'ant-design-vue/es/select';
+
+import { type PropType } from 'vue';
+
+import { useVModel } from '@vueuse/core';
+import { Input } from 'ant-design-vue';
+
+const props = defineProps({
+  value: {
+    type: [String, Number, Array] as PropType<SelectValue>,
+    default: undefined,
+  },
+  api: {
+    type: String as PropType<string>,
+    default: '',
+  },
+  fieldNames: {
+    type: Object as PropType<{ label: string; options: any; value: string }>,
+    default: () => ({ label: 'label', value: 'value', options: null }),
+  },
+});
+const emit = defineEmits(['update:value']);
+const modelValue = useVModel(props, 'value', emit, {
+  defaultValue: props.value,
+  passive: true,
+});
+// const options = ref<BasicOptionResult[]>([]);
+
+// const isFirstLoad = ref(true);
+</script>
+
+<template>
+  <div class="bc-popup w-full">
+    <div class="bc-popup-selector">
+      <div class="bc-popup-selection-overflow">
+        <div class="bc-popup-selection-item">
+          <div class="bc-popup-selection-item-content">asdfasdf</div>
+        </div>
+        <div class="bc-popup-selection-item">
+          <div class="bc-popup-selection-item-content">bbb</div>
+        </div>
+        <div class="bc-popup-selection-placeholder">请选择</div>
+        <div class="bc-popup-selection-item">
+          <div style="width: 4px">
+            <Input v-model="modelValue" stype="opacity: 0; " />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.bc-popup {
+  margin: 0;
+  padding: 0;
+  color: rgba(0, 0, 0, 0.88);
+  font-size: 14px;
+  list-style: none;
+  position: relative;
+  display: inline-block;
+  cursor: pointer;
+
+  &-selector {
+    cursor: text;
+
+    &-overflow {
+      position: relative;
+      display: flex;
+      flex: auto;
+      flex-wrap: wrap;
+      max-width: 100%;
+
+      &-placeholder {
+        position: absolute;
+        top: 50%;
+        inset-inline-start: 11px;
+        inset-inline-end: 11px;
+        transform: translateY(-50%);
+        transition: all 0.3s;
+      }
+
+      &-item {
+        flex: none;
+        align-self: center;
+        max-width: 100%;
+        display: inline-flex;
+        position: relative;
+        display: flex;
+        flex: none;
+        box-sizing: border-box;
+        max-width: 100%;
+        height: 24px;
+        margin-top: 2px;
+        margin-bottom: 2px;
+        line-height: 22px;
+        background: rgba(0, 0, 0, 0.06);
+        border: 1px solid rgba(5, 5, 5, 0.06);
+        border-radius: 4px;
+        cursor: default;
+        transition:
+          font-size 0.3s,
+          line-height 0.3s,
+          height 0.3s;
+        user-select: none;
+        margin-inline-end: 4px;
+        padding-inline-start: 8px;
+        padding-inline-end: 4px;
+
+        &-content {
+          display: inline-block;
+          margin-inline-end: 4px;
+          overflow: hidden;
+          white-space: pre;
+          text-overflow: ellipsis;
+        }
+      }
+    }
+  }
+}
+</style>

+ 3 - 16
apps/web-baicai/src/components/form/components/api-radio.vue

@@ -12,10 +12,10 @@ import { isFunction } from '@vben/utils';
 import { useVModel } from '@vueuse/core';
 import { RadioGroup, Spin } from 'ant-design-vue';
 
-import { EnumApi, QueryApi } from '#/api';
-import { requestClient } from '#/api/request';
 import { get, omit } from '#/utils';
 
+import { createApiFunction } from '../helper';
+
 const props = defineProps({
   value: {
     type: [String, Number, Array] as PropType<SelectValue>,
@@ -76,20 +76,7 @@ const emitChange = () => {
 };
 
 const fetch = async () => {
-  const api: any =
-    typeof props.api.url === 'string' || !props.api.url
-      ? (params: any) => {
-          if (props.api.type === 'enum') {
-            return EnumApi.getList(params);
-          } else if (props.api.type === 'api') {
-            return QueryApi.postOptions(params);
-          } else
-            return (requestClient as any)[props.api.method](
-              props.api.url as any,
-              props.api.method === 'get' ? { params } : params,
-            );
-        }
-      : props.api.url;
+  const api = createApiFunction({ ...props.api });
   if (!api || !isFunction(api)) return;
   try {
     loading.value = true;

+ 3 - 16
apps/web-baicai/src/components/form/components/api-select.vue

@@ -12,11 +12,11 @@ import { isFunction } from '@vben/utils';
 import { useVModel } from '@vueuse/core';
 import { Select } from 'ant-design-vue';
 
-import { EnumApi, QueryApi } from '#/api';
-import { requestClient } from '#/api/request';
 import { Icon } from '#/components/icon';
 import { get, omit } from '#/utils';
 
+import { createApiFunction } from '../helper';
+
 const props = defineProps({
   value: {
     type: [String, Number, Array] as PropType<SelectValue>,
@@ -78,20 +78,7 @@ const emitChange = () => {
 };
 
 const fetch = async () => {
-  const api: any =
-    typeof props.api.url === 'string' || !props.api.url
-      ? (params: any) => {
-          if (props.api.type === 'enum') {
-            return EnumApi.getList(params);
-          } else if (props.api.type === 'api') {
-            return QueryApi.postOptions(params);
-          } else
-            return (requestClient as any)[props.api.method](
-              props.api.url as any,
-              props.api.method === 'get' ? { params } : params,
-            );
-        }
-      : props.api.url;
+  const api = createApiFunction({ ...props.api });
   if (!api || !isFunction(api)) return;
   try {
     loading.value = true;

+ 3 - 17
apps/web-baicai/src/components/form/components/api-tree-select.vue

@@ -11,11 +11,11 @@ import { isFunction } from '@vben/utils';
 import { useVModel } from '@vueuse/core';
 import { TreeSelect } from 'ant-design-vue';
 
-import { EnumApi, QueryApi } from '#/api';
-import { requestClient } from '#/api/request';
 import { Icon } from '#/components/icon';
 import { get } from '#/utils';
 
+import { createApiFunction } from '../helper';
+
 const props = defineProps({
   value: {
     type: [String, Number, Array] as PropType<SelectValue>,
@@ -65,21 +65,7 @@ const emitChange = () => {
 };
 
 const fetch = async () => {
-  const api: any =
-    typeof props.api.url === 'string' || !props.api.url
-      ? (params: any) => {
-          if (props.api.type === 'enum') {
-            return EnumApi.getList(params);
-          } else if (props.api.type === 'api') {
-            return QueryApi.getExecuteTree(params);
-          } else {
-            return (requestClient as any)[props.api.method](
-              props.api.url as any,
-              props.api.method === 'get' ? { params } : params,
-            );
-          }
-        }
-      : props.api.url;
+  const api = createApiFunction({ ...props.api });
   if (!api || !isFunction(api)) return;
   try {
     loading.value = true;

+ 30 - 0
apps/web-baicai/src/components/form/helper.ts

@@ -0,0 +1,30 @@
+import type { ApiConfig } from './types';
+
+import { DictionaryApi, EnumApi, QueryApi } from '#/api';
+import { requestClient } from '#/api/request';
+
+export const createApiFunction = (apiConfig: ApiConfig) => {
+  const api: any =
+    typeof apiConfig.url === 'string' || !apiConfig.url
+      ? (params: any) => {
+          switch (apiConfig.type) {
+            case 'api': {
+              return QueryApi.postOptions(params);
+            }
+            case 'dict': {
+              return DictionaryApi.getItemList(params);
+            }
+            case 'enum': {
+              return EnumApi.getList(params);
+            }
+            default: {
+              return (requestClient as any)[apiConfig.method](
+                apiConfig.url as any,
+                apiConfig.method === 'get' ? { params } : params,
+              );
+            }
+          }
+        }
+      : apiConfig.url;
+  return api;
+};

+ 1 - 1
apps/web-baicai/src/components/select-card/index.ts

@@ -1 +1 @@
-export { default as SelectCard } from './select-card.vue';
+export { default as SelectCard } from './src/select-card.vue';

+ 4 - 4
apps/web-baicai/src/components/select-card/select-card-item.vue → apps/web-baicai/src/components/select-card/src/select-card-item.vue

@@ -7,10 +7,10 @@ import { Tooltip } from 'ant-design-vue';
 
 import { Icon } from '#/components/icon';
 
-import defaultImg from './images/head-default.png';
-import FemaleImg from './images/head-female.png';
-import MaleImg from './images/head-male.png';
-import RoleImg from './images/role.png';
+import defaultImg from '../images/head-default.png';
+import FemaleImg from '../images/head-female.png';
+import MaleImg from '../images/head-male.png';
+import RoleImg from '../images/role.png';
 
 defineOptions({
   name: 'SelectCardItem',

+ 1 - 1
apps/web-baicai/src/components/select-card/select-card.vue → apps/web-baicai/src/components/select-card/src/select-card.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { Recordable } from '@vben/types';
 
-import type { ApiConfig } from '../form/types';
+import type { ApiConfig } from '../../form/types';
 
 import { type PropType, reactive, ref } from 'vue';
 

+ 2 - 2
apps/web-baicai/src/components/table-action/index.ts

@@ -1,2 +1,2 @@
-export { default as TableAction } from './table-action.vue';
-export type * from './types';
+export { default as TableAction } from './src/table-action.vue';
+export type * from './src/types';

+ 0 - 0
apps/web-baicai/src/components/table-action/table-action.vue → apps/web-baicai/src/components/table-action/src/table-action.vue


+ 0 - 0
apps/web-baicai/src/components/table-action/types.d.ts → apps/web-baicai/src/components/table-action/src/types.d.ts


+ 2 - 2
apps/web-baicai/src/router/guard.ts

@@ -8,7 +8,7 @@ import { startProgress, stopProgress } from '@vben/utils';
 import { useTitle } from '@vueuse/core';
 
 import { $t } from '#/locales';
-import { coreRouteNames, dynamicRoutes } from '#/router/routes';
+import { accessRoutes, coreRouteNames } from '#/router/routes';
 import { useAuthStore } from '#/store';
 
 import { generateAccess } from './access';
@@ -103,7 +103,7 @@ function setupAccessGuard(router: Router) {
       roles: userRoles,
       router,
       // 则会在菜单中显示,但是访问会被重定向到403
-      routes: dynamicRoutes,
+      routes: accessRoutes,
     });
 
     // 保存菜单信息和路由信息

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

@@ -1,6 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { DEFAULT_HOME_PATH } from '@vben/constants';
+import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 
 import { AuthPageLayout } from '#/layouts';
 import { $t } from '#/locales';
@@ -32,17 +32,19 @@ const coreRoutes: RouteRecordRaw[] = [
   {
     component: AuthPageLayout,
     meta: {
+      hideInTab: true,
       title: 'Authentication',
     },
     name: 'Authentication',
     path: '/auth',
+    redirect: LOGIN_PATH,
     children: [
       {
         name: 'Login',
         path: 'login',
         component: Login,
         meta: {
-          title: $t('page.core.login'),
+          title: $t('page.auth.login'),
         },
       },
       {
@@ -50,7 +52,7 @@ const coreRoutes: RouteRecordRaw[] = [
         path: 'code-login',
         component: () => import('#/views/_core/authentication/code-login.vue'),
         meta: {
-          title: $t('page.core.codeLogin'),
+          title: $t('page.auth.codeLogin'),
         },
       },
       {
@@ -59,7 +61,7 @@ const coreRoutes: RouteRecordRaw[] = [
         component: () =>
           import('#/views/_core/authentication/qrcode-login.vue'),
         meta: {
-          title: $t('page.core.qrcodeLogin'),
+          title: $t('page.auth.qrcodeLogin'),
         },
       },
       {
@@ -68,7 +70,7 @@ const coreRoutes: RouteRecordRaw[] = [
         component: () =>
           import('#/views/_core/authentication/forget-password.vue'),
         meta: {
-          title: $t('page.core.forgetPassword'),
+          title: $t('page.auth.forgetPassword'),
         },
       },
       {
@@ -76,7 +78,7 @@ const coreRoutes: RouteRecordRaw[] = [
         path: 'register',
         component: () => import('#/views/_core/authentication/register.vue'),
         meta: {
-          title: $t('page.core.register'),
+          title: $t('page.auth.register'),
         },
       },
     ],

+ 8 - 2
apps/web-baicai/src/router/routes/index.ts

@@ -10,12 +10,16 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
 
 // 有需要可以自行打开注释,并创建文件夹
 // const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
 
 /** 动态路由 */
 const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
-/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统 */
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+/** 不需要权限的菜单列表(会显示在菜单中) */
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
 /** 路由列表,由基本路由+静态路由组成 */
@@ -28,4 +32,6 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
-export { coreRouteNames, dynamicRoutes, routes };
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+
+export { accessRoutes, coreRouteNames, routes };

+ 5 - 5
apps/web-baicai/src/views/system/design/query/components/edit.vue

@@ -9,8 +9,8 @@ import { message, Steps } from 'ant-design-vue';
 
 import { QueryApi } from '#/api';
 
-import StepBaseConfig from './stepBaseConfig.vue';
-import StepDetailConfig from './stepDetailConfig.vue';
+import StepBaseConfig from './step/baseConfig.vue';
+import StepDetailConfig from './step/detailConfig.vue';
 
 defineOptions({
   name: 'QueryEdit',
@@ -18,8 +18,8 @@ defineOptions({
 const emit = defineEmits(['success']);
 const modelRef = ref<Record<string, any>>({});
 const isUpdate = ref(true);
-const stepBaseConfig = ref(null);
-const stepDetailConfig = ref(null);
+const stepBaseConfig = ref();
+const stepDetailConfig = ref();
 
 const state = reactive<{
   dbBaseOptions: BasicOptionResult[];
@@ -88,7 +88,7 @@ const [Modal, { close, getData, setState }] = useVbenModal({
         close();
         emit('success');
       }
-    } catch (error) {
+    } catch (error: any) {
       message.error(error?.message || '操作失败');
     } finally {
       setState({ confirmLoading: false });

+ 0 - 0
apps/web-baicai/src/views/system/design/query/components/stepBaseConfig.vue → apps/web-baicai/src/views/system/design/query/components/step/baseConfig.vue


+ 0 - 0
apps/web-baicai/src/views/system/design/query/components/stepDetailConfig.vue → apps/web-baicai/src/views/system/design/query/components/step/detailConfig.vue


+ 186 - 0
apps/web-baicai/src/views/system/design/table/components/edit.vue

@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import type { BasicOptionResult } from '#/api/model';
+
+import { provide, reactive, ref, unref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Steps } from 'ant-design-vue';
+
+import { QueryApi, TableApi } from '#/api';
+
+import StepBaseConfig from './step/baseConfig.vue';
+import StepButtonConfig from './step/buttonConfig.vue';
+import StepColumnConfig from './step/columnConfig.vue';
+import StepSearchConfig from './step/searchConfig.vue';
+
+defineOptions({
+  name: 'TableEdit',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+const stepBaseConfig = ref();
+const stepSearchConfig = ref();
+const stepColumnConfig = ref();
+const stepButtonConfig = ref();
+
+const state = reactive<{
+  dbBaseOptions: BasicOptionResult[];
+  stepIndex: number;
+  stepItems: any[];
+  stepStyle: Record<string, any>;
+}>({
+  stepIndex: 0,
+  stepItems: [
+    { title: '基本信息', description: '基本数据设置' },
+    { title: '条件设置', description: '查询条件设置' },
+    { title: '列表设置', description: '显示列表设置' },
+    { title: '按钮设置', description: '操作按钮设置' },
+  ],
+  stepStyle: { marginBottom: '20px' },
+  dbBaseOptions: [],
+});
+
+const columns = ref<QueryApi.Column[]>([]);
+provide('page-columns', columns);
+
+const [Modal, { close, getData, setState }] = useVbenModal({
+  fullscreenButton: false,
+  fullscreen: true,
+  draggable: true,
+  cancelText: '上一步',
+  confirmText: '下一步',
+  onCancel() {
+    if (state.stepIndex === 0) {
+      close();
+    } else {
+      state.stepIndex -= 1;
+    }
+    setState({
+      confirmText:
+        state.stepIndex === state.stepItems.length - 1 ? '确定' : '下一步',
+      cancelText: state.stepIndex === 0 ? '取消' : '上一步',
+    });
+  },
+  onConfirm: async () => {
+    setState({ confirmLoading: true });
+    try {
+      if (
+        state.stepIndex === 0 &&
+        (await stepBaseConfig.value.stepValidate())
+      ) {
+        const values = await stepBaseConfig.value.stepGetValues();
+        columns.value = await QueryApi.getColumns(values.databaseQueryId);
+        Object.assign(modelRef.value, values);
+        await stepSearchConfig.value.stepSetValues({ ...unref(modelRef) });
+        state.stepIndex += 1;
+        setState({
+          confirmText:
+            state.stepIndex === state.stepItems.length - 1 ? '确定' : '下一步',
+          cancelText: state.stepIndex === 0 ? '取消' : '上一步',
+        });
+      } else if (
+        state.stepIndex === 1 &&
+        (await stepSearchConfig.value.stepValidate())
+      ) {
+        await stepColumnConfig.value.stepSetValues({ ...unref(modelRef) });
+        state.stepIndex += 1;
+        setState({
+          confirmText:
+            state.stepIndex === state.stepItems.length - 1 ? '确定' : '下一步',
+          cancelText: state.stepIndex === 0 ? '取消' : '上一步',
+        });
+      } else if (
+        state.stepIndex === 2 &&
+        (await stepColumnConfig.value.stepValidate())
+      ) {
+        await stepButtonConfig.value.stepSetValues({ ...unref(modelRef) });
+        state.stepIndex += 1;
+        setState({
+          confirmText:
+            state.stepIndex === state.stepItems.length - 1 ? '确定' : '下一步',
+          cancelText: state.stepIndex === 0 ? '取消' : '上一步',
+        });
+      } else if (
+        state.stepIndex === 3 &&
+        (await stepButtonConfig.value.stepValidate())
+      ) {
+        const searchConfig = await stepSearchConfig.value.stepGetValues();
+        const columnConfig = await stepColumnConfig.value.stepGetValues();
+        const buttomConfig = await stepButtonConfig.value.stepGetValues();
+
+        const postParams = unref(modelRef);
+
+        postParams.config = {
+          search: searchConfig,
+          columns: columnConfig,
+          buttons: buttomConfig,
+        };
+
+        await (unref(isUpdate)
+          ? TableApi.editDetail(postParams as TableApi.RecordItem)
+          : TableApi.addDetail(postParams as TableApi.BasicRecordItem));
+        message.success('操作成功');
+
+        close();
+        emit('success');
+      }
+    } catch (error: any) {
+      message.error(error?.message || '操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      setState({
+        title: unref(isUpdate) ? '编辑页面' : '新增页面',
+        cancelText: '取消',
+        confirmText: '下一步',
+      });
+
+      state.stepIndex = 0;
+
+      if (unref(isUpdate)) {
+        const entity = await TableApi.getDetail(data.baseData.id);
+        modelRef.value = { ...entity };
+        stepBaseConfig.value.stepSetValues(entity);
+      }
+      setState({ loading: false });
+    }
+  },
+  title: '新增页面',
+});
+</script>
+<template>
+  <Modal class="w-[1000px]">
+    <div class="h-full">
+      <Steps
+        :current="state.stepIndex"
+        :items="state.stepItems"
+        :percent="60"
+        :style="state.stepStyle"
+      />
+      <div style="height: calc(100% - 82px)">
+        <StepBaseConfig v-show="state.stepIndex === 0" ref="stepBaseConfig" />
+        <StepSearchConfig
+          v-show="state.stepIndex === 1"
+          ref="stepSearchConfig"
+        />
+        <StepColumnConfig
+          v-show="state.stepIndex === 2"
+          ref="stepColumnConfig"
+        />
+        <StepButtonConfig
+          v-show="state.stepIndex === 3"
+          ref="stepButtonConfig"
+        />
+      </div>
+    </div>
+  </Modal>
+</template>

+ 129 - 0
apps/web-baicai/src/views/system/design/table/components/page.vue

@@ -0,0 +1,129 @@
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { TableApi } from '#/api';
+
+defineOptions({
+  name: 'TablePage',
+});
+const props = defineProps({
+  tableQueryId: {
+    type: [Number, String],
+    default: 0,
+  },
+});
+const emit = defineEmits(['success']);
+
+const modelRef = ref<Record<string, any>>({});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    toolbarConfig: {
+      refresh: true,
+      print: false,
+      export: true,
+      zoom: true,
+      custom: true,
+    },
+    columns: [],
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: {},
+    exportConfig: {},
+    proxyConfig: {
+      autoLoad: true,
+    },
+  },
+});
+
+const loadPage = async (id: number) => {
+  const entity: TableApi.RecordItem = await TableApi.getDetail(id);
+  modelRef.value = { ...entity };
+
+  const columns: any[] = [{ title: '序号', type: 'seq', width: 50 }];
+
+  entity.config.columns.forEach((item: any) => {
+    const col: any = {
+      align: item.align,
+      field: item.field,
+      title: item.title,
+    };
+
+    if (!item.autoWidth) {
+      col.width = item.width;
+    }
+
+    columns.push(col);
+  });
+
+  if (columns.length > 0) {
+    const buttons = entity.config?.buttons.filter(
+      (btn: any) => btn.position === 'item',
+    ).length;
+
+    if (buttons > 0) {
+      const col: any = {
+        field: 'action',
+        fixed: 'right',
+        slots: { default: 'action' },
+        title: '操作',
+        width: buttons > 2 ? 150 : 120,
+      };
+      columns.push(col);
+    }
+
+    const gridState: any = {
+      gridOptions: {
+        columns,
+        proxyConfig: {
+          ajax: {
+            query: async ({ page }: any, formValues: any) => {
+              return await TableApi.postExecute({
+                id: entity.id,
+                pageIndex: page.currentPage,
+                pageSize: page.pageSize,
+                formData: { ...formValues },
+              });
+            },
+          },
+        },
+      },
+    };
+
+    const formSchema: any[] = [];
+    entity.config.search.forEach((item: any) => {
+      const row = {
+        ...item.schema,
+      };
+      formSchema.push(row);
+    });
+
+    if (formSchema.length > 0) {
+      gridState.formOptions = {
+        showCollapseButton: false,
+        schema: formSchema,
+      };
+    }
+
+    gridApi.setState(gridState);
+
+    emit('success');
+  }
+};
+
+watch(
+  () => props.tableQueryId,
+  async (value) => {
+    if (value && Number(value) > 0) {
+      await loadPage(Number(value));
+    }
+  },
+  { deep: true },
+);
+</script>
+<template>
+  <div class="h-full">
+    <Grid />
+  </div>
+</template>

+ 52 - 0
apps/web-baicai/src/views/system/design/table/components/preview.vue

@@ -0,0 +1,52 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import TablePage from '#/views/system/design/table/components/page';
+
+defineOptions({
+  name: 'TablePreview',
+});
+const emit = defineEmits(['success']);
+const modelRef = ref<number>(0);
+const isUpdate = ref(true);
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  draggable: true,
+  fullscreen: true,
+  onConfirm: async () => {
+    try {
+      close();
+      emit('success');
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+
+      modelRef.value = data.baseData.id;
+      setState({ title: `预览${data.baseData.name}` });
+    }
+  },
+  title: '预览表格',
+});
+
+const handelLoadData = () => {
+  setState({ loading: false });
+};
+</script>
+<template>
+  <Modal class="w-[1000px]">
+    <TablePage :table-query-id="modelRef" @success="handelLoadData" />
+  </Modal>
+</template>

+ 90 - 0
apps/web-baicai/src/views/system/design/table/components/step/baseConfig.vue

@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+
+import { Card } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter';
+
+const [Form, { validate, setValues, getValues }] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'ApiSelect',
+      componentProps: {
+        placeholder: '请输入页面类型',
+        api: {
+          type: 'dict',
+          params: 'table_type',
+        },
+      },
+      fieldName: 'tableType',
+      label: '页面类型',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'name',
+      label: '名称',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'databaseQueryCode',
+      label: '接口',
+      rules: 'required',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'remark',
+      label: '备注',
+    },
+  ],
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-1',
+});
+
+const stepValidate = async () => {
+  const { valid } = await validate();
+  return valid;
+};
+
+const stepSetValues = async (data: Record<string, any>) => {
+  await setValues(data);
+};
+
+const stepGetValues = async () => {
+  return await getValues();
+};
+
+onMounted(async () => {});
+
+defineExpose({ stepSetValues, stepValidate, stepGetValues });
+</script>
+
+<template>
+  <Card class="h-full" title="基本参数配置">
+    <Form />
+  </Card>
+</template>

+ 176 - 0
apps/web-baicai/src/views/system/design/table/components/step/buttonConfig.vue

@@ -0,0 +1,176 @@
+<script lang="ts" setup>
+import type { QueryApi } from '#/api';
+
+import { Card } from 'ant-design-vue';
+
+import { useVbenForm, useVbenVxeGrid, type VxeGridListeners } from '#/adapter';
+
+// const columns = inject<QueryApi.Column[]>('page-columns') as QueryApi.Column[];
+
+const [Form, { setValues, getValues }] = useVbenForm({
+  showDefaultActions: false,
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请输入按钮位置',
+        options: [
+          { label: '头部', value: 'main' },
+          { label: '行', value: 'item' },
+        ],
+      },
+      fieldName: 'position',
+      label: '按钮位置',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'label',
+      label: '名称',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入权限',
+      },
+      fieldName: 'access',
+      label: '权限',
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请输入接口类型',
+        options: [
+          { label: '多选', value: 'checkbox' },
+          { label: '单选', value: 'radio' },
+        ],
+      },
+      fieldName: 'api.type',
+      label: '接口类型',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入接口地址',
+      },
+      fieldName: 'api.url',
+      label: '接口地址',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入接口参数',
+      },
+      fieldName: 'api.params',
+      label: '接口参数',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入请求方式',
+      },
+      fieldName: 'api.method',
+      label: '请求方式',
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+});
+
+const gridEvents: VxeGridListeners<QueryApi.Column> = {
+  currentChange: async ({
+    newValue,
+    oldValue,
+  }: {
+    newValue: any;
+    oldValue: any;
+  }) => {
+    // console.log('rowIndex', newValue, oldValue);
+    if (oldValue !== null) {
+      const updateData = await getValues();
+      Object.assign(oldValue, updateData);
+    }
+    setValues(newValue as any);
+  },
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridEvents,
+  gridOptions: {
+    checkboxConfig: {
+      highlight: true,
+      labelField: 'name',
+    },
+    rowConfig: {
+      isCurrent: true,
+      isHover: true,
+    },
+    columns: [
+      { title: '序号', type: 'seq', width: 50 },
+      {
+        align: 'left',
+        field: 'lable',
+        title: '名称',
+        width: 200,
+        type: 'checkbox',
+      },
+      { align: 'left', field: 'position', title: '按钮位置', width: 200 },
+      { align: 'left', field: 'access', title: '权限' },
+    ],
+    editConfig: {
+      mode: 'cell',
+      trigger: 'click',
+    },
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: {
+      enabled: false,
+    },
+  },
+});
+
+const stepSetValues = async (data: Record<string, any>) => {
+  const tableColumns: Record<string, any>[] = [];
+
+  const { buttons } = data && data.config;
+  if (buttons) {
+    buttons.forEach((item: any) => {
+      tableColumns.push({ ...item });
+    });
+  } else {
+    const values = {
+      lable: '添加',
+      position: 'main',
+      access: 'add',
+    };
+    tableColumns.push(values);
+  }
+
+  gridApi.setGridOptions({ data: tableColumns });
+};
+
+const stepGetValues = async () => {
+  const buttomConfig = gridApi.grid.getData();
+  return buttomConfig;
+};
+
+const stepValidate = async () => {
+  return true;
+};
+
+defineExpose({ stepSetValues, stepValidate, stepGetValues });
+</script>
+<template>
+  <div class="flex h-full">
+    <Grid class="w-3/5" table-title="操作按钮" />
+    <Card class="ml-4 w-2/5" ttitle="属性配置">
+      <Form />
+    </Card>
+  </div>
+</template>

+ 184 - 0
apps/web-baicai/src/views/system/design/table/components/step/columnConfig.vue

@@ -0,0 +1,184 @@
+<script lang="ts" setup>
+import type { QueryApi } from '#/api';
+
+import { inject, unref } from 'vue';
+
+import { Card, message } from 'ant-design-vue';
+
+import { useVbenForm, useVbenVxeGrid, type VxeGridListeners } from '#/adapter';
+
+const columns = inject<QueryApi.Column[]>('page-columns') as QueryApi.Column[];
+
+const [Form, { setValues, getValues }] = useVbenForm({
+  showDefaultActions: false,
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入显示名称',
+      },
+      fieldName: 'title',
+      label: '显示名称',
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请输入对齐方式',
+        options: [
+          { label: '左对齐', value: 'left' },
+          { label: '居中对齐', value: 'center' },
+          { label: '右对齐', value: 'right' },
+        ],
+      },
+      fieldName: 'align',
+      label: '对齐方式',
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入宽度',
+      },
+      fieldName: 'autoWidth',
+      label: '自动宽度',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入宽度',
+      },
+      fieldName: 'width',
+      label: '宽度',
+      dependencies: {
+        triggerFields: ['autoWidth'],
+        show: (values) => values.autoWidth === false,
+      },
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请输入选择类型',
+        options: [
+          { label: '多选', value: 'checkbox' },
+          { label: '单选', value: 'radio' },
+        ],
+      },
+      fieldName: 'selectType',
+      label: '选择类型',
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+});
+
+const gridEvents: VxeGridListeners<QueryApi.Column> = {
+  currentChange: async ({
+    newValue,
+    oldValue,
+  }: {
+    newValue: any;
+    oldValue: any;
+  }) => {
+    // console.log('rowIndex', newValue, oldValue);
+    if (oldValue !== null) {
+      const updateData = await getValues();
+      Object.assign(oldValue, updateData);
+    }
+    setValues(newValue as any);
+  },
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridEvents,
+  gridOptions: {
+    checkboxConfig: {
+      highlight: true,
+      labelField: 'field',
+    },
+    rowConfig: {
+      isCurrent: true,
+      isHover: true,
+      keyField: 'field',
+    },
+    columns: [
+      { title: '序号', type: 'seq', width: 50 },
+      {
+        align: 'left',
+        field: 'field',
+        title: '字段名称',
+        width: 200,
+        type: 'checkbox',
+      },
+      { align: 'left', field: 'title', title: '显示名称' },
+    ],
+    editConfig: {
+      mode: 'cell',
+      trigger: 'click',
+    },
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: {
+      enabled: false,
+    },
+  },
+});
+
+const stepSetValues = async (data: Record<string, any>) => {
+  const tableColumns: Record<string, any>[] = [];
+
+  const _columns = data && data.config?.columns;
+  const checkedRows: Record<string, any>[] = [];
+  unref(columns).forEach((item: any) => {
+    const values = {
+      title: item.remark,
+      field: item.alias,
+      align: 'left',
+      width: 100,
+      autoWidth: false,
+    };
+
+    if (_columns) {
+      const find = _columns.find((row: any) => row.field === values.field);
+      if (find) {
+        Object.assign(values, find);
+        checkedRows.push({ ...values });
+      }
+    }
+    tableColumns.push(values);
+  });
+
+  gridApi.setGridOptions({ data: tableColumns });
+
+  if (checkedRows.length > 0) {
+    // console.log('checkedRows', checkedRows);
+    gridApi.grid.setCheckboxRow(checkedRows, true);
+  }
+};
+
+const stepGetValues = async () => {
+  const columnConfig = gridApi.grid.getCheckboxRecords();
+  return columnConfig;
+};
+
+const stepValidate = async () => {
+  const selectedColumns = gridApi.grid.getCheckboxRecords();
+  if (selectedColumns.length === 0) {
+    message.warning('请选择显示的列');
+    return false;
+  }
+  return true;
+};
+
+defineExpose({ stepSetValues, stepValidate, stepGetValues });
+</script>
+<template>
+  <div class="flex h-full">
+    <Grid class="w-3/5" table-title="显示列表" />
+    <Card class="ml-4 w-2/5" title="属性配置">
+      <Form />
+    </Card>
+  </div>
+</template>

+ 221 - 0
apps/web-baicai/src/views/system/design/table/components/step/searchConfig.vue

@@ -0,0 +1,221 @@
+<script lang="ts" setup>
+import type { QueryApi } from '#/api';
+
+import { inject, unref } from 'vue';
+
+import { Card } from 'ant-design-vue';
+
+import { useVbenForm, useVbenVxeGrid, type VxeGridListeners } from '#/adapter';
+
+const columns = inject<QueryApi.Column[]>('page-columns') as QueryApi.Column[];
+
+const stepValidate = async () => {
+  return true;
+};
+
+const [Form, { setValues, getValues }] = useVbenForm({
+  showDefaultActions: false,
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  schema: [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'fieldName',
+      label: '字段',
+      rules: 'required',
+      disabled: true,
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入名称',
+      },
+      fieldName: 'label',
+      label: '名称',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入占位提示',
+      },
+      fieldName: 'componentProps.placeholder',
+      label: '占位提示',
+      rules: 'required',
+      dependencies: {
+        triggerFields: ['label'],
+        trigger(values, form) {
+          form.setFieldValue(
+            'componentProps.placeholder',
+            `请输入${values.label || ''}`,
+          );
+        },
+      },
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请输入类型',
+        options: [
+          { label: '文本', value: 'Input' },
+          { label: '下拉框', value: 'Select' },
+          { label: '多选框', value: 'Checkbox' },
+          { label: '日期', value: 'DatePicker' },
+          { label: '时间', value: 'TimePicker' },
+        ],
+      },
+      fieldName: 'component',
+      label: '类型',
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入日期格式',
+      },
+      fieldName: 'componentProps.format',
+      label: '日期格式',
+      dependencies: {
+        triggerFields: ['component'],
+        show: (values) =>
+          values.component === 'DatePicker' ||
+          values.component === 'TimePicker',
+      },
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+});
+
+const gridEvents: VxeGridListeners<QueryApi.Column> = {
+  currentChange: async ({
+    newValue,
+    oldValue,
+  }: {
+    newValue: any;
+    oldValue: any;
+  }) => {
+    if (oldValue !== null) {
+      const updateData = await getValues();
+      Object.assign(oldValue.schema, updateData);
+    }
+    setValues(newValue.schema as any);
+  },
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridEvents,
+  gridOptions: {
+    checkboxConfig: {
+      highlight: true,
+      labelField: 'field',
+    },
+    rowConfig: {
+      isCurrent: true,
+      isHover: true,
+      keyField: 'field',
+    },
+    columns: [
+      { title: '序号', type: 'seq', width: 50 },
+      {
+        align: 'left',
+        field: 'field',
+        title: '字段名称',
+        width: 200,
+        type: 'checkbox',
+      },
+      { align: 'left', field: 'label', title: '显示名称' },
+      {
+        align: 'left',
+        field: 'queryType',
+        title: '查询类型',
+        width: 200,
+        editRender: {
+          name: 'select',
+          options: [
+            { label: '等于', value: 0 },
+            { label: '包含', value: 1 },
+            { label: '大于', value: 2 },
+            { label: '大于等于', value: 3 },
+            { label: '小于', value: 4 },
+            { label: '小于等于', value: 5 },
+            { label: 'In', value: 6 },
+            { label: 'Not In', value: 7 },
+            { label: '左包含', value: 8 },
+            { label: '右包含', value: 9 },
+            { label: '不等于', value: 10 },
+            { label: '不等于空', value: 11 },
+            { label: 'IsNot', value: 12 },
+            { label: '不包含', value: 13 },
+            { label: '为空', value: 14 },
+            { label: 'In Like', value: 15 },
+            { label: 'Range', value: 16 },
+          ],
+        },
+      },
+    ],
+    editConfig: {
+      mode: 'cell',
+      trigger: 'click',
+    },
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: {
+      enabled: false,
+    },
+  },
+});
+
+const stepSetValues = async (data: Record<string, any>) => {
+  const tableColumns: Record<string, any>[] = [];
+
+  const { search } = data && data.config;
+  const checkedRows: Record<string, any>[] = [];
+  unref(columns).forEach((item: any) => {
+    const values = {
+      field: item.alias,
+      label: item.remark,
+      queryType: 0,
+      schema: {
+        label: item.remark,
+        fieldName: item.alias,
+        component: 'Input',
+        componentProps: { placeholder: `请输入${item.remark || ''}` },
+      },
+    };
+    if (search) {
+      const find = search.find((row: any) => row.field === values.field);
+      if (find) {
+        Object.assign(values, find);
+        checkedRows.push({ ...values });
+      }
+    }
+    tableColumns.push({ ...values });
+  });
+  gridApi.setGridOptions({ data: tableColumns });
+
+  if (checkedRows.length > 0) {
+    gridApi.grid.setCheckboxRow(checkedRows, true);
+  }
+};
+
+const stepGetValues = async () => {
+  const searchConfig = gridApi.grid.getCheckboxRecords();
+  return searchConfig;
+};
+
+defineExpose({ stepSetValues, stepValidate, stepGetValues });
+</script>
+<template>
+  <div class="flex h-full">
+    <Grid class="w-3/5" table-title="搜索条件" />
+    <Card class="ml-4 w-2/5" title="属性配置">
+      <Form />
+    </Card>
+  </div>
+</template>

+ 53 - 0
apps/web-baicai/src/views/system/design/table/data.config.ts

@@ -0,0 +1,53 @@
+import type { VbenFormProps, VxeGridProps } from '#/adapter';
+
+import { TableApi } from '#/api';
+
+export const searchFormOptions: VbenFormProps = {
+  showCollapseButton: false,
+  schema: [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: '名称',
+    },
+  ],
+};
+
+export const gridOptions: VxeGridProps<TableApi.RecordItem> = {
+  toolbarConfig: {
+    refresh: true,
+    print: false,
+    export: true,
+    zoom: true,
+    custom: true,
+  },
+  columns: [
+    { title: '序号', type: 'seq', width: 50 },
+    { align: 'left', field: 'tableType', title: '类型', width: 120 },
+    { align: 'left', field: 'name', title: '查询名称', width: 200 },
+    { field: 'sort', title: '排序', width: 50 },
+    { align: 'left', field: 'remark', title: '备注' },
+    {
+      field: 'action',
+      fixed: 'right',
+      slots: { default: 'action' },
+      title: '操作',
+      width: 140,
+    },
+  ],
+  height: 'auto',
+  keepSource: true,
+  pagerConfig: {},
+  exportConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }, formValues) => {
+        return await TableApi.getPage({
+          pageIndex: page.currentPage,
+          pageSize: page.pageSize,
+          ...formValues,
+        });
+      },
+    },
+  },
+};

+ 109 - 0
apps/web-baicai/src/views/system/design/table/index.vue

@@ -0,0 +1,109 @@
+<script lang="ts" setup>
+import { useAccess } from '@vben/access';
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter';
+import { TableApi } from '#/api';
+import { TableAction } from '#/components/table-action';
+
+import FormEdit from './components/edit.vue';
+import FormPreview from './components/preview.vue';
+import { gridOptions, searchFormOptions } from './data.config';
+
+const { hasAccessByCodes } = useAccess();
+
+const [Grid, { reload }] = useVbenVxeGrid({
+  formOptions: searchFormOptions,
+  gridOptions,
+});
+
+const [FormEditModal, formEditApi] = useVbenModal({
+  connectedComponent: FormEdit,
+});
+
+const [FormPreviewModal, formPreviewApi] = useVbenModal({
+  connectedComponent: FormPreview,
+});
+
+const handleDelete = (id: number) => {
+  Modal.confirm({
+    iconType: 'info',
+    title: '删除提示',
+    content: `确定要删除选择的记录吗?`,
+    cancelText: `关闭`,
+    onOk: async () => {
+      await TableApi.deleteDetail(id);
+      message.success('数据删除成功');
+      reload();
+    },
+  });
+};
+
+const handleEdit = (record: any, isUpdate: boolean) => {
+  formEditApi.setData({
+    isUpdate,
+    baseData: { ...record },
+  });
+
+  formEditApi.open();
+};
+
+const handlePreview = (record: any) => {
+  formPreviewApi.setData({
+    isPreview: true,
+    baseData: { id: record.id, name: record.name },
+  });
+
+  formPreviewApi.open();
+};
+const handelSuccess = () => {
+  reload();
+};
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormEditModal :close-on-click-modal="false" @success="handelSuccess" />
+    <FormPreviewModal :close-on-click-modal="false" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button
+          class="mr-2"
+          type="primary"
+          v-access:code="'page:add'"
+          @click="() => handleEdit({}, false)"
+        >
+          新增页面
+        </Button>
+      </template>
+      <template #action="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              type: 'text',
+              disabled: !hasAccessByCodes(['page:edit']),
+              onClick: handleEdit.bind(null, row, true),
+            },
+            {
+              label: '预览',
+              type: 'text',
+              disabled: !hasAccessByCodes(['page:view']),
+              onClick: handlePreview.bind(null, row),
+            },
+          ]"
+          :drop-down-actions="[
+            {
+              label: '删除',
+              type: 'link',
+              disabled: !hasAccessByCodes(['page:delete']),
+              onClick: handleDelete.bind(null, row.id),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>

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

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

+ 9 - 1
apps/web-ele/src/adapter/vxe-table.ts

@@ -11,8 +11,15 @@ setupVbenVxeTable({
     vxeUI.setConfig({
       grid: {
         align: 'center',
-        border: true,
+        border: false,
+        columnConfig: {
+          resizable: true,
+        },
         minHeight: 180,
+        formConfig: {
+          // 全局禁用vxe-table的表单配置,使用formOptions
+          enabled: false,
+        },
         proxyConfig: {
           autoLoad: true,
           response: {
@@ -24,6 +31,7 @@ setupVbenVxeTable({
           showResponseMsg: false,
         },
         round: true,
+        showOverflow: true,
         size: 'small',
       },
     });

+ 2 - 2
apps/web-ele/src/router/guard.ts

@@ -8,7 +8,7 @@ import { startProgress, stopProgress } from '@vben/utils';
 import { useTitle } from '@vueuse/core';
 
 import { $t } from '#/locales';
-import { coreRouteNames, dynamicRoutes } from '#/router/routes';
+import { accessRoutes, coreRouteNames } from '#/router/routes';
 import { useAuthStore } from '#/store';
 
 import { generateAccess } from './access';
@@ -105,7 +105,7 @@ function setupAccessGuard(router: Router) {
       roles: userRoles,
       router,
       // 则会在菜单中显示,但是访问会被重定向到403
-      routes: dynamicRoutes,
+      routes: accessRoutes,
     });
 
     // 保存菜单信息和路由信息

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

@@ -1,6 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { DEFAULT_HOME_PATH } from '@vben/constants';
+import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 
 import { AuthPageLayout } from '#/layouts';
 import { $t } from '#/locales';
@@ -37,6 +37,7 @@ const coreRoutes: RouteRecordRaw[] = [
     },
     name: 'Authentication',
     path: '/auth',
+    redirect: LOGIN_PATH,
     children: [
       {
         name: 'Login',

+ 7 - 2
apps/web-ele/src/router/routes/index.ts

@@ -10,12 +10,16 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
 
 // 有需要可以自行打开注释,并创建文件夹
 // const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
 
 /** 动态路由 */
 const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
-/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统 */
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+/** 不需要权限的菜单列表(会显示在菜单中) */
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
 /** 路由列表,由基本路由+静态路由组成 */
@@ -28,4 +32,5 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
-export { coreRouteNames, dynamicRoutes, routes };
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };

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

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

+ 9 - 1
apps/web-naive/src/adapter/vxe-table.ts

@@ -11,8 +11,15 @@ setupVbenVxeTable({
     vxeUI.setConfig({
       grid: {
         align: 'center',
-        border: true,
+        border: false,
+        columnConfig: {
+          resizable: true,
+        },
         minHeight: 180,
+        formConfig: {
+          // 全局禁用vxe-table的表单配置,使用formOptions
+          enabled: false,
+        },
         proxyConfig: {
           autoLoad: true,
           response: {
@@ -24,6 +31,7 @@ setupVbenVxeTable({
           showResponseMsg: false,
         },
         round: true,
+        showOverflow: true,
         size: 'small',
       },
     });

+ 2 - 2
apps/web-naive/src/router/guard.ts

@@ -8,7 +8,7 @@ import { startProgress, stopProgress } from '@vben/utils';
 import { useTitle } from '@vueuse/core';
 
 import { $t } from '#/locales';
-import { coreRouteNames, dynamicRoutes } from '#/router/routes';
+import { accessRoutes, coreRouteNames } from '#/router/routes';
 import { useAuthStore } from '#/store';
 
 import { generateAccess } from './access';
@@ -104,7 +104,7 @@ function setupAccessGuard(router: Router) {
       roles: userRoles,
       router,
       // 则会在菜单中显示,但是访问会被重定向到403
-      routes: dynamicRoutes,
+      routes: accessRoutes,
     });
 
     // 保存菜单信息和路由信息

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

@@ -1,6 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { DEFAULT_HOME_PATH } from '@vben/constants';
+import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
 
 import { AuthPageLayout } from '#/layouts';
 import { $t } from '#/locales';
@@ -37,6 +37,7 @@ const coreRoutes: RouteRecordRaw[] = [
     },
     name: 'Authentication',
     path: '/auth',
+    redirect: LOGIN_PATH,
     children: [
       {
         name: 'Login',

+ 7 - 2
apps/web-naive/src/router/routes/index.ts

@@ -10,12 +10,16 @@ const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
 
 // 有需要可以自行打开注释,并创建文件夹
 // const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
 
 /** 动态路由 */
 const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
-/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统 */
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+/** 不需要权限的菜单列表(会显示在菜单中) */
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
 /** 路由列表,由基本路由+静态路由组成 */
@@ -28,4 +32,5 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
-export { coreRouteNames, dynamicRoutes, routes };
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };

+ 1 - 1
docs/package.json

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

+ 8 - 6
docs/src/components/common-ui/vben-modal.md

@@ -110,12 +110,14 @@ const [Modal, modalApi] = useVbenModal({
 
 以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。
 
-| 事件名 | 描述 | 类型 |
-| --- | --- | --- |
-| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
-| onCancel | 点击取消按钮触发 | `()=>void` |
-| onConfirm | 点击确认按钮触发 | `()=>void` |
-| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
+| 事件名 | 描述 | 类型 | 版本号 |
+| --- | --- | --- | --- |
+| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |  |
+| onCancel | 点击取消按钮触发 | `()=>void` |  |
+| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.4.3 |
+| onConfirm | 点击确认按钮触发 | `()=>void` |  |
+| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |  |
+| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.4.3 |
 
 ### Slots
 

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

@@ -384,6 +384,10 @@ interface RouteMeta {
    * The menu is visible, but access will be redirected to 403
    */
   menuVisibleWithForbidden?: boolean;
+  /**
+   * Open in a new window
+   */
+  openInNewWindow?: boolean;
   /**
    * Used for route->menu sorting
    */

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

@@ -10,7 +10,7 @@ outline: deep
 
 Before starting the project, ensure that your environment meets the following requirements:
 
-- [Node.js](https://nodejs.org/en) version 20 or above. It is recommended to use [fnm](https://github.com/Schniz/fnm) or [nvm](https://github.com/nvm-sh/nvm) for version management.
+- [Node.js](https://nodejs.org/en) version 20.15.0 or above. It is recommended to use [fnm](https://github.com/Schniz/fnm), [nvm](https://github.com/nvm-sh/nvm), or directly use [pnpm](https://pnpm.io/cli/env) for version management.
 - [Git](https://git-scm.com/) any version.
 
 To verify if your environment meets the above requirements, you can check the versions using the following commands:

+ 1 - 1
docs/src/guide/essentials/concept.md

@@ -1,6 +1,6 @@
 # 基础概念
 
-新版本中,整体工程进行了重构,现在我们将会介绍一些基础概念,以便于你更好的理解整个文档。请务必仔阅读这一部分。
+新版本中,整体工程进行了重构,现在我们将会介绍一些基础概念,以便于你更好的理解整个文档。请务必仔阅读这一部分。
 
 ## 大仓
 

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

@@ -382,6 +382,10 @@ interface RouteMeta {
    * 菜单可以看到,但是访问会被重定向到403
    */
   menuVisibleWithForbidden?: boolean;
+  /**
+   * 在新窗口打开
+   */
+  openInNewWindow?: boolean;
   /**
    * 用于路由->菜单排序
    */
@@ -539,6 +543,13 @@ interface RouteMeta {
 
 用于配置页面在菜单可以看到,但是访问会被重定向到403。
 
+### openInNewWindow
+
+- 类型:`boolean`
+- 默认值:`false`
+
+设置为 `true` 时,会在新窗口打开页面。
+
 ### order
 
 - 类型:`number`

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

@@ -6,7 +6,7 @@
 
 ## 原理
 
-由 `vite-plugin-inject-app-loading` 插件实现,插件会在每个应用注入一个全局的 `loading html`。
+由 `vite-plugin-inject-app-loading` 插件实现,插件会在每个应用注入一个全局的 `loading html`。
 
 ## 关闭
 

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

@@ -10,7 +10,7 @@ outline: deep
 
 在启动项目前,你需要确保你的环境满足以下要求:
 
-- [Node.js](https://nodejs.org/en) 20.15.0 及以上版本,推荐使用 [fnm](https://github.com/Schniz/fnm) 或者 [nvm](https://github.com/nvm-sh/nvm) 进行版本管理。
+- [Node.js](https://nodejs.org/en) 20.15.0 及以上版本,推荐使用 [fnm](https://github.com/Schniz/fnm) 、 [nvm](https://github.com/nvm-sh/nvm) 或者直接使用[pnpm](https://pnpm.io/cli/env) 进行版本管理。
 - [Git](https://git-scm.com/) 任意版本。
 
 验证你的环境是否满足以上要求,你可以通过以下命令查看版本:
@@ -74,7 +74,7 @@ pnpm install
 
 #### 选择项目
 
-执行以下命运行项目:
+执行以下命运行项目:
 
 ```bash
 # 启动项目

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

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

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

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/node-utils",
-  "version": "5.4.2",
+  "version": "5.4.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -35,13 +35,9 @@
     "dayjs": "catalog:",
     "execa": "catalog:",
     "find-up": "catalog:",
-    "nanoid": "catalog:",
     "ora": "catalog:",
     "pkg-types": "catalog:",
     "prettier": "catalog:",
     "rimraf": "catalog:"
-  },
-  "devDependencies": {
-    "@types/chalk": "catalog:"
   }
 }

+ 0 - 2
internal/node-utils/src/index.ts

@@ -13,8 +13,6 @@ export { default as colors } from 'chalk';
 export { consola } from 'consola';
 export * from 'execa';
 
-export { nanoid } from 'nanoid';
-
 export { default as fs } from 'node:fs/promises';
 
 export { type PackageJson, readPackageJSON } from 'pkg-types';

+ 0 - 1
internal/tailwind-config/build.config.ts

@@ -4,7 +4,6 @@ export default defineBuildConfig({
   clean: true,
   declaration: true,
   entries: ['src/index', './src/postcss.config'],
-  externals: ['@vben/node-utils'],
   rollup: {
     emitCJS: true,
   },

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/tailwind-config",
-  "version": "5.4.2",
+  "version": "5.4.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -12,7 +12,7 @@
   "license": "MIT",
   "type": "module",
   "scripts": {
-    "stub": "pnpm unbuild --stub"
+    "stub": "pnpm unbuild"
   },
   "files": [
     "dist"
@@ -48,6 +48,7 @@
   "dependencies": {
     "@iconify/json": "catalog:",
     "@iconify/tailwind": "catalog:",
+    "@manypkg/get-packages": "catalog:",
     "@tailwindcss/nesting": "catalog:",
     "@tailwindcss/typography": "catalog:",
     "autoprefixer": "catalog:",
@@ -60,7 +61,6 @@
     "tailwindcss-animate": "catalog:"
   },
   "devDependencies": {
-    "@types/postcss-import": "catalog:",
-    "@vben/node-utils": "workspace:*"
+    "@types/postcss-import": "catalog:"
   }
 }

+ 2 - 3
internal/tailwind-config/src/index.ts

@@ -2,9 +2,8 @@ import type { Config } from 'tailwindcss';
 
 import path from 'node:path';
 
-import { getPackagesSync } from '@vben/node-utils';
-
 import { addDynamicIconSelectors } from '@iconify/tailwind';
+import { getPackagesSync } from '@manypkg/get-packages';
 import typographyPlugin from '@tailwindcss/typography';
 import animate from 'tailwindcss-animate';
 
@@ -12,7 +11,7 @@ import { enterAnimationPlugin } from './plugins/entry';
 
 // import defaultTheme from 'tailwindcss/defaultTheme';
 
-const { packages } = getPackagesSync();
+const { packages } = getPackagesSync(process.cwd());
 
 const tailwindPackages: string[] = [];
 

+ 1 - 1
internal/tsconfig/package.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/vite-config",
-  "version": "5.4.2",
+  "version": "5.4.3",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -35,7 +35,6 @@
     "html-minifier-terser": "catalog:",
     "nitropack": "catalog:",
     "resolve.exports": "catalog:",
-    "vite-plugin-lib-inject-css": "catalog:",
     "vite-plugin-pwa": "catalog:",
     "vite-plugin-vue-devtools": "catalog:"
   },

+ 1 - 1
internal/vite-config/src/config/common.ts

@@ -3,7 +3,7 @@ import type { UserConfig } from 'vite';
 async function getCommonConfig(): Promise<UserConfig> {
   return {
     build: {
-      chunkSizeWarningLimit: 1000,
+      chunkSizeWarningLimit: 2000,
       reportCompressedSize: false,
       sourcemap: false,
     },

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

@@ -19,7 +19,6 @@ function defineLibraryConfig(userConfigPromise?: DefineLibraryOptions) {
 
     const plugins = await loadLibraryPlugins({
       dts: false,
-      injectLibCss: true,
       injectMetadata: true,
       isBuild,
       mode,

+ 1 - 6
internal/vite-config/src/plugins/index.ts

@@ -14,7 +14,6 @@ import { visualizer as viteVisualizerPlugin } from 'rollup-plugin-visualizer';
 import viteCompressPlugin from 'vite-plugin-compression';
 import viteDtsPlugin from 'vite-plugin-dts';
 import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';
-import { libInjectCss as viteLibInjectCss } from 'vite-plugin-lib-inject-css';
 import { VitePWA } from 'vite-plugin-pwa';
 import viteVueDevTools from 'vite-plugin-vue-devtools';
 
@@ -225,7 +224,7 @@ async function loadLibraryPlugins(
 ): Promise<PluginOption[]> {
   // 单独取,否则commonOptions拿不到
   const isBuild = options.isBuild;
-  const { dts, injectLibCss, ...commonOptions } = options;
+  const { dts, ...commonOptions } = options;
   const commonPlugins = await loadCommonPlugins(commonOptions);
   return await loadConditionPlugins([
     ...commonPlugins,
@@ -233,10 +232,6 @@ async function loadLibraryPlugins(
       condition: isBuild && !!dts,
       plugins: () => [viteDtsPlugin({ logLevel: 'error' })],
     },
-    {
-      condition: injectLibCss,
-      plugins: () => [viteLibInjectCss()],
-    },
   ]);
 }
 

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

@@ -130,9 +130,6 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
 interface LibraryPluginOptions extends CommonPluginOptions {
   /** 开启 dts 输出 */
   dts?: boolean | PluginOptions;
-
-  /** 是否注入lib css */
-  injectLibCss?: boolean;
 }
 
 type ApplicationOptions = ApplicationPluginOptions;

+ 3 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vben-admin-monorepo",
-  "version": "5.4.2",
+  "version": "5.4.3",
   "private": true,
   "keywords": [
     "monorepo",
@@ -99,7 +99,7 @@
     "node": ">=20.10.0",
     "pnpm": ">=9.5.0"
   },
-  "packageManager": "pnpm@9.12.2",
+  "packageManager": "pnpm@9.12.3",
   "pnpm": {
     "peerDependencyRules": {
       "allowedVersions": {
@@ -107,6 +107,7 @@
       }
     },
     "overrides": {
+      "@ast-grep/napi": "catalog:",
       "@ctrl/tinycolor": "catalog:",
       "clsx": "catalog:",
       "pinia": "catalog:",

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/design",
-  "version": "5.4.2",
+  "version": "5.4.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {
@@ -28,7 +28,7 @@
     ".": {
       "types": "./src/index.ts",
       "development": "./src/index.ts",
-      "default": "./dist/index.mjs"
+      "default": "./dist/style.css"
     }
   },
   "publishConfig": {

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

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

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

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

+ 12 - 1
packages/@core/base/shared/src/utils/window.ts

@@ -23,4 +23,15 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
   window.open(url, target, features);
 }
 
-export { openWindow };
+/**
+ * 在新窗口中打开路由。
+ * @param path
+ */
+function openRouteInNewWindow(path: string) {
+  const { hash, origin } = location;
+  const fullPath = path.startsWith('/') ? path : `/${path}`;
+  const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
+  openWindow(url, { target: '_blank' });
+}
+
+export { openRouteInNewWindow, openWindow };

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

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

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

@@ -98,6 +98,10 @@ interface RouteMeta {
    * 菜单可以看到,但是访问会被重定向到403
    */
   menuVisibleWithForbidden?: boolean;
+  /**
+   * 在新窗口打开
+   */
+  openInNewWindow?: boolean;
   /**
    * 用于路由->菜单排序
    */

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

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

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

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

+ 4 - 1
packages/@core/ui-kit/form-ui/src/form-render/form.vue

@@ -81,7 +81,10 @@ const formCollapsed = computed(() => {
 });
 
 const computedSchema = computed(
-  (): ({ commonComponentProps: Record<string, any> } & FormSchema)[] => {
+  (): ({
+    commonComponentProps: Record<string, any>;
+    formFieldProps: Record<string, any>;
+  } & Omit<FormSchema, 'formFieldProps'>)[] => {
     const {
       componentProps = {},
       controlClass = '',

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

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

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

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

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

@@ -64,7 +64,6 @@ const activePath = ref<MenuProvider['activePath']>(props.defaultActive);
 const items = ref<MenuProvider['items']>({});
 const subMenus = ref<MenuProvider['subMenus']>({});
 const mouseInChild = ref(false);
-const defaultSlots: VNodeArrayChildren = slots.default?.() ?? [];
 
 const isMenuPopup = computed<MenuProvider['isMenuPopup']>(() => {
   return (
@@ -73,6 +72,9 @@ const isMenuPopup = computed<MenuProvider['isMenuPopup']>(() => {
 });
 
 const getSlot = computed(() => {
+  // 更新插槽内容
+  const defaultSlots: VNodeArrayChildren = slots.default?.() ?? [];
+
   const originalSlot = flattedChildren(defaultSlots) as VNodeArrayChildren;
   const slotDefault =
     sliceIndex.value === -1
@@ -718,6 +720,10 @@ $namespace: vben;
     align-items: center;
     width: 100%;
     height: var(--menu-item-height);
+
+    span {
+      @include menu-title;
+    }
   }
 
   &.is-collapse-show-title {

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

@@ -42,7 +42,9 @@ const hasChildren = computed(() => {
     :icon="menu.icon"
     :path="menu.path"
   >
-    <template #title>{{ menu.name }}</template>
+    <template #title>
+      <span>{{ menu.name }}</span>
+    </template>
   </MenuItem>
   <SubMenuComp
     v-else
@@ -59,7 +61,9 @@ const hasChildren = computed(() => {
         class="right-6"
       />
     </template>
-    <template #title>{{ menu.name }}</template>
+    <template #title>
+      <span>{{ menu.name }}</span>
+    </template>
     <template v-for="childItem in menu.children || []" :key="childItem.path">
       <SubMenu :menu="childItem" />
     </template>

+ 15 - 0
packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts

@@ -110,4 +110,19 @@ describe('modalApi', () => {
     expect(modalApi.store.state.title).toBe('Batch Title');
     expect(modalApi.store.state.confirmText).toBe('Batch Confirm');
   });
+
+  it('should call onClosed callback when provided', () => {
+    const onClosed = vi.fn();
+    const modalApiWithHook = new ModalApi({ onClosed });
+    modalApiWithHook.onClosed();
+    expect(onClosed).toHaveBeenCalled();
+  });
+
+  it('should call onOpened callback when provided', () => {
+    const onOpened = vi.fn();
+    const modalApiWithHook = new ModalApi({ onOpened });
+    modalApiWithHook.open();
+    modalApiWithHook.onOpened();
+    expect(onOpened).toHaveBeenCalled();
+  });
 });

+ 29 - 2
packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts

@@ -6,7 +6,12 @@ import { bindMethods, isFunction } from '@vben-core/shared/utils';
 export class ModalApi {
   private api: Pick<
     ModalApiOptions,
-    'onBeforeClose' | 'onCancel' | 'onConfirm' | 'onOpenChange'
+    | 'onBeforeClose'
+    | 'onCancel'
+    | 'onClosed'
+    | 'onConfirm'
+    | 'onOpenChange'
+    | 'onOpened'
   >;
   // private prevState!: ModalState;
   private state!: ModalState;
@@ -23,13 +28,15 @@ export class ModalApi {
       connectedComponent: _,
       onBeforeClose,
       onCancel,
+      onClosed,
       onConfirm,
       onOpenChange,
+      onOpened,
       ...storeState
     } = options;
 
     const defaultState: ModalState = {
-      bordered: false,
+      bordered: true,
       centered: false,
       class: '',
       closeOnClickModal: true,
@@ -77,8 +84,10 @@ export class ModalApi {
     this.api = {
       onBeforeClose,
       onCancel,
+      onClosed,
       onConfirm,
       onOpenChange,
+      onOpened,
     };
     bindMethods(this);
   }
@@ -115,6 +124,15 @@ export class ModalApi {
     }
   }
 
+  /**
+   * 弹窗关闭动画播放完毕后的回调
+   */
+  onClosed() {
+    if (!this.state.isOpen) {
+      this.api.onClosed?.();
+    }
+  }
+
   /**
    * 确认操作
    */
@@ -122,6 +140,15 @@ export class ModalApi {
     this.api.onConfirm?.();
   }
 
+  /**
+   * 弹窗打开动画播放完毕后的回调
+   */
+  onOpened() {
+    if (this.state.isOpen) {
+      this.api.onOpened?.();
+    }
+  }
+
   open() {
     this.store.setState((prev) => ({ ...prev, isOpen: true }));
   }

+ 10 - 0
packages/@core/ui-kit/popup-ui/src/modal/modal.ts

@@ -139,6 +139,11 @@ export interface ModalApiOptions extends ModalState {
    * 点击取消按钮的回调
    */
   onCancel?: () => void;
+  /**
+   * 弹窗关闭动画结束的回调
+   * @returns
+   */
+  onClosed?: () => void;
   /**
    * 点击确定按钮的回调
    */
@@ -149,4 +154,9 @@ export interface ModalApiOptions extends ModalState {
    * @returns
    */
   onOpenChange?: (isOpen: boolean) => void;
+  /**
+   * 弹窗打开动画结束的回调
+   * @returns
+   */
+  onOpened?: () => void;
 }

+ 9 - 1
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@@ -188,10 +188,12 @@ function handleFocusOutside(e: Event) {
       :show-close="closable"
       close-class="top-3"
       @close-auto-focus="handleFocusOutside"
+      @closed="() => modalApi?.onClosed()"
       @escape-key-down="escapeKeyDown"
       @focus-outside="handleFocusOutside"
       @interact-outside="interactOutside"
       @open-auto-focus="handerOpenAutoFocus"
+      @opened="() => modalApi?.onOpened()"
       @pointer-down-outside="pointerDownOutside"
     >
       <DialogHeader
@@ -258,7 +260,13 @@ function handleFocusOutside(e: Event) {
         v-if="showFooter"
         ref="footerRef"
         :class="
-          cn('flex-row items-center justify-end border-t p-2', footerClass)
+          cn(
+            'flex-row items-center justify-end p-2',
+            {
+              'border-t': bordered,
+            },
+            footerClass,
+          )
         "
       >
         <slot name="prepend-footer"></slot>

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/shadcn-ui",
-  "version": "5.4.2",
+  "version": "5.4.3",
   "#main": "./dist/index.mjs",
   "#module": "./dist/index.mjs",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",

+ 7 - 6
packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-background.vue

@@ -6,14 +6,12 @@ import { VbenIcon } from '../icon';
 interface Props extends BreadcrumbProps {}
 
 defineOptions({ name: 'Breadcrumb' });
-withDefaults(defineProps<Props>(), {
-  showIcon: false,
-});
+const { breadcrumbs, showIcon } = defineProps<Props>();
 
 const emit = defineEmits<{ select: [string] }>();
 
-function handleClick(path?: string) {
-  if (!path) {
+function handleClick(index: number, path?: string) {
+  if (!path || index === breadcrumbs.length - 1) {
     return;
   }
   emit('select', path);
@@ -27,7 +25,10 @@ function handleClick(path?: string) {
         :key="`${item.path}-${item.title}-${index}`"
       >
         <li>
-          <a href="javascript:void 0" @click.stop="handleClick(item.path)">
+          <a
+            href="javascript:void 0"
+            @click.stop="handleClick(index, item.path)"
+          >
             <span class="flex-center z-10 h-full">
               <VbenIcon
                 v-if="showIcon"

+ 11 - 2
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

@@ -27,7 +27,9 @@ const props = withDefaults(
   >(),
   { showClose: true },
 );
-const emits = defineEmits<{ close: [] } & DialogContentEmits>();
+const emits = defineEmits<
+  { close: []; closed: []; opened: [] } & DialogContentEmits
+>();
 
 const delegatedProps = computed(() => {
   const {
@@ -44,7 +46,13 @@ const delegatedProps = computed(() => {
 const forwarded = useForwardPropsEmits(delegatedProps, emits);
 
 const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
-
+function onAnimationEnd() {
+  if (props.open) {
+    emits('opened');
+  } else {
+    emits('closed');
+  }
+}
 defineExpose({
   getContentRef: () => contentRef.value,
 });
@@ -57,6 +65,7 @@ defineExpose({
     </Transition>
     <DialogContent
       ref="contentRef"
+      @animationend="onAnimationEnd"
       v-bind="forwarded"
       :class="
         cn(

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

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

+ 1 - 1
packages/constants/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/constants",
-  "version": "5.4.2",
+  "version": "5.4.3",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác