351 lines
8.5 KiB
Vue
351 lines
8.5 KiB
Vue
<script setup>
|
||
// 顶部导航栏:站点名、主导航(SVG 图标在上 + 文字在下的竖排布局)、消息入口(带未读角标)、积分、用户菜单
|
||
// 小屏(≤900px)不再收进汉堡抽屉,而是隐藏文字仅留图标(title 悬浮提示文字)
|
||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useUserStore } from '../stores/user'
|
||
import { useThemeStore } from '../stores/theme'
|
||
import { useChatStore } from '../stores/chat'
|
||
import NavIcon from './NavIcon.vue'
|
||
import PixelAvatar from './PixelAvatar.vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
const themeStore = useThemeStore()
|
||
const chatStore = useChatStore()
|
||
|
||
// 主导航配置(icon 为 NavIcon 组件里的 SVG 名称)
|
||
const navs = [
|
||
{ name: 'lobby', path: '/', label: '大厅', icon: 'lobby' },
|
||
{ name: 'center', path: '/center', label: '游戏中心', icon: 'center' },
|
||
{ name: 'battle', path: '/battle', label: '联机对战', icon: 'battle' },
|
||
{ name: 'signin', path: '/signin', label: '签到中心', icon: 'signin' },
|
||
{ name: 'vip', path: '/vip', label: 'VIP', icon: 'vip' },
|
||
{ name: 'rank', path: '/rank', label: '排行榜', icon: 'rank' },
|
||
]
|
||
|
||
const points = computed(() => userStore.user?.points ?? 0)
|
||
|
||
// 未读私聊 + 待处理好友申请合计(角标)
|
||
const msgBadge = computed(() => {
|
||
const n = chatStore.unread + chatStore.reqBadge
|
||
return n > 99 ? '99+' : n
|
||
})
|
||
|
||
// 仅饥荒游玩页:导航变悬浮小条,默认隐藏;由 GamePlay 的吸顶按钮统一展开/收起
|
||
const navFloat = computed(() => route.path.startsWith('/play/starve'))
|
||
const navShow = ref(true)
|
||
watch(navFloat, (v) => { navShow.value = !v }, { immediate: true })
|
||
function onStarveNavToggle() {
|
||
if (!navFloat.value) return
|
||
navShow.value = !navShow.value
|
||
}
|
||
onMounted(() => window.addEventListener('nlg:starve-nav-toggle', onStarveNavToggle))
|
||
onBeforeUnmount(() => window.removeEventListener('nlg:starve-nav-toggle', onStarveNavToggle))
|
||
|
||
// 当前路由是否命中某导航
|
||
function isActive(path) {
|
||
return route.path === path || (path !== '/' && route.path.startsWith(path))
|
||
}
|
||
|
||
// 退出登录
|
||
function logout() {
|
||
userStore.logout()
|
||
router.push('/login')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<header class="nav" :class="{ 'nav-float': navFloat, 'nav-hidden': navFloat && !navShow }">
|
||
<div class="nav-inner">
|
||
<router-link to="/" class="brand">
|
||
<span class="brand-icon"><NavIcon name="lobby" :size="26" /></span>
|
||
<span class="brand-name">{{ themeStore.siteName }}</span>
|
||
</router-link>
|
||
<!-- 主导航:图标在上、文字在下;小屏自动隐藏文字 -->
|
||
<nav class="links">
|
||
<router-link
|
||
v-for="n in navs"
|
||
:key="n.name"
|
||
:to="n.path"
|
||
class="link"
|
||
:class="{ active: isActive(n.path) }"
|
||
:title="n.label"
|
||
>
|
||
<NavIcon :name="n.icon" :size="22" class="link-icon" />
|
||
<span class="link-text">{{ n.label }}</span>
|
||
</router-link>
|
||
<router-link
|
||
to="/chat"
|
||
class="link msg-link"
|
||
:class="{ active: route.path.startsWith('/chat') }"
|
||
title="消息"
|
||
>
|
||
<NavIcon name="chat" :size="22" class="link-icon" />
|
||
<span class="link-text">消息</span>
|
||
<em v-if="chatStore.unread + chatStore.reqBadge > 0" class="msg-badge">{{ msgBadge }}</em>
|
||
</router-link>
|
||
</nav>
|
||
<div class="right">
|
||
<span class="points" title="当前积分"><NavIcon name="coin" :size="17" class="coin-icon" /> {{ points }}</span>
|
||
<router-link v-if="userStore.isAdmin" to="/admin" class="link tool-link" :class="{ active: route.path.startsWith('/admin') }" title="后台管理">
|
||
<NavIcon name="admin" :size="22" class="link-icon" />
|
||
<span class="link-text">后台</span>
|
||
</router-link>
|
||
<router-link to="/profile" class="user" title="个人中心">
|
||
<span class="avatar"><PixelAvatar :code="userStore.user?.avatar || 'px:01'" :size="30" /></span>
|
||
<span class="nickname">{{ userStore.user?.nickname }}</span>
|
||
</router-link>
|
||
<button class="link tool-link logout-link" title="退出登录" @click="logout">
|
||
<NavIcon name="logout" :size="22" class="link-icon" />
|
||
<span class="link-text">退出</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.nav {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
background: color-mix(in srgb, var(--bg-panel) 88%, transparent);
|
||
backdrop-filter: blur(10px);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.nav-inner {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
height: 64px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 0 16px;
|
||
}
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 800;
|
||
font-size: 17px;
|
||
white-space: nowrap;
|
||
}
|
||
.brand-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
color: var(--primary);
|
||
filter: drop-shadow(var(--glow));
|
||
}
|
||
.brand-name {
|
||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
color: transparent;
|
||
}
|
||
.links {
|
||
display: flex;
|
||
gap: 2px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
/* 竖排导航项:SVG 图标在上、11px 文字在下 */
|
||
.link {
|
||
padding: 6px 12px 5px;
|
||
border-radius: calc(var(--radius) / 1.6);
|
||
color: var(--text-dim);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 3px;
|
||
transition: all 0.15s;
|
||
white-space: nowrap;
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
}
|
||
.link-icon {
|
||
display: block;
|
||
flex-shrink: 0;
|
||
}
|
||
.link-text {
|
||
font-size: 11px;
|
||
line-height: 1;
|
||
}
|
||
.link:hover {
|
||
color: var(--text);
|
||
background: var(--bg-card);
|
||
}
|
||
.link.active {
|
||
color: #fff;
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-2));
|
||
box-shadow: var(--glow);
|
||
}
|
||
.msg-link {
|
||
position: relative;
|
||
}
|
||
.msg-badge {
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 4px;
|
||
min-width: 16px;
|
||
height: 16px;
|
||
padding: 0 4px;
|
||
border-radius: 999px;
|
||
background: #e5484d;
|
||
color: #fff;
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
font-style: normal;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
.points {
|
||
font-weight: 700;
|
||
color: var(--primary-2);
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.coin-icon {
|
||
flex-shrink: 0;
|
||
}
|
||
.tool-link {
|
||
padding: 5px 9px 4px;
|
||
}
|
||
.tool-link.active {
|
||
color: #fff;
|
||
}
|
||
.logout-link:hover {
|
||
color: #ff8a8e;
|
||
}
|
||
.user {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
}
|
||
.nickname {
|
||
font-size: 13px;
|
||
max-width: 90px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
/* 小屏:隐藏文字仅留图标(title 悬浮提示),不再使用汉堡抽屉 */
|
||
@media (max-width: 900px) {
|
||
.nav-inner {
|
||
gap: 8px;
|
||
height: 56px;
|
||
}
|
||
.links {
|
||
gap: 0;
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
.links::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
.link {
|
||
padding: 8px 9px;
|
||
}
|
||
.link-text {
|
||
display: none;
|
||
}
|
||
.msg-badge {
|
||
top: 1px;
|
||
right: 0;
|
||
}
|
||
.nickname {
|
||
display: none;
|
||
}
|
||
.right {
|
||
gap: 6px;
|
||
}
|
||
}
|
||
@media (max-width: 420px) {
|
||
.brand-name {
|
||
display: none;
|
||
}
|
||
.points {
|
||
font-size: 13px;
|
||
}
|
||
.avatar {
|
||
width: 28px;
|
||
height: 28px;
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
|
||
/* ---- 仅饥荒游玩页:悬浮小导航,自动隐藏,鼠标靠近顶部展示 ---- */
|
||
.nav.nav-float {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 150;
|
||
height: 40px;
|
||
transition: transform 0.25s ease;
|
||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
|
||
}
|
||
.nav.nav-float.nav-hidden {
|
||
transform: translateY(-100%);
|
||
}
|
||
.nav-float .nav-inner {
|
||
max-width: none;
|
||
height: 40px;
|
||
padding: 0 10px;
|
||
gap: 10px;
|
||
}
|
||
.nav-float .brand {
|
||
font-size: 14px;
|
||
gap: 6px;
|
||
}
|
||
.nav-float .brand-name,
|
||
.nav-float .link-text,
|
||
.nav-float .nickname {
|
||
display: none;
|
||
}
|
||
.nav-float .links {
|
||
gap: 0;
|
||
}
|
||
.nav-float .link {
|
||
flex-direction: row;
|
||
padding: 4px 8px;
|
||
gap: 4px;
|
||
}
|
||
.nav-float .link-icon {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
.nav-float .points {
|
||
font-size: 13px;
|
||
}
|
||
.nav-float .avatar {
|
||
width: 26px;
|
||
height: 26px;
|
||
}
|
||
</style>
|