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 UserDropdown from '@/components/UserDropdown.vue'
import BrandLogo from '@/components/BrandLogo.vue' import BrandLogo from '@/components/BrandLogo.vue'
const styles = {
background: 'var(--color-gray-900)',
color: 'var(--color-text-inverse)'
}
const userStore = useUserStore() const userStore = useUserStore()
// 计算是否应该显示用户组件 // 计算是否应该显示用户组件
@@ -19,29 +14,17 @@ const shouldShowUser = computed(() => {
</script> </script>
<template> <template>
<header class="header-box" :style="styles"> <header
<div> class="fixed top-0 left-0 right-0 z-[100]
<div class="h-[70px] flex items-center"> h-[70px] flex items-center px-[30px]
<div class="flex items-center gap-3 flex-1 pl-[30px]"> bg-gray-900 text-white
<BrandLogo :size="40" /> border-b border-border"
</div> >
<div class="flex items-center gap-4 pr-[35px]"> <div class="flex items-center gap-3 flex-1">
<template v-if="shouldShowUser"> <BrandLogo :size="40" />
<UserDropdown /> </div>
</template> <div class="flex items-center gap-4 pr-1">
</div> <UserDropdown v-if="shouldShowUser" />
</div>
</div> </div>
</header> </header>
</template> </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 { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user' 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 router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
// 根据用户名生成稳定的渐变色 - 使用更协调的蓝金配色 // 根据用户名生成稳定的渐变色
const avatarGradient = computed(() => { const avatarGradient = computed(() => {
const name = userStore.displayName || 'User' const name = userStore.displayName || 'User'
const gradients = [ const gradients = [
@@ -18,7 +26,6 @@ const avatarGradient = computed(() => {
'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)' 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)'
] ]
// 根据用户名生成稳定的索引
let hash = 0 let hash = 0
for (let i = 0; i < name.length; i++) { for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash) hash = name.charCodeAt(i) + ((hash << 5) - hash)
@@ -26,12 +33,8 @@ const avatarGradient = computed(() => {
return gradients[Math.abs(hash) % gradients.length] return gradients[Math.abs(hash) % gradients.length]
}) })
const handleMenuClick = ({ key }) => { const handleProfile = () => {
if (key === 'profile') { router.push('/user/profile')
router.push('/user/profile')
} else if (key === 'logout') {
handleLogout()
}
} }
async function handleLogout() { async function handleLogout() {
@@ -45,178 +48,68 @@ async function handleLogout() {
</script> </script>
<template> <template>
<a-dropdown placement="bottomRight" :trigger="['hover']"> <DropdownMenu>
<div class="user-trigger"> <DropdownMenuTrigger
<div class="user-avatar-ring"> class="group flex items-center gap-3 rounded-full px-3 py-1.5 pl-1.5
<div class="user-avatar-wrapper"> bg-white/5 border border-white/10
<img cursor-pointer outline-none
v-if="userStore.displayAvatar" transition-all duration-250 ease-out
class="user-avatar" hover:bg-white/10 hover:border-white/20 hover:-translate-y-0.5
:src="userStore.displayAvatar" data-[state=open]:bg-white/10 data-[state=open]:border-white/20"
alt="avatar" >
/> <!-- 头像容器 -->
<div <div class="relative w-9 h-9">
v-else <!-- 渐变环 - hover 时显示 -->
class="user-avatar-placeholder" <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 }" :style="{ background: avatarGradient }"
> >
<span class="avatar-text">{{ userStore.displayName?.charAt(0)?.toUpperCase() || 'U' }}</span> {{ userStore.displayName?.charAt(0)?.toUpperCase() || 'U' }}
</div> </AvatarFallback>
</div> </Avatar>
<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>
<template #overlay> <!-- 在线状态点 -->
<a-menu class="user-menu" @click="handleMenuClick"> <div
<a-menu-item key="profile"> class="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-green-500
<template #icon> border-2 border-gray-900 z-20
<UserOutlined /> shadow-[0_0_0_2px_rgba(34,197,94,0.3)]"
</template> />
个人中心 </div>
</a-menu-item>
<a-menu-divider /> <!-- 用户名 -->
<a-menu-item key="logout" danger> <span class="text-sm font-medium text-white truncate max-w-[100px]">
<template #icon> {{ userStore.displayName || '用户' }}
<LogoutOutlined /> </span>
</template>
退出登录 <!-- 下拉箭头 -->
</a-menu-item> <Icon
</a-menu> icon="lucide:chevron-down"
</template> class="w-4 h-4 text-white/50 shrink-0 transition-transform duration-250
</a-dropdown> 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> </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", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.12.0",
"@types/node": "^25.0.6", "@types/node": "^25.0.6",
"aplayer": "^1.10.1", "aplayer": "^1.10.1",
"axios": "^1.12.2", "axios": "^1.12.2",