Files
guoju0808/ry-vue3/src/components/PortalNavbar.vue
T
2026-08-26 23:14:46 +08:00

411 lines
12 KiB
Vue

<template>
<!-- 公开门户共享顶部导航 (首页 / 项目公示 / 公示详情 共用)
active 态按当前 route 推导, 无需各页面各自传参 -->
<header class="top-nav" :class="topNavClass">
<a class="logo" title="返回首页" @click.prevent="goHome">
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
<div class="logo-text">
<span class="logo-title">北京整合医学学会</span>
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
</div>
</a>
<ul class="nav-list">
<li class="nav-item"><a class="nav-link" :class="{ active: isHome }" @click.prevent="goHome">年度项目规划</a></li>
<li class="nav-item"><a class="nav-link" :class="{ active: isPublicity }" @click.prevent="goPublicity">项目公示</a></li>
</ul>
<div class="top-tools">
<template v-if="!loggedIn">
<span class="login-btn" @click="goLogin">登录</span>
</template>
<template v-else>
<el-dropdown trigger="click" @command="onUserCmd">
<a class="user-link" @click.prevent>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>{{ userName }}</span>
</a>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="account">我的主页</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</div>
<!-- 手机端汉堡按钮 (桌面隐藏) -->
<button class="hamburger" type="button" aria-label="打开菜单" @click="drawerOpen = true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</header>
<!-- 手机端抽屉菜单 (桌面隐藏) -->
<transition name="drawer-fade">
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false"></div>
</transition>
<transition name="drawer-slide">
<aside v-if="drawerOpen" class="drawer-panel" role="dialog" aria-label="导航菜单">
<div class="drawer-header">
<span class="drawer-title">导航菜单</span>
<button class="drawer-close" type="button" aria-label="关闭菜单" @click="drawerOpen = false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="6" y1="18" x2="18" y2="6"/>
</svg>
</button>
</div>
<nav class="drawer-nav">
<a class="drawer-link" @click.prevent="goHomeAndClose">年度项目规划</a>
<a class="drawer-link" @click.prevent="goPublicityAndClose">项目公示</a>
</nav>
<div class="drawer-divider"></div>
<div class="drawer-tools">
<template v-if="!loggedIn">
<span class="drawer-login" @click="goLoginAndClose">登录</span>
</template>
<template v-else>
<div class="drawer-user">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>{{ userName }}</span>
</div>
<a class="drawer-link" @click.prevent="goAccountAndClose">我的主页</a>
<a class="drawer-link danger" @click.prevent="goLogoutAndClose">退出登录</a>
</template>
</div>
</aside>
</transition>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/store/user'
import { logout as logoutApi } from '@/api/auth'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const isScrolled = ref(false)
const drawerOpen = ref(false)
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
const loggedIn = computed(() => !!userStore.token)
const userName = computed(() => userStore.user?.userName || '用户')
// active 态: 按当前路由推导 (首页=年度项目规划, /publicity* = 项目公示)
const isHome = computed(() => route.path === '/')
const isPublicity = computed(() => route.path === '/publicity' || route.path.startsWith('/publicity/'))
function handleScroll() { isScrolled.value = window.scrollY > 10 }
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
// 当前已在目标页时点导航 = 仅回顶, 否则跳转
function goHome() { if (route.path !== '/') router.push('/'); else window.scrollTo({ top: 0, behavior: 'smooth' }) }
function goPublicity() { if (route.path !== '/publicity') router.push('/publicity'); else window.scrollTo({ top: 0, behavior: 'smooth' }) }
function goLogin() { router.push('/login') }
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
async function onUserCmd(cmd) {
if (cmd === 'logout') return goLogout()
if (cmd === 'account') {
const r = (userStore.user?.role || '')
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
router.push(map[r] || '/admin/workbench')
}
}
// 抽屉版导航: 跳转后关闭抽屉
function goHomeAndClose() { drawerOpen.value = false; goHome() }
function goPublicityAndClose() { drawerOpen.value = false; goPublicity() }
function goLoginAndClose() { drawerOpen.value = false; goLogin() }
function goAccountAndClose() { drawerOpen.value = false; onUserCmd('account') }
async function goLogoutAndClose() { drawerOpen.value = false; await goLogout() }
</script>
<style scoped>
* { margin: 0; padding: 0; box-sizing: border-box; }
a { color: inherit; text-decoration: none; }
/* ========== 顶部导航 ========== */
.top-nav {
position: sticky;
top: 0;
z-index: 1000;
height: 72px;
padding: 0 60px;
background: var(--brand-primary);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
transition: box-shadow 0.3s;
}
.top-nav.is-scrolled {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.logo-icon {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.logo-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.logo-text {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.logo-title {
font-size: 16px;
font-weight: 600;
color: #fff;
letter-spacing: 0.5px;
}
.logo-subtitle {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
margin-top: 2px;
letter-spacing: 0.3px;
}
.nav-list {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 36px;
list-style: none;
}
.nav-item {
position: relative;
height: 72px;
display: flex;
align-items: center;
}
.nav-link {
font-size: 15px;
font-weight: 500;
color: rgba(255, 255, 255, 0.85);
cursor: pointer;
transition: color 0.25s;
position: relative;
height: 72px;
display: flex;
align-items: center;
}
.nav-link::after {
content: '';
position: absolute;
left: 50%;
bottom: 0;
width: 0;
height: 2px;
background: #fff;
transform: translateX(-50%);
transition: width 0.3s ease;
}
.nav-item:hover .nav-link,
.nav-link.active {
color: #fff;
}
.nav-item:hover .nav-link::after,
.nav-link.active::after {
width: 100%;
}
/* 顶部工具栏 */
.top-tools {
display: flex;
align-items: center;
gap: 16px;
}
.login-btn {
padding: 7px 20px;
background: #fff;
color: var(--brand-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
letter-spacing: 1px;
}
.login-btn:hover {
background: #f3f4f6;
}
.user-link {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.user-link:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* ========== 汉堡按钮 (桌面隐藏, 手机显示) ========== */
.hamburger {
display: none;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: #fff;
padding: 8px;
cursor: pointer;
margin-left: auto;
border-radius: 4px;
transition: background 0.2s;
}
.hamburger:hover { background: rgba(255, 255, 255, 0.12); }
.hamburger:active { background: rgba(255, 255, 255, 0.2); }
/* ========== 抽屉 (手机端) ========== */
.drawer-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1999;
}
.drawer-panel {
position: fixed;
top: 0;
right: 0;
width: 260px;
max-width: 80vw;
height: 100vh;
background: #fff;
z-index: 2000;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
padding: 0 16px 0 20px;
border-bottom: 1px solid var(--brand-slate-200);
flex-shrink: 0;
}
.drawer-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.drawer-close {
background: transparent;
border: none;
color: var(--el-text-color-secondary);
cursor: pointer;
padding: 6px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
}
.drawer-close:hover { background: var(--brand-slate-100); color: var(--el-text-color-primary); }
.drawer-nav { display: flex; flex-direction: column; padding: 8px 0; }
.drawer-link {
display: block;
padding: 12px 20px;
font-size: 14px;
color: var(--el-text-color-primary);
cursor: pointer;
transition: background 0.15s, color 0.15s;
border-left: 3px solid transparent;
}
.drawer-link:hover { background: var(--brand-slate-50); }
.drawer-link.danger { color: var(--el-color-danger); }
.drawer-divider { height: 1px; background: var(--brand-slate-200); margin: 4px 20px; }
.drawer-tools { display: flex; flex-direction: column; padding: 8px 0; gap: 0; }
.drawer-user {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
font-size: 13px;
color: var(--el-text-color-secondary);
border-bottom: 1px solid var(--brand-slate-100);
margin-bottom: 4px;
}
.drawer-user svg { color: var(--brand-primary); }
.drawer-login {
display: block;
margin: 4px 20px;
padding: 10px 0;
text-align: center;
background: var(--brand-primary);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
transition: opacity 0.2s;
}
.drawer-login:hover { opacity: 0.9; }
/* 抽屉过渡 */
.drawer-fade-enter-active, .drawer-fade-leave-active { transition: opacity 0.2s; }
.drawer-fade-enter-from, .drawer-fade-leave-to { opacity: 0; }
.drawer-slide-enter-active, .drawer-slide-leave-active { transition: transform 0.25s ease; }
.drawer-slide-enter-from, .drawer-slide-leave-to { transform: translateX(100%); }
/* ========== 移动端适配 ========== */
@media (max-width: 768px) {
/* 顶栏: 高度收窄 + padding 减小 */
.top-nav { height: 56px !important; padding: 0 16px !important; }
.logo-title { font-size: 14px !important; }
.logo-subtitle { display: none; }
/* 桌面 nav-list + 顶部 tools 隐藏, 改用汉堡 */
.nav-list { display: none !important; }
.top-tools { display: none !important; }
.hamburger { display: inline-flex !important; }
}
</style>