Files
jczxw-pc/app/components/SiteHeader.vue
赵忠林 55f1cded6b refactor(i18n): 移除多语言支持及相关组件,切换登录页文本为中文
- 删除英文和中文语言文件,去除国际化配置
- 移除语言切换组件 LangSwitch.vue
- 登录页中静态替换多语言文本为中文文本
- 站点头部登录、退出等文案替换为中文
- 更新 Nuxt 配置,移除 i18n 模块和相关配置
- 保持核心功能不变,只保留中文语言显示
2026-04-26 02:39:43 +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')">{{ '登录' }}</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">{{ '退出登录' }}</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="'导航' || '导航'" 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')">{{ '登录' || '登录' }}</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">{{ '退出登录' || '退出登录' }}</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>