Просмотр исходного кода

Merge branch 'main' into fix-downloader

Jin Mao 7 месяцев назад
Родитель
Сommit
1d9cd88dd7
100 измененных файлов с 637 добавлено и 314 удалено
  1. 1 0
      .gitignore
  2. 5 1
      README.ja-JP.md
  3. 5 1
      README.md
  4. 5 1
      README.zh-CN.md
  5. 3 1
      apps/backend-mock/api/auth/codes.ts
  6. 7 1
      apps/backend-mock/api/auth/login.post.ts
  7. 2 0
      apps/backend-mock/api/auth/logout.post.ts
  8. 3 1
      apps/backend-mock/api/auth/refresh.post.ts
  9. 4 0
      apps/backend-mock/api/demo/bigint.ts
  10. 3 1
      apps/backend-mock/api/menu/all.ts
  11. 3 0
      apps/backend-mock/api/status.ts
  12. 1 0
      apps/backend-mock/api/system/dept/.post.ts
  13. 1 0
      apps/backend-mock/api/system/dept/[id].delete.ts
  14. 1 0
      apps/backend-mock/api/system/dept/[id].put.ts
  15. 1 0
      apps/backend-mock/api/system/dept/list.ts
  16. 1 0
      apps/backend-mock/api/system/menu/list.ts
  17. 2 1
      apps/backend-mock/api/system/menu/name-exists.ts
  18. 2 1
      apps/backend-mock/api/system/menu/path-exists.ts
  19. 1 0
      apps/backend-mock/api/system/role/list.ts
  20. 62 18
      apps/backend-mock/api/table/list.ts
  21. 2 0
      apps/backend-mock/api/test.get.ts
  22. 2 0
      apps/backend-mock/api/test.post.ts
  23. 2 1
      apps/backend-mock/api/upload.ts
  24. 2 1
      apps/backend-mock/api/user/info.ts
  25. 1 0
      apps/backend-mock/middleware/1.api.ts
  26. 2 0
      apps/backend-mock/routes/[...].ts
  27. 2 0
      apps/backend-mock/utils/cookie-utils.ts
  28. 22 4
      apps/backend-mock/utils/jwt-utils.ts
  29. 3 3
      apps/backend-mock/utils/mock-data.ts
  30. 2 0
      apps/backend-mock/utils/response.ts
  31. 1 1
      apps/web-antd/package.json
  32. 10 5
      apps/web-antd/src/layouts/basic.vue
  33. 1 1
      apps/web-ele/package.json
  34. 10 5
      apps/web-ele/src/layouts/basic.vue
  35. 1 1
      apps/web-naive/package.json
  36. 10 5
      apps/web-naive/src/layouts/basic.vue
  37. 1 1
      docs/package.json
  38. 49 25
      docs/src/components/common-ui/vben-form.md
  39. 1 1
      internal/lint-configs/commitlint-config/package.json
  40. 1 1
      internal/lint-configs/stylelint-config/package.json
  41. 1 1
      internal/node-utils/package.json
  42. 1 1
      internal/tailwind-config/package.json
  43. 1 1
      internal/tsconfig/package.json
  44. 1 1
      internal/vite-config/package.json
  45. 2 2
      package.json
  46. 1 1
      packages/@core/base/design/package.json
  47. 19 5
      packages/@core/base/design/src/css/ui.css
  48. 1 1
      packages/@core/base/icons/package.json
  49. 1 0
      packages/@core/base/icons/src/lucide.ts
  50. 1 1
      packages/@core/base/shared/package.json
  51. 1 1
      packages/@core/base/shared/src/utils/window.ts
  52. 1 1
      packages/@core/base/typings/package.json
  53. 1 1
      packages/@core/composables/package.json
  54. 2 0
      packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
  55. 1 1
      packages/@core/preferences/package.json
  56. 4 1
      packages/@core/preferences/src/config.ts
  57. 8 0
      packages/@core/preferences/src/types.ts
  58. 1 1
      packages/@core/ui-kit/form-ui/package.json
  59. 1 1
      packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  60. 21 68
      packages/@core/ui-kit/form-ui/src/form-api.ts
  61. 4 3
      packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
  62. 5 5
      packages/@core/ui-kit/form-ui/src/form-render/form.vue
  63. 5 2
      packages/@core/ui-kit/form-ui/src/types.ts
  64. 1 1
      packages/@core/ui-kit/layout-ui/package.json
  65. 3 2
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  66. 1 1
      packages/@core/ui-kit/menu-ui/package.json
  67. 5 2
      packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
  68. 9 3
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  69. 1 1
      packages/@core/ui-kit/shadcn-ui/package.json
  70. 4 1
      packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue
  71. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/pin-input/input.vue
  72. 10 2
      packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue
  73. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/segmented/tabs-indicator.vue
  74. 1 1
      packages/@core/ui-kit/shadcn-ui/src/ui/checkbox/Checkbox.vue
  75. 3 8
      packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue
  76. 1 1
      packages/@core/ui-kit/shadcn-ui/src/ui/input/Input.vue
  77. 3 3
      packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue
  78. 1 1
      packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsList.vue
  79. 2 0
      packages/@core/ui-kit/shadcn-ui/src/ui/tree/index.ts
  80. 147 71
      packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue
  81. 20 0
      packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts
  82. 1 1
      packages/@core/ui-kit/tabs-ui/package.json
  83. 1 1
      packages/constants/package.json
  84. 1 1
      packages/effects/access/package.json
  85. 1 1
      packages/effects/common-ui/package.json
  86. 1 1
      packages/effects/common-ui/src/components/index.ts
  87. 3 9
      packages/effects/common-ui/src/components/page/page.vue
  88. 1 0
      packages/effects/common-ui/src/components/tree/index.ts
  89. 25 0
      packages/effects/common-ui/src/components/tree/tree.vue
  90. 11 1
      packages/effects/common-ui/src/ui/authentication/code-login.vue
  91. 2 2
      packages/effects/common-ui/src/ui/authentication/dingding-login.vue
  92. 11 1
      packages/effects/common-ui/src/ui/authentication/qrcode-login.vue
  93. 10 5
      packages/effects/common-ui/src/ui/authentication/third-party-login.vue
  94. 1 1
      packages/effects/hooks/package.json
  95. 1 1
      packages/effects/layouts/package.json
  96. 12 3
      packages/effects/layouts/src/authentication/authentication.vue
  97. 6 1
      packages/effects/layouts/src/authentication/form.vue
  98. 3 1
      packages/effects/layouts/src/basic/layout.vue
  99. 9 2
      packages/effects/layouts/src/basic/menu/use-navigation.ts
  100. 1 1
      packages/effects/layouts/src/widgets/language-toggle.vue

+ 1 - 0
.gitignore

@@ -49,3 +49,4 @@ vite.config.ts.*
 *.sln
 *.sw?
 .history
+.cursor

+ 5 - 1
README.ja-JP.md

@@ -140,8 +140,12 @@ pnpm build
 
 ## 貢献者
 
+<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
+  <img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
+ </a>
+
 <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
-  <img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
+  <img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
 </a>
 
 ## Discord

+ 5 - 1
README.md

@@ -140,8 +140,12 @@ If you think this project is helpful to you, you can help the author buy a cup o
 
 ## Contributors
 
+<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
+  <img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
+ </a>
+
 <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
-  <img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
+  <img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
 </a>
 
 ## Discord

+ 5 - 1
README.zh-CN.md

@@ -140,8 +140,12 @@ pnpm build
 
 ## 贡献者
 
+<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
+  <img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
+ </a>
+
 <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
-  <img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
+  <img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
 </a>
 
 ## Discord

+ 3 - 1
apps/backend-mock/api/auth/codes.ts

@@ -1,5 +1,7 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { MOCK_CODES } from '~/utils/mock-data';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
 
 export default eventHandler((event) => {
   const userinfo = verifyAccessToken(event);

+ 7 - 1
apps/backend-mock/api/auth/login.post.ts

@@ -1,9 +1,15 @@
+import { defineEventHandler, readBody, setResponseStatus } from 'h3';
 import {
   clearRefreshTokenCookie,
   setRefreshTokenCookie,
 } from '~/utils/cookie-utils';
 import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
-import { forbiddenResponse } from '~/utils/response';
+import { MOCK_USERS } from '~/utils/mock-data';
+import {
+  forbiddenResponse,
+  useResponseError,
+  useResponseSuccess,
+} from '~/utils/response';
 
 export default defineEventHandler(async (event) => {
   const { password, username } = await readBody(event);

+ 2 - 0
apps/backend-mock/api/auth/logout.post.ts

@@ -1,7 +1,9 @@
+import { defineEventHandler } from 'h3';
 import {
   clearRefreshTokenCookie,
   getRefreshTokenFromCookie,
 } from '~/utils/cookie-utils';
+import { useResponseSuccess } from '~/utils/response';
 
 export default defineEventHandler(async (event) => {
   const refreshToken = getRefreshTokenFromCookie(event);

+ 3 - 1
apps/backend-mock/api/auth/refresh.post.ts

@@ -1,9 +1,11 @@
+import { defineEventHandler } from 'h3';
 import {
   clearRefreshTokenCookie,
   getRefreshTokenFromCookie,
   setRefreshTokenCookie,
 } from '~/utils/cookie-utils';
-import { verifyRefreshToken } from '~/utils/jwt-utils';
+import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
+import { MOCK_USERS } from '~/utils/mock-data';
 import { forbiddenResponse } from '~/utils/response';
 
 export default defineEventHandler(async (event) => {

+ 4 - 0
apps/backend-mock/api/demo/bigint.ts

@@ -1,3 +1,7 @@
+import { eventHandler, setHeader } from 'h3';
+import { verifyAccessToken } from '~/utils/jwt-utils';
+import { unAuthorizedResponse } from '~/utils/response';
+
 export default eventHandler(async (event) => {
   const userinfo = verifyAccessToken(event);
   if (!userinfo) {

+ 3 - 1
apps/backend-mock/api/menu/all.ts

@@ -1,5 +1,7 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { MOCK_MENUS } from '~/utils/mock-data';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
 
 export default eventHandler(async (event) => {
   const userinfo = verifyAccessToken(event);

+ 3 - 0
apps/backend-mock/api/status.ts

@@ -1,3 +1,6 @@
+import { eventHandler, getQuery, setResponseStatus } from 'h3';
+import { useResponseError } from '~/utils/response';
+
 export default eventHandler((event) => {
   const { status } = getQuery(event);
   setResponseStatus(event, Number(status));

+ 1 - 0
apps/backend-mock/api/system/dept/.post.ts

@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import {
   sleep,

+ 1 - 0
apps/backend-mock/api/system/dept/[id].delete.ts

@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import {
   sleep,

+ 1 - 0
apps/backend-mock/api/system/dept/[id].put.ts

@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import {
   sleep,

+ 1 - 0
apps/backend-mock/api/system/dept/list.ts

@@ -1,4 +1,5 @@
 import { faker } from '@faker-js/faker';
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
 

+ 1 - 0
apps/backend-mock/api/system/menu/list.ts

@@ -1,3 +1,4 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import { MOCK_MENU_LIST } from '~/utils/mock-data';
 import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

+ 2 - 1
apps/backend-mock/api/system/menu/name-exists.ts

@@ -1,6 +1,7 @@
+import { eventHandler, getQuery } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import { MOCK_MENU_LIST } from '~/utils/mock-data';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
 
 const namesMap: Record<string, any> = {};
 

+ 2 - 1
apps/backend-mock/api/system/menu/path-exists.ts

@@ -1,6 +1,7 @@
+import { eventHandler, getQuery } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import { MOCK_MENU_LIST } from '~/utils/mock-data';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
 
 const pathMap: Record<string, any> = { '/': 0 };
 

+ 1 - 0
apps/backend-mock/api/system/role/list.ts

@@ -1,4 +1,5 @@
 import { faker } from '@faker-js/faker';
+import { eventHandler, getQuery } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
 import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
 import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';

+ 62 - 18
apps/backend-mock/api/table/list.ts

@@ -1,6 +1,11 @@
 import { faker } from '@faker-js/faker';
+import { eventHandler, getQuery } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
+import {
+  sleep,
+  unAuthorizedResponse,
+  usePageResponseSuccess,
+} from '~/utils/response';
 
 function generateMockDataList(count: number) {
   const dataList = [];
@@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
   await sleep(600);
 
   const { page, pageSize, sortBy, sortOrder } = getQuery(event);
+  // 规范化分页参数,处理 string[]
+  const pageRaw = Array.isArray(page) ? page[0] : page;
+  const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
+  const pageNumber = Math.max(
+    1,
+    Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
+  );
+  const pageSizeNumber = Math.min(
+    100,
+    Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
+  );
   const listData = structuredClone(mockData);
-  if (sortBy && Reflect.has(listData[0], sortBy as string)) {
+
+  // 规范化 query 入参,兼容 string[]
+  const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
+  const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
+  // 检查 sortBy 是否是 listData 元素的合法属性键
+  if (
+    typeof sortKeyRaw === 'string' &&
+    listData[0] &&
+    Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
+  ) {
+    // 定义数组元素的类型
+    type ItemType = (typeof listData)[0];
+    const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
+    const isDesc = sortOrderRaw === 'desc';
     listData.sort((a, b) => {
-      if (sortOrder === 'asc') {
-        if (sortBy === 'price') {
-          return (
-            Number.parseFloat(a[sortBy as string]) -
-            Number.parseFloat(b[sortBy as string])
-          );
+      const aValue = a[sortKey] as unknown;
+      const bValue = b[sortKey] as unknown;
+
+      let result = 0;
+
+      if (typeof aValue === 'number' && typeof bValue === 'number') {
+        result = aValue - bValue;
+      } else if (aValue instanceof Date && bValue instanceof Date) {
+        result = aValue.getTime() - bValue.getTime();
+      } else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
+        if (aValue === bValue) {
+          result = 0;
         } else {
-          return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
+          result = aValue ? 1 : -1;
         }
       } else {
-        if (sortBy === 'price') {
-          return (
-            Number.parseFloat(b[sortBy as string]) -
-            Number.parseFloat(a[sortBy as string])
-          );
-        } else {
-          return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
-        }
+        const aStr = String(aValue);
+        const bStr = String(bValue);
+        const aNum = Number(aStr);
+        const bNum = Number(bStr);
+        result =
+          Number.isFinite(aNum) && Number.isFinite(bNum)
+            ? aNum - bNum
+            : aStr.localeCompare(bStr, undefined, {
+                numeric: true,
+                sensitivity: 'base',
+              });
       }
+
+      return isDesc ? -result : result;
     });
   }
 
-  return usePageResponseSuccess(page as string, pageSize as string, listData);
+  return usePageResponseSuccess(
+    String(pageNumber),
+    String(pageSizeNumber),
+    listData,
+  );
 });

+ 2 - 0
apps/backend-mock/api/test.get.ts

@@ -1 +1,3 @@
+import { defineEventHandler } from 'h3';
+
 export default defineEventHandler(() => 'Test get handler');

+ 2 - 0
apps/backend-mock/api/test.post.ts

@@ -1 +1,3 @@
+import { defineEventHandler } from 'h3';
+
 export default defineEventHandler(() => 'Test post handler');

+ 2 - 1
apps/backend-mock/api/upload.ts

@@ -1,5 +1,6 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
 
 export default eventHandler((event) => {
   const userinfo = verifyAccessToken(event);

+ 2 - 1
apps/backend-mock/api/user/info.ts

@@ -1,5 +1,6 @@
+import { eventHandler } from 'h3';
 import { verifyAccessToken } from '~/utils/jwt-utils';
-import { unAuthorizedResponse } from '~/utils/response';
+import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
 
 export default eventHandler((event) => {
   const userinfo = verifyAccessToken(event);

+ 1 - 0
apps/backend-mock/middleware/1.api.ts

@@ -1,3 +1,4 @@
+import { defineEventHandler } from 'h3';
 import { forbiddenResponse, sleep } from '~/utils/response';
 
 export default defineEventHandler(async (event) => {

+ 2 - 0
apps/backend-mock/routes/[...].ts

@@ -1,3 +1,5 @@
+import { defineEventHandler } from 'h3';
+
 export default defineEventHandler(() => {
   return `
 <h1>Hello Vben Admin</h1>

+ 2 - 0
apps/backend-mock/utils/cookie-utils.ts

@@ -1,5 +1,7 @@
 import type { EventHandlerRequest, H3Event } from 'h3';
 
+import { deleteCookie, getCookie, setCookie } from 'h3';
+
 export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
   deleteCookie(event, 'jwt', {
     httpOnly: true,

+ 22 - 4
apps/backend-mock/utils/jwt-utils.ts

@@ -1,8 +1,11 @@
 import type { EventHandlerRequest, H3Event } from 'h3';
 
+import type { UserInfo } from './mock-data';
+
+import { getHeader } from 'h3';
 import jwt from 'jsonwebtoken';
 
-import { UserInfo } from './mock-data';
+import { MOCK_USERS } from './mock-data';
 
 // TODO: Replace with your own secret key
 const ACCESS_TOKEN_SECRET = 'access_token_secret';
@@ -31,12 +34,22 @@ export function verifyAccessToken(
     return null;
   }
 
-  const token = authHeader.split(' ')[1];
+  const tokenParts = authHeader.split(' ');
+  if (tokenParts.length !== 2) {
+    return null;
+  }
+  const token = tokenParts[1] as string;
   try {
-    const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
+    const decoded = jwt.verify(
+      token,
+      ACCESS_TOKEN_SECRET,
+    ) as unknown as UserPayload;
 
     const username = decoded.username;
     const user = MOCK_USERS.find((item) => item.username === username);
+    if (!user) {
+      return null;
+    }
     const { password: _pwd, ...userinfo } = user;
     return userinfo;
   } catch {
@@ -50,7 +63,12 @@ export function verifyRefreshToken(
   try {
     const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
     const username = decoded.username;
-    const user = MOCK_USERS.find((item) => item.username === username);
+    const user = MOCK_USERS.find(
+      (item) => item.username === username,
+    ) as UserInfo;
+    if (!user) {
+      return null;
+    }
     const { password: _pwd, ...userinfo } = user;
     return userinfo;
   } catch {

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

@@ -276,7 +276,7 @@ export const MOCK_MENU_LIST = [
         children: [
           {
             id: 20_401,
-            pid: 201,
+            pid: 202,
             name: 'SystemDeptCreate',
             status: 1,
             type: 'button',
@@ -285,7 +285,7 @@ export const MOCK_MENU_LIST = [
           },
           {
             id: 20_402,
-            pid: 201,
+            pid: 202,
             name: 'SystemDeptEdit',
             status: 1,
             type: 'button',
@@ -294,7 +294,7 @@ export const MOCK_MENU_LIST = [
           },
           {
             id: 20_403,
-            pid: 201,
+            pid: 202,
             name: 'SystemDeptDelete',
             status: 1,
             type: 'button',

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

@@ -1,5 +1,7 @@
 import type { EventHandlerRequest, H3Event } from 'h3';
 
+import { setResponseStatus } from 'h3';
+
 export function useResponseSuccess<T = any>(data: T) {
   return {
     code: 0,

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

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

+ 10 - 5
apps/web-antd/src/layouts/basic.vue

@@ -6,7 +6,7 @@ import { computed, ref, watch } from 'vue';
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
 import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
 import {
   BasicLayout,
   LockScreen,
@@ -76,7 +76,7 @@ const menus = computed(() => [
         target: '_blank',
       });
     },
-    icon: MdiGithub,
+    icon: SvgGithubIcon,
     text: 'GitHub',
   },
   {
@@ -106,11 +106,16 @@ function handleMakeAll() {
   notifications.value.forEach((item) => (item.isRead = true));
 }
 watch(
-  () => preferences.app.watermark,
-  async (enable) => {
+  () => ({
+    enable: preferences.app.watermark,
+    content: preferences.app.watermarkContent,
+  }),
+  async ({ enable, content }) => {
     if (enable) {
       await updateWatermark({
-        content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+        content:
+          content ||
+          `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
       });
     } else {
       destroyWatermark();

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

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

+ 10 - 5
apps/web-ele/src/layouts/basic.vue

@@ -6,7 +6,7 @@ import { computed, ref, watch } from 'vue';
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
 import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
 import {
   BasicLayout,
   LockScreen,
@@ -76,7 +76,7 @@ const menus = computed(() => [
         target: '_blank',
       });
     },
-    icon: MdiGithub,
+    icon: SvgGithubIcon,
     text: 'GitHub',
   },
   {
@@ -106,11 +106,16 @@ function handleMakeAll() {
   notifications.value.forEach((item) => (item.isRead = true));
 }
 watch(
-  () => preferences.app.watermark,
-  async (enable) => {
+  () => ({
+    enable: preferences.app.watermark,
+    content: preferences.app.watermarkContent,
+  }),
+  async ({ enable, content }) => {
     if (enable) {
       await updateWatermark({
-        content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+        content:
+          content ||
+          `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
       });
     } else {
       destroyWatermark();

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

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

+ 10 - 5
apps/web-naive/src/layouts/basic.vue

@@ -6,7 +6,7 @@ import { computed, ref, watch } from 'vue';
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
 import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
 import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
 import {
   BasicLayout,
   LockScreen,
@@ -76,7 +76,7 @@ const menus = computed(() => [
         target: '_blank',
       });
     },
-    icon: MdiGithub,
+    icon: SvgGithubIcon,
     text: 'GitHub',
   },
   {
@@ -107,11 +107,16 @@ function handleMakeAll() {
 }
 
 watch(
-  () => preferences.app.watermark,
-  async (enable) => {
+  () => ({
+    enable: preferences.app.watermark,
+    content: preferences.app.watermarkContent,
+  }),
+  async ({ enable, content }) => {
     if (enable) {
       await updateWatermark({
-        content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+        content:
+          content ||
+          `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
       });
     } else {
       destroyWatermark();

+ 1 - 1
docs/package.json

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

+ 49 - 25
docs/src/components/common-ui/vben-form.md

@@ -90,30 +90,52 @@ import { h } from 'vue';
 import { globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
-import {
-  AutoComplete,
-  Button,
-  Checkbox,
-  CheckboxGroup,
-  DatePicker,
-  Divider,
-  Input,
-  InputNumber,
-  InputPassword,
-  Mentions,
-  notification,
-  Radio,
-  RadioGroup,
-  RangePicker,
-  Rate,
-  Select,
-  Space,
-  Switch,
-  Textarea,
-  TimePicker,
-  TreeSelect,
-  Upload,
-} from 'ant-design-vue';
+const AutoComplete = defineAsyncComponent(
+  () => import('ant-design-vue/es/auto-complete'),
+);
+const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
+const Checkbox = defineAsyncComponent(
+  () => import('ant-design-vue/es/checkbox'),
+);
+const CheckboxGroup = defineAsyncComponent(() =>
+  import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
+);
+const DatePicker = defineAsyncComponent(
+  () => import('ant-design-vue/es/date-picker'),
+);
+const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
+const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
+const InputNumber = defineAsyncComponent(
+  () => import('ant-design-vue/es/input-number'),
+);
+const InputPassword = defineAsyncComponent(() =>
+  import('ant-design-vue/es/input').then((res) => res.InputPassword),
+);
+const Mentions = defineAsyncComponent(
+  () => import('ant-design-vue/es/mentions'),
+);
+const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
+const RadioGroup = defineAsyncComponent(() =>
+  import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
+);
+const RangePicker = defineAsyncComponent(() =>
+  import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
+);
+const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
+const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
+const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
+const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
+const Textarea = defineAsyncComponent(() =>
+  import('ant-design-vue/es/input').then((res) => res.Textarea),
+);
+const TimePicker = defineAsyncComponent(
+  () => import('ant-design-vue/es/time-picker'),
+);
+const TreeSelect = defineAsyncComponent(
+  () => import('ant-design-vue/es/tree-select'),
+);
+const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
+
 
 const withDefaultPlaceholder = <T extends Component>(
   component: T,
@@ -304,7 +326,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
 
 | 属性名 | 描述 | 类型 | 默认值 |
 | --- | --- | --- | --- |
-| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` |
+| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
 | showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
 | wrapperClass | 表单的布局,基于tailwindcss | `any` | - |
 | actionWrapperClass | 表单操作区域class | `any` | - |
@@ -451,6 +473,8 @@ export interface FormSchema<
   fieldName: string;
   /** 帮助信息 */
   help?: CustomRenderType;
+  /** 是否隐藏表单项 */
+  hide?: boolean;
   /** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */
   label?: CustomRenderType;
   /** 自定义组件内部渲染  */

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

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

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

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

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

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

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

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

+ 1 - 1
internal/tsconfig/package.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/vite-config",
-  "version": "5.5.8",
+  "version": "5.5.9",
   "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.5.8",
+  "version": "5.5.9",
   "private": true,
   "keywords": [
     "monorepo",
@@ -98,7 +98,7 @@
     "node": ">=20.10.0",
     "pnpm": ">=9.12.0"
   },
-  "packageManager": "pnpm@10.12.4",
+  "packageManager": "pnpm@10.14.0",
   "pnpm": {
     "peerDependencyRules": {
       "allowedVersions": {

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

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

+ 19 - 5
packages/@core/base/design/src/css/ui.css

@@ -1,5 +1,5 @@
 .side-content {
-  animation-duration: 0.2s;
+  animation-duration: 0.3s;
   animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
 }
 
@@ -37,7 +37,7 @@
 @keyframes slide-down {
   from {
     opacity: 0;
-    transform: translateY(-10px);
+    transform: translateY(50px);
   }
 
   to {
@@ -49,7 +49,7 @@
 @keyframes slide-left {
   from {
     opacity: 0;
-    transform: translateX(-10px);
+    transform: translateX(-50px);
   }
 
   to {
@@ -61,7 +61,7 @@
 @keyframes slide-right {
   from {
     opacity: 0;
-    transform: translateX(-10px);
+    transform: translateX(50px);
   }
 
   to {
@@ -73,7 +73,7 @@
 @keyframes slide-up {
   from {
     opacity: 0;
-    transform: translateY(10px);
+    transform: translateY(-50px);
   }
 
   to {
@@ -85,3 +85,17 @@
 .z-popup {
   z-index: var(--popup-z-index);
 }
+
+@keyframes shrink {
+  0% {
+    transform: scale(1);
+  }
+
+  50% {
+    transform: scale(0.9);
+  }
+
+  100% {
+    transform: scale(1);
+  }
+}

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

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

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

@@ -32,6 +32,7 @@ export {
   Grip,
   GripVertical,
   Menu as IconDefault,
+  Inbox,
   Info,
   InspectionPanel,
   Languages,

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

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

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

@@ -30,7 +30,7 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
 function openRouteInNewWindow(path: string) {
   const { hash, origin } = location;
   const fullPath = path.startsWith('/') ? path : `/${path}`;
-  const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
+  const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
   openWindow(url, { target: '_blank' });
 }
 

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

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

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

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

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

@@ -22,6 +22,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "enableCheckUpdates": true,
     "enablePreferences": true,
     "enableRefreshToken": false,
+    "enableStickyPreferencesNavigationBar": true,
     "isMobile": false,
     "layout": "sidebar-nav",
     "locale": "zh-CN",
@@ -29,6 +30,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "name": "Vben Admin",
     "preferencesButtonPosition": "auto",
     "watermark": false,
+    "watermarkContent": "",
     "zIndex": 200,
   },
   "breadcrumb": {

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

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

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

@@ -1,4 +1,4 @@
-import type { Preferences } from './types';
+import type { Preferences } from "./types";
 
 const defaultPreferences: Preferences = {
   app: {
@@ -22,6 +22,7 @@ const defaultPreferences: Preferences = {
     enableCheckUpdates: true,
     enablePreferences: true,
     enableRefreshToken: false,
+    enableStickyPreferencesNavigationBar: true,
     isMobile: false,
     layout: 'sidebar-nav',
     locale: 'zh-CN',
@@ -29,7 +30,9 @@ const defaultPreferences: Preferences = {
     name: 'Vben Admin',
     preferencesButtonPosition: 'auto',
     watermark: false,
+    watermarkContent: '',
     zIndex: 200,
+
   },
   breadcrumb: {
     enable: true,

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

@@ -59,6 +59,10 @@ interface AppPreferences {
    * @zh_CN 是否开启refreshToken
    */
   enableRefreshToken: boolean;
+  /**
+   * @zh_CN 是否开启首选项导航栏吸顶效果
+   */
+  enableStickyPreferencesNavigationBar: boolean;
   /** 是否移动端 */
   isMobile: boolean;
   /** 布局方式 */
@@ -75,6 +79,10 @@ interface AppPreferences {
    * @zh_CN 是否开启水印
    */
   watermark: boolean;
+  /**
+   * @zh_CN 水印文案
+   */
+  watermarkContent: string;
   /** z-index */
   zIndex: number;
 }

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/form-ui",
-  "version": "5.5.8",
+  "version": "5.5.9",
   "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/src/components/form-actions.vue

@@ -82,11 +82,11 @@ const actionWrapperClass = computed(() => {
 
   const cls = [
     'flex',
-    'w-full',
     'items-center',
     'gap-3',
     props.compact ? 'pb-2' : 'pb-4',
     props.layout === 'vertical' ? 'self-end' : 'self-center',
+    props.layout === 'inline' ? '' : 'w-full',
     props.actionWrapperClass,
   ];
 

+ 21 - 68
packages/@core/ui-kit/form-ui/src/form-api.ts

@@ -342,13 +342,12 @@ export class FormApi {
           isObject(obj[key]) &&
           !isDayjsObject(obj[key]) &&
           !isDate(obj[key])
-            ? fieldMergeFn(obj[key], value)
+            ? fieldMergeFn(value, obj[key])
             : value;
       }
       return true;
     });
     const filteredFields = fieldMergeFn(fields, form.values);
-    this.handleStringToArrayFields(filteredFields);
     form.setValues(filteredFields, shouldValidate);
   }
 
@@ -358,7 +357,6 @@ export class FormApi {
     const form = await this.getForm();
     await form.submitForm();
     const rawValues = toRaw(await this.getValues());
-    this.handleArrayToStringFields(rawValues);
     await this.state?.handleSubmit?.(rawValues);
 
     return rawValues;
@@ -458,16 +456,31 @@ export class FormApi {
     return this.form;
   }
 
-  private handleArrayToStringFields = (originValues: Record<string, any>) => {
+  private handleMultiFields = (originValues: Record<string, any>) => {
     const arrayToStringFields = this.state?.arrayToStringFields;
     if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
       return;
     }
 
     const processFields = (fields: string[], separator: string = ',') => {
-      this.processFields(fields, separator, originValues, (value, sep) =>
-        Array.isArray(value) ? value.join(sep) : value,
-      );
+      this.processFields(fields, separator, originValues, (value, sep) => {
+        if (Array.isArray(value)) {
+          return value.join(sep);
+        } else if (typeof value === 'string') {
+          // 处理空字符串的情况
+          if (value === '') {
+            return [];
+          }
+          // 处理复杂分隔符的情况
+          const escapedSeparator = sep.replaceAll(
+            /[.*+?^${}()|[\]\\]/g,
+            String.raw`\$&`,
+          );
+          return value.split(new RegExp(escapedSeparator));
+        } else {
+          return value;
+        }
+      });
     };
 
     // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
@@ -503,8 +516,7 @@ export class FormApi {
     const values = { ...originValues };
     const fieldMappingTime = this.state?.fieldMappingTime;
 
-    this.handleStringToArrayFields(values);
-
+    this.handleMultiFields(values);
     if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
       return values;
     }
@@ -550,65 +562,6 @@ export class FormApi {
     return values;
   };
 
-  private handleStringToArrayFields = (originValues: Record<string, any>) => {
-    const arrayToStringFields = this.state?.arrayToStringFields;
-    if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
-      return;
-    }
-
-    const processFields = (fields: string[], separator: string = ',') => {
-      this.processFields(fields, separator, originValues, (value, sep) => {
-        if (typeof value !== 'string') {
-          return value;
-        }
-        // 处理空字符串的情况
-        if (value === '') {
-          return [];
-        }
-        // 处理复杂分隔符的情况
-        const escapedSeparator = sep.replaceAll(
-          /[.*+?^${}()|[\]\\]/g,
-          String.raw`\$&`,
-        );
-        return value.split(new RegExp(escapedSeparator));
-      });
-    };
-
-    // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
-    if (arrayToStringFields.every((item) => typeof item === 'string')) {
-      const lastItem =
-        arrayToStringFields[arrayToStringFields.length - 1] || '';
-      const fields =
-        lastItem.length === 1
-          ? arrayToStringFields.slice(0, -1)
-          : arrayToStringFields;
-      const separator = lastItem.length === 1 ? lastItem : ',';
-      processFields(fields, separator);
-      return;
-    }
-
-    // 处理嵌套数组格式 [['field1'], ';']
-    arrayToStringFields.forEach((fieldConfig) => {
-      if (Array.isArray(fieldConfig)) {
-        const [fields, separator = ','] = fieldConfig;
-        if (Array.isArray(fields)) {
-          processFields(fields, separator);
-        } else if (typeof originValues[fields] === 'string') {
-          const value = originValues[fields];
-          if (value === '') {
-            originValues[fields] = [];
-          } else {
-            const escapedSeparator = separator.replaceAll(
-              /[.*+?^${}()|[\]\\]/g,
-              String.raw`\$&`,
-            );
-            originValues[fields] = value.split(new RegExp(escapedSeparator));
-          }
-        }
-      }
-    });
-  };
-
   private processFields = (
     fields: string[],
     separator: string,

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

@@ -41,6 +41,7 @@ const {
   emptyStateValue,
   fieldName,
   formFieldProps,
+  hide,
   label,
   labelClass,
   labelWidth,
@@ -59,7 +60,7 @@ const values = useFormValues();
 const errors = useFieldError(fieldName);
 const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
 const formApi = formRenderProps.form;
-const compact = formRenderProps.compact;
+const compact = computed(() => formRenderProps.compact);
 const isInValid = computed(() => errors.value?.length > 0);
 
 const FieldComponent = computed(() => {
@@ -95,7 +96,7 @@ const currentRules = computed(() => {
 });
 
 const visible = computed(() => {
-  return isIf.value && isShow.value;
+  return !hide && isIf.value && isShow.value;
 });
 
 const shouldRequired = computed(() => {
@@ -283,7 +284,7 @@ onUnmounted(() => {
 
 <template>
   <FormField
-    v-if="isIf"
+    v-if="!hide && isIf"
     v-bind="fieldProps"
     v-slot="slotProps"
     :name="fieldName"

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

@@ -42,11 +42,11 @@ const emits = defineEmits<{
 }>();
 
 const wrapperClass = computed(() => {
-  const cls = ['flex flex-col'];
-  if (props.layout === 'vertical') {
-    cls.push(props.compact ? 'gap-x-2' : 'gap-x-4');
+  const cls = ['flex'];
+  if (props.layout === 'inline') {
+    cls.push('flex-wrap gap-x-2');
   } else {
-    cls.push('gap-2');
+    cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
   }
   return cn(...cls, props.wrapperClass);
 });
@@ -170,7 +170,7 @@ const computedSchema = computed(
 
 <template>
   <component :is="formComponent" v-bind="formComponentProps">
-    <div ref="wrapperRef" :class="wrapperClass" class="grid">
+    <div ref="wrapperRef" :class="wrapperClass">
       <template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
         <!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
           <slot :definition="cSchema" :name="cSchema.fieldName"> </slot>

+ 5 - 2
packages/@core/ui-kit/form-ui/src/types.ts

@@ -8,7 +8,7 @@ import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
 
 import type { FormApi } from './form-api';
 
-export type FormLayout = 'horizontal' | 'vertical';
+export type FormLayout = 'horizontal' | 'inline' | 'vertical';
 
 export type BaseFormComponentType =
   | 'DefaultButton'
@@ -255,6 +255,8 @@ export interface FormSchema<
   fieldName: string;
   /** 帮助信息 */
   help?: CustomRenderType;
+  /** 是否隐藏表单项 */
+  hide?: boolean;
   /** 表单项 */
   label?: CustomRenderType;
   // 自定义组件内部渲染
@@ -277,7 +279,8 @@ export interface FormRenderProps<
    */
   arrayToStringFields?: ArrayToStringFields;
   /**
-   * 是否展开,在showCollapseButton=true下生效
+   * 是否折叠,在showCollapseButton=true下生效
+   * true:折叠 false:展开
    */
   collapsed?: boolean;
   /**

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

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

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

@@ -10,7 +10,7 @@ import {
   useLayoutFooterStyle,
   useLayoutHeaderStyle,
 } from '@vben-core/composables';
-import { Menu } from '@vben-core/icons';
+import { IconifyIcon } from '@vben-core/icons';
 import { VbenIconButton } from '@vben-core/shadcn-ui';
 import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
 
@@ -559,7 +559,8 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
               class="my-0 mr-1 rounded-md"
               @click="handleHeaderToggle"
             >
-              <Menu class="size-4" />
+              <IconifyIcon v-if="showSidebar" icon="ep:fold" />
+              <IconifyIcon v-else icon="ep:expand" />
             </VbenIconButton>
           </template>
           <slot name="header"></slot>

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

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

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

@@ -107,7 +107,6 @@ export class ModalApi {
       this.store.setState((prev) => ({
         ...prev,
         isOpen: false,
-        submitting: false,
       }));
     }
   }
@@ -162,7 +161,11 @@ export class ModalApi {
   }
 
   open() {
-    this.store.setState((prev) => ({ ...prev, isOpen: true }));
+    this.store.setState((prev) => ({
+      ...prev,
+      isOpen: true,
+      submitting: false,
+    }));
   }
 
   setData<T>(payload: T) {

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

@@ -180,7 +180,7 @@ function escapeKeyDown(e: KeyboardEvent) {
   }
 }
 
-function handerOpenAutoFocus(e: Event) {
+function handleOpenAutoFocus(e: Event) {
   if (!openAutoFocus.value) {
     e?.preventDefault();
   }
@@ -209,6 +209,12 @@ const getForceMount = computed(() => {
   return !unref(destroyOnClose) && unref(firstOpened);
 });
 
+const handleOpened = () => {
+  requestAnimationFrame(() => {
+    props.modalApi?.onOpened();
+  });
+};
+
 function handleClosed() {
   isClosed.value = true;
   props.modalApi?.onClosed();
@@ -253,8 +259,8 @@ function handleClosed() {
       @escape-key-down="escapeKeyDown"
       @focus-outside="handleFocusOutside"
       @interact-outside="interactOutside"
-      @open-auto-focus="handerOpenAutoFocus"
-      @opened="() => modalApi?.onOpened()"
+      @open-auto-focus="handleOpenAutoFocus"
+      @opened="handleOpened"
       @pointer-down-outside="pointerDownOutside"
     >
       <DialogHeader

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

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

+ 4 - 1
packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue

@@ -21,7 +21,10 @@ isFullscreen.value = !!(
 );
 </script>
 <template>
-  <VbenIconButton @click="toggle">
+  <VbenIconButton
+    class="hover:animate-[shrink_0.3s_ease-in-out]"
+    @click="toggle"
+  >
     <Minimize v-if="isFullscreen" class="text-foreground size-4" />
     <Maximize v-else class="text-foreground size-4" />
   </VbenIconButton>

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

@@ -59,9 +59,9 @@ function handleComplete(e: string[]) {
 async function handleSend(e: Event) {
   try {
     e?.preventDefault();
-    await handleSendCode();
     countdown.value = maxTime;
     startCountdown();
+    await handleSendCode();
   } catch (error) {
     console.error('Failed to send code:', error);
     // Consider emitting an error event or showing a notification

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

@@ -35,16 +35,24 @@ const tabsIndicatorStyle = computed(() => {
     width: `${(100 / props.tabs.length).toFixed(0)}%`,
   };
 });
+
+function activeClass(tab: string): string[] {
+  return tab === activeTab.value ? ['!font-bold', 'text-primary'] : [];
+}
 </script>
 
 <template>
   <Tabs v-model="activeTab" :default-value="getDefaultValue">
-    <TabsList :style="tabsStyle" class="bg-accent relative grid w-full">
+    <TabsList
+      :style="tabsStyle"
+      class="bg-accent !outline-heavy relative grid w-full !outline !outline-2"
+    >
       <TabsIndicator :style="tabsIndicatorStyle" />
       <template v-for="tab in tabs" :key="tab.value">
         <TabsTrigger
           :value="tab.value"
-          class="z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
+          :class="activeClass(tab.value)"
+          class="hover:text-primary z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
         >
           {{ tab.label }}
         </TabsTrigger>

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

@@ -23,7 +23,7 @@ const forwardedProps = useForwardProps(delegatedProps);
     v-bind="forwardedProps"
     :class="
       cn(
-        'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-1 transition-[width,transform] duration-300',
+        'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-0.5 transition-[width,transform] duration-300',
         props.class,
       )
     "

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/ui/checkbox/Checkbox.vue

@@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
     v-bind="forwarded"
     :class="
       cn(
-        'focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground border-border peer h-4 w-4 shrink-0 rounded-sm border transition focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
+        'focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground border-border hover:border-primary peer h-4 w-4 shrink-0 rounded-sm border transition focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
         props.class,
       )
     "

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

@@ -8,12 +8,7 @@ import { computed, ref } from 'vue';
 import { cn } from '@vben-core/shared/utils';
 
 import { X } from 'lucide-vue-next';
-import {
-  DialogClose,
-  DialogContent,
-  DialogPortal,
-  useForwardPropsEmits,
-} from 'radix-vue';
+import { DialogClose, DialogContent, useForwardPropsEmits } from 'radix-vue';
 
 import DialogOverlay from './DialogOverlay.vue';
 
@@ -87,7 +82,7 @@ defineExpose({
 </script>
 
 <template>
-  <DialogPortal :to="appendTo">
+  <Teleport defer :to="appendTo">
     <Transition name="fade">
       <DialogOverlay
         v-if="open && modal"
@@ -132,5 +127,5 @@ defineExpose({
         <X class="h-4 w-4" />
       </DialogClose>
     </DialogContent>
-  </DialogPortal>
+  </Teleport>
 </template>

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/ui/input/Input.vue

@@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
     v-model="modelValue"
     :class="
       cn(
-        'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
+        'border-input bg-background ring-offset-background placeholder:text-muted-foreground/50 focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
         props.class,
       )
     "

+ 3 - 3
packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue

@@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
 
 import { cn } from '@vben-core/shared/utils';
 
-import { DialogContent, DialogPortal, useForwardPropsEmits } from 'radix-vue';
+import { DialogContent, useForwardPropsEmits } from 'radix-vue';
 
 import { sheetVariants } from './sheet';
 import SheetOverlay from './SheetOverlay.vue';
@@ -73,7 +73,7 @@ function onAnimationEnd(event: AnimationEvent) {
 </script>
 
 <template>
-  <DialogPortal :to="appendTo">
+  <Teleport defer :to="appendTo">
     <Transition name="fade">
       <SheetOverlay
         v-if="open && modal"
@@ -103,5 +103,5 @@ function onAnimationEnd(event: AnimationEvent) {
         <Cross2Icon class="h-5 w-" />
       </DialogClose> -->
     </DialogContent>
-  </DialogPortal>
+  </Teleport>
 </template>

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsList.vue

@@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
     v-bind="delegatedProps"
     :class="
       cn(
-        'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1',
+        'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-md p-1',
         props.class,
       )
     "

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

@@ -1,2 +1,4 @@
 export { default as VbenTree } from './tree.vue';
+export type { TreeProps } from './types';
+export { treePropsDefaults } from './types';
 export type { FlattenedItem } from 'radix-vue';

+ 147 - 71
packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue

@@ -14,25 +14,9 @@ import { cn, get } from '@vben-core/shared/utils';
 import { TreeItem, TreeRoot } from 'radix-vue';
 
 import { Checkbox } from '../checkbox';
+import { treePropsDefaults } from './types';
 
-const props = withDefaults(defineProps<TreeProps>(), {
-  allowClear: false,
-  autoCheckParent: true,
-  bordered: false,
-  checkStrictly: false,
-  defaultExpandedKeys: () => [],
-  defaultExpandedLevel: 0,
-  disabled: false,
-  disabledField: 'disabled',
-  expanded: () => [],
-  iconField: 'icon',
-  labelField: 'label',
-  multiple: false,
-  showIcon: true,
-  transition: true,
-  valueField: 'value',
-  childrenField: 'children',
-});
+const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
 
 const emits = defineEmits<{
   expand: [value: FlattenedItem<Recordable<any>>];
@@ -41,7 +25,9 @@ const emits = defineEmits<{
 
 interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
   hasChildren: boolean;
+  id: P;
   level: number;
+  parentId: null | P;
   parents: P[];
   value: T;
 }
@@ -50,24 +36,25 @@ function flatten<T = Recordable<any>, P = number | string>(
   items: T[],
   childrenField: string = 'children',
   level = 0,
+  parentId: null | P = null,
   parents: P[] = [],
 ): InnerFlattenItem<T, P>[] {
   const result: InnerFlattenItem<T, P>[] = [];
   items.forEach((item) => {
     const children = get(item, childrenField) as Array<T>;
-    const val = {
+    const id = get(item, props.valueField) as P;
+    const val: InnerFlattenItem<T, P> = {
       hasChildren: Array.isArray(children) && children.length > 0,
+      id,
       level,
+      parentId,
       parents: [...parents],
       value: item,
     };
     result.push(val);
     if (val.hasChildren)
       result.push(
-        ...flatten(children, childrenField, level + 1, [
-          ...parents,
-          get(item, props.valueField),
-        ]),
+        ...flatten(children, childrenField, level + 1, id, [...parents, id]),
       );
   });
   return result;
@@ -171,6 +158,24 @@ function collapseAll() {
   expanded.value = [];
 }
 
+function checkAll() {
+  if (!props.multiple) return;
+  modelValue.value = [
+    ...new Set(
+      flattenData.value
+        .filter((item) => !get(item.value, props.disabledField))
+        .map((item) => get(item.value, props.valueField)),
+    ),
+  ];
+  updateTreeValue();
+}
+
+function unCheckAll() {
+  if (!props.multiple) return;
+  modelValue.value = [];
+  updateTreeValue();
+}
+
 function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
   return props.disabled || get(item.value, props.disabledField);
 }
@@ -195,12 +200,51 @@ function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
           get(i.value, props.valueField) === get(item.value, props.valueField)
         );
       })
-      ?.parents?.forEach((p) => {
+      ?.parents?.filter((item) => !get(item, props.disabledField))
+      ?.forEach((p) => {
         if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) {
           modelValue.value.push(p);
         }
       });
   }
+  if (
+    !props.checkStrictly &&
+    props.multiple &&
+    props.autoCheckParent &&
+    !isSelected
+  ) {
+    flattenData.value
+      .find((i) => {
+        return (
+          get(i.value, props.valueField) === get(item.value, props.valueField)
+        );
+      })
+      ?.parents?.filter((item) => !get(item, props.disabledField))
+      ?.reverse()
+      .forEach((p) => {
+        const children = flattenData.value.filter((i) => {
+          return (
+            i.parents.length > 0 &&
+            i.parents.includes(p) &&
+            i.id !== item._id &&
+            i.parentId === p
+          );
+        });
+        if (Array.isArray(modelValue.value)) {
+          const hasSelectedChild = children.some((child) =>
+            (modelValue.value as unknown[]).includes(
+              get(child.value, props.valueField),
+            ),
+          );
+          if (!hasSelectedChild) {
+            const index = modelValue.value.indexOf(p);
+            if (index !== -1) {
+              modelValue.value.splice(index, 1);
+            }
+          }
+        }
+      });
+  }
   updateTreeValue();
   emits('select', item);
 }
@@ -210,6 +254,8 @@ defineExpose({
   collapseNodes,
   expandAll,
   expandNodes,
+  checkAll,
+  unCheckAll,
   expandToLevel,
   getItemByValue,
 });
@@ -230,15 +276,41 @@ defineExpose({
     v-slot="{ flattenItems }"
     :class="
       cn(
-        'text-blackA11 container select-none list-none rounded-lg p-2 text-sm font-medium',
+        'text-blackA11 container select-none list-none rounded-lg text-sm font-medium',
         $attrs.class as unknown as ClassType,
         bordered ? 'border' : '',
       )
     "
   >
-    <div class="w-full" v-if="$slots.header">
+    <div
+      :class="
+        cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')
+      "
+      v-if="$slots.header"
+    >
       <slot name="header"> </slot>
     </div>
+    <div
+      :class="
+        cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')
+      "
+      v-if="treeData.length > 0"
+    >
+      <div
+        class="flex size-5 flex-1 cursor-pointer items-center"
+        @click="() => (expanded?.length > 0 ? collapseAll() : expandAll())"
+      >
+        <ChevronRight
+          :class="{ 'rotate-90': expanded?.length > 0 }"
+          class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
+        />
+        <Checkbox
+          v-if="multiple"
+          @click.stop
+          @update:checked="(checked) => (checked ? checkAll() : unCheckAll())"
+        />
+      </div>
+    </div>
     <TransitionGroup :name="transition ? 'fade' : ''">
       <TreeItem
         v-for="item in flattenItems"
@@ -250,11 +322,11 @@ defineExpose({
           handleToggle,
         }"
         :key="item._id"
-        :style="{ 'padding-left': `${item.level - 0.5}rem` }"
+        :style="{ 'margin-left': `${item.level - 1}rem` }"
         :class="
           cn('cursor-pointer', getNodeClass?.(item), {
             'data-[selected]:bg-accent': !multiple,
-            'cursor-not-allowed': isNodeDisabled(item),
+            'text-foreground/50 cursor-not-allowed': isNodeDisabled(item),
           })
         "
         v-bind="
@@ -284,7 +356,7 @@ defineExpose({
             !isNodeDisabled(item) && onToggle(item);
           }
         "
-        class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
+        class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded p-1 outline-none focus:ring-2"
       >
         <ChevronRight
           v-if="
@@ -292,7 +364,7 @@ defineExpose({
             Array.isArray(item.value[childrenField]) &&
             item.value[childrenField].length > 0
           "
-          class="size-4 cursor-pointer transition"
+          class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
           :class="{ 'rotate-90': isExpanded }"
           @click.stop="
             () => {
@@ -301,52 +373,56 @@ defineExpose({
             }
           "
         />
-        <div v-else class="h-4 w-4">
-          <!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> -->
-        </div>
-        <Checkbox
-          v-if="multiple"
-          :checked="isSelected && !isNodeDisabled(item)"
-          :disabled="isNodeDisabled(item)"
-          :indeterminate="isIndeterminate && !isNodeDisabled(item)"
-          @click="
-            (event: MouseEvent) => {
-              if (isNodeDisabled(item)) {
-                event.preventDefault();
-                event.stopPropagation();
-                return;
+        <div v-else class="h-4 w-4"></div>
+        <div class="flex items-center gap-1">
+          <Checkbox
+            v-if="multiple"
+            :checked="isSelected && !isNodeDisabled(item)"
+            :disabled="isNodeDisabled(item)"
+            :indeterminate="isIndeterminate && !isNodeDisabled(item)"
+            @click="
+              (event: MouseEvent) => {
+                if (isNodeDisabled(item)) {
+                  event.preventDefault();
+                  event.stopPropagation();
+                  return;
+                }
+                handleSelect();
               }
-              handleSelect();
-            }
-          "
-        />
-        <div
-          class="flex items-center gap-1 pl-2"
-          @click="
-            (event: MouseEvent) => {
-              if (isNodeDisabled(item)) {
-                event.preventDefault();
-                event.stopPropagation();
-                return;
+            "
+          />
+          <div
+            class="flex items-center gap-1"
+            @click="
+              (event: MouseEvent) => {
+                if (isNodeDisabled(item)) {
+                  event.preventDefault();
+                  event.stopPropagation();
+                  return;
+                }
+                handleSelect();
               }
-              event.stopPropagation();
-              event.preventDefault();
-              handleSelect();
-            }
-          "
-        >
-          <slot name="node" v-bind="item">
-            <IconifyIcon
-              class="size-4"
-              v-if="showIcon && get(item.value, iconField)"
-              :icon="get(item.value, iconField)"
-            />
-            {{ get(item.value, labelField) }}
-          </slot>
+            "
+          >
+            <slot name="node" v-bind="item">
+              <IconifyIcon
+                class="size-4"
+                v-if="showIcon && get(item.value, iconField)"
+                :icon="get(item.value, iconField)"
+              />
+              {{ get(item.value, labelField) }}
+            </slot>
+          </div>
         </div>
+        <div class="h-4 w-4"></div>
       </TreeItem>
     </TransitionGroup>
-    <div class="w-full" v-if="$slots.footer">
+    <div
+      :class="
+        cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-t' : '')
+      "
+      v-if="$slots.footer"
+    >
       <slot name="footer"> </slot>
     </div>
   </TreeRoot>

+ 20 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts

@@ -40,3 +40,23 @@ export interface TreeProps {
   /** 值字段 */
   valueField?: string;
 }
+
+export function treePropsDefaults() {
+  return {
+    allowClear: false,
+    autoCheckParent: true,
+    bordered: false,
+    checkStrictly: false,
+    defaultExpandedKeys: () => [],
+    defaultExpandedLevel: 0,
+    disabled: false,
+    disabledField: 'disabled',
+    iconField: 'icon',
+    labelField: 'label',
+    multiple: false,
+    showIcon: true,
+    transition: true,
+    valueField: 'value',
+    childrenField: 'children',
+  };
+}

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben-core/tabs-ui",
-  "version": "5.5.8",
+  "version": "5.5.9",
   "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.5.8",
+  "version": "5.5.9",
   "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.5.8",
+  "version": "5.5.9",
   "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.5.8",
+  "version": "5.5.9",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

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

@@ -9,6 +9,7 @@ export * from './loading';
 export * from './page';
 export * from './resize';
 export * from './tippy';
+export * from './tree';
 export * from '@vben-core/form-ui';
 export * from '@vben-core/popup-ui';
 
@@ -27,7 +28,6 @@ export {
   VbenPinInput,
   VbenSelect,
   VbenSpinner,
-  VbenTree,
 } from '@vben-core/shadcn-ui';
 
 export type { FlattenedItem } from '@vben-core/shadcn-ui';

+ 3 - 9
packages/effects/common-ui/src/components/page/page.vue

@@ -25,7 +25,7 @@ const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
 const contentStyle = computed<StyleValue>(() => {
   if (autoContentHeight) {
     return {
-      height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,
+      height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${footerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,
       overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
     };
   }
@@ -50,7 +50,7 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="relative">
+  <div class="relative flex min-h-full flex-col">
     <div
       v-if="
         description ||
@@ -89,16 +89,10 @@ onMounted(() => {
     <div :class="cn('h-full p-4', contentClass)" :style="contentStyle">
       <slot></slot>
     </div>
-
     <div
       v-if="$slots.footer"
       ref="footerRef"
-      :class="
-        cn(
-          'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4',
-          footerClass,
-        )
-      "
+      :class="cn('bg-card align-center flex px-6 py-4', footerClass)"
     >
       <slot name="footer"></slot>
     </div>

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

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

+ 25 - 0
packages/effects/common-ui/src/components/tree/tree.vue

@@ -0,0 +1,25 @@
+<script setup lang="ts">
+import type { TreeProps } from '@vben-core/shadcn-ui';
+
+import { Inbox } from '@vben/icons';
+import { $t } from '@vben/locales';
+
+import { treePropsDefaults, VbenTree } from '@vben-core/shadcn-ui';
+
+const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
+</script>
+
+<template>
+  <VbenTree v-if="props.treeData?.length > 0" v-bind="props">
+    <template v-for="(_, key) in $slots" :key="key" #[key]="slotProps">
+      <slot :name="key" v-bind="slotProps"> </slot>
+    </template>
+  </VbenTree>
+  <div
+    v-else
+    class="flex-col-center text-muted-foreground cursor-pointer rounded-lg border p-10 text-sm font-medium"
+  >
+    <Inbox class="size-10" />
+    <div class="mt-1">{{ $t('common.noData') }}</div>
+  </div>
+</template>

+ 11 - 1
packages/effects/common-ui/src/ui/authentication/code-login.vue

@@ -35,6 +35,10 @@ interface Props {
    * @zh_CN 按钮文本
    */
   submitButtonText?: string;
+  /**
+   * @zh_CN 是否显示返回按钮
+   */
+  showBack?: boolean;
 }
 
 defineOptions({
@@ -43,6 +47,7 @@ defineOptions({
 
 const props = withDefaults(defineProps<Props>(), {
   loading: false,
+  showBack: true,
   loginPath: '/auth/login',
   submitButtonText: '',
   subTitle: '',
@@ -110,7 +115,12 @@ defineExpose({
         {{ submitButtonText || $t('common.login') }}
       </slot>
     </VbenButton>
-    <VbenButton class="mt-4 w-full" variant="outline" @click="goToLogin()">
+    <VbenButton
+      v-if="showBack"
+      class="mt-4 w-full"
+      variant="outline"
+      @click="goToLogin()"
+    >
       {{ $t('common.back') }}
     </VbenButton>
   </div>

+ 2 - 2
packages/effects/common-ui/src/ui/authentication/dingding-login.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { useRoute } from 'vue-router';
 
-import { RiDingding } from '@vben/icons';
+import { SvgDingDingIcon } from '@vben/icons';
 import { $t } from '@vben/locales';
 
 import { alert, useVbenModal } from '@vben-core/popup-ui';
@@ -96,7 +96,7 @@ const handleLogin = () => {
       :tooltip="$t('authentication.dingdingLogin')"
       tooltip-side="top"
     >
-      <RiDingding />
+      <SvgDingDingIcon />
     </VbenIconButton>
     <Modal>
       <div id="dingding_qrcode_login_element"></div>

+ 11 - 1
packages/effects/common-ui/src/ui/authentication/qrcode-login.vue

@@ -35,6 +35,10 @@ interface Props {
    * @zh_CN 描述
    */
   description?: string;
+  /**
+   * @zh_CN 是否显示返回按钮
+   */
+  showBack?: boolean;
 }
 
 defineOptions({
@@ -44,6 +48,7 @@ defineOptions({
 const props = withDefaults(defineProps<Props>(), {
   description: '',
   loading: false,
+  showBack: true,
   loginPath: '/auth/login',
   submitButtonText: '',
   subTitle: '',
@@ -88,7 +93,12 @@ function goToLogin() {
       </p>
     </div>
 
-    <VbenButton class="mt-4 w-full" variant="outline" @click="goToLogin()">
+    <VbenButton
+      v-if="showBack"
+      class="mt-4 w-full"
+      variant="outline"
+      @click="goToLogin()"
+    >
       {{ $t('common.back') }}
     </VbenButton>
   </div>

+ 10 - 5
packages/effects/common-ui/src/ui/authentication/third-party-login.vue

@@ -1,6 +1,11 @@
 <script setup lang="ts">
 import { useAppConfig } from '@vben/hooks';
-import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben/icons';
+import {
+  SvgGithubIcon,
+  SvgGoogleIcon,
+  SvgQQChatIcon,
+  SvgWeChatIcon,
+} from '@vben/icons';
 import { $t } from '@vben/locales';
 
 import { VbenIconButton } from '@vben-core/shadcn-ui';
@@ -32,28 +37,28 @@ const {
         tooltip-side="top"
         class="mb-3"
       >
-        <MdiWechat />
+        <SvgWeChatIcon />
       </VbenIconButton>
       <VbenIconButton
         :tooltip="$t('authentication.qqLogin')"
         tooltip-side="top"
         class="mb-3"
       >
-        <MdiQqchat />
+        <SvgQQChatIcon />
       </VbenIconButton>
       <VbenIconButton
         :tooltip="$t('authentication.githubLogin')"
         tooltip-side="top"
         class="mb-3"
       >
-        <MdiGithub />
+        <SvgGithubIcon />
       </VbenIconButton>
       <VbenIconButton
         :tooltip="$t('authentication.googleLogin')"
         tooltip-side="top"
         class="mb-3"
       >
-        <MdiGoogle />
+        <SvgGoogleIcon />
       </VbenIconButton>
       <DingdingLogin
         v-if="dingdingAuthConfig"

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

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

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

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

+ 12 - 3
packages/effects/layouts/src/authentication/authentication.vue

@@ -50,7 +50,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
     <AuthenticationFormView
       v-if="authPanelLeft"
       class="min-h-full w-2/5 flex-1"
-      transition-name="slide-left"
+      data-side="left"
     >
       <template v-if="copyright" #copyright>
         <slot name="copyright">
@@ -86,7 +86,14 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
         class="bg-background-deep absolute inset-0 h-full w-full dark:bg-[#070709]"
       >
         <div class="login-background absolute left-0 top-0 size-full"></div>
-        <div class="flex-col-center -enter-x mr-20 h-full">
+        <div
+          :key="authPanelLeft ? 'left' : authPanelRight ? 'right' : 'center'"
+          class="flex-col-center mr-20 h-full"
+          :class="{
+            'enter-x': authPanelLeft,
+            '-enter-x': authPanelRight,
+          }"
+        >
           <template v-if="sloganImage">
             <img
               :alt="appName"
@@ -110,6 +117,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
       <div class="login-background absolute left-0 top-0 size-full"></div>
       <AuthenticationFormView
         class="md:bg-background shadow-primary/5 shadow-float w-full rounded-3xl pb-20 md:w-2/3 lg:w-1/2 xl:w-[36%]"
+        data-side="bottom"
       >
         <template v-if="copyright" #copyright>
           <slot name="copyright">
@@ -125,7 +133,8 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
     <!-- 右侧认证面板 -->
     <AuthenticationFormView
       v-if="authPanelRight"
-      class="min-h-full w-[34%] flex-1"
+      class="min-h-full w-2/5 flex-1"
+      data-side="right"
     >
       <template v-if="copyright" #copyright>
         <slot name="copyright">

+ 6 - 1
packages/effects/layouts/src/authentication/form.vue

@@ -2,6 +2,10 @@
 defineOptions({
   name: 'AuthenticationFormView',
 });
+
+defineProps<{
+  dataSide?: 'bottom' | 'left' | 'right' | 'top';
+}>();
 </script>
 
 <template>
@@ -16,7 +20,8 @@ defineOptions({
           <component
             :is="Component"
             :key="route.fullPath"
-            class="enter-x mt-6 w-full sm:mx-auto md:max-w-md"
+            class="side-content mt-6 w-full sm:mx-auto md:max-w-md"
+            :data-side="dataSide"
           />
         </KeepAlive>
       </Transition>

+ 3 - 1
packages/effects/layouts/src/basic/layout.vue

@@ -158,7 +158,9 @@ function clickLogo() {
 function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {
   // 只在双列模式下生效
   if (
-    preferences.app.layout === 'sidebar-mixed-nav' &&
+    ['header-mixed-nav', 'sidebar-mixed-nav'].includes(
+      preferences.app.layout,
+    ) &&
     route.meta &&
     route.meta.hideInMenu
   ) {

+ 9 - 2
packages/effects/layouts/src/basic/menu/use-navigation.ts

@@ -29,7 +29,8 @@ function useNavigation() {
       return true;
     }
     const route = routeMetaMap.get(path);
-    return route?.meta?.openInNewWindow ?? false;
+    // 如果有外链或者设置了在新窗口打开,返回 true
+    return !!(route?.meta?.link || route?.meta?.openInNewWindow);
   };
 
   const resolveHref = (path: string): string => {
@@ -39,7 +40,13 @@ function useNavigation() {
   const navigation = async (path: string) => {
     try {
       const route = routeMetaMap.get(path);
-      const { openInNewWindow = false, query = {} } = route?.meta ?? {};
+      const { openInNewWindow = false, query = {}, link } = route?.meta ?? {};
+
+      // 检查是否有外链
+      if (link && typeof link === 'string') {
+        openWindow(link, { target: '_blank' });
+        return;
+      }
 
       if (isHttpUrl(path)) {
         openWindow(path, { target: '_blank' });

+ 1 - 1
packages/effects/layouts/src/widgets/language-toggle.vue

@@ -31,7 +31,7 @@ async function handleUpdate(value: string | undefined) {
       :model-value="preferences.app.locale"
       @update:model-value="handleUpdate"
     >
-      <VbenIconButton>
+      <VbenIconButton class="hover:animate-[shrink_0.3s_ease-in-out]">
         <Languages class="text-foreground size-4" />
       </VbenIconButton>
     </VbenDropdownRadioMenu>

Некоторые файлы не были показаны из-за большого количества измененных файлов