Files
jczxw-pc/app/components/SiteHeader.vue
2026-04-23 16:30:57 +08:00

422 lines
16 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">
<img src="/logo.png" alt="websopy" class="logo-img" />
<div class="site-name mx-2">{{ 'websopy' }}</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="orders"><ShoppingCartOutlined style="margin-right: 8px" />我的订单</a-menu-item>
<a-menu-item key="account-kyc">
<IdcardOutlined style="margin-right: 8px" />
实名认证
</a-menu-item>
<template v-if="isDeveloper">
<a-menu-divider />
<a-menu-item key="developer">🛠 {{ $t('nav.developer') || '开发者中心' }}</a-menu-item>
<a-menu-item key="env-dev" @click.stop="switchEnv('dev')">
<span :class="{ 'font-bold': currentEnv === 'dev' }">🔧 {{ $t('common.devEnv') || '开发环境' }}</span>
<span v-if="currentEnv === 'dev'" class="ml-2 text-green-500"></span>
</a-menu-item>
<a-menu-item key="env-prod" @click.stop="switchEnv('prod')">
<span :class="{ 'font-bold': currentEnv === 'prod' }">🚀 {{ $t('common.prodEnv') || '生产环境' }}</span>
<span v-if="currentEnv === 'prod'" class="ml-2 text-green-500"></span>
</a-menu-item>
</template>
<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, IdcardOutlined, ShoppingCartOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { ENV_CONFIG, getCurrentEnv, setCurrentEnv, type EnvKey } from '@/config/setting'
import InviteBell from './invite/InviteBell.vue'
const nav = computed(() => mainNav)
const route = useRoute()
const open = ref(false)
// 环境切换
const currentEnv = ref<EnvKey>(getCurrentEnv())
function switchEnv(env: EnvKey) {
setCurrentEnv(env)
currentEnv.value = env
// 同时设置 Cookie让服务器端代理也能识别环境
const cookieValue = env === 'dev' ? 'dev' : 'prod'
document.cookie = `websopy_api_env=${cookieValue}; path=/; max-age=31536000`
message.success(`已切换到 ${ENV_CONFIG[env].name},正在刷新...`)
setTimeout(() => {
window.location.reload()
}, 500)
}
const selectedKeys = computed(() => {
const hit = nav.value.find((n) => n.to === route.path)
if (hit) return [hit.to]
if (route.path.startsWith('/products')) return ['/products']
if (route.path.startsWith('/ai-agent')) return ['/ai-agent']
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 isDeveloper = computed(() => (user.value as any)?.type === 2)
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('/console')
}
function goDeveloperCenter() {
if (!isAuthed.value) return navigateTo('/login')
open.value = false
navigateTo('/developer')
}
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('/console/account')
if (info.key === 'orders') return navigateTo('/console/orders')
if (info.key === 'account-kyc') return navigateTo('/console/account/kyc')
if (info.key === 'developer') return navigateTo('/developer')
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-img {
height: 22px;
width: auto;
display: block;
transition: opacity 0.2s;
}
.site-name {
color: #fff;
font-family: 'Alimama FangYuanTi VF,sans-serif', sans-serif;
font-size: 18px;
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 .site-name {
opacity: 0.85;
}
.logo-link:hover .logo-img {
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>