Files
jczxw-pc/app/components/SiteHeader.vue
赵忠林 17ce26411d feat(home): 完成首页全面重构与新增多个板块组件
- 重构首页结构,增加Banner轮播与左侧快捷入口
- 新增单位企业广告区及会员服务广告区
- 实现决策咨询板块,涵盖市县决策、行业资讯、前沿观察、企业动态
- 设计决策参考板块,展示政策原文、深度解读、研究成果、专题研究
- 完善专家资讯板块,包含专家视点、专家动态及专家申请卡片
- 添加智库观察板块与翰墨文谈板块,丰富内容分类
- 底部功能区新增资料下载、申报模板、成果报送、联系我们快捷链接
- 采用蓝色主色调和卡片式布局,提升视觉统一与响应式体验
- 使用模拟数据占位,便于后续接入实际API数据
- 重构SiteFooter提升一致性与风格统一
2026-04-24 19:50: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>