mirror of
				https://github.com/soybeanjs/soybean-admin.git
				synced 2025-11-04 15:53:43 +08:00 
			
		
		
		
	feat(projects): 面包屑
This commit is contained in:
		@@ -1,7 +1,18 @@
 | 
			
		||||
import type { MenuOption } from 'naive-ui';
 | 
			
		||||
import type { MenuOption, DropdownOption } from 'naive-ui';
 | 
			
		||||
 | 
			
		||||
/** 菜单项配置 */
 | 
			
		||||
export type GlobalMenuOption = MenuOption & {
 | 
			
		||||
  routeName: string;
 | 
			
		||||
  routePath: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** 面包屑 */
 | 
			
		||||
export type GlobalBreadcrumb = DropdownOption & {
 | 
			
		||||
  key: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
  disabled: boolean;
 | 
			
		||||
  routeName: string;
 | 
			
		||||
  hasChildren: boolean;
 | 
			
		||||
  iconName?: string;
 | 
			
		||||
  children?: GlobalBreadcrumb[];
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-breadcrumb class="px-12px">
 | 
			
		||||
    <template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.key">
 | 
			
		||||
      <n-breadcrumb-item>
 | 
			
		||||
        <n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
 | 
			
		||||
          <span>
 | 
			
		||||
            <component :is="breadcrumb.icon" v-if="theme.header.crumb.showIcon" class="inline-block mr-4px text-16px" />
 | 
			
		||||
            <span>{{ breadcrumb.label }}</span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </n-dropdown>
 | 
			
		||||
        <template v-else>
 | 
			
		||||
          <component :is="breadcrumb.icon" v-if="theme.header.crumb.showIcon" class="inline-block mr-4px text-16px" />
 | 
			
		||||
          <span>{{ breadcrumb.label }}</span>
 | 
			
		||||
        </template>
 | 
			
		||||
      </n-breadcrumb-item>
 | 
			
		||||
    </template>
 | 
			
		||||
  </n-breadcrumb>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { NBreadcrumb, NBreadcrumbItem, NDropdown } from 'naive-ui';
 | 
			
		||||
import { routePath } from '@/router';
 | 
			
		||||
import { useThemeStore, useRouteStore } from '@/store';
 | 
			
		||||
import { useRouterPush } from '@/composables';
 | 
			
		||||
import { getBreadcrumbByRouteKey } from '@/utils';
 | 
			
		||||
import type { GlobalMenuOption } from '@/interface';
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const theme = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPush } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
const breadcrumbs = computed(() =>
 | 
			
		||||
  getBreadcrumbByRouteKey(route.name as string, routeStore.menus as GlobalMenuOption[], routePath('root'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function dropdownSelect(key: string) {
 | 
			
		||||
  routerPush({ name: key });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										26
									
								
								src/layouts/common/GlobalHeader/components/HeaderMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/layouts/common/GlobalHeader/components/HeaderMenu.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-menu :value="activeKey" mode="horizontal" :options="menus" @update:value="handleUpdateMenu" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { NMenu } from 'naive-ui';
 | 
			
		||||
import type { MenuOption } from 'naive-ui';
 | 
			
		||||
import { useRouteStore } from '@/store';
 | 
			
		||||
import { useRouterPush } from '@/composables';
 | 
			
		||||
import type { GlobalMenuOption } from '@/interface';
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPush } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
 | 
			
		||||
const activeKey = computed(() => route.name as string);
 | 
			
		||||
 | 
			
		||||
function handleUpdateMenu(_key: string, item: MenuOption) {
 | 
			
		||||
  const menuItem = item as GlobalMenuOption;
 | 
			
		||||
  routerPush(menuItem.routePath);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import MenuCollapse from './MenuCollapse.vue';
 | 
			
		||||
import GlobalBreadcrumb from './GlobalBreadcrumb.vue';
 | 
			
		||||
import HeaderMenu from './HeaderMenu.vue';
 | 
			
		||||
import GithubSite from './GithubSite.vue';
 | 
			
		||||
import FullScreen from './FullScreen.vue';
 | 
			
		||||
import ThemeMode from './ThemeMode.vue';
 | 
			
		||||
import UserAvatar from './UserAvatar.vue';
 | 
			
		||||
 | 
			
		||||
export { MenuCollapse, GithubSite, FullScreen, ThemeMode, UserAvatar };
 | 
			
		||||
export { MenuCollapse, GlobalBreadcrumb, HeaderMenu, GithubSite, FullScreen, ThemeMode, UserAvatar };
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,11 @@
 | 
			
		||||
    <global-logo v-if="showLogo" :show-title="true" class="h-full" :style="{ width: theme.sider.width + 'px' }" />
 | 
			
		||||
    <div v-if="!showHeaderMenu" class="flex-1-hidden flex-y-center h-full">
 | 
			
		||||
      <menu-collapse v-if="showMenuCollape" />
 | 
			
		||||
      <!-- <global-breadcrumb v-if="theme.header.crumb.visible" /> -->
 | 
			
		||||
      <global-breadcrumb v-if="theme.header.crumb.visible" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-else class="flex-1-hidden flex-y-center h-full" :style="{ justifyContent: theme.menu.horizontalPosition }">
 | 
			
		||||
      <header-menu />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
      v-else
 | 
			
		||||
      class="flex-1-hidden flex-y-center h-full"
 | 
			
		||||
      :style="{ justifyContent: theme.menu.horizontalPosition }"
 | 
			
		||||
    ></div>
 | 
			
		||||
    <div class="flex justify-end h-full">
 | 
			
		||||
      <github-site />
 | 
			
		||||
      <full-screen />
 | 
			
		||||
@@ -24,7 +22,15 @@ import { DarkModeContainer } from '@/components';
 | 
			
		||||
import { useThemeStore } from '@/store';
 | 
			
		||||
import type { GlobalHeaderProps } from '@/interface';
 | 
			
		||||
import GlobalLogo from '../GlobalLogo/index.vue';
 | 
			
		||||
import { MenuCollapse, GithubSite, FullScreen, ThemeMode, UserAvatar } from './components';
 | 
			
		||||
import {
 | 
			
		||||
  MenuCollapse,
 | 
			
		||||
  GlobalBreadcrumb,
 | 
			
		||||
  HeaderMenu,
 | 
			
		||||
  GithubSite,
 | 
			
		||||
  FullScreen,
 | 
			
		||||
  ThemeMode,
 | 
			
		||||
  UserAvatar
 | 
			
		||||
} from './components';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  /** 显示logo */
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ export const constantRoutes: AuthRoute.Route[] = [
 | 
			
		||||
      singleLayout: 'blank'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  // 匹配无效的路径重定向not-found的页面
 | 
			
		||||
  // 匹配无效路径的路由
 | 
			
		||||
  {
 | 
			
		||||
    name: 'not-found-page',
 | 
			
		||||
    path: '/:pathMatch(.*)*',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								src/utils/router/breadcrumb.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/utils/router/breadcrumb.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import type { GlobalMenuOption, GlobalBreadcrumb } from '@/interface';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取面包屑数据
 | 
			
		||||
 * @param activeKey - 当前页面路由的key
 | 
			
		||||
 * @param menus - 菜单数据
 | 
			
		||||
 * @param rootPath - 根路由路径
 | 
			
		||||
 */
 | 
			
		||||
export function getBreadcrumbByRouteKey(activeKey: string, menus: GlobalMenuOption[], rootPath: string) {
 | 
			
		||||
  return menus.map(menu => getBreadcrumbItem(activeKey, menu, rootPath)).flat(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getBreadcrumbItem(activeKey: string, menu: GlobalMenuOption, rootPath: string) {
 | 
			
		||||
  const list: GlobalBreadcrumb[] = [];
 | 
			
		||||
  if (activeKey.includes(menu.routeName)) {
 | 
			
		||||
    const breadcrumb: GlobalBreadcrumb = {
 | 
			
		||||
      key: menu.routeName,
 | 
			
		||||
      label: menu.label as string,
 | 
			
		||||
      routeName: menu.routeName,
 | 
			
		||||
      disabled: menu.routePath === rootPath,
 | 
			
		||||
      hasChildren: false
 | 
			
		||||
    };
 | 
			
		||||
    if (menu.icon) {
 | 
			
		||||
      breadcrumb.icon = menu.icon;
 | 
			
		||||
    }
 | 
			
		||||
    if (menu.children && menu.children.length) {
 | 
			
		||||
      breadcrumb.hasChildren = true;
 | 
			
		||||
      breadcrumb.children = menu.children
 | 
			
		||||
        .map(item => getBreadcrumbItem(activeKey, item as GlobalMenuOption, rootPath))
 | 
			
		||||
        .flat(1);
 | 
			
		||||
    }
 | 
			
		||||
    list.push(breadcrumb);
 | 
			
		||||
  }
 | 
			
		||||
  return list;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
export * from './helpers';
 | 
			
		||||
export * from './menu';
 | 
			
		||||
export * from './breadcrumb';
 | 
			
		||||
export * from './regexp';
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user