fix:appheader的样式切换

This commit is contained in:
小张 2025-08-29 17:58:32 +08:00
parent 4f90499ada
commit cea9929ebc
9 changed files with 326 additions and 39 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -38,10 +38,6 @@
<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>
<!-- 搜索框 -->
@ -54,10 +50,10 @@
<!-- 移动端汉堡菜单按钮 -->
<div class="mobile-menu-toggle" @click="toggleMobileMenu">
<n-icon size="24">
<NIcon size="24">
<MenuOutline v-if="!mobileMenuOpen" />
<CloseOutline v-else />
</n-icon>
</NIcon>
</div>
<!-- 右侧操作区域 -->
@ -84,31 +80,32 @@
<span>{{ t('header.learningCenter') }}</span>
</div>
<!-- 管理端 -->
<!-- 管理端
<div class="action-item">
<img src="/nav-icons/管理端.png" alt="" class="action-icon default-icon" />
<img src="/nav-icons/管理端-选中.png" alt="" class="action-icon hover-icon" />
<span>{{ t('header.management') }}</span>
</div>
</div> -->
<!-- 登录按钮 -->
<div v-if="!userStore.isLoggedIn" class="auth-buttons" @click="showLoginModal">
<div class="auth-combined-btn">
<span class="auth-login" >{{ t('header.login') }}</span>
<span class="auth-login">{{ t('header.login') }}</span>
</div>
</div>
<!-- 登录后的用户区域 -->
<div v-else class="user-menu">
<n-dropdown :options="userMenuOptions" @select="handleUserMenuSelect">
<NDropdown :options="userMenuOptions" @select="handleUserMenuSelect">
<div class="user-info">
<SafeAvatar :src="userStore.user?.avatar"
<SafeAvatar
:src="userStore.user?.avatar"
:name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username"
:size="32" />
<span class="username">{{ userStore.user?.profile?.realName || userStore.user?.nickname ||
userStore.user?.username }}</span>
:size="32"
/>
<span class="username">{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username }}</span>
</div>
</n-dropdown>
</NDropdown>
</div>
</div>
@ -117,14 +114,11 @@
<!-- 注册模态框 -->
<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, watch } from 'vue'
import { computed, ref, onMounted, onUnmounted, h, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
@ -133,9 +127,9 @@ import {
MenuOutline,
CloseOutline
} from '@vicons/ionicons5'
import { NDropdown, NIcon } from 'naive-ui'
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()
@ -195,7 +189,7 @@ const setActiveKeyFromRoute = () => {
//
const loginModalVisible = ref(false)
const registerModalVisible = ref(false)
const registerNoticeVisible = ref(false)
//
const showLanguageDropdown = ref(false)
@ -229,17 +223,150 @@ const handleLearningCenter = () => {
}
}
//
const setupMenuHoverEffects = () => {
const handleMouseEnter = (e: Event) => {
const option = e.currentTarget as HTMLElement
const container = option.querySelector('.menu-icon-container') as HTMLElement
if (!container) return
const defaultIcon = container.querySelector('.default-icon') as HTMLElement
const hoverIcon = container.querySelector('.hover-icon') as HTMLElement
//
if (defaultIcon) defaultIcon.style.opacity = '0'
if (hoverIcon) hoverIcon.style.opacity = '1'
// - 使
option.style.setProperty('color', '#0088D1', 'important')
option.style.setProperty('background', 'linear-gradient(135deg, #f0f8ff, #e6f4ff)', 'important')
//
const allTextElements = option.querySelectorAll('*')
allTextElements.forEach((el: Element) => {
const element = el as HTMLElement
if (element.textContent && element.textContent.trim()) {
element.style.setProperty('color', '#0088D1', 'important')
}
})
console.log('悬停进入:', option.textContent?.trim())
}
const handleMouseLeave = (e: Event) => {
const option = e.currentTarget as HTMLElement
const container = option.querySelector('.menu-icon-container') as HTMLElement
if (!container) return
const defaultIcon = container.querySelector('.default-icon') as HTMLElement
const hoverIcon = container.querySelector('.hover-icon') as HTMLElement
//
if (defaultIcon) defaultIcon.style.opacity = '1'
if (hoverIcon) hoverIcon.style.opacity = '0'
//
option.style.removeProperty('color')
option.style.removeProperty('background')
//
const allTextElements = option.querySelectorAll('*')
allTextElements.forEach((el: Element) => {
const element = el as HTMLElement
element.style.removeProperty('color')
})
console.log('悬停离开:', option.textContent?.trim())
}
// 使MutationObserverdropdown
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
const menuOptions = document.querySelectorAll('.n-dropdown-option')
if (menuOptions.length > 0) {
menuOptions.forEach((option) => {
const container = option.querySelector('.menu-icon-container')
if (container) {
//
option.removeEventListener('mouseenter', handleMouseEnter)
option.removeEventListener('mouseleave', handleMouseLeave)
//
option.addEventListener('mouseenter', handleMouseEnter)
option.addEventListener('mouseleave', handleMouseLeave)
}
})
console.log('菜单悬停事件已绑定')
}
}
})
})
// body
observer.observe(document.body, {
childList: true,
subtree: true
})
//
setTimeout(() => {
const menuOptions = document.querySelectorAll('.n-dropdown-option')
if (menuOptions.length > 0) {
menuOptions.forEach((option) => {
const container = option.querySelector('.menu-icon-container')
if (container) {
option.addEventListener('mouseenter', handleMouseEnter)
option.addEventListener('mouseleave', handleMouseLeave)
}
})
console.log('菜单悬停事件已立即绑定')
}
}, 500)
}
//
const userMenuOptions = computed(() => [
{
label: '个人中心',
key: 'profile',
icon: () => h('div', { class: 'custom-icon' }, '👤')
icon: () => h('div', {
class: 'menu-icon-container',
style: 'position: relative; width: 18px; height: 18px; display: inline-block; margin-right: 1px; overflow: hidden;'
}, [
h('img', {
src: '/images/personal/用户_user备份@2x.png',
alt: '个人中心',
class: 'menu-icon default-icon',
style: 'width: 18px !important; height: 18px !important; max-width: 18px; max-height: 18px; object-fit: contain; position: absolute; top: 0; left: 0;'
}),
h('img', {
src: '/images/personal/用户_user备份 2@2x.png',
alt: '个人中心',
class: 'menu-icon hover-icon',
style: 'width: 18px !important; height: 18px !important; max-width: 18px; max-height: 18px; object-fit: contain; position: absolute; top: 0; left: 0; opacity: 0;'
})
])
},
{
label: '切换教师端',
key: 'teacher',
icon: () => h('div', { class: 'custom-icon' }, '👨‍🏫')
icon: () => h('div', {
class: 'menu-icon-container',
style: 'position: relative; width: 18px; height: 18px; display: inline-block; margin-right: 1px; overflow: hidden;'
}, [
h('img', {
src: '/images/personal/切换_switch备份@2x.png',
alt: '切换教师端',
class: 'menu-icon default-icon',
style: 'width: 18px !important; height: 18px !important; max-width: 18px; max-height: 18px; object-fit: contain; position: absolute; top: 0; left: 0;'
}),
h('img', {
src: '/images/personal/切换_switch备份 2@2x.png',
alt: '切换教师端',
class: 'menu-icon hover-icon',
style: 'width: 18px !important; height: 18px !important; max-width: 18px; max-height: 18px; object-fit: contain; position: absolute; top: 0; left: 0; opacity: 0;'
})
])
},
{
type: 'divider'
@ -247,10 +374,28 @@ const userMenuOptions = computed(() => [
{
label: '退出登录',
key: 'logout',
icon: () => h('div', { class: 'custom-icon' }, '🚪')
icon: () => h('div', {
class: 'menu-icon-container',
style: 'position: relative; width: 18px; height: 18px; display: inline-block; margin-right: 1px; overflow: hidden;'
}, [
h('img', {
src: '/images/personal/退出_logout备份 2@2x.png',
alt: '退出登录',
class: 'menu-icon default-icon',
style: 'width: 18px !important; height: 18px !important; max-width: 18px; max-height: 18px; object-fit: contain; position: absolute; top: 0; left: 0;'
}),
h('img', {
src: '/images/personal/退出_logout备份 3@2x.png',
alt: '退出登录',
class: 'menu-icon hover-icon',
style: 'width: 18px !important; height: 18px !important; max-width: 18px; max-height: 18px; object-fit: contain; position: absolute; top: 0; left: 0; opacity: 0;'
})
])
}
])
// CSS
//
const handleMenuSelect = (key: string) => {
activeKey.value = key
@ -324,10 +469,6 @@ const handleAuthSuccess = () => {
console.log('认证成功')
}
//
const closeRegisterNotice = () => {
registerNoticeVisible.value = false
}
// handleClickOutside
const handleClickOutside = (event: MouseEvent) => {
@ -342,6 +483,8 @@ onMounted(() => {
document.addEventListener('click', handleClickOutside)
//
setActiveKeyFromRoute()
//
setupMenuHoverEffects()
})
onUnmounted(() => {
@ -754,25 +897,169 @@ watch(() => route.path, () => {
:deep(.n-dropdown-option) {
padding: 12px 16px;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
border-radius: 0;
}
/* 默认文字颜色 */
:deep(.n-dropdown-option .n-dropdown-option__label) {
color: #666666 !important;
transition: color 0.2s ease;
}
/* 强制覆盖Naive UI的默认样式 */
:deep(.n-dropdown-option) {
color: #666666 !important;
transition: all 0.2s ease;
}
/* 菜单图标容器样式 - 使用更强的选择器 */
:deep(.menu-icon-container) {
position: relative !important;
width: 18px !important;
height: 18px !important;
display: inline-block !important;
margin-right: 8px !important;
flex-shrink: 0 !important;
overflow: hidden !important;
}
/* 菜单图标样式 - 使用通配符强制覆盖所有图片 */
:deep(.n-dropdown-option img),
:deep(.n-dropdown-option .menu-icon-container img),
:deep(.n-dropdown-option .menu-icon-container .menu-icon),
:deep(.n-dropdown-option img.menu-icon),
:deep(.menu-icon-container img),
:deep(.menu-icon-container .menu-icon),
:deep(img.menu-icon),
:deep(.n-dropdown-option *) img {
width: 18px !important;
height: 18px !important;
max-width: 18px !important;
max-height: 18px !important;
min-width: 18px !important;
min-height: 18px !important;
object-fit: contain !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
transition: opacity 0.2s ease !important;
display: block !important;
transform: scale(1) !important;
}
/* 默认状态:显示默认图标,隐藏悬停图标 */
:deep(.menu-icon-container img.default-icon),
:deep(.menu-icon-container .menu-icon.default-icon),
:deep(img.menu-icon.default-icon) {
opacity: 1 !important;
z-index: 2 !important;
}
:deep(.menu-icon-container img.hover-icon),
:deep(.menu-icon-container .menu-icon.hover-icon),
:deep(img.menu-icon.hover-icon) {
opacity: 0 !important;
z-index: 1 !important;
}
/* 悬停时的背景和文字颜色效果 */
:deep(.n-dropdown-option:hover) {
background: linear-gradient(135deg, #f0f8ff, #e6f4ff);
color: #1890ff;
background: linear-gradient(135deg, #f0f8ff, #e6f4ff) !important;
color: #0088D1 !important;
}
/* 悬停时文字颜色变化 - 使用多重选择器确保生效 */
:deep(.n-dropdown-option:hover .n-dropdown-option__label),
:deep(.n-dropdown-option:hover span),
:deep(.n-dropdown-option:hover) {
color: #0088D1 !important;
}
/* 强制覆盖所有可能的文字颜色 */
:deep(.n-dropdown-option:hover *:not(img)) {
color: #0088D1 !important;
}
/* 悬停时的图标切换效果 - 使用所有可能的选择器 */
:deep(.n-dropdown-option:hover .menu-icon-container img.default-icon),
:deep(.n-dropdown-option:hover .menu-icon-container .menu-icon.default-icon),
:deep(.n-dropdown-option:hover img.menu-icon.default-icon) {
opacity: 0 !important;
}
:deep(.n-dropdown-option:hover .menu-icon-container img.hover-icon),
:deep(.n-dropdown-option:hover .menu-icon-container .menu-icon.hover-icon),
:deep(.n-dropdown-option:hover img.menu-icon.hover-icon) {
opacity: 1 !important;
}
/* 覆盖Naive UI的图标容器样式 */
: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;
margin-right: 8px !important;
width: 18px !important;
height: 18px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
flex-shrink: 0 !important;
overflow: hidden !important;
}
/* 确保图标容器内的元素正确显示 */
:deep(.n-dropdown-option .n-dropdown-option-icon .menu-icon-container) {
width: 18px !important;
height: 18px !important;
}
/* 强制限制所有图片元素的尺寸 - 全局重置 */
:deep(.n-dropdown-option img) {
width: 18px !important;
height: 18px !important;
max-width: 18px !important;
max-height: 18px !important;
min-width: 18px !important;
min-height: 18px !important;
object-fit: contain !important;
box-sizing: border-box !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
/* 全局强制重置 - 覆盖任何可能的样式 */
:deep(.n-dropdown) img,
:deep(.n-dropdown-menu) img,
:deep(.n-dropdown-option) img {
width: 18px !important;
height: 18px !important;
max-width: 18px !important;
max-height: 18px !important;
min-width: 18px !important;
min-height: 18px !important;
}
/* 全局样式重置 - 不使用 :deep() */
.n-dropdown img,
.n-dropdown-menu img,
.n-dropdown-option img {
width: 18px !important;
height: 18px !important;
max-width: 18px !important;
max-height: 18px !important;
min-width: 18px !important;
min-height: 18px !important;
object-fit: contain !important;
}
/* 针对用户菜单的特殊重置 */
.user-menu img,
.user-menu * img {
width: 18px !important;
height: 18px !important;
max-width: 18px !important;
max-height: 18px !important;
}
.custom-icon {
font-size: 14px;