Browse Source

悬浮菜单

master
han\hanst 5 months ago
parent
commit
ba1d6a4b99
  1. 5
      src/store/modules/common.js
  2. 44
      src/views/main-navbar.vue
  3. 795
      src/views/main-sidebar-hover.vue
  4. 12
      src/views/main.vue

5
src/store/modules/common.js

@ -12,6 +12,8 @@ export default {
// 侧边栏, 菜单 // 侧边栏, 菜单
menuList: [], menuList: [],
menuActiveName: '', menuActiveName: '',
// 菜单模式, true为悬浮菜单, false为传统菜单
useHoverMenu: true,
// 内容, 是否需要刷新 // 内容, 是否需要刷新
contentIsNeedRefresh: false, contentIsNeedRefresh: false,
// 主入口标签页 // 主入口标签页
@ -45,6 +47,9 @@ export default {
}, },
updateMainTabsActiveName (state, name) { updateMainTabsActiveName (state, name) {
state.mainTabsActiveName = name state.mainTabsActiveName = name
},
updateUseHoverMenu (state, useHover) {
state.useHoverMenu = useHover
} }
} }
} }

44
src/views/main-navbar.vue

@ -9,6 +9,9 @@
<el-menu-item class="site-navbar__switch" index="0" @click="sidebarFold = !sidebarFold"> <el-menu-item class="site-navbar__switch" index="0" @click="sidebarFold = !sidebarFold">
<icon-svg name="zhedie"></icon-svg> <icon-svg name="zhedie"></icon-svg>
</el-menu-item> </el-menu-item>
<el-menu-item class="site-navbar__switch menu-toggle-btn" index="0-1" @click="toggleMenuMode" :title="menuModeTitle">
<i class="el-icon-refresh"></i>
</el-menu-item>
</el-menu> </el-menu>
<a class="site-navbar__brand-lg" href="javascript:;">{{ pageLanguage.XjSysManage }}</a> <a class="site-navbar__brand-lg" href="javascript:;">{{ pageLanguage.XjSysManage }}</a>
<a class="site-navbar__brand-mini" href="javascript:;">{{ pageLanguage.abbreviation }}</a> <a class="site-navbar__brand-mini" href="javascript:;">{{ pageLanguage.abbreviation }}</a>
@ -252,6 +255,20 @@ export default {
get() { get() {
return this.$store.state.user.userDisplay return this.$store.state.user.userDisplay
} }
},
useHoverMenu: {
get() {
return this.$store.state.common.useHoverMenu
},
set(val) {
this.$store.commit('common/updateUseHoverMenu', val)
}
},
menuModeIcon() {
return this.useHoverMenu ? 'zhedie' : 'zhedie'
},
menuModeTitle() {
return this.useHoverMenu ? '切换到传统菜单' : '切换到悬浮菜单'
} }
}, },
activated() { activated() {
@ -369,6 +386,11 @@ export default {
}) })
}).catch(() => { }).catch(() => {
}) })
},
//
toggleMenuMode() {
this.useHoverMenu = !this.useHoverMenu
this.$message.success(this.useHoverMenu ? '已切换到悬浮菜单模式' : '已切换到传统菜单模式')
} }
}, },
created() { created() {
@ -398,5 +420,27 @@ export default {
color: #3b4249; color: #3b4249;
} }
/* 菜单切换按钮悬停显示效果 */
.menu-toggle-btn {
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
transform: translateX(-10px);
}
.site-navbar__menu:hover .menu-toggle-btn,
.menu-toggle-btn:hover {
opacity: 1;
visibility: visible;
transform: translateX(0);
}
/* 为了更好的用户体验,也可以在整个导航栏悬停时显示 */
.site-navbar__brand:hover .menu-toggle-btn {
opacity: 1;
visibility: visible;
transform: translateX(0);
}
</style> </style>

795
src/views/main-sidebar-hover.vue

@ -0,0 +1,795 @@
<template>
<aside class="site-sidebar-hover" :class="['site-sidebar--' + sidebarLayoutSkin, { 'site-sidebar--fold': sidebarFold }]">
<div class="site-sidebar__inner">
<!-- 展开状态的内容 -->
<template v-if="!sidebarFold">
<!-- 搜索框 -->
<div class="sidebar-search">
<el-input
v-model="search"
placeholder="搜索"
@keyup.enter.native="searchMenu1"
size="small">
<i slot="suffix" class="el-icon-search" @click="searchMenu1()"></i>
</el-input>
</div>
<!-- 首页 -->
<div class="sidebar-home-item" @click="$router.push({ name: 'home' })">
<icon-svg name="shouye" class="sidebar-menu-icon"></icon-svg>
<span>{{ pageLanguage.homePage }}</span>
</div>
<!-- 主菜单列表 -->
<div class="sidebar-main-menu">
<div
v-for="menu in menuList"
:key="menu.menuId"
class="main-menu-item"
@mouseenter="showSubMenu(menu, $event)"
@mouseleave="hideSubMenu"
@click="handleMainMenuClick(menu)">
<icon-svg :name="menu.icon || ''" class="sidebar-menu-icon"></icon-svg>
<span class="menu-title">{{ menu.name }}</span>
<i v-if="menu.list && menu.list.length > 0" class="el-icon-arrow-right menu-arrow"></i>
</div>
<!-- 收藏夹 -->
<div
v-for="menu in favoriteList"
:key="'fav-' + menu.menuId"
class="main-menu-item favorite-menu"
@mouseenter="showSubMenu(menu, $event)"
@mouseleave="hideSubMenu"
@click="handleMainMenuClick(menu)">
<icon-svg :name="menu.icon || 'star-on'" class="sidebar-menu-icon"></icon-svg>
<span class="menu-title">{{ menu.name }}</span>
<i v-if="menu.list && menu.list.length > 0" class="el-icon-arrow-right menu-arrow"></i>
</div>
</div>
</template>
<!-- 折叠状态的内容 - 只显示图标 -->
<template v-else>
<!-- 首页图标 -->
<div class="sidebar-home-item folded-item" @click="$router.push({ name: 'home' })" :title="pageLanguage.homePage">
<icon-svg name="shouye" class="sidebar-menu-icon"></icon-svg>
</div>
<!-- 主菜单图标列表 -->
<div class="sidebar-main-menu">
<div
v-for="menu in menuList"
:key="menu.menuId"
class="main-menu-item folded-item"
@mouseenter="showSubMenu(menu, $event)"
@mouseleave="hideSubMenu"
@click="handleMainMenuClick(menu)"
:title="menu.name">
<icon-svg :name="menu.icon || ''" class="sidebar-menu-icon"></icon-svg>
</div>
<!-- 收藏夹图标 -->
<div
v-for="menu in favoriteList"
:key="'fav-' + menu.menuId"
class="main-menu-item folded-item favorite-menu"
@mouseenter="showSubMenu(menu, $event)"
@mouseleave="hideSubMenu"
@click="handleMainMenuClick(menu)"
:title="menu.name">
<icon-svg :name="menu.icon || 'star-on'" class="sidebar-menu-icon"></icon-svg>
</div>
</div>
</template>
</div>
<!-- 悬浮子菜单 -->
<div
v-if="hoveredMenu && hoveredMenu.list && hoveredMenu.list.length > 0"
class="hover-submenu"
:style="submenuStyle"
@mouseenter="keepSubMenuVisible"
@mouseleave="hideSubMenu">
<div class="submenu-content">
<div class="submenu-title">{{ hoveredMenu.name }}</div>
<div class="submenu-items">
<div
v-for="subItem in hoveredMenu.list"
:key="subItem.menuId"
class="submenu-category">
<!-- 统一的显示样式都显示为分类标题 + 子项 -->
<div class="category-title">{{ subItem.name }}</div>
<div class="category-items">
<!-- 如果有子菜单递归显示所有子项 -->
<template v-if="subItem.list && subItem.list.length > 0">
<div v-for="grandChild in subItem.list" :key="grandChild.menuId">
<!-- 如果孙子菜单还有子菜单显示为子分类 -->
<div v-if="grandChild.list && grandChild.list.length > 0" class="sub-category">
<div class="sub-category-title">{{ grandChild.name }}</div>
<div class="sub-category-items">
<span
v-for="greatGrandChild in getAllLeafMenus(grandChild)"
:key="greatGrandChild.menuId"
class="submenu-link sub-link"
@click="gotoRouteHandle(greatGrandChild)">
{{ greatGrandChild.name }}
</span>
</div>
</div>
<!-- 如果孙子菜单没有子菜单直接显示 -->
<span
v-else
class="submenu-link"
@click="gotoRouteHandle(grandChild)">
{{ grandChild.name }}
</span>
</div>
</template>
<!-- 如果没有子菜单将自己作为子项显示 -->
<template v-else>
<span
class="submenu-link"
@click="gotoRouteHandle(subItem)">
{{ subItem.name }}
</span>
</template>
</div>
</div>
</div>
</div>
</div>
</aside>
</template>
<script>
import { isURL } from '@/utils/validate'
import { userFavoriteList } from '@/api/userFavorite.js'
import { searchFunctionButtonList } from '@/api/sysLanguage.js'
export default {
name: 'MainSidebarHover',
data() {
return {
dynamicMenuRoutes: [],
search: '',
favoriteList: [],
hoveredMenu: null,
submenuStyle: {},
hideTimer: null,
list: [],
pageLanguage: {
homePage: '首页'
}
}
},
computed: {
sidebarLayoutSkin: {
get() {
return this.$store.state.common.sidebarLayoutSkin
}
},
sidebarFold: {
get() {
return this.$store.state.common.sidebarFold
}
},
menuList: {
get() {
return this.$store.state.common.menuList
},
set(val) {
this.$store.commit('common/updateMenuList', val)
}
},
menuActiveName: {
get() {
return this.$store.state.common.menuActiveName
},
set(val) {
this.$store.commit('common/updateMenuActiveName', val)
}
},
mainTabs: {
get() {
return this.$store.state.common.mainTabs
},
set(val) {
this.$store.commit('common/updateMainTabs', val)
}
},
mainTabsActiveName: {
get() {
return this.$store.state.common.mainTabsActiveName
},
set(val) {
this.$store.commit('common/updateMainTabsActiveName', val)
}
}
},
watch: {
$route: 'routeHandle'
},
created() {
this.menuList = JSON.parse(sessionStorage.getItem('menuList') || '[]').filter(item => item.menuId != 999)
this.favoriteList = JSON.parse(sessionStorage.getItem('menuList') || '[]').filter(item => item.menuId == 999)
this.userFavorites()
this.dynamicMenuRoutes = JSON.parse(sessionStorage.getItem('dynamicMenuRoutes') || '[]')
this.routeHandle(this.$route)
this.getFunctionButtonList()
},
methods: {
//
getFunctionButtonList() {
let queryButton = {
functionId: 'systemInformation',
tableId: 'systemInformation',
languageCode: this.$i18n.locale,
objectId: 'homePage'
}
searchFunctionButtonList(queryButton).then(({data}) => {
if (data.code == 0) {
this.pageLanguage = data.data
}
})
},
//
userFavorites() {
let query = {
userId: this.$store.state.user.id,
languageCode: this.$i18n.locale
}
userFavoriteList(query).then(({data}) => {
if (data.list && data.list.length > 0 && this.favoriteList.length > 0) {
this.favoriteList[0].list = data.list
}
})
},
//
showSubMenu(menu, event) {
if (this.hideTimer) {
clearTimeout(this.hideTimer)
this.hideTimer = null
}
if (menu.list && menu.list.length > 0) {
this.hoveredMenu = menu
const rect = event.currentTarget.getBoundingClientRect()
this.submenuStyle = {
top: rect.top + 'px',
left: rect.right + 'px',
display: 'block'
}
}
},
//
hideSubMenu() {
this.hideTimer = setTimeout(() => {
this.hoveredMenu = null
this.submenuStyle = { display: 'none' }
}, 100)
},
//
keepSubMenuVisible() {
if (this.hideTimer) {
clearTimeout(this.hideTimer)
this.hideTimer = null
}
},
//
handleMainMenuClick(menu) {
if (!menu.list || menu.list.length === 0) {
this.gotoRouteHandle(menu)
}
},
// menuId()
gotoRouteHandle(menu) {
console.log('点击菜单:', menu.name, 'menuId:', menu.menuId)
console.log('动态路由数量:', this.dynamicMenuRoutes.length)
var route = this.dynamicMenuRoutes.filter(item => item.meta.menuId === menu.menuId)
console.log('匹配到的路由:', route)
if (route.length >= 1) {
console.log('跳转到路由:', route[0].name)
this.$router.push({ name: route[0].name })
this.hideSubMenu()
} else {
console.warn('未找到对应的路由,menuId:', menu.menuId)
//
if (menu.url) {
console.log('尝试使用URL跳转:', menu.url)
this.$router.push(menu.url)
this.hideSubMenu()
} else {
this.$message.warning(`菜单 "${menu.name}" 暂无对应页面`)
}
}
},
//
searchMenu1() {
if (this.search == '') {
this.menuList = JSON.parse(sessionStorage.getItem('menuList') || '[]').filter(item => item.menuId != 999)
} else {
//
this.menuList = JSON.parse(sessionStorage.getItem('menuList') || '[]').filter(item => item.menuId != 999)
this.list = this.treeFindPath(this.menuList)
let list = this.treeFindPath(this.menuList)
list = this.distinct(list, list)
this.menuList = list.filter(item => item.name.indexOf(this.search) != -1)
let menuSum = []
for (let data of this.menuList) {
menuSum.push(data)
this.getParent(data.parentId, menuSum)
}
menuSum = Array.from(new Set([...menuSum]))
if (menuSum.length > 0) {
let menuList = menuSum.filter(item => item.parentId == 0)
this.menuList = menuList
this.treeList(menuList, menuSum)
}
}
},
//
getParent(val, sum) {
if (val == 0) {
return
}
let parent = this.list.filter(item => item.menuId == val)
if (parent.length > 0) {
parent[0].list.length = 0
sum.push(parent[0])
this.getParent(parent[0].parentId, sum)
}
},
//
treeList(menuList, menuSum) {
for (let m1 of menuList) {
for (let m2 of menuSum) {
if (m1.menuId == m2.parentId) {
m1.list.push(m2)
this.treeList(m1.list, menuSum)
}
}
}
},
// list
treeFindPath(tree, path = []) {
if (!tree) return []
for (const data of tree) {
path.push(data)
this.treeFindPath(data.list, path)
}
return path
},
//
distinct(a, b) {
return Array.from(new Set([...a, ...b]))
},
//
getAllLeafMenus(menu) {
const leafMenus = []
function collectLeafMenus(menuItem) {
if (!menuItem.list || menuItem.list.length === 0) {
//
leafMenus.push(menuItem)
} else {
//
menuItem.list.forEach(child => {
collectLeafMenus(child)
})
}
}
collectLeafMenus(menu)
return leafMenus
},
//
routeHandle(route) {
if (route.meta.isTab) {
var tab = this.mainTabs.filter(item => item.name === route.name)[0]
if (!tab) {
if (route.meta.isDynamic) {
route = this.dynamicMenuRoutes.filter(item => item.name === route.name)[0]
if (!route) {
return console.error('未能找到可用标签页!')
}
}
tab = {
menuId: route.meta.menuId || route.name,
name: route.name,
title: route.meta.title,
type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module',
iframeUrl: route.meta.iframeUrl || '',
params: route.params,
query: route.query
}
this.mainTabs = this.mainTabs.concat(tab)
}
this.menuActiveName = tab.menuId + ''
this.mainTabsActiveName = tab.name
}
}
}
}
</script>
<style lang="scss" scoped>
.site-sidebar-hover {
position: fixed;
top: 32px;
left: 0;
bottom: 0;
z-index: 1020;
width: 230px;
background-color: #263238;
overflow: hidden;
transition: width 0.3s ease;
&.site-sidebar--fold {
width: 64px;
.site-sidebar__inner {
width: 64px;
}
//
.main-menu-item.folded-item {
display: flex;
align-items: center;
justify-content: center;
padding: 14px 0;
transition: all 0.3s;
.sidebar-menu-icon {
width: 20px !important;
height: 20px !important;
font-size: 26px !important;
line-height: 32px !important;
display: inline-flex;
align-items: center;
justify-content: center;
// svg
svg {
width: 20px;
height: 20px;
}
}
.menu-title {
display: none;
}
}
//
.el-submenu__icon-arrow {
display: none;
}
}
.site-sidebar__inner {
position: relative;
z-index: 1;
width: 230px;
height: 100%;
padding: 10px 0;
overflow-y: auto;
}
.sidebar-search {
padding: 0 15px 10px;
border-bottom: 1px solid #37474f;
margin-bottom: 10px;
.el-input__inner {
background: #37474f;
border: none;
color: #fff;
font-size: 12px;
&::placeholder {
color: #8a979e;
}
}
.el-icon-search {
color: #8a979e;
cursor: pointer;
&:hover {
color: #fff;
}
}
}
.sidebar-home-item {
display: flex;
align-items: center;
padding: 12px 20px;
color: #8a979e;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #37474f;
color: #fff;
}
.sidebar-menu-icon {
width: 20px;
margin-right: 10px;
text-align: center;
font-size: 16px;
}
}
.sidebar-main-menu {
.main-menu-item {
display: flex;
align-items: center;
padding: 14px 20px;
color: #8a979e;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
border-radius: 0 25px 25px 0;
margin: 2px 0;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
background: linear-gradient(135deg, #17B3A3, #0BB2D4);
transition: width 0.3s ease;
}
&:hover {
background: linear-gradient(135deg, rgba(23, 179, 163, 0.1), rgba(11, 178, 212, 0.1));
color: #fff;
transform: translateX(5px);
&::before {
width: 4px;
}
.menu-arrow {
transform: translateX(5px);
opacity: 1;
}
}
&.favorite-menu {
border-top: 1px solid #37474f;
margin-top: 15px;
padding-top: 18px;
&::after {
content: '★';
position: absolute;
right: 35px;
color: #ffd700;
font-size: 12px;
opacity: 0.7;
}
}
.sidebar-menu-icon {
width: 24px;
margin-right: 12px;
text-align: center;
font-size: 18px;
color: inherit;
transition: transform 0.3s ease;
}
.menu-title {
flex: 1;
font-size: 14px;
font-weight: 500;
}
.menu-arrow {
font-size: 14px;
opacity: 0.6;
transition: all 0.3s ease;
color: #17B3A3;
}
&:hover .sidebar-menu-icon {
transform: scale(1.1);
}
}
}
}
.hover-submenu {
position: fixed;
z-index: 1030;
background: linear-gradient(135deg, #fafbfc 0%, #f8f9fa 100%);
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow:
0 8px 32px 0 rgba(0, 0, 0, 0.12),
0 2px 8px 0 rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(255, 255, 255, 0.05);
min-width: 900px;
max-width: 1200px;
max-height: 600px;
overflow-y: auto;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
.submenu-content {
padding: 25px;
.submenu-title {
font-size: 18px;
font-weight: bold;
color: #303133;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #17B3A3;
position: relative;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 60px;
height: 2px;
background: linear-gradient(90deg, #17B3A3, #0BB2D4);
}
}
.submenu-items {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
.submenu-category {
min-height: 60px; //
.category-title {
font-size: 15px;
font-weight: 600;
color: #17B3A3;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e8f4f8;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
bottom: -1px;
width: 30px;
height: 1px;
background: #17B3A3;
}
}
.category-items {
display: flex;
flex-direction: column;
gap: 6px;
.sub-category {
margin: 8px 0;
padding-left: 12px;
border-left: 2px solid #e8f4f8;
.sub-category-title {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 6px;
padding-bottom: 4px;
border-bottom: 1px solid #f0f0f0;
}
.sub-category-items {
display: flex;
flex-direction: column;
gap: 4px;
}
}
.submenu-link {
display: inline-block;
padding: 8px 12px;
color: #606266;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s ease;
position: relative;
&:hover {
background: linear-gradient(135deg, #f0f9ff, #e0f7fa);
color: #17B3A3;
transform: translateX(3px);
box-shadow: 0 2px 8px rgba(23, 179, 163, 0.15);
}
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 2px;
background: #17B3A3;
transition: width 0.3s ease;
}
&:hover::before {
width: 3px;
}
&.sub-link {
font-size: 12px;
font-weight: 400;
padding: 6px 10px;
margin-left: 8px;
// background: rgba(23, 179, 163, 0.05);
border-radius: 3px;
&:hover {
background: rgba(23, 179, 163, 0.1);
transform: translateX(2px);
}
}
}
}
}
}
}
//
animation: slideInRight 0.3s ease-out;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
//
.site-sidebar--dark {
background-color: #263238;
}
</style>

12
src/views/main.vue

@ -6,7 +6,8 @@
element-loading-text="拼命加载中"> element-loading-text="拼命加载中">
<template v-if="!loading"> <template v-if="!loading">
<main-navbar/> <main-navbar/>
<main-sidebar/>
<main-sidebar-hover v-if="useHoverMenu"/>
<main-sidebar v-else/>
<div class="site-content__wrapper" :style="{ 'min-height': documentClientHeight + 'px' }"> <div class="site-content__wrapper" :style="{ 'min-height': documentClientHeight + 'px' }">
<main-content v-if="!$store.state.common.contentIsNeedRefresh"/> <main-content v-if="!$store.state.common.contentIsNeedRefresh"/>
</div> </div>
@ -18,6 +19,7 @@
import MainNavbar from './main-navbar' import MainNavbar from './main-navbar'
import MainSidebar from './main-sidebar' import MainSidebar from './main-sidebar'
import MainSidebarHover from './main-sidebar-hover'
import MainContent from './main-content' import MainContent from './main-content'
export default { export default {
@ -40,6 +42,7 @@
components: { components: {
MainNavbar, MainNavbar,
MainSidebar, MainSidebar,
MainSidebarHover,
MainContent MainContent
}, },
computed: { computed: {
@ -96,9 +99,16 @@
this.$store.commit('user/updateUserDisplay', val) this.$store.commit('user/updateUserDisplay', val)
} }
}, },
useHoverMenu: {
get() {
return this.$store.state.common.useHoverMenu
}
},
}, },
created() { created() {
this.getUserInfo() this.getUserInfo()
//
this.$store.commit('common/updateUseHoverMenu', true)
}, },
mounted() { mounted() {
this.resetDocumentClientHeight() this.resetDocumentClientHeight()

Loading…
Cancel
Save