376 lines
14 KiB
Vue
376 lines
14 KiB
Vue
<template>
|
||
<a-affix :offset-top="0">
|
||
<a-layout-header class="header">
|
||
<div class="nav-bar mx-auto max-w-screen-xl px-4 h-full">
|
||
<!-- 左侧:Logo + 菜单 -->
|
||
<div class="flex items-center gap-8 nav-left">
|
||
<!-- Logo -->
|
||
<NuxtLink to="/" class="flex items-center logo-link cursor-pointer flex-shrink-0">
|
||
<div class="logo-text">决策咨询网</div>
|
||
</NuxtLink>
|
||
|
||
<!-- PC 导航菜单 -->
|
||
<a-menu
|
||
mode="horizontal"
|
||
theme="dark"
|
||
:selected-keys="selectedKeys"
|
||
class="menu hidden md:flex"
|
||
>
|
||
<template v-for="item in nav" :key="item.key">
|
||
<a-sub-menu v-if="item.children && item.children.length" :key="item.key">
|
||
<template #title>
|
||
<NuxtLink :to="item.to" class="nav-item-wrapper">
|
||
<span>{{ item.label }}</span>
|
||
<span v-if="item.badge" :class="getBadgeClass(item.badge)">{{ item.badge }}</span>
|
||
</NuxtLink>
|
||
</template>
|
||
<a-menu-item v-for="sub in item.children" :key="sub.key">
|
||
<a v-if="sub.href" :href="sub.href" target="_blank" rel="noopener" class="nav-item-wrapper">{{ sub.label }}</a>
|
||
<NuxtLink v-else :to="sub.to">{{ sub.label }}</NuxtLink>
|
||
</a-menu-item>
|
||
</a-sub-menu>
|
||
<a-menu-item v-else :key="item.to">
|
||
<NuxtLink :to="item.to" class="nav-item-wrapper">
|
||
<span>{{ item.label }}</span>
|
||
<span v-if="item.badge" :class="getBadgeClass(item.badge)">{{ item.badge }}</span>
|
||
</NuxtLink>
|
||
</a-menu-item>
|
||
</template>
|
||
</a-menu>
|
||
</div>
|
||
|
||
<!-- 右侧操作区 -->
|
||
<div class="flex items-center gap-2 flex-shrink-0 nav-right">
|
||
<!-- PC 端:登录/头像 -->
|
||
<div class="hidden md:flex items-center gap-3">
|
||
<template v-if="!isAuthed">
|
||
<a-button type="primary" @click="navigateTo('/login')">{{ $t('nav.login') }}</a-button>
|
||
</template>
|
||
<template v-else>
|
||
<!-- 控制台快捷按钮 -->
|
||
<a-button class="console-btn" @click="navigateTo('/console')">{{ $t('nav.dashboard') }}</a-button>
|
||
<!-- 消息通知铃铛仅在 /console 页面显示 -->
|
||
<a-dropdown :trigger="['hover']" placement="bottomRight">
|
||
<a-space>
|
||
<a-avatar :src="userAvatar" :size="32">
|
||
<template v-if="!userAvatar" #icon>
|
||
<UserOutlined />
|
||
</template>
|
||
</a-avatar>
|
||
<span class="text-gray-100">{{ userName }}</span>
|
||
</a-space>
|
||
<template #overlay>
|
||
<a-menu @click="onUserMenuClick">
|
||
<a-menu-item key="profile"><ProfileOutlined style="margin-right: 8px" />个人信息</a-menu-item>
|
||
<a-menu-item key="my-suggestions"><MessageOutlined style="margin-right: 8px" />我的建言</a-menu-item>
|
||
<template v-if="isSuperAdmin">
|
||
<a-menu-divider />
|
||
<a-menu-item key="admin">⚙️ 后台管理</a-menu-item>
|
||
</template>
|
||
<a-menu-divider />
|
||
<a-menu-item key="logout">{{ $t('nav.logout') }}</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 移动端:汉堡菜单按钮 -->
|
||
<button class="md:hidden flex flex-col justify-center items-center w-10 h-10 gap-1.5 rounded-lg bg-white/10 hover:bg-white/20 border border-white/20 transition-colors" @click="open = true">
|
||
<span class="block w-5 h-0.5 bg-white rounded-full"></span>
|
||
<span class="block w-5 h-0.5 bg-white rounded-full"></span>
|
||
<span class="block w-5 h-0.5 bg-white rounded-full"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</a-layout-header>
|
||
</a-affix>
|
||
|
||
<a-drawer v-model:open="open" :title="$t('nav.navigation') || '导航'" placement="right">
|
||
<a-menu mode="inline" :selected-keys="selectedKeys">
|
||
<template v-for="item in nav" :key="item.key">
|
||
<a-sub-menu v-if="item.children && item.children.length" :key="item.key">
|
||
<template #title>{{ item.label }}</template>
|
||
<a-menu-item v-for="sub in item.children" :key="sub.key">
|
||
<a v-if="sub.href" :href="sub.href" target="_blank" rel="noopener" @click="open = false">{{ sub.label }}</a>
|
||
<span v-else @click="onNav(sub.to)">{{ sub.label }}</span>
|
||
</a-menu-item>
|
||
</a-sub-menu>
|
||
<a-menu-item v-else :key="item.to" @click="onNav(item.to)">
|
||
<NuxtLink :to="item.to" class="nav-item-wrapper">
|
||
<span>{{ item.label }}</span>
|
||
<span v-if="item.badge" :class="getBadgeClass(item.badge)">{{ item.badge }}</span>
|
||
</NuxtLink>
|
||
</a-menu-item>
|
||
</template>
|
||
</a-menu>
|
||
<div class="mt-4">
|
||
<a-button v-if="!isAuthed" block type="primary" @click="onNav('/login')">{{ $t('nav.login') }}</a-button>
|
||
<template v-else>
|
||
<a-button block type="primary" class="mb-2" @click="onNav('/console')">{{ $t('nav.dashboard') }}</a-button>
|
||
<a-button block @click="onNav('/console/account')">{{ $t('nav.userCenter') }}</a-button>
|
||
<a-button block @click="onNav('/console/orders')">{{ $t('nav.orders') || '我的订单' }}</a-button>
|
||
<a-button v-if="isDeveloper" block @click="onNav('/developer')">🛠️ {{ $t('nav.developer') || '开发者中心' }}</a-button>
|
||
<a-button v-if="isSuperAdmin" block @click="onNav('/admin')">⚙️ {{ $t('nav.admin') || '平台管理' }}</a-button>
|
||
<a-button block danger class="mt-2" @click="logout">{{ $t('nav.logout') }}</a-button>
|
||
</template>
|
||
</div>
|
||
</a-drawer>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { mainNav } from '@/config/nav'
|
||
import { getUserInfo } from '@/api/layout'
|
||
import type { User } from '@/api/system/user/model'
|
||
import { getToken, removeToken } from '@/utils/token-util'
|
||
import { clearAuthz, setAuthzFromUser } from '@/utils/permission'
|
||
import { UserOutlined, ProfileOutlined, MessageOutlined } from '@ant-design/icons-vue'
|
||
import { message } from 'ant-design-vue'
|
||
|
||
|
||
const nav = computed(() => mainNav)
|
||
const route = useRoute()
|
||
const open = ref(false)
|
||
|
||
const selectedKeys = computed(() => {
|
||
const hit = nav.value.find((n) => n.to === route.path)
|
||
if (hit) return [hit.to]
|
||
if (route.path.startsWith('/news')) return ['/news']
|
||
if (route.path.startsWith('/consultation')) return ['/consultation']
|
||
if (route.path.startsWith('/reference')) return ['/reference']
|
||
if (route.path.startsWith('/expert')) return ['/expert']
|
||
if (route.path.startsWith('/think-tank')) return ['/think-tank']
|
||
if (route.path.startsWith('/suggestions')) return ['/suggestions']
|
||
if (route.path.startsWith('/membership')) return ['/membership']
|
||
if (route.path.startsWith('/hanmo')) return ['/hanmo']
|
||
if (route.path.startsWith('/about')) return ['/about']
|
||
return ['/']
|
||
})
|
||
|
||
// 获取 badge 样式类
|
||
function getBadgeClass(badge: string) {
|
||
const baseClass = 'ml-1.5 px-1.5 py-0.5 text-xs font-medium rounded'
|
||
if (badge === 'HOT') {
|
||
return `${baseClass} bg-orange-500 text-white`
|
||
}
|
||
if (badge === 'NEW') {
|
||
return `${baseClass} bg-green-500 text-white`
|
||
}
|
||
return `${baseClass} bg-gray-500 text-white`
|
||
}
|
||
|
||
const siteName = ref('葳溯科技')
|
||
|
||
const token = ref('')
|
||
const user = ref<User | null>(null)
|
||
const isAuthed = computed(() => !!token.value)
|
||
const userName = computed(() => String(user.value?.nickname || user.value?.username || '已登录'))
|
||
const isSuperAdmin = computed(() => !!(user.value as any)?.isAdmin)
|
||
const userAvatar = computed(() => {
|
||
const candidate =
|
||
user.value?.avatarUrl ||
|
||
user.value?.avatar ||
|
||
user.value?.merchantAvatar ||
|
||
user.value?.logo ||
|
||
''
|
||
if (typeof candidate !== 'string') return ''
|
||
const normalized = candidate.trim()
|
||
if (!normalized || normalized === 'null' || normalized === 'undefined') return ''
|
||
return normalized
|
||
})
|
||
|
||
function onNav(to: string) {
|
||
open.value = false
|
||
navigateTo(to)
|
||
}
|
||
|
||
async function refreshAuth() {
|
||
token.value = getToken()
|
||
if (!token.value) {
|
||
user.value = null
|
||
clearAuthz()
|
||
return
|
||
}
|
||
try {
|
||
user.value = await getUserInfo()
|
||
setAuthzFromUser(user.value)
|
||
} catch {
|
||
// token may be expired; keep authed UI but without profile info
|
||
clearAuthz()
|
||
}
|
||
}
|
||
|
||
function goConsoleCenter() {
|
||
if (!isAuthed.value) return navigateTo('/login')
|
||
open.value = false
|
||
navigateTo('/profile')
|
||
}
|
||
|
||
function logout() {
|
||
removeToken()
|
||
try {
|
||
localStorage.removeItem('TenantId')
|
||
localStorage.removeItem('UserId')
|
||
} catch {
|
||
// ignore
|
||
}
|
||
clearAuthz()
|
||
user.value = null
|
||
token.value = ''
|
||
open.value = false
|
||
navigateTo('/')
|
||
}
|
||
|
||
function onUserMenuClick(info: { key: string }) {
|
||
if (info.key === 'profile') return navigateTo('/profile')
|
||
if (info.key === 'my-suggestions') return navigateTo('/my/suggestions')
|
||
if (info.key === 'admin') return navigateTo('/admin')
|
||
if (info.key === 'logout') return logout()
|
||
}
|
||
|
||
onMounted(() => {
|
||
refreshAuth()
|
||
|
||
window.addEventListener('auth-token-changed', refreshAuth)
|
||
window.addEventListener('storage', refreshAuth)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('auth-token-changed', refreshAuth)
|
||
window.removeEventListener('storage', refreshAuth)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.header {
|
||
background: #111827;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||
padding: 0;
|
||
height: 64px;
|
||
}
|
||
|
||
/* 两栏布局:左侧 logo+菜单,右侧操作按钮 */
|
||
.nav-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.nav-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5rem; /* logo 与菜单的间距 */
|
||
}
|
||
.nav-right {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.menu {
|
||
background: transparent;
|
||
border-bottom: none;
|
||
min-width: 0;
|
||
}
|
||
.logo-link {
|
||
text-decoration: none;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.logo-text {
|
||
color: #fff;
|
||
font-family: 'Alimama FangYuanTi VF,sans-serif', sans-serif;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
white-space: nowrap;
|
||
line-height: 1;
|
||
background: linear-gradient(135deg, #ffffff 0%, #a5c8ff 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.logo-link:hover .logo-text {
|
||
opacity: 0.85;
|
||
}
|
||
.nav-item-wrapper {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
|
||
/* ── 统一去掉选中/展开/hover 时的蓝色,改为白色文字 + 底部橙线 ── */
|
||
|
||
/* 选中项背景去掉 */
|
||
:deep(.ant-menu-dark .ant-menu-item-selected),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title),
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-item-selected),
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-submenu-selected > .ant-menu-submenu-title) {
|
||
background-color: transparent !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
/* 选中时底部橙线 */
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-item-selected),
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-submenu-selected) {
|
||
background-color: transparent !important;
|
||
border-bottom: 2px solid #f97316 !important;
|
||
}
|
||
|
||
/* sub-menu 展开/hover 时标题不变蓝 */
|
||
:deep(.ant-menu-dark .ant-menu-submenu-open > .ant-menu-submenu-title),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title),
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-submenu-open),
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-submenu-active) {
|
||
background-color: transparent !important;
|
||
color: #fff !important;
|
||
}
|
||
:deep(.ant-menu-dark .ant-menu-submenu-open > .ant-menu-submenu-title > .ant-menu-title-content),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title > .ant-menu-title-content),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-open > .ant-menu-submenu-title > .ant-menu-title-content a),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title > .ant-menu-title-content a) {
|
||
color: #fff !important;
|
||
}
|
||
|
||
/* 展开箭头不变蓝 */
|
||
:deep(.ant-menu-dark .ant-menu-submenu-open > .ant-menu-submenu-title .ant-menu-submenu-arrow),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title .ant-menu-submenu-arrow) {
|
||
color: rgba(255, 255, 255, 0.65) !important;
|
||
}
|
||
|
||
/* 所有菜单项 hover/active 时文字统一白色 */
|
||
:deep(.ant-menu-dark .ant-menu-item-active > .ant-menu-item-content),
|
||
:deep(.ant-menu-dark .ant-menu-item-active > .ant-menu-item-content a),
|
||
:deep(.ant-menu-dark .ant-menu-item-open > .ant-menu-item-content),
|
||
:deep(.ant-menu-dark .ant-menu-item-open > .ant-menu-item-content a),
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-item-active),
|
||
:deep(.ant-menu-dark.ant-menu-horizontal > .ant-menu-item-active a) {
|
||
color: #fff !important;
|
||
}
|
||
|
||
/* 所有菜单项默认文字颜色(覆盖 ant-menu-item a 的蓝色) */
|
||
:deep(.ant-menu-dark .ant-menu-item a),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-title a) {
|
||
color: rgba(255, 255, 255, 0.85) !important;
|
||
}
|
||
:deep(.ant-menu-dark .ant-menu-item a:hover),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-title a:hover) {
|
||
color: #fff !important;
|
||
}
|
||
|
||
/* 选中项文字强制白色 */
|
||
:deep(.ant-menu-dark .ant-menu-item-selected a),
|
||
:deep(.ant-menu-dark .ant-menu-submenu-selected .ant-menu-submenu-title a) {
|
||
color: #fff !important;
|
||
}
|
||
/* 控制台按钮:白色边框 + 白色文字,hover 加白色背景 */
|
||
.console-btn {
|
||
background: transparent !important;
|
||
border-color: rgba(255, 255, 255, 0.45) !important;
|
||
color: #fff !important;
|
||
}
|
||
.console-btn:hover {
|
||
border-color: #fff !important;
|
||
background: rgba(255, 255, 255, 0.1) !important;
|
||
color: #fff !important;
|
||
}
|
||
</style>
|