DESKTOP-USV654P\pc il y a 1 an
Parent
commit
53853d3af5
100 fichiers modifiés avec 2090 ajouts et 558 suppressions
  1. 1 1
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 1
      .github/ISSUE_TEMPLATE/feature-request.yml
  3. 2 1
      .vscode/settings.json
  4. 1 1
      apps/web-antd/package.json
  5. 3 2
      apps/web-antd/src/router/routes/index.ts
  6. 1 1
      apps/web-baicai/package.json
  7. 1 1
      apps/web-ele/package.json
  8. 3 1
      apps/web-ele/src/adapter/component/index.ts
  9. 3 2
      apps/web-ele/src/router/routes/index.ts
  10. 15 0
      apps/web-ele/src/views/demos/element/index.vue
  11. 1 1
      apps/web-naive/package.json
  12. 3 2
      apps/web-naive/src/router/routes/index.ts
  13. 28 26
      cspell.json
  14. 4 4
      docs/.vitepress/config/en.mts
  15. 6 6
      docs/.vitepress/config/zh.mts
  16. 1 1
      docs/package.json
  17. 4 1
      docs/src/commercial/community.md
  18. 1 0
      docs/src/components/common-ui/vben-drawer.md
  19. 18 1
      docs/src/components/common-ui/vben-form.md
  20. 2 1
      docs/src/components/common-ui/vben-modal.md
  21. 124 82
      docs/src/en/guide/essentials/route.md
  22. 4 6
      docs/src/en/guide/introduction/vben.md
  23. 20 2
      docs/src/guide/essentials/route.md
  24. 6 6
      docs/src/guide/introduction/vben.md
  25. 1 1
      internal/lint-configs/commitlint-config/package.json
  26. 1 1
      internal/lint-configs/eslint-config/src/configs/typescript.ts
  27. 1 0
      internal/lint-configs/eslint-config/src/configs/unicorn.ts
  28. 1 1
      internal/lint-configs/stylelint-config/package.json
  29. 1 1
      internal/node-utils/package.json
  30. 1 1
      internal/tailwind-config/package.json
  31. 1 1
      internal/tsconfig/package.json
  32. 1 1
      internal/vite-config/package.json
  33. 2 2
      package.json
  34. 2 2
      packages/@core/base/design/package.json
  35. 2 0
      packages/@core/base/design/src/design-tokens/dark.css
  36. 2 0
      packages/@core/base/design/src/design-tokens/default.css
  37. 1 1
      packages/@core/base/icons/package.json
  38. 1 3
      packages/@core/base/shared/package.json
  39. 0 4
      packages/@core/base/shared/src/utils/download.ts
  40. 1 1
      packages/@core/base/typings/package.json
  41. 1 1
      packages/@core/composables/package.json
  42. 1 1
      packages/@core/preferences/package.json
  43. 1 1
      packages/@core/ui-kit/form-ui/package.json
  44. 5 0
      packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  45. 14 5
      packages/@core/ui-kit/form-ui/src/form-api.ts
  46. 9 1
      packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
  47. 7 3
      packages/@core/ui-kit/form-ui/src/vben-use-form.vue
  48. 1 1
      packages/@core/ui-kit/layout-ui/package.json
  49. 4 1
      packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue
  50. 1 1
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  51. 1 1
      packages/@core/ui-kit/menu-ui/package.json
  52. 1 0
      packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
  53. 8 0
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
  54. 10 2
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
  55. 1 0
      packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
  56. 4 0
      packages/@core/ui-kit/popup-ui/src/modal/modal.ts
  57. 3 1
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  58. 1 1
      packages/@core/ui-kit/shadcn-ui/package.json
  59. 2 2
      packages/@core/ui-kit/shadcn-ui/src/components/spine-text/spine-text.vue
  60. 8 5
      packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue
  61. 1 1
      packages/@core/ui-kit/tabs-ui/package.json
  62. 1 1
      packages/constants/package.json
  63. 1 1
      packages/effects/access/package.json
  64. 1 1
      packages/effects/common-ui/package.json
  65. 3 2
      packages/effects/common-ui/src/components/icon-picker/icon-picker.vue
  66. 1 0
      packages/effects/common-ui/src/components/index.ts
  67. 1 0
      packages/effects/common-ui/src/components/resize/index.ts
  68. 1122 0
      packages/effects/common-ui/src/components/resize/resize.vue
  69. 1 1
      packages/effects/hooks/package.json
  70. 3 0
      packages/effects/hooks/src/use-design-tokens.ts
  71. 18 7
      packages/effects/hooks/src/use-watermark.ts
  72. 1 1
      packages/effects/layouts/package.json
  73. 1 2
      packages/effects/plugins/package.json
  74. 21 5
      packages/effects/plugins/src/vxe-table/extends.ts
  75. 104 0
      packages/effects/plugins/src/vxe-table/style.css
  76. 2 2
      packages/effects/plugins/src/vxe-table/use-vxe-grid.vue
  77. 1 1
      packages/effects/request/package.json
  78. 2 2
      packages/effects/request/src/request-client/preset-interceptors.ts
  79. 1 1
      packages/icons/package.json
  80. 1 1
      packages/locales/package.json
  81. 1 1
      packages/preferences/package.json
  82. 1 1
      packages/stores/package.json
  83. 1 1
      packages/styles/package.json
  84. 1 1
      packages/types/package.json
  85. 1 1
      packages/utils/package.json
  86. 1 1
      playground/package.json
  87. 3 0
      playground/src/locales/langs/zh-CN/examples.json
  88. 3 3
      playground/src/router/routes/index.ts
  89. 12 0
      playground/src/router/routes/modules/demos.ts
  90. 9 0
      playground/src/router/routes/modules/examples.ts
  91. 13 13
      playground/src/views/demos/access/button-control.vue
  92. 11 0
      playground/src/views/demos/features/icons/index.vue
  93. 24 4
      playground/src/views/demos/features/watermark/index.vue
  94. 14 3
      playground/src/views/examples/drawer/index.vue
  95. 2 2
      playground/src/views/examples/modal/auto-height-demo.vue
  96. 58 0
      playground/src/views/examples/resize/basic.vue
  97. 226 232
      pnpm-lock.yaml
  98. 65 67
      pnpm-workspace.yaml
  99. 1 1
      scripts/turbo-run/package.json
  100. 1 1
      scripts/vsh/package.json

+ 1 - 1
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -62,7 +62,7 @@ body:
       description: Before submitting the issue, please make sure you do the following
       # description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com).
       options:
-        - label: Read the [docs](https://anncwb.github.io/vue-vben-admin-doc/)
+        - label: Read the [docs](https://doc.vben.pro/)
           required: true
         - label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
           required: true

+ 1 - 1
.github/ISSUE_TEMPLATE/feature-request.yml

@@ -62,7 +62,7 @@ body:
       label: Validations
       description: Before submitting the issue, please make sure you do the following
       options:
-        - label: Read the [docs](https://anncwb.github.io/vue-vben-admin-doc/)
+        - label: Read the [docs](https://doc.vben.pro/)
           required: true
         - label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
           required: true

+ 2 - 1
.vscode/settings.json

@@ -222,5 +222,6 @@
   "commentTranslate.hover.enabled": false,
   "commentTranslate.multiLineMerge": true,
   "vue.server.hybridMode": true,
-  "typescript.tsdk": "node_modules/typescript/lib"
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "oxc.enable": false
 }

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

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

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

@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
 /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
-/** 不需要权限的菜单列表(会显示在菜单中) */
 // const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
 const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
-/** 路由列表,由基本路由+静态路由组成 */
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ *  无需走权限验证(会一直显示在菜单中) */
 const routes: RouteRecordRaw[] = [
   ...coreRoutes,
   ...externalRoutes,
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
 const accessRoutes = [...dynamicRoutes, ...staticRoutes];
 export { accessRoutes, coreRouteNames, routes };

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

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

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

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

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

@@ -15,6 +15,7 @@ import {
   ElButton,
   ElCheckbox,
   ElCheckboxGroup,
+  ElDatePicker,
   ElDivider,
   ElInput,
   ElInputNumber,
@@ -64,7 +65,7 @@ async function initComponentAdapter() {
     Checkbox: ElCheckbox,
     CheckboxGroup: ElCheckboxGroup,
     // 自定义默认按钮
-    DefaulButton: (props, { attrs, slots }) => {
+    DefaultButton: (props, { attrs, slots }) => {
       return h(ElButton, { ...props, attrs, type: 'info' }, slots);
     },
     // 自定义主要按钮
@@ -79,6 +80,7 @@ async function initComponentAdapter() {
     Space: ElSpace,
     Switch: ElSwitch,
     TimePicker: ElTimePicker,
+    DatePicker: ElDatePicker,
     TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
     Upload: ElUpload,
   };

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

@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
 /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
-/** 不需要权限的菜单列表(会显示在菜单中) */
 // const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
 const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
-/** 路由列表,由基本路由+静态路由组成 */
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ *  无需走权限验证(会一直显示在菜单中) */
 const routes: RouteRecordRaw[] = [
   ...coreRoutes,
   ...externalRoutes,
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
 const accessRoutes = [...dynamicRoutes, ...staticRoutes];
 export { accessRoutes, coreRouteNames, routes };

+ 15 - 0
apps/web-ele/src/views/demos/element/index.vue

@@ -1,4 +1,6 @@
 <script lang="ts" setup>
+import { ref } from 'vue';
+
 import { Page } from '@vben/common-ui';
 
 import {
@@ -6,6 +8,7 @@ import {
   ElCard,
   ElMessage,
   ElNotification,
+  ElSegmented,
   ElSpace,
   ElTable,
 } from 'element-plus';
@@ -47,6 +50,10 @@ const tableData = [
   { prop1: '5', prop2: 'E' },
   { prop1: '6', prop2: 'F' },
 ];
+
+const segmentedValue = ref('Mon');
+
+const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
 </script>
 
 <template>
@@ -84,6 +91,14 @@ const tableData = [
         <ElButton type="success" @click="notify('success')"> 成功 </ElButton>
       </ElSpace>
     </ElCard>
+    <ElCard class="mb-5">
+      <template #header> Segmented </template>
+      <ElSegmented
+        v-model="segmentedValue"
+        :options="segmentedOptions"
+        size="large"
+      />
+    </ElCard>
     <ElCard class="mb-5">
       <ElTable :data="tableData" stripe>
         <ElTable.TableColumn label="测试列1" prop="prop1" />

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

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

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

@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
 /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
-/** 不需要权限的菜单列表(会显示在菜单中) */
 // const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
 const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
-/** 路由列表,由基本路由+静态路由组成 */
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ *  无需走权限验证(会一直显示在菜单中) */
 const routes: RouteRecordRaw[] = [
   ...coreRoutes,
   ...externalRoutes,
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
 const accessRoutes = [...dynamicRoutes, ...staticRoutes];
 export { accessRoutes, coreRouteNames, routes };

+ 28 - 26
cspell.json

@@ -4,53 +4,55 @@
   "language": "en,en-US",
   "allowCompoundWords": true,
   "words": [
-    "clsx",
-    "esno",
-    "demi",
-    "unref",
-    "taze",
     "acmr",
     "antd",
-    "lucide",
+    "antdv",
+    "astro",
     "brotli",
+    "clsx",
     "defu",
+    "demi",
+    "echarts",
+    "ependencies",
+    "esno",
+    "etag",
     "execa",
     "iconify",
+    "iconoir",
     "intlify",
+    "lockb",
+    "lucide",
+    "minh",
+    "minw",
     "mkdist",
     "mockjs",
-    "vitejs",
+    "naiveui",
+    "nocheck",
     "noopener",
     "noreferrer",
     "nprogress",
+    "nuxt",
     "pinia",
+    "prefixs",
     "publint",
     "qrcode",
     "shadcn",
     "sonner",
+    "sortablejs",
+    "styl",
+    "taze",
+    "ui-kit",
+    "uicons",
     "unplugin",
+    "unref",
     "vben",
     "vbenjs",
-    "vueuse",
-    "yxxx",
-    "nuxt",
-    "lockb",
-    "astro",
-    "ui-kit",
-    "styl",
-    "vnode",
-    "nocheck",
-    "prefixs",
-    "vitepress",
-    "antdv",
-    "ependencies",
     "vite",
-    "echarts",
-    "sortablejs",
-    "etag",
-    "naiveui",
-    "uicons",
-    "iconoir"
+    "vitejs",
+    "vitepress",
+    "vnode",
+    "vueuse",
+    "yxxx"
   ],
   "ignorePaths": [
     "**/node_modules/**",

+ 4 - 4
docs/.vitepress/config/en.mts

@@ -221,9 +221,9 @@ function nav(): DefaultTheme.NavItem[] {
       link: '/commercial/community',
       text: '👨‍👦‍👦 Community',
     },
-    {
-      link: '/friend-links/',
-      text: '🤝 Friend Links',
-    },
+    // {
+    //   link: '/friend-links/',
+    //   text: '🤝 Friend Links',
+    // },
   ];
 }

+ 6 - 6
docs/.vitepress/config/zh.mts

@@ -124,7 +124,7 @@ function sidebarCommercial(): DefaultTheme.SidebarItem[] {
   return [
     {
       link: 'community',
-      text: '社区',
+      text: '交流群',
     },
     {
       link: 'technical-support',
@@ -266,7 +266,7 @@ function nav(): DefaultTheme.NavItem[] {
     },
     {
       link: '/commercial/community',
-      text: '👨‍👦‍👦 社区',
+      text: '👨‍👦‍👦 交流群',
       // items: [
       //   {
       //     link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',
@@ -282,10 +282,10 @@ function nav(): DefaultTheme.NavItem[] {
       //   },
       // ],
     },
-    {
-      link: '/friend-links/',
-      text: '🤝 友情链接',
-    },
+    // {
+    //   link: '/friend-links/',
+    //   text: '🤝 友情链接',
+    // },
   ];
 }
 

+ 1 - 1
docs/package.json

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

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

@@ -20,7 +20,10 @@
 
 ::: tip
 
-因为微信群人数有限制,加微信群前,你可以通过[赞助](../sponsor/personal.md)任意金额,主动发送截图给作者,备注`加入微信群`即可。
+因为微信群人数有限制,加微信群要求:
+
+- 通过[赞助](../sponsor/personal.md)任意金额。
+- 发送赞助`截图`,备注`加入微信群`即可。
 
 :::
 

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

@@ -88,6 +88,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
 | closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
 | confirmText | 确认按钮文本 | `string\|slot` | `确认` |
 | cancelText | 取消按钮文本 | `string\|slot` | `取消` |
+| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
 | showCancelButton | 显示取消按钮 | `boolean` | `true` |
 | showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
 | class | modal的class,宽度通过这个配置 | `string` | - |

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

@@ -419,7 +419,7 @@ export interface FormSchema<
   help?: string;
   /** 表单项 */
   label?: string;
-  // 自定义组件内部渲染
+  /** 自定义组件内部渲染  */
   renderComponentContent?: RenderComponentContentType;
   /** 字段规则 */
   rules?: FormSchemaRuleType;
@@ -500,3 +500,20 @@ import { z } from '#/adapter/form';
             });
 }
 ```
+
+## Slots
+
+可以使用以下插槽在表单中插入自定义的内容
+
+| 插槽名        | 描述               |
+| ------------- | ------------------ |
+| reset-before  | 重置按钮之前的位置 |
+| submit-before | 提交按钮之前的位置 |
+| expand-before | 展开按钮之前的位置 |
+| expand-after  | 展开按钮之后的位置 |
+
+::: tip 字段插槽
+
+除了以上内置插槽之外,`schema`属性中每个字段的`fieldName`都可以作为插槽名称,这些字段插槽的优先级高于`component`定义的组件。也就是说,当提供了与`fieldName`同名的插槽时,这些插槽的内容将会作为这些字段的组件,此时`component`的值将会被忽略。
+
+:::

+ 2 - 1
docs/src/components/common-ui/vben-modal.md

@@ -93,13 +93,14 @@ const [Modal, modalApi] = useVbenModal({
 | modal | 显示遮罩 | `boolean` | `true` |
 | header | 显示header | `boolean` | `true` |
 | footer | 显示footer | `boolean\|slot` | `true` |
+| confirmDisabled | 禁用确认按钮 | `boolean` | `false` |
 | confirmLoading | 确认按钮loading状态 | `boolean` | `false` |
 | closeOnClickModal | 点击遮罩关闭弹窗 | `boolean` | `true` |
 | closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
 | confirmText | 确认按钮文本 | `string\|slot` | `确认` |
 | cancelText | 取消按钮文本 | `string\|slot` | `取消` |
 | showCancelButton | 显示取消按钮 | `boolean` | `true` |
-| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
+| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
 | class | modal的class,宽度通过这个配置 | `string` | - |
 | contentClass | modal内容区域的class | `string` | - |
 | footerClass | modal底部区域的class | `string` | - |

+ 124 - 82
docs/src/en/guide/essentials/route.md

@@ -2,42 +2,66 @@
 outline: deep
 ---
 
-# Routing and Menus
+# Routes and Menus
 
-In the project, the framework provides a basic routing system and **automatically generates the corresponding menu structure based on the routing file**.
+::: info
 
-## Route Types
+This page is translated by machine translation and may not be very accurate.
 
-Routes are divided into static routes and dynamic routes. Static routes are routes that have been determined when the project starts. Dynamic routes are generally routes that are dynamically generated based on the user's permissions after the user logs in.
+:::
+
+In the project, the framework provides a basic routing system and **automatically generates the corresponding menu structure based on the routing files**.
+
+## Types of Routes
+
+Routes are divided into core routes, static routes, and dynamic routes. Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc.; static routes are routes that are determined when the project starts; dynamic routes are generally generated dynamically based on the user's permissions after the user logs in.
+
+Both static and dynamic routes go through permission control, which can be controlled by configuring the `authority` field in the `meta` property of the route.
+
+### Core Routes
+
+Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc. The configuration of core routes is in the `src/router/routes/core` directory under the application.
+
+::: tip
+
+Core routes are mainly used for the basic functions of the framework, so it is not recommended to put business-related routes in core routes. It is recommended to put business-related routes in static or dynamic routes.
+
+:::
 
 ### Static Routes
 
-If your page project does not require permission control, you can directly use static routes. The configuration of static routes is in the `src/router/routes/index` directory under the application. Open the commented file content:
+The configuration of static routes is in the `src/router/routes/index` directory under the application. Open the commented file content:
+
+::: tip
+
+Permission control is controlled by the `authority` field in the `meta` property of the route. If your page project does not require permission control, you can omit the `authority` field.
+
+:::
 
 ```ts
-// If necessary, you can open your own comments and create folders
+// Uncomment if needed and create the folder
 // const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); // [!code --]
 const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); // [!code ++]
-/** Dynamic routing */
+/** Dynamic routes */
 const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
-/** External routing lists, which can be accessed without Layout, may be used for embedding in other systems */
+/** External route list, these pages can be accessed without Layout, possibly used for embedding in other systems */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles) // [!code --]
 const externalRoutes: RouteRecordRaw[] = []; // [!code --]
 const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); // [!code ++]
 ```
 
-### Dynamic routing
+### Dynamic Routes
 
-The configuration of dynamic routing is in the corresponding application `src/router/routes/modules` directory. All routing files are stored in this directory. The content format of each file is as follows, which is consistent with the routing configuration format of Vue Router. The following is the configuration of secondary routes and multi-level routes.
+The configuration of dynamic routes is in the `src/router/routes/modules` directory under the corresponding application. This directory contains all the route files. The content format of each file is consistent with the Vue Router route configuration format. Below is the configuration of secondary and multi-level routes.
 
-## Define the route
+## Route Definition
 
-Static routes and dynamic routes are configured in the same way. The configuration of the level-2 and multi-level routes is as follows:
+The configuration method of static routes and dynamic routes is the same. Below is the configuration of secondary and multi-level routes:
 
-### Secondary route
+### Secondary Routes
 
-::: details Example code of the secondary route
+::: details Secondary Route Example Code
 
 ```ts
 import type { RouteRecordRaw } from 'vue-router';
@@ -81,17 +105,16 @@ export default routes;
 
 :::
 
-### Multilevel routing
+### Multi-level Routes
 
 ::: tip
 
-- The parent route of multi-level routing does not need to set the 'component' attribute, only the 'children' attribute needs to be set. Unless you really need to display content under nested parent routing.
-
-- If there are no special circumstances, the 'redirect' attribute of the parent route does not need to be specified and will default to the first child route.
+- The parent route of multi-level routes does not need to set the `component` property, just set the `children` property. Unless you really need to display content nested under the parent route.
+- In most cases, the `redirect` property of the parent route does not need to be specified, it will default to the first child route.
 
 :::
 
-::: details Multilevel Routing Example Code
+::: details Multi-level Route Example Code
 
 ```ts
 import type { RouteRecordRaw } from 'vue-router';
@@ -112,7 +135,7 @@ const routes: RouteRecordRaw[] = [
     path: '/demos',
     redirect: '/demos/access',
     children: [
-      // 嵌套菜单
+      // Nested menu
       {
         meta: {
           icon: 'ic:round-menu',
@@ -208,13 +231,13 @@ export default routes;
 
 :::
 
-## Add a New Page
+## Adding a New Page
 
 To add a new page, you only need to add a route and the corresponding page component.
 
-### Add a Route
+### Adding a Route
 
-Add a route object in the corresponding routing file as follows:
+Add a route object in the corresponding route file, as follows:
 
 ```ts
 import type { RouteRecordRaw } from 'vue-router';
@@ -251,9 +274,9 @@ const routes: RouteRecordRaw[] = [
 export default routes;
 ```
 
-### Add Page Component
+### Adding a Page Component
 
-In `#/views/home/`, add a new `index.vue` file as follows:
+In `#/views/home/`, add a new `index.vue` file, as follows:
 
 ```vue
 <template>
@@ -265,11 +288,11 @@ In `#/views/home/`, add a new `index.vue` file as follows:
 
 ### Verification
 
-At this point, the page has been added. Access `http://localhost:5555/home/index` to see the corresponding page.
+At this point, the page has been added. Visit `http://localhost:5555/home/index` to see the corresponding page.
 
 ## Route Configuration
 
-The route configuration mainly resides in the `meta` attribute of the route object. Below are some commonly used configuration items:
+The route configuration items are mainly in the `meta` property of the route object. The following are common configuration items:
 
 ```ts {5-8}
 const routes = [
@@ -293,22 +316,21 @@ interface RouteMeta {
    */
   activeIcon?: string;
   /**
-   * The currently active menu, used when you want to activate a parent menu instead of the existing one
-   * @default false
+   * The currently active menu, sometimes you don't want to activate the existing menu, use this to activate the parent menu
    */
   activePath?: string;
   /**
-   * Whether to affix the tab
+   * Whether to fix the tab
    * @default false
    */
   affixTab?: boolean;
   /**
-   * The order of the affixed tab
+   * The order of fixed tabs
    * @default 0
    */
   affixTabOrder?: number;
   /**
-   * Specific role identifiers required for access
+   * Specific roles required to access
    * @default []
    */
   authority?: string[];
@@ -331,22 +353,22 @@ interface RouteMeta {
     | 'warning'
     | string;
   /**
-   * Children of the current route do not show in the menu
+   * The children of the current route are not displayed in the menu
    * @default false
    */
   hideChildrenInMenu?: boolean;
   /**
-   * The current route does not show in the breadcrumb
+   * The current route is not displayed in the breadcrumb
    * @default false
    */
   hideInBreadcrumb?: boolean;
   /**
-   * The current route does not show in the menu
+   * The current route is not displayed in the menu
    * @default false
    */
   hideInMenu?: boolean;
   /**
-   * The current route does not show in tabs
+   * The current route is not displayed in the tab
    * @default false
    */
   hideInTab?: boolean;
@@ -359,16 +381,16 @@ interface RouteMeta {
    */
   iframeSrc?: string;
   /**
-   * Ignore access, can be accessed directly
+   * Ignore permissions, can be accessed directly
    * @default false
    */
   ignoreAccess?: boolean;
   /**
-   * Enable KeepAlive caching
+   * Enable KeepAlive cache
    */
   keepAlive?: boolean;
   /**
-   * External link - redirect path
+   * External link - jump path
    */
   link?: string;
   /**
@@ -381,7 +403,7 @@ interface RouteMeta {
    */
   maxNumOfOpenTab?: number;
   /**
-   * The menu is visible, but access will be redirected to 403
+   * The menu can be seen, but access will be redirected to 403
    */
   menuVisibleWithForbidden?: boolean;
   /**
@@ -389,9 +411,13 @@ interface RouteMeta {
    */
   openInNewWindow?: boolean;
   /**
-   * Used for route->menu sorting
+   * Used for route -> menu sorting
    */
   order?: number;
+  /**
+   * Parameters carried by the menu
+   */
+  query?: Recordable;
   /**
    * Title name
    */
@@ -404,153 +430,169 @@ interface RouteMeta {
 ### title
 
 - Type: `string`
-- Default value: `''`
+- Default: `''`
 
-Used to configure the page title, which will be displayed in the menu and tabs. It is generally used in conjunction with internationalization.
+Used to configure the title of the page, which will be displayed in the menu and tab. Generally used with internationalization.
 
 ### icon
 
 - Type: `string`
-- Default value: `''`
+- Default: `''`
 
-Used to configure the page icon, which will be displayed in the menu and tabs. It is generally used in conjunction with an icon library. If it is an `http` link, the image will be automatically loaded.
+Used to configure the icon of the page, which will be displayed in the menu and tab. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
 
 ### activeIcon
 
 - Type: `string`
-- Default value: `''`
+- Default: `''`
 
-Used to configure the active icon of the page, which will be displayed in the menu. It is generally used in conjunction with an icon library. If it is an `http` link, the image will be automatically loaded.
+Used to configure the active icon of the page, which will be displayed in the menu. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
 
 ### keepAlive
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
-Used to configure whether the page caching is enabled. Once enabled, the page will be cached and not reloaded, only effective when tabs are enabled.
+Used to configure whether the page cache is enabled. When enabled, the page will be cached and will not reload, only effective when the tab is enabled.
 
 ### hideInMenu
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
-Used to configure whether the page is hidden in the menu. If hidden, the page will not be displayed in the menu.
+Used to configure whether the page is hidden in the menu. When hidden, the page will not be displayed in the menu.
 
 ### hideInTab
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
-Used to configure whether the page is hidden in tabs. If hidden, the page will not be displayed in tabs.
+Used to configure whether the page is hidden in the tab. When hidden, the page will not be displayed in the tab.
 
 ### hideInBreadcrumb
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
-Used to configure whether the page is hidden in the breadcrumb. If hidden, the page will not be displayed in the breadcrumb.
+Used to configure whether the page is hidden in the breadcrumb. When hidden, the page will not be displayed in the breadcrumb.
 
 ### hideChildrenInMenu
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
-Used to configure whether the child pages of the page are hidden in the menu. If hidden, the child pages will not be displayed in the menu.
+Used to configure whether the subpages of the page are hidden in the menu. When hidden, the subpages will not be displayed in the menu.
 
 ### authority
 
 - Type: `string[]`
-- Default value: `[]`
+- Default: `[]`
 
-Used to configure the page's permissions. Only users with corresponding permissions can access the page. If not configured, no permissions are required.
+Used to configure the permissions of the page. Only users with the corresponding permissions can access the page. If not configured, no permissions are required.
 
 ### badge
 
 - Type: `string`
-- Default value: `''`
+- Default: `''`
 
-Used to configure the page's badge, which will be displayed in the menu.
+Used to configure the badge of the page, which will be displayed in the menu.
 
 ### badgeType
 
 - Type: `'dot' | 'normal'`
-- Default value: `'normal'`
+- Default: `'normal'`
 
-Used to configure the type of the page's badge. `dot` is a small red dot, `normal` is text.
+Used to configure the badge type of the page. `dot` is a small red dot, `normal` is text.
 
 ### badgeVariants
 
 - Type: `'default' | 'destructive' | 'primary' | 'success' | 'warning' | string`
-- Default value: `'success'`
+- Default: `'success'`
 
-Used to configure the color of the page's badge.
+Used to configure the badge color of the page.
 
 ### activePath
 
 - Type: `string`
-- Default value: `''`
+- Default: `''`
 
-Used to configure the currently active menu. Sometimes when the page is not displayed in the menu, it is used to activate the parent menu.
+Used to configure the currently active menu. Sometimes the page is not displayed in the menu, and this is used to activate the parent menu.
 
 ### affixTab
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
-Used to configure whether the page tab is pinned. Once pinned, the page cannot be closed.
+Used to configure whether the page is fixed in the tab. When fixed, the page cannot be closed.
 
 ### affixTabOrder
 
 - Type: `number`
-- Default value: `0`
+- Default: `0`
 
-Used to configure the order of the pinned page tabs, sorted in ascending order.
+Used to configure the order of fixed tabs, sorted in ascending order.
 
 ### iframeSrc
 
 - Type: `string`
-- Default value: `''`
+- Default: `''`
 
-Used to configure the `iframe` address of the embedded page. Once set, the corresponding page will be embedded in the current page.
+Used to configure the `iframe` address of the embedded page. When set, the corresponding page will be embedded in the current page.
 
 ### ignoreAccess
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
 Used to configure whether the page ignores permissions and can be accessed directly.
 
 ### link
 
 - Type: `string`
-- Default value: `''`
+- Default: `''`
 
-Used to configure the external link jump path, which will be opened in a new window.
+Used to configure the external link jump path, which will open in a new window.
 
 ### maxNumOfOpenTab
 
 - Type: `number`
-- Default value: `-1`
+- Default: `-1`
 
-Used to configure the maximum number of open tabs. Once set, the earliest opened tab will be automatically closed when a new tab is opened (only effective when opening tabs with the same name).
+Used to configure the maximum number of open tabs. When set, the earliest opened tab will be automatically closed when opening a new tab (only effective when opening tabs with the same name).
 
 ### menuVisibleWithForbidden
 
 - Type: `boolean`
-- Default value: `false`
+- Default: `false`
 
 Used to configure whether the page can be seen in the menu, but access will be redirected to 403.
 
+### openInNewWindow
+
+- Type: `boolean`
+- Default: `false`
+
+When set to `true`, the page will open in a new window.
+
 ### order
 
 - Type: `number`
-- Default value: `0`
+- Default: `0`
+
+Used to configure the sorting of the page, used for route to menu sorting.
+
+_Note:_ Sorting is only effective for first-level menus. The sorting of second-level menus needs to be set in the corresponding first-level menu in code order.
+
+### query
+
+- Type: `Recordable`
+- Default: `{}`
 
-Used to configure the page's order, for routing to menu sorting.
+Used to configure the menu parameters of the page, which will be passed to the page in the menu.
 
 ## Route Refresh
 
-The way to refresh the route is as follows:
+The route refresh method is as follows:
 
 ```vue
 <script setup lang="ts">

+ 4 - 6
docs/src/en/guide/introduction/vben.md

@@ -18,7 +18,7 @@
 - **Permission Validation**: Comprehensive permission validation solutions, including button-level permission control.
 - **Multi-Theme**: Built-in multiple theme configurations & dark mode to meet personalized needs.
 - **Dynamic Menu**: Supports dynamic menus that can display based on permissions.
-- **Mock Data**: High-performance local Mock data solution based on Nitro.
+- **Mock Data**: High-performance local Mock data solution based on `Nitro`.
 - **Rich Components**: Provides a wide range of components to meet most business needs.
 - **Standardization**: Code quality is ensured with tools like `ESLint`, `Prettier`, `Stylelint`, `Publint`, and `CSpell`.
 - **Engineering**: Development efficiency is improved with tools like `Pnpm Monorepo`, `TurboRepo`, and `Changeset`.
@@ -26,9 +26,9 @@
 
 ## Browser Support
 
-**Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
+- **Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
 
-**Production environment** supports modern browsers, IE is not supported.
+- **Production environment** supports modern browsers, IE is not supported.
 
 | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px"  />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
 | :-: | :-: | :-: | :-: | :-: |
@@ -37,12 +37,10 @@
 ## Contribution
 
 - [Vben Admin](https://github.com/vbenjs/vue-vben-admin) is still being actively updated. Contributions are welcome to help maintain and improve the project, aiming to create a better mid- to backend solution.
-- If you wish to join us, you can provide suggestions or submit pull requests. We will invite you to join based on your activity.
+- If you wish to join us, you can start by contributing in the following ways, and we will invite you to join based on your activity.
 
 ::: info Join Us
 
-If you wish to join us, you can start by contributing in the following ways, and we will invite you to join based on your activity:
-
 - Regularly submit `PRs`.
 - Provide valuable suggestions.
 - Participate in discussions and help resolve some `issues`.

+ 20 - 2
docs/src/guide/essentials/route.md

@@ -8,11 +8,29 @@ outline: deep
 
 ## 路由类型
 
-路由分为静态路由和动态路由,静态路由是在项目启动时就已经确定的路由。动态路由一般是在用户登录后,根据用户的权限动态生成的路由。
+路由分为核心路由、静态路由和动态路由,核心路由是框架内置的路由,包含了根路由、登录路由、404路由等;静态路由是在项目启动时就已经确定的路由;动态路由一般是在用户登录后,根据用户的权限动态生成的路由。
+
+静态路由和动态路由都会走权限控制,可以通过配置路由的 `meta` 属性中的 `authority` 字段来控制权限,可以参考[路由权限控制](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/modules/demos.ts)。
+
+### 核心路由
+
+核心路由是框架内置的路由,包含了根路由、登录路由、404路由等,核心路由的配置在应用下 `src/router/routes/core` 目录下
+
+::: tip
+
+核心路由主要用于框架的基础功能,因此不建议将业务相关的路由放在核心路由中,推荐将业务相关的路由放在静态路由或动态路由中。
+
+:::
 
 ### 静态路由
 
-如果你的页面项目不需要权限控制,可以直接使用静态路由,静态路由的配置在应用下 `src/router/routes/index` 目录下,打开注释的文件内容:
+静态路由的配置在应用下 `src/router/routes/index` 目录下,打开注释的文件内容:
+
+::: tip
+
+权限控制是通过路由的 `meta` 属性中的 `authority` 字段来控制的,如果你的页面项目不需要权限控制,可以不设置 `authority` 字段。
+
+:::
 
 ```ts
 // 有需要可以自行打开注释,并创建文件夹

+ 6 - 6
docs/src/guide/introduction/vben.md

@@ -18,7 +18,7 @@
 - **权限验证**:完善的权限验证方案,按钮级别权限控制。
 - **多主题**:内置多种主题配置和黑暗模式,满足个性化需求。
 - **动态菜单**:支持动态菜单,可以根据权限配置显示菜单。
-- **Mock 数据**:基于 Nitro 的本地高性能 Mock 数据方案。
+- **Mock 数据**:基于 `Nitro` 的本地高性能 Mock 数据方案。
 - **组件丰富**:提供了丰富的组件,可以满足大部分的业务需求。
 - **规范**:代码规范,使用 `ESLint`、`Prettier`、`Stylelint`、`Publint`、`CSpell` 等工具保证代码质量。
 - **工程化**:使用 `Pnpm Monorepo`、`TurboRepo`、`Changeset` 等工具,提高开发效率。
@@ -26,9 +26,9 @@
 
 ## 浏览器支持
 
-**本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
+- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
 
-**生产环境**支持现代浏览器,不支持 IE。
+- **生产环境**支持现代浏览器,不支持 IE。
 
 | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px"  />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
 | :-: | :-: | :-: | :-: | :-: |
@@ -37,13 +37,13 @@
 ## 贡献
 
 - [Vben Admin](https://github.com/vbenjs/vue-vben-admin) 还在持续更新中,本项目欢迎您的参与,共同维护,逐步完善,打造更好的中后台解决方案。
-- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,我们会根据你的活跃度邀请你加入。
+- 如果你有兴趣加入我们,可以通过以下方式开始,我们会根据你的活跃度邀请你加入。
 
 ::: info 加入我们
 
 - 长期提交 `PR`。
-- 提供一些好的建议。
-- 参与讨论,帮助解决一些 `issue`。
+- 提供有价值的建议。
+- 参与讨论,帮助解决 `issue`。
 - 共同维护文档。
 
 :::

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/commitlint-config",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "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/typescript.ts

@@ -22,7 +22,7 @@ export async function typescript(): Promise<Linter.Config[]> {
           ecmaVersion: 'latest',
           extraFileExtensions: ['.vue'],
           jsxPragma: 'React',
-          project: './tsconfig.*?.json',
+          project: './tsconfig.*.json',
           sourceType: 'module',
         },
       },

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

@@ -18,6 +18,7 @@ export async function unicorn(): Promise<Linter.Config[]> {
         'unicorn/better-regex': 'off',
         'unicorn/consistent-destructuring': 'off',
         'unicorn/consistent-function-scoping': 'off',
+        'unicorn/expiring-todo-comments': 'off',
         'unicorn/filename-case': 'off',
         'unicorn/import-style': 'off',
         'unicorn/no-array-for-each': 'off',

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

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

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

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

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

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vben-admin-monorepo",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "private": true,
   "keywords": [
     "monorepo",
@@ -99,7 +99,7 @@
     "node": ">=20.10.0",
     "pnpm": ">=9.12.0"
   },
-  "packageManager": "pnpm@9.12.3",
+  "packageManager": "pnpm@9.14.4",
   "pnpm": {
     "peerDependencyRules": {
       "allowedVersions": {

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

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

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

@@ -58,6 +58,8 @@
 
   /* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
   --accent: 216 5% 19%;
+  --accent-dark: 240 0% 22%;
+  --accent-darker: 240 0% 26%;
   --accent-lighter: 216 5% 12%;
   --accent-hover: 216 5% 24%;
   --accent-foreground: 0 0% 98%;

+ 2 - 0
packages/@core/base/design/src/design-tokens/default.css

@@ -58,6 +58,8 @@
 
   /* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
   --accent: 240 5% 96%;
+  --accent-dark: 216 14% 93%;
+  --accent-darker: 216 11% 91%;
   --accent-lighter: 240 0% 98%;
   --accent-hover: 200deg 10% 90%;
   --accent-foreground: 240 6% 10%;

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

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/shared",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {
@@ -81,13 +81,11 @@
   "dependencies": {
     "@ctrl/tinycolor": "catalog:",
     "@tanstack/vue-store": "catalog:",
-    "@types/lodash.get": "catalog:",
     "@vue/shared": "catalog:",
     "clsx": "catalog:",
     "dayjs": "catalog:",
     "defu": "catalog:",
     "lodash.clonedeep": "catalog:",
-    "lodash.get": "catalog:",
     "nprogress": "catalog:",
     "tailwind-merge": "catalog:",
     "theme-colors": "catalog:"

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

@@ -64,8 +64,6 @@ export async function downloadFileFromImageUrl({
 
 /**
  * 通过 Blob 下载文件
- * @param blob - 文件的 Blob 对象
- * @param fileName - 可选,下载的文件名称
  */
 export function downloadFileFromBlob({
   fileName = DEFAULT_FILENAME,
@@ -81,8 +79,6 @@ export function downloadFileFromBlob({
 
 /**
  * 下载文件,支持 Blob、字符串和其他 BlobPart 类型
- * @param data - 文件的 BlobPart 数据
- * @param fileName - 下载的文件名称
  */
 export function downloadFileFromBlobPart({
   fileName = DEFAULT_FILENAME,

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/typings",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "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.4.6",
+  "version": "5.4.8",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

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

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

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

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

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

@@ -91,6 +91,11 @@ function handleRangeTimeValue(values: Record<string, any>) {
 
   fieldMappingTime.forEach(
     ([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
+      if (startTimeKey && endTimeKey && values[field] === null) {
+        delete values[startTimeKey];
+        delete values[endTimeKey];
+      }
+
       if (!values[field]) {
         delete values[field];
         return;

+ 14 - 5
packages/@core/ui-kit/form-ui/src/form-api.ts

@@ -13,13 +13,13 @@ import { toRaw } from 'vue';
 import { Store } from '@vben-core/shared/store';
 import {
   bindMethods,
+  createMerge,
   isFunction,
+  isObject,
   mergeWithArrayOverride,
   StateHandler,
 } from '@vben-core/shared/utils';
 
-import { objectPick } from '@vueuse/core';
-
 function getDefaultState(): VbenFormProps {
   return {
     actionWrapperClass: '',
@@ -185,7 +185,7 @@ export class FormApi {
     const fieldSet = new Set(fields);
     const schema = this.state?.schema ?? [];
 
-    const filterSchema = schema.filter((item) => fieldSet.has(item.fieldName));
+    const filterSchema = schema.filter((item) => !fieldSet.has(item.fieldName));
 
     this.setState({
       schema: filterSchema,
@@ -250,8 +250,17 @@ export class FormApi {
       form.setValues(fields, shouldValidate);
       return;
     }
-    const fieldNames = this.state?.schema?.map((item) => item.fieldName) ?? [];
-    const filteredFields = objectPick(fields, fieldNames);
+
+    const fieldMergeFn = createMerge((obj, key, value) => {
+      if (key in obj) {
+        obj[key] =
+          !Array.isArray(obj[key]) && isObject(obj[key])
+            ? fieldMergeFn(obj[key], value)
+            : value;
+      }
+      return true;
+    });
+    const filteredFields = fieldMergeFn(fields, form.values);
     form.setValues(filteredFields, shouldValidate);
   }
 

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

@@ -209,8 +209,9 @@ function fieldBindEvent(slotProps: Record<string, any>) {
   if (modelValue && isObject(modelValue) && bindEventField) {
     value = isEventObjectLike(modelValue)
       ? modelValue?.target?.[bindEventField]
-      : modelValue;
+      : (modelValue?.[bindEventField] ?? modelValue);
   }
+
   if (bindEventField) {
     return {
       [`onUpdate:${bindEventField}`]: handler,
@@ -223,6 +224,7 @@ function fieldBindEvent(slotProps: Record<string, any>) {
             if (!shouldUnwrap) {
               return onChange?.(e);
             }
+
             return onChange?.(e?.target?.[bindEventField] ?? e);
           },
       onInput: () => {},
@@ -238,6 +240,12 @@ function createComponentProps(slotProps: Record<string, any>) {
     ...slotProps.componentField,
     ...computedProps.value,
     ...bindEvents,
+    ...(Reflect.has(computedProps.value, 'onChange')
+      ? { onChange: computedProps.value.onChange }
+      : {}),
+    ...(Reflect.has(computedProps.value, 'onInput')
+      ? { onInput: computedProps.value.onInput }
+      : {}),
   };
 
   return binds;

+ 7 - 3
packages/@core/ui-kit/form-ui/src/vben-use-form.vue

@@ -40,6 +40,13 @@ const handleUpdateCollapsed = (value: boolean) => {
 };
 
 function handleKeyDownEnter(event: KeyboardEvent) {
+  if (
+    !state.value.submitOnEnter ||
+    !formActionsRef.value ||
+    !formActionsRef.value.handleSubmit
+  ) {
+    return;
+  }
   // 如果是 textarea 不阻止默认行为,否则会导致无法换行。
   // 跳过 textarea 的回车提交处理
   if (event.target instanceof HTMLTextAreaElement) {
@@ -47,9 +54,6 @@ function handleKeyDownEnter(event: KeyboardEvent) {
   }
   event.preventDefault();
 
-  if (!state.value.submitOnEnter || !formActionsRef.value) {
-    return;
-  }
   formActionsRef.value?.handleSubmit?.();
 }
 </script>

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

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

+ 4 - 1
packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue

@@ -191,7 +191,10 @@ watchEffect(() => {
 function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
   const { extraWidth, fixedExtra, isSidebarMixed, show, width } = props;
 
-  let widthValue = `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
+  let widthValue =
+    width === 0
+      ? '0px'
+      : `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
 
   const { collapseWidth } = props;
 

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

@@ -192,7 +192,7 @@ const headerFixed = computed(() => {
 });
 
 const showSidebar = computed(() => {
-  return isSideMode.value && sidebarEnable.value;
+  return isSideMode.value && sidebarEnable.value && !props.sidebarHidden;
 });
 
 /**

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

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

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

@@ -41,6 +41,7 @@ export class DrawerApi {
       loading: false,
       modal: true,
       openAutoFocus: false,
+      placement: 'right',
       showCancelButton: true,
       showConfirmButton: true,
       title: '',

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

@@ -4,6 +4,8 @@ import type { DrawerApi } from './drawer-api';
 
 import type { Component, Ref } from 'vue';
 
+export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
+
 export interface DrawerProps {
   /**
    * 取消按钮文字
@@ -72,6 +74,12 @@ export interface DrawerProps {
    * 是否自动聚焦
    */
   openAutoFocus?: boolean;
+
+  /**
+   * 抽屉位置
+   * @default right
+   */
+  placement?: DrawerPlacement;
   /**
    * 是否显示取消按钮
    * @default true

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

@@ -62,6 +62,7 @@ const {
   loading: showLoading,
   modal,
   openAutoFocus,
+  placement,
   showCancelButton,
   showConfirmButton,
   title,
@@ -119,11 +120,13 @@ function handleFocusOutside(e: Event) {
     <SheetContent
       :class="
         cn('flex w-[520px] flex-col', drawerClass, {
-          '!w-full': isMobile,
+          '!w-full': isMobile || placement === 'bottom' || placement === 'top',
+          'max-h-[100vh]': placement === 'bottom' || placement === 'top',
         })
       "
       :modal="modal"
       :open="state?.isOpen"
+      :side="placement"
       @close-auto-focus="handleFocusOutside"
       @escape-key-down="escapeKeyDown"
       @focus-outside="handleFocusOutside"
@@ -178,7 +181,12 @@ function handleFocusOutside(e: Event) {
           </SheetClose>
         </div>
       </SheetHeader>
-
+      <template v-else>
+        <VisuallyHidden>
+          <SheetTitle />
+          <SheetDescription />
+        </VisuallyHidden>
+      </template>
       <div
         ref="wrapperRef"
         :class="

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

@@ -41,6 +41,7 @@ export class ModalApi {
       class: '',
       closeOnClickModal: true,
       closeOnPressEscape: true,
+      confirmDisabled: false,
       confirmLoading: false,
       contentClass: '',
       draggable: false,

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

@@ -35,6 +35,10 @@ export interface ModalProps {
    * @default true
    */
   closeOnPressEscape?: boolean;
+  /**
+   * 禁用确认按钮
+   */
+  confirmDisabled?: boolean;
   /**
    * 确定按钮 loading
    * @default false

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

@@ -59,6 +59,7 @@ const {
   closable,
   closeOnClickModal,
   closeOnPressEscape,
+  confirmDisabled,
   confirmLoading,
   confirmText,
   contentClass,
@@ -235,7 +236,7 @@ function handleFocusOutside(e: Event) {
         ref="wrapperRef"
         :class="
           cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {
-            'overflow-hidden': showLoading,
+            'pointer-events-none overflow-hidden': showLoading,
           })
         "
       >
@@ -285,6 +286,7 @@ function handleFocusOutside(e: Event) {
           <component
             :is="components.PrimaryButton || VbenButton"
             v-if="showConfirmButton"
+            :disabled="confirmDisabled"
             :loading="confirmLoading"
             @click="() => modalApi?.onConfirm()"
           >

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

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

+ 2 - 2
packages/@core/ui-kit/shadcn-ui/src/components/spine-text/spine-text.vue

@@ -32,8 +32,8 @@ const style = computed(() => {
 
 .dark .vben-spine-text {
   background:
-    radial-gradient(circle at center, rgb(24 24 26 / 80%), transparent) -200% 50% /
-      200% 100% no-repeat,
+    radial-gradient(circle at center, rgb(24 24 26 / 80%), transparent) -200%
+      50% / 200% 100% no-repeat,
     #f4f4f4;
 }
 

+ 8 - 5
packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

@@ -48,11 +48,14 @@ const delegatedProps = computed(() => {
 const forwarded = useForwardPropsEmits(delegatedProps, emits);
 
 const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
-function onAnimationEnd() {
-  if (props.open) {
-    emits('opened');
-  } else {
-    emits('closed');
+function onAnimationEnd(event: AnimationEvent) {
+  // 只有在 contentRef 的动画结束时才触发 opened/closed 事件
+  if (event.target === contentRef.value?.$el) {
+    if (props.open) {
+      emits('opened');
+    } else {
+      emits('closed');
+    }
   }
 }
 defineExpose({

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

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

+ 1 - 1
packages/constants/package.json

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

+ 1 - 1
packages/effects/access/package.json

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

+ 1 - 1
packages/effects/common-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/common-ui",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 3 - 2
packages/effects/common-ui/src/components/icon-picker/icon-picker.vue

@@ -41,6 +41,7 @@ const emit = defineEmits<{
 const refTrigger = useTemplateRef<HTMLElement>('refTrigger');
 const currentSelect = ref('');
 const currentList = ref(props.icons);
+const currentPage = ref(1);
 
 watch(
   () => props.icons,
@@ -72,6 +73,7 @@ const handleClick = (icon: string) => {
 };
 
 const handlePageChange = (page: number) => {
+  currentPage.value = page;
   setCurrentPage(page);
 };
 
@@ -114,7 +116,6 @@ defineExpose({ changeOpenState });
         class="flex-center flex justify-end overflow-hidden border-t py-2 pr-3"
       >
         <Pagination
-          v-slot="{ page }"
           :items-per-page="36"
           :sibling-count="1"
           :total="total"
@@ -136,7 +137,7 @@ defineExpose({ changeOpenState });
                 as-child
               >
                 <Button
-                  :variant="item.value === page ? 'default' : 'outline'"
+                  :variant="item.value === currentPage ? 'default' : 'outline'"
                   class="size-5 p-0 text-sm"
                 >
                   {{ item.value }}

+ 1 - 0
packages/effects/common-ui/src/components/index.ts

@@ -2,6 +2,7 @@ export * from './captcha';
 export * from './ellipsis-text';
 export * from './icon-picker';
 export * from './page';
+export * from './resize';
 export * from '@vben-core/form-ui';
 export * from '@vben-core/popup-ui';
 

+ 1 - 0
packages/effects/common-ui/src/components/resize/index.ts

@@ -0,0 +1 @@
+export { default as VResize } from './resize.vue';

+ 1122 - 0
packages/effects/common-ui/src/components/resize/resize.vue

@@ -0,0 +1,1122 @@
+<script lang="ts" setup>
+/**
+ * This components is refactored from vue-drag-resize: https://github.com/kirillmurashov/vue-drag-resize
+ */
+
+import {
+  computed,
+  getCurrentInstance,
+  nextTick,
+  onBeforeUnmount,
+  onMounted,
+  ref,
+  toRefs,
+  watch,
+} from 'vue';
+
+const props = defineProps({
+  stickSize: {
+    type: Number,
+    default: 8,
+  },
+  parentScaleX: {
+    type: Number,
+    default: 1,
+  },
+  parentScaleY: {
+    type: Number,
+    default: 1,
+  },
+  isActive: {
+    type: Boolean,
+    default: false,
+  },
+  preventActiveBehavior: {
+    type: Boolean,
+    default: false,
+  },
+  isDraggable: {
+    type: Boolean,
+    default: true,
+  },
+  isResizable: {
+    type: Boolean,
+    default: true,
+  },
+  aspectRatio: {
+    type: Boolean,
+    default: false,
+  },
+  parentLimitation: {
+    type: Boolean,
+    default: false,
+  },
+  snapToGrid: {
+    type: Boolean,
+    default: false,
+  },
+  gridX: {
+    type: Number,
+    default: 50,
+    validator(val: number) {
+      return val >= 0;
+    },
+  },
+  gridY: {
+    type: Number,
+    default: 50,
+    validator(val: number) {
+      return val >= 0;
+    },
+  },
+  parentW: {
+    type: Number,
+    default: 0,
+    validator(val: number) {
+      return val >= 0;
+    },
+  },
+  parentH: {
+    type: Number,
+    default: 0,
+    validator(val: number) {
+      return val >= 0;
+    },
+  },
+  w: {
+    type: [String, Number],
+    default: 200,
+    validator(val: number) {
+      return typeof val === 'string' ? val === 'auto' : val >= 0;
+    },
+  },
+  h: {
+    type: [String, Number],
+    default: 200,
+    validator(val: number) {
+      return typeof val === 'string' ? val === 'auto' : val >= 0;
+    },
+  },
+  minw: {
+    type: Number,
+    default: 50,
+    validator(val: number) {
+      return val >= 0;
+    },
+  },
+  minh: {
+    type: Number,
+    default: 50,
+    validator(val: number) {
+      return val >= 0;
+    },
+  },
+  x: {
+    type: Number,
+    default: 0,
+    validator(val: number) {
+      return typeof val === 'number';
+    },
+  },
+  y: {
+    type: Number,
+    default: 0,
+    validator(val: number) {
+      return typeof val === 'number';
+    },
+  },
+  z: {
+    type: [String, Number],
+    default: 'auto',
+    validator(val: number) {
+      return typeof val === 'string' ? val === 'auto' : val >= 0;
+    },
+  },
+  dragHandle: {
+    type: String,
+    default: null,
+  },
+  dragCancel: {
+    type: String,
+    default: null,
+  },
+  sticks: {
+    type: Array<'bl' | 'bm' | 'br' | 'ml' | 'mr' | 'tl' | 'tm' | 'tr'>,
+    default() {
+      return ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'];
+    },
+  },
+  axis: {
+    type: String,
+    default: 'both',
+    validator(val: string) {
+      return ['both', 'none', 'x', 'y'].includes(val);
+    },
+  },
+  contentClass: {
+    type: String,
+    required: false,
+    default: '',
+  },
+});
+
+const emit = defineEmits([
+  'clicked',
+  'dragging',
+  'dragstop',
+  'resizing',
+  'resizestop',
+  'activated',
+  'deactivated',
+]);
+
+const styleMapping = {
+  y: {
+    t: 'top',
+    m: 'marginTop',
+    b: 'bottom',
+  },
+  x: {
+    l: 'left',
+    m: 'marginLeft',
+    r: 'right',
+  },
+};
+
+function addEvents(events: Map<string, (...args: any[]) => void>) {
+  events.forEach((cb, eventName) => {
+    document.documentElement.addEventListener(eventName, cb);
+  });
+}
+
+function removeEvents(events: Map<string, (...args: any[]) => void>) {
+  events.forEach((cb, eventName) => {
+    document.documentElement.removeEventListener(eventName, cb);
+  });
+}
+
+const {
+  stickSize,
+  parentScaleX,
+  parentScaleY,
+  isActive,
+  preventActiveBehavior,
+  isDraggable,
+  isResizable,
+  aspectRatio,
+  parentLimitation,
+  snapToGrid,
+  gridX,
+  gridY,
+  parentW,
+  parentH,
+  w,
+  h,
+  minw,
+  minh,
+  x,
+  y,
+  z,
+  dragHandle,
+  dragCancel,
+  sticks,
+  axis,
+  contentClass,
+} = toRefs(props);
+
+// states
+const active = ref(false);
+const zIndex = ref<null | number>(null);
+const parentWidth = ref<null | number>(null);
+const parentHeight = ref<null | number>(null);
+const left = ref<null | number>(null);
+const top = ref<null | number>(null);
+const right = ref<null | number>(null);
+const bottom = ref<null | number>(null);
+
+const aspectFactor = ref<null | number>(null);
+
+// state end
+
+const stickDrag = ref(false);
+const bodyDrag = ref(false);
+const dimensionsBeforeMove = ref({
+  pointerX: 0,
+  pointerY: 0,
+  x: 0,
+  y: 0,
+  w: 0,
+  h: 0,
+  top: 0,
+  right: 0,
+  bottom: 0,
+  left: 0,
+  width: 0,
+  height: 0,
+});
+const limits = ref({
+  left: { min: null as null | number, max: null as null | number },
+  right: { min: null as null | number, max: null as null | number },
+  top: { min: null as null | number, max: null as null | number },
+  bottom: { min: null as null | number, max: null as null | number },
+});
+const currentStick = ref<null | string>(null);
+
+const parentElement = ref<HTMLElement | null>(null);
+
+const width = computed(() => parentWidth.value! - left.value! - right.value!);
+
+const height = computed(() => parentHeight.value! - top.value! - bottom.value!);
+
+const rect = computed(() => ({
+  left: Math.round(left.value!),
+  top: Math.round(top.value!),
+  width: Math.round(width.value),
+  height: Math.round(height.value),
+}));
+
+const saveDimensionsBeforeMove = ({
+  pointerX,
+  pointerY,
+}: {
+  pointerX: number;
+  pointerY: number;
+}) => {
+  dimensionsBeforeMove.value.pointerX = pointerX;
+  dimensionsBeforeMove.value.pointerY = pointerY;
+
+  dimensionsBeforeMove.value.left = left.value as number;
+  dimensionsBeforeMove.value.right = right.value as number;
+  dimensionsBeforeMove.value.top = top.value as number;
+  dimensionsBeforeMove.value.bottom = bottom.value as number;
+
+  dimensionsBeforeMove.value.width = width.value as number;
+  dimensionsBeforeMove.value.height = height.value as number;
+
+  aspectFactor.value = width.value / height.value;
+};
+
+const sideCorrectionByLimit = (
+  limit: { max: number; min: number },
+  current: number,
+) => {
+  let value = current;
+
+  if (limit.min !== null && current < limit.min) {
+    value = limit.min;
+  } else if (limit.max !== null && limit.max < current) {
+    value = limit.max;
+  }
+
+  return value;
+};
+
+const rectCorrectionByLimit = (rect: {
+  newBottom: number;
+  newLeft: number;
+  newRight: number;
+  newTop: number;
+}) => {
+  // const { limits } = this;
+  let { newRight, newLeft, newBottom, newTop } = rect;
+
+  type RectRange = {
+    max: number;
+    min: number;
+  };
+
+  newLeft = sideCorrectionByLimit(limits.value.left as RectRange, newLeft);
+  newRight = sideCorrectionByLimit(limits.value.right as RectRange, newRight);
+  newTop = sideCorrectionByLimit(limits.value.top as RectRange, newTop);
+  newBottom = sideCorrectionByLimit(
+    limits.value.bottom as RectRange,
+    newBottom,
+  );
+
+  return {
+    newLeft,
+    newRight,
+    newTop,
+    newBottom,
+  };
+};
+
+const rectCorrectionByAspectRatio = (rect: {
+  newBottom: number;
+  newLeft: number;
+  newRight: number;
+  newTop: number;
+}) => {
+  let { newLeft, newRight, newTop, newBottom } = rect;
+  // const { parentWidth, parentHeight, currentStick, aspectFactor, dimensionsBeforeMove } = this;
+
+  let newWidth = parentWidth.value! - newLeft - newRight;
+  let newHeight = parentHeight.value! - newTop - newBottom;
+
+  if (currentStick.value![1] === 'm') {
+    const deltaHeight = newHeight - dimensionsBeforeMove.value.height;
+
+    newLeft -= (deltaHeight * aspectFactor.value!) / 2;
+    newRight -= (deltaHeight * aspectFactor.value!) / 2;
+  } else if (currentStick.value![0] === 'm') {
+    const deltaWidth = newWidth - dimensionsBeforeMove.value.width;
+
+    newTop -= deltaWidth / aspectFactor.value! / 2;
+    newBottom -= deltaWidth / aspectFactor.value! / 2;
+  } else if (newWidth / newHeight > aspectFactor.value!) {
+    newWidth = aspectFactor.value! * newHeight;
+
+    if (currentStick.value![1] === 'l') {
+      newLeft = parentWidth.value! - newRight - newWidth;
+    } else {
+      newRight = parentWidth.value! - newLeft - newWidth;
+    }
+  } else {
+    newHeight = newWidth / aspectFactor.value!;
+
+    if (currentStick.value![0] === 't') {
+      newTop = parentHeight.value! - newBottom - newHeight;
+    } else {
+      newBottom = parentHeight.value! - newTop - newHeight;
+    }
+  }
+
+  return { newLeft, newRight, newTop, newBottom };
+};
+
+const stickMove = (delta: { x: number; y: number }) => {
+  let newTop = dimensionsBeforeMove.value.top;
+  let newBottom = dimensionsBeforeMove.value.bottom;
+  let newLeft = dimensionsBeforeMove.value.left;
+  let newRight = dimensionsBeforeMove.value.right;
+  switch (currentStick.value![0]) {
+    case 'b': {
+      newBottom = dimensionsBeforeMove.value.bottom + delta.y;
+
+      if (snapToGrid.value) {
+        newBottom =
+          (parentHeight.value as number) -
+          Math.round(
+            ((parentHeight.value as number) - newBottom) / gridY.value,
+          ) *
+            gridY.value;
+      }
+
+      break;
+    }
+
+    case 't': {
+      newTop = dimensionsBeforeMove.value.top - delta.y;
+
+      if (snapToGrid.value) {
+        newTop = Math.round(newTop / gridY.value) * gridY.value;
+      }
+
+      break;
+    }
+    default: {
+      break;
+    }
+  }
+
+  switch (currentStick.value![1]) {
+    case 'l': {
+      newLeft = dimensionsBeforeMove.value.left - delta.x;
+
+      if (snapToGrid.value) {
+        newLeft = Math.round(newLeft / gridX.value) * gridX.value;
+      }
+
+      break;
+    }
+
+    case 'r': {
+      newRight = dimensionsBeforeMove.value.right + delta.x;
+
+      if (snapToGrid.value) {
+        newRight =
+          (parentWidth.value as number) -
+          Math.round(((parentWidth.value as number) - newRight) / gridX.value) *
+            gridX.value;
+      }
+
+      break;
+    }
+    default: {
+      break;
+    }
+  }
+
+  ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByLimit({
+    newLeft,
+    newRight,
+    newTop,
+    newBottom,
+  }));
+
+  if (aspectRatio.value) {
+    ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByAspectRatio({
+      newLeft,
+      newRight,
+      newTop,
+      newBottom,
+    }));
+  }
+
+  left.value = newLeft;
+  right.value = newRight;
+  top.value = newTop;
+  bottom.value = newBottom;
+
+  emit('resizing', rect.value);
+};
+
+const stickUp = () => {
+  stickDrag.value = false;
+  // dimensionsBeforeMove.value = {
+  //   pointerX: 0,
+  //   pointerY: 0,
+  //   x: 0,
+  //   y: 0,
+  //   w: 0,
+  //   h: 0,
+  // };
+
+  Object.assign(dimensionsBeforeMove.value, {
+    pointerX: 0,
+    pointerY: 0,
+    x: 0,
+    y: 0,
+    w: 0,
+    h: 0,
+  });
+
+  limits.value = {
+    left: { min: null, max: null },
+    right: { min: null, max: null },
+    top: { min: null, max: null },
+    bottom: { min: null, max: null },
+  };
+
+  emit('resizing', rect.value);
+  emit('resizestop', rect.value);
+};
+
+const calcDragLimitation = () => {
+  return {
+    left: { min: 0, max: (parentWidth.value as number) - width.value },
+    right: { min: 0, max: (parentWidth.value as number) - width.value },
+    top: { min: 0, max: (parentHeight.value as number) - height.value },
+    bottom: { min: 0, max: (parentHeight.value as number) - height.value },
+  };
+};
+
+const calcResizeLimits = () => {
+  // const { aspectFactor, width, height, bottom, top, left, right } = this;
+
+  const parentLim = parentLimitation.value ? 0 : null;
+
+  if (aspectRatio.value) {
+    if (minw.value / minh.value > (aspectFactor.value as number)) {
+      minh.value = minw.value / (aspectFactor.value as number);
+    } else {
+      minw.value = ((aspectFactor.value as number) * minh.value) as number;
+    }
+  }
+
+  const limits = {
+    left: {
+      min: parentLim,
+      max: (left.value as number) + (width.value - minw.value),
+    },
+    right: {
+      min: parentLim,
+      max: (right.value as number) + (width.value - minw.value),
+    },
+    top: {
+      min: parentLim,
+      max: (top.value as number) + (height.value - minh.value),
+    },
+    bottom: {
+      min: parentLim,
+      max: (bottom.value as number) + (height.value - minh.value),
+    },
+  };
+
+  if (aspectRatio.value) {
+    const aspectLimits = {
+      left: {
+        min:
+          left.value! -
+          Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,
+        max:
+          left.value! +
+          ((height.value - minh.value!) / 2) * aspectFactor.value! * 2,
+      },
+      right: {
+        min:
+          right.value! -
+          Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,
+        max:
+          right.value! +
+          ((height.value - minh.value!) / 2) * aspectFactor.value! * 2,
+      },
+      top: {
+        min:
+          top.value! -
+          (Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,
+        max:
+          top.value! +
+          ((width.value - minw.value) / 2 / aspectFactor.value!) * 2,
+      },
+      bottom: {
+        min:
+          bottom.value! -
+          (Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,
+        max:
+          bottom.value! +
+          ((width.value - minw.value) / 2 / aspectFactor.value!) * 2,
+      },
+    };
+
+    if (currentStick.value![0] === 'm') {
+      limits.left = {
+        min: Math.max(limits.left.min!, aspectLimits.left.min),
+        max: Math.min(limits.left.max, aspectLimits.left.max),
+      };
+      limits.right = {
+        min: Math.max(limits.right.min!, aspectLimits.right.min),
+        max: Math.min(limits.right.max, aspectLimits.right.max),
+      };
+    } else if (currentStick.value![1] === 'm') {
+      limits.top = {
+        min: Math.max(limits.top.min!, aspectLimits.top.min),
+        max: Math.min(limits.top.max, aspectLimits.top.max),
+      };
+      limits.bottom = {
+        min: Math.max(limits.bottom.min!, aspectLimits.bottom.min),
+        max: Math.min(limits.bottom.max, aspectLimits.bottom.max),
+      };
+    }
+  }
+
+  return limits;
+};
+
+const positionStyle = computed(() => ({
+  top: `${top.value}px`,
+  left: `${left.value}px`,
+  zIndex: zIndex.value!,
+}));
+
+const sizeStyle = computed(() => ({
+  width: w.value === 'auto' ? 'auto' : `${width.value}px`,
+  height: h.value === 'auto' ? 'auto' : `${height.value}px`,
+}));
+
+const stickStyles = computed(() => (stick: string) => {
+  const stickStyle = {
+    width: `${stickSize.value / parentScaleX.value}px`,
+    height: `${stickSize.value / parentScaleY.value}px`,
+  };
+  stickStyle[
+    styleMapping.y[stick[0] as 'b' | 'm' | 't'] as 'height' | 'width'
+  ] = `${stickSize.value / parentScaleX.value / -2}px`;
+  stickStyle[
+    styleMapping.x[stick[1] as 'l' | 'm' | 'r'] as 'height' | 'width'
+  ] = `${stickSize.value / parentScaleX.value / -2}px`;
+  return stickStyle;
+});
+
+const bodyMove = (delta: { x: number; y: number }) => {
+  let newTop = dimensionsBeforeMove.value.top - delta.y;
+  let newBottom = dimensionsBeforeMove.value.bottom + delta.y;
+  let newLeft = dimensionsBeforeMove.value.left - delta.x;
+  let newRight = dimensionsBeforeMove.value.right + delta.x;
+
+  if (snapToGrid.value) {
+    let alignTop = true;
+    let alignLeft = true;
+
+    let diffT = newTop - Math.floor(newTop / gridY.value) * gridY.value;
+    let diffB =
+      (parentHeight.value as number) -
+      newBottom -
+      Math.floor(((parentHeight.value as number) - newBottom) / gridY.value) *
+        gridY.value;
+    let diffL = newLeft - Math.floor(newLeft / gridX.value) * gridX.value;
+    let diffR =
+      (parentWidth.value as number) -
+      newRight -
+      Math.floor(((parentWidth.value as number) - newRight) / gridX.value) *
+        gridX.value;
+
+    if (diffT > gridY.value / 2) {
+      diffT -= gridY.value;
+    }
+    if (diffB > gridY.value / 2) {
+      diffB -= gridY.value;
+    }
+    if (diffL > gridX.value / 2) {
+      diffL -= gridX.value;
+    }
+    if (diffR > gridX.value / 2) {
+      diffR -= gridX.value;
+    }
+
+    if (Math.abs(diffB) < Math.abs(diffT)) {
+      alignTop = false;
+    }
+    if (Math.abs(diffR) < Math.abs(diffL)) {
+      alignLeft = false;
+    }
+
+    newTop -= alignTop ? diffT : diffB;
+    newBottom = (parentHeight.value as number) - height.value - newTop;
+    newLeft -= alignLeft ? diffL : diffR;
+    newRight = (parentWidth.value as number) - width.value - newLeft;
+  }
+
+  ({
+    newLeft: left.value,
+    newRight: right.value,
+    newTop: top.value,
+    newBottom: bottom.value,
+  } = rectCorrectionByLimit({ newLeft, newRight, newTop, newBottom }));
+
+  emit('dragging', rect.value);
+};
+
+const bodyUp = () => {
+  bodyDrag.value = false;
+  emit('dragging', rect.value);
+  emit('dragstop', rect.value);
+
+  // dimensionsBeforeMove.value = { pointerX: 0, pointerY: 0, x: 0, y: 0, w: 0, h: 0 };
+  Object.assign(dimensionsBeforeMove.value, {
+    pointerX: 0,
+    pointerY: 0,
+    x: 0,
+    y: 0,
+    w: 0,
+    h: 0,
+  });
+
+  limits.value = {
+    left: { min: null, max: null },
+    right: { min: null, max: null },
+    top: { min: null, max: null },
+    bottom: { min: null, max: null },
+  };
+};
+
+const stickDown = (
+  stick: string,
+  ev: { pageX: any; pageY: any; touches?: any },
+  force = false,
+) => {
+  if ((!isResizable.value || !active.value) && !force) {
+    return;
+  }
+
+  stickDrag.value = true;
+
+  const pointerX = ev.pageX === undefined ? ev.touches[0].pageX : ev.pageX;
+  const pointerY = ev.pageY === undefined ? ev.touches[0].pageY : ev.pageY;
+
+  saveDimensionsBeforeMove({ pointerX, pointerY });
+
+  currentStick.value = stick;
+
+  limits.value = calcResizeLimits();
+};
+
+const move = (ev: MouseEvent & TouchEvent) => {
+  if (!stickDrag.value && !bodyDrag.value) {
+    return;
+  }
+
+  ev.stopPropagation();
+
+  // touches 兼容性代码
+  const pageX = ev.pageX === undefined ? ev.touches![0]!.pageX : ev.pageX;
+  const pageY = ev.pageY === undefined ? ev.touches![0]!.pageY : ev.pageY;
+
+  const delta = {
+    x: (dimensionsBeforeMove.value.pointerX - pageX) / parentScaleX.value,
+    y: (dimensionsBeforeMove.value.pointerY - pageY) / parentScaleY.value,
+  };
+
+  if (stickDrag.value) {
+    stickMove(delta);
+  }
+
+  if (bodyDrag.value) {
+    switch (axis.value) {
+      case 'none': {
+        return;
+      }
+      case 'x': {
+        delta.y = 0;
+
+        break;
+      }
+      case 'y': {
+        delta.x = 0;
+
+        break;
+      }
+      // No default
+    }
+    bodyMove(delta);
+  }
+};
+
+const up = () => {
+  if (stickDrag.value) {
+    stickUp();
+  } else if (bodyDrag.value) {
+    bodyUp();
+  }
+};
+
+const deselect = () => {
+  if (preventActiveBehavior.value) {
+    return;
+  }
+  active.value = false;
+};
+
+const domEvents = ref(
+  new Map([
+    ['mousedown', deselect],
+    ['mouseleave', up],
+    ['mousemove', move],
+    ['mouseup', up],
+    ['touchcancel', up],
+    ['touchend', up],
+    ['touchmove', move],
+    ['touchstart', up],
+  ]),
+);
+
+const container = ref<HTMLDivElement>();
+
+onMounted(() => {
+  const currentInstance = getCurrentInstance();
+  const $el = currentInstance?.vnode.el as HTMLElement;
+
+  parentElement.value = $el?.parentNode as HTMLElement;
+  parentWidth.value = parentW.value ?? parentElement.value?.clientWidth;
+  parentHeight.value = parentH.value ?? parentElement.value?.clientHeight;
+
+  left.value = x.value;
+  top.value = y.value;
+  right.value = (parentWidth.value -
+    (w.value === 'auto' ? container.value!.scrollWidth : (w.value as number)) -
+    left.value) as number;
+  bottom.value = (parentHeight.value -
+    (h.value === 'auto' ? container.value!.scrollHeight : (h.value as number)) -
+    top.value) as number;
+
+  addEvents(domEvents.value);
+
+  if (dragHandle.value) {
+    [...($el?.querySelectorAll(dragHandle.value) || [])].forEach(
+      (dragHandle) => {
+        (dragHandle as HTMLElement).dataset.dragHandle = String(
+          currentInstance?.uid,
+        );
+      },
+    );
+  }
+
+  if (dragCancel.value) {
+    [...($el?.querySelectorAll(dragCancel.value) || [])].forEach(
+      (cancelHandle) => {
+        (cancelHandle as HTMLElement).dataset.dragCancel = String(
+          currentInstance?.uid,
+        );
+      },
+    );
+  }
+});
+
+onBeforeUnmount(() => {
+  removeEvents(domEvents.value);
+});
+
+const bodyDown = (ev: MouseEvent & TouchEvent) => {
+  const { target, button } = ev;
+
+  if (!preventActiveBehavior.value) {
+    active.value = true;
+  }
+
+  if (button && button !== 0) {
+    return;
+  }
+
+  emit('clicked', ev);
+
+  if (!active.value) {
+    return;
+  }
+
+  if (
+    dragHandle.value &&
+    (target! as HTMLElement).dataset.dragHandle !==
+      getCurrentInstance()?.uid.toString()
+  ) {
+    return;
+  }
+
+  if (
+    dragCancel.value &&
+    (target! as HTMLElement).dataset.dragCancel ===
+      getCurrentInstance()?.uid.toString()
+  ) {
+    return;
+  }
+
+  if (ev.stopPropagation !== undefined) {
+    ev.stopPropagation();
+  }
+
+  if (ev.preventDefault !== undefined) {
+    ev.preventDefault();
+  }
+
+  if (isDraggable.value) {
+    bodyDrag.value = true;
+  }
+
+  const pointerX = ev.pageX === undefined ? ev.touches[0]!.pageX : ev.pageX;
+  const pointerY = ev.pageY === undefined ? ev.touches[0]!.pageY : ev.pageY;
+
+  saveDimensionsBeforeMove({ pointerX, pointerY });
+
+  if (parentLimitation.value) {
+    limits.value = calcDragLimitation();
+  }
+};
+
+watch(
+  () => active.value,
+  (isActive) => {
+    if (isActive) {
+      emit('activated');
+    } else {
+      emit('deactivated');
+    }
+  },
+);
+
+watch(
+  () => isActive.value,
+  (val) => {
+    active.value = val;
+  },
+  { immediate: true },
+);
+
+watch(
+  () => z.value,
+  (val) => {
+    if ((val as number) >= 0 || val === 'auto') {
+      zIndex.value = val as number;
+    }
+  },
+  { immediate: true },
+);
+
+watch(
+  () => x.value,
+  (newVal, oldVal) => {
+    if (stickDrag.value || bodyDrag.value || newVal === left.value) {
+      return;
+    }
+
+    const delta = oldVal - newVal;
+
+    bodyDown({ pageX: left.value!, pageY: top.value! } as MouseEvent &
+      TouchEvent);
+    bodyMove({ x: delta, y: 0 });
+
+    nextTick(() => {
+      bodyUp();
+    });
+  },
+);
+
+watch(
+  () => y.value,
+  (newVal, oldVal) => {
+    if (stickDrag.value || bodyDrag.value || newVal === top.value) {
+      return;
+    }
+
+    const delta = oldVal - newVal;
+
+    bodyDown({ pageX: left.value, pageY: top.value } as MouseEvent &
+      TouchEvent);
+    bodyMove({ x: 0, y: delta });
+
+    nextTick(() => {
+      bodyUp();
+    });
+  },
+);
+
+watch(
+  () => w.value,
+  (newVal, oldVal) => {
+    if (stickDrag.value || bodyDrag.value || newVal === width.value) {
+      return;
+    }
+
+    const stick = 'mr';
+    const delta = (oldVal as number) - (newVal as number);
+
+    stickDown(
+      stick,
+      { pageX: right.value, pageY: top.value! + height.value / 2 },
+      true,
+    );
+    stickMove({ x: delta, y: 0 });
+
+    nextTick(() => {
+      stickUp();
+    });
+  },
+);
+
+watch(
+  () => h.value,
+  (newVal, oldVal) => {
+    if (stickDrag.value || bodyDrag.value || newVal === height.value) {
+      return;
+    }
+
+    const stick = 'bm';
+    const delta = (oldVal as number) - (newVal as number);
+
+    stickDown(
+      stick,
+      { pageX: left.value! + width.value / 2, pageY: bottom.value },
+      true,
+    );
+    stickMove({ x: 0, y: delta });
+
+    nextTick(() => {
+      stickUp();
+    });
+  },
+);
+
+watch(
+  () => parentW.value,
+  (val) => {
+    right.value = val - width.value - left.value!;
+    parentWidth.value = val;
+  },
+);
+
+watch(
+  () => parentH.value,
+  (val) => {
+    bottom.value = val - height.value - top.value!;
+    parentHeight.value = val;
+  },
+);
+</script>
+
+<template>
+  <div
+    :class="`${active || isActive ? 'active' : 'inactive'} ${contentClass ? contentClass : ''}`"
+    :style="positionStyle"
+    class="resize"
+    @mousedown="bodyDown($event as TouchEvent & MouseEvent)"
+    @touchend="up"
+    @touchstart="bodyDown($event as TouchEvent & MouseEvent)"
+  >
+    <div ref="container" :style="sizeStyle" class="content-container">
+      <slot></slot>
+    </div>
+    <div
+      v-for="(stick, index) of sticks"
+      :key="index"
+      :class="[`resize-stick-${stick}`, isResizable ? '' : 'not-resizable']"
+      :style="stickStyles(stick)"
+      class="resize-stick"
+      @mousedown.stop.prevent="
+        stickDown(stick, $event as TouchEvent & MouseEvent)
+      "
+      @touchstart.stop.prevent="
+        stickDown(stick, $event as TouchEvent & MouseEvent)
+      "
+    ></div>
+  </div>
+</template>
+
+<style lang="css" scoped>
+.resize {
+  position: absolute;
+  box-sizing: border-box;
+}
+
+.resize.active::before {
+  position: absolute;
+  top: 0;
+  left: 0;
+  box-sizing: border-box;
+  width: 100%;
+  height: 100%;
+  content: '';
+  outline: 1px dashed #d6d6d6;
+}
+
+.resize-stick {
+  position: absolute;
+  box-sizing: border-box;
+  font-size: 1px;
+  background: #fff;
+  border: 1px solid #6c6c6c;
+  box-shadow: 0 0 2px #bbb;
+}
+
+.inactive .resize-stick {
+  display: none;
+}
+
+.resize-stick-tl,
+.resize-stick-br {
+  cursor: nwse-resize;
+}
+
+.resize-stick-tm,
+.resize-stick-bm {
+  left: 50%;
+  cursor: ns-resize;
+}
+
+.resize-stick-tr,
+.resize-stick-bl {
+  cursor: nesw-resize;
+}
+
+.resize-stick-ml,
+.resize-stick-mr {
+  top: 50%;
+  cursor: ew-resize;
+}
+
+.resize-stick.not-resizable {
+  display: none;
+}
+
+.content-container {
+  position: relative;
+  display: block;
+}
+</style>

+ 1 - 1
packages/effects/hooks/package.json

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

+ 3 - 0
packages/effects/hooks/src/use-design-tokens.ts

@@ -260,6 +260,9 @@ export function useElementPlusDesignTokens() {
         '--el-fill-color-light': getCssVariableValue('--accent'),
         '--el-fill-color-lighter': getCssVariableValue('--accent-lighter'),
 
+        '--el-fill-color-dark': getCssVariableValue('--accent-dark'),
+        '--el-fill-color-darker': getCssVariableValue('--accent-darker'),
+
         // 解决ElLoading背景色问题
         '--el-mask-color': isDark.value
           ? 'rgba(0,0,0,.8)'

+ 18 - 7
packages/effects/hooks/src/use-watermark.ts

@@ -1,8 +1,11 @@
 import type { Watermark, WatermarkOptions } from 'watermark-js-plus';
 
-import { nextTick, onUnmounted, ref } from 'vue';
+import { nextTick, onUnmounted, readonly, ref } from 'vue';
+
+import { updatePreferences } from '@vben/preferences';
 
 const watermark = ref<Watermark>();
+const unmountedHooked = ref<boolean>(false);
 const cachedOptions = ref<Partial<WatermarkOptions>>({
   advancedStyle: {
     colorStops: [
@@ -45,7 +48,7 @@ export function useWatermark() {
       ...options,
     };
     watermark.value = new Watermark(cachedOptions.value);
-
+    updatePreferences({ app: { watermark: true } });
     await watermark.value?.create();
   }
 
@@ -62,16 +65,24 @@ export function useWatermark() {
   }
 
   function destroyWatermark() {
-    watermark.value?.destroy();
+    if (watermark.value) {
+      watermark.value.destroy();
+      watermark.value = undefined;
+    }
+    updatePreferences({ app: { watermark: false } });
   }
 
-  onUnmounted(() => {
-    destroyWatermark();
-  });
+  // 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印
+  if (!unmountedHooked.value) {
+    unmountedHooked.value = true;
+    onUnmounted(() => {
+      destroyWatermark();
+    });
+  }
 
   return {
     destroyWatermark,
     updateWatermark,
-    watermark,
+    watermark: readonly(watermark),
   };
 }

+ 1 - 1
packages/effects/layouts/package.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/plugins",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {
@@ -34,7 +34,6 @@
     "@vben/types": "workspace:*",
     "@vben/utils": "workspace:*",
     "@vueuse/core": "catalog:",
-    "dayjs": "catalog:",
     "echarts": "catalog:",
     "vue": "catalog:",
     "vxe-pc-ui": "catalog:",

+ 21 - 5
packages/effects/plugins/src/vxe-table/extends.ts

@@ -1,3 +1,4 @@
+import type { Recordable } from '@vben/types';
 import type { VxeGridProps, VxeUIExport } from 'vxe-table';
 
 import type { VxeGridApi } from './api';
@@ -7,7 +8,7 @@ import { formatDate, formatDateTime, isFunction } from '@vben/utils';
 export function extendProxyOptions(
   api: VxeGridApi,
   options: VxeGridProps,
-  getFormValues: () => Record<string, any>,
+  getFormValues: () => Recordable<any>,
 ) {
   [
     'query',
@@ -25,17 +26,32 @@ function extendProxyOption(
   key: string,
   api: VxeGridApi,
   options: VxeGridProps,
-  getFormValues: () => Record<string, any>,
+  getFormValues: () => Recordable<any>,
 ) {
   const { proxyConfig } = options;
-  const configFn = (proxyConfig?.ajax as any)?.[key];
+  const configFn = (proxyConfig?.ajax as Recordable<any>)?.[key];
   if (!isFunction(configFn)) {
     return options;
   }
 
-  const wrapperFn = async (params: any, _formValues: any, ...args: any[]) => {
+  const wrapperFn = async (
+    params: Recordable<any>,
+    customValues: Recordable<any>,
+    ...args: Recordable<any>[]
+  ) => {
     const formValues = getFormValues();
-    const data = await configFn(params, formValues, ...args);
+    const data = await configFn(
+      params,
+      {
+        /**
+         * 开启toolbarConfig.refresh功能
+         * 点击刷新按钮 这里的值为PointerEvent 会携带错误参数
+         */
+        ...(customValues instanceof PointerEvent ? {} : customValues),
+        ...formValues,
+      },
+      ...args,
+    );
     return data;
   };
   api.setState({

+ 104 - 0
packages/effects/plugins/src/vxe-table/style.css

@@ -0,0 +1,104 @@
+:root .vxe-grid {
+  --vxe-ui-font-color: hsl(var(--foreground));
+  --vxe-ui-font-primary-color: hsl(var(--primary));
+
+  /* --vxe-ui-font-lighten-color: #babdc0;
+  --vxe-ui-font-darken-color: #86898e; */
+  --vxe-ui-font-disabled-color: hsl(var(--foreground) / 50%);
+
+  /* base */
+  --vxe-ui-base-popup-border-color: hsl(var(--border));
+  --vxe-ui-input-disabled-color: hsl(var(--border) / 60%);
+
+  /* --vxe-ui-base-popup-box-shadow: 0px 12px 30px 8px rgb(0 0 0 / 50%); */
+
+  /* layout */
+  --vxe-ui-layout-background-color: hsl(var(--background));
+  --vxe-ui-table-resizable-line-color: hsl(var(--heavy));
+
+  /* --vxe-ui-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px hsl(var(--accent));
+  --vxe-ui-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px hsl(var(--accent)); */
+
+  /* input */
+  --vxe-ui-input-border-color: hsl(var(--border));
+
+  /* --vxe-ui-input-placeholder-color: #8d9095; */
+
+  /* --vxe-ui-input-disabled-background-color: #262727; */
+
+  /* loading */
+  --vxe-ui-loading-background-color: hsl(var(--overlay-content));
+
+  /* table */
+  --vxe-ui-table-header-background-color: hsl(var(--accent));
+  --vxe-ui-table-border-color: hsl(var(--border));
+  --vxe-ui-table-row-hover-background-color: hsl(var(--accent-hover));
+  --vxe-ui-table-row-striped-background-color: hsl(var(--accent) / 60%);
+  --vxe-ui-table-row-hover-striped-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-radio-checked-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-hover-radio-checked-background-color: hsl(
+    var(--accent-hover)
+  );
+  --vxe-ui-table-row-checkbox-checked-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-hover-checkbox-checked-background-color: hsl(
+    var(--accent-hover)
+  );
+  --vxe-ui-table-row-current-background-color: hsl(var(--accent));
+  --vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover));
+
+  /* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */
+}
+
+.vxe-pager {
+  .vxe-pager--prev-btn:not(.is--disabled):active,
+  .vxe-pager--next-btn:not(.is--disabled):active,
+  .vxe-pager--num-btn:not(.is--disabled):active,
+  .vxe-pager--jump-prev:not(.is--disabled):active,
+  .vxe-pager--jump-next:not(.is--disabled):active,
+  .vxe-pager--prev-btn:not(.is--disabled):focus,
+  .vxe-pager--next-btn:not(.is--disabled):focus,
+  .vxe-pager--num-btn:not(.is--disabled):focus,
+  .vxe-pager--jump-prev:not(.is--disabled):focus,
+  .vxe-pager--jump-next:not(.is--disabled):focus {
+    color: hsl(var(--accent-foreground));
+    background-color: hsl(var(--accent));
+    border: 1px solid hsl(var(--border));
+    box-shadow: 0 0 0 1px hsl(var(--border));
+  }
+
+  .vxe-pager--wrapper {
+    display: flex;
+    align-items: center;
+  }
+
+  .vxe-pager--sizes {
+    margin-right: auto;
+  }
+}
+
+.vxe-pager--wrapper {
+  @apply justify-center md:justify-end;
+}
+
+.vxe-tools--operate {
+  margin-right: 0.25rem;
+  margin-left: 0.75rem;
+}
+
+.vxe-table-custom--checkbox-option:hover {
+  background: none !important;
+}
+
+.vxe-toolbar {
+  padding: 0;
+}
+
+.vxe-buttons--wrapper:not(:empty),
+.vxe-tools--operate:not(:empty),
+.vxe-tools--wrapper:not(:empty) {
+  padding: 0.6em 0;
+}
+
+.vxe-tools--operate:not(:has(button)) {
+  margin-left: 0;
+}

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

@@ -32,7 +32,7 @@ import { useTableForm } from './init';
 
 import 'vxe-table/styles/cssvar.scss';
 import 'vxe-pc-ui/styles/cssvar.scss';
-import './theme.css';
+import './style.css';
 
 interface Props extends VxeGridProps {
   api: ExtendedVxeGridApi;
@@ -270,7 +270,7 @@ onUnmounted(() => {
       ref="gridRef"
       :class="
         cn(
-          'p-2',
+          'p-2 pt-0',
           {
             'pt-0': showToolbar && !formOptions,
           },

+ 1 - 1
packages/effects/request/package.json

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

+ 2 - 2
packages/effects/request/src/request-client/preset-interceptors.ts

@@ -20,9 +20,9 @@ export const authenticateResponseInterceptor = ({
 }): ResponseInterceptorConfig => {
   return {
     rejected: async (error) => {
-      const { config, response, data } = error;
+      const { config, response } = error;
       // 如果不是 401 错误,直接抛出异常
-      if (response?.status !== 401 && data?.code !== 401) {
+      if (response?.status !== 401) {
         throw error;
       }
       // 判断是否启用了 refreshToken 功能

+ 1 - 1
packages/icons/package.json

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

+ 1 - 1
packages/locales/package.json

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

+ 1 - 1
packages/preferences/package.json

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

+ 1 - 1
packages/stores/package.json

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

+ 1 - 1
packages/styles/package.json

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

+ 1 - 1
packages/types/package.json

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

+ 1 - 1
packages/utils/package.json

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

+ 1 - 1
playground/package.json

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

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

@@ -9,6 +9,9 @@
   "ellipsis": {
     "title": "文本省略"
   },
+  "resize": {
+    "title": "拖动调整"
+  },
   "form": {
     "title": "表单",
     "basic": "基础表单",

+ 3 - 3
playground/src/router/routes/index.ts

@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
 /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
-/** 不需要权限的菜单列表(会显示在菜单中) */
 // const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
 const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
-/** 路由列表,由基本路由+静态路由组成 */
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ *  无需走权限验证(会一直显示在菜单中) */
 const routes: RouteRecordRaw[] = [
   ...coreRoutes,
   ...externalRoutes,
@@ -32,6 +32,6 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
 const accessRoutes = [...dynamicRoutes, ...staticRoutes];
-
 export { accessRoutes, coreRouteNames, routes };

+ 12 - 0
playground/src/router/routes/modules/demos.ts

@@ -156,6 +156,18 @@ const routes: RouteRecordRaw[] = [
               title: $t('demos.features.hideChildrenInMenu'),
             },
             children: [
+              {
+                name: 'HideChildrenInMenuDemo',
+                path: '',
+                component: () =>
+                  import(
+                    '#/views/demos/features/hide-menu-children/children.vue'
+                  ),
+                meta: {
+                  hideInMenu: true,
+                  title: $t('demos.features.hideChildrenInMenu'),
+                },
+              },
               {
                 name: 'HideChildrenInMenuChildrenDemo',
                 path: '/demos/features/hide-menu-children/children',

+ 9 - 0
playground/src/router/routes/modules/examples.ts

@@ -228,6 +228,15 @@ const routes: RouteRecordRaw[] = [
           title: $t('examples.ellipsis.title'),
         },
       },
+      {
+        name: 'VueResizeDemo',
+        path: '/demos/resize/basic',
+        component: () => import('#/views/examples/resize/basic.vue'),
+        meta: {
+          icon: 'material-symbols:resize',
+          title: $t('examples.resize.title'),
+        },
+      },
     ],
   },
 ];

+ 13 - 13
playground/src/views/demos/access/button-control.vue

@@ -81,17 +81,17 @@ async function changeAccount(role: string) {
 
     <Card class="mb-5" title="组件形式控制 - 权限码">
       <AccessControl :codes="['AC_100100']" type="code">
-        <Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
+        <Button class="mr-4"> Super 账号可见 ["AC_100100"] </Button>
       </AccessControl>
       <AccessControl :codes="['AC_100030']" type="code">
-        <Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
+        <Button class="mr-4"> Admin 账号可见 ["AC_100030"] </Button>
       </AccessControl>
       <AccessControl :codes="['AC_1000001']" type="code">
         <Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
       </AccessControl>
-      <AccessControl :codes="['AC_100100', 'AC_100010']" type="code">
+      <AccessControl :codes="['AC_100100', 'AC_100030']" type="code">
         <Button class="mr-4">
-          Super & Admin 账号可见 ["AC_100100","AC_1000001"]
+          Super & Admin 账号可见 ["AC_100100","AC_100030"]
         </Button>
       </AccessControl>
     </Card>
@@ -117,35 +117,35 @@ async function changeAccount(role: string) {
 
     <Card class="mb-5" title="函数形式控制">
       <Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
-        Super 账号可见 ["AC_1000001"]
+        Super 账号可见 ["AC_100100"]
       </Button>
       <Button v-if="hasAccessByCodes(['AC_100030'])" class="mr-4">
-        Admin 账号可见 ["AC_100010"]
+        Admin 账号可见 ["AC_100030"]
       </Button>
       <Button v-if="hasAccessByCodes(['AC_1000001'])" class="mr-4">
         User 账号可见 ["AC_1000001"]
       </Button>
-      <Button v-if="hasAccessByCodes(['AC_100100', 'AC_1000001'])" class="mr-4">
-        Super & Admin 账号可见 ["AC_100100","AC_1000001"]
+      <Button v-if="hasAccessByCodes(['AC_100100', 'AC_100030'])" class="mr-4">
+        Super & Admin 账号可见 ["AC_100100","AC_100030"]
       </Button>
     </Card>
 
     <Card class="mb-5" title="指令方式 - 权限码">
       <Button class="mr-4" v-access:code="['AC_100100']">
-        Super 账号可见 ["AC_1000001"]
+        Super 账号可见 ["AC_100100"]
       </Button>
       <Button class="mr-4" v-access:code="['AC_100030']">
-        Admin 账号可见 ["AC_100010"]
+        Admin 账号可见 ["AC_100030"]
       </Button>
       <Button class="mr-4" v-access:code="['AC_1000001']">
         User 账号可见 ["AC_1000001"]
       </Button>
-      <Button class="mr-4" v-access:code="['AC_100100', 'AC_1000001']">
-        Super & Admin 账号可见 ["AC_100100","AC_1000001"]
+      <Button class="mr-4" v-access:code="['AC_100100', 'AC_100030']">
+        Super & Admin 账号可见 ["AC_100100","AC_100030"]
       </Button>
     </Card>
 
-    <Card class="mb-5" title="指令方式 - 角色">
+    <Card v-if="accessMode === 'frontend'" class="mb-5" title="指令方式 - 角色">
       <Button class="mr-4" v-access:role="['super']"> Super 角色可见 </Button>
       <Button class="mr-4" v-access:role="['admin']"> Admin 角色可见 </Button>
       <Button class="mr-4" v-access:role="['user']"> User 角色可见 </Button>

+ 11 - 0
playground/src/views/demos/features/icons/index.vue

@@ -60,6 +60,17 @@ import IconPicker from './icon-picker.vue';
       </div>
     </Card>
 
+    <Card class="mb-5" title="Tailwind CSS">
+      <div class="flex items-center gap-5 text-3xl">
+        <span class="icon-[ant-design--alipay-circle-outlined]"></span>
+        <span class="icon-[ant-design--account-book-filled]"></span>
+        <span class="icon-[ant-design--container-outlined]"></span>
+        <span class="icon-[svg-spinners--wind-toy]"></span>
+        <span class="icon-[svg-spinners--blocks-wave]"></span>
+        <span class="icon-[line-md--compass-filled-loop]"></span>
+      </div>
+    </Card>
+
     <Card class="mb-5" title="图标选择器(Iconify)">
       <div class="flex items-center gap-5">
         <IconPicker width="300px" />

+ 24 - 4
playground/src/views/demos/features/watermark/index.vue

@@ -4,7 +4,12 @@ import { useWatermark } from '@vben/hooks';
 
 import { Button, Card } from 'ant-design-vue';
 
-const { destroyWatermark, updateWatermark } = useWatermark();
+const { destroyWatermark, updateWatermark, watermark } = useWatermark();
+
+async function recreateWaterMark() {
+  destroyWatermark();
+  await updateWatermark({});
+}
 
 async function createWaterMark() {
   await updateWatermark({
@@ -21,7 +26,7 @@ async function createWaterMark() {
       ],
       type: 'linear',
     },
-    content: 'hello my watermark',
+    content: `hello my watermark\n${new Date().toLocaleString()}`,
     globalAlpha: 0.5,
     gridLayoutOptions: {
       cols: 2,
@@ -57,10 +62,25 @@ async function createWaterMark() {
     </template>
 
     <Card title="使用">
-      <Button class="mr-2" type="primary" @click="createWaterMark()">
+      <Button
+        :disabled="!!watermark"
+        class="mr-2"
+        type="primary"
+        @click="recreateWaterMark"
+      >
         创建水印
       </Button>
-      <Button danger @click="destroyWatermark">移除水印</Button>
+      <Button
+        :disabled="!watermark"
+        class="mr-2"
+        type="primary"
+        @click="createWaterMark"
+      >
+        更新水印
+      </Button>
+      <Button :disabled="!watermark" danger @click="destroyWatermark">
+        移除水印
+      </Button>
     </Card>
   </Page>
 </template>

+ 14 - 3
playground/src/views/examples/drawer/index.vue

@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import { Page, useVbenDrawer } from '@vben/common-ui';
+import { type DrawerPlacement, Page, useVbenDrawer } from '@vben/common-ui';
 
 import { Button, Card } from 'ant-design-vue';
 
@@ -13,6 +13,7 @@ import SharedDataDemo from './shared-data-demo.vue';
 const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
   // 连接抽离的组件
   connectedComponent: BaseDemo,
+  // placement: 'left',
 });
 
 const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
@@ -31,7 +32,8 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
   connectedComponent: FormDrawerDemo,
 });
 
-function openBaseDrawer() {
+function openBaseDrawer(placement: DrawerPlacement = 'right') {
+  baseDrawerApi.setState({ placement });
   baseDrawerApi.open();
 }
 
@@ -81,7 +83,16 @@ function openFormDrawer() {
 
     <Card class="mb-4" title="基本使用">
       <p class="mb-3">一个基础的抽屉示例</p>
-      <Button type="primary" @click="openBaseDrawer">打开抽屉</Button>
+      <Button type="primary" @click="openBaseDrawer('right')">右侧打开</Button>
+      <Button class="ml-2" type="primary" @click="openBaseDrawer('bottom')">
+        底部打开
+      </Button>
+      <Button class="ml-2" type="primary" @click="openBaseDrawer('left')">
+        左侧打开
+      </Button>
+      <Button class="ml-2" type="primary" @click="openBaseDrawer('top')">
+        顶部打开
+      </Button>
     </Card>
 
     <Card class="mb-4" title="内容高度自适应滚动">

+ 2 - 2
playground/src/views/examples/modal/auto-height-demo.vue

@@ -22,10 +22,10 @@ const [Modal, modalApi] = useVbenModal({
 });
 
 function handleUpdate(len: number) {
-  modalApi.setState({ loading: true });
+  modalApi.setState({ confirmDisabled: true, loading: true });
   setTimeout(() => {
     list.value = Array.from({ length: len }, (_v, k) => k + 1);
-    modalApi.setState({ loading: false });
+    modalApi.setState({ confirmDisabled: false, loading: false });
   }, 2000);
 }
 </script>

+ 58 - 0
playground/src/views/examples/resize/basic.vue

@@ -0,0 +1,58 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { Page, VResize } from '@vben/common-ui';
+
+const colorMap = ['red', 'green', 'yellow', 'gray'];
+
+type TSize = {
+  height: number;
+  left: number;
+  top: number;
+  width: number;
+};
+
+const sizeList = ref<TSize[]>([
+  { height: 200, left: 200, top: 200, width: 200 },
+  { height: 300, left: 300, top: 300, width: 300 },
+  { height: 400, left: 400, top: 400, width: 400 },
+  { height: 500, left: 500, top: 500, width: 500 },
+]);
+
+const resize = (size?: TSize, rect?: TSize) => {
+  if (!size || !rect) return;
+
+  size.height = rect.height;
+  size.left = rect.left;
+  size.top = rect.top;
+  size.width = rect.width;
+};
+</script>
+
+<template>
+  <Page description="Resize组件基础示例" title="Resize组件">
+    <div class="m-4 bg-blue-500 p-48 text-xl">
+      <div v-for="size in sizeList" :key="size.width">
+        {{
+          `width: ${size.width}px, height: ${size.height}px, top: ${size.top}px, left: ${size.left}px`
+        }}
+      </div>
+    </div>
+
+    <template v-for="(_, idx) of 4" :key="idx">
+      <VResize
+        :h="100 * (idx + 1)"
+        :w="100 * (idx + 1)"
+        :x="100 * (idx + 1)"
+        :y="100 * (idx + 1)"
+        @dragging="(rect) => resize(sizeList[idx], rect)"
+        @resizing="(rect) => resize(sizeList[idx], rect)"
+      >
+        <div
+          :style="{ backgroundColor: colorMap[idx] }"
+          class="h-full w-full"
+        ></div>
+      </VResize>
+    </template>
+  </Page>
+</template>

Fichier diff supprimé car celui-ci est trop grand
+ 226 - 232
pnpm-lock.yaml


+ 65 - 67
pnpm-workspace.yaml

@@ -13,68 +13,67 @@ packages:
   - docs
   - playground
 catalog:
-  '@ast-grep/napi': ^0.29.0
+  '@ast-grep/napi': ^0.30.1
   '@changesets/changelog-github': ^0.5.0
-  '@changesets/cli': ^2.27.9
-  '@changesets/git': ^3.0.1
-  '@clack/prompts': ^0.7.0
-  '@commitlint/cli': ^19.5.0
-  '@commitlint/config-conventional': ^19.5.0
+  '@changesets/cli': ^2.27.10
+  '@changesets/git': ^3.0.2
+  '@clack/prompts': ^0.8.2
+  '@commitlint/cli': ^19.6.0
+  '@commitlint/config-conventional': ^19.6.0
   '@ctrl/tinycolor': ^4.1.0
-  '@eslint/js': ^9.14.0
+  '@eslint/js': ^9.16.0
   '@faker-js/faker': ^9.2.0
-  '@iconify/json': ^2.2.270
+  '@iconify/json': ^2.2.278
   '@iconify/tailwind': ^1.1.3
   '@iconify/vue': ^4.1.2
-  '@intlify/core-base': ^10.0.4
-  '@intlify/unplugin-vue-i18n': ^5.2.0
+  '@intlify/core-base': ^10.0.5
+  '@intlify/unplugin-vue-i18n': ^6.0.0
   '@jspm/generator': ^2.4.1
   '@manypkg/get-packages': ^2.2.2
-  '@nolebase/vitepress-plugin-git-changelog': ^2.8.1
-  '@playwright/test': ^1.48.2
-  '@pnpm/workspace.read-manifest': ^2.2.1
+  '@nolebase/vitepress-plugin-git-changelog': ^2.11.1
+  '@playwright/test': ^1.49.0
+  '@pnpm/workspace.read-manifest': ^1000.0.0
   '@stylistic/stylelint-plugin': ^3.1.1
   '@tailwindcss/nesting': 0.0.0-insiders.565cd3e
   '@tailwindcss/typography': ^0.5.15
-  '@tanstack/vue-query': ^5.59.20
-  '@tanstack/vue-store': ^0.5.6
+  '@tanstack/vue-query': ^5.62.0
+  '@tanstack/vue-store': ^0.6.0
   '@types/archiver': ^6.0.3
   '@types/eslint': ^9.6.1
   '@types/html-minifier-terser': ^7.0.2
   '@types/jsonwebtoken': ^9.0.7
   '@types/lodash.clonedeep': ^4.5.9
-  '@types/lodash.get': ^4.4.9
-  '@types/node': ^22.9.0
+  '@types/node': ^22.10.0
   '@types/nprogress': ^0.2.3
   '@types/postcss-import': ^14.0.3
   '@types/qrcode': ^1.5.5
   '@types/sortablejs': ^1.15.8
-  '@typescript-eslint/eslint-plugin': ^8.13.0
-  '@typescript-eslint/parser': ^8.13.0
+  '@typescript-eslint/eslint-plugin': ^8.16.0
+  '@typescript-eslint/parser': ^8.16.0
   '@vee-validate/zod': ^4.14.7
   '@vite-pwa/vitepress': ^0.5.3
-  '@vitejs/plugin-vue': ^5.1.4
-  '@vitejs/plugin-vue-jsx': ^4.0.1
-  '@vue/reactivity': ^3.5.12
-  '@vue/shared': ^3.5.12
+  '@vitejs/plugin-vue': ^5.2.1
+  '@vitejs/plugin-vue-jsx': ^4.1.1
+  '@vue/reactivity': ^3.5.13
+  '@vue/shared': ^3.5.13
   '@vue/test-utils': ^2.4.6
-  '@vueuse/core': ^11.2.0
-  '@vueuse/integrations': ^11.2.0
-  ant-design-vue: ^4.2.5
+  '@vueuse/core': ^12.0.0
+  '@vueuse/integrations': ^12.0.0
+  ant-design-vue: ^4.2.6
   archiver: ^7.0.1
   autoprefixer: ^10.4.20
-  axios: ^1.7.7
+  axios: ^1.7.8
   axios-mock-adapter: ^2.1.0
   cac: ^6.7.14
   chalk: ^5.3.0
   cheerio: 1.0.0
   circular-dependency-scanner: ^2.3.0
-  class-variance-authority: ^0.7.0
+  class-variance-authority: ^0.7.1
   clsx: ^2.1.1
   commitlint-plugin-function-rules: ^4.0.1
   consola: ^3.2.3
   cross-env: ^7.0.3
-  cspell: ^8.16.0
+  cspell: ^8.16.1
   cssnano: ^7.0.6
   cz-git: ^1.11.0
   czg: ^1.11.0
@@ -83,65 +82,64 @@ catalog:
   depcheck: ^1.4.7
   dotenv: ^16.4.5
   echarts: ^5.5.1
-  element-plus: ^2.8.7
-  eslint: ^9.14.0
-  eslint-config-turbo: ^2.2.3
+  element-plus: ^2.9.0
+  eslint: ^9.16.0
+  eslint-config-turbo: ^2.3.3
   eslint-plugin-command: ^0.2.6
   eslint-plugin-eslint-comments: ^3.2.0
-  eslint-plugin-import-x: ^4.4.0
-  eslint-plugin-jsdoc: ^50.4.3
-  eslint-plugin-jsonc: ^2.17.0
-  eslint-plugin-n: ^17.13.1
+  eslint-plugin-import-x: ^4.4.3
+  eslint-plugin-jsdoc: ^50.6.0
+  eslint-plugin-jsonc: ^2.18.2
+  eslint-plugin-n: ^17.14.0
   eslint-plugin-no-only-tests: ^3.3.0
   eslint-plugin-perfectionist: ^3.9.1
   eslint-plugin-prettier: ^5.2.1
-  eslint-plugin-regexp: ^2.6.0
-  eslint-plugin-unicorn: ^56.0.0
+  eslint-plugin-regexp: ^2.7.0
+  eslint-plugin-unicorn: ^56.0.1
   eslint-plugin-unused-imports: ^4.1.4
   eslint-plugin-vitest: ^0.5.4
-  eslint-plugin-vue: ^9.30.0
+  eslint-plugin-vue: ^9.32.0
   execa: ^9.5.1
   find-up: ^7.0.0
   get-port: ^7.1.0
   globals: ^15.12.0
   h3: ^1.13.0
-  happy-dom: ^15.11.0
+  happy-dom: ^15.11.7
   html-minifier-terser: ^7.2.0
-  husky: ^9.1.6
+  husky: ^9.1.7
   is-ci: ^3.0.1
   jsonc-eslint-parser: ^2.4.0
   jsonwebtoken: ^9.0.2
   lint-staged: ^15.2.10
   lodash.clonedeep: ^4.5.0
-  lodash.get: ^4.4.2
-  lucide-vue-next: ^0.456.0
+  lucide-vue-next: ^0.461.0
   medium-zoom: ^1.1.0
-  naive-ui: ^2.40.1
+  naive-ui: ^2.40.2
   nitropack: ^2.10.4
   nprogress: ^0.2.0
   ora: ^8.1.1
   pinia: 2.2.2
   pinia-plugin-persistedstate: ^4.1.3
   pkg-types: ^1.2.1
-  playwright: ^1.48.2
-  postcss: ^8.4.47
+  playwright: ^1.49.0
+  postcss: ^8.4.49
   postcss-antd-fixes: ^0.2.0
   postcss-html: ^1.7.0
   postcss-import: ^16.1.0
-  postcss-preset-env: ^10.0.9
+  postcss-preset-env: ^10.1.1
   postcss-scss: ^4.0.9
-  prettier: ^3.3.3
-  prettier-plugin-tailwindcss: ^0.6.8
+  prettier: ^3.4.1
+  prettier-plugin-tailwindcss: ^0.6.9
   publint: ^0.2.12
   qrcode: ^1.5.4
-  radix-vue: ^1.9.9
+  radix-vue: ^1.9.10
   resolve.exports: ^2.0.2
   rimraf: ^6.0.1
-  rollup: ^4.25.0
+  rollup: ^4.28.0
   rollup-plugin-visualizer: ^5.12.0
   sass: 1.80.6
-  sortablejs: ^1.15.3
-  stylelint: ^16.10.0
+  sortablejs: ^1.15.6
+  stylelint: ^16.11.0
   stylelint-config-recess-order: ^5.1.1
   stylelint-config-recommended: ^14.0.1
   stylelint-config-recommended-scss: ^14.1.0
@@ -149,33 +147,33 @@ catalog:
   stylelint-config-standard: ^36.0.1
   stylelint-order: ^6.0.4
   stylelint-prettier: ^5.0.2
-  stylelint-scss: ^6.8.1
-  tailwind-merge: ^2.5.4
-  tailwindcss: ^3.4.14
+  stylelint-scss: ^6.10.0
+  tailwind-merge: ^2.5.5
+  tailwindcss: ^3.4.15
   tailwindcss-animate: ^1.0.7
   theme-colors: ^0.1.0
-  turbo: ^2.2.3
-  typescript: ^5.6.3
+  turbo: ^2.3.3
+  typescript: 5.6.3
   unbuild: ^3.0.0-rc.11
   unplugin-element-plus: ^0.8.0
   vee-validate: ^4.14.7
-  vite: ^5.4.10
+  vite: ^6.0.1
   vite-plugin-compression: ^0.5.1
   vite-plugin-dts: 4.2.1
   vite-plugin-html: ^3.2.2
   vite-plugin-lazy-import: ^1.0.7
-  vite-plugin-pwa: ^0.20.5
-  vite-plugin-vue-devtools: ^7.6.3
+  vite-plugin-pwa: ^0.21.1
+  vite-plugin-vue-devtools: ^7.6.7
   vitepress: ^1.5.0
   vitepress-plugin-group-icons: ^1.3.0
-  vitest: ^2.1.4
-  vue: ^3.5.12
+  vitest: ^2.1.6
+  vue: ^3.5.13
   vue-eslint-parser: ^9.4.3
-  vue-i18n: ^10.0.4
-  vue-router: ^4.4.5
+  vue-i18n: ^10.0.5
+  vue-router: ^4.5.0
   vue-tsc: ^2.1.10
-  vxe-pc-ui: ^4.2.49
-  vxe-table: ^4.8.9
+  vxe-pc-ui: ^4.3.10
+  vxe-table: ^4.9.10
   watermark-js-plus: ^1.5.7
   zod: ^3.23.8
   zod-defaults: ^0.1.3

+ 1 - 1
scripts/turbo-run/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/turbo-run",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "private": true,
   "license": "MIT",
   "type": "module",

+ 1 - 1
scripts/vsh/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/vsh",
-  "version": "5.4.6",
+  "version": "5.4.8",
   "private": true,
   "license": "MIT",
   "type": "module",

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff