initialˆ
This commit is contained in:
205
reijm-read/src/components/layout/TheHeader.vue
Normal file
205
reijm-read/src/components/layout/TheHeader.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
|
||||
<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"
|
||||
>
|
||||
Union
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user