This commit is contained in:
2026-03-15 23:40:31 +08:00
parent a546774e0a
commit 6fa977b229
3 changed files with 86 additions and 208 deletions

View File

@@ -4,11 +4,6 @@ import { useUserStore } from '@/stores/user'
import UserDropdown from '@/components/UserDropdown.vue'
import BrandLogo from '@/components/BrandLogo.vue'
const styles = {
background: 'var(--color-gray-900)',
color: 'var(--color-text-inverse)'
}
const userStore = useUserStore()
// 计算是否应该显示用户组件
@@ -19,29 +14,17 @@ const shouldShowUser = computed(() => {
</script>
<template>
<header class="header-box" :style="styles">
<div>
<div class="h-[70px] flex items-center">
<div class="flex items-center gap-3 flex-1 pl-[30px]">
<BrandLogo :size="40" />
</div>
<div class="flex items-center gap-4 pr-[35px]">
<template v-if="shouldShowUser">
<UserDropdown />
</template>
</div>
</div>
<header
class="fixed top-0 left-0 right-0 z-[100]
h-[70px] flex items-center px-[30px]
bg-gray-900 text-white
border-b border-border"
>
<div class="flex items-center gap-3 flex-1">
<BrandLogo :size="40" />
</div>
<div class="flex items-center gap-4 pr-1">
<UserDropdown v-if="shouldShowUser" />
</div>
</header>
</template>
<style scoped>
.header-box {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
border-bottom: 1px solid var(--color-border);
}
</style>

View File

@@ -2,12 +2,20 @@
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { UserOutlined, LogoutOutlined } from '@ant-design/icons-vue'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Icon } from '@iconify/vue'
const router = useRouter()
const userStore = useUserStore()
// 根据用户名生成稳定的渐变色 - 使用更协调的蓝金配色
// 根据用户名生成稳定的渐变色
const avatarGradient = computed(() => {
const name = userStore.displayName || 'User'
const gradients = [
@@ -18,7 +26,6 @@ const avatarGradient = computed(() => {
'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)'
]
// 根据用户名生成稳定的索引
let hash = 0
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash)
@@ -26,12 +33,8 @@ const avatarGradient = computed(() => {
return gradients[Math.abs(hash) % gradients.length]
})
const handleMenuClick = ({ key }) => {
if (key === 'profile') {
router.push('/user/profile')
} else if (key === 'logout') {
handleLogout()
}
const handleProfile = () => {
router.push('/user/profile')
}
async function handleLogout() {
@@ -45,178 +48,68 @@ async function handleLogout() {
</script>
<template>
<a-dropdown placement="bottomRight" :trigger="['hover']">
<div class="user-trigger">
<div class="user-avatar-ring">
<div class="user-avatar-wrapper">
<img
v-if="userStore.displayAvatar"
class="user-avatar"
:src="userStore.displayAvatar"
alt="avatar"
/>
<div
v-else
class="user-avatar-placeholder"
<DropdownMenu>
<DropdownMenuTrigger
class="group flex items-center gap-3 rounded-full px-3 py-1.5 pl-1.5
bg-white/5 border border-white/10
cursor-pointer outline-none
transition-all duration-250 ease-out
hover:bg-white/10 hover:border-white/20 hover:-translate-y-0.5
data-[state=open]:bg-white/10 data-[state=open]:border-white/20"
>
<!-- 头像容器 -->
<div class="relative w-9 h-9">
<!-- 渐变环 - hover 时显示 -->
<div
class="absolute -inset-0.5 rounded-full opacity-0 group-hover:opacity-100
transition-all duration-400 -z-10"
style="background: conic-gradient(from 0deg, rgba(59, 130, 246, 0.8), rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.8), rgba(59, 130, 246, 0.8))"
/>
<!-- 头像背景遮罩 -->
<div class="absolute inset-0 rounded-full bg-gray-900 -z-5" />
<!-- 头像 -->
<Avatar class="w-9 h-9 relative z-10">
<AvatarImage v-if="userStore.displayAvatar" :src="userStore.displayAvatar" alt="avatar" />
<AvatarFallback
class="flex items-center justify-center text-white font-bold text-[15px]"
:style="{ background: avatarGradient }"
>
<span class="avatar-text">{{ userStore.displayName?.charAt(0)?.toUpperCase() || 'U' }}</span>
</div>
</div>
<div class="status-dot"></div>
</div>
<div class="user-info">
<span class="user-name">{{ userStore.displayName || '用户' }}</span>
</div>
<svg class="dropdown-arrow" viewBox="0 0 12 12" fill="none">
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
{{ userStore.displayName?.charAt(0)?.toUpperCase() || 'U' }}
</AvatarFallback>
</Avatar>
<template #overlay>
<a-menu class="user-menu" @click="handleMenuClick">
<a-menu-item key="profile">
<template #icon>
<UserOutlined />
</template>
个人中心
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" danger>
<template #icon>
<LogoutOutlined />
</template>
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<!-- 在线状态点 -->
<div
class="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-green-500
border-2 border-gray-900 z-20
shadow-[0_0_0_2px_rgba(34,197,94,0.3)]"
/>
</div>
<!-- 用户名 -->
<span class="text-sm font-medium text-white truncate max-w-[100px]">
{{ userStore.displayName || '用户' }}
</span>
<!-- 下拉箭头 -->
<Icon
icon="lucide:chevron-down"
class="w-4 h-4 text-white/50 shrink-0 transition-transform duration-250
group-hover:rotate-180 group-data-[state=open]:rotate-180"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="min-w-[160px]">
<DropdownMenuItem @select="handleProfile" class="gap-2">
<Icon icon="lucide:user" class="w-4 h-4 opacity-70" />
个人中心
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" @select="handleLogout" class="gap-2">
<Icon icon="lucide:log-out" class="w-4 h-4 opacity-70" />
退出登录
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
<style scoped lang="less">
.user-trigger {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 12px 6px 6px;
border-radius: 40px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
.user-avatar-ring::before {
opacity: 1;
transform: rotate(180deg);
}
.dropdown-arrow {
transform: rotate(180deg);
}
}
}
.user-avatar-ring {
position: relative;
width: 36px;
height: 36px;
&::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 50%;
background: conic-gradient(
from 0deg,
rgba(59, 130, 246, 0.8),
rgba(99, 102, 241, 0.8),
rgba(139, 92, 246, 0.8),
rgba(59, 130, 246, 0.8)
);
opacity: 0;
transition: all 0.4s ease;
z-index: 0;
}
&::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--color-gray-900);
z-index: 1;
}
}
.user-avatar-wrapper {
position: relative;
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
z-index: 2;
}
.user-avatar,
.user-avatar-placeholder {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
}
.avatar-text {
color: #fff;
font-weight: 700;
font-size: 15px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.status-dot {
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
background: #22c55e;
border: 2px solid var(--color-gray-900);
border-radius: 50%;
z-index: 3;
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
}
.user-info {
display: flex;
align-items: center;
min-width: 0;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100px;
}
.dropdown-arrow {
width: 12px;
height: 12px;
color: rgba(255, 255, 255, 0.5);
transition: transform 0.25s ease;
flex-shrink: 0;
}
</style>

View File

@@ -11,6 +11,8 @@
"license": "ISC",
"description": "",
"dependencies": {
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.12.0",
"@types/node": "^25.0.6",
"aplayer": "^1.10.1",
"axios": "^1.12.2",