2025-08-19 19:04:11 +08:00

849 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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>
<div class="header-container">
<!-- Logo区域 -->
<div class="logo-section">
<div class="logo" @click="$router.push('/')">
<img src="/logo/logo1.png" alt="Logo" class="logo-image" />
</div>
</div>
<!-- 导航菜单 -->
<div class="nav-menu" :class="{ 'mobile-open': mobileMenuOpen }">
<div class="nav-item" :class="{ active: activeKey === 'home' }" @click="handleMenuSelect('home')">
{{ t('header.home') }}
</div>
<div class="nav-item" :class="{ active: activeKey === 'courses' }" @click="handleMenuSelect('courses')">
<img src="/nav-icons/火.png" alt="" class="nav-icon" />
{{ t('header.courses') }}
</div>
<div class="nav-item" :class="{ active: activeKey === 'training' }" @click="handleMenuSelect('training')">
{{ t('header.training') }}
</div>
<div class="nav-item" :class="{ active: activeKey === 'faculty' }" @click="handleMenuSelect('faculty')">
{{ t('header.learningPaths') }}
</div>
<div class="nav-item" :class="{ active: activeKey === 'resources' }" @click="handleMenuSelect('resources')">
{{ t('header.resources') }}
</div>
<div class="nav-item" :class="{ active: activeKey === 'activities' }" @click="handleMenuSelect('activities')">
{{ t('header.about') }}
<img src="/nav-icons/new.png" alt="new" class="new-badge" />
</div>
<div class="nav-item" :class="{ active: activeKey === 'ai' }" @click="handleMenuSelect('ai')">
<span class="nav-item-ai">AI体验</span>
</div>
<!-- <div class="nav-item" :class="{ active: activeKey === 'practice' }" @click="handleMenuSelect('practice')">
<span class="nav-item-practice">AI伴学</span>
</div> -->
</div>
<!-- 搜索框 -->
<div class="search-section">
<div class="search-box">
<img src="/nav-icons/搜索.png" alt="" class="search-icon" />
<input type="text" placeholder="请输入感兴趣的课程" class="search-input" />
</div>
</div>
<!-- 移动端汉堡菜单按钮 -->
<div class="mobile-menu-toggle" @click="toggleMobileMenu">
<n-icon size="24">
<MenuOutline v-if="!mobileMenuOpen" />
<CloseOutline v-else />
</n-icon>
</div>
<!-- 右侧操作区域 -->
<div class="header-actions">
<!-- 切换语言 -->
<div class="action-item language-switcher" @click="toggleLanguageDropdown" ref="languageSwitcherRef">
<img src="/nav-icons/矩形.png" alt="" class="action-icon" />
<span>{{ t('header.languageSwitch') }}</span>
<div v-if="showLanguageDropdown" class="language-dropdown">
<div class="language-option" @click.stop="switchLanguage('zh')">
<span class="language-text">{{ t('languageDropdown.switchToChinese') }}</span>
</div>
<div class="language-option" @click.stop="switchLanguage('en')">
<span class="language-text">{{ t('languageDropdown.switchToEnglish') }}</span>
</div>
</div>
</div>
<!-- 学习中心 -->
<div class="action-item" @click="handleLearningCenter">
<img src="/nav-icons/学习中心.png" alt="" class="action-icon" />
<span>{{ t('header.learningCenter') }}</span>
</div>
<!-- 管理端 -->
<div class="action-item">
<img src="/nav-icons/管理端.png" alt="" class="action-icon" />
<span>{{ t('header.management') }}</span>
</div>
<!-- 登录/注册按钮 -->
<div v-if="!userStore.isLoggedIn" class="auth-buttons">
<div class="auth-combined-btn">
<span class="auth-login" @click="showLoginModal">{{ t('header.login') }}</span>
<span class="auth-divider">|</span>
<span class="auth-register" @click="showRegisterModal">{{ t('header.register') }}</span>
</div>
</div>
<!-- 登录后的用户区域 -->
<div v-else class="user-menu">
<n-dropdown :options="userMenuOptions" @select="handleUserMenuSelect">
<div class="user-info">
<SafeAvatar :src="userStore.user?.avatar" :name="userStore.user?.username" :size="32" />
<span class="username">{{ userStore.user?.username }}</span>
</div>
</n-dropdown>
</div>
</div>
<!-- 登录模态框 -->
<LoginModal v-model:show="loginModalVisible" @success="handleAuthSuccess" />
<!-- 注册模态框 -->
<RegisterModal v-model:show="registerModalVisible" @success="handleAuthSuccess" />
<!-- 内测通知模态框 -->
<RegisterNotice v-model:show="registerNoticeVisible" @close="closeRegisterNotice" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, h, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import {
MenuOutline,
CloseOutline
} from '@vicons/ionicons5'
import LoginModal from '@/components/auth/LoginModal.vue'
import RegisterModal from '@/components/auth/RegisterModal.vue'
import RegisterNotice from '@/components/auth/RegisterNotice.vue'
import SafeAvatar from '@/components/common/SafeAvatar.vue'
const router = useRouter()
const { t, locale } = useI18n()
const userStore = useUserStore()
// 移动端菜单状态
const mobileMenuOpen = ref(false)
// 当前激活的菜单项
const activeKey = ref('home')
// 认证模态框相关
const loginModalVisible = ref(false)
const registerModalVisible = ref(false)
const registerNoticeVisible = ref(false)
// 语言切换相关
const showLanguageDropdown = ref(false)
const languageSwitcherRef = ref<HTMLElement | null>(null)
// 切换移动端菜单
const toggleMobileMenu = () => {
mobileMenuOpen.value = !mobileMenuOpen.value
}
// 切换语言下拉框
const toggleLanguageDropdown = () => {
showLanguageDropdown.value = !showLanguageDropdown.value
}
// 切换语言
const switchLanguage = (lang: string) => {
locale.value = lang
localStorage.setItem('locale', lang)
showLanguageDropdown.value = false
console.log('语言已切换到:', lang === 'zh' ? '中文' : '英文')
}
const handleLearningCenter = () => {
console.log('学习中心被点击了')
try {
router.push('/learning-center')
console.log('路由跳转成功')
} catch (error) {
console.error('路由跳转失败:', error)
}
}
// 用户菜单选项
const userMenuOptions = computed(() => [
{
label: '个人中心',
key: 'profile',
icon: () => h('div', { class: 'custom-icon' }, '👤')
},
{
label: '切换教师端',
key: 'teacher',
icon: () => h('div', { class: 'custom-icon' }, '👨‍🏫')
},
{
type: 'divider'
},
{
label: '退出登录',
key: 'logout',
icon: () => h('div', { class: 'custom-icon' }, '🚪')
}
])
// 处理菜单选择
const handleMenuSelect = (key: string) => {
activeKey.value = key
// 关闭移动菜单
mobileMenuOpen.value = false
switch (key) {
case 'home':
router.push('/')
break
case 'courses':
router.push('/courses')
break
case 'training':
router.push('/special-training')
break
case 'faculty':
router.push('/faculty')
break
case 'resources':
router.push('/resources')
break
case 'activities':
router.push('/activities')
break
case 'ai':
router.push('/ai')
break
case 'practice':
router.push('/ai-companion')
break
}
}
// 处理用户菜单选择
const handleUserMenuSelect = (key: string) => {
switch (key) {
case 'profile':
router.push('/profile').then(() => {
// 检查sessionStorage中是否已有刷新标志
window.location.reload();
})
break
case 'teacher':
// 切换教师端逻辑
console.log('切换到教师端')
// 这里可以添加切换教师端的逻辑,比如跳转到教师端页面
router.push('/teacher')
break
case 'logout':
userStore.logout()
router.push('/')
break
}
}
// 显示登录模态框
const showLoginModal = () => {
loginModalVisible.value = true
}
// 显示注册模态框
const showRegisterModal = () => {
// 这里可以根据实际需求判断是否弹出内测通知
// 例如如果要弹出内测通知直接显示registerNoticeVisible
// 否则显示注册弹窗
// 目前假设直接弹注册弹窗
registerModalVisible.value = true
registerNoticeVisible.value = false
}
// 认证成功处理
const handleAuthSuccess = () => {
// 认证成功后可以进行一些操作,比如刷新用户信息等
console.log('认证成功')
}
// 关闭内测通知
const closeRegisterNotice = () => {
registerNoticeVisible.value = false
}
// 修复未定义的handleClickOutside
const handleClickOutside = (event: MouseEvent) => {
// 这里只处理语言下拉的关闭,其他弹窗由自身控制
if (languageSwitcherRef.value && !languageSwitcherRef.value.contains(event.target as Node)) {
showLanguageDropdown.value = false
}
}
// 生命周期钩子
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: none;
margin: 0;
padding: 0 30px;
height: 64px;
background: white;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1001;
}
/* Logo区域 */
.logo-section {
flex-shrink: 0;
margin-right: 40px;
}
.logo {
display: flex;
align-items: center;
cursor: pointer;
gap: 8px;
}
.logo:hover {
opacity: 0.8;
}
.logo-image {
width: 72px;
height: 61px;
object-fit: contain;
}
.nav-icon {
max-width: 12px;
max-height: 12px;
width: auto;
height: auto;
margin-right: 4px;
object-fit: contain;
}
/* AI图标样式 */
.ai-icon {
max-width: 34px;
max-height: 34px;
margin-right: 0;
}
.action-icon {
max-width: 18px;
max-height: 18px;
width: auto;
height: auto;
object-fit: contain;
}
/* 导航菜单 */
.nav-menu {
display: flex;
align-items: center;
gap: 30px;
flex: 1;
margin-right: 40px;
}
.nav-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
font-size: 14px;
font-weight: 400;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #000;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
position: relative;
height: 24px;
}
/* 两个字的导航项:首页 */
.nav-item:nth-child(1) {
width: 36px;
}
/* 四个字的导航项:热门好课、专题训练、师资力量、精选资源 */
.nav-item:nth-child(2),
.nav-item:nth-child(3),
.nav-item:nth-child(4),
.nav-item:nth-child(5) {
width: 72px;
}
/* 两个字的导航项:活动 */
.nav-item:nth-child(6) {
width: 36px;
padding-right: 16px;
/* 为HOT标签留出空间 */
}
/* AI导航项 */
.nav-item:nth-child(7) {
/* width: 50px;
height: 40px; */
background-image: url('/images/ai/ai-bg.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.nav-item:nth-child(8) {
padding: 0;
}
.nav-item.active .nav-item-ai {
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.nav-item-ai:hover {
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.nav-item:hover {
color: #0084CD;
}
.nav-item.active {
color: #0084CD;
font-weight: 400;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
position: relative;
}
.nav-item.active::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 2px;
background-color: #0084CD;
border-radius: 1px;
}
.new-badge {
position: absolute;
top: -10px;
right: -22px;
max-width: 28px;
max-height: 28px;
width: auto;
height: auto;
object-fit: contain;
z-index: 10;
}
/* 搜索区域 */
.search-section {
display: flex;
align-items: center;
margin-right: 40px;
}
.search-box {
/* border-left: 1px solid #ececec;
border-right: 1px solid #ececec; */
display: flex;
align-items: center;
padding: 22px 16px;
width: 280px;
transition: all 0.2s;
}
/* .search-box:hover {
background: #eeeeee;
} */
.search-icon {
max-width: 18px;
max-height: 18px;
width: auto;
height: auto;
margin-right: 8px;
object-fit: contain;
}
.search-input {
flex: 1;
border: none;
background: transparent;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #999;
}
/* 移动端汉堡菜单按钮 */
.mobile-menu-toggle {
display: none;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.2s;
}
.mobile-menu-toggle:hover {
background: rgba(0, 0, 0, 0.05);
}
/* 右侧操作区域 */
.header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.action-item {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
font-size: 13px;
color: #000;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
white-space: nowrap;
}
.action-item:hover {
color: #1890ff;
background: #f0f8ff;
}
/* 语言切换器 */
.language-switcher {
position: relative;
cursor: pointer;
}
.language-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #e8e8e8;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 4px;
min-width: 120px;
}
.language-option {
padding: 8px 12px;
font-size: 13px;
color: #666;
cursor: pointer;
transition: all 0.2s;
border-bottom: 1px solid #f5f5f5;
}
.language-option:last-child {
border-bottom: none;
}
.language-option:hover {
background: #f0f8ff;
color: #1890ff;
}
.language-text {
white-space: nowrap;
}
/* 认证按钮 */
.auth-buttons {
display: flex;
align-items: center;
gap: 12px;
}
.auth-combined-btn {
display: flex;
align-items: center;
background: #0088D1;
color: white;
border-radius: 5px;
padding: 8px 10px;
font-size: 12px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
}
.auth-combined-btn:hover {
background: #40a9ff;
}
.auth-login,
.auth-register {
padding: 0 8px;
cursor: pointer;
transition: all 0.2s;
}
.auth-login:hover,
.auth-register:hover {
opacity: 0.8;
}
.auth-divider {
color: rgba(255, 255, 255, 0.6);
margin: 0 4px;
font-weight: 300;
}
/* 用户菜单 */
.user-menu {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.user-info:hover {
background: #f0f8ff;
}
.username {
font-size: 14px;
color: #333;
white-space: nowrap;
}
/* 美化用户菜单样式 */
:deep(.n-dropdown-menu) {
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid #f0f0f0;
padding: 8px 0;
}
:deep(.n-dropdown-option) {
padding: 12px 16px;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
border-radius: 0;
}
:deep(.n-dropdown-option:hover) {
background: linear-gradient(135deg, #f0f8ff, #e6f4ff);
color: #1890ff;
}
:deep(.n-dropdown-option .n-dropdown-option-icon) {
margin-right: 12px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
.custom-icon {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
/* 分割线样式 */
:deep(.n-dropdown-divider) {
margin: 8px 0;
border-color: #f0f0f0;
}
/* 大屏幕 */
@media (min-width: 1200px) {
.header-container {
padding: 0 32px;
}
.search-input {
width: 280px;
}
}
/* 平板 */
@media (max-width: 992px) {
.header-container {
padding: 0 20px;
}
.nav-menu {
margin: 0 20px;
max-width: 300px;
}
.search-input {
width: 200px;
}
}
/* 小平板 */
@media (max-width: 768px) {
.header-container {
padding: 0 16px;
}
.nav-menu {
display: none;
position: fixed;
top: 64px;
left: 0;
right: 0;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 999;
flex-direction: column;
padding: 16px 0;
}
.nav-menu.mobile-open {
display: flex;
}
.nav-menu .nav-item {
padding: 12px 24px;
border-radius: 0;
justify-content: flex-start;
}
.mobile-menu-toggle {
display: flex;
}
.search-input {
width: 160px;
}
.logo-text {
display: none;
}
.user-actions {
gap: 12px;
}
}
/* 手机 */
@media (max-width: 576px) {
.header-container {
padding: 0 12px;
}
.search-input {
width: 120px;
}
.user-actions {
gap: 8px;
}
.username {
display: none;
}
.auth-buttons {
gap: 4px;
}
}
/* 小手机 */
@media (max-width: 480px) {
.header-container {
padding: 0 8px;
}
.logo {
font-size: 16px;
}
.search-input {
width: 100px;
}
.user-actions {
gap: 6px;
}
}
/* 全屏模式样式现在在App.vue中统一管理 */
</style>