Files
jczxw-pc/app/components/SiteHeader.vue
赵忠林 72f5025979 feat(about): 重构“关于我们”页面并丰富内容展示
- 采用左右分栏布局,左侧新增图标导航
- 全新设计顶部 Banner,提升视觉效果
- 添加学会简介数据亮点和主要职能展示
- 新增组织机构图、主要领导及专家委员会成员展示
- 引入学会章程章节分明条目展示
- 丰富咨询服务内容,新增服务项目卡片和联系方式
- “加入我们”板块支持企业与个人会员申请详情说明
- 支持资料下载并优化排版与交互体验
- 增强响应式支持,保证移动端体验一致
- 页面样式大幅调整,提升整体美观与可读性
2026-04-26 01:44:20 +08:00

371 lines
13 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

<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-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('/profile')">个人中心</a-button>
<a-button v-if="isSuperAdmin" block @click="onNav('/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>