feat: 优化

This commit is contained in:
2026-03-04 02:13:16 +08:00
parent aa06782953
commit 7f5d9d9f19
17 changed files with 1958 additions and 1727 deletions

View File

@@ -0,0 +1,246 @@
<script setup>
/**
* 牟野品牌 Logo
* 设计理念Neo-Organic Tech新有机科技
* 融合自然有机形态与科技几何,体现"野"的生机与"道"的智慧
*/
defineProps({
size: {
type: Number,
default: 36
}
})
</script>
<template>
<div class="brand-logo" :style="{ '--logo-size': `${size}px` }">
<svg
class="logo-icon"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<!-- 主色渐变金色流光 -->
<linearGradient id="brandGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#F59E0B" />
<stop offset="50%" stop-color="#FBBF24" />
<stop offset="100%" stop-color="#F59E0B" />
</linearGradient>
<!-- 辅助渐变深金 -->
<linearGradient id="brandGradientDark" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#D97706" />
<stop offset="100%" stop-color="#F59E0B" />
</linearGradient>
<!-- 光晕效果 -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1.5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- 内阴影 -->
<filter id="innerShadow" x="-50%" y="-50%" width="200%" height="200%">
<feOffset dx="0" dy="1"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite operator="out" in="SourceGraphic"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="SourceGraphic"/>
</filter>
</defs>
<!-- 背景圆 - 深色基底 -->
<circle cx="24" cy="24" r="22" fill="url(#brandGradientDark)" opacity="0.15"/>
<!-- 核心图形抽象的"野" + 山野轮廓 -->
<g class="logo-core" filter="url(#glow)">
<!-- 左侧山峰 - 代表"野"的山野意象 -->
<path
class="peak peak-left"
d="M12 32 L18 18 L24 28"
stroke="url(#brandGradient)"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
<!-- 右侧山峰 - 呼应平衡 -->
<path
class="peak peak-right"
d="M24 28 L30 16 L36 32"
stroke="url(#brandGradient)"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
<!-- 中间连接 - 代表"道"的路径 -->
<path
class="path-line"
d="M18 24 Q24 20 30 24"
stroke="url(#brandGradient)"
stroke-width="2"
stroke-linecap="round"
fill="none"
/>
<!-- 底部流动线 - 数据流/AI智能 -->
<path
class="flow-line"
d="M14 36 Q24 32 34 36"
stroke="url(#brandGradient)"
stroke-width="1.5"
stroke-linecap="round"
fill="none"
opacity="0.7"
/>
<!-- 数据节点 -->
<circle class="node node-1" cx="18" cy="18" r="2" fill="url(#brandGradient)"/>
<circle class="node node-2" cx="30" cy="16" r="2" fill="url(#brandGradient)"/>
<circle class="node node-3" cx="24" cy="28" r="2.5" fill="url(#brandGradient)" filter="url(#innerShadow)"/>
</g>
<!-- 能量环 - 科技感装饰 -->
<circle
class="energy-ring"
cx="24"
cy="24"
r="20"
stroke="url(#brandGradient)"
stroke-width="0.5"
fill="none"
opacity="0.4"
stroke-dasharray="4 6"
/>
</svg>
</div>
</template>
<style scoped>
.brand-logo {
display: inline-flex;
align-items: center;
cursor: pointer;
transition: transform 200ms ease;
}
.brand-logo:hover {
transform: scale(1.05);
}
.logo-icon {
width: var(--logo-size);
height: var(--logo-size);
flex-shrink: 0;
}
/* 能量环旋转动画 */
@keyframes ringRotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.energy-ring {
transform-origin: center;
animation: ringRotate 20s linear infinite;
}
/* 节点脉冲动画 */
@keyframes nodePulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.2);
}
}
.node {
transform-origin: center;
transition: all 300ms ease;
}
.brand-logo:hover .node-1 {
animation: nodePulse 1.5s ease-in-out infinite;
}
.brand-logo:hover .node-2 {
animation: nodePulse 1.5s ease-in-out infinite 0.3s;
}
.brand-logo:hover .node-3 {
animation: nodePulse 1.5s ease-in-out infinite 0.6s;
}
/* 山峰绘制动画 */
@keyframes drawPeak {
from { stroke-dashoffset: 30; }
to { stroke-dashoffset: 0; }
}
.peak {
stroke-dasharray: 30;
stroke-dashoffset: 0;
transition: all 300ms ease;
}
.brand-logo:hover .peak-left {
animation: drawPeak 0.6s ease-out;
}
.brand-logo:hover .peak-right {
animation: drawPeak 0.6s ease-out 0.15s;
}
/* 路径流动动画 */
@keyframes pathFlow {
0% { stroke-dashoffset: 20; }
100% { stroke-dashoffset: 0; }
}
.path-line {
stroke-dasharray: 5 3;
stroke-dashoffset: 0;
}
.brand-logo:hover .path-line {
animation: pathFlow 1s linear infinite;
}
/* 底部流线波动 */
@keyframes flowWave {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-1px); }
}
.flow-line {
transition: all 300ms ease;
}
.brand-logo:hover .flow-line {
animation: flowWave 1s ease-in-out infinite;
}
/* Logo 整体光晕 */
@keyframes logoGlow {
0%, 100% {
filter: drop-shadow(0 0 3px rgba(245, 158, 11, 0.4));
}
50% {
filter: drop-shadow(0 0 8px rgba(245, 158, 11, 0.7));
}
}
.brand-logo:hover .logo-core {
animation: logoGlow 2s ease-in-out infinite;
}
</style>

View File

@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
import UserDropdown from '@/components/UserDropdown.vue'
import BrandLogo from '@/components/BrandLogo.vue'
const styles = {
background: 'var(--color-gray-900)',
@@ -22,7 +23,7 @@ const shouldShowUser = computed(() => {
<div>
<div class="h-[70px] flex items-center">
<div class="flex items-center gap-3 flex-1 pl-[30px]">
<!-- 左侧可放 logo 或其他内容 -->
<BrandLogo :size="40" />
</div>
<div class="flex items-center gap-4 pr-[35px]">
<template v-if="shouldShowUser">

View File

@@ -1,4 +1,5 @@
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { UserOutlined, LogoutOutlined } from '@ant-design/icons-vue'
@@ -6,6 +7,25 @@ import { UserOutlined, LogoutOutlined } from '@ant-design/icons-vue'
const router = useRouter()
const userStore = useUserStore()
// 根据用户名生成稳定的渐变色 - 使用更协调的蓝金配色
const avatarGradient = computed(() => {
const name = userStore.displayName || 'User'
const gradients = [
'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
'linear-gradient(135deg, #6366f1 0%, #4f46e5 100%)',
'linear-gradient(135deg, #14b8a6 0%, #0d9488 100%)',
'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)
}
return gradients[Math.abs(hash) % gradients.length]
})
const handleMenuClick = ({ key }) => {
if (key === 'profile') {
router.push('/user/profile')
@@ -26,20 +46,35 @@ async function handleLogout() {
<template>
<a-dropdown placement="bottomRight" :trigger="['hover']">
<div class="user-avatar-container">
<img
v-if="userStore.displayAvatar"
class="user-avatar"
:src="userStore.displayAvatar"
alt="avatar"
/>
<div v-else class="user-avatar-placeholder">
{{ userStore.displayName?.charAt(0) || 'U' }}
<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"
: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>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu class="user-menu" @click="handleMenuClick">
<a-menu-item key="profile">
<template #icon>
<UserOutlined />
@@ -58,44 +93,130 @@ async function handleLogout() {
</a-dropdown>
</template>
<style scoped>
.user-avatar-container {
cursor: pointer;
<style scoped lang="less">
.user-trigger {
display: flex;
align-items: center;
will-change: transform;
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-container:hover {
transform: scale(1.05);
.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: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--color-border, #e5e7eb);
transition: border-color 0.15s, box-shadow 0.15s;
}
.user-avatar-container:hover .user-avatar,
.user-avatar-container:hover .user-avatar-placeholder {
border-color: var(--color-primary, #1890ff);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.user-avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-avatar-placeholder {
background: linear-gradient(135deg, var(--color-primary, #1890ff), var(--color-blue, #36cfc9));
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
}
.avatar-text {
color: #fff;
font-weight: 600;
font-size: 16px;
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>