feat(components): 添加面包屑
This commit is contained in:
parent
667282f81a
commit
c1cdc3a9ed
@ -20,7 +20,7 @@ module.exports = {
|
|||||||
['components', '组件相关'],
|
['components', '组件相关'],
|
||||||
['hooks', 'hook 相关'],
|
['hooks', 'hook 相关'],
|
||||||
['utils', 'utils 相关'],
|
['utils', 'utils 相关'],
|
||||||
['element-ui', '对 element-ui 的调整'],
|
['types', 'ts类型相关'],
|
||||||
['styles', '样式相关'],
|
['styles', '样式相关'],
|
||||||
['deps', '项目依赖'],
|
['deps', '项目依赖'],
|
||||||
['auth', '对 auth 修改'],
|
['auth', '对 auth 修改'],
|
||||||
|
@ -72,6 +72,7 @@ soybean-admin
|
|||||||
│ │ ├── dark-mode.ts //windicss暗黑模式插件
|
│ │ ├── dark-mode.ts //windicss暗黑模式插件
|
||||||
│ │ └── smooth-scroll.ts //滚动平滑插件
|
│ │ └── smooth-scroll.ts //滚动平滑插件
|
||||||
│ ├── router //vue路由
|
│ ├── router //vue路由
|
||||||
|
│ │ ├── menus.ts //菜单
|
||||||
│ │ ├── permission.ts //路由守卫相关函数
|
│ │ ├── permission.ts //路由守卫相关函数
|
||||||
│ │ └── routes.ts //声明的路由
|
│ │ └── routes.ts //声明的路由
|
||||||
│ ├── service //网络请求
|
│ ├── service //网络请求
|
||||||
|
@ -6,7 +6,7 @@ export default [
|
|||||||
injectHtml({
|
injectHtml({
|
||||||
injectData: {
|
injectData: {
|
||||||
title: viteEnv.VITE_APP_TITLE,
|
title: viteEnv.VITE_APP_TITLE,
|
||||||
appName: viteEnv.VITE_APP_TITLE_Label
|
appName: viteEnv.VITE_APP_TITLE_LABEL
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
20
index.html
20
index.html
@ -8,7 +8,25 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="appProvider" style="display: none"></div>
|
<div id="appProvider" style="display: none"></div>
|
||||||
<div id="app"></div>
|
<div id="app">
|
||||||
|
<!-- 页面渲染之前加载动画 -->
|
||||||
|
<div class="app-loading">
|
||||||
|
<img class="app-loading_logo" src="/resource/logo.png" />
|
||||||
|
<div class="app-loading__dot-wrapper">
|
||||||
|
<div class="app-loading__dot">
|
||||||
|
<i class="left top"></i>
|
||||||
|
<i class="left bottom delay-400"></i>
|
||||||
|
<i class="right top delay-800"></i>
|
||||||
|
<i class="right bottom delay-1200"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="app-loading_title"><%= appName %></h2>
|
||||||
|
<style>
|
||||||
|
@import '/resource/loading.css';
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
<!-- End -->
|
||||||
|
</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
90
public/resource/loading.css
Normal file
90
public/resource/loading.css
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
.app-loading {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: -1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.app-loading_logo {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
|
.app-loading__dot-wrapper {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin: 36px 0;
|
||||||
|
}
|
||||||
|
.app-loading__dot {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
animation-name: loadingRotate;
|
||||||
|
animation-duration: 1.2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
@keyframes loadingRotate {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(405deg);
|
||||||
|
transform: rotate(405deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes loadingRotate {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(405deg);
|
||||||
|
transform: rotate(405deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-loading__dot > i {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #1890ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-transform: scale(0.75);
|
||||||
|
transform: scale(0.75);
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
opacity: 0.3;
|
||||||
|
animation: spinOpacity 1s infinite linear alternate;
|
||||||
|
}
|
||||||
|
@keyframes spinOpacity {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes spinOpacity {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.delay-400 {
|
||||||
|
animation-delay: 0.4s !important;
|
||||||
|
}
|
||||||
|
.delay-800 {
|
||||||
|
animation-delay: 0.8s !important;
|
||||||
|
}
|
||||||
|
.delay-1200 {
|
||||||
|
animation-delay: 1.2s !important;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.app-loading_title {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #646464;
|
||||||
|
}
|
BIN
public/resource/logo.png
Normal file
BIN
public/resource/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -9,8 +9,8 @@ interface RouteMeta {
|
|||||||
title?: string;
|
title?: string;
|
||||||
/** 页面100%视高 */
|
/** 页面100%视高 */
|
||||||
fullPage?: boolean;
|
fullPage?: boolean;
|
||||||
/** 作为菜单 */
|
/** 不作为菜单 */
|
||||||
asMenu?: boolean;
|
isNotMenu?: boolean;
|
||||||
/** 菜单和面包屑对应的图标 */
|
/** 菜单和面包屑对应的图标 */
|
||||||
icon?: Component;
|
icon?: Component;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<n-breadcrumb class="px-12px">
|
||||||
|
<template v-for="breadcrumb in breadcrumbList" :key="breadcrumb.key">
|
||||||
|
<n-breadcrumb-item>
|
||||||
|
<n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
|
||||||
|
<span>{{ breadcrumb.label }}</span>
|
||||||
|
</n-dropdown>
|
||||||
|
<span v-else>{{ breadcrumb.label }}</span>
|
||||||
|
</n-breadcrumb-item>
|
||||||
|
</template>
|
||||||
|
</n-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { NBreadcrumb, NBreadcrumbItem, NDropdown } from 'naive-ui';
|
||||||
|
import type { DropdownOption } from 'naive-ui';
|
||||||
|
import type { RouteLocationMatched } from 'vue-router';
|
||||||
|
import { EnumRoutePath } from '@/enum';
|
||||||
|
import type { RoutePathKey } from '@/interface';
|
||||||
|
|
||||||
|
type Breadcrumb = DropdownOption & {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
disabled: boolean;
|
||||||
|
routeName: RoutePathKey;
|
||||||
|
hasChildren: boolean;
|
||||||
|
children?: Breadcrumb[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const breadcrumbList = computed<Breadcrumb[]>(() => generateBreadcrumb());
|
||||||
|
|
||||||
|
function generateBreadcrumb() {
|
||||||
|
const { matched } = route;
|
||||||
|
return recursionBreadcrumb(matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 递归匹配路由获取面包屑数据 */
|
||||||
|
function recursionBreadcrumb(routeMatched: RouteLocationMatched[]) {
|
||||||
|
return routeMatched.map(item => {
|
||||||
|
const routeName = item.name as RoutePathKey;
|
||||||
|
const breadcrumItem: Breadcrumb = {
|
||||||
|
key: routeName,
|
||||||
|
label: (item.meta?.title as string) || '',
|
||||||
|
disabled: item.path === EnumRoutePath.root,
|
||||||
|
routeName,
|
||||||
|
hasChildren: false
|
||||||
|
};
|
||||||
|
if (item.children && item.children.length) {
|
||||||
|
breadcrumItem.hasChildren = true;
|
||||||
|
breadcrumItem.children = recursionBreadcrumb(item.children as RouteLocationMatched[]);
|
||||||
|
}
|
||||||
|
return breadcrumItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropdownSelect(optionKey: string) {
|
||||||
|
const key = optionKey as RoutePathKey;
|
||||||
|
router.push({ name: key });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -1,3 +1,4 @@
|
|||||||
|
import GlobalBreadcrumb from './GlobalBreadcrumb.vue';
|
||||||
import UserAvatar from './UserAvatar.vue';
|
import UserAvatar from './UserAvatar.vue';
|
||||||
import MenuCollapse from './MenuCollapse.vue';
|
import MenuCollapse from './MenuCollapse.vue';
|
||||||
import FullScreen from './FullScreen.vue';
|
import FullScreen from './FullScreen.vue';
|
||||||
@ -5,4 +6,4 @@ import SettingDrawerButton from './SettingDrawerButton.vue';
|
|||||||
import GihubSite from './GihubSite.vue';
|
import GihubSite from './GihubSite.vue';
|
||||||
import HeaderItem from './HeaderItem.vue';
|
import HeaderItem from './HeaderItem.vue';
|
||||||
|
|
||||||
export { UserAvatar, MenuCollapse, FullScreen, SettingDrawerButton, GihubSite, HeaderItem };
|
export { GlobalBreadcrumb, UserAvatar, MenuCollapse, FullScreen, SettingDrawerButton, GihubSite, HeaderItem };
|
||||||
|
@ -5,7 +5,10 @@
|
|||||||
<div v-if="!theme.isVerticalNav" class="menu-width h-full">
|
<div v-if="!theme.isVerticalNav" class="menu-width h-full">
|
||||||
<global-logo />
|
<global-logo />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-y-center h-full">
|
||||||
<menu-collapse />
|
<menu-collapse />
|
||||||
|
<global-breadcrumb v-if="theme.crumbsStyle.visible" />
|
||||||
|
</div>
|
||||||
<div class="flex-1 flex justify-end h-full">
|
<div class="flex-1 flex justify-end h-full">
|
||||||
<gihub-site />
|
<gihub-site />
|
||||||
<full-screen />
|
<full-screen />
|
||||||
@ -20,7 +23,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { NLayoutHeader } from 'naive-ui';
|
import { NLayoutHeader } from 'naive-ui';
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
import { UserAvatar, MenuCollapse, FullScreen, GihubSite, SettingDrawerButton } from './components';
|
import { GlobalBreadcrumb, UserAvatar, MenuCollapse, FullScreen, GihubSite, SettingDrawerButton } from './components';
|
||||||
import { GlobalLogo } from '../common';
|
import { GlobalLogo } from '../common';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
@ -31,7 +31,7 @@ export function transformRouteToMenu(routes: CustomRoute[]) {
|
|||||||
|
|
||||||
/** 判断路由是否作为菜单 */
|
/** 判断路由是否作为菜单 */
|
||||||
function asMenu(route: CustomRoute) {
|
function asMenu(route: CustomRoute) {
|
||||||
return Boolean(route.meta?.asMenu);
|
return !route.meta?.isNotMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 给菜单添加可选属性 */
|
/** 给菜单添加可选属性 */
|
||||||
|
@ -91,7 +91,7 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
path: EnumRoutePath.root,
|
path: EnumRoutePath.root,
|
||||||
redirect: { name: RouteNameMap.get('dashboard-analysis') },
|
redirect: { name: RouteNameMap.get('dashboard-analysis') },
|
||||||
meta: {
|
meta: {
|
||||||
asMenu: false
|
isNotMenu: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -101,7 +101,6 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
redirect: { name: RouteNameMap.get('dashboard-analysis') },
|
redirect: { name: RouteNameMap.get('dashboard-analysis') },
|
||||||
meta: {
|
meta: {
|
||||||
title: EnumRouteTitle.dashboard,
|
title: EnumRouteTitle.dashboard,
|
||||||
asMenu: true,
|
|
||||||
icon: Dashboard
|
icon: Dashboard
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@ -110,8 +109,7 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
path: EnumRoutePath['dashboard-analysis'],
|
path: EnumRoutePath['dashboard-analysis'],
|
||||||
component: () => import('@/views/dashboard/analysis/index.vue'),
|
component: () => import('@/views/dashboard/analysis/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: EnumRouteTitle['dashboard-analysis'],
|
title: EnumRouteTitle['dashboard-analysis']
|
||||||
asMenu: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -119,8 +117,7 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
path: EnumRoutePath['dashboard-workbench'],
|
path: EnumRoutePath['dashboard-workbench'],
|
||||||
component: () => import('@/views/dashboard/workbench/index.vue'),
|
component: () => import('@/views/dashboard/workbench/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: EnumRouteTitle['dashboard-workbench'],
|
title: EnumRouteTitle['dashboard-workbench']
|
||||||
asMenu: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -131,7 +128,6 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
component: BasicLayout,
|
component: BasicLayout,
|
||||||
meta: {
|
meta: {
|
||||||
title: EnumRouteTitle.exception,
|
title: EnumRouteTitle.exception,
|
||||||
asMenu: true,
|
|
||||||
icon: ExceptionOutlined
|
icon: ExceptionOutlined
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@ -141,8 +137,7 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
component: () => import('@/views/system/exception/403.vue'),
|
component: () => import('@/views/system/exception/403.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: EnumRouteTitle['exception-403'],
|
title: EnumRouteTitle['exception-403'],
|
||||||
fullPage: true,
|
fullPage: true
|
||||||
asMenu: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -151,8 +146,7 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
component: () => import('@/views/system/exception/404.vue'),
|
component: () => import('@/views/system/exception/404.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: EnumRouteTitle['exception-404'],
|
title: EnumRouteTitle['exception-404'],
|
||||||
fullPage: true,
|
fullPage: true
|
||||||
asMenu: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -161,8 +155,7 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
component: () => import('@/views/system/exception/500.vue'),
|
component: () => import('@/views/system/exception/500.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: EnumRouteTitle['exception-500'],
|
title: EnumRouteTitle['exception-500'],
|
||||||
fullPage: true,
|
fullPage: true
|
||||||
asMenu: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user