Sfoglia il codice sorgente

Merge tag 'v5.5.7' into admin-dz

laiqi 11 mesi fa
parent
commit
a38c247f32
76 ha cambiato i file con 783 aggiunte e 231 eliminazioni
  1. 28 0
      apps/backend-mock/api/demo/bigint.ts
  2. 1 1
      apps/web-ele/package.json
  3. 23 21
      apps/web-ele/src/adapter/form.ts
  4. 5 0
      apps/web-ele/src/bootstrap.ts
  5. 1 1
      apps/web-ele/src/router/guard.ts
  6. 16 0
      docs/src/demos/vben-ellipsis-text/auto-display/index.vue
  7. 1 1
      internal/lint-configs/commitlint-config/package.json
  8. 1 1
      internal/lint-configs/stylelint-config/package.json
  9. 1 1
      internal/node-utils/package.json
  10. 2 2
      internal/tailwind-config/package.json
  11. 3 0
      internal/tailwind-config/tsconfig.json
  12. 1 1
      internal/tsconfig/package.json
  13. 1 1
      package.json
  14. 1 1
      packages/@core/base/design/package.json
  15. 1 1
      packages/@core/base/icons/package.json
  16. 1 1
      packages/@core/base/shared/package.json
  17. 1 1
      packages/@core/base/typings/package.json
  18. 2 1
      packages/@core/base/typings/src/app.d.ts
  19. 6 1
      packages/@core/base/typings/src/tabs.ts
  20. 4 0
      packages/@core/base/typings/src/vue-router.d.ts
  21. 1 1
      packages/@core/composables/package.json
  22. 13 0
      packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
  23. 1 1
      packages/@core/preferences/package.json
  24. 14 0
      packages/@core/preferences/src/config.ts
  25. 26 0
      packages/@core/preferences/src/types.ts
  26. 1 1
      packages/@core/ui-kit/form-ui/package.json
  27. 20 3
      packages/@core/ui-kit/form-ui/src/form-api.ts
  28. 38 1
      packages/@core/ui-kit/form-ui/src/use-form-context.ts
  29. 1 1
      packages/@core/ui-kit/layout-ui/package.json
  30. 1 1
      packages/@core/ui-kit/menu-ui/package.json
  31. 1 2
      packages/@core/ui-kit/popup-ui/src/alert/alert.vue
  32. 2 1
      packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts
  33. 1 1
      packages/@core/ui-kit/shadcn-ui/package.json
  34. 13 1
      packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue
  35. 12 1
      packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts
  36. 41 9
      packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue
  37. 6 0
      packages/@core/ui-kit/shadcn-ui/src/components/logo/logo.vue
  38. 1 1
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue
  39. 11 5
      packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue
  40. 1 1
      packages/@core/ui-kit/tabs-ui/package.json
  41. 2 2
      packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue
  42. 2 2
      packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue
  43. 1 1
      packages/constants/package.json
  44. 1 1
      packages/effects/access/package.json
  45. 9 0
      packages/effects/access/src/accessible.ts
  46. 1 1
      packages/effects/common-ui/package.json
  47. 86 2
      packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue
  48. 1 1
      packages/effects/hooks/package.json
  49. 70 30
      packages/effects/hooks/src/use-design-tokens.ts
  50. 21 3
      packages/effects/hooks/src/use-tabs.ts
  51. 1 1
      packages/effects/layouts/package.json
  52. 1 1
      packages/effects/layouts/src/authentication/authentication.vue
  53. 1 0
      packages/effects/layouts/src/authentication/index.ts
  54. 5 5
      packages/effects/layouts/src/basic/content/content.vue
  55. 14 0
      packages/effects/layouts/src/basic/layout.vue
  56. 4 1
      packages/effects/layouts/src/basic/menu/use-mixed-menu.ts
  57. 1 1
      packages/effects/layouts/src/basic/tabbar/tabbar.vue
  58. 9 5
      packages/effects/layouts/src/basic/tabbar/use-tabbar.ts
  59. 2 1
      packages/effects/layouts/src/widgets/check-updates/check-updates.vue
  60. 5 1
      packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
  61. 5 1
      packages/effects/plugins/src/vxe-table/index.ts
  62. 12 2
      packages/effects/plugins/src/vxe-table/use-vxe-grid.vue
  63. 1 1
      packages/effects/request/package.json
  64. 1 1
      packages/icons/package.json
  65. 1 1
      packages/locales/package.json
  66. 1 1
      packages/preferences/package.json
  67. 1 1
      packages/stores/package.json
  68. 22 17
      packages/stores/src/modules/tabbar.test.ts
  69. 142 76
      packages/stores/src/modules/tabbar.ts
  70. 1 1
      packages/styles/package.json
  71. 1 1
      packages/types/package.json
  72. 1 1
      packages/utils/package.json
  73. 10 0
      playground/src/api/examples/json-bigint.ts
  74. 39 0
      playground/src/views/demos/features/json-bigint/index.vue
  75. 2 0
      pnpm-workspace.yaml
  76. 1 1
      scripts/turbo-run/package.json

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

@@ -0,0 +1,28 @@
+export default eventHandler(async (event) => {
+  const userinfo = verifyAccessToken(event);
+  if (!userinfo) {
+    return unAuthorizedResponse(event);
+  }
+  const data = `
+  {
+    "code": 0,
+    "message": "success",
+    "data": [
+              {
+                "id": 123456789012345678901234567890123456789012345678901234567890,
+                "name": "John Doe",
+                "age": 30,
+                "email": "john-doe@demo.com"
+                },
+                {
+                "id": 987654321098765432109876543210987654321098765432109876543210,
+                "name": "Jane Smith",
+                "age": 25,
+                "email": "jane@demo.com"
+                }
+            ]
+  }
+  `;
+  setHeader(event, 'Content-Type', 'application/json');
+  return data;
+});

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

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

+ 23 - 21
apps/web-ele/src/adapter/form.ts

@@ -8,32 +8,34 @@ import type { ComponentType } from './component';
 import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
-setupVbenForm<ComponentType>({
-  config: {
-    modelPropNameMap: {
-      Upload: 'fileList',
-      CheckboxGroup: 'model-value',
+async function initSetupVbenForm() {
+  setupVbenForm<ComponentType>({
+    config: {
+      modelPropNameMap: {
+        Upload: 'fileList',
+        CheckboxGroup: 'model-value',
+      },
     },
-  },
-  defineRules: {
-    required: (value, _params, ctx) => {
-      if (value === undefined || value === null || value.length === 0) {
-        return $t('ui.formRules.required', [ctx.label]);
-      }
-      return true;
+    defineRules: {
+      required: (value, _params, ctx) => {
+        if (value === undefined || value === null || value.length === 0) {
+          return $t('ui.formRules.required', [ctx.label]);
+        }
+        return true;
+      },
+      selectRequired: (value, _params, ctx) => {
+        if (value === undefined || value === null) {
+          return $t('ui.formRules.selectRequired', [ctx.label]);
+        }
+        return true;
+      },
     },
-    selectRequired: (value, _params, ctx) => {
-      if (value === undefined || value === null) {
-        return $t('ui.formRules.selectRequired', [ctx.label]);
-      }
-      return true;
-    },
-  },
-});
+  });
+}
 
 const useVbenForm = useForm<ComponentType>;
 
-export { useVbenForm, z };
+export { initSetupVbenForm, useVbenForm, z };
 
 export type VbenFormSchema = FormSchema<ComponentType>;
 export type { VbenFormProps };

+ 5 - 0
apps/web-ele/src/bootstrap.ts

@@ -13,12 +13,17 @@ import { ElLoading } from 'element-plus';
 import { $t, setupI18n } from '#/locales';
 
 import { initComponentAdapter } from './adapter/component';
+import { initSetupVbenForm } from './adapter/form';
 import App from './app.vue';
 import { router } from './router';
 
 async function bootstrap(namespace: string) {
   // 初始化组件适配器
   await initComponentAdapter();
+
+  // 初始化表单组件
+  await initSetupVbenForm();
+
   // // 设置弹窗的默认配置
   // setDefaultModalProps({
   //   fullscreenButton: false,

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

@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
   // 记录已经加载的页面
   const loadedPaths = new Set<string>();
 
-  router.beforeEach(async (to) => {
+  router.beforeEach((to) => {
     to.meta.loaded = loadedPaths.has(to.path);
 
     // 页面加载进度条

+ 16 - 0
docs/src/demos/vben-ellipsis-text/auto-display/index.vue

@@ -0,0 +1,16 @@
+<script lang="ts" setup>
+import { EllipsisText } from '@vben/common-ui';
+
+const text = `
+Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。
+`;
+</script>
+<template>
+  <EllipsisText :line="2" :tooltip-when-ellipsis="true">
+    {{ text }}
+  </EllipsisText>
+
+  <EllipsisText :line="3" :tooltip-when-ellipsis="true">
+    {{ text }}
+  </EllipsisText>
+</template>

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/commitlint-config",
-  "version": "5.5.6",
+  "version": "5.5.7",
   "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.6",
+  "version": "5.5.7",
   "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.6",
+  "version": "5.5.7",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

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

@@ -1,6 +1,6 @@
 {
   "name": "@vben/tailwind-config",
-  "version": "5.5.6",
+  "version": "5.5.7",
   "private": true,
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
@@ -12,7 +12,7 @@
   "license": "MIT",
   "type": "module",
   "scripts": {
-    "stub": "pnpm unbuild"
+    "stub": "pnpm unbuild --stub"
   },
   "files": [
     "dist"

+ 3 - 0
internal/tailwind-config/tsconfig.json

@@ -1,6 +1,9 @@
 {
   "$schema": "https://json.schemastore.org/tsconfig",
   "extends": "@vben/tsconfig/node.json",
+  "compilerOptions": {
+    "moduleResolution": "bundler"
+  },
   "include": ["src"],
   "exclude": ["node_modules"]
 }

+ 1 - 1
internal/tsconfig/package.json

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

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vben-admin-monorepo",
-  "version": "5.5.6",
+  "version": "5.5.7",
   "private": true,
   "keywords": [
     "monorepo",

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

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

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

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

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

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

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

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

+ 2 - 1
packages/@core/base/typings/src/app.d.ts

@@ -60,8 +60,9 @@ type BreadcrumbStyleType = 'background' | 'normal';
  * 权限模式
  * backend 后端权限模式
  * frontend 前端权限模式
+ * mixed 混合权限模式
  */
-type AccessModeType = 'backend' | 'frontend';
+type AccessModeType = 'backend' | 'frontend' | 'mixed';
 
 /**
  * 导航风格

+ 6 - 1
packages/@core/base/typings/src/tabs.ts

@@ -1,3 +1,8 @@
 import type { RouteLocationNormalized } from 'vue-router';
 
-export type TabDefinition = RouteLocationNormalized;
+export interface TabDefinition extends RouteLocationNormalized {
+  /**
+   * 标签页的key
+   */
+  key?: string;
+}

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

@@ -44,6 +44,10 @@ interface RouteMeta {
     | 'warning'
     | string;
   /**
+   * 路由的完整路径作为key(默认true)
+   */
+  fullPathKey?: boolean;
+  /**
    * 当前路由的子级在菜单中不展现
    * @default false
    */

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

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

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

@@ -10,6 +10,12 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "colorWeakMode": false,
     "compact": false,
     "contentCompact": "wide",
+    "contentCompactWidth": 1200,
+    "contentPadding": 0,
+    "contentPaddingBottom": 0,
+    "contentPaddingLeft": 0,
+    "contentPaddingRight": 0,
+    "contentPaddingTop": 0,
     "defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
     "defaultHomePath": "/analytics",
     "dynamicTitle": true,
@@ -23,6 +29,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "name": "Vben Admin",
     "preferencesButtonPosition": "auto",
     "watermark": false,
+    "zIndex": 200,
   },
   "breadcrumb": {
     "enable": true,
@@ -43,15 +50,18 @@ exports[`defaultPreferences immutability test > should not modify the config obj
   "footer": {
     "enable": false,
     "fixed": false,
+    "height": 32,
   },
   "header": {
     "enable": true,
+    "height": 50,
     "hidden": false,
     "menuAlign": "start",
     "mode": "fixed",
   },
   "logo": {
     "enable": true,
+    "fit": "contain",
     "source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
   },
   "navigation": {
@@ -68,14 +78,17 @@ exports[`defaultPreferences immutability test > should not modify the config obj
   },
   "sidebar": {
     "autoActivateChild": false,
+    "collapseWidth": 60,
     "collapsed": false,
     "collapsedButton": true,
     "collapsedShowTitle": false,
     "enable": true,
     "expandOnHover": true,
     "extraCollapse": false,
+    "extraCollapsedWidth": 60,
     "fixedButton": true,
     "hidden": false,
+    "mixedWidth": 80,
     "width": 224,
   },
   "tabbar": {

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

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

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

@@ -9,6 +9,12 @@ const defaultPreferences: Preferences = {
     colorWeakMode: false,
     compact: false,
     contentCompact: 'wide',
+    contentCompactWidth: 1200,
+    contentPadding: 0,
+    contentPaddingBottom: 0,
+    contentPaddingLeft: 0,
+    contentPaddingRight: 0,
+    contentPaddingTop: 0,
     defaultAvatar:
       'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
     defaultHomePath: '/analytics',
@@ -23,6 +29,7 @@ const defaultPreferences: Preferences = {
     name: 'Samool Admin',
     preferencesButtonPosition: 'auto',
     watermark: false,
+    zIndex: 200,
   },
   breadcrumb: {
     enable: true,
@@ -43,16 +50,20 @@ const defaultPreferences: Preferences = {
   footer: {
     enable: false,
     fixed: false,
+    height: 32,
   },
   header: {
     enable: true,
+    height: 50,
     hidden: false,
     menuAlign: 'start',
     mode: 'fixed',
   },
+
   logo: {
     enable: true,
     source: '/images/logo.png',
+    fit: 'contain',
   },
   navigation: {
     accordion: true,
@@ -71,12 +82,15 @@ const defaultPreferences: Preferences = {
     collapsed: false,
     collapsedButton: true,
     collapsedShowTitle: false,
+    collapseWidth: 60,
     enable: true,
     expandOnHover: true,
     extraCollapse: false,
+    extraCollapsedWidth: 60,
     fixedButton: true,
     hidden: false,
     width: 250,
+    mixedWidth: 80,
   },
   tabbar: {
     draggable: true,

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

@@ -33,6 +33,18 @@ interface AppPreferences {
   compact: boolean;
   /** 是否开启内容紧凑模式 */
   contentCompact: ContentCompactType;
+  /** 内容紧凑宽度 */
+  contentCompactWidth: number;
+  /** 内容内边距 */
+  contentPadding: number;
+  /** 内容底部内边距 */
+  contentPaddingBottom: number;
+  /** 内容左侧内边距 */
+  contentPaddingLeft: number;
+  /** 内容右侧内边距 */
+  contentPaddingRight: number;
+  /** 内容顶部内边距 */
+  contentPaddingTop: number;
   // /** 应用默认头像 */
   defaultAvatar: string;
   /** 默认首页地址 */
@@ -63,6 +75,8 @@ interface AppPreferences {
    * @zh_CN 是否开启水印
    */
   watermark: boolean;
+  /** z-index */
+  zIndex: number;
 }
 
 interface BreadcrumbPreferences {
@@ -100,11 +114,15 @@ interface FooterPreferences {
   enable: boolean;
   /** 底栏是否固定 */
   fixed: boolean;
+  /** 底栏高度 */
+  height: number;
 }
 
 interface HeaderPreferences {
   /** 顶栏是否启用 */
   enable: boolean;
+  /** 顶栏高度 */
+  height: number;
   /** 顶栏是否隐藏,css-隐藏 */
   hidden: boolean;
   /** 顶栏菜单位置 */
@@ -116,6 +134,8 @@ interface HeaderPreferences {
 interface LogoPreferences {
   /** logo是否可见 */
   enable: boolean;
+  /** logo图片适应方式 */
+  fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
   /** logo地址 */
   source: string;
 }
@@ -138,16 +158,22 @@ interface SidebarPreferences {
   collapsedButton: boolean;
   /** 侧边栏折叠时,是否显示title */
   collapsedShowTitle: boolean;
+  /** 侧边栏折叠宽度 */
+  collapseWidth: number;
   /** 侧边栏是否可见 */
   enable: boolean;
   /** 菜单自动展开状态 */
   expandOnHover: boolean;
   /** 侧边栏扩展区域是否折叠 */
   extraCollapse: boolean;
+  /** 侧边栏扩展区域折叠宽度 */
+  extraCollapsedWidth: number;
   /** 侧边栏固定按钮是否可见 */
   fixedButton: boolean;
   /** 侧边栏是否隐藏 - css */
   hidden: boolean;
+  /** 混合侧边栏宽度 */
+  mixedWidth: number;
   /** 侧边栏宽度 */
   width: number;
 }

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

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

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

@@ -11,7 +11,7 @@ import type { Recordable } from '@vben-core/typings';
 
 import type { FormActions, FormSchema, VbenFormProps } from './types';
 
-import { toRaw } from 'vue';
+import { isRef, toRaw } from 'vue';
 
 import { Store } from '@vben-core/shared/store';
 import {
@@ -100,9 +100,26 @@ export class FormApi {
   getFieldComponentRef<T = ComponentPublicInstance>(
     fieldName: string,
   ): T | undefined {
-    return this.componentRefMap.has(fieldName)
-      ? (this.componentRefMap.get(fieldName) as T)
+    let target = this.componentRefMap.has(fieldName)
+      ? (this.componentRefMap.get(fieldName) as ComponentPublicInstance)
       : undefined;
+    if (
+      target &&
+      target.$.type.name === 'AsyncComponentWrapper' &&
+      target.$.subTree.ref
+    ) {
+      if (Array.isArray(target.$.subTree.ref)) {
+        if (
+          target.$.subTree.ref.length > 0 &&
+          isRef(target.$.subTree.ref[0]?.r)
+        ) {
+          target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance;
+        }
+      } else if (isRef(target.$.subTree.ref.r)) {
+        target = target.$.subTree.ref.r.value as ComponentPublicInstance;
+      }
+    }
+    return target as T;
   }
 
   /**

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

@@ -10,7 +10,7 @@ import { createContext } from '@vben-core/shadcn-ui';
 import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils';
 
 import { useForm } from 'vee-validate';
-import { object } from 'zod';
+import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
 import { getDefaultsForSchema } from 'zod-defaults';
 
 type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
@@ -52,7 +52,12 @@ export function useFormInitial(
       if (Reflect.has(item, 'defaultValue')) {
         set(initialValues, item.fieldName, item.defaultValue);
       } else if (item.rules && !isString(item.rules)) {
+        // 检查规则是否适合提取默认值
+        const customDefaultValue = getCustomDefaultValue(item.rules);
         zodObject[item.fieldName] = item.rules;
+        if (customDefaultValue !== undefined) {
+          initialValues[item.fieldName] = customDefaultValue;
+        }
       }
     });
 
@@ -64,6 +69,38 @@ export function useFormInitial(
     }
     return mergeWithArrayOverride(initialValues, zodDefaults);
   }
+  // 自定义默认值提取逻辑
+  function getCustomDefaultValue(rule: any): any {
+    if (rule instanceof ZodString) {
+      return ''; // 默认为空字符串
+    } else if (rule instanceof ZodNumber) {
+      return null; // 默认为 null(避免显示 0)
+    } else if (rule instanceof ZodObject) {
+      // 递归提取嵌套对象的默认值
+      const defaultValues: Record<string, any> = {};
+      for (const [key, valueSchema] of Object.entries(rule.shape)) {
+        defaultValues[key] = getCustomDefaultValue(valueSchema);
+      }
+      return defaultValues;
+    } else if (rule instanceof ZodIntersection) {
+      // 对于交集类型,从schema 提取默认值
+      const leftDefaultValue = getCustomDefaultValue(rule._def.left);
+      const rightDefaultValue = getCustomDefaultValue(rule._def.right);
+
+      // 如果左右两边都能提取默认值,合并它们
+      if (
+        typeof leftDefaultValue === 'object' &&
+        typeof rightDefaultValue === 'object'
+      ) {
+        return { ...leftDefaultValue, ...rightDefaultValue };
+      }
+
+      // 否则优先使用左边的默认值
+      return leftDefaultValue ?? rightDefaultValue;
+    } else {
+      return undefined; // 其他类型不提供默认值
+    }
+  }
 
   return {
     delegatedSlots,

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

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

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

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

+ 1 - 2
packages/@core/ui-kit/popup-ui/src/alert/alert.vue

@@ -34,7 +34,6 @@ const props = withDefaults(defineProps<AlertProps>(), {
   bordered: true,
   buttonAlign: 'end',
   centered: true,
-  containerClass: 'w-[520px]',
 });
 const emits = defineEmits(['closed', 'confirm', 'opened']);
 const open = defineModel<boolean>('open', { default: false });
@@ -148,7 +147,7 @@ async function handleOpenChange(val: boolean) {
       :class="
         cn(
           containerClass,
-          'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
+          'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-[var(--radius)]',
           {
             'border-border border': bordered,
             'shadow-3xl': !bordered,

+ 2 - 1
packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts

@@ -94,8 +94,9 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
     injectData.options?.onOpenChange?.(isOpen);
   };
 
+  const onClosed = mergedOptions.onClosed;
   mergedOptions.onClosed = () => {
-    options.onClosed?.();
+    onClosed?.();
     if (mergedOptions.destroyOnClose) {
       injectData.reCreateModal?.();
     }

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

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

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

@@ -5,6 +5,8 @@ import type {
   AvatarRootProps,
 } from 'radix-vue';
 
+import type { CSSProperties } from 'vue';
+
 import type { ClassType } from '@vben-core/typings';
 
 import { computed } from 'vue';
@@ -16,6 +18,7 @@ interface Props extends AvatarFallbackProps, AvatarImageProps, AvatarRootProps {
   class?: ClassType;
   dot?: boolean;
   dotClass?: ClassType;
+  fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
   size?: number;
 }
 
@@ -28,6 +31,15 @@ const props = withDefaults(defineProps<Props>(), {
   as: 'button',
   dot: false,
   dotClass: 'bg-green-500',
+  fit: 'cover',
+});
+
+const imageStyle = computed<CSSProperties>(() => {
+  const { fit } = props;
+  if (fit) {
+    return { objectFit: fit };
+  }
+  return {};
 });
 
 const text = computed(() => {
@@ -51,7 +63,7 @@ const rootStyle = computed(() => {
     class="relative flex flex-shrink-0 items-center"
   >
     <Avatar :class="props.class" class="size-full">
-      <AvatarImage :alt="alt" :src="src" />
+      <AvatarImage :alt="alt" :src="src" :style="imageStyle" />
       <AvatarFallback>{{ text }}</AvatarFallback>
     </Avatar>
     <span

+ 12 - 1
packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts

@@ -29,14 +29,25 @@ export type ValueType = boolean | number | string;
 
 export interface VbenButtonGroupProps
   extends Pick<VbenButtonProps, 'disabled'> {
+  /** 单选模式下允许清除选中 */
+  allowClear?: boolean;
+  /** 值改变前的回调 */
   beforeChange?: (
     value: ValueType,
     isChecked: boolean,
   ) => boolean | PromiseLike<boolean | undefined> | undefined;
+  /** 按钮样式 */
   btnClass?: any;
+  /** 按钮间隔距离 */
   gap?: number;
+  /** 多选模式下限制最多选择的数量。0表示不限制 */
+  maxCount?: number;
+  /** 是否允许多选 */
   multiple?: boolean;
-  options?: { label: CustomRenderType; value: ValueType }[];
+  /** 选项 */
+  options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
+  /** 显示图标 */
   showIcon?: boolean;
+  /** 尺寸 */
   size?: 'large' | 'middle' | 'small';
 }

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

@@ -19,6 +19,8 @@ const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
   multiple: false,
   showIcon: true,
   size: 'middle',
+  allowClear: false,
+  maxCount: 0,
 });
 const emit = defineEmits(['btnClick']);
 const btnDefaultProps = computed(() => {
@@ -82,12 +84,22 @@ async function onBtnClick(value: ValueType) {
     if (innerValue.value.includes(value)) {
       innerValue.value = innerValue.value.filter((item) => item !== value);
     } else {
+      if (props.maxCount > 0 && innerValue.value.length >= props.maxCount) {
+        innerValue.value = innerValue.value.slice(0, props.maxCount - 1);
+      }
       innerValue.value.push(value);
     }
     modelValue.value = innerValue.value;
   } else {
-    innerValue.value = [value];
-    modelValue.value = value;
+    if (props.allowClear && innerValue.value.includes(value)) {
+      innerValue.value = [];
+      modelValue.value = undefined;
+      emit('btnClick', undefined);
+      return;
+    } else {
+      innerValue.value = [value];
+      modelValue.value = value;
+    }
   }
   emit('btnClick', value);
 }
@@ -112,14 +124,20 @@ async function onBtnClick(value: ValueType) {
       @click="onBtnClick(btn.value)"
     >
       <div class="icon-wrapper" v-if="props.showIcon">
-        <LoaderCircle
-          class="animate-spin"
-          v-if="loadingValues.includes(btn.value)"
-        />
-        <CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
-        <Circle v-else />
+        <slot
+          name="icon"
+          :loading="loadingValues.includes(btn.value)"
+          :checked="innerValue.includes(btn.value)"
+        >
+          <LoaderCircle
+            class="animate-spin"
+            v-if="loadingValues.includes(btn.value)"
+          />
+          <CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
+          <Circle v-else />
+        </slot>
       </div>
-      <slot name="option" :label="btn.label" :value="btn.value">
+      <slot name="option" :label="btn.label" :value="btn.value" :data="btn">
         <VbenRenderContent :content="btn.label" />
       </slot>
     </Button>
@@ -127,6 +145,9 @@ async function onBtnClick(value: ValueType) {
 </template>
 <style lang="scss" scoped>
 .vben-check-button-group {
+  display: flex;
+  flex-wrap: wrap;
+
   &:deep(.size-large) button {
     .icon-wrapper {
       margin-right: 0.3rem;
@@ -159,5 +180,16 @@ async function onBtnClick(value: ValueType) {
       }
     }
   }
+
+  &.no-gap > :deep(button):nth-of-type(1) {
+    border-right-width: 0;
+  }
+
+  &.no-gap {
+    :deep(button + button) {
+      margin-right: -1px;
+      border-left-width: 1px;
+    }
+  }
 }
 </style>

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

@@ -7,6 +7,10 @@ interface Props {
    */
   collapsed?: boolean;
   /**
+   * @zh_CN Logo 图片适应方式
+   */
+  fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
+  /**
    * @zh_CN Logo 跳转地址
    */
   href?: string;
@@ -38,6 +42,7 @@ withDefaults(defineProps<Props>(), {
   logoSize: 32,
   src: '',
   theme: 'light',
+  fit: 'cover',
 });
 </script>
 
@@ -53,6 +58,7 @@ withDefaults(defineProps<Props>(), {
         :alt="text"
         :src="src"
         :size="logoSize"
+        :fit="fit"
         class="relative rounded-none bg-transparent"
       />
       <template v-if="!collapsed">

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

@@ -80,7 +80,7 @@ defineExpose({
       v-bind="forwarded"
       :class="
         cn(
-          'z-popup bg-background w-full p-6 shadow-lg outline-none sm:rounded-xl',
+          'z-popup bg-background p-6 shadow-lg outline-none sm:rounded-xl',
           'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
           'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
           {

+ 11 - 5
packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue

@@ -224,15 +224,20 @@ defineExpose({
         :class="
           cn('cursor-pointer', getNodeClass?.(item), {
             'data-[selected]:bg-accent': !multiple,
+            'cursor-not-allowed': disabled,
+          })
+        "
+        v-bind="
+          Object.assign(item.bind, {
+            onfocus: disabled ? 'this.blur()' : undefined,
           })
         "
-        v-bind="item.bind"
         @select="
           (event) => {
             if (event.detail.originalEvent.type === 'click') {
               event.preventDefault();
             }
-            onSelect(item, event.detail.isSelected);
+            !disabled && onSelect(item, event.detail.isSelected);
           }
         "
         @toggle="
@@ -240,7 +245,7 @@ defineExpose({
             if (event.detail.originalEvent.type === 'click') {
               event.preventDefault();
             }
-            onToggle(item);
+            !disabled && onToggle(item);
           }
         "
         class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
@@ -262,10 +267,11 @@ defineExpose({
         <Checkbox
           v-if="multiple"
           :checked="isSelected"
+          :disabled="disabled"
           :indeterminate="isIndeterminate"
           @click="
             () => {
-              handleSelect();
+              !disabled && handleSelect();
               // onSelect(item, !isSelected);
             }
           "
@@ -276,7 +282,7 @@ defineExpose({
             (_event) => {
               // $event.stopPropagation();
               // $event.preventDefault();
-              handleSelect();
+              !disabled && handleSelect();
               // onSelect(item, !isSelected);
             }
           "

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

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

+ 2 - 2
packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue

@@ -40,14 +40,14 @@ const style = computed(() => {
 
 const tabsView = computed(() => {
   return props.tabs.map((tab) => {
-    const { fullPath, meta, name, path } = tab || {};
+    const { fullPath, meta, name, path, key } = tab || {};
     const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
     return {
       affixTab: !!affixTab,
       closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
       fullPath,
       icon: icon as string,
-      key: fullPath || path,
+      key,
       meta,
       name,
       path,

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

@@ -47,14 +47,14 @@ const typeWithClass = computed(() => {
 
 const tabsView = computed(() => {
   return props.tabs.map((tab) => {
-    const { fullPath, meta, name, path } = tab || {};
+    const { fullPath, meta, name, path, key } = tab || {};
     const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
     return {
       affixTab: !!affixTab,
       closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
       fullPath,
       icon: icon as string,
-      key: fullPath || path,
+      key,
       meta,
       name,
       path,

+ 1 - 1
packages/constants/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/constants",
-  "version": "5.5.6",
+  "version": "5.5.7",
   "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.6",
+  "version": "5.5.7",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 9 - 0
packages/effects/access/src/accessible.ts

@@ -101,6 +101,15 @@ async function generateRoutes(
       console.log('frontend routes:', resultRoutes);
       break;
     }
+    case 'mixed': {
+      const [frontend_resultRoutes, backend_resultRoutes] = await Promise.all([
+        generateRoutesByFrontend(routes, roles || [], forbiddenComponent),
+        generateRoutesByBackend(options),
+      ]);
+
+      resultRoutes = [...frontend_resultRoutes, ...backend_resultRoutes];
+      break;
+    }
   }
 
   /**

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

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

+ 86 - 2
packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue

@@ -1,7 +1,14 @@
 <script setup lang="ts">
 import type { CSSProperties } from 'vue';
 
-import { computed, ref, watchEffect } from 'vue';
+import {
+  computed,
+  onBeforeUnmount,
+  onMounted,
+  onUpdated,
+  ref,
+  watchEffect,
+} from 'vue';
 
 import { VbenTooltip } from '@vben-core/shadcn-ui';
 
@@ -34,6 +41,16 @@ interface Props {
    */
   tooltip?: boolean;
   /**
+   * 是否只在文本被截断时显示提示框
+   * @default false
+   */
+  tooltipWhenEllipsis?: boolean;
+  /**
+   * 文本截断检测的像素差异阈值,越大则判断越严格
+   * @default 3
+   */
+  ellipsisThreshold?: number;
+  /**
    * 提示框背景颜色,优先级高于 overlayStyle
    */
   tooltipBackgroundColor?: string;
@@ -62,12 +79,15 @@ const props = withDefaults(defineProps<Props>(), {
   maxWidth: '100%',
   placement: 'top',
   tooltip: true,
+  tooltipWhenEllipsis: false,
+  ellipsisThreshold: 3,
   tooltipBackgroundColor: '',
   tooltipColor: '',
   tooltipFontSize: 14,
   tooltipMaxWidth: undefined,
   tooltipOverlayStyle: () => ({ textAlign: 'justify' }),
 });
+
 const emit = defineEmits<{ expandChange: [boolean] }>();
 
 const textMaxWidth = computed(() => {
@@ -79,9 +99,67 @@ const textMaxWidth = computed(() => {
 const ellipsis = ref();
 const isExpand = ref(false);
 const defaultTooltipMaxWidth = ref();
+const isEllipsis = ref(false);
 
 const { width: eleWidth } = useElementSize(ellipsis);
 
+// 检测文本是否被截断
+const checkEllipsis = () => {
+  if (!ellipsis.value || !props.tooltipWhenEllipsis) return;
+
+  const element = ellipsis.value;
+
+  const originalText = element.textContent || '';
+  const originalTrimmed = originalText.trim();
+
+  // 对于空文本直接返回 false
+  if (!originalTrimmed) {
+    isEllipsis.value = false;
+    return;
+  }
+
+  const widthDiff = element.scrollWidth - element.clientWidth;
+  const heightDiff = element.scrollHeight - element.clientHeight;
+
+  // 使用足够大的差异阈值确保只有真正被截断的文本才会显示 tooltip
+  isEllipsis.value =
+    props.line === 1
+      ? widthDiff > props.ellipsisThreshold
+      : heightDiff > props.ellipsisThreshold;
+};
+
+// 使用 ResizeObserver 监听尺寸变化
+let resizeObserver: null | ResizeObserver = null;
+
+onMounted(() => {
+  if (typeof ResizeObserver !== 'undefined' && props.tooltipWhenEllipsis) {
+    resizeObserver = new ResizeObserver(() => {
+      checkEllipsis();
+    });
+
+    if (ellipsis.value) {
+      resizeObserver.observe(ellipsis.value);
+    }
+  }
+
+  // 初始检测
+  checkEllipsis();
+});
+
+// 使用onUpdated钩子检测内容变化
+onUpdated(() => {
+  if (props.tooltipWhenEllipsis) {
+    checkEllipsis();
+  }
+});
+
+onBeforeUnmount(() => {
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+    resizeObserver = null;
+  }
+});
+
 watchEffect(
   () => {
     if (props.tooltip && eleWidth.value) {
@@ -91,9 +169,13 @@ watchEffect(
   },
   { flush: 'post' },
 );
+
 function onExpand() {
   isExpand.value = !isExpand.value;
   emit('expandChange', isExpand.value);
+  if (props.tooltipWhenEllipsis) {
+    checkEllipsis();
+  }
 }
 
 function handleExpand() {
@@ -110,7 +192,9 @@ function handleExpand() {
         color: tooltipColor,
         backgroundColor: tooltipBackgroundColor,
       }"
-      :disabled="!props.tooltip || isExpand"
+      :disabled="
+        !props.tooltip || isExpand || (props.tooltipWhenEllipsis && !isEllipsis)
+      "
       :side="placement"
     >
       <slot name="tooltip">

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

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

+ 70 - 30
packages/effects/hooks/src/use-design-tokens.ts

@@ -193,67 +193,107 @@ export function useElementPlusDesignTokens() {
 
         '--el-border-radius-base': getCssVariableValue('--radius', false),
         '--el-color-danger': getCssVariableValue('--destructive-500'),
-        '--el-color-danger-dark-2': getCssVariableValue('--destructive'),
-        '--el-color-danger-light-3': getCssVariableValue('--destructive-400'),
-        '--el-color-danger-light-5': getCssVariableValue('--destructive-300'),
-        '--el-color-danger-light-7': getCssVariableValue('--destructive-200'),
+        '--el-color-danger-dark-2': isDark.value
+          ? getCssVariableValue('--destructive-400')
+          : getCssVariableValue('--destructive-600'),
+        '--el-color-danger-light-3': isDark.value
+          ? getCssVariableValue('--destructive-600')
+          : getCssVariableValue('--destructive-400'),
+        '--el-color-danger-light-5': isDark.value
+          ? getCssVariableValue('--destructive-700')
+          : getCssVariableValue('--destructive-300'),
+        '--el-color-danger-light-7': isDark.value
+          ? getCssVariableValue('--destructive-800')
+          : getCssVariableValue('--destructive-200'),
         '--el-color-danger-light-8': isDark.value
-          ? border
+          ? getCssVariableValue('--destructive-900')
           : getCssVariableValue('--destructive-100'),
         '--el-color-danger-light-9': isDark.value
-          ? accent
+          ? getCssVariableValue('--destructive-950')
           : getCssVariableValue('--destructive-50'),
 
         '--el-color-error': getCssVariableValue('--destructive-500'),
-        '--el-color-error-dark-2': getCssVariableValue('--destructive'),
-        '--el-color-error-light-3': getCssVariableValue('--destructive-400'),
-        '--el-color-error-light-5': getCssVariableValue('--destructive-300'),
-        '--el-color-error-light-7': getCssVariableValue('--destructive-200'),
+        '--el-color-error-dark-2': isDark.value
+          ? getCssVariableValue('--destructive-400')
+          : getCssVariableValue('--destructive-600'),
+        '--el-color-error-light-3': isDark.value
+          ? getCssVariableValue('--destructive-600')
+          : getCssVariableValue('--destructive-400'),
+        '--el-color-error-light-5': isDark.value
+          ? getCssVariableValue('--destructive-700')
+          : getCssVariableValue('--destructive-300'),
+        '--el-color-error-light-7': isDark.value
+          ? getCssVariableValue('--destructive-800')
+          : getCssVariableValue('--destructive-200'),
         '--el-color-error-light-8': isDark.value
-          ? border
+          ? getCssVariableValue('--destructive-900')
           : getCssVariableValue('--destructive-100'),
         '--el-color-error-light-9': isDark.value
-          ? accent
+          ? getCssVariableValue('--destructive-950')
           : getCssVariableValue('--destructive-50'),
 
+        '--el-color-info-light-5': border,
         '--el-color-info-light-8': border,
         '--el-color-info-light-9': getCssVariableValue('--info'), // getCssVariableValue('--secondary'),
+
         '--el-color-primary': getCssVariableValue('--primary-500'),
-        '--el-color-primary-dark-2': getCssVariableValue('--primary'),
-        '--el-color-primary-light-3': getCssVariableValue('--primary-400'),
-        '--el-color-primary-light-5': getCssVariableValue('--primary-300'),
+        '--el-color-primary-dark-2': isDark.value
+          ? getCssVariableValue('--primary-400')
+          : getCssVariableValue('--primary-600'),
+        '--el-color-primary-light-3': isDark.value
+          ? getCssVariableValue('--primary-600')
+          : getCssVariableValue('--primary-400'),
+        '--el-color-primary-light-5': isDark.value
+          ? getCssVariableValue('--primary-700')
+          : getCssVariableValue('--primary-300'),
         '--el-color-primary-light-7': isDark.value
-          ? border
+          ? getCssVariableValue('--primary-800')
           : getCssVariableValue('--primary-200'),
         '--el-color-primary-light-8': isDark.value
-          ? border
+          ? getCssVariableValue('--primary-900')
           : getCssVariableValue('--primary-100'),
         '--el-color-primary-light-9': isDark.value
-          ? accent
+          ? getCssVariableValue('--primary-950')
           : getCssVariableValue('--primary-50'),
 
         '--el-color-success': getCssVariableValue('--success-500'),
-        '--el-color-success-dark-2': getCssVariableValue('--success'),
-        '--el-color-success-light-3': getCssVariableValue('--success-400'),
-        '--el-color-success-light-5': getCssVariableValue('--success-300'),
-        '--el-color-success-light-7': getCssVariableValue('--success-200'),
+        '--el-color-success-dark-2': isDark.value
+          ? getCssVariableValue('--success-400')
+          : getCssVariableValue('--success-600'),
+        '--el-color-success-light-3': isDark.value
+          ? getCssVariableValue('--success-600')
+          : getCssVariableValue('--success-400'),
+        '--el-color-success-light-5': isDark.value
+          ? getCssVariableValue('--success-700')
+          : getCssVariableValue('--success-300'),
+        '--el-color-success-light-7': isDark.value
+          ? getCssVariableValue('--success-800')
+          : getCssVariableValue('--success-200'),
         '--el-color-success-light-8': isDark.value
-          ? border
+          ? getCssVariableValue('--success-900')
           : getCssVariableValue('--success-100'),
         '--el-color-success-light-9': isDark.value
-          ? accent
+          ? getCssVariableValue('--success-950')
           : getCssVariableValue('--success-50'),
 
         '--el-color-warning': getCssVariableValue('--warning-500'),
-        '--el-color-warning-dark-2': getCssVariableValue('--warning'),
-        '--el-color-warning-light-3': getCssVariableValue('--warning-400'),
-        '--el-color-warning-light-5': getCssVariableValue('--warning-300'),
-        '--el-color-warning-light-7': getCssVariableValue('--warning-200'),
+        '--el-color-warning-dark-2': isDark.value
+          ? getCssVariableValue('--warning-400')
+          : getCssVariableValue('--warning-600'),
+        '--el-color-warning-light-3': isDark.value
+          ? getCssVariableValue('--warning-600')
+          : getCssVariableValue('--warning-400'),
+        '--el-color-warning-light-5': isDark.value
+          ? getCssVariableValue('--warning-700')
+          : getCssVariableValue('--warning-300'),
+        '--el-color-warning-light-7': isDark.value
+          ? getCssVariableValue('--warning-800')
+          : getCssVariableValue('--warning-200'),
         '--el-color-warning-light-8': isDark.value
-          ? border
+          ? getCssVariableValue('--warning-900')
           : getCssVariableValue('--warning-100'),
         '--el-color-warning-light-9': isDark.value
-          ? accent
+          ? getCssVariableValue('--warning-950')
           : getCssVariableValue('--warning-50'),
 
         '--el-fill-color': getCssVariableValue('--accent'),

+ 21 - 3
packages/effects/hooks/src/use-tabs.ts

@@ -1,3 +1,4 @@
+import type { ComputedRef } from 'vue';
 import type { RouteLocationNormalized } from 'vue-router';
 
 import { useRoute, useRouter } from 'vue-router';
@@ -41,8 +42,8 @@ export function useTabs() {
     await tabbarStore.toggleTabPin(tab || route);
   }
 
-  async function refreshTab() {
-    await tabbarStore.refresh(router);
+  async function refreshTab(name?: string) {
+    await tabbarStore.refresh(name || router);
   }
 
   async function openTabInNewWindow(tab?: RouteLocationNormalized) {
@@ -53,7 +54,24 @@ export function useTabs() {
     await tabbarStore.closeTabByKey(key, router);
   }
 
-  async function setTabTitle(title: string) {
+  /**
+   * 设置当前标签页的标题
+   *
+   * @description 支持设置静态标题字符串或动态计算标题
+   * @description 动态标题会在每次渲染时重新计算,适用于多语言或状态相关的标题
+   *
+   * @param title - 标题内容
+   *   - 静态标题: 直接传入字符串
+   *   - 动态标题: 传入 ComputedRef
+   *
+   * @example
+   * // 静态标题
+   * setTabTitle('标签页')
+   *
+   * // 动态标题(多语言)
+   * setTabTitle(computed(() => t('page.title')))
+   */
+  async function setTabTitle(title: ComputedRef<string> | string) {
     tabbarStore.setUpdateTime();
     await tabbarStore.setTabTitle(route, title);
   }

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

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

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

@@ -38,7 +38,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
 
 <template>
   <div
-    :class="[isDark]"
+    :class="[isDark ? 'dark' : '']"
     class="flex min-h-full flex-1 select-none overflow-x-hidden"
   >
     <template v-if="toolbar">

+ 1 - 0
packages/effects/layouts/src/authentication/index.ts

@@ -1 +1,2 @@
 export { default as AuthPageLayout } from './authentication.vue';
+export * from './types';

+ 5 - 5
packages/effects/layouts/src/basic/content/content.vue

@@ -9,7 +9,7 @@ import { computed } from 'vue';
 import { RouterView } from 'vue-router';
 
 import { preferences, usePreferences } from '@vben/preferences';
-import { storeToRefs, useTabbarStore } from '@vben/stores';
+import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
 
 import { IFrameRouterView } from '../../iframe';
 
@@ -115,13 +115,13 @@ function transformComponent(
             :is="transformComponent(Component, route)"
             v-if="renderRouteView"
             v-show="!route.meta.iframeSrc"
-            :key="route.fullPath"
+            :key="getTabKey(route)"
           />
         </KeepAlive>
         <component
           :is="Component"
           v-else-if="renderRouteView"
-          :key="route.fullPath"
+          :key="getTabKey(route)"
         />
       </Transition>
       <template v-else>
@@ -134,13 +134,13 @@ function transformComponent(
             :is="transformComponent(Component, route)"
             v-if="renderRouteView"
             v-show="!route.meta.iframeSrc"
-            :key="route.fullPath"
+            :key="getTabKey(route)"
           />
         </KeepAlive>
         <component
           :is="Component"
           v-else-if="renderRouteView"
-          :key="route.fullPath"
+          :key="getTabKey(route)"
         />
       </template>
     </RouterView>

+ 14 - 0
packages/effects/layouts/src/basic/layout.vue

@@ -180,8 +180,16 @@ const headerSlots = computed(() => {
   <VbenAdminLayout
     v-model:sidebar-extra-visible="sidebarExtraVisible"
     :content-compact="preferences.app.contentCompact"
+    :content-compact-width="preferences.app.contentCompactWidth"
+    :content-padding="preferences.app.contentPadding"
+    :content-padding-bottom="preferences.app.contentPaddingBottom"
+    :content-padding-left="preferences.app.contentPaddingLeft"
+    :content-padding-right="preferences.app.contentPaddingRight"
+    :content-padding-top="preferences.app.contentPaddingTop"
     :footer-enable="preferences.footer.enable"
     :footer-fixed="preferences.footer.fixed"
+    :footer-height="preferences.footer.height"
+    :header-height="preferences.header.height"
     :header-hidden="preferences.header.hidden"
     :header-mode="preferences.header.mode"
     :header-theme="headerTheme"
@@ -196,11 +204,15 @@ const headerSlots = computed(() => {
     :sidebar-fixed-button="preferences.sidebar.fixedButton"
     :sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
     :sidebar-extra-collapse="preferences.sidebar.extraCollapse"
+    :sidebar-extra-collapsed-width="preferences.sidebar.extraCollapsedWidth"
     :sidebar-hidden="preferences.sidebar.hidden"
+    :sidebar-mixed-width="preferences.sidebar.mixedWidth"
     :sidebar-theme="sidebarTheme"
     :sidebar-width="preferences.sidebar.width"
+    :side-collapse-width="preferences.sidebar.collapseWidth"
     :tabbar-enable="preferences.tabbar.enable"
     :tabbar-height="preferences.tabbar.height"
+    :z-index="preferences.app.zIndex"
     @side-mouse-leave="handleSideMouseLeave"
     @toggle-sidebar="toggleSidebar"
     @update:sidebar-collapse="
@@ -222,6 +234,7 @@ const headerSlots = computed(() => {
     <template #logo>
       <VbenLogo
         v-if="preferences.logo.enable"
+        :fit="preferences.logo.fit"
         :class="logoClass"
         :collapsed="logoCollapsed"
         :src="preferences.logo.source"
@@ -312,6 +325,7 @@ const headerSlots = computed(() => {
     <template #side-extra-title>
       <VbenLogo
         v-if="preferences.logo.enable"
+        :fit="preferences.logo.fit"
         :text="preferences.app.name"
         :theme="theme"
       >

+ 4 - 1
packages/effects/layouts/src/basic/menu/use-mixed-menu.ts

@@ -140,7 +140,10 @@ function useMixedMenu() {
   watch(
     () => route.path,
     (path) => {
-      const currentPath = (route?.meta?.activePath as string) ?? path;
+      const currentPath = route?.meta?.activePath ?? route?.meta?.link ?? path;
+      if (willOpenedByWindow(currentPath)) {
+        return;
+      }
       calcSideMenus(currentPath);
       if (rootMenuPath.value)
         defaultSubMap.set(rootMenuPath.value, currentPath);

+ 1 - 1
packages/effects/layouts/src/basic/tabbar/tabbar.vue

@@ -30,7 +30,7 @@ const {
 } = useTabbar();
 
 const menus = computed(() => {
-  const tab = tabbarStore.getTabByPath(currentActive.value);
+  const tab = tabbarStore.getTabByKey(currentActive.value);
   const menus = createContextMenus(tab);
   return menus.map((item) => {
     return {

+ 9 - 5
packages/effects/layouts/src/basic/tabbar/use-tabbar.ts

@@ -22,7 +22,7 @@ import {
   X,
 } from '@vben/icons';
 import { $t, useI18n } from '@vben/locales';
-import { useAccessStore, useTabbarStore } from '@vben/stores';
+import { getTabKey, useAccessStore, useTabbarStore } from '@vben/stores';
 import { filterTree } from '@vben/utils';
 
 export function useTabbar() {
@@ -44,8 +44,11 @@ export function useTabbar() {
     toggleTabPin,
   } = useTabs();
 
+  /**
+   * 当前路径对应的tab的key
+   */
   const currentActive = computed(() => {
-    return route.fullPath;
+    return getTabKey(route);
   });
 
   const { locale } = useI18n();
@@ -73,7 +76,8 @@ export function useTabbar() {
 
   // 点击tab,跳转路由
   const handleClick = (key: string) => {
-    router.push(key);
+    const { fullPath, path } = tabbarStore.getTabByKey(key);
+    router.push(fullPath || path);
   };
 
   // 关闭tab
@@ -100,7 +104,7 @@ export function useTabbar() {
   );
 
   watch(
-    () => route.path,
+    () => route.fullPath,
     () => {
       const meta = route.matched?.[route.matched.length - 1]?.meta;
       tabbarStore.addTab({
@@ -158,7 +162,7 @@ export function useTabbar() {
       },
       {
         disabled: disabledRefresh,
-        handler: refreshTab,
+        handler: () => refreshTab(),
         icon: RotateCw,
         key: 'reload',
         text: $t('preferences.tabbar.contextMenu.reload'),

+ 2 - 1
packages/effects/layouts/src/widgets/check-updates/check-updates.vue

@@ -6,7 +6,7 @@ import { $t } from '@vben/locales';
 import { useVbenModal } from '@vben-core/popup-ui';
 
 interface Props {
-  // 轮时间,分钟
+  // 轮时间,分钟
   checkUpdatesInterval?: number;
   // 检查更新的地址
   checkUpdateUrl?: string;
@@ -46,6 +46,7 @@ async function getVersionTag() {
     const response = await fetch(props.checkUpdateUrl, {
       cache: 'no-cache',
       method: 'HEAD',
+      redirect: 'manual',
     });
 
     return (

+ 5 - 1
packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue

@@ -46,7 +46,11 @@ interface Props {
   /**
    * 菜单数组
    */
-  menus?: Array<{ handler: AnyFunction; icon?: Component; text: string }>;
+  menus?: Array<{
+    handler: AnyFunction;
+    icon?: Component | Function | string;
+    text: string;
+  }>;
 
   /**
    * 标签文本

+ 5 - 1
packages/effects/plugins/src/vxe-table/index.ts

@@ -3,4 +3,8 @@ export type { VxeTableGridOptions } from './types';
 export * from './use-vxe-grid';
 
 export { default as VbenVxeGrid } from './use-vxe-grid.vue';
-export type { VxeGridListeners, VxeGridProps } from 'vxe-table';
+export type {
+  VxeGridListeners,
+  VxeGridProps,
+  VxeGridPropTypes,
+} from 'vxe-table';

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

@@ -59,6 +59,7 @@ const FORM_SLOT_PREFIX = 'form-';
 
 const TOOLBAR_ACTIONS = 'toolbar-actions';
 const TOOLBAR_TOOLS = 'toolbar-tools';
+const TABLE_TITLE = 'table-title';
 
 const gridRef = useTemplateRef<VxeGridInstance>('gridRef');
 
@@ -129,7 +130,7 @@ const [Form, formApi] = useTableForm({
 });
 
 const showTableTitle = computed(() => {
-  return !!slots.tableTitle?.() || tableTitle.value;
+  return !!slots[TABLE_TITLE]?.() || tableTitle.value;
 });
 
 const showToolbar = computed(() => {
@@ -277,6 +278,15 @@ const delegatedFormSlots = computed(() => {
   return resultSlots.map((key) => key.replace(FORM_SLOT_PREFIX, ''));
 });
 
+const showDefaultEmpty = computed(() => {
+  // 检查是否有原生的 VXE Table 空状态配置
+  const hasEmptyText = options.value.emptyText !== undefined;
+  const hasEmptyRender = options.value.emptyRender !== undefined;
+
+  // 如果有原生配置,就不显示默认的空状态
+  return !hasEmptyText && !hasEmptyRender;
+});
+
 async function init() {
   await nextTick();
   const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
@@ -458,7 +468,7 @@ onUnmounted(() => {
         </slot>
       </template>
       <!-- 统一控状态 -->
-      <template #empty>
+      <template v-if="showDefaultEmpty" #empty>
         <slot name="empty">
           <EmptyIcon class="mx-auto" />
           <div class="mt-2">{{ $t('common.noData') }}</div>

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

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

+ 1 - 1
packages/icons/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/icons",
-  "version": "5.5.6",
+  "version": "5.5.7",
   "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.5.6",
+  "version": "5.5.7",
   "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.5.6",
+  "version": "5.5.7",
   "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.5.6",
+  "version": "5.5.7",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 22 - 17
packages/stores/src/modules/tabbar.test.ts

@@ -22,12 +22,13 @@ describe('useAccessStore', () => {
     const tab: any = {
       fullPath: '/home',
       meta: {},
+      key: '/home',
       name: 'Home',
       path: '/home',
     };
-    store.addTab(tab);
+    const addNewTab = store.addTab(tab);
     expect(store.tabs.length).toBe(1);
-    expect(store.tabs[0]).toEqual(tab);
+    expect(store.tabs[0]).toEqual(addNewTab);
   });
 
   it('adds a new tab if it does not exist', () => {
@@ -38,20 +39,22 @@ describe('useAccessStore', () => {
       name: 'New',
       path: '/new',
     };
-    store.addTab(newTab);
-    expect(store.tabs).toContainEqual(newTab);
+    const addNewTab = store.addTab(newTab);
+    expect(store.tabs).toContainEqual(addNewTab);
   });
 
   it('updates an existing tab instead of adding a new one', () => {
     const store = useTabbarStore();
     const initialTab: any = {
       fullPath: '/existing',
-      meta: {},
+      meta: {
+        fullPathKey: false,
+      },
       name: 'Existing',
       path: '/existing',
       query: {},
     };
-    store.tabs.push(initialTab);
+    store.addTab(initialTab);
     const updatedTab = { ...initialTab, query: { id: '1' } };
     store.addTab(updatedTab);
     expect(store.tabs.length).toBe(1);
@@ -60,9 +63,12 @@ describe('useAccessStore', () => {
 
   it('closes all tabs', async () => {
     const store = useTabbarStore();
-    store.tabs = [
-      { fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
-    ] as any;
+    store.addTab({
+      fullPath: '/home',
+      meta: {},
+      name: 'Home',
+      path: '/home',
+    } as any);
     router.replace = vi.fn();
 
     await store.closeAllTabs(router);
@@ -157,7 +163,7 @@ describe('useAccessStore', () => {
       path: '/contact',
     } as any);
 
-    await store._bulkCloseByPaths(['/home', '/contact']);
+    await store._bulkCloseByKeys(['/home', '/contact']);
 
     expect(store.tabs).toHaveLength(1);
     expect(store.tabs[0]?.name).toBe('About');
@@ -183,9 +189,8 @@ describe('useAccessStore', () => {
       name: 'Contact',
       path: '/contact',
     };
-    store.addTab(targetTab);
-
-    await store.closeLeftTabs(targetTab);
+    const addTargetTab = store.addTab(targetTab);
+    await store.closeLeftTabs(addTargetTab);
 
     expect(store.tabs).toHaveLength(1);
     expect(store.tabs[0]?.name).toBe('Contact');
@@ -205,7 +210,7 @@ describe('useAccessStore', () => {
       name: 'About',
       path: '/about',
     };
-    store.addTab(targetTab);
+    const addTargetTab = store.addTab(targetTab);
     store.addTab({
       fullPath: '/contact',
       meta: {},
@@ -213,7 +218,7 @@ describe('useAccessStore', () => {
       path: '/contact',
     } as any);
 
-    await store.closeOtherTabs(targetTab);
+    await store.closeOtherTabs(addTargetTab);
 
     expect(store.tabs).toHaveLength(1);
     expect(store.tabs[0]?.name).toBe('About');
@@ -227,7 +232,7 @@ describe('useAccessStore', () => {
       name: 'Home',
       path: '/home',
     };
-    store.addTab(targetTab);
+    const addTargetTab = store.addTab(targetTab);
     store.addTab({
       fullPath: '/about',
       meta: {},
@@ -241,7 +246,7 @@ describe('useAccessStore', () => {
       path: '/contact',
     } as any);
 
-    await store.closeRightTabs(targetTab);
+    await store.closeRightTabs(addTargetTab);
 
     expect(store.tabs).toHaveLength(1);
     expect(store.tabs[0]?.name).toBe('Home');

+ 142 - 76
packages/stores/src/modules/tabbar.ts

@@ -1,4 +1,9 @@
-import type { Router, RouteRecordNormalized } from 'vue-router';
+import type { ComputedRef } from 'vue';
+import type {
+  RouteLocationNormalized,
+  Router,
+  RouteRecordNormalized,
+} from 'vue-router';
 
 import type { TabDefinition } from '@vben-core/typings';
 
@@ -52,23 +57,23 @@ export const useTabbarStore = defineStore('core-tabbar', {
     /**
      * Close tabs in bulk
      */
-    async _bulkCloseByPaths(paths: string[]) {
-      this.tabs = this.tabs.filter((item) => {
-        return !paths.includes(getTabPath(item));
-      });
+    async _bulkCloseByKeys(keys: string[]) {
+      const keySet = new Set(keys);
+      this.tabs = this.tabs.filter(
+        (item) => !keySet.has(getTabKeyFromTab(item)),
+      );
 
-      this.updateCacheTabs();
+      await this.updateCacheTabs();
     },
     /**
      * @zh_CN 关闭标签页
      * @param tab
      */
     _close(tab: TabDefinition) {
-      const { fullPath } = tab;
       if (isAffixTab(tab)) {
         return;
       }
-      const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
+      const index = this.tabs.findIndex((item) => equalTab(item, tab));
       index !== -1 && this.tabs.splice(index, 1);
     },
     /**
@@ -101,14 +106,17 @@ export const useTabbarStore = defineStore('core-tabbar', {
      * @zh_CN 添加标签页
      * @param routeTab
      */
-    addTab(routeTab: TabDefinition) {
-      const tab = cloneTab(routeTab);
+    addTab(routeTab: TabDefinition): TabDefinition {
+      let tab = cloneTab(routeTab);
+      if (!tab.key) {
+        tab.key = getTabKey(routeTab);
+      }
       if (!isTabShown(tab)) {
-        return;
+        return tab;
       }
 
-      const tabIndex = this.tabs.findIndex((tab) => {
-        return getTabPath(tab) === getTabPath(routeTab);
+      const tabIndex = this.tabs.findIndex((item) => {
+        return equalTab(item, tab);
       });
 
       if (tabIndex === -1) {
@@ -154,10 +162,11 @@ export const useTabbarStore = defineStore('core-tabbar', {
             mergedTab.meta.newTabTitle = curMeta.newTabTitle;
           }
         }
-
+        tab = mergedTab;
         this.tabs.splice(tabIndex, 1, mergedTab);
       }
       this.updateCacheTabs();
+      return tab;
     },
     /**
      * @zh_CN 关闭所有标签页
@@ -173,65 +182,63 @@ export const useTabbarStore = defineStore('core-tabbar', {
      * @param tab
      */
     async closeLeftTabs(tab: TabDefinition) {
-      const index = this.tabs.findIndex(
-        (item) => getTabPath(item) === getTabPath(tab),
-      );
+      const index = this.tabs.findIndex((item) => equalTab(item, tab));
 
       if (index < 1) {
         return;
       }
 
       const leftTabs = this.tabs.slice(0, index);
-      const paths: string[] = [];
+      const keys: string[] = [];
 
       for (const item of leftTabs) {
         if (!isAffixTab(item)) {
-          paths.push(getTabPath(item));
+          keys.push(item.key as string);
         }
       }
-      await this._bulkCloseByPaths(paths);
+      await this._bulkCloseByKeys(keys);
     },
     /**
      * @zh_CN 关闭其他标签页
      * @param tab
      */
     async closeOtherTabs(tab: TabDefinition) {
-      const closePaths = this.tabs.map((item) => getTabPath(item));
+      const closeKeys = this.tabs.map((item) => getTabKeyFromTab(item));
 
-      const paths: string[] = [];
+      const keys: string[] = [];
 
-      for (const path of closePaths) {
-        if (path !== tab.fullPath) {
-          const closeTab = this.tabs.find((item) => getTabPath(item) === path);
+      for (const key of closeKeys) {
+        if (key !== getTabKeyFromTab(tab)) {
+          const closeTab = this.tabs.find(
+            (item) => getTabKeyFromTab(item) === key,
+          );
           if (!closeTab) {
             continue;
           }
           if (!isAffixTab(closeTab)) {
-            paths.push(getTabPath(closeTab));
+            keys.push(closeTab.key as string);
           }
         }
       }
-      await this._bulkCloseByPaths(paths);
+      await this._bulkCloseByKeys(keys);
     },
     /**
      * @zh_CN 关闭右侧标签页
      * @param tab
      */
     async closeRightTabs(tab: TabDefinition) {
-      const index = this.tabs.findIndex(
-        (item) => getTabPath(item) === getTabPath(tab),
-      );
+      const index = this.tabs.findIndex((item) => equalTab(item, tab));
 
       if (index !== -1 && index < this.tabs.length - 1) {
         const rightTabs = this.tabs.slice(index + 1);
 
-        const paths: string[] = [];
+        const keys: string[] = [];
         for (const item of rightTabs) {
           if (!isAffixTab(item)) {
-            paths.push(getTabPath(item));
+            keys.push(item.key as string);
           }
         }
-        await this._bulkCloseByPaths(paths);
+        await this._bulkCloseByKeys(keys);
       }
     },
 
@@ -242,15 +249,14 @@ export const useTabbarStore = defineStore('core-tabbar', {
      */
     async closeTab(tab: TabDefinition, router: Router) {
       const { currentRoute } = router;
-
       // 关闭不是激活选项卡
-      if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
+      if (getTabKey(currentRoute.value) !== getTabKeyFromTab(tab)) {
         this._close(tab);
         this.updateCacheTabs();
         return;
       }
       const index = this.getTabs.findIndex(
-        (item) => getTabPath(item) === getTabPath(currentRoute.value),
+        (item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value),
       );
 
       const before = this.getTabs[index - 1];
@@ -277,7 +283,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
     async closeTabByKey(key: string, router: Router) {
       const originKey = decodeURIComponent(key);
       const index = this.tabs.findIndex(
-        (item) => getTabPath(item) === originKey,
+        (item) => getTabKeyFromTab(item) === originKey,
       );
       if (index === -1) {
         return;
@@ -290,12 +296,12 @@ export const useTabbarStore = defineStore('core-tabbar', {
     },
 
     /**
-     * 根据路径获取标签页
-     * @param path
+     * 根据tab的key获取tab
+     * @param key
      */
-    getTabByPath(path: string) {
+    getTabByKey(key: string) {
       return this.getTabs.find(
-        (item) => getTabPath(item) === path,
+        (item) => getTabKeyFromTab(item) === key,
       ) as TabDefinition;
     },
     /**
@@ -311,22 +317,19 @@ export const useTabbarStore = defineStore('core-tabbar', {
      * @param tab
      */
     async pinTab(tab: TabDefinition) {
-      const index = this.tabs.findIndex(
-        (item) => getTabPath(item) === getTabPath(tab),
-      );
-      if (index !== -1) {
-        const oldTab = this.tabs[index];
-        tab.meta.affixTab = true;
-        tab.meta.title = oldTab?.meta?.title as string;
-        // this.addTab(tab);
-        this.tabs.splice(index, 1, tab);
+      const index = this.tabs.findIndex((item) => equalTab(item, tab));
+      if (index === -1) {
+        return;
       }
+      const oldTab = this.tabs[index];
+      tab.meta.affixTab = true;
+      tab.meta.title = oldTab?.meta?.title as string;
+      // this.addTab(tab);
+      this.tabs.splice(index, 1, tab);
       // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
       const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
       // 获得固定tabs的index
-      const newIndex = affixTabs.findIndex(
-        (item) => getTabPath(item) === getTabPath(tab),
-      );
+      const newIndex = affixTabs.findIndex((item) => equalTab(item, tab));
       // 交换位置重新排序
       await this.sortTabs(index, newIndex);
     },
@@ -334,7 +337,13 @@ export const useTabbarStore = defineStore('core-tabbar', {
     /**
      * 刷新标签页
      */
-    async refresh(router: Router) {
+    async refresh(router: Router | string) {
+      // 如果是Router路由,那么就根据当前路由刷新
+      // 如果是string字符串,为路由名称,则定向刷新指定标签页,不能是当前路由名称,否则不会刷新
+      if (typeof router === 'string') {
+        return await this.refreshByName(router);
+      }
+
       const { currentRoute } = router;
       const { name } = currentRoute.value;
 
@@ -350,15 +359,22 @@ export const useTabbarStore = defineStore('core-tabbar', {
     },
 
     /**
+     * 根据路由名称刷新指定标签页
+     */
+    async refreshByName(name: string) {
+      this.excludeCachedTabs.add(name);
+      await new Promise((resolve) => setTimeout(resolve, 200));
+      this.excludeCachedTabs.delete(name);
+    },
+
+    /**
      * @zh_CN 重置标签页标题
      */
     async resetTabTitle(tab: TabDefinition) {
       if (tab?.meta?.newTabTitle) {
         return;
       }
-      const findTab = this.tabs.find(
-        (item) => getTabPath(item) === getTabPath(tab),
-      );
+      const findTab = this.tabs.find((item) => equalTab(item, tab));
       if (findTab) {
         findTab.meta.newTabTitle = undefined;
         await this.updateCacheTabs();
@@ -386,13 +402,24 @@ export const useTabbarStore = defineStore('core-tabbar', {
 
     /**
      * @zh_CN 设置标签页标题
-     * @param tab
-     * @param title
+     *
+     * @zh_CN 支持设置静态标题字符串或计算属性作为动态标题
+     * @zh_CN 当标题为计算属性时,标题会随计算属性值变化而自动更新
+     * @zh_CN 适用于需要根据状态或多语言动态更新标题的场景
+     *
+     * @param {TabDefinition} tab - 标签页对象
+     * @param {ComputedRef<string> | string} title - 标题内容,支持静态字符串或计算属性
+     *
+     * @example
+     * // 设置静态标题
+     * setTabTitle(tab, '新标签页');
+     *
+     * @example
+     * // 设置动态标题
+     * setTabTitle(tab, computed(() => t('common.dashboard')));
      */
-    async setTabTitle(tab: TabDefinition, title: string) {
-      const findTab = this.tabs.find(
-        (item) => getTabPath(item) === getTabPath(tab),
-      );
+    async setTabTitle(tab: TabDefinition, title: ComputedRef<string> | string) {
+      const findTab = this.tabs.find((item) => equalTab(item, tab));
 
       if (findTab) {
         findTab.meta.newTabTitle = title;
@@ -433,17 +460,15 @@ export const useTabbarStore = defineStore('core-tabbar', {
      * @param tab
      */
     async unpinTab(tab: TabDefinition) {
-      const index = this.tabs.findIndex(
-        (item) => getTabPath(item) === getTabPath(tab),
-      );
-
-      if (index !== -1) {
-        const oldTab = this.tabs[index];
-        tab.meta.affixTab = false;
-        tab.meta.title = oldTab?.meta?.title as string;
-        // this.addTab(tab);
-        this.tabs.splice(index, 1, tab);
+      const index = this.tabs.findIndex((item) => equalTab(item, tab));
+      if (index === -1) {
+        return;
       }
+      const oldTab = this.tabs[index];
+      tab.meta.affixTab = false;
+      tab.meta.title = oldTab?.meta?.title as string;
+      // this.addTab(tab);
+      this.tabs.splice(index, 1, tab);
       // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
       const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
       // 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置
@@ -576,11 +601,49 @@ function isTabShown(tab: TabDefinition) {
 }
 
 /**
- * @zh_CN 获取标签页路径
+ * 从route获取tab页的key
  * @param tab
  */
-function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
-  return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
+function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
+  const {
+    fullPath,
+    path,
+    meta: { fullPathKey } = {},
+    query = {},
+  } = tab as RouteLocationNormalized;
+  // pageKey可能是数组(查询参数重复时可能出现)
+  const pageKey = Array.isArray(query.pageKey)
+    ? query.pageKey[0]
+    : query.pageKey;
+  let rawKey;
+  if (pageKey) {
+    rawKey = pageKey;
+  } else {
+    rawKey = fullPathKey === false ? path : (fullPath ?? path);
+  }
+  try {
+    return decodeURIComponent(rawKey);
+  } catch {
+    return rawKey;
+  }
+}
+
+/**
+ * 从tab获取tab页的key
+ * 如果tab没有key,那么就从route获取key
+ * @param tab
+ */
+function getTabKeyFromTab(tab: TabDefinition): string {
+  return tab.key ?? getTabKey(tab);
+}
+
+/**
+ * 比较两个tab是否相等
+ * @param a
+ * @param b
+ */
+function equalTab(a: TabDefinition, b: TabDefinition) {
+  return getTabKeyFromTab(a) === getTabKeyFromTab(b);
 }
 
 function routeToTab(route: RouteRecordNormalized) {
@@ -588,5 +651,8 @@ function routeToTab(route: RouteRecordNormalized) {
     meta: route.meta,
     name: route.name,
     path: route.path,
+    key: getTabKey(route),
   } as TabDefinition;
 }
+
+export { getTabKey };

+ 1 - 1
packages/styles/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vben/styles",
-  "version": "5.5.6",
+  "version": "5.5.7",
   "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.5.6",
+  "version": "5.5.7",
   "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.5.6",
+  "version": "5.5.7",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {

+ 10 - 0
playground/src/api/examples/json-bigint.ts

@@ -0,0 +1,10 @@
+import { requestClient } from '#/api/request';
+
+/**
+ * 发起请求
+ */
+async function getBigIntData() {
+  return requestClient.get('/demo/bigint');
+}
+
+export { getBigIntData };

+ 39 - 0
playground/src/views/demos/features/json-bigint/index.vue

@@ -0,0 +1,39 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+
+import { Alert, Button, Card } from 'ant-design-vue';
+
+import { getBigIntData } from '#/api/examples/json-bigint';
+
+const response = ref('');
+function fetchData() {
+  getBigIntData().then((res) => {
+    response.value = res;
+  });
+}
+</script>
+<template>
+  <Page
+    title="JSON BigInt Support"
+    description="解析后端返回的长整数(long/bigInt)。代码位置:playground/src/api/request.ts中的transformResponse"
+  >
+    <Card>
+      <Alert>
+        <template #message>
+          有些后端接口返回的ID是长整数,但javascript原生的JSON解析是不支持超过2^53-1的长整数的。
+          这种情况可以建议后端返回数据前将长整数转换为字符串类型。如果后端不接受我们的建议😡……
+          <br />
+          下面的按钮点击后会发起请求,接口返回的JSON数据中的id字段是超出整数范围的数字,已自动将其解析为字符串
+        </template>
+      </Alert>
+      <Button class="mt-4" type="primary" @click="fetchData">发起请求</Button>
+      <div>
+        <pre>
+        {{ response }}
+        </pre>
+      </div>
+    </Card>
+  </Page>
+</template>

+ 2 - 0
pnpm-workspace.yaml

@@ -41,6 +41,7 @@ catalog:
   '@types/archiver': ^6.0.3
   '@types/eslint': ^9.6.1
   '@types/html-minifier-terser': ^7.0.2
+  '@types/json-bigint': ^1.0.4
   '@types/jsonwebtoken': ^9.0.9
   '@types/lodash.clonedeep': ^4.5.9
   '@types/lodash.get': ^4.4.9
@@ -112,6 +113,7 @@ catalog:
   happy-dom: ^17.4.6
   html-minifier-terser: ^7.2.0
   is-ci: ^4.1.0
+  json-bigint: ^1.0.0
   jsonc-eslint-parser: ^2.4.0
   jsonwebtoken: ^9.0.2
   lefthook: ^1.11.12

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

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