feat(components): 添加鼠标交互粒子效果和高级浮动效果组件

refactor(sections): 移除各区块的多余标签元素
style(contact-section): 优化联系表单布局和动画效果
This commit is contained in:
张翔
2026-02-23 22:48:32 +08:00
parent a75673fa27
commit 44ba75e4d1
7 changed files with 650 additions and 18 deletions
@@ -0,0 +1,450 @@
'use client';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useMemo, useState, useEffect, useRef } from 'react';
import { Cpu, Shield, Zap, Globe, FileText, TrendingUp, BarChart3, Users } from 'lucide-react';
interface FloatingOrbProps {
size?: number;
color?: string;
delay?: number;
x?: number;
y?: number;
duration?: number;
icon?: any;
className?: string;
}
function FloatingOrb({
size = 80,
color = 'rgba(196, 30, 58, 0.08)',
delay = 0,
x = 0,
y = 0,
duration = 8,
icon: Icon,
className = ''
}: FloatingOrbProps) {
return (
<motion.div
className={`absolute rounded-full pointer-events-none ${className}`}
style={{
width: size,
height: size,
backgroundColor: color,
backdropFilter: 'blur(20px)',
boxShadow: '0 0 40px rgba(196, 30, 58, 0.1)',
}}
initial={{ opacity: 0, scale: 0, x, y }}
animate={{
opacity: [0, 1, 1],
scale: [0.5, 1, 1],
y: [y, y - 30, y],
x: [x, x + 15, x],
}}
transition={{
duration: duration,
delay,
repeat: Infinity,
ease: 'easeInOut',
times: [0, 0.5, 1],
}}
>
{Icon && (
<div className="absolute inset-0 flex items-center justify-center">
<Icon className="w-5 h-5 text-[#C41E3A]/30" />
</div>
)}
</motion.div>
);
}
interface FloatingLineProps {
startX?: number;
startY?: number;
endX?: number;
endY?: number;
color?: string;
delay?: number;
duration?: number;
className?: string;
}
function FloatingLine({
startX = 0,
startY = 0,
endX = 200,
endY = 0,
color = 'rgba(28, 28, 28, 0.1)',
delay = 0,
duration = 6,
className = ''
}: FloatingLineProps) {
return (
<motion.svg
className={`absolute pointer-events-none ${className}`}
style={{
left: startX,
top: startY,
width: Math.abs(endX - startX) || 100,
height: Math.abs(endY - startY) || 2,
overflow: 'visible',
}}
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 0.5, 1] }}
transition={{
duration,
delay,
repeat: Infinity,
ease: 'easeInOut',
}}
>
<motion.path
d={`M0 0 Q${(endX - startX) / 2} ${-20 + Math.random() * 40} ${endX - startX} 0`}
fill="none"
stroke={color}
strokeWidth="1"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: [0, 1, 0] }}
transition={{
duration: duration * 2,
delay,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</motion.svg>
);
}
interface FloatingIconProps {
icon: any;
size?: number;
color?: string;
delay?: number;
x?: number;
y?: number;
rotation?: number;
className?: string;
}
function FloatingIcon({
icon: Icon,
size = 24,
color = '#1C1C1C',
delay = 0,
x = 0,
y = 0,
rotation = 0,
className = ''
}: FloatingIconProps) {
return (
<motion.div
className={`absolute pointer-events-none ${className}`}
style={{
left: x,
top: y,
}}
initial={{ opacity: 0, scale: 0, rotate: rotation - 15, x, y }}
animate={{
opacity: [0, 1, 0.8],
scale: [0.8, 1, 0.9],
rotate: [rotation - 10, rotation + 10, rotation],
y: [y, y - 25, y - 10],
}}
transition={{
duration: 7 + Math.random() * 3,
delay,
repeat: Infinity,
ease: 'easeInOut',
times: [0, 0.5, 1],
}}
>
<div
className="flex items-center justify-center rounded-full"
style={{
width: size + 24,
height: size + 24,
backgroundColor: 'rgba(255, 255, 255, 0.5)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(28, 28, 28, 0.08)',
boxShadow: '0 4px 20px rgba(28, 28, 28, 0.05)',
}}
>
<Icon className="w-5 h-5" style={{ color }} />
</div>
</motion.div>
);
}
interface ParticleRingProps {
size?: number;
color?: string;
delay?: number;
x?: number;
y?: number;
className?: string;
}
function ParticleRing({
size = 120,
color = 'rgba(196, 30, 58, 0.1)',
delay = 0,
x = 0,
y = 0,
className = ''
}: ParticleRingProps) {
return (
<motion.div
className={`absolute pointer-events-none ${className}`}
style={{
left: x,
top: y,
width: size,
height: size,
}}
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0, 1, 0.5],
scale: [0.5, 1.2, 0.8],
rotate: [0, 90, 180],
}}
transition={{
duration: 12,
delay,
repeat: Infinity,
ease: 'linear',
}}
>
<svg width={size} height={size} viewBox="0 0 120 120">
{[0, 60, 120, 180, 240, 300].map((angle, i) => {
const rad = (angle * Math.PI) / 180;
const px = 60 + Math.cos(rad) * 45;
const py = 60 + Math.sin(rad) * 45;
return (
<motion.circle
key={i}
cx={px}
cy={py}
r={3}
fill={color}
initial={{ opacity: 0 }}
animate={{
opacity: [0.3, 1, 0.3],
scale: [0.5, 1.5, 0.5],
}}
transition={{
duration: 4,
delay: delay + i * 0.3,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
);
})}
<circle
cx={60}
cy={60}
r={50}
fill="none"
stroke={color}
strokeWidth="1"
strokeDasharray="5 5"
/>
</svg>
</motion.div>
);
}
interface GlowingDotProps {
size?: number;
color?: string;
delay?: number;
x?: number;
y?: number;
className?: string;
}
function GlowingDot({
size = 8,
color = '#C41E3A',
delay = 0,
x = 0,
y = 0,
className = ''
}: GlowingDotProps) {
return (
<motion.div
className={`absolute rounded-full pointer-events-none ${className}`}
style={{
left: x,
top: y,
width: size,
height: size,
backgroundColor: color,
boxShadow: `0 0 ${size * 2}px ${color}`,
}}
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0, 1, 0.5, 1],
scale: [0.5, 1.5, 0.8, 1.2],
}}
transition={{
duration: 3 + Math.random() * 2,
delay,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
);
}
interface AdvancedFloatingEffectsProps {
variant?: 'minimal' | 'balanced' | 'rich' | 'parallax';
className?: string;
}
export function AdvancedFloatingEffects({
variant = 'balanced',
className = ''
}: AdvancedFloatingEffectsProps) {
const [isMounted, setIsMounted] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const { scrollY } = useScroll();
useEffect(() => {
setIsMounted(true);
}, []);
const config = {
minimal: { orbs: 2, icons: 3, rings: 0, lines: 2, dots: 5 },
balanced: { orbs: 3, icons: 5, rings: 1, lines: 4, dots: 8 },
rich: { orbs: 5, icons: 8, rings: 2, lines: 6, dots: 12 },
parallax: { orbs: 4, icons: 6, rings: 2, lines: 5, dots: 10 },
};
const { orbs, icons, rings, lines, dots } = config[variant];
const iconsList = [Cpu, Shield, Zap, Globe, FileText, TrendingUp, BarChart3, Users];
const elements = useMemo(() => {
if (!isMounted) return [];
const items = [];
const width = typeof window !== 'undefined' ? window.innerWidth : 1920;
const height = typeof window !== 'undefined' ? window.innerHeight : 1080;
for (let i = 0; i < orbs; i++) {
items.push({
type: 'orb',
id: `orb-${i}`,
props: {
size: 60 + Math.random() * 60,
color: i % 2 === 0 ? 'rgba(196, 30, 58, 0.08)' : 'rgba(28, 28, 28, 0.05)',
delay: i * 0.5,
x: width * 0.1 + (i * width * 0.35),
y: height * 0.15 + Math.random() * height * 0.5,
duration: 7 + Math.random() * 4,
icon: i % 3 === 0 ? iconsList[i % iconsList.length] : undefined,
},
parallaxDepth: 0.1 + i * 0.1,
});
}
for (let i = 0; i < icons; i++) {
items.push({
type: 'icon',
id: `icon-${i}`,
props: {
icon: iconsList[i % iconsList.length],
size: 20,
color: i % 2 === 0 ? '#C41E3A' : '#1C1C1C',
delay: i * 0.4,
x: width * 0.08 + (i * width * 0.12),
y: height * 0.1 + Math.random() * height * 0.65,
rotation: -15 + Math.random() * 30,
},
parallaxDepth: 0.2 + i * 0.05,
});
}
for (let i = 0; i < rings; i++) {
items.push({
type: 'ring',
id: `ring-${i}`,
props: {
size: 100 + Math.random() * 80,
color: i % 2 === 0 ? 'rgba(196, 30, 58, 0.1)' : 'rgba(28, 28, 28, 0.08)',
delay: i * 0.8,
x: width * 0.2 + (i * width * 0.4),
y: height * 0.2 + Math.random() * height * 0.4,
},
parallaxDepth: 0.05 + i * 0.1,
});
}
for (let i = 0; i < lines; i++) {
items.push({
type: 'line',
id: `line-${i}`,
props: {
startX: width * 0.05 + (i * width * 0.15),
startY: height * 0.1 + Math.random() * height * 0.7,
endX: width * 0.05 + (i * width * 0.15) + 80 + Math.random() * 120,
endY: height * 0.1 + Math.random() * height * 0.7,
color: i % 2 === 0 ? 'rgba(196, 30, 58, 0.15)' : 'rgba(28, 28, 28, 0.1)',
delay: i * 0.6,
duration: 5 + Math.random() * 3,
},
parallaxDepth: 0.15 + i * 0.05,
});
}
for (let i = 0; i < dots; i++) {
items.push({
type: 'dot',
id: `dot-${i}`,
props: {
size: 4 + Math.random() * 6,
color: i % 3 === 0 ? '#C41E3A' : i % 3 === 1 ? '#1C1C1C' : '#D4A574',
delay: i * 0.3,
x: Math.random() * width,
y: Math.random() * height,
},
parallaxDepth: 0.25 + i * 0.02,
});
}
return items;
}, [orbs, icons, rings, lines, dots, isMounted, iconsList]);
const getParallaxStyle = (depth: number) => {
if (variant !== 'parallax') return {};
const y = useTransform(scrollY, [0, 500], [0, -depth * 100]);
return { y };
};
return (
<div
ref={containerRef}
className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}
>
{elements.map((el) => {
const parallaxStyle = getParallaxStyle(el.parallaxDepth);
return (
<motion.div key={el.id} style={parallaxStyle}>
{el.type === 'orb' && <FloatingOrb {...el.props} />}
{el.type === 'icon' && <FloatingIcon {...el.props} />}
{el.type === 'ring' && <ParticleRing {...el.props} />}
{el.type === 'line' && <FloatingLine {...el.props} />}
{el.type === 'dot' && <GlowingDot {...el.props} />}
</motion.div>
);
})}
</div>
);
}
export default AdvancedFloatingEffects;
@@ -0,0 +1,194 @@
'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
interface InteractiveParticle {
x: number;
y: number;
originX: number;
originY: number;
vx: number;
vy: number;
size: number;
opacity: number;
color: string;
life: number;
}
interface MouseInteractiveParticlesProps {
particleCount?: number;
className?: string;
colorScheme?: 'red' | 'dark' | 'mixed';
interactionRadius?: number;
}
export function MouseInteractiveParticles({
particleCount = 80,
className = '',
colorScheme = 'mixed',
interactionRadius = 150,
}: MouseInteractiveParticlesProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isMounted, setIsMounted] = useState(false);
const mouseRef = useRef({ x: -1000, y: -1000, active: false });
const particlesRef = useRef<InteractiveParticle[]>([]);
const animationRef = useRef<number | null>(null);
const getColors = useCallback(() => {
switch (colorScheme) {
case 'red':
return ['#C41E3A', '#E04A68', '#A01830'];
case 'dark':
return ['#1C1C1C', '#2D2D2D', '#3E3E3E'];
case 'mixed':
default:
return ['#C41E3A', '#1C1C1C', '#D4A574'];
}
}, [colorScheme]);
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (!isMounted) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let width = window.innerWidth;
let height = window.innerHeight;
const resize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
initParticles();
};
const initParticles = () => {
const colors = getColors();
particlesRef.current = [];
for (let i = 0; i < particleCount; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
particlesRef.current.push({
x,
y,
originX: x,
originY: y,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
size: Math.random() * 3 + 1,
opacity: Math.random() * 0.5 + 0.2,
color: colors[Math.floor(Math.random() * colors.length)],
life: Math.random() * Math.PI * 2,
});
}
};
const handleMouseMove = (e: MouseEvent) => {
mouseRef.current.x = e.clientX;
mouseRef.current.y = e.clientY;
mouseRef.current.active = true;
};
const handleMouseLeave = () => {
mouseRef.current.x = -1000;
mouseRef.current.y = -1000;
mouseRef.current.active = false;
};
const animate = () => {
ctx.clearRect(0, 0, width, height);
particlesRef.current.forEach((particle, i) => {
particle.life += 0.02;
if (mouseRef.current.active) {
const dx = mouseRef.current.x - particle.x;
const dy = mouseRef.current.y - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < interactionRadius) {
const force = (interactionRadius - distance) / interactionRadius;
const angle = Math.atan2(dy, dx);
particle.vx -= Math.cos(angle) * force * 0.5;
particle.vy -= Math.sin(angle) * force * 0.5;
}
}
const returnForce = 0.01;
particle.vx += (particle.originX - particle.x) * returnForce;
particle.vy += (particle.originY - particle.y) * returnForce;
particle.vx *= 0.98;
particle.vy *= 0.98;
particle.x += particle.vx;
particle.y += particle.vy;
particle.x += Math.sin(particle.life + i) * 0.1;
particle.y += Math.cos(particle.life * 0.8 + i) * 0.1;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = particle.color;
ctx.globalAlpha = particle.opacity;
ctx.fill();
particlesRef.current.forEach((otherParticle, j) => {
if (i === j) return;
const dx = particle.x - otherParticle.x;
const dy = particle.y - otherParticle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(otherParticle.x, otherParticle.y);
ctx.strokeStyle = particle.color;
ctx.globalAlpha = 0.05 * (1 - distance / 100);
ctx.stroke();
}
});
});
ctx.globalAlpha = 1;
animationRef.current = requestAnimationFrame(animate);
};
resize();
initParticles();
animate();
window.addEventListener('resize', resize);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseleave', handleMouseLeave);
return () => {
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isMounted, particleCount, getColors, interactionRadius]);
if (!isMounted) return null;
return (
<canvas
ref={canvasRef}
className={`absolute inset-0 pointer-events-none ${className}`}
/>
);
}
export default MouseInteractiveParticles;
@@ -53,9 +53,6 @@ export function AboutSection() {
className="max-w-4xl mx-auto"
>
<div className="text-center mb-16">
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium mb-4">
</span>
<h2 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[#C41E3A]">{COMPANY_INFO.shortName}</span>
</h2>
+6 -6
View File
@@ -130,7 +130,7 @@ export function ContactSection() {
<div className="grid lg:grid-cols-5 gap-12 lg:gap-16">
<div
className={`
lg:col-span-2 space-y-8
lg:col-span-2 space-y-8 flex flex-col
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up stagger-1' : ''}
`}
@@ -209,16 +209,16 @@ export function ContactSection() {
<div
className={`
lg:col-span-3
lg:col-span-3 flex flex-col
opacity-0 translate-y-4
${isVisible ? 'animate-fade-in-up stagger-2' : ''}
`}
>
<div className="bg-[#F5F7FA] p-6 sm:p-8 rounded-lg border border-[#E2E8F0]">
<div className="bg-[#F5F7FA] p-6 sm:p-8 rounded-lg border border-[#E2E8F0] flex-1 flex flex-col">
<h3 className="text-lg font-semibold text-[#1A1A2E] mb-6"></h3>
{isSubmitted ? (
<div className="text-center py-12">
<div className="text-center py-12 flex-1 flex items-center justify-center">
<div className="w-16 h-16 bg-[#C41E3A] rounded-full flex items-center justify-center mx-auto mb-4 animate-stamp-in">
<CheckCircle2 className="w-8 h-8 text-white" />
</div>
@@ -226,7 +226,7 @@ export function ContactSection() {
<p className="text-[#718096]"></p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-5">
<form onSubmit={handleSubmit} className="space-y-5 flex-1 flex flex-col">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="姓名"
@@ -275,7 +275,7 @@ export function ContactSection() {
<Button
type="submit"
size="lg"
className="w-full group"
className="w-full group mt-auto"
disabled={isSubmitting}
>
{isSubmitting ? (
-3
View File
@@ -20,9 +20,6 @@ export function NewsSection() {
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
<span className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium mb-4">
</span>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[#C41E3A]"></span>
</h2>
@@ -24,9 +24,6 @@ export function ProductsSection() {
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium mb-4">
</span>
<h2 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[#C41E3A]"></span>
</h2>
@@ -31,9 +31,6 @@ export function ServicesSection() {
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium mb-4">
</span>
<h2 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
<span className="text-[#C41E3A]"></span>
</h2>