Files
ReisaAdmin/reijm-read/src/components/layout/TheHeader.vue
2025-10-05 20:08:52 +08:00

206 lines
5.6 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.

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useRoute } from "vue-router";
import ThemeToggle from "@/components/ThemeToggle.vue";
import eventBus from "@/eventBus";
import axios from "axios";
const route = useRoute();
const isMenuOpen = ref(false);
// 用户信息
const closeMenu = () => {
isMenuOpen.value = false;
};
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
closeMenu();
}
};
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest(".mobile-menu") && !target.closest(".menu-button")) {
closeMenu();
}
};
let refreshInterval: number | null = null;
// 新增刷新token的函数
const refreshToken = async () => {
try {
const token = localStorage.getItem('token');
if (!token) return;
const response = await axios.post('/api/user/ref', {
data: token,
timestamp: Date.now()
}, {
headers: {
'Token' : token,
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Refresh token failed:', error);
}
};
// 在组件挂载时添加事件监听
onMounted(() => {
// 每30秒刷新一次token
refreshInterval = window.setInterval(refreshToken, 30000);
window.addEventListener("click", handleClickOutside);
window.addEventListener("keydown", handleKeydown);});
// 在组件卸载时移除事件监听,防止内存泄漏
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
window.removeEventListener("click", handleClickOutside);
window.removeEventListener("keydown", handleKeydown);});
const navItems = [
{ name: "首页", path: "/" },
{ name: "用户", path: "/user" },
];
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
};
</script>
<template>
<header class="fixed w-full top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
<nav class="container mx-auto px-4 py-3 md:py-4">
<div class="flex items-center justify-between">
<router-link to="/" class="logo-link flex items-center space-x-2">
<span
class="text-xl md:text-2xl font-bold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent bg-[length:200%_auto] hover:animate-gradient whitespace-nowrap"
>
ReiJM
</span>
</router-link>
<!-- 桌面端导航 -->
<div class="hidden md:flex items-center space-x-6">
<router-link
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="nav-link"
:class="{ 'text-primary': route.path === item.path }"
>
{{ item.name }}
</router-link>
<ThemeToggle />
</div>
<!-- 移动端菜单按钮 -->
<div class="md:hidden flex items-center space-x-2">
<ThemeToggle />
<button
class="menu-button p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
@click.stop="toggleMenu"
aria-label="Toggle menu"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-if="!isMenuOpen"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<!-- 移动端导航菜单 -->
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-show="isMenuOpen" class="mobile-menu md:hidden">
<div class="py-2 space-y-1">
<router-link
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="block px-4 py-2 text-base hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
:class="{
'bg-primary/10 text-primary': route.path === item.path,
}"
@click="closeMenu"
>
{{ item.name }}
</router-link>
</div>
</div>
</transition>
</nav>
</header>
</template>
<style scoped>
.nav-link {
@apply text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary transition-colors;
}
.mobile-menu {
@apply absolute top-full left-0 right-0 bg-white/95 dark:bg-gray-900/95 backdrop-blur-sm
border-t border-gray-200 dark:border-gray-700 shadow-lg;
}
/* 移动端导航链接悬停效果 */
@media (hover: hover) {
.mobile-menu .router-link-active {
@apply bg-primary-10 text-primary;
}
}
/* Logo 悬停动画 */
.logo-link {
@apply inline-block py-1;
}
.logo-link:hover span:first-child {
@apply transform scale-105 transition-transform duration-300;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.hover\:animate-gradient:hover {
animation: gradient 3s linear infinite;
}
</style>