Files
nl-game/src/components/NavBar.vue
2026-08-15 16:46:17 +08:00

351 lines
8.5 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>
// 顶部导航栏站点名、主导航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>