feat: 更新官网页面与交互效果

This commit is contained in:
leisileken
2026-06-18 21:51:25 +08:00
parent 99deb970a0
commit b71fd0336a
19 changed files with 2032 additions and 356 deletions

View File

@@ -0,0 +1,372 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
interface Particle {
element: HTMLElement
angle: number
radius: number
speed: number
size: number
opacity: number
baseOpacity: number
color: string
centerX: number
centerY: number
twinkleSpeed: number
twinkleOffset: number
}
const particlesContainer = ref<HTMLDivElement | null>(null)
const particles: Particle[] = []
const mousePos = { x: 0, y: 0 }
let animationFrameId: number | null = null
let time = 0
const nebulaColors = [
'#6366f1',
'#8b5cf6',
'#a855f7',
'#d946ef',
'#ec4899',
'#f43f5e',
'#3b82f6',
'#60a5fa',
'#22d3ee',
'#14b8a6',
]
const createNebulaParticle = (centerX: number, centerY: number, maxRadius: number) => {
const particle = document.createElement('div')
particle.className = 'particle nebula'
particle.style.position = 'absolute'
particle.style.borderRadius = '50%'
particle.style.pointerEvents = 'none'
particle.style.transition = 'opacity 0.3s ease'
const angle = Math.random() * Math.PI * 2
const radius = Math.random() * maxRadius
const size = Math.random() * 3 + 1
const color = nebulaColors[Math.floor(Math.random() * nebulaColors.length)]
const baseOpacity = Math.random() * 0.4 + 0.2
particle.style.width = `${size}px`
particle.style.height = `${size}px`
particle.style.backgroundColor = color
particle.style.left = `${centerX + Math.cos(angle) * radius}px`
particle.style.top = `${centerY + Math.sin(angle) * radius}px`
particle.style.opacity = `${baseOpacity}`
if (particlesContainer.value) {
particlesContainer.value.appendChild(particle)
}
particles.push({
element: particle,
angle,
radius,
speed: (Math.random() * 0.001 + 0.0005) * (Math.random() > 0.5 ? 1 : -1),
size,
opacity: baseOpacity,
baseOpacity,
color,
centerX,
centerY,
twinkleSpeed: Math.random() * 0.005 + 0.002,
twinkleOffset: Math.random() * Math.PI * 2,
})
}
const createNebulaCenter = () => {
const centerX = window.innerWidth / 2
const centerY = window.innerHeight / 2
for (let i = 0; i < 150; i++) {
createNebulaParticle(centerX, centerY, Math.min(window.innerWidth, window.innerHeight) * 0.4)
}
for (let i = 0; i < 100; i++) {
createNebulaParticle(centerX, centerY, Math.min(window.innerWidth, window.innerHeight) * 0.6)
}
for (let i = 0; i < 50; i++) {
createNebulaParticle(centerX, centerY, Math.min(window.innerWidth, window.innerHeight) * 0.8)
}
}
const updateParticles = () => {
particles.forEach((particle) => {
const x = particle.centerX + Math.cos(particle.angle) * particle.radius
const y = particle.centerY + Math.sin(particle.angle) * particle.radius
particle.element.style.left = `${x}px`
particle.element.style.top = `${y}px`
particle.element.style.opacity = `${particle.baseOpacity}`
const glowSize = particle.size * 6
particle.element.style.boxShadow = `0 0 ${glowSize}px ${particle.color}40`
})
animationFrameId = requestAnimationFrame(updateParticles)
}
const handleMouseMove = (e: MouseEvent) => {
mousePos.x = e.clientX
mousePos.y = e.clientY
}
const handleResize = () => {
particles.forEach(p => {
p.element.remove()
})
particles.length = 0
createNebulaCenter()
}
onMounted(() => {
createNebulaCenter()
updateParticles()
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('resize', handleResize)
particles.forEach(p => {
p.element.remove()
})
})
</script>
<template>
<div class="cosmic-background">
<div ref="particlesContainer" class="particles-container"></div>
<div class="bg-gradient-radial-from-center"></div>
<div class="bg-gradient-radial-abyss"></div>
<div class="galaxy-core"></div>
<div class="galaxy-arms"></div>
<div class="cosmic-dust"></div>
</div>
</template>
<style scoped>
.cosmic-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background: #000;
overflow: hidden;
}
.cosmic-background::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('/images/星云.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.6;
z-index: 0;
}
.particles-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.bg-gradient-radial-from-center {
position: absolute;
inset: 0;
background: radial-gradient(
ellipse at center,
rgba(30, 30, 60, 0.4) 0%,
rgba(20, 20, 40, 0.3) 30%,
rgba(10, 10, 20, 0.2) 60%,
rgba(0, 0, 0, 0) 100%
);
z-index: 2;
}
.bg-gradient-radial-abyss {
position: absolute;
inset: 0;
background: radial-gradient(
ellipse 80% 50% at 50% 100%,
rgba(0, 0, 0, 0.85) 0%,
rgba(5, 5, 15, 0.7) 40%,
rgba(10, 10, 30, 0.5) 70%,
rgba(0, 0, 0, 0.8) 100%
);
z-index: 3;
}
.galaxy-core {
display: none;
}
.galaxy-arms {
position: absolute;
top: 50%;
left: 50%;
width: 800px;
height: 800px;
transform: translate(-50%, -50%);
animation: rotate-arms 120s linear infinite;
z-index: 4;
}
.galaxy-arms::before,
.galaxy-arms::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
transform-origin: center;
}
.galaxy-arms::before {
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(100, 100, 200, 0.02) 10deg,
rgba(150, 150, 255, 0.04) 20deg,
rgba(100, 100, 200, 0.02) 30deg,
transparent 40deg,
transparent 90deg,
rgba(100, 100, 200, 0.02) 100deg,
rgba(150, 150, 255, 0.04) 110deg,
rgba(100, 100, 200, 0.02) 120deg,
transparent 130deg,
transparent 180deg,
rgba(100, 100, 200, 0.02) 190deg,
rgba(150, 150, 255, 0.04) 200deg,
rgba(100, 100, 200, 0.02) 210deg,
transparent 220deg,
transparent 270deg,
rgba(100, 100, 200, 0.02) 280deg,
rgba(150, 150, 255, 0.04) 290deg,
rgba(100, 100, 200, 0.02) 300deg,
transparent 310deg,
transparent 360deg
);
border-radius: 50%;
}
.galaxy-arms::after {
background: conic-gradient(
from 45deg,
transparent 0deg,
rgba(180, 180, 255, 0.015) 15deg,
rgba(200, 200, 255, 0.03) 30deg,
rgba(180, 180, 255, 0.015) 45deg,
transparent 60deg,
transparent 150deg,
rgba(180, 180, 255, 0.015) 165deg,
rgba(200, 200, 255, 0.03) 180deg,
rgba(180, 180, 255, 0.015) 195deg,
transparent 210deg,
transparent 300deg,
rgba(180, 180, 255, 0.015) 315deg,
rgba(200, 200, 255, 0.03) 330deg,
rgba(180, 180, 255, 0.015) 345deg,
transparent 360deg
);
border-radius: 50%;
}
.cosmic-dust {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(1px 1px at 20px 30px, rgba(255,255,255,0.3), transparent),
radial-gradient(1px 1px at 40px 70px, rgba(255,255,255,0.2), transparent),
radial-gradient(2px 2px at 90px 40px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 160px 120px, rgba(255,255,255,0.2), transparent),
radial-gradient(2px 2px at 230px 80px, rgba(255,255,255,0.3), transparent),
radial-gradient(1px 1px at 300px 150px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 370px 60px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 450px 200px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 520px 100px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 600px 180px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 680px 90px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 750px 250px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 830px 130px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 900px 300px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 980px 160px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 20px 280px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 100px 350px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 180px 420px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 260px 300px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 340px 480px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 420px 360px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 500px 440px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 580px 320px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 660px 500px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 740px 380px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 820px 560px, rgba(255,255,255,0.4), transparent),
radial-gradient(1px 1px at 900px 450px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 980px 520px, rgba(255,255,255,0.3), transparent);
background-size: 1000px 600px;
animation: float-dust 60s linear infinite;
z-index: 5;
}
@keyframes rotate-core {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes rotate-arms {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes pulse-core {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.5;
}
50% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0.2;
}
}
@keyframes float-dust {
from {
background-position: 0 0;
}
to {
background-position: 1000px 600px;
}
}
</style>

View File

@@ -0,0 +1,227 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
interface TrailPoint {
id: number
x: number
y: number
opacity: number
size: number
life: number
}
const isVisible = ref(false)
const trailPoints = ref<TrailPoint[]>([])
const mouseX = ref(0)
const mouseY = ref(0)
let animationFrameId: number | null = null
let pointId = 0
let lastX = 0
let lastY = 0
let lastTime = 0
let moveSpeed = 0
let lastTrailTime = 0
const handleMouseMove = (e: MouseEvent) => {
const currentTime = Date.now()
const deltaTime = currentTime - lastTime
const dx = e.clientX - lastX
const dy = e.clientY - lastY
mouseX.value = e.clientX
mouseY.value = e.clientY
if (deltaTime > 0) {
moveSpeed = Math.sqrt(dx * dx + dy * dy) / deltaTime
}
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance > 5 && currentTime - lastTrailTime > 20) {
const trailLength = Math.min(Math.floor(moveSpeed * 1.5), 5)
for (let i = 0; i < trailLength; i++) {
const ratio = i / trailLength
const wave = Math.sin(ratio * Math.PI)
const sizeVariation = Math.sin(ratio * Math.PI * 3) * 2
trailPoints.value.push({
id: pointId++,
x: e.clientX - dx * ratio,
y: e.clientY - dy * ratio,
opacity: wave * (1 - ratio * 0.4),
size: 6 + wave * 3 + sizeVariation,
life: 1
})
}
lastTrailTime = currentTime
}
lastX = e.clientX
lastY = e.clientY
lastTime = currentTime
if (!isVisible.value) {
isVisible.value = true
}
}
const animate = () => {
const decaySpeed = moveSpeed > 0.5 ? 0.015 : 0.03
trailPoints.value = trailPoints.value
.map(p => ({
...p,
life: p.life - decaySpeed,
opacity: p.opacity * (1 - decaySpeed * 0.5),
size: p.size * (1 - decaySpeed * 0.3)
}))
.filter(p => p.life > 0 && p.opacity > 0)
if (moveSpeed < 0.1) {
moveSpeed *= 0.95
}
animationFrameId = requestAnimationFrame(animate)
}
const handleMouseLeave = () => {
isVisible.value = false
trailPoints.value = []
}
onMounted(() => {
if (window.matchMedia('(pointer: fine)').matches) {
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseleave', handleMouseLeave)
animate()
}
})
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseleave', handleMouseLeave)
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
})
</script>
<template>
<div
v-show="isVisible"
class="comet-trail-container"
>
<div
v-for="point in trailPoints"
:key="point.id"
class="comet-trail-point"
:style="{
left: `${point.x}px`,
top: `${point.y}px`,
opacity: point.opacity,
width: `${point.size}px`,
height: `${point.size}px`
}"
></div>
<div
class="comet-head"
:style="{
left: `${mouseX}px`,
top: `${mouseY}px`
}"
></div>
<div
class="comet-glow"
:style="{
left: `${mouseX}px`,
top: `${mouseY}px`
}"
></div>
</div>
</template>
<style scoped>
.comet-trail-container {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.comet-trail-point {
position: absolute;
border-radius: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(
circle,
rgba(147, 197, 253, 1) 0%,
rgba(139, 92, 246, 0.8) 40%,
rgba(99, 102, 241, 0.5) 70%,
transparent 100%
);
box-shadow:
0 0 6px rgba(147, 197, 253, 0.9),
0 0 12px rgba(139, 92, 246, 0.6),
0 0 18px rgba(99, 102, 241, 0.4);
filter: blur(1px);
}
.comet-head {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(
circle,
rgba(255, 255, 255, 1) 0%,
rgba(200, 220, 255, 1) 30%,
rgba(147, 197, 253, 0.9) 60%,
transparent 100%
);
box-shadow:
0 0 15px rgba(255, 255, 255, 1),
0 0 30px rgba(147, 197, 253, 0.9),
0 0 45px rgba(59, 130, 246, 0.6),
0 0 60px rgba(139, 92, 246, 0.4);
}
.comet-glow {
position: absolute;
width: 40px;
height: 40px;
border-radius: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(
circle,
rgba(147, 197, 253, 0.4) 0%,
rgba(59, 130, 246, 0.2) 40%,
transparent 70%
);
filter: blur(10px);
animation: glow-pulse 1s ease-in-out infinite;
}
@keyframes glow-pulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.8;
}
50% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 1;
}
}
@media (pointer: coarse) {
.comet-trail-container {
display: none !important;
}
}
</style>

View File

@@ -7,7 +7,7 @@ const isMenuOpen = ref(false)
const isScrolled = ref(false)
const navItems = [
{ name: '首页', path: '/' },
{ name: '首页', path: '/home' },
{ name: '产品中心', path: '/products' },
{ name: '解决方案', path: '/solutions' },
{ name: '关于我们', path: '/about' },
@@ -42,7 +42,7 @@ onUnmounted(() => {
>
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16 lg:h-20">
<div class="flex items-center space-x-3 cursor-pointer" @click="router.push('/')">
<div class="flex items-center space-x-3 cursor-pointer" @click="router.push('/home')">
<img src="/images/logo.jpg" alt="云阶智元" class="w-10 h-10 rounded-lg object-cover" />
<div>
<span