add 支持顶部导航

This commit is contained in:
lau 2026-03-09 23:43:53 +08:00
parent 705e68759d
commit 1681a32dbc
10 changed files with 340 additions and 40 deletions

View File

@ -282,3 +282,9 @@ h6 {
.top-right-btn {
margin-left: auto;
}
/* horizontal el menu */
.el-menu--horizontal .el-menu-item .svg-icon + span,
.el-menu--horizontal .el-sub-menu__title .svg-icon + span {
margin-left: 3px;
}

17
src/enums/NavTypeEnum.ts Normal file
View File

@ -0,0 +1,17 @@
/**
*
*/
export enum NavTypeEnum {
/**
*
*/
LEFT = 'left',
/**
*
*/
TOP = 'top',
/**
*
*/
MIX = 'mix'
}

View File

@ -1,9 +1,14 @@
<template>
<div class="navbar">
<div class="navbar" :class="'nav' + navType">
<hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggle-click="toggleSideBar" />
<breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
<breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
<template v-if="navType == NavTypeEnum.TOP">
<logo v-show="showLogo" :collapse="false"></logo>
<top-bar id="topbar-container" class="topbar-container" />
</template>
<div class="right-menu flex align-center">
<template v-if="appStore.device !== 'mobile'">
<el-select
@ -99,6 +104,9 @@ import { TenantVO } from '@/api/types';
import notice from './notice/index.vue';
import router from '@/router';
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
import Logo from "@/layout/components/Sidebar/Logo.vue";
import TopBar from './TopBar'
const appStore = useAppStore();
const userStore = useUserStore();
@ -109,6 +117,9 @@ const newNotice = ref(<number>0);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userId = ref(userStore.userId);
const navType = computed(() => settingsStore.navType);
const showLogo = computed(() => settingsStore.sidebarLogo);
const companyName = ref(undefined);
const tenantList = ref<TenantVO[]>([]);
//
@ -201,6 +212,12 @@ watch(
</script>
<style lang="scss" scoped>
.navbar.navtop {
.hamburger-container {
display: none !important;
}
}
:deep(.el-select .el-input__wrapper) {
height: 30px;
}
@ -223,14 +240,22 @@ watch(
position: relative;
//background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
align-items: center;
// padding: 0 8px;
box-sizing: border-box;
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
//float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
display: flex;
align-items: center;
flex-shrink: 0;
margin-right: 8px;
&:hover {
background: rgba(0, 0, 0, 0.025);
@ -238,7 +263,8 @@ watch(
}
.breadcrumb-container {
float: left;
//float: left;
flex-shrink: 0;
}
.topmenu-container {
@ -246,16 +272,28 @@ watch(
left: 50px;
}
.topbar-container {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
overflow: hidden;
margin-left: 8px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
//float: right;
height: 100%;
line-height: 50px;
display: flex;
align-items: center;
margin-left: auto;
&:focus {
outline: none;

View File

@ -1,5 +1,40 @@
<template>
<el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
<h3 class="drawer-title">菜单导航设置</h3>
<div class="nav-wrap">
<el-tooltip content="左侧菜单" placement="bottom">
<div
class="item left"
@click="handleNavType(NavTypeEnum.LEFT)"
:style="{ '--theme': theme }"
:class="{ activeItem: navType == NavTypeEnum.LEFT }"
>
<b></b><b></b>
</div>
</el-tooltip>
<el-tooltip content="混合菜单" placement="bottom">
<div
class="item mix"
@click="handleNavType(NavTypeEnum.MIX)"
:style="{ '--theme': theme }"
:class="{ activeItem: navType == NavTypeEnum.MIX }"
>
<b></b><b></b>
</div>
</el-tooltip>
<el-tooltip content="顶部菜单" placement="bottom">
<div
class="item top"
@click="handleNavType(NavTypeEnum.TOP)"
:style="{ '--theme': theme }"
:class="{ activeItem: navType == NavTypeEnum.TOP }"
>
<b></b><b></b>
</div>
</el-tooltip>
</div>
<h3 class="drawer-title">主题风格设置</h3>
<div class="setting-drawer-block-checbox">
@ -45,13 +80,6 @@
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启 TopNav</span>
<span class="comp-style">
<el-switch v-model="settingsStore.topNav" class="drawer-switch" @change="topNavChange" />
</span>
</div>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<span class="comp-style">
@ -101,6 +129,7 @@ import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission';
import { handleThemeStyle } from '@/utils/theme';
import { SideThemeEnum } from '@/enums/SideThemeEnum';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
import defaultSettings from '@/settings';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@ -113,7 +142,7 @@ const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
const navType = ref(settingsStore.navType);
//
const isDark = useDark({
storageKey: 'useDarkKey',
@ -130,11 +159,26 @@ watch(isDark, () => {
});
const toggleDark = () => useToggle(isDark);
const topNavChange = (val: any) => {
if (!val) {
appStore.toggleSideBarHide(false);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
}
/** 菜单导航设置 */
watch(
() => navType,
(val: string) => {
if (val.value === NavTypeEnum.TOP) {
appStore.toggleSideBarHide(true);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
} else if (val.value === NavTypeEnum.LEFT) {
appStore.toggleSideBarHide(false);
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
} else if (val.value === NavTypeEnum.MIX) {
appStore.toggleSideBarHide(false);
}
},
{ immediate: true, deep: true }
);
const handleNavType = (val: NavTypeEnum) => {
settingsStore.navType = val;
navType.value = val;
};
const dynamicTitleChange = () => {
@ -158,7 +202,6 @@ const handleTheme = (val: string) => {
const saveSetting = () => {
proxy?.$modal.loading('正在保存到本地,请稍候...');
const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
settings.value.topNav = storeSettings.value.topNav;
settings.value.tagsView = storeSettings.value.tagsView;
settings.value.tagsIcon = storeSettings.value.tagsIcon;
settings.value.fixedHeader = storeSettings.value.fixedHeader;
@ -166,6 +209,7 @@ const saveSetting = () => {
settings.value.dynamicTitle = storeSettings.value.dynamicTitle;
settings.value.sideTheme = storeSettings.value.sideTheme;
settings.value.theme = storeSettings.value.theme;
settings.value.navType = storeSettings.value.navType;
setTimeout(() => {
proxy?.$modal.closeLoading();
}, 1000);
@ -243,4 +287,67 @@ defineExpose({
margin: -3px 8px 0px 0px;
}
}
//
.nav-wrap {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.activeItem {
border: 2px solid #{'var(--theme)'} !important;
}
.item {
position: relative;
margin-right: 16px;
cursor: pointer;
width: 56px;
height: 48px;
border-radius: 4px;
background: #f0f2f5;
border: 2px solid transparent;
}
.left {
b:first-child {
display: block;
height: 30%;
background: #fff;
}
b:last-child {
width: 30%;
background: #1b2a47;
position: absolute;
height: 100%;
top: 0;
border-radius: 4px 0 0 4px;
}
}
.mix {
b:first-child {
border-radius: 4px 4px 0 0;
display: block;
height: 30%;
background: #1b2a47;
}
b:last-child {
width: 30%;
background: #1b2a47;
position: absolute;
height: 70%;
border-radius: 0 0 0 4px;
}
}
.top {
b:first-child {
display: block;
height: 30%;
background: #1b2a47;
border-radius: 4px 4px 0 0;
}
}
}
</style>

View File

@ -2,18 +2,17 @@
<div
class="sidebar-logo-container"
:class="{ collapse: collapse }"
:style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
>
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
<h1 v-else class="sidebar-title">
{{ title }}
</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
<h1 class="sidebar-title">
{{ title }}
</h1>
</router-link>
@ -26,6 +25,7 @@ import variables from '@/assets/styles/variables.module.scss';
import logo from '@/assets/logo/logo.png';
import { useSettingsStore } from '@/store/modules/settings';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { NavTypeEnum } from '@/enums/NavTypeEnum';
defineProps({
collapse: {
@ -37,6 +37,28 @@ defineProps({
const title = import.meta.env.VITE_APP_LOGO_TITLE;
const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme);
// Logo
const getLogoBackground = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-bg)'
}
if (settingsStore.navType == NavTypeEnum.TOP) {
return variables.menuLightBackground
}
return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBackground
})
// Logo
const getLogoTextColor = computed(() => {
if (settingsStore.isDark) {
return 'var(--sidebar-text)'
}
if (settingsStore.navType == NavTypeEnum.TOP) {
return variables.logoLightTitleColor
}
return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor
})
</script>
<style lang="scss" scoped>
@ -51,10 +73,9 @@ const sideTheme = computed(() => settingsStore.sideTheme);
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
background: v-bind(getLogoBackground);
text-align: center;
overflow: hidden;
@ -67,21 +88,17 @@ const sideTheme = computed(() => settingsStore.sideTheme);
height: 32px;
vertical-align: middle;
margin-right: 12px;
margin-left: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
color: v-bind(getLogoTextColor);
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family:
Avenir,
Helvetica Neue,
Arial,
Helvetica,
sans-serif;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}

View File

@ -0,0 +1,103 @@
<template>
<el-menu class="topbar-menu" :ellipsis="false" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
<sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
<el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
<template #title>
<span>更多菜单</span>
</template>
<sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
</el-sub-menu>
</el-menu>
</template>
<script setup>
import SidebarItem from '../Sidebar/SidebarItem'
import {useAppStore} from '@/store/modules/app'
import {useSettingsStore} from '@/store/modules/settings'
import {usePermissionStore} from '@/store/modules/permission'
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const theme = computed(() => settingsStore.theme)
const device = computed(() => appStore.device)
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
})
const visibleNumber = ref(5)
const topMenus = computed(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
})
const moreRoutes = computed(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value)
})
function setVisibleNumber() {
let width = document.body.getBoundingClientRect().width
if (width >= 1000) {
width -= 500
}
visibleNumber.value = parseInt(width / 3 / 85)
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
/* menu item */
#app .topbar-menu.el-menu--horizontal .el-sub-menu__title, #app .topbar-menu.el-menu--horizontal .el-menu-item {
padding: 0 10px !important;
}
.topbar-menu.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.el-sub-menu.is-active .svg-icon, .el-menu-item.is-active .svg-icon + span, .el-sub-menu.is-active .svg-icon + span, .el-sub-menu.is-active .el-sub-menu__title span {
color: v-bind(theme);
}
/* sub-menu item */
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
line-height: 50px !important;
color: #303133 !important;
margin: 0 15px -3px!important;
}
/* topbar more arrow */
.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
margin-left: 8px;
margin-top: 0px;
display: block !important;
}
/* menu__title el-menu-item */
.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
height: 60px;
}
</style>

View File

@ -26,6 +26,7 @@ import SideBar from './components/Sidebar/index.vue';
import { AppMain, Navbar, Settings, TagsView } from './components';
import { useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
import { initWebSocket } from '@/utils/websocket';
import { initSSE } from '@/utils/sse';
@ -35,6 +36,13 @@ const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const layout = computed(() => settingsStore.navType);
//
const showSidebar = computed(() => {
if (sidebar.value.hide) return false;
return layout.value === NavTypeEnum.LEFT || layout.value === NavTypeEnum.MIX;
});
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,

View File

@ -1,4 +1,5 @@
import { LanguageEnum } from '@/enums/LanguageEnum';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
const setting: DefaultSettings = {
/**
@ -18,9 +19,9 @@ const setting: DefaultSettings = {
showSettings: true,
/**
*
*
*/
topNav: false,
navType: NavTypeEnum.LEFT,
/**
* tagsView
@ -35,7 +36,7 @@ const setting: DefaultSettings = {
/**
*
*/
fixedHeader: false,
fixedHeader: true,
/**
* logo

View File

@ -3,6 +3,7 @@ import defaultSettings from '@/settings';
import { useDynamicTitle } from '@/utils/dynamicTitle';
import { useStorage } from '@vueuse/core';
import { ref } from 'vue';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
export const useSettingsStore = defineStore('setting', () => {
const storageSetting = useStorage<LayoutSetting>('layout-setting', {
@ -13,13 +14,13 @@ export const useSettingsStore = defineStore('setting', () => {
sidebarLogo: defaultSettings.sidebarLogo,
dynamicTitle: defaultSettings.dynamicTitle,
sideTheme: defaultSettings.sideTheme,
theme: defaultSettings.theme
theme: defaultSettings.theme,
navType: defaultSettings.navType
});
const title = ref<string>(defaultSettings.title);
const theme = ref<string>(storageSetting.value.theme);
const sideTheme = ref<string>(storageSetting.value.sideTheme);
const showSettings = ref<boolean>(defaultSettings.showSettings);
const topNav = ref<boolean>(storageSetting.value.topNav);
const tagsView = ref<boolean>(storageSetting.value.tagsView);
const tagsIcon = ref<boolean>(storageSetting.value.tagsIcon);
const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);
@ -27,6 +28,7 @@ export const useSettingsStore = defineStore('setting', () => {
const dynamicTitle = ref<boolean>(storageSetting.value.dynamicTitle);
const animationEnable = ref<boolean>(defaultSettings.animationEnable);
const dark = ref<boolean>(defaultSettings.dark);
const navType = ref<NavTypeEnum>(storageSetting.value.navType || NavTypeEnum.LEFT);
const setTitle = (value: string) => {
title.value = value;
@ -37,7 +39,6 @@ export const useSettingsStore = defineStore('setting', () => {
theme,
sideTheme,
showSettings,
topNav,
tagsView,
tagsIcon,
fixedHeader,
@ -45,6 +46,7 @@ export const useSettingsStore = defineStore('setting', () => {
dynamicTitle,
animationEnable,
dark,
navType,
setTitle
};
});

View File

@ -1,5 +1,6 @@
import type { PropType as VuePropType, ComponentInternalInstance as ComponentInstance } from 'vue';
import { LanguageEnum } from '@/enums/LanguageEnum';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
declare global {
/** vue Instance */
@ -90,9 +91,9 @@ declare global {
}
declare interface LayoutSetting {
/**
*
*
*/
topNav: boolean;
navType: NavTypeEnum;
/**
*