浏览代码

Merge tag 'v5.5.6' into admin-dz

laiqi 11 月之前
父节点
当前提交
b56fd6fb11
共有 100 个文件被更改,包括 2278 次插入326 次删除
  1. 1 1
      .gitpod.yml
  2. 0 6
      .husky/commit-msg
  3. 0 3
      .husky/post-merge
  4. 0 7
      .husky/pre-commit
  5. 0 20
      .lintstagedrc.mjs
  6. 1 1
      .node-version
  7. 1 1
      .npmrc
  8. 17 3
      .vscode/settings.json
  9. 13 0
      apps/backend-mock/api/upload.ts
  10. 3 0
      apps/web-ele/.env
  11. 1 1
      apps/web-ele/package.json
  12. 150 67
      apps/web-ele/src/adapter/component/index.ts
  13. 3 2
      apps/web-ele/src/bootstrap.ts
  14. 1 1
      apps/web-ele/src/layouts/basic.vue
  15. 5 5
      apps/web-ele/src/router/guard.ts
  16. 7 8
      apps/web-ele/src/router/routes/core.ts
  17. 5 2
      apps/web-ele/src/store/auth.ts
  18. 166 0
      docs/src/components/common-ui/vben-alert.md
  19. 36 0
      docs/src/demos/vben-alert/alert/index.vue
  20. 75 0
      docs/src/demos/vben-alert/confirm/index.vue
  21. 118 0
      docs/src/demos/vben-alert/prompt/index.vue
  22. 1 1
      internal/lint-configs/commitlint-config/package.json
  23. 1 1
      internal/lint-configs/eslint-config/src/configs/perfectionist.ts
  24. 10 6
      internal/lint-configs/eslint-config/src/configs/vue.ts
  25. 7 0
      internal/lint-configs/eslint-config/src/custom-config.ts
  26. 1 0
      internal/lint-configs/stylelint-config/index.mjs
  27. 1 1
      internal/lint-configs/stylelint-config/package.json
  28. 1 1
      internal/node-utils/package.json
  29. 1 1
      internal/tailwind-config/package.json
  30. 1 1
      internal/tsconfig/package.json
  31. 1 1
      internal/vite-config/package.json
  32. 218 39
      internal/vite-config/src/typing.ts
  33. 76 0
      lefthook.yml
  34. 6 8
      package.json
  35. 1 1
      packages/@core/base/design/package.json
  36. 1 1
      packages/@core/base/design/src/design-tokens/dark.css
  37. 1 1
      packages/@core/base/icons/package.json
  38. 2 0
      packages/@core/base/icons/src/lucide.ts
  39. 3 1
      packages/@core/base/shared/package.json
  40. 1 0
      packages/@core/base/shared/src/utils/index.ts
  41. 1 0
      packages/@core/base/shared/src/utils/inference.ts
  42. 1 1
      packages/@core/base/typings/package.json
  43. 1 1
      packages/@core/composables/package.json
  44. 2 0
      packages/@core/composables/src/use-simple-locale/messages.ts
  45. 3 0
      packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
  46. 1 1
      packages/@core/preferences/package.json
  47. 3 0
      packages/@core/preferences/src/config.ts
  48. 10 3
      packages/@core/preferences/src/preferences.ts
  49. 6 0
      packages/@core/preferences/src/types.ts
  50. 2 1
      packages/@core/ui-kit/form-ui/package.json
  51. 1 1
      packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  52. 119 0
      packages/@core/ui-kit/form-ui/src/form-api.ts
  53. 7 1
      packages/@core/ui-kit/form-ui/src/form-render/expandable.ts
  54. 23 2
      packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
  55. 29 1
      packages/@core/ui-kit/form-ui/src/types.ts
  56. 7 3
      packages/@core/ui-kit/form-ui/src/use-form-context.ts
  57. 1 1
      packages/@core/ui-kit/form-ui/src/use-vben-form.ts
  58. 37 6
      packages/@core/ui-kit/form-ui/src/vben-use-form.vue
  59. 1 1
      packages/@core/ui-kit/layout-ui/package.json
  60. 8 2
      packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue
  61. 10 0
      packages/@core/ui-kit/layout-ui/src/vben-layout.ts
  62. 4 0
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  63. 1 1
      packages/@core/ui-kit/menu-ui/package.json
  64. 26 14
      packages/@core/ui-kit/menu-ui/src/components/menu.vue
  65. 1 1
      packages/@core/ui-kit/menu-ui/src/components/sub-menu-content.vue
  66. 2 0
      packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue
  67. 46 0
      packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts
  68. 0 6
      packages/@core/ui-kit/menu-ui/src/menu.vue
  69. 6 0
      packages/@core/ui-kit/menu-ui/src/types.ts
  70. 244 0
      packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts
  71. 99 0
      packages/@core/ui-kit/popup-ui/src/alert/alert.ts
  72. 211 0
      packages/@core/ui-kit/popup-ui/src/alert/alert.vue
  73. 14 0
      packages/@core/ui-kit/popup-ui/src/alert/index.ts
  74. 0 1
      packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts
  75. 4 3
      packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
  76. 6 6
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
  77. 43 22
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
  78. 13 2
      packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts
  79. 1 0
      packages/@core/ui-kit/popup-ui/src/index.ts
  80. 1 0
      packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
  81. 4 4
      packages/@core/ui-kit/popup-ui/src/modal/modal.ts
  82. 35 21
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  83. 15 5
      packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts
  84. 1 1
      packages/@core/ui-kit/shadcn-ui/package.json
  85. 4 4
      packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue
  86. 7 6
      packages/@core/ui-kit/shadcn-ui/src/components/logo/logo.vue
  87. 3 1
      packages/@core/ui-kit/shadcn-ui/src/components/popover/popover.vue
  88. 18 2
      packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue
  89. 16 4
      packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue
  90. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue
  91. 26 4
      packages/@core/ui-kit/shadcn-ui/src/components/select/select.vue
  92. 16 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue
  93. 13 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue
  94. 13 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue
  95. 101 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue
  96. 28 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue
  97. 8 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue
  98. 30 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue
  99. 6 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts
  100. 1 0
      packages/@core/ui-kit/shadcn-ui/src/ui/index.ts

+ 1 - 1
.gitpod.yml

@@ -2,5 +2,5 @@ ports:
   - port: 5555
     onOpen: open-preview
 tasks:
-  - init: corepack enable && pnpm install
+  - init: npm i -g corepack && pnpm install
     command: pnpm run dev:play

+ 0 - 6
.husky/commit-msg

@@ -1,6 +0,0 @@
-echo Start running commit-msg hook...
-
-# Check whether the git commit information is standardized
-pnpm exec commitlint --edit "$1"
-
-echo Run commit-msg hook done.

+ 0 - 3
.husky/post-merge

@@ -1,3 +0,0 @@
-# 每次 git pull 之后, 安装依赖
-
-pnpm install

+ 0 - 7
.husky/pre-commit

@@ -1,7 +0,0 @@
-# update `.vscode/vben-admin.code-workspace` file
-pnpm vsh code-workspace --auto-commit
-
-# Format and submit code according to lintstagedrc.js configuration
-pnpm exec lint-staged
-
-echo Run pre-commit hook done.

+ 0 - 20
.lintstagedrc.mjs

@@ -1,20 +0,0 @@
-export default {
-  '*.md': ['prettier --cache --ignore-unknown --write'],
-  '*.vue': [
-    'prettier --write',
-    'eslint --cache --fix',
-    'stylelint --fix --allow-empty-input',
-  ],
-  '*.{js,jsx,ts,tsx}': [
-    'prettier --cache --ignore-unknown  --write',
-    'eslint --cache --fix',
-  ],
-  '*.{scss,less,styl,html,vue,css}': [
-    'prettier --cache --ignore-unknown --write',
-    'stylelint --fix --allow-empty-input',
-  ],
-  'package.json': ['prettier --cache --write'],
-  '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
-    'prettier --cache --write--parser json',
-  ],
-};

+ 1 - 1
.node-version

@@ -1 +1 @@
-20.14.0
+22.1.0

+ 1 - 1
.npmrc

@@ -1,5 +1,5 @@
 registry = "https://registry.npmmirror.com"
-public-hoist-pattern[]=husky
+public-hoist-pattern[]=lefthook
 public-hoist-pattern[]=eslint
 public-hoist-pattern[]=prettier
 public-hoist-pattern[]=prettier-plugin-tailwindcss

+ 17 - 3
.vscode/settings.json

@@ -14,7 +14,7 @@
   "editor.tabSize": 2,
   "editor.detectIndentation": false,
   "editor.cursorBlinking": "expand",
-  "editor.largeFileOptimizations": false,
+  "editor.largeFileOptimizations": true,
   "editor.accessibilitySupport": "off",
   "editor.cursorSmoothCaretAnimation": "on",
   "editor.guides.bracketPairs": "active",
@@ -91,6 +91,7 @@
     "**/bower_components": true,
     "**/.turbo": true,
     "**/.idea": true,
+    "**/.vitepress": true,
     "**/tmp": true,
     "**/.git": true,
     "**/.svn": true,
@@ -112,6 +113,8 @@
     "**/yarn.lock": true
   },
 
+  "typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
+
   // search
   "search.searchEditor.singleClickBehaviour": "peekDefinition",
   "search.followSymlinks": false,
@@ -216,12 +219,23 @@
     "*.env": "$(capture).env.*",
     "README.md": "README*,CHANGELOG*,LICENSE,CNAME",
     "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
-    "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
+    "eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
     "tailwind.config.mjs": "postcss.*"
   },
   "commentTranslate.hover.enabled": false,
   "commentTranslate.multiLineMerge": true,
   "vue.server.hybridMode": true,
   "typescript.tsdk": "node_modules/typescript/lib",
-  "oxc.enable": false
+  "oxc.enable": false,
+  "cSpell.words": [
+    "archiver",
+    "axios",
+    "dotenv",
+    "isequal",
+    "jspm",
+    "napi",
+    "nolebase",
+    "rollup",
+    "vitest"
+  ]
 }

+ 13 - 0
apps/backend-mock/api/upload.ts

@@ -0,0 +1,13 @@
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { unAuthorizedResponse } from '~/utils/response';
+
+export default eventHandler((event) => {
+  const userinfo = verifyAccessToken(event);
+  if (!userinfo) {
+    return unAuthorizedResponse(event);
+  }
+  return useResponseSuccess({
+    url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
+  });
+  // return useResponseError("test")
+});

+ 3 - 0
apps/web-ele/.env

@@ -3,3 +3,6 @@ VITE_APP_TITLE=达川农机优惠劵平台
 
 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离
 VITE_APP_NAMESPACE=samool-dcnj-admin
+
+# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
+VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key

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

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

+ 150 - 67
apps/web-ele/src/adapter/component/index.ts

@@ -8,41 +8,132 @@ import type { Component } from 'vue';
 import type { BaseFormComponentType } from '@vben/common-ui';
 import type { Recordable } from '@vben/types';
 
-import { defineComponent, getCurrentInstance, h, ref } from 'vue';
+import {
+  defineAsyncComponent,
+  defineComponent,
+  getCurrentInstance,
+  h,
+  ref,
+} from 'vue';
 
 import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
-import {
-  ElButton,
-  ElCheckbox,
-  ElCheckboxButton,
-  ElCheckboxGroup,
-  ElDatePicker,
-  ElDivider,
-  ElInput,
-  ElInputNumber,
-  ElNotification,
-  ElRadio,
-  ElRadioButton,
-  ElRadioGroup,
-  ElSelectV2,
-  ElSpace,
-  ElSwitch,
-  ElTimePicker,
-  ElTreeSelect,
-  ElUpload,
-} from 'element-plus';
+import { ElNotification } from 'element-plus';
+
+const ElButton = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/button/index'),
+    import('element-plus/es/components/button/style/css'),
+  ]).then(([res]) => res.ElButton),
+);
+const ElCheckbox = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/checkbox/index'),
+    import('element-plus/es/components/checkbox/style/css'),
+  ]).then(([res]) => res.ElCheckbox),
+);
+const ElCheckboxButton = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/checkbox/index'),
+    import('element-plus/es/components/checkbox-button/style/css'),
+  ]).then(([res]) => res.ElCheckboxButton),
+);
+const ElCheckboxGroup = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/checkbox/index'),
+    import('element-plus/es/components/checkbox-group/style/css'),
+  ]).then(([res]) => res.ElCheckboxGroup),
+);
+const ElDatePicker = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/date-picker/index'),
+    import('element-plus/es/components/date-picker/style/css'),
+  ]).then(([res]) => res.ElDatePicker),
+);
+const ElDivider = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/divider/index'),
+    import('element-plus/es/components/divider/style/css'),
+  ]).then(([res]) => res.ElDivider),
+);
+const ElInput = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/input/index'),
+    import('element-plus/es/components/input/style/css'),
+  ]).then(([res]) => res.ElInput),
+);
+const ElInputNumber = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/input-number/index'),
+    import('element-plus/es/components/input-number/style/css'),
+  ]).then(([res]) => res.ElInputNumber),
+);
+const ElRadio = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/radio/index'),
+    import('element-plus/es/components/radio/style/css'),
+  ]).then(([res]) => res.ElRadio),
+);
+const ElRadioButton = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/radio/index'),
+    import('element-plus/es/components/radio-button/style/css'),
+  ]).then(([res]) => res.ElRadioButton),
+);
+const ElRadioGroup = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/radio/index'),
+    import('element-plus/es/components/radio-group/style/css'),
+  ]).then(([res]) => res.ElRadioGroup),
+);
+const ElSelectV2 = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/select-v2/index'),
+    import('element-plus/es/components/select-v2/style/css'),
+  ]).then(([res]) => res.ElSelectV2),
+);
+const ElSpace = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/space/index'),
+    import('element-plus/es/components/space/style/css'),
+  ]).then(([res]) => res.ElSpace),
+);
+const ElSwitch = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/switch/index'),
+    import('element-plus/es/components/switch/style/css'),
+  ]).then(([res]) => res.ElSwitch),
+);
+const ElTimePicker = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/time-picker/index'),
+    import('element-plus/es/components/time-picker/style/css'),
+  ]).then(([res]) => res.ElTimePicker),
+);
+const ElTreeSelect = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/tree-select/index'),
+    import('element-plus/es/components/tree-select/style/css'),
+  ]).then(([res]) => res.ElTreeSelect),
+);
+const ElUpload = defineAsyncComponent(() =>
+  Promise.all([
+    import('element-plus/es/components/upload/index'),
+    import('element-plus/es/components/upload/style/css'),
+  ]).then(([res]) => res.ElUpload),
+);
 
 import PermissionTree from '../../components/permission-tree/index.vue';
 
 const withDefaultPlaceholder = <T extends Component>(
   component: T,
   type: 'input' | 'select',
+  componentProps: Recordable<any> = {},
 ) => {
   return defineComponent({
-    inheritAttrs: false,
     name: component.name,
+    inheritAttrs: false,
     setup: (props: any, { attrs, expose, slots }) => {
       const placeholder =
         props?.placeholder ||
@@ -61,7 +152,11 @@ const withDefaultPlaceholder = <T extends Component>(
         }
       });
       return () =>
-        h(component, { ...props, ...attrs, placeholder, ref: innerRef }, slots);
+        h(
+          component,
+          { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
+          slots,
+        );
     },
   });
 };
@@ -92,37 +187,33 @@ async function initComponentAdapter() {
     // 如果你的组件体积比较大,可以使用异步加载
     // Button: () =>
     // import('xxx').then((res) => res.Button),
-    ApiSelect: (props, { attrs, slots }) => {
-      return h(
-        ApiComponent,
-        {
-          placeholder: $t('ui.placeholder.select'),
-          ...props,
-          ...attrs,
-          component: ElSelectV2,
-          loadingSlot: 'loading',
-          visibleEvent: 'onVisibleChange',
-        },
-        slots,
-      );
-    },
-    ApiTreeSelect: (props, { attrs, slots }) => {
-      return h(
-        ApiComponent,
-        {
-          placeholder: $t('ui.placeholder.select'),
-          ...props,
-          ...attrs,
-          component: ElTreeSelect,
-          props: { label: 'label', children: 'children' },
-          nodeKey: 'value',
-          loadingSlot: 'loading',
-          optionsPropName: 'data',
-          visibleEvent: 'onVisibleChange',
-        },
-        slots,
-      );
-    },
+    ApiSelect: withDefaultPlaceholder(
+      {
+        ...ApiComponent,
+        name: 'ApiSelect',
+      },
+      'select',
+      {
+        component: ElSelectV2,
+        loadingSlot: 'loading',
+        visibleEvent: 'onVisibleChange',
+      },
+    ),
+    ApiTreeSelect: withDefaultPlaceholder(
+      {
+        ...ApiComponent,
+        name: 'ApiTreeSelect',
+      },
+      'select',
+      {
+        component: ElTreeSelect,
+        props: { label: 'label', children: 'children' },
+        nodeKey: 'value',
+        loadingSlot: 'loading',
+        optionsPropName: 'data',
+        visibleEvent: 'onVisibleChange',
+      },
+    ),
     Checkbox: ElCheckbox,
     CheckboxGroup: (props, { attrs, slots }) => {
       let defaultSlot;
@@ -152,19 +243,11 @@ async function initComponentAdapter() {
       return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
     },
     Divider: ElDivider,
-    IconPicker: (props, { attrs, slots }) => {
-      return h(
-        IconPicker,
-        {
-          iconSlot: 'append',
-          modelValueProp: 'model-value',
-          inputComponent: ElInput,
-          ...props,
-          ...attrs,
-        },
-        slots,
-      );
-    },
+    IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
+      iconSlot: 'append',
+      modelValueProp: 'model-value',
+      inputComponent: ElInput,
+    }),
     Input: withDefaultPlaceholder(ElInput, 'input'),
     InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
     PermissionTree: (props, { attrs, slots }) => {

+ 3 - 2
apps/web-ele/src/bootstrap.ts

@@ -1,8 +1,7 @@
 import { createApp, watchEffect } from 'vue';
 
 import { registerAccessDirective } from '@vben/access';
-import { initTippy, registerLoadingDirective } from '@vben/common-ui';
-import { MotionPlugin } from '@vben/plugins/motion';
+import { registerLoadingDirective } from '@vben/common-ui';
 import { preferences } from '@vben/preferences';
 import { initStores } from '@vben/stores';
 import '@vben/styles';
@@ -49,12 +48,14 @@ async function bootstrap(namespace: string) {
   registerAccessDirective(app);
 
   // 初始化 tippy
+  const { initTippy } = await import('@vben/common-ui/es/tippy');
   initTippy(app);
 
   // 配置路由及路由守卫
   app.use(router);
 
   // 配置Motion插件
+  const { MotionPlugin } = await import('@vben/plugins/motion');
   app.use(MotionPlugin);
 
   // 动态更新标题

+ 1 - 1
apps/web-ele/src/layouts/basic.vue

@@ -125,7 +125,7 @@ watch(
   async (enable) => {
     if (enable) {
       await updateWatermark({
-        content: `${userStore.userInfo?.username}`,
+        content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
       });
     } else {
       destroyWatermark();

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

@@ -1,6 +1,6 @@
 import type { Router } from 'vue-router';
 
-import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
+import { LOGIN_PATH } from '@vben/constants';
 import { preferences } from '@vben/preferences';
 import { useAccessStore, useUserStore } from '@vben/stores';
 import { startProgress, stopProgress } from '@vben/utils';
@@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) {
         return decodeURIComponent(
           (to.query?.redirect as string) ||
             userStore.userInfo?.homePath ||
-            DEFAULT_HOME_PATH,
+            preferences.app.defaultHomePath,
         );
       }
       return true;
@@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) {
           path: LOGIN_PATH,
           // 如不需要,直接删除 query
           query:
-            to.fullPath === DEFAULT_HOME_PATH
+            to.fullPath === preferences.app.defaultHomePath
               ? {}
               : { redirect: encodeURIComponent(to.fullPath) },
           // 携带当前跳转的页面,登录后重新跳转该页面
@@ -109,8 +109,8 @@ function setupAccessGuard(router: Router) {
     accessStore.setAccessRoutes(accessibleRoutes);
     accessStore.setIsAccessChecked(true);
     const redirectPath = (from.query.redirect ??
-      (to.path === DEFAULT_HOME_PATH
-        ? userInfo.homePath || DEFAULT_HOME_PATH
+      (to.path === preferences.app.defaultHomePath
+        ? userInfo.homePath || preferences.app.defaultHomePath
         : to.fullPath)) as string;
 
     return {

+ 7 - 8
apps/web-ele/src/router/routes/core.ts

@@ -1,13 +1,12 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
 
-import { AuthPageLayout, BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
-import Login from '#/views/_core/authentication/login.vue';
-
-import { dynamicRoutes } from './dynamic';
 
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
 /** 全局404页面 */
 const fallbackNotFoundRoute: RouteRecordRaw = {
   component: () => import('#/views/_core/fallback/not-found.vue'),
@@ -36,8 +35,8 @@ const coreRoutes: RouteRecordRaw[] = [
     },
     name: 'Root',
     path: '/',
-    redirect: DEFAULT_HOME_PATH,
-    children: dynamicRoutes,
+    redirect: preferences.app.defaultHomePath,
+    children: [],
   },
   {
     component: AuthPageLayout,
@@ -52,7 +51,7 @@ const coreRoutes: RouteRecordRaw[] = [
       {
         name: 'Login',
         path: 'login',
-        component: Login,
+        component: () => import('#/views/_core/authentication/login.vue'),
         meta: {
           title: $t('page.auth.login'),
         },

+ 5 - 2
apps/web-ele/src/store/auth.ts

@@ -3,7 +3,8 @@ import type { Recordable, UserInfo } from '@vben/types';
 import { ref } from 'vue';
 import { useRouter } from 'vue-router';
 
-import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
 import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
 import { useStorage } from '@vben/utils';
 
@@ -92,7 +93,9 @@ export const useAuthStore = defineStore('auth', () => {
         } else {
           onSuccess
             ? await onSuccess?.()
-            : await router.push(userInfo?.homePath || DEFAULT_HOME_PATH);
+            : await router.push(
+                userInfo.homePath || preferences.app.defaultHomePath,
+              );
         }
 
         if (userInfo?.realName) {

+ 166 - 0
docs/src/components/common-ui/vben-alert.md

@@ -0,0 +1,166 @@
+---
+outline: deep
+---
+
+# Vben Alert 轻量提示框
+
+框架提供的一些用于轻量提示的弹窗,仅使用js代码即可快速动态创建提示而不需要在template写任何代码。
+
+::: info 应用场景
+
+Alert提供的功能与Modal类似,但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求,请使用VbenModal
+
+:::
+
+::: tip 注意
+
+Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下,不支持HMR(热更新),代码变更后需要关闭这些弹窗后重新打开。
+
+:::
+
+::: tip README
+
+下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
+
+:::
+
+## 基础用法
+
+使用 `alert` 创建只有一个确认按钮的提示框。
+
+<DemoPreview dir="demos/vben-alert/alert" />
+
+使用 `confirm` 创建有确认和取消按钮的提示框。
+
+<DemoPreview dir="demos/vben-alert/confirm" />
+
+使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。
+
+<DemoPreview dir="demos/vben-alert/prompt" />
+
+## useAlertContext
+
+当弹窗的content、footer、icon使用自定义组件时,在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
+
+::: tip 注意
+
+`useAlertContext`只能用在setup或者函数式组件中。
+
+:::
+
+### Methods
+
+| 方法      | 描述               | 类型     | 版本要求 |
+| --------- | ------------------ | -------- | -------- |
+| doConfirm | 调用弹窗的确认操作 | ()=>void | >5.5.4   |
+| doCancel  | 调用弹窗的取消操作 | ()=>void | >5.5.4   |
+
+## 类型说明
+
+```ts
+/** 预置的图标类型 */
+export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
+
+export type BeforeCloseScope = {
+  /** 是否为点击确认按钮触发的关闭 */
+  isConfirm: boolean;
+};
+
+/**
+ * alert 属性
+ */
+export type AlertProps = {
+  /** 关闭前的回调,如果返回false,则终止关闭 */
+  beforeClose?: (
+    scope: BeforeCloseScope,
+  ) => boolean | Promise<boolean | undefined> | undefined;
+  /** 边框 */
+  bordered?: boolean;
+  /** 按钮对齐方式 */
+  buttonAlign?: 'center' | 'end' | 'start';
+  /** 取消按钮的标题 */
+  cancelText?: string;
+  /** 是否居中显示 */
+  centered?: boolean;
+  /** 确认按钮的标题 */
+  confirmText?: string;
+  /** 弹窗容器的额外样式 */
+  containerClass?: string;
+  /** 弹窗提示内容 */
+  content: Component | string;
+  /** 弹窗内容的额外样式 */
+  contentClass?: string;
+  /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
+  contentMasking?: boolean;
+  /** 弹窗底部内容(与按钮在同一个容器中) */
+  footer?: Component | string;
+  /** 弹窗的图标(在标题的前面) */
+  icon?: Component | IconType;
+  /**
+   * 弹窗遮罩模糊效果
+   */
+  overlayBlur?: number;
+  /** 是否显示取消按钮 */
+  showCancel?: boolean;
+  /** 弹窗标题 */
+  title?: string;
+};
+
+/** prompt 属性 */
+export type PromptProps<T = any> = {
+  /** 关闭前的回调,如果返回false,则终止关闭 */
+  beforeClose?: (scope: {
+    isConfirm: boolean;
+    value: T | undefined;
+  }) => boolean | Promise<boolean | undefined> | undefined;
+  /** 用于接受用户输入的组件 */
+  component?: Component;
+  /** 输入组件的属性 */
+  componentProps?: Recordable<any>;
+  /** 输入组件的插槽 */
+  componentSlots?: Recordable<Component>;
+  /** 默认值 */
+  defaultValue?: T;
+  /** 输入组件的值属性名 */
+  modelPropName?: string;
+} & Omit<AlertProps, 'beforeClose'>;
+
+/**
+ * 函数签名
+ * alert和confirm的函数签名相同。
+ * confirm默认会显示取消按钮,而alert默认只有一个按钮
+ *  */
+export function alert(options: AlertProps): Promise<void>;
+export function alert(
+  message: string,
+  options?: Partial<AlertProps>,
+): Promise<void>;
+export function alert(
+  message: string,
+  title?: string,
+  options?: Partial<AlertProps>,
+): Promise<void>;
+
+/**
+ * 弹出输入框的函数签名。
+ * beforeClose的参数会传入用户当前输入的值
+ * component指定接受用户输入的组件,默认为Input
+ * componentProps 为输入组件设置的属性数据
+ * defaultValue 默认的值
+ * modelPropName 输入组件的值属性名称。默认为modelValue
+ */
+export async function prompt<T = any>(
+  options: Omit<AlertProps, 'beforeClose'> & {
+    beforeClose?: (
+      scope: BeforeCloseScope & {
+        /** 输入组件的当前值 */
+        value: T;
+      },
+    ) => boolean | Promise<boolean | undefined> | undefined;
+    component?: Component;
+    componentProps?: Recordable<any>;
+    defaultValue?: T;
+    modelPropName?: string;
+  },
+): Promise<T | undefined>;
+```

+ 36 - 0
docs/src/demos/vben-alert/alert/index.vue

@@ -0,0 +1,36 @@
+<script lang="ts" setup>
+import { h } from 'vue';
+
+import { alert, VbenButton } from '@vben/common-ui';
+
+import { Result } from 'ant-design-vue';
+
+function showAlert() {
+  alert('This is an alert message');
+}
+
+function showIconAlert() {
+  alert({
+    content: 'This is an alert message with icon',
+    icon: 'success',
+  });
+}
+
+function showCustomAlert() {
+  alert({
+    buttonAlign: 'center',
+    content: h(Result, {
+      status: 'success',
+      subTitle: '已成功创建订单。订单ID:2017182818828182881',
+      title: '操作成功',
+    }),
+  });
+}
+</script>
+<template>
+  <div class="flex gap-4">
+    <VbenButton @click="showAlert">Alert</VbenButton>
+    <VbenButton @click="showIconAlert">Alert With Icon</VbenButton>
+    <VbenButton @click="showCustomAlert">Alert With Custom Content</VbenButton>
+  </div>
+</template>

+ 75 - 0
docs/src/demos/vben-alert/confirm/index.vue

@@ -0,0 +1,75 @@
+<script lang="ts" setup>
+import { h, ref } from 'vue';
+
+import { alert, confirm, VbenButton } from '@vben/common-ui';
+
+import { Checkbox, message } from 'ant-design-vue';
+
+function showConfirm() {
+  confirm('This is an alert message')
+    .then(() => {
+      alert('Confirmed');
+    })
+    .catch(() => {
+      alert('Canceled');
+    });
+}
+
+function showIconConfirm() {
+  confirm({
+    content: 'This is an alert message with icon',
+    icon: 'success',
+  });
+}
+
+function showfooterConfirm() {
+  const checked = ref(false);
+  confirm({
+    cancelText: '不要虾扯蛋',
+    confirmText: '是的,我们都是NPC',
+    content:
+      '刚才发生的事情,为什么我似乎早就经历过一般?\n我甚至能在事情发生过程中潜意识里预知到接下来会发生什么。\n\n听起来挺玄乎的,你有过这种感觉吗?',
+    footer: () =>
+      h(
+        Checkbox,
+        {
+          checked: checked.value,
+          class: 'flex-1',
+          'onUpdate:checked': (v) => (checked.value = v),
+        },
+        '不再提示',
+      ),
+    icon: 'question',
+    title: '未解之谜',
+  }).then(() => {
+    if (checked.value) {
+      message.success('我不会再拿这个问题烦你了');
+    } else {
+      message.info('下次还要继续问你哟');
+    }
+  });
+}
+
+function showAsyncConfirm() {
+  confirm({
+    beforeClose({ isConfirm }) {
+      if (isConfirm) {
+        // 这里可以执行一些异步操作。如果最终返回了false,将阻止关闭弹窗
+        return new Promise((resolve) => setTimeout(resolve, 2000));
+      }
+    },
+    content: 'This is an alert message with async confirm',
+    icon: 'success',
+  }).then(() => {
+    alert('Confirmed');
+  });
+}
+</script>
+<template>
+  <div class="flex gap-4">
+    <VbenButton @click="showConfirm">Confirm</VbenButton>
+    <VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
+    <VbenButton @click="showfooterConfirm">Confirm With Footer</VbenButton>
+    <VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
+  </div>
+</template>

+ 118 - 0
docs/src/demos/vben-alert/prompt/index.vue

@@ -0,0 +1,118 @@
+<script lang="ts" setup>
+import { h } from 'vue';
+
+import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
+
+import { Input, RadioGroup, Select } from 'ant-design-vue';
+import { BadgeJapaneseYen } from 'lucide-vue-next';
+
+function showPrompt() {
+  prompt({
+    content: '请输入一些东西',
+  })
+    .then((val) => {
+      alert(`已收到你的输入:${val}`);
+    })
+    .catch(() => {
+      alert('Canceled');
+    });
+}
+
+function showSlotsPrompt() {
+  prompt({
+    component: () => {
+      // 获取弹窗上下文。注意:只能在setup或者函数式组件中调用
+      const { doConfirm } = useAlertContext();
+      return h(
+        Input,
+        {
+          onKeydown(e: KeyboardEvent) {
+            if (e.key === 'Enter') {
+              e.preventDefault();
+              // 调用弹窗提供的确认方法
+              doConfirm();
+            }
+          },
+          placeholder: '请输入',
+          prefix: '充值金额:',
+          type: 'number',
+        },
+        {
+          addonAfter: () => h(BadgeJapaneseYen),
+        },
+      );
+    },
+    content:
+      '此弹窗演示了如何使用自定义插槽,并且可以使用useAlertContext获取到弹窗的上下文。\n在输入框中按下回车键会触发确认操作。',
+    icon: 'question',
+    modelPropName: 'value',
+  }).then((val) => {
+    if (val) alert(`你输入的是${val}`);
+  });
+}
+
+function showSelectPrompt() {
+  prompt({
+    component: Select,
+    componentProps: {
+      options: [
+        { label: 'Option A', value: 'Option A' },
+        { label: 'Option B', value: 'Option B' },
+        { label: 'Option C', value: 'Option C' },
+      ],
+      placeholder: '请选择',
+      // 弹窗会设置body的pointer-events为none,这回影响下拉框的点击事件
+      popupClassName: 'pointer-events-auto',
+    },
+    content: '此弹窗演示了如何使用component传递自定义组件',
+    icon: 'question',
+    modelPropName: 'value',
+  }).then((val) => {
+    if (val) {
+      alert(`你选择了${val}`);
+    }
+  });
+}
+
+function sleep(ms: number) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function showAsyncPrompt() {
+  prompt({
+    async beforeClose(scope) {
+      if (scope.isConfirm) {
+        if (scope.value) {
+          // 模拟异步操作,如果不成功,可以返回false
+          await sleep(2000);
+        } else {
+          alert('请选择一个选项');
+          return false;
+        }
+      }
+    },
+    component: RadioGroup,
+    componentProps: {
+      class: 'flex flex-col',
+      options: [
+        { label: 'Option 1', value: 'option1' },
+        { label: 'Option 2', value: 'option2' },
+        { label: 'Option 3', value: 'option3' },
+      ],
+    },
+    content: '选择一个选项后再点击[确认]',
+    icon: 'question',
+    modelPropName: 'value',
+  }).then((val) => {
+    alert(`${val} 已设置。`);
+  });
+}
+</script>
+<template>
+  <div class="flex gap-4">
+    <VbenButton @click="showPrompt">Prompt</VbenButton>
+    <VbenButton @click="showSlotsPrompt"> Prompt With slots </VbenButton>
+    <VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton>
+    <VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
+  </div>
+</template>

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

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

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

@@ -70,7 +70,7 @@ export async function perfectionist(): Promise<Linter.Config[]> {
           },
         ],
         'perfectionist/sort-objects': [
-          'error',
+          'off',
           {
             customGroups: {
               items: 'items',

+ 10 - 6
internal/lint-configs/eslint-config/src/configs/vue.ts

@@ -10,7 +10,15 @@ export async function vue(): Promise<Linter.Config[]> {
     interopDefault(import('@typescript-eslint/parser')),
   ] as const);
 
+  const flatEssential = pluginVue.configs?.['flat/essential'] || [];
+  const flatStronglyRecommended =
+    pluginVue.configs?.['flat/strongly-recommended'] || [];
+  const flatRecommended = pluginVue.configs?.['flat/recommended'] || [];
+
   return [
+    ...flatEssential,
+    ...flatStronglyRecommended,
+    ...flatRecommended,
     {
       files: ['**/*.vue'],
       languageOptions: {
@@ -43,12 +51,9 @@ export async function vue(): Promise<Linter.Config[]> {
       plugins: {
         vue: pluginVue,
       },
-      processor: pluginVue.processors['.vue'],
+      processor: pluginVue.processors?.['.vue'],
       rules: {
-        ...pluginVue.configs.base.rules,
-        ...pluginVue.configs['vue3-essential'].rules,
-        ...pluginVue.configs['vue3-strongly-recommended'].rules,
-        ...pluginVue.configs['vue3-recommended'].rules,
+        ...pluginVue.configs?.base?.rules,
 
         'vue/attribute-hyphenation': [
           'error',
@@ -131,7 +136,6 @@ export async function vue(): Promise<Linter.Config[]> {
         'vue/require-default-prop': 'error',
         'vue/require-explicit-emits': 'error',
         'vue/require-prop-types': 'off',
-        'vue/script-setup-uses-vars': 'error',
         'vue/singleline-html-element-content-newline': 'off',
         'vue/space-infix-ops': 'error',
         'vue/space-unary-ops': ['error', { nonwords: false, words: true }],

+ 7 - 0
internal/lint-configs/eslint-config/src/custom-config.ts

@@ -29,6 +29,13 @@ const customConfig: Linter.Config[] = [
     },
   },
   {
+    files: ['**/**.vue'],
+    ignores: restrictedImportIgnores,
+    rules: {
+      'perfectionist/sort-objects': 'off',
+    },
+  },
+  {
     // apps内部的一些基础规则
     files: ['apps/**/**'],
     ignores: restrictedImportIgnores,

+ 1 - 0
internal/lint-configs/stylelint-config/index.mjs

@@ -43,6 +43,7 @@ export default {
     'stylelint-scss',
   ],
   rules: {
+    'at-rule-no-deprecated': null,
     'at-rule-no-unknown': [
       true,
       {

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

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

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

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

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

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

+ 1 - 1
internal/tsconfig/package.json

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

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

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

+ 218 - 39
internal/vite-config/src/typing.ts

@@ -3,149 +3,328 @@ import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
 import type { PluginOptions } from 'vite-plugin-dts';
 import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
 
+/**
+ * ImportMap 配置接口
+ * @description 用于配置模块导入映射,支持自定义导入路径和范围
+ * @example
+ * ```typescript
+ * {
+ *   imports: {
+ *     'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
+ *   },
+ *   scopes: {
+ *     'https://site.com/': {
+ *       'vue': 'https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.js'
+ *     }
+ *   }
+ * }
+ * ```
+ */
 interface IImportMap {
+  /** 模块导入映射 */
   imports?: Record<string, string>;
+  /** 作用域特定的导入映射 */
   scopes?: {
     [scope: string]: Record<string, string>;
   };
 }
+
+/**
+ * 打印插件配置选项
+ * @description 用于配置控制台打印信息
+ */
 interface PrintPluginOptions {
   /**
-   * 打印的数据
+   * 打印的数据映射
+   * @description 键值对形式的数据,将在控制台打印
+   * @example
+   * ```typescript
+   * {
+   *   'App Version': '1.0.0',
+   *   'Build Time': '2024-01-01'
+   * }
+   * ```
    */
   infoMap?: Record<string, string | undefined>;
 }
 
+/**
+ * Nitro Mock 插件配置选项
+ * @description 用于配置 Nitro Mock 服务器的行为
+ */
 interface NitroMockPluginOptions {
   /**
-   * mock server 包名
+   * Mock 服务器包名
+   * @default '@vbenjs/nitro-mock'
    */
   mockServerPackage?: string;
 
   /**
-   * mock 服务端口
+   * Mock 服务端口
+   * @default 3000
    */
   port?: number;
 
   /**
-   * mock 日志是否打印
+   * 是否打印 Mock 日志
+   * @default false
    */
   verbose?: boolean;
 }
 
+/**
+ * 归档插件配置选项
+ * @description 用于配置构建产物的压缩归档
+ */
 interface ArchiverPluginOptions {
   /**
    * 输出文件名
-   * @default dist
+   * @default 'dist'
    */
   name?: string;
   /**
    * 输出目录
-   * @default .
+   * @default '.'
    */
   outputDir?: string;
 }
 
 /**
- * importmap 插件配置
+ * ImportMap 插件配置
+ * @description 用于配置模块的 CDN 导入
  */
 interface ImportmapPluginOptions {
   /**
    * CDN 供应商
-   * @default jspm.io
+   * @default 'jspm.io'
+   * @description 支持 esm.sh 和 jspm.io 两种 CDN 供应商
    */
   defaultProvider?: 'esm.sh' | 'jspm.io';
-  /** importmap 配置 */
+  /**
+   * ImportMap 配置数组
+   * @description 配置需要从 CDN 导入的包
+   * @example
+   * ```typescript
+   * [
+   *   { name: 'vue' },
+   *   { name: 'pinia', range: '^2.0.0' }
+   * ]
+   * ```
+   */
   importmap?: Array<{ name: string; range?: string }>;
-  /** 手动配置importmap */
+  /**
+   * 手动配置 ImportMap
+   * @description 自定义 ImportMap 配置
+   */
   inputMap?: IImportMap;
 }
 
 /**
- * 用于判断是否需要加载插件
+ * 条件插件配置
+ * @description 用于根据条件动态加载插件
  */
 interface ConditionPlugin {
-  // 判断条件
+  /**
+   * 判断条件
+   * @description 当条件为 true 时加载插件
+   */
   condition?: boolean;
-  // 插件对象
+  /**
+   * 插件对象
+   * @description 返回插件数组或 Promise
+   */
   plugins: () => PluginOption[] | PromiseLike<PluginOption[]>;
 }
 
+/**
+ * 通用插件配置选项
+ * @description 所有插件共用的基础配置
+ */
 interface CommonPluginOptions {
-  /** 是否开启devtools */
+  /**
+   * 是否开启开发工具
+   * @default false
+   */
   devtools?: boolean;
-  /** 环境变量 */
+  /**
+   * 环境变量
+   * @description 自定义环境变量
+   */
   env?: Record<string, any>;
-  /** 是否注入metadata */
+  /**
+   * 是否注入元数据
+   * @default true
+   */
   injectMetadata?: boolean;
-  /** 是否构建模式 */
+  /**
+   * 是否为构建模式
+   * @default false
+   */
   isBuild?: boolean;
-  /** 构建模式 */
+  /**
+   * 构建模式
+   * @default 'development'
+   */
   mode?: string;
-  /** 开启依赖分析 */
+  /**
+   * 是否开启依赖分析
+   * @default false
+   * @description 使用 rollup-plugin-visualizer 分析依赖
+   */
   visualizer?: boolean | PluginVisualizerOptions;
 }
 
+/**
+ * 应用插件配置选项
+ * @description 用于配置应用构建时的插件选项
+ */
 interface ApplicationPluginOptions extends CommonPluginOptions {
-  /** 开启后,会在打包dist同级生成dist.zip */
+  /**
+   * 是否开启压缩归档
+   * @default false
+   * @description 开启后会在打包目录生成 zip 文件
+   */
   archiver?: boolean;
-  /** 压缩归档插件配置 */
+  /**
+   * 压缩归档插件配置
+   * @description 配置压缩归档的行为
+   */
   archiverPluginOptions?: ArchiverPluginOptions;
-  /** 开启 gzip|brotli 压缩 */
+  /**
+   * 是否开启压缩
+   * @default false
+   * @description 支持 gzip 和 brotli 压缩
+   */
   compress?: boolean;
-  /** 压缩类型 */
+  /**
+   * 压缩类型
+   * @default ['gzip']
+   * @description 可选的压缩类型
+   */
   compressTypes?: ('brotli' | 'gzip')[];
-  /** 在构建的时候抽离配置文件 */
+  /**
+   * 是否抽离配置文件
+   * @default false
+   * @description 在构建时抽离配置文件
+   */
   extraAppConfig?: boolean;
-  /** 是否开启html插件  */
+  /**
+   * 是否开启 HTML 插件
+   * @default true
+   */
   html?: boolean;
-  /** 是否开启i18n */
+  /**
+   * 是否开启国际化
+   * @default false
+   */
   i18n?: boolean;
-  /** 是否开启 importmap CDN  */
+  /**
+   * 是否开启 ImportMap CDN
+   * @default false
+   */
   importmap?: boolean;
-  /** importmap 插件配置 */
+  /**
+   * ImportMap 插件配置
+   */
   importmapOptions?: ImportmapPluginOptions;
-  /** 是否注入app loading */
+  /**
+   * 是否注入应用加载动画
+   * @default true
+   */
   injectAppLoading?: boolean;
-  /** 是否注入全局scss */
+  /**
+   * 是否注入全局 SCSS
+   * @default true
+   */
   injectGlobalScss?: boolean;
-  /** 是否注入版权信息 */
+  /**
+   * 是否注入版权信息
+   * @default true
+   */
   license?: boolean;
-  /** 是否开启nitro mock */
+  /**
+   * 是否开启 Nitro Mock
+   * @default false
+   */
   nitroMock?: boolean;
-  /** nitro mock 插件配置 */
+  /**
+   * Nitro Mock 插件配置
+   */
   nitroMockOptions?: NitroMockPluginOptions;
-  /** 开启控制台自定义打印 */
+  /**
+   * 是否开启控制台打印
+   * @default false
+   */
   print?: boolean;
-  /** 打印插件配置 */
+  /**
+   * 打印插件配置
+   */
   printInfoMap?: PrintPluginOptions['infoMap'];
-  /** 是否开启pwa */
+  /**
+   * 是否开启 PWA
+   * @default false
+   */
   pwa?: boolean;
-  /** pwa 插件配置 */
+  /**
+   * PWA 插件配置
+   */
   pwaOptions?: Partial<PwaPluginOptions>;
-  /** 是否开启vxe-table懒加载 */
+  /**
+   * 是否开启 VXE Table 懒加载
+   * @default false
+   */
   vxeTableLazyImport?: boolean;
 }
 
+/**
+ * 库插件配置选项
+ * @description 用于配置库构建时的插件选项
+ */
 interface LibraryPluginOptions extends CommonPluginOptions {
-  /** 开启 dts 输出 */
+  /**
+   * 是否开启 DTS 输出
+   * @default true
+   * @description 生成 TypeScript 类型声明文件
+   */
   dts?: boolean | PluginOptions;
 }
 
+/**
+ * 应用配置选项类型
+ */
 type ApplicationOptions = ApplicationPluginOptions;
 
+/**
+ * 库配置选项类型
+ */
 type LibraryOptions = LibraryPluginOptions;
 
+/**
+ * 应用配置定义函数类型
+ * @description 用于定义应用构建配置
+ */
 type DefineApplicationOptions = (config?: ConfigEnv) => Promise<{
+  /** 应用插件配置 */
   application?: ApplicationOptions;
+  /** Vite 配置 */
   vite?: UserConfig;
 }>;
 
+/**
+ * 库配置定义函数类型
+ * @description 用于定义库构建配置
+ */
 type DefineLibraryOptions = (config?: ConfigEnv) => Promise<{
+  /** 库插件配置 */
   library?: LibraryOptions;
+  /** Vite 配置 */
   vite?: UserConfig;
 }>;
 
+/**
+ * 配置定义类型
+ * @description 应用或库的配置定义
+ */
 type DefineConfig = DefineApplicationOptions | DefineLibraryOptions;
 
 export type {

+ 76 - 0
lefthook.yml

@@ -0,0 +1,76 @@
+# EXAMPLE USAGE:
+#
+#   Refer for explanation to following link:
+#   https://lefthook.dev/configuration/
+#
+# pre-push:
+#   jobs:
+#     - name: packages audit
+#       tags:
+#         - frontend
+#         - security
+#       run: yarn audit
+#
+#     - name: gems audit
+#       tags:
+#         - backend
+#         - security
+#       run: bundle audit
+#
+# pre-commit:
+#   parallel: true
+#   jobs:
+#     - run: yarn eslint {staged_files}
+#       glob: "*.{js,ts,jsx,tsx}"
+#
+#     - name: rubocop
+#       glob: "*.rb"
+#       exclude:
+#         - config/application.rb
+#         - config/routes.rb
+#       run: bundle exec rubocop --force-exclusion {all_files}
+#
+#     - name: govet
+#       files: git ls-files -m
+#       glob: "*.go"
+#       run: go vet {files}
+#
+#     - script: "hello.js"
+#       runner: node
+#
+#     - script: "hello.go"
+#       runner: go run
+
+pre-commit:
+  parallel: true
+  commands:
+    code-workspace:
+      run: pnpm vsh code-workspace --auto-commit
+    lint-md:
+      run: pnpm prettier --cache --ignore-unknown --write {staged_files}
+      glob: '*.md'
+    lint-vue:
+      run: pnpm prettier --write {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
+      glob: '*.vue'
+    lint-js:
+      run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm eslint --cache --fix {staged_files}
+      glob: '*.{js,jsx,ts,tsx}'
+    lint-style:
+      run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
+      glob: '*.{scss,less,styl,html,vue,css}'
+    lint-package:
+      run: pnpm prettier --cache --write {staged_files}
+      glob: 'package.json'
+    lint-json:
+      run: pnpm prettier --cache --write --parser json {staged_files}
+      glob: '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}'
+
+post-merge:
+  commands:
+    install:
+      run: pnpm install
+
+commit-msg:
+  commands:
+    commitlint:
+      run: pnpm exec commitlint --edit $1

+ 6 - 8
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vben-admin-monorepo",
-  "version": "5.5.4",
+  "version": "5.5.6",
   "private": true,
   "keywords": [
     "monorepo",
@@ -43,14 +43,14 @@
     "lint": "vsh lint",
     "postinstall": "pnpm -r run stub --if-present",
     "preinstall": "npx only-allow pnpm",
-    "prepare": "is-ci || husky",
     "preview": "turbo-run preview",
     "publint": "vsh publint",
     "reinstall": "pnpm clean --del-lock && pnpm install",
     "test:unit": "vitest run --dom",
     "test:e2e": "turbo run test:e2e",
     "update:deps": "npx taze -r -w",
-    "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
+    "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile",
+    "catalog": "pnpx codemod pnpm/catalog"
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
@@ -77,9 +77,8 @@
     "cross-env": "catalog:",
     "cspell": "catalog:",
     "happy-dom": "catalog:",
-    "husky": "catalog:",
     "is-ci": "catalog:",
-    "lint-staged": "catalog:",
+    "lefthook": "catalog:",
     "playwright": "catalog:",
     "rimraf": "catalog:",
     "tailwindcss": "catalog:",
@@ -96,7 +95,7 @@
     "node": ">=20.10.0",
     "pnpm": ">=9.12.0"
   },
-  "packageManager": "pnpm@9.15.7",
+  "packageManager": "pnpm@10.10.0",
   "pnpm": {
     "peerDependencyRules": {
       "allowedVersions": {
@@ -108,8 +107,7 @@
       "@cspell/url": "8.17.2",
       "@ctrl/tinycolor": "catalog:",
       "clsx": "catalog:",
-      "cspell": "8.17.2",
-      "esbuild": "0.24.0",
+      "esbuild": "0.25.3",
       "pinia": "catalog:",
       "vue": "catalog:"
     },

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

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

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

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

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

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

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

@@ -15,8 +15,10 @@ export {
   ChevronsLeft,
   ChevronsRight,
   Circle,
+  CircleAlert,
   CircleCheckBig,
   CircleHelp,
+  CircleX,
   Copy,
   CornerDownLeft,
   Ellipsis,

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/shared",
-  "version": "5.5.4",
+  "version": "5.5.6",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {
@@ -88,6 +88,7 @@
     "lodash.clonedeep": "catalog:",
     "lodash.get": "catalog:",
     "lodash.isequal": "catalog:",
+    "lodash.set": "catalog:",
     "nprogress": "catalog:",
     "tailwind-merge": "catalog:",
     "theme-colors": "catalog:"
@@ -96,6 +97,7 @@
     "@types/lodash.clonedeep": "catalog:",
     "@types/lodash.get": "catalog:",
     "@types/lodash.isequal": "catalog:",
+    "@types/lodash.set": "catalog:",
     "@types/nprogress": "catalog:"
   }
 }

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

@@ -17,3 +17,4 @@ export * from './window';
 export { default as cloneDeep } from 'lodash.clonedeep';
 export { default as get } from 'lodash.get';
 export { default as isEqual } from 'lodash.isequal';
+export { default as set } from 'lodash.set';

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

@@ -1,3 +1,4 @@
+// eslint-disable-next-line vue/prefer-import-from-vue
 import { isFunction, isObject, isString } from '@vue/shared';
 
 /**

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

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

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

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

+ 2 - 0
packages/@core/composables/src/use-simple-locale/messages.ts

@@ -6,6 +6,7 @@ export const messages: Record<Locale, Record<string, string>> = {
     collapse: 'Collapse',
     confirm: 'Confirm',
     expand: 'Expand',
+    prompt: 'Prompt',
     reset: 'Reset',
     submit: 'Submit',
   },
@@ -14,6 +15,7 @@ export const messages: Record<Locale, Record<string, string>> = {
     collapse: '收起',
     confirm: '确认',
     expand: '展开',
+    prompt: '提示',
     reset: '重置',
     submit: '提交',
   },

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

@@ -11,6 +11,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "compact": false,
     "contentCompact": "wide",
     "defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
+    "defaultHomePath": "/analytics",
     "dynamicTitle": true,
     "enableCheckUpdates": true,
     "enablePreferences": true,
@@ -68,10 +69,12 @@ exports[`defaultPreferences immutability test > should not modify the config obj
   "sidebar": {
     "autoActivateChild": false,
     "collapsed": false,
+    "collapsedButton": true,
     "collapsedShowTitle": false,
     "enable": true,
     "expandOnHover": true,
     "extraCollapse": false,
+    "fixedButton": true,
     "hidden": false,
     "width": 224,
   },

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

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

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

@@ -11,6 +11,7 @@ const defaultPreferences: Preferences = {
     contentCompact: 'wide',
     defaultAvatar:
       'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
+    defaultHomePath: '/analytics',
     dynamicTitle: true,
     enableCheckUpdates: true,
     enablePreferences: true,
@@ -68,10 +69,12 @@ const defaultPreferences: Preferences = {
   sidebar: {
     autoActivateChild: false,
     collapsed: false,
+    collapsedButton: true,
     collapsedShowTitle: false,
     enable: true,
     expandOnHover: true,
     extraCollapse: false,
+    fixedButton: true,
     hidden: false,
     width: 250,
   },

+ 10 - 3
packages/@core/preferences/src/preferences.ts

@@ -198,9 +198,16 @@ class PreferenceManager {
     window
       .matchMedia('(prefers-color-scheme: dark)')
       .addEventListener('change', ({ matches: isDark }) => {
-        this.updatePreferences({
-          theme: { mode: isDark ? 'dark' : 'light' },
-        });
+        // 如果偏好设置中主题模式为auto,则跟随系统更新
+        if (this.state.theme.mode === 'auto') {
+          this.updatePreferences({
+            theme: { mode: isDark ? 'dark' : 'light' },
+          });
+          // 恢复为auto模式
+          this.updatePreferences({
+            theme: { mode: 'auto' },
+          });
+        }
       });
   }
 

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

@@ -35,6 +35,8 @@ interface AppPreferences {
   contentCompact: ContentCompactType;
   // /** 应用默认头像 */
   defaultAvatar: string;
+  /** 默认首页地址 */
+  defaultHomePath: string;
   // /** 开启动态标题 */
   dynamicTitle: boolean;
   /** 是否开启检查更新 */
@@ -132,6 +134,8 @@ interface SidebarPreferences {
   autoActivateChild: boolean;
   /** 侧边栏是否折叠 */
   collapsed: boolean;
+  /** 侧边栏折叠按钮是否可见 */
+  collapsedButton: boolean;
   /** 侧边栏折叠时,是否显示title */
   collapsedShowTitle: boolean;
   /** 侧边栏是否可见 */
@@ -140,6 +144,8 @@ interface SidebarPreferences {
   expandOnHover: boolean;
   /** 侧边栏扩展区域是否折叠 */
   extraCollapse: boolean;
+  /** 侧边栏固定按钮是否可见 */
+  fixedButton: boolean;
   /** 侧边栏是否隐藏 - css */
   hidden: boolean;
   /** 侧边栏宽度 */

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/form-ui",
-  "version": "5.5.4",
+  "version": "5.5.6",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {
@@ -41,6 +41,7 @@
   },
   "dependencies": {
     "@vben-core/composables": "workspace:*",
+    "@vben-core/icons": "workspace:*",
     "@vben-core/shadcn-ui": "workspace:*",
     "@vben-core/shared": "workspace:*",
     "@vben-core/typings": "workspace:*",

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

@@ -62,7 +62,7 @@ async function handleReset(e: Event) {
   e?.stopPropagation();
   const props = unref(rootProps);
 
-  const values = toRaw(props.formApi?.getValues());
+  const values = toRaw(await props.formApi?.getValues());
 
   if (isFunction(props.handleReset)) {
     await props.handleReset?.(values);

+ 119 - 0
packages/@core/ui-kit/form-ui/src/form-api.ts

@@ -295,6 +295,7 @@ export class FormApi {
       return true;
     });
     const filteredFields = fieldMergeFn(fields, form.values);
+    this.handleStringToArrayFields(filteredFields);
     form.setValues(filteredFields, shouldValidate);
   }
 
@@ -304,6 +305,7 @@ export class FormApi {
     const form = await this.getForm();
     await form.submitForm();
     const rawValues = toRaw(await this.getValues());
+    this.handleArrayToStringFields(rawValues);
     await this.state?.handleSubmit?.(rawValues);
 
     return rawValues;
@@ -392,10 +394,53 @@ export class FormApi {
     return this.form;
   }
 
+  private handleArrayToStringFields = (originValues: Record<string, any>) => {
+    const arrayToStringFields = this.state?.arrayToStringFields;
+    if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
+      return;
+    }
+
+    const processFields = (fields: string[], separator: string = ',') => {
+      this.processFields(fields, separator, originValues, (value, sep) =>
+        Array.isArray(value) ? value.join(sep) : value,
+      );
+    };
+
+    // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
+    if (arrayToStringFields.every((item) => typeof item === 'string')) {
+      const lastItem =
+        arrayToStringFields[arrayToStringFields.length - 1] || '';
+      const fields =
+        lastItem.length === 1
+          ? arrayToStringFields.slice(0, -1)
+          : arrayToStringFields;
+      const separator = lastItem.length === 1 ? lastItem : ',';
+      processFields(fields, separator);
+      return;
+    }
+
+    // 处理嵌套数组格式 [['field1'], ';']
+    arrayToStringFields.forEach((fieldConfig) => {
+      if (Array.isArray(fieldConfig)) {
+        const [fields, separator = ','] = fieldConfig;
+        // 根据类型定义,fields 应该始终是字符串数组
+        if (!Array.isArray(fields)) {
+          console.warn(
+            `Invalid field configuration: fields should be an array of strings, got ${typeof fields}`,
+          );
+          return;
+        }
+        processFields(fields, separator);
+      }
+    });
+  };
+
   private handleRangeTimeValue = (originValues: Record<string, any>) => {
     const values = { ...originValues };
     const fieldMappingTime = this.state?.fieldMappingTime;
 
+    this.handleStringToArrayFields(values);
+
     if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
       return values;
     }
@@ -441,6 +486,80 @@ export class FormApi {
     return values;
   };
 
+  private handleStringToArrayFields = (originValues: Record<string, any>) => {
+    const arrayToStringFields = this.state?.arrayToStringFields;
+    if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
+      return;
+    }
+
+    const processFields = (fields: string[], separator: string = ',') => {
+      this.processFields(fields, separator, originValues, (value, sep) => {
+        if (typeof value !== 'string') {
+          return value;
+        }
+        // 处理空字符串的情况
+        if (value === '') {
+          return [];
+        }
+        // 处理复杂分隔符的情况
+        const escapedSeparator = sep.replaceAll(
+          /[.*+?^${}()|[\]\\]/g,
+          String.raw`\$&`,
+        );
+        return value.split(new RegExp(escapedSeparator));
+      });
+    };
+
+    // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
+    if (arrayToStringFields.every((item) => typeof item === 'string')) {
+      const lastItem =
+        arrayToStringFields[arrayToStringFields.length - 1] || '';
+      const fields =
+        lastItem.length === 1
+          ? arrayToStringFields.slice(0, -1)
+          : arrayToStringFields;
+      const separator = lastItem.length === 1 ? lastItem : ',';
+      processFields(fields, separator);
+      return;
+    }
+
+    // 处理嵌套数组格式 [['field1'], ';']
+    arrayToStringFields.forEach((fieldConfig) => {
+      if (Array.isArray(fieldConfig)) {
+        const [fields, separator = ','] = fieldConfig;
+        if (Array.isArray(fields)) {
+          processFields(fields, separator);
+        } else if (typeof originValues[fields] === 'string') {
+          const value = originValues[fields];
+          if (value === '') {
+            originValues[fields] = [];
+          } else {
+            const escapedSeparator = separator.replaceAll(
+              /[.*+?^${}()|[\]\\]/g,
+              String.raw`\$&`,
+            );
+            originValues[fields] = value.split(new RegExp(escapedSeparator));
+          }
+        }
+      }
+    });
+  };
+
+  private processFields = (
+    fields: string[],
+    separator: string,
+    originValues: Record<string, any>,
+    transformFn: (value: any, separator: string) => any,
+  ) => {
+    fields.forEach((field) => {
+      const value = originValues[field];
+      if (value === undefined || value === null) {
+        return;
+      }
+      originValues[field] = transformFn(value, separator);
+    });
+  };
+
   private updateState() {
     const currentSchema = this.state?.schema ?? [];
     const prevSchema = this.prevState?.schema ?? [];

+ 7 - 1
packages/@core/ui-kit/form-ui/src/form-render/expandable.ts

@@ -2,13 +2,18 @@ import type { FormRenderProps } from '../types';
 
 import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
 
-import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
+import {
+  breakpointsTailwind,
+  useBreakpoints,
+  useElementVisibility,
+} from '@vueuse/core';
 
 /**
  * 动态计算行数
  */
 export function useExpandable(props: FormRenderProps) {
   const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef');
+  const isVisible = useElementVisibility(wrapperRef);
   const rowMapping = ref<Record<number, number>>({});
   // 是否已经计算过一次
   const isCalculated = ref(false);
@@ -31,6 +36,7 @@ export function useExpandable(props: FormRenderProps) {
       () => props.showCollapseButton,
       () => breakpoints.active().value,
       () => props.schema?.length,
+      () => isVisible.value,
     ],
     async ([val]) => {
       if (val) {

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

@@ -5,6 +5,7 @@ import type { FormSchema, MaybeComponentProps } from '../types';
 
 import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
 
+import { CircleAlert } from '@vben-core/icons';
 import {
   FormControl,
   FormDescription,
@@ -12,6 +13,7 @@ import {
   FormItem,
   FormMessage,
   VbenRenderContent,
+  VbenTooltip,
 } from '@vben-core/shadcn-ui';
 import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
 
@@ -290,6 +292,7 @@ onUnmounted(() => {
       v-show="isShow"
       :class="{
         'form-valid-error': isInValid,
+        'form-is-required': shouldRequired,
         'flex-col': isVertical,
         'flex-row items-center': !isVertical,
         'pb-6': !compact,
@@ -320,7 +323,7 @@ onUnmounted(() => {
           <VbenRenderContent :content="label" />
         </template>
       </FormLabel>
-      <div class="flex-auto overflow-hidden">
+      <div class="flex-auto overflow-hidden p-[1px]">
         <div :class="cn('relative flex w-full items-center', wrapperClass)">
           <FormControl :class="cn(controlClass)">
             <slot
@@ -353,6 +356,24 @@ onUnmounted(() => {
                 </template>
                 <!-- <slot></slot> -->
               </component>
+              <VbenTooltip
+                v-if="compact && isInValid"
+                :delay-duration="300"
+                side="left"
+              >
+                <template #trigger>
+                  <slot name="trigger">
+                    <CircleAlert
+                      :class="
+                        cn(
+                          'text-foreground/80 hover:text-foreground inline-flex size-5 cursor-pointer',
+                        )
+                      "
+                    />
+                  </slot>
+                </template>
+                <FormMessage />
+              </VbenTooltip>
             </slot>
           </FormControl>
           <!-- 自定义后缀 -->
@@ -364,7 +385,7 @@ onUnmounted(() => {
           </FormDescription>
         </div>
 
-        <Transition name="slide-up">
+        <Transition name="slide-up" v-if="!compact">
           <FormMessage class="absolute bottom-1" />
         </Transition>
       </div>

+ 29 - 1
packages/@core/ui-kit/form-ui/src/types.ts

@@ -232,6 +232,12 @@ export type FieldMappingTime = [
   )?,
 ][];
 
+export type ArrayToStringFields = Array<
+  | [string[], string?] // 嵌套数组格式,可选分隔符
+  | string // 单个字段,使用默认分隔符
+  | string[] // 简单数组格式,最后一个元素可以是分隔符
+>;
+
 export interface FormSchema<
   T extends BaseFormComponentType = BaseFormComponentType,
 > extends FormCommonConfig {
@@ -267,6 +273,10 @@ export interface FormRenderProps<
   T extends BaseFormComponentType = BaseFormComponentType,
 > {
   /**
+   * 表单字段数组映射字符串配置 默认使用","
+   */
+  arrayToStringFields?: ArrayToStringFields;
+  /**
    * 是否展开,在showCollapseButton=true下生效
    */
   collapsed?: boolean;
@@ -297,6 +307,10 @@ export interface FormRenderProps<
    */
   componentMap: Record<BaseFormComponentType, Component>;
   /**
+   * 表单字段映射到时间格式
+   */
+  fieldMappingTime?: FieldMappingTime;
+  /**
    * 表单实例
    */
   form?: FormContext<GenericObject>;
@@ -308,11 +322,16 @@ export interface FormRenderProps<
    * 表单定义
    */
   schema?: FormSchema<T>[];
+
   /**
    * 是否显示展开/折叠
    */
   showCollapseButton?: boolean;
   /**
+   * 格式化日期
+   */
+
+  /**
    * 表单栅格布局
    * @default "grid-cols-1"
    */
@@ -340,6 +359,11 @@ export interface VbenFormProps<
    */
   actionWrapperClass?: ClassType;
   /**
+   * 表单字段数组映射字符串配置 默认使用","
+   */
+  arrayToStringFields?: ArrayToStringFields;
+
+  /**
    * 表单字段映射
    */
   fieldMappingTime?: FieldMappingTime;
@@ -354,11 +378,15 @@ export interface VbenFormProps<
   /**
    * 表单值变化回调
    */
-  handleValuesChange?: (values: Record<string, any>) => void;
+  handleValuesChange?: (
+    values: Record<string, any>,
+    fieldsChanged: string[],
+  ) => void;
   /**
    * 重置按钮参数
    */
   resetButtonOptions?: ActionButtonOptions;
+
   /**
    * 是否显示默认操作按钮
    * @default true

+ 7 - 3
packages/@core/ui-kit/form-ui/src/use-form-context.ts

@@ -7,7 +7,7 @@ import type { ExtendedFormApi, FormActions, VbenFormProps } from './types';
 import { computed, unref, useSlots } from 'vue';
 
 import { createContext } from '@vben-core/shadcn-ui';
-import { isString } from '@vben-core/shared/utils';
+import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils';
 
 import { useForm } from 'vee-validate';
 import { object } from 'zod';
@@ -50,7 +50,7 @@ export function useFormInitial(
     const zodObject: ZodRawShape = {};
     (unref(props).schema || []).forEach((item) => {
       if (Reflect.has(item, 'defaultValue')) {
-        initialValues[item.fieldName] = item.defaultValue;
+        set(initialValues, item.fieldName, item.defaultValue);
       } else if (item.rules && !isString(item.rules)) {
         zodObject[item.fieldName] = item.rules;
       }
@@ -58,7 +58,11 @@ export function useFormInitial(
 
     const schemaInitialValues = getDefaultsForSchema(object(zodObject));
 
-    return { ...initialValues, ...schemaInitialValues };
+    const zodDefaults: Record<string, any> = {};
+    for (const key in schemaInitialValues) {
+      set(zodDefaults, key, schemaInitialValues[key]);
+    }
+    return mergeWithArrayOverride(initialValues, zodDefaults);
   }
 
   return {

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

@@ -31,8 +31,8 @@ export function useVbenForm<
         h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
     },
     {
-      inheritAttrs: false,
       name: 'VbenUseForm',
+      inheritAttrs: false,
     },
   );
   // Add reactivity support

+ 37 - 6
packages/@core/ui-kit/form-ui/src/vben-use-form.vue

@@ -1,12 +1,13 @@
 <script setup lang="ts">
+import type { Recordable } from '@vben-core/typings';
+
 import type { ExtendedFormApi, VbenFormProps } from './types';
 
 // import { toRaw, watch } from 'vue';
 import { nextTick, onMounted, watch } from 'vue';
-// import { isFunction } from '@vben-core/shared/utils';
 
 import { useForwardPriorityValues } from '@vben-core/composables';
-import { cloneDeep } from '@vben-core/shared/utils';
+import { cloneDeep, get, isEqual, set } from '@vben-core/shared/utils';
 
 import { useDebounceFn } from '@vueuse/core';
 
@@ -61,16 +62,46 @@ function handleKeyDownEnter(event: KeyboardEvent) {
 }
 
 const handleValuesChangeDebounced = useDebounceFn(async () => {
-  forward.value.handleValuesChange?.(
-    cloneDeep(await forward.value.formApi.getValues()),
-  );
   state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
 }, 300);
 
+const valuesCache: Recordable<any> = {};
+
 onMounted(async () => {
   // 只在挂载后开始监听,form.values会有一个初始化的过程
   await nextTick();
-  watch(() => form.values, handleValuesChangeDebounced, { deep: true });
+  watch(
+    () => form.values,
+    async (newVal) => {
+      if (forward.value.handleValuesChange) {
+        const fields = state.value.schema?.map((item) => {
+          return item.fieldName;
+        });
+
+        if (fields && fields.length > 0) {
+          const changedFields: string[] = [];
+          fields.forEach((field) => {
+            const newFieldValue = get(newVal, field);
+            const oldFieldValue = get(valuesCache, field);
+            if (!isEqual(newFieldValue, oldFieldValue)) {
+              changedFields.push(field);
+              set(valuesCache, field, newFieldValue);
+            }
+          });
+
+          if (changedFields.length > 0) {
+            // 调用handleValuesChange回调,传入所有表单值的深拷贝和变更的字段列表
+            forward.value.handleValuesChange(
+              cloneDeep(await forward.value.formApi.getValues()),
+              changedFields,
+            );
+          }
+        }
+      }
+      handleValuesChangeDebounced();
+    },
+    { deep: true },
+  );
 });
 </script>
 

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

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

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

@@ -65,10 +65,15 @@ interface Props {
   show?: boolean;
   /**
    * 显示折叠按钮
-   * @default false
+   * @default true
    */
   showCollapseButton?: boolean;
   /**
+   * 显示固定按钮
+   * @default true
+   */
+  showFixedButton?: boolean;
+  /**
    * 主题
    */
   theme: string;
@@ -95,6 +100,7 @@ const props = withDefaults(defineProps<Props>(), {
   paddingTop: 0,
   show: true,
   showCollapseButton: true,
+  showFixedButton: true,
   zIndex: 0,
 });
 
@@ -267,7 +273,7 @@ function handleMouseleave() {
     @mouseleave="handleMouseleave"
   >
     <SidebarFixedButton
-      v-if="!collapse && !isSidebarMixed"
+      v-if="!collapse && !isSidebarMixed && showFixedButton"
       v-model:expand-on-hover="expandOnHover"
     />
     <div v-if="slots.logo" :style="headerStyle">

+ 10 - 0
packages/@core/ui-kit/layout-ui/src/vben-layout.ts

@@ -107,6 +107,11 @@ interface VbenLayoutProps {
    */
   sidebarCollapse?: boolean;
   /**
+   * 侧边菜单折叠按钮
+   * @default true
+   */
+  sidebarCollapsedButton?: boolean;
+  /**
    * 侧边菜单是否折叠时,是否显示title
    * @default true
    */
@@ -122,6 +127,11 @@ interface VbenLayoutProps {
    */
   sidebarExtraCollapsedWidth?: number;
   /**
+   * 侧边菜单折叠按钮是否固定
+   * @default true
+   */
+  sidebarFixedButton?: boolean;
+  /**
    * 侧边栏是否隐藏
    * @default false
    */

+ 4 - 0
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@@ -49,8 +49,10 @@ const props = withDefaults(defineProps<Props>(), {
   headerVisible: true,
   isMobile: false,
   layout: 'sidebar-nav',
+  sidebarCollapsedButton: true,
   sidebarCollapseShowTitle: false,
   sidebarExtraCollapsedWidth: 60,
+  sidebarFixedButton: true,
   sidebarHidden: false,
   sidebarMixedWidth: 80,
   sidebarTheme: 'dark',
@@ -487,6 +489,8 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
       v-model:expand-on-hovering="sidebarExpandOnHovering"
       v-model:extra-collapse="sidebarExtraCollapse"
       v-model:extra-visible="sidebarExtraVisible"
+      :show-collapse-button="sidebarCollapsedButton"
+      :show-fixed-button="sidebarFixedButton"
       :collapse-width="getSideCollapseWidth"
       :dom-visible="!isMobile"
       :extra-width="sidebarExtraWidth"

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

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

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

@@ -23,7 +23,6 @@ import {
 
 import { useNamespace } from '@vben-core/composables';
 import { Ellipsis } from '@vben-core/icons';
-import { isHttpUrl } from '@vben-core/shared/utils';
 
 import { useResizeObserver } from '@vueuse/core';
 
@@ -32,6 +31,7 @@ import {
   createSubMenuContext,
   useMenuStyle,
 } from '../hooks';
+import { useMenuScroll } from '../hooks/use-menu-scroll';
 import { flattedChildren } from '../utils';
 import SubMenu from './sub-menu.vue';
 
@@ -45,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
   mode: 'vertical',
   rounded: true,
   theme: 'dark',
+  scrollToActive: false,
 });
 
 const emit = defineEmits<{
@@ -207,15 +208,19 @@ function handleResize() {
   isFirstTimeRender = false;
 }
 
-function getActivePaths() {
-  const activeItem = activePath.value && items.value[activePath.value];
+const enableScroll = computed(
+  () => props.scrollToActive && props.mode === 'vertical' && !props.collapse,
+);
 
-  if (!activeItem || props.mode === 'horizontal' || props.collapse) {
-    return [];
-  }
+const { scrollToActiveItem } = useMenuScroll(activePath, {
+  enable: enableScroll,
+  delay: 320,
+});
 
-  return activeItem.parentPaths;
-}
+// 监听 activePath 变化,自动滚动到激活项
+watch(activePath, () => {
+  scrollToActiveItem();
+});
 
 // 默认展开菜单
 function initMenu() {
@@ -248,9 +253,6 @@ function handleMenuItemClick(data: MenuItemClicked) {
   if (!path || !parentPaths) {
     return;
   }
-  if (!isHttpUrl(path)) {
-    activePath.value = path;
-  }
 
   emit('select', path, parentPaths);
 }
@@ -322,6 +324,16 @@ function removeSubMenu(subMenu: MenuItemRegistered) {
 function removeMenuItem(item: MenuItemRegistered) {
   Reflect.deleteProperty(items.value, item.path);
 }
+
+function getActivePaths() {
+  const activeItem = activePath.value && items.value[activePath.value];
+
+  if (!activeItem || props.mode === 'horizontal' || props.collapse) {
+    return [];
+  }
+
+  return activeItem.parentPaths;
+}
 </script>
 <template>
   <ul
@@ -378,10 +390,10 @@ $namespace: vben;
     var(--menu-item-margin-x);
   font-size: var(--menu-font-size);
   color: var(--menu-item-color);
-  text-decoration: none;
   white-space: nowrap;
-  list-style: none;
+  text-decoration: none;
   cursor: pointer;
+  list-style: none;
   background: var(--menu-item-background-color);
   border: none;
   border-radius: var(--menu-item-radius);
@@ -705,8 +717,8 @@ $namespace: vben;
     width: var(--menu-item-icon-size);
     height: var(--menu-item-icon-size);
     margin-right: 8px;
-    text-align: center;
     vertical-align: middle;
+    text-align: center;
   }
 }
 

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

@@ -10,7 +10,7 @@ import { VbenIcon } from '@vben-core/shadcn-ui';
 import { useMenuContext } from '../hooks';
 
 interface Props extends MenuItemProps {
-  isMenuMore: boolean;
+  isMenuMore?: boolean;
   isTopLevelMenuSubmenu: boolean;
   level?: number;
 }

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

@@ -208,6 +208,8 @@ onBeforeUnmount(() => {
           nsMenu.e('popup-container'),
           is(rootMenu.theme, true),
           opened ? '' : 'hidden',
+          'overflow-auto',
+          'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]',
         ]"
         :content-props="contentProps"
         :open="true"

+ 46 - 0
packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts

@@ -0,0 +1,46 @@
+import type { Ref } from 'vue';
+
+import { watch } from 'vue';
+
+import { useDebounceFn } from '@vueuse/core';
+
+interface UseMenuScrollOptions {
+  delay?: number;
+  enable?: boolean | Ref<boolean>;
+}
+
+export function useMenuScroll(
+  activePath: Ref<string | undefined>,
+  options: UseMenuScrollOptions = {},
+) {
+  const { enable = true, delay = 320 } = options;
+
+  function scrollToActiveItem() {
+    const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
+    if (!isEnabled) return;
+
+    const activeElement = document.querySelector(
+      `aside li[role=menuitem].is-active`,
+    );
+    if (activeElement) {
+      activeElement.scrollIntoView({
+        behavior: 'smooth',
+        block: 'center',
+        inline: 'center',
+      });
+    }
+  }
+
+  const debouncedScroll = useDebounceFn(scrollToActiveItem, delay);
+
+  watch(activePath, () => {
+    const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
+    if (!isEnabled) return;
+
+    debouncedScroll();
+  });
+
+  return {
+    scrollToActiveItem,
+  };
+}

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

@@ -18,15 +18,9 @@ defineOptions({
 
 const props = withDefaults(defineProps<Props>(), {
   collapse: false,
-  // theme: 'dark',
 });
 
 const forward = useForwardProps(props);
-
-// const emit = defineEmits<{
-//   'update:openKeys': [key: Key[]];
-//   'update:selectedKeys': [key: Key[]];
-// }>();
 </script>
 
 <template>

+ 6 - 0
packages/@core/ui-kit/menu-ui/src/types.ts

@@ -43,6 +43,12 @@ interface MenuProps {
   rounded?: boolean;
 
   /**
+   * @zh_CN 是否自动滚动到激活的菜单项
+   * @default false
+   */
+  scrollToActive?: boolean;
+
+  /**
    * @zh_CN 菜单主题
    * @default dark
    */

+ 244 - 0
packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts

@@ -0,0 +1,244 @@
+import type { Component, VNode } from 'vue';
+
+import type { Recordable } from '@vben-core/typings';
+
+import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
+
+import { h, nextTick, ref, render } from 'vue';
+
+import { useSimpleLocale } from '@vben-core/composables';
+import { Input, VbenRenderContent } from '@vben-core/shadcn-ui';
+import { isFunction, isString } from '@vben-core/shared/utils';
+
+import Alert from './alert.vue';
+
+const alerts = ref<Array<{ container: HTMLElement; instance: Component }>>([]);
+
+const { $t } = useSimpleLocale();
+
+export function vbenAlert(options: AlertProps): Promise<void>;
+export function vbenAlert(
+  message: string,
+  options?: Partial<AlertProps>,
+): Promise<void>;
+export function vbenAlert(
+  message: string,
+  title?: string,
+  options?: Partial<AlertProps>,
+): Promise<void>;
+
+export function vbenAlert(
+  arg0: AlertProps | string,
+  arg1?: Partial<AlertProps> | string,
+  arg2?: Partial<AlertProps>,
+): Promise<void> {
+  return new Promise((resolve, reject) => {
+    const options: AlertProps = isString(arg0)
+      ? {
+          content: arg0,
+        }
+      : { ...arg0 };
+    if (arg1) {
+      if (isString(arg1)) {
+        options.title = arg1;
+      } else if (!isString(arg1)) {
+        // 如果第二个参数是对象,则合并到选项中
+        Object.assign(options, arg1);
+      }
+    }
+
+    if (arg2 && !isString(arg2)) {
+      Object.assign(options, arg2);
+    }
+    // 创建容器元素
+    const container = document.createElement('div');
+    document.body.append(container);
+
+    // 创建一个引用,用于在回调中访问实例
+    const alertRef = { container, instance: null as any };
+
+    const props: AlertProps & Recordable<any> = {
+      onClosed: (isConfirm: boolean) => {
+        // 移除组件实例以及创建的所有dom(恢复页面到打开前的状态)
+        // 从alerts数组中移除该实例
+        alerts.value = alerts.value.filter((item) => item !== alertRef);
+
+        // 从DOM中移除容器
+        render(null, container);
+        if (container.parentNode) {
+          container.remove();
+        }
+
+        // 解析 Promise,传递用户操作结果
+        if (isConfirm) {
+          resolve();
+        } else {
+          reject(new Error('dialog cancelled'));
+        }
+      },
+      ...options,
+      open: true,
+      title: options.title ?? $t.value('prompt'),
+    };
+
+    // 创建Alert组件的VNode
+    const vnode = h(Alert, props);
+
+    // 渲染组件到容器
+    render(vnode, container);
+
+    // 保存组件实例引用
+    alertRef.instance = vnode.component?.proxy as Component;
+
+    // 将实例和容器添加到alerts数组中
+    alerts.value.push(alertRef);
+  });
+}
+
+export function vbenConfirm(options: AlertProps): Promise<void>;
+export function vbenConfirm(
+  message: string,
+  options?: Partial<AlertProps>,
+): Promise<void>;
+export function vbenConfirm(
+  message: string,
+  title?: string,
+  options?: Partial<AlertProps>,
+): Promise<void>;
+
+export function vbenConfirm(
+  arg0: AlertProps | string,
+  arg1?: Partial<AlertProps> | string,
+  arg2?: Partial<AlertProps>,
+): Promise<void> {
+  const defaultProps: Partial<AlertProps> = {
+    showCancel: true,
+  };
+  if (!arg1) {
+    return isString(arg0)
+      ? vbenAlert(arg0, defaultProps)
+      : vbenAlert({ ...defaultProps, ...arg0 });
+  } else if (!arg2) {
+    return isString(arg1)
+      ? vbenAlert(arg0 as string, arg1, defaultProps)
+      : vbenAlert(arg0 as string, { ...defaultProps, ...arg1 });
+  }
+  return vbenAlert(arg0 as string, arg1 as string, {
+    ...defaultProps,
+    ...arg2,
+  });
+}
+
+export async function vbenPrompt<T = any>(
+  options: PromptProps<T>,
+): Promise<T | undefined> {
+  const {
+    component: _component,
+    componentProps: _componentProps,
+    componentSlots,
+    content,
+    defaultValue,
+    modelPropName: _modelPropName,
+    ...delegated
+  } = options;
+
+  const modelValue = ref<T | undefined>(defaultValue);
+  const inputComponentRef = ref<null | VNode>(null);
+  const staticContents: Component[] = [];
+
+  staticContents.push(h(VbenRenderContent, { content, renderBr: true }));
+
+  const modelPropName = _modelPropName || 'modelValue';
+  const componentProps = { ..._componentProps };
+
+  // 每次渲染时都会重新计算的内容函数
+  const contentRenderer = () => {
+    const currentProps = { ...componentProps };
+
+    // 设置当前值
+    currentProps[modelPropName] = modelValue.value;
+
+    // 设置更新处理函数
+    currentProps[`onUpdate:${modelPropName}`] = (val: T) => {
+      modelValue.value = val;
+    };
+
+    // 创建输入组件
+    inputComponentRef.value = h(
+      _component || Input,
+      currentProps,
+      componentSlots,
+    );
+
+    // 返回包含静态内容和输入组件的数组
+    return h(
+      'div',
+      { class: 'flex flex-col gap-2' },
+      { default: () => [...staticContents, inputComponentRef.value] },
+    );
+  };
+
+  const props: AlertProps & Recordable<any> = {
+    ...delegated,
+    async beforeClose(scope: BeforeCloseScope) {
+      if (delegated.beforeClose) {
+        return await delegated.beforeClose({
+          ...scope,
+          value: modelValue.value,
+        });
+      }
+    },
+    // 使用函数形式,每次渲染都会重新计算内容
+    content: contentRenderer,
+    contentMasking: true,
+    async onOpened() {
+      await nextTick();
+      const componentRef: null | VNode = inputComponentRef.value;
+      if (componentRef) {
+        if (
+          componentRef.component?.exposed &&
+          isFunction(componentRef.component.exposed.focus)
+        ) {
+          componentRef.component.exposed.focus();
+        } else {
+          if (componentRef.el) {
+            if (
+              isFunction(componentRef.el.focus) &&
+              ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(
+                componentRef.el.tagName,
+              )
+            ) {
+              componentRef.el.focus();
+            } else if (isFunction(componentRef.el.querySelector)) {
+              const focusableElement = componentRef.el.querySelector(
+                'input, select, textarea, button',
+              );
+              if (focusableElement && isFunction(focusableElement.focus)) {
+                focusableElement.focus();
+              }
+            } else if (
+              componentRef.el.nextElementSibling &&
+              isFunction(componentRef.el.nextElementSibling.focus)
+            ) {
+              componentRef.el.nextElementSibling.focus();
+            }
+          }
+        }
+      }
+    },
+  };
+
+  await vbenConfirm(props);
+  return modelValue.value;
+}
+
+export function clearAllAlerts() {
+  alerts.value.forEach((alert) => {
+    // 从DOM中移除容器
+    render(null, alert.container);
+    if (alert.container.parentNode) {
+      alert.container.remove();
+    }
+  });
+  alerts.value = [];
+}

+ 99 - 0
packages/@core/ui-kit/popup-ui/src/alert/alert.ts

@@ -0,0 +1,99 @@
+import type { Component, VNode, VNodeArrayChildren } from 'vue';
+
+import type { Recordable } from '@vben-core/typings';
+
+import { createContext } from '@vben-core/shadcn-ui';
+
+export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
+
+export type BeforeCloseScope = {
+  isConfirm: boolean;
+};
+
+export type AlertProps = {
+  /** 关闭前的回调,如果返回false,则终止关闭 */
+  beforeClose?: (
+    scope: BeforeCloseScope,
+  ) => boolean | Promise<boolean | undefined> | undefined;
+  /** 边框 */
+  bordered?: boolean;
+  /**
+   * 按钮对齐方式
+   * @default 'end'
+   */
+  buttonAlign?: 'center' | 'end' | 'start';
+  /** 取消按钮的标题 */
+  cancelText?: string;
+  /** 是否居中显示 */
+  centered?: boolean;
+  /** 确认按钮的标题 */
+  confirmText?: string;
+  /** 弹窗容器的额外样式 */
+  containerClass?: string;
+  /** 弹窗提示内容 */
+  content: Component | string;
+  /** 弹窗内容的额外样式 */
+  contentClass?: string;
+  /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
+  contentMasking?: boolean;
+  /** 弹窗底部内容(与按钮在同一个容器中) */
+  footer?: Component | string;
+  /** 弹窗的图标(在标题的前面) */
+  icon?: Component | IconType;
+  /**
+   * 弹窗遮罩模糊效果
+   */
+  overlayBlur?: number;
+  /** 是否显示取消按钮 */
+  showCancel?: boolean;
+  /** 弹窗标题 */
+  title?: string;
+};
+
+/** Prompt属性 */
+export type PromptProps<T = any> = {
+  /** 关闭前的回调,如果返回false,则终止关闭 */
+  beforeClose?: (scope: {
+    isConfirm: boolean;
+    value: T | undefined;
+  }) => boolean | Promise<boolean | undefined> | undefined;
+  /** 用于接受用户输入的组件 */
+  component?: Component;
+  /** 输入组件的属性 */
+  componentProps?: Recordable<any>;
+  /** 输入组件的插槽 */
+  componentSlots?:
+    | (() => any)
+    | Recordable<unknown>
+    | VNode
+    | VNodeArrayChildren;
+  /** 默认值 */
+  defaultValue?: T;
+  /** 输入组件的值属性名 */
+  modelPropName?: string;
+} & Omit<AlertProps, 'beforeClose'>;
+
+/**
+ * Alert上下文
+ */
+export type AlertContext = {
+  /** 执行取消操作 */
+  doCancel: () => void;
+  /** 执行确认操作 */
+  doConfirm: () => void;
+};
+
+export const [injectAlertContext, provideAlertContext] =
+  createContext<AlertContext>('VbenAlertContext');
+
+/**
+ * 获取Alert上下文
+ * @returns AlertContext
+ */
+export function useAlertContext() {
+  const context = injectAlertContext();
+  if (!context) {
+    throw new Error('useAlertContext must be used within an AlertProvider');
+  }
+  return context;
+}

+ 211 - 0
packages/@core/ui-kit/popup-ui/src/alert/alert.vue

@@ -0,0 +1,211 @@
+<script lang="ts" setup>
+import type { Component } from 'vue';
+
+import type { AlertProps } from './alert';
+
+import { computed, h, nextTick, ref } from 'vue';
+
+import { useSimpleLocale } from '@vben-core/composables';
+import {
+  CircleAlert,
+  CircleCheckBig,
+  CircleHelp,
+  CircleX,
+  Info,
+  X,
+} from '@vben-core/icons';
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogTitle,
+  VbenButton,
+  VbenLoading,
+  VbenRenderContent,
+} from '@vben-core/shadcn-ui';
+import { globalShareState } from '@vben-core/shared/global-state';
+import { cn } from '@vben-core/shared/utils';
+
+import { provideAlertContext } from './alert';
+
+const props = withDefaults(defineProps<AlertProps>(), {
+  bordered: true,
+  buttonAlign: 'end',
+  centered: true,
+  containerClass: 'w-[520px]',
+});
+const emits = defineEmits(['closed', 'confirm', 'opened']);
+const open = defineModel<boolean>('open', { default: false });
+const { $t } = useSimpleLocale();
+const components = globalShareState.getComponents();
+const isConfirm = ref(false);
+
+function onAlertClosed() {
+  emits('closed', isConfirm.value);
+  isConfirm.value = false;
+}
+
+function onEscapeKeyDown() {
+  isConfirm.value = false;
+}
+
+const getIconRender = computed(() => {
+  let iconRender: Component | null = null;
+  if (props.icon) {
+    if (typeof props.icon === 'string') {
+      switch (props.icon) {
+        case 'error': {
+          iconRender = h(CircleX, {
+            style: { color: 'hsl(var(--destructive))' },
+          });
+          break;
+        }
+        case 'info': {
+          iconRender = h(Info, { style: { color: 'hsl(var(--info))' } });
+          break;
+        }
+        case 'question': {
+          iconRender = CircleHelp;
+          break;
+        }
+        case 'success': {
+          iconRender = h(CircleCheckBig, {
+            style: { color: 'hsl(var(--success))' },
+          });
+          break;
+        }
+        case 'warning': {
+          iconRender = h(CircleAlert, {
+            style: { color: 'hsl(var(--warning))' },
+          });
+          break;
+        }
+        default: {
+          iconRender = null;
+          break;
+        }
+      }
+    }
+  } else {
+    iconRender = props.icon ?? null;
+  }
+  return iconRender;
+});
+
+function doCancel() {
+  handleCancel();
+  handleOpenChange(false);
+}
+
+function doConfirm() {
+  handleConfirm();
+  handleOpenChange(false);
+}
+
+provideAlertContext({
+  doCancel,
+  doConfirm,
+});
+
+function handleConfirm() {
+  isConfirm.value = true;
+  emits('confirm');
+}
+
+function handleCancel() {
+  isConfirm.value = false;
+}
+
+const loading = ref(false);
+async function handleOpenChange(val: boolean) {
+  await nextTick(); // 等待标记isConfirm状态
+  if (!val && props.beforeClose) {
+    loading.value = true;
+    try {
+      const res = await props.beforeClose({ isConfirm: isConfirm.value });
+      if (res !== false) {
+        open.value = false;
+      }
+    } finally {
+      loading.value = false;
+    }
+  } else {
+    open.value = val;
+  }
+}
+</script>
+<template>
+  <AlertDialog :open="open" @update:open="handleOpenChange">
+    <AlertDialogContent
+      :open="open"
+      :centered="centered"
+      :overlay-blur="overlayBlur"
+      @opened="emits('opened')"
+      @closed="onAlertClosed"
+      @escape-key-down="onEscapeKeyDown"
+      :class="
+        cn(
+          containerClass,
+          'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
+          {
+            'border-border border': bordered,
+            'shadow-3xl': !bordered,
+          },
+        )
+      "
+    >
+      <div :class="cn('relative flex-1 overflow-y-auto p-3', contentClass)">
+        <AlertDialogTitle v-if="title">
+          <div class="flex items-center">
+            <component :is="getIconRender" class="mr-2" />
+            <span class="flex-auto">{{ $t(title) }}</span>
+            <AlertDialogCancel v-if="showCancel" as-child>
+              <VbenButton
+                variant="ghost"
+                size="icon"
+                class="rounded-full"
+                :disabled="loading"
+                @click="handleCancel"
+              >
+                <X class="text-muted-foreground size-4" />
+              </VbenButton>
+            </AlertDialogCancel>
+          </div>
+        </AlertDialogTitle>
+        <AlertDialogDescription>
+          <div class="m-4 min-h-[30px]">
+            <VbenRenderContent :content="content" render-br />
+          </div>
+          <VbenLoading v-if="loading && contentMasking" :spinning="loading" />
+        </AlertDialogDescription>
+        <div
+          class="flex items-center justify-end gap-x-2"
+          :class="`justify-${buttonAlign}`"
+        >
+          <VbenRenderContent :content="footer" />
+          <AlertDialogCancel v-if="showCancel" as-child>
+            <component
+              :is="components.DefaultButton || VbenButton"
+              :disabled="loading"
+              variant="ghost"
+              @click="handleCancel"
+            >
+              {{ cancelText || $t('cancel') }}
+            </component>
+          </AlertDialogCancel>
+          <AlertDialogAction as-child>
+            <component
+              :is="components.PrimaryButton || VbenButton"
+              :loading="loading"
+              @click="handleConfirm"
+            >
+              {{ confirmText || $t('confirm') }}
+            </component>
+          </AlertDialogAction>
+        </div>
+      </div>
+    </AlertDialogContent>
+  </AlertDialog>
+</template>

+ 14 - 0
packages/@core/ui-kit/popup-ui/src/alert/index.ts

@@ -0,0 +1,14 @@
+export type {
+  AlertProps,
+  BeforeCloseScope,
+  IconType,
+  PromptProps,
+} from './alert';
+export { useAlertContext } from './alert';
+export { default as Alert } from './alert.vue';
+export {
+  vbenAlert as alert,
+  clearAllAlerts,
+  vbenConfirm as confirm,
+  vbenPrompt as prompt,
+} from './AlertBuilder';

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

@@ -54,7 +54,6 @@ describe('drawerApi', () => {
   });
 
   it('should close the drawer if onBeforeClose allows it', () => {
-    drawerApi.open();
     drawerApi.close();
     expect(drawerApi.store.state.isOpen).toBe(false);
   });

+ 4 - 3
packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts

@@ -86,12 +86,13 @@ export class DrawerApi {
   }
 
   /**
-   * 关闭弹窗
+   * 关闭抽屉
+   * @description 关闭抽屉时会调用 onBeforeClose 钩子函数,如果 onBeforeClose 返回 false,则不关闭弹窗
    */
-  close() {
+  async close() {
     // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗
     // 如果 onBeforeClose 返回 false,则不关闭弹窗
-    const allowClose = this.api.onBeforeClose?.() ?? true;
+    const allowClose = (await this.api.onBeforeClose?.()) ?? true;
     if (allowClose) {
       this.store.setState((prev) => ({
         ...prev,

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

@@ -1,6 +1,6 @@
 import type { Component, Ref } from 'vue';
 
-import type { ClassType } from '@vben-core/typings';
+import type { ClassType, MaybePromise } from '@vben-core/typings';
 
 import type { DrawerApi } from './drawer-api';
 
@@ -53,6 +53,10 @@ export interface DrawerProps {
    */
   description?: string;
   /**
+   * 在关闭时销毁抽屉
+   */
+  destroyOnClose?: boolean;
+  /**
    * 是否显示底部
    * @default true
    */
@@ -144,14 +148,10 @@ export interface DrawerApiOptions extends DrawerState {
    */
   connectedComponent?: Component;
   /**
-   * 在关闭时销毁抽屉。仅在使用 connectedComponent 时有效
-   */
-  destroyOnClose?: boolean;
-  /**
    * 关闭前的回调,返回 false 可以阻止关闭
    * @returns
    */
-  onBeforeClose?: () => void;
+  onBeforeClose?: () => MaybePromise<boolean | undefined>;
   /**
    * 点击取消按钮的回调
    */

+ 43 - 22
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { DrawerProps, ExtendedDrawerApi } from './drawer';
 
-import { computed, provide, ref, useId, watch } from 'vue';
+import { computed, provide, ref, unref, useId, watch } from 'vue';
 
 import {
   useIsMobile,
@@ -35,6 +35,7 @@ interface Props extends DrawerProps {
 const props = withDefaults(defineProps<Props>(), {
   appendToMain: false,
   closeIconPlacement: 'right',
+  destroyOnClose: false,
   drawerApi: undefined,
   submitting: false,
   zIndex: 1000,
@@ -63,6 +64,7 @@ const {
   confirmText,
   contentClass,
   description,
+  destroyOnClose,
   footer: showFooter,
   footerClass,
   header: showHeader,
@@ -80,17 +82,17 @@ const {
   zIndex,
 } = usePriorityValues(props, state);
 
-watch(
-  () => showLoading.value,
-  (v) => {
-    if (v && wrapperRef.value) {
-      wrapperRef.value.scrollTo({
-        // behavior: 'smooth',
-        top: 0,
-      });
-    }
-  },
-);
+// watch(
+//   () => showLoading.value,
+//   (v) => {
+//     if (v && wrapperRef.value) {
+//       wrapperRef.value.scrollTo({
+//         // behavior: 'smooth',
+//         top: 0,
+//       });
+//     }
+//   },
+// );
 
 function interactOutside(e: Event) {
   if (!closeOnClickModal.value || submitting.value) {
@@ -131,6 +133,29 @@ const getAppendTo = computed(() => {
     ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
     : undefined;
 });
+
+/**
+ * destroyOnClose功能完善
+ */
+// 是否打开过
+const hasOpened = ref(false);
+const isClosed = ref(true);
+watch(
+  () => state?.value?.isOpen,
+  (value) => {
+    isClosed.value = false;
+    if (value && !unref(hasOpened)) {
+      hasOpened.value = true;
+    }
+  },
+);
+function handleClosed() {
+  isClosed.value = true;
+  props.drawerApi?.onClosed();
+}
+const getForceMount = computed(() => {
+  return !unref(destroyOnClose) && unref(hasOpened);
+});
 </script>
 <template>
   <Sheet
@@ -144,15 +169,17 @@ const getAppendTo = computed(() => {
         cn('flex w-[520px] flex-col', drawerClass, {
           '!w-full': isMobile || placement === 'bottom' || placement === 'top',
           'max-h-[100vh]': placement === 'bottom' || placement === 'top',
+          hidden: isClosed,
         })
       "
       :modal="modal"
       :open="state?.isOpen"
       :side="placement"
       :z-index="zIndex"
+      :force-mount="getForceMount"
       :overlay-blur="overlayBlur"
       @close-auto-focus="handleFocusOutside"
-      @closed="() => drawerApi?.onClosed()"
+      @closed="handleClosed"
       @escape-key-down="escapeKeyDown"
       @focus-outside="handleFocusOutside"
       @interact-outside="interactOutside"
@@ -239,19 +266,13 @@ const getAppendTo = computed(() => {
         ref="wrapperRef"
         :class="
           cn('relative flex-1 overflow-y-auto p-3', contentClass, {
-            'overflow-hidden': showLoading,
+            'pointer-events-none': showLoading || submitting,
           })
         "
       >
-        <VbenLoading
-          v-if="showLoading || submitting"
-          class="size-full"
-          spinning
-        />
-
         <slot></slot>
       </div>
-
+      <VbenLoading v-if="showLoading || submitting" spinning />
       <SheetFooter
         v-if="showFooter"
         :class="
@@ -274,7 +295,7 @@ const getAppendTo = computed(() => {
               {{ cancelText || $t('cancel') }}
             </slot>
           </component>
-
+          <slot name="center-footer"></slot>
           <component
             :is="components.PrimaryButton || VbenButton"
             v-if="showConfirmButton"

+ 13 - 2
packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts

@@ -9,6 +9,7 @@ import {
   h,
   inject,
   nextTick,
+  onDeactivated,
   provide,
   reactive,
   ref,
@@ -64,11 +65,20 @@ export function useVbenDrawer<
             slots,
           );
       },
+      // eslint-disable-next-line vue/one-component-per-file
       {
-        inheritAttrs: false,
         name: 'VbenParentDrawer',
+        inheritAttrs: false,
       },
     );
+
+    /**
+     * 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
+     */
+    onDeactivated(() => {
+      (extendedApi as ExtendedDrawerApi)?.close?.();
+    });
+
     return [Drawer, extendedApi as ExtendedDrawerApi] as const;
   }
 
@@ -105,9 +115,10 @@ export function useVbenDrawer<
       return () =>
         h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);
     },
+    // eslint-disable-next-line vue/one-component-per-file
     {
-      inheritAttrs: false,
       name: 'VbenDrawer',
+      inheritAttrs: false,
     },
   );
   injectData.extendApi?.(extendedApi);

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

@@ -1,2 +1,3 @@
+export * from './alert';
 export * from './drawer';
 export * from './modal';

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

@@ -44,6 +44,7 @@ export class ModalApi {
       confirmDisabled: false,
       confirmLoading: false,
       contentClass: '',
+      destroyOnClose: true,
       draggable: false,
       footer: true,
       footerClass: '',

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

@@ -61,6 +61,10 @@ export interface ModalProps {
    */
   description?: string;
   /**
+   * 在关闭时销毁弹窗
+   */
+  destroyOnClose?: boolean;
+  /**
    * 是否可拖拽
    * @default false
    */
@@ -154,10 +158,6 @@ export interface ModalApiOptions extends ModalState {
    */
   connectedComponent?: Component;
   /**
-   * 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效
-   */
-  destroyOnClose?: boolean;
-  /**
    * 关闭前的回调,返回 false 可以阻止关闭
    * @returns
    */

+ 35 - 21
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { ExtendedModalApi, ModalProps } from './modal';
 
-import { computed, nextTick, provide, ref, useId, watch } from 'vue';
+import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
 
 import {
   useIsMobile,
@@ -34,6 +34,7 @@ interface Props extends ModalProps {
 
 const props = withDefaults(defineProps<Props>(), {
   appendToMain: false,
+  destroyOnClose: false,
   modalApi: undefined,
 });
 
@@ -67,6 +68,7 @@ const {
   confirmText,
   contentClass,
   description,
+  destroyOnClose,
   draggable,
   footer: showFooter,
   footerClass,
@@ -100,10 +102,15 @@ const { dragging, transform } = useModalDraggable(
   shouldDraggable,
 );
 
+const firstOpened = ref(false);
+const isClosed = ref(true);
+
 watch(
   () => state?.value?.isOpen,
   async (v) => {
     if (v) {
+      isClosed.value = false;
+      if (!firstOpened.value) firstOpened.value = true;
       await nextTick();
       if (!contentRef.value) return;
       const innerContentRef = contentRef.value.getContentRef();
@@ -113,19 +120,20 @@ watch(
       dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
     }
   },
+  { immediate: true },
 );
 
-watch(
-  () => [showLoading.value, submitting.value],
-  ([l, s]) => {
-    if ((s || l) && wrapperRef.value) {
-      wrapperRef.value.scrollTo({
-        // behavior: 'smooth',
-        top: 0,
-      });
-    }
-  },
-);
+// watch(
+//   () => [showLoading.value, submitting.value],
+//   ([l, s]) => {
+//     if ((s || l) && wrapperRef.value) {
+//       wrapperRef.value.scrollTo({
+//         // behavior: 'smooth',
+//         top: 0,
+//       });
+//     }
+//   },
+// );
 
 function handleFullscreen() {
   props.modalApi?.setState((prev) => {
@@ -176,6 +184,15 @@ const getAppendTo = computed(() => {
     ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
     : undefined;
 });
+
+const getForceMount = computed(() => {
+  return !unref(destroyOnClose) && unref(firstOpened);
+});
+
+function handleClosed() {
+  isClosed.value = true;
+  props.modalApi?.onClosed();
+}
 </script>
 <template>
   <Dialog
@@ -197,9 +214,11 @@ const getAppendTo = computed(() => {
               shouldFullscreen,
             'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
             'duration-300': !dragging,
+            hidden: isClosed,
           },
         )
       "
+      :force-mount="getForceMount"
       :modal="modal"
       :open="state?.isOpen"
       :show-close="closable"
@@ -207,7 +226,7 @@ const getAppendTo = computed(() => {
       :overlay-blur="overlayBlur"
       close-class="top-3"
       @close-auto-focus="handleFocusOutside"
-      @closed="() => modalApi?.onClosed()"
+      @closed="handleClosed"
       :close-disabled="submitting"
       @escape-key-down="escapeKeyDown"
       @focus-outside="handleFocusOutside"
@@ -255,18 +274,13 @@ const getAppendTo = computed(() => {
         ref="wrapperRef"
         :class="
           cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {
-            'overflow-hidden': showLoading || submitting,
+            'pointer-events-none': showLoading || submitting,
           })
         "
       >
-        <VbenLoading
-          v-if="showLoading || submitting"
-          class="size-full h-auto min-h-full"
-          spinning
-        />
         <slot></slot>
       </div>
-
+      <VbenLoading v-if="showLoading || submitting" spinning />
       <VbenIconButton
         v-if="fullscreenButton"
         class="hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-10 top-3 hidden size-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none sm:block"
@@ -302,7 +316,7 @@ const getAppendTo = computed(() => {
               {{ cancelText || $t('cancel') }}
             </slot>
           </component>
-
+          <slot name="center-footer"></slot>
           <component
             :is="components.PrimaryButton || VbenButton"
             v-if="showConfirmButton"

+ 15 - 5
packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts

@@ -5,6 +5,7 @@ import {
   h,
   inject,
   nextTick,
+  onDeactivated,
   provide,
   reactive,
   ref,
@@ -63,11 +64,20 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
             slots,
           );
       },
+      // eslint-disable-next-line vue/one-component-per-file
       {
-        inheritAttrs: false,
         name: 'VbenParentModal',
+        inheritAttrs: false,
       },
     );
+
+    /**
+     * 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
+     */
+    onDeactivated(() => {
+      (extendedApi as ExtendedModalApi)?.close?.();
+    });
+
     return [Modal, extendedApi as ExtendedModalApi] as const;
   }
 
@@ -84,14 +94,13 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
     injectData.options?.onOpenChange?.(isOpen);
   };
 
-  const onClosed = mergedOptions.onClosed;
-
   mergedOptions.onClosed = () => {
-    onClosed?.();
+    options.onClosed?.();
     if (mergedOptions.destroyOnClose) {
       injectData.reCreateModal?.();
     }
   };
+
   const api = new ModalApi(mergedOptions);
 
   const extendedApi: ExtendedModalApi = api as never;
@@ -113,9 +122,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
           slots,
         );
     },
+    // eslint-disable-next-line vue/one-component-per-file
     {
-      inheritAttrs: false,
       name: 'VbenModal',
+      inheritAttrs: false,
     },
   );
   injectData.extendApi?.(extendedApi);

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

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

+ 4 - 4
packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue

@@ -6,11 +6,11 @@ import type { ValueType, VbenButtonGroupProps } from './button';
 import { computed, ref, watch } from 'vue';
 
 import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
-import { VbenRenderContent } from '@vben-core/shadcn-ui';
 import { cn, isFunction } from '@vben-core/shared/utils';
 
 import { objectOmit } from '@vueuse/core';
 
+import { VbenRenderContent } from '../render-content';
 import VbenButtonGroup from './button-group.vue';
 import Button from './button.vue';
 
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
   showIcon: true,
   size: 'middle',
 });
-
+const emit = defineEmits(['btnClick']);
 const btnDefaultProps = computed(() => {
   return {
     ...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),
@@ -41,7 +41,6 @@ watch(
         innerValue.value.length > 0 ? innerValue.value[0] : undefined;
     }
   },
-  { immediate: true },
 );
 
 watch(
@@ -60,7 +59,7 @@ watch(
       innerValue.value = val === undefined ? [] : [val as ValueType];
     }
   },
-  { deep: true },
+  { deep: true, immediate: true },
 );
 
 async function onBtnClick(value: ValueType) {
@@ -90,6 +89,7 @@ async function onBtnClick(value: ValueType) {
     innerValue.value = [value];
     modelValue.value = value;
   }
+  emit('btnClick', value);
 }
 </script>
 <template>

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

@@ -55,12 +55,13 @@ withDefaults(defineProps<Props>(), {
         :size="logoSize"
         class="relative rounded-none bg-transparent"
       />
-      <span
-        v-if="!collapsed"
-        class="text-foreground truncate text-nowrap font-semibold"
-      >
-        {{ text }}
-      </span>
+      <template v-if="!collapsed">
+        <slot name="text">
+          <span class="text-foreground truncate text-nowrap font-semibold">
+            {{ text }}
+          </span>
+        </slot>
+      </template>
     </a>
   </div>
 </template>

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

@@ -21,6 +21,7 @@ interface Props extends PopoverRootProps {
   class?: ClassType;
   contentClass?: ClassType;
   contentProps?: PopoverContentProps;
+  triggerClass?: ClassType;
 }
 
 const props = withDefaults(defineProps<Props>(), {});
@@ -32,6 +33,7 @@ const delegatedProps = computed(() => {
     class: _cls,
     contentClass: _,
     contentProps: _cProps,
+    triggerClass: _tClass,
     ...delegated
   } = props;
 
@@ -43,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
 
 <template>
   <PopoverRoot v-bind="forwarded">
-    <PopoverTrigger>
+    <PopoverTrigger :class="triggerClass">
       <slot name="trigger"></slot>
 
       <PopoverContent

+ 18 - 2
packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue

@@ -3,7 +3,7 @@ import type { Component, PropType } from 'vue';
 
 import { defineComponent, h } from 'vue';
 
-import { isFunction, isObject } from '@vben-core/shared/utils';
+import { isFunction, isObject, isString } from '@vben-core/shared/utils';
 
 export default defineComponent({
   name: 'RenderContent',
@@ -14,6 +14,10 @@ export default defineComponent({
         | undefined,
       type: [Object, String, Function],
     },
+    renderBr: {
+      default: false,
+      type: Boolean,
+    },
   },
   setup(props, { attrs, slots }) {
     return () => {
@@ -24,7 +28,19 @@ export default defineComponent({
         (isObject(props.content) || isFunction(props.content)) &&
         props.content !== null;
       if (!isComponent) {
-        return props.content;
+        if (props.renderBr && isString(props.content)) {
+          const lines = props.content.split('\n');
+          const result = [];
+          for (const [i, line] of lines.entries()) {
+            result.push(h('p', { key: i }, line));
+            // if (i < lines.length - 1) {
+            //   result.push(h('br'));
+            // }
+          }
+          return result;
+        } else {
+          return props.content;
+        }
       }
       return h(props.content as never, {
         ...attrs,

+ 16 - 4
packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue

@@ -39,6 +39,14 @@ const isAtRight = ref(false);
 const isAtBottom = ref(false);
 const isAtLeft = ref(true);
 
+/**
+ * We have to check if the scroll amount is close enough to some threshold in order to
+ * more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
+ * numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
+ * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
+ */
+const ARRIVED_STATE_THRESHOLD_PIXELS = 1;
+
 const showShadowTop = computed(() => props.shadow && props.shadowTop);
 const showShadowBottom = computed(() => props.shadow && props.shadowBottom);
 const showShadowLeft = computed(() => props.shadow && props.shadowLeft);
@@ -60,14 +68,18 @@ function handleScroll(event: Event) {
   const target = event.target as HTMLElement;
   const scrollTop = target?.scrollTop ?? 0;
   const scrollLeft = target?.scrollLeft ?? 0;
-  const offsetHeight = target?.offsetHeight ?? 0;
-  const offsetWidth = target?.offsetWidth ?? 0;
+  const clientHeight = target?.clientHeight ?? 0;
+  const clientWidth = target?.clientWidth ?? 0;
   const scrollHeight = target?.scrollHeight ?? 0;
   const scrollWidth = target?.scrollWidth ?? 0;
   isAtTop.value = scrollTop <= 0;
   isAtLeft.value = scrollLeft <= 0;
-  isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
-  isAtRight.value = scrollLeft + offsetWidth >= scrollWidth;
+  isAtBottom.value =
+    Math.abs(scrollTop) + clientHeight >=
+    scrollHeight - ARRIVED_STATE_THRESHOLD_PIXELS;
+  isAtRight.value =
+    Math.abs(scrollLeft) + clientWidth >=
+    scrollWidth - ARRIVED_STATE_THRESHOLD_PIXELS;
 
   emit('scrollAt', {
     bottom: isAtBottom.value,

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

@@ -10,7 +10,7 @@ import TabsIndicator from './tabs-indicator.vue';
 
 interface Props {
   defaultValue?: string;
-  tabs: SegmentedItem[];
+  tabs?: SegmentedItem[];
 }
 
 const props = withDefaults(defineProps<Props>(), {

+ 26 - 4
packages/@core/ui-kit/shadcn-ui/src/components/select/select.vue

@@ -1,4 +1,6 @@
 <script lang="ts" setup>
+import { CircleX } from '@vben-core/icons';
+
 import {
   Select,
   SelectContent,
@@ -8,17 +10,33 @@ import {
 } from '../../ui';
 
 interface Props {
+  allowClear?: boolean;
   class?: any;
   options?: Array<{ label: string; value: string }>;
   placeholder?: string;
 }
 
-const props = defineProps<Props>();
+const props = withDefaults(defineProps<Props>(), {
+  allowClear: false,
+});
+
+const modelValue = defineModel<string>();
+
+function handleClear() {
+  modelValue.value = undefined;
+}
 </script>
 <template>
-  <Select>
-    <SelectTrigger :class="props.class">
-      <SelectValue :placeholder="placeholder" />
+  <Select v-model="modelValue">
+    <SelectTrigger :class="props.class" class="flex w-full items-center">
+      <SelectValue class="flex-auto text-left" :placeholder="placeholder" />
+      <CircleX
+        @pointerdown.stop
+        @click.stop.prevent="handleClear"
+        v-if="allowClear && modelValue"
+        data-clear-button
+        class="mr-1 size-4 cursor-pointer opacity-50 hover:opacity-100"
+      />
     </SelectTrigger>
     <SelectContent>
       <template v-for="item in options" :key="item.value">
@@ -32,4 +50,8 @@ const props = defineProps<Props>();
 button[role='combobox'][data-placeholder] {
   color: hsl(var(--muted-foreground));
 }
+
+button {
+  --ring: var(--primary);
+}
 </style>

+ 16 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue

@@ -0,0 +1,16 @@
+<script setup lang="ts">
+import type { AlertDialogEmits, AlertDialogProps } from 'radix-vue';
+
+import { AlertDialogRoot, useForwardPropsEmits } from 'radix-vue';
+
+const props = defineProps<AlertDialogProps>();
+const emits = defineEmits<AlertDialogEmits>();
+
+const forwarded = useForwardPropsEmits(props, emits);
+</script>
+
+<template>
+  <AlertDialogRoot v-bind="forwarded">
+    <slot></slot>
+  </AlertDialogRoot>
+</template>

+ 13 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue

@@ -0,0 +1,13 @@
+<script setup lang="ts">
+import type { AlertDialogActionProps } from 'radix-vue';
+
+import { AlertDialogAction } from 'radix-vue';
+
+const props = defineProps<AlertDialogActionProps>();
+</script>
+
+<template>
+  <AlertDialogAction v-bind="props">
+    <slot></slot>
+  </AlertDialogAction>
+</template>

+ 13 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue

@@ -0,0 +1,13 @@
+<script setup lang="ts">
+import type { AlertDialogCancelProps } from 'radix-vue';
+
+import { AlertDialogCancel } from 'radix-vue';
+
+const props = defineProps<AlertDialogCancelProps>();
+</script>
+
+<template>
+  <AlertDialogCancel v-bind="props">
+    <slot></slot>
+  </AlertDialogCancel>
+</template>

+ 101 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue

@@ -0,0 +1,101 @@
+<script setup lang="ts">
+import type {
+  AlertDialogContentEmits,
+  AlertDialogContentProps,
+} from 'radix-vue';
+
+import type { ClassType } from '@vben-core/typings';
+
+import { computed, ref } from 'vue';
+
+import { cn } from '@vben-core/shared/utils';
+
+import {
+  AlertDialogContent,
+  AlertDialogPortal,
+  useForwardPropsEmits,
+} from 'radix-vue';
+
+import AlertDialogOverlay from './AlertDialogOverlay.vue';
+
+const props = withDefaults(
+  defineProps<
+    AlertDialogContentProps & {
+      centered?: boolean;
+      class?: ClassType;
+      modal?: boolean;
+      open?: boolean;
+      overlayBlur?: number;
+      zIndex?: number;
+    }
+  >(),
+  { modal: true },
+);
+const emits = defineEmits<
+  AlertDialogContentEmits & { close: []; closed: []; opened: [] }
+>();
+
+const delegatedProps = computed(() => {
+  const { class: _, modal: _modal, open: _open, ...delegated } = props;
+
+  return delegated;
+});
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits);
+
+const contentRef = ref<InstanceType<typeof AlertDialogContent> | null>(null);
+function onAnimationEnd(event: AnimationEvent) {
+  // 只有在 contentRef 的动画结束时才触发 opened/closed 事件
+  if (event.target === contentRef.value?.$el) {
+    if (props.open) {
+      emits('opened');
+    } else {
+      emits('closed');
+    }
+  }
+}
+defineExpose({
+  getContentRef: () => contentRef.value,
+});
+</script>
+
+<template>
+  <AlertDialogPortal>
+    <Transition name="fade" appear>
+      <AlertDialogOverlay
+        v-if="open && modal"
+        :style="{
+          ...(zIndex ? { zIndex } : {}),
+          position: 'fixed',
+          backdropFilter:
+            overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
+        }"
+        @click="() => emits('close')"
+      />
+    </Transition>
+    <AlertDialogContent
+      ref="contentRef"
+      :style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
+      @animationend="onAnimationEnd"
+      v-bind="forwarded"
+      :class="
+        cn(
+          'z-popup bg-background w-full p-6 shadow-lg outline-none sm:rounded-xl',
+          'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
+          'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
+          {
+            'data-[state=open]:slide-in-from-top-[48%] data-[state=closed]:slide-out-to-top-[48%]':
+              !centered,
+            'data-[state=open]:slide-in-from-top-[98%] data-[state=closed]:slide-out-to-top-[148%]':
+              centered,
+            'top-[10vh]': !centered,
+            'top-1/2 -translate-y-1/2': centered,
+          },
+          props.class,
+        )
+      "
+    >
+      <slot></slot>
+    </AlertDialogContent>
+  </AlertDialogPortal>
+</template>

+ 28 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue

@@ -0,0 +1,28 @@
+<script lang="ts" setup>
+import type { AlertDialogDescriptionProps } from 'radix-vue';
+
+import { computed } from 'vue';
+
+import { cn } from '@vben-core/shared/utils';
+
+import { AlertDialogDescription, useForwardProps } from 'radix-vue';
+
+const props = defineProps<AlertDialogDescriptionProps & { class?: any }>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+  <AlertDialogDescription
+    v-bind="forwardedProps"
+    :class="cn('text-muted-foreground text-sm', props.class)"
+  >
+    <slot></slot>
+  </AlertDialogDescription>
+</template>

+ 8 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue

@@ -0,0 +1,8 @@
+<script setup lang="ts">
+import { useScrollLock } from '@vben-core/composables';
+
+useScrollLock();
+</script>
+<template>
+  <div class="bg-overlay z-popup inset-0"></div>
+</template>

+ 30 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue

@@ -0,0 +1,30 @@
+<script setup lang="ts">
+import type { AlertDialogTitleProps } from 'radix-vue';
+
+import { computed } from 'vue';
+
+import { cn } from '@vben-core/shared/utils';
+
+import { AlertDialogTitle, useForwardProps } from 'radix-vue';
+
+const props = defineProps<AlertDialogTitleProps & { class?: any }>();
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props;
+
+  return delegated;
+});
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+  <AlertDialogTitle
+    v-bind="forwardedProps"
+    :class="
+      cn('text-lg font-semibold leading-none tracking-tight', props.class)
+    "
+  >
+    <slot></slot>
+  </AlertDialogTitle>
+</template>

+ 6 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts

@@ -0,0 +1,6 @@
+export { default as AlertDialog } from './AlertDialog.vue';
+export { default as AlertDialogAction } from './AlertDialogAction.vue';
+export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
+export { default as AlertDialogContent } from './AlertDialogContent.vue';
+export { default as AlertDialogDescription } from './AlertDialogDescription.vue';
+export { default as AlertDialogTitle } from './AlertDialogTitle.vue';

+ 1 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/index.ts

@@ -1,4 +1,5 @@
 export * from './accordion';
+export * from './alert-dialog';
 export * from './avatar';
 export * from './badge';
 export * from './breadcrumb';

部分文件因为文件数量过多而无法显示