Browse Source

feat: menu routing support opens in a new window (#4715)

Vben 5 months ago
parent
commit
23768ea620

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

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

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

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

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

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

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

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

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

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

+ 8 - 6
packages/effects/layouts/src/basic/menu/use-navigation.ts

@@ -1,23 +1,25 @@
-import { useRouter } from 'vue-router';
+import { type RouteRecordNormalized, useRouter } from 'vue-router';
 
-import { isHttpUrl, openWindow } from '@vben/utils';
+import { isHttpUrl, openRouteInNewWindow, openWindow } from '@vben/utils';
 
 function useNavigation() {
   const router = useRouter();
   const routes = router.getRoutes();
 
-  const routeMetaMap = new Map<string, any>();
+  const routeMetaMap = new Map<string, RouteRecordNormalized>();
 
   routes.forEach((route) => {
-    routeMetaMap.set(route.path, route.meta);
+    routeMetaMap.set(route.path, route);
   });
 
   const navigation = async (path: string) => {
+    const route = routeMetaMap.get(path);
+    const { openInNewWindow = false, query = {} } = route?.meta ?? {};
     if (isHttpUrl(path)) {
       openWindow(path, { target: '_blank' });
+    } else if (openInNewWindow) {
+      openRouteInNewWindow(path);
     } else {
-      const meta = routeMetaMap.get(path);
-      const query = meta?.query ?? {};
       await router.push({
         path,
         query,

+ 2 - 6
packages/stores/src/modules/tabbar.ts

@@ -4,7 +4,7 @@ import type { Router, RouteRecordNormalized } from 'vue-router';
 import { toRaw } from 'vue';
 
 import {
-  openWindow,
+  openRouteInNewWindow,
   startProgress,
   stopProgress,
 } from '@vben-core/shared/utils';
@@ -290,11 +290,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
      * @param tab
      */
     async openTabInNewWindow(tab: TabDefinition) {
-      const { hash, origin } = location;
-      const path = tab.fullPath || tab.path;
-      const fullPath = path.startsWith('/') ? path : `/${path}`;
-      const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
-      openWindow(url, { target: '_blank' });
+      openRouteInNewWindow(tab.fullPath || tab.path);
     },
 
     /**

+ 2 - 1
playground/src/locales/langs/en-US/demos.json

@@ -48,7 +48,8 @@
     "tabDetail": "Tab Detail Page",
     "fullScreen": "FullScreen",
     "clipboard": "Clipboard",
-    "menuWithQuery": "Menu With Query"
+    "menuWithQuery": "Menu With Query",
+    "openInNewWindow": "Open in New Window"
   },
   "breadcrumb": {
     "navigation": "Breadcrumb Navigation",

+ 2 - 1
playground/src/locales/langs/zh-CN/demos.json

@@ -48,7 +48,8 @@
     "tabDetail": "标签详情页",
     "fullScreen": "全屏",
     "clipboard": "剪贴板",
-    "menuWithQuery": "带参菜单"
+    "menuWithQuery": "带参菜单",
+    "openInNewWindow": "新窗口打开"
   },
   "breadcrumb": {
     "navigation": "面包屑导航",

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

@@ -174,7 +174,7 @@ const routes: RouteRecordRaw[] = [
               import('#/views/demos/features/full-screen/index.vue'),
             meta: {
               icon: 'lucide:fullscreen',
-              title: $t('demos.features.title'),
+              title: $t('demos.features.fullScreen'),
             },
           },
           {
@@ -200,6 +200,17 @@ const routes: RouteRecordRaw[] = [
               title: $t('demos.features.menuWithQuery'),
             },
           },
+          {
+            name: 'NewWindowDemo',
+            path: '/demos/new-window',
+            component: () =>
+              import('#/views/demos/features/new-window/index.vue'),
+            meta: {
+              icon: 'lucide:app-window',
+              openInNewWindow: true,
+              title: $t('demos.features.openInNewWindow'),
+            },
+          },
           {
             name: 'VueQueryDemo',
             path: '/demos/features/vue-query',

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

@@ -0,0 +1,11 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/common-ui';
+</script>
+
+<template>
+  <Fallback
+    description="当前页面已在新窗口内打开"
+    status="coming-soon"
+    title="新窗口打开页面"
+  />
+</template>

File diff suppressed because it is too large
+ 230 - 158
pnpm-lock.yaml


+ 5 - 5
pnpm-workspace.yaml

@@ -43,13 +43,13 @@ catalog:
   '@types/html-minifier-terser': ^7.0.2
   '@types/jsonwebtoken': ^9.0.7
   '@types/lodash.clonedeep': ^4.5.9
-  '@types/node': ^22.7.7
+  '@types/node': ^22.7.8
   '@types/nprogress': ^0.2.3
   '@types/postcss-import': ^14.0.3
   '@types/qrcode': ^1.5.5
   '@types/sortablejs': ^1.15.8
-  '@typescript-eslint/eslint-plugin': ^8.10.0
-  '@typescript-eslint/parser': ^8.10.0
+  '@typescript-eslint/eslint-plugin': ^8.11.0
+  '@typescript-eslint/parser': ^8.11.0
   '@vee-validate/zod': ^4.14.3
   '@vite-pwa/vitepress': ^0.5.3
   '@vitejs/plugin-vue': ^5.1.4
@@ -84,7 +84,7 @@ catalog:
   echarts: ^5.5.1
   element-plus: ^2.8.6
   eslint: ^9.13.0
-  eslint-config-turbo: ^2.2.1
+  eslint-config-turbo: ^2.2.3
   eslint-plugin-command: ^0.2.6
   eslint-plugin-eslint-comments: ^3.2.0
   eslint-plugin-import-x: ^4.3.1
@@ -153,7 +153,7 @@ catalog:
   tailwindcss: ^3.4.14
   tailwindcss-animate: ^1.0.7
   theme-colors: ^0.1.0
-  turbo: ^2.2.1
+  turbo: ^2.2.3
   typescript: ^5.6.3
   unbuild: ^2.0.0
   unplugin-element-plus: ^0.8.0

Some files were not shown because too many files changed in this diff