Files
schoolNews/schoolNewsWeb/src/components/base/TopNavigation.vue
2025-11-14 12:03:02 +08:00

558 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<nav class="top-navigation">
<div class="nav-container">
<!-- Logo区域 -->
<div class="nav-logo">
<img src="@/assets/imgs/logo-icon.svg" alt="Logo" class="logo-icon" />
<span class="logo-text">红色思政学习平台</span>
</div>
<!-- 导航菜单 -->
<div class="nav-menu" @wheel="handleWheel">
<div
v-for="menu in navigationMenus"
:key="menu.menuID"
class="nav-item"
:class="{ active: isActive(menu) }"
@mouseenter="(e) => handleMouseEnter(menu, e)"
@mouseleave="handleMouseLeave"
>
<div class="nav-link" @click="handleNavClick(menu)">
<span>{{ menu.name }}</span>
<img v-if="hasNavigationChildren(menu)" class="arrow-down" src="@/assets/imgs/arrow-down.svg" alt="arrow" />
</div>
<!-- 下拉菜单 -->
<Teleport to="body" v-if="hasNavigationChildren(menu)">
<div
class="dropdown-menu"
:class="{ show: activeDropdown === menu.menuID }"
:style="getDropdownPosition(menu)"
@mouseenter="() => { if (menu.menuID) activeDropdown = menu.menuID }"
@mouseleave="handleMouseLeave"
>
<div
v-for="child in getNavigationChildren(menu)"
:key="child.menuID"
class="dropdown-item"
:class="{ active: isActive(child) }"
@click="handleNavClick(child)"
>
<span>{{ child.name }}</span>
</div>
</div>
</Teleport>
</div>
</div>
<!-- 右侧用户区域 -->
<div class="nav-right">
<!-- 搜索框 -->
<Search @search="handleSearch" />
<ChangeHome />
<Notice />
<UserDropdown :user="userInfo" @logout="handleLogout" />
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useStore } from 'vuex';
import type { SysMenu } from '@/types';
import { MenuType } from '@/types/enums';
// @ts-ignore - Vue 3.5 组件导入兼容性
import {UserDropdown, Search, Notice, ChangeHome} from '@/components/base';
const router = useRouter();
const route = useRoute();
const store = useStore();
const activeDropdown = ref<string | null>(null);
const searchKeyword = ref('');
const dropdownPositions = ref<Record<string, { left: number; top: number; width: number }>>({});
// 获取所有菜单
const allMenus = computed(() => store.getters['auth/menuTree']);
const userInfo = computed(() => store.getters['auth/user']);
// 获取第一层的导航菜单MenuType.NAVIGATION过滤掉用户相关菜单
const navigationMenus = computed(() => {
const menus = allMenus.value.filter((menu: SysMenu) => {
// 过滤掉"用户下拉菜单"容器这些显示在UserDropdown中
if (menu.menuID === 'menu_user_dropdown') {
return false;
}
return menu.type === MenuType.NAVIGATION;
});
// console.log('导航菜单数据:', menus);
// menus.forEach((menu: SysMenu) => {
// console.log(`菜单 ${menu.name}:`, {
// menuID: menu.menuID,
// parentID: menu.parentID,
// children: menu.children,
// childrenCount: menu.children?.length || 0
// });
// });
return menus;
});
// 检查菜单是否有导航类型的子菜单
function hasNavigationChildren(menu: SysMenu): boolean {
return !!(menu.children && menu.children.some(child => child.type === MenuType.NAVIGATION));
}
// 获取导航类型的子菜单
function getNavigationChildren(menu: SysMenu): SysMenu[] {
if (!menu.children) {
// console.log(`菜单 ${menu.name} 没有子菜单`);
return [];
}
const children = menu.children.filter(child => child.type === MenuType.NAVIGATION);
// console.log(`菜单 ${menu.name} 的子菜单:`, children);
return children;
}
// 判断菜单是否激活
function isActive(menu: SysMenu): boolean {
if (!menu.url) return false;
// 精确匹配
if (route.path === menu.url) return true;
// 检查是否是子路由
if (menu.children && menu.children.length > 0) {
return isMenuOrChildActive(menu);
}
return false;
}
// 递归检查菜单或其子菜单是否激活
function isMenuOrChildActive(menu: SysMenu): boolean {
if (route.path === menu.url) return true;
if (menu.children) {
return menu.children.some(child => isMenuOrChildActive(child));
}
return false;
}
// 处理鼠标进入
function handleMouseEnter(menu: SysMenu, event?: MouseEvent) {
if (hasNavigationChildren(menu)) {
activeDropdown.value = menu.menuID || null;
// 计算下拉菜单位置
if (event && menu.menuID) {
const target = event.currentTarget as HTMLElement;
const navLink = target.querySelector('.nav-link') as HTMLElement;
const rect = (navLink || target).getBoundingClientRect();
dropdownPositions.value[menu.menuID] = {
left: rect.left,
top: rect.bottom,
width: rect.width
};
}
}
}
// 获取下拉菜单位置样式
function getDropdownPosition(menu: SysMenu) {
const menuID = menu.menuID;
const pos = menuID && dropdownPositions.value[menuID];
if (!pos) {
return {
position: 'fixed' as const,
visibility: 'hidden' as const
};
}
return {
position: 'fixed' as const,
left: `${pos.left}px`,
top: `${pos.top}px`,
width: `${pos.width}px`
};
}
// 处理鼠标离开
function handleMouseLeave() {
activeDropdown.value = null;
}
// 处理鼠标滚轮水平滚动
function handleWheel(event: WheelEvent) {
const container = event.currentTarget as HTMLElement;
if (container) {
event.preventDefault();
container.scrollLeft += event.deltaY;
}
}
// 处理导航点击
function handleNavClick(menu: SysMenu) {
activeDropdown.value = null;
if (menu.url) {
router.push(menu.url);
} else if (menu.children && menu.children.length > 0) {
// 如果没有url但有子菜单跳转到第一个子菜单
const firstChild = menu.children.find(child => child.url);
if (firstChild?.url) {
router.push(firstChild.url);
}
}
}
// 处理搜索
function handleSearch() {
if (searchKeyword.value.trim()) {
// 这里可以跳转到搜索页面或触发搜索功能
router.push(`/search?keyword=${encodeURIComponent(searchKeyword.value.trim())}`);
}
}
// 处理登出
function handleLogout() {
store.dispatch('auth/logout');
}
</script>
<style lang="scss" scoped>
.top-navigation {
height: 76px;
background: #ffffff;
border-bottom: 1px solid rgba(72, 72, 72, 0.1);
position: sticky;
top: 0;
z-index: 1000;
overflow: hidden;
}
.nav-container {
height: 100%;
display: flex;
align-items: center;
padding: 0 50px;
margin: 0 auto;
gap: 20px;
width: 100%;
position: relative;
overflow: visible;
}
.nav-logo {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
.logo-icon {
width: 28px;
height: 28px;
display: block;
}
.logo-text {
font-family: 'PingFang SC', sans-serif;
font-weight: 700;
font-size: 20.6px;
line-height: 1.31;
color: #C62828;
white-space: nowrap;
}
}
.nav-menu {
display: flex;
align-items: center;
gap: 0;
flex: 1;
overflow-x: auto;
overflow-y: visible;
scroll-behavior: smooth;
/* 自定义滚动条样式 */
scrollbar-width: auto;
scrollbar-color: #C62828 #f5f5f5;
&::-webkit-scrollbar {
height: 8px;
}
&::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 4px;
margin: 0 10px; /* 滚动条内缩 */
}
&::-webkit-scrollbar-thumb {
background: #C62828;
border-radius: 4px;
&:hover {
background: #B71C1C;
}
}
}
.nav-item {
position: relative;
height: 76px;
display: flex;
align-items: center;
flex-shrink: 0; /* 防止菜单项被压缩 */
&:hover {
.nav-link {
background: #f5f5f5;
font-weight: 600;
}
}
&.active {
.nav-link {
color: #C62828;
font-weight: 600;
}
}
}
.nav-link {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
padding: 26px 21px;
color: #141F38;
font-family: 'PingFang SC', sans-serif;
font-weight: 500;
font-size: 16px;
line-height: 1.5;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
user-select: none;
min-width: 106px;
&:hover {
color: #C62828;
}
.arrow-down {
font-size: 8px;
margin-left: 2px;
transition: transform 0.3s;
}
&:hover .arrow-down {
transform: rotate(180deg);
}
}
.dropdown-menu {
background: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0;
visibility: hidden;
transform: scaleY(0);
transform-origin: top center;
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
z-index: 10000;
pointer-events: none;
&.show {
opacity: 1;
visibility: visible;
transform: scaleY(1);
pointer-events: auto;
}
}
.dropdown-item {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
color: #333;
font-family: 'PingFang SC', sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 1.5;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
&:hover {
background: #f5f5f5;
color: #C62828;
}
&.active {
background: #ffe6e6;
color: #C62828;
font-weight: 500;
}
}
.nav-search {
margin-left: auto;
margin-right: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.search-box {
position: relative;
width: 221px;
height: 36px;
border: 1px solid #BAC0CC;
border-radius: 30px;
background: white;
display: flex;
align-items: center;
overflow: hidden;
.search-input {
flex: 1;
height: 100%;
border: none;
outline: none;
padding: 7px 20px;
font-family: 'PingFang SC', sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 1.57;
color: #333;
&::placeholder {
color: rgba(0, 0, 0, 0.3);
}
}
.search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 17px;
height: 17px;
display: flex;
align-items: center;
justify-content: center;
background: #C62828;
border-radius: 50%;
padding: 2px;
img {
width: 14px;
height: 14px;
}
}
}
.nav-right {
margin-left: auto;
display: flex;
flex-shrink: 0; /* 防止右侧区域被压缩 */
// gap: 20px;
align-items: center;
// 添加搜索框样式
:deep(.resource-search) {
width: 221px;
height: 36px;
padding: 0;
.search-box {
height: 36px;
}
input {
font-size: 14px;
padding: 0 70px 0 20px;
}
.search-button {
width: 48px;
height: 36px;
img {
width: 17px;
height: 17px;
}
}
}
}
/* 响应式设计 */
@media (max-width: 1200px) {
.nav-container {
padding: 0 30px;
gap: 15px;
}
.nav-link {
min-width: 90px;
padding: 26px 15px;
font-size: 15px;
}
.nav-menu {
/* 在小屏幕上确保滚动条可见 */
scrollbar-width: auto;
}
}
@media (max-width: 768px) {
.nav-container {
padding: 0 20px;
gap: 10px;
}
.nav-logo .logo-text {
font-size: 18px;
}
.nav-link {
min-width: 80px;
padding: 26px 12px;
font-size: 14px;
}
.search-box {
width: 180px;
}
}
@media (max-width: 480px) {
.nav-container {
padding: 0 15px;
gap: 8px;
}
.nav-logo .logo-text {
font-size: 16px;
}
.nav-link {
min-width: 70px;
padding: 26px 10px;
font-size: 13px;
}
.search-box {
width: 150px;
}
}
</style>