feat(components): 添加鼠标交互粒子效果和高级浮动效果组件
refactor(sections): 移除各区块的多余标签元素 style(contact-section): 优化联系表单布局和动画效果
This commit is contained in:
@@ -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"
|
className="max-w-4xl mx-auto"
|
||||||
>
|
>
|
||||||
<div className="text-center mb-16">
|
<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">
|
<h2 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
|
||||||
关于 <span className="text-[#C41E3A]">{COMPANY_INFO.shortName}</span>
|
关于 <span className="text-[#C41E3A]">{COMPANY_INFO.shortName}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function ContactSection() {
|
|||||||
<div className="grid lg:grid-cols-5 gap-12 lg:gap-16">
|
<div className="grid lg:grid-cols-5 gap-12 lg:gap-16">
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
lg:col-span-2 space-y-8
|
lg:col-span-2 space-y-8 flex flex-col
|
||||||
opacity-0 translate-y-4
|
opacity-0 translate-y-4
|
||||||
${isVisible ? 'animate-fade-in-up stagger-1' : ''}
|
${isVisible ? 'animate-fade-in-up stagger-1' : ''}
|
||||||
`}
|
`}
|
||||||
@@ -209,16 +209,16 @@ export function ContactSection() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
lg:col-span-3
|
lg:col-span-3 flex flex-col
|
||||||
opacity-0 translate-y-4
|
opacity-0 translate-y-4
|
||||||
${isVisible ? 'animate-fade-in-up stagger-2' : ''}
|
${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>
|
<h3 className="text-lg font-semibold text-[#1A1A2E] mb-6">发送消息</h3>
|
||||||
|
|
||||||
{isSubmitted ? (
|
{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">
|
<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" />
|
<CheckCircle2 className="w-8 h-8 text-white" />
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +226,7 @@ export function ContactSection() {
|
|||||||
<p className="text-[#718096]">感谢您的留言,我们会尽快与您联系!</p>
|
<p className="text-[#718096]">感谢您的留言,我们会尽快与您联系!</p>
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="姓名"
|
label="姓名"
|
||||||
@@ -275,7 +275,7 @@ export function ContactSection() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full group"
|
className="w-full group mt-auto"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ export function NewsSection() {
|
|||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="text-center max-w-3xl mx-auto mb-16"
|
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">
|
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-[#1C1C1C] mb-6">
|
||||||
最新<span className="text-[#C41E3A]">资讯</span>
|
最新<span className="text-[#C41E3A]">资讯</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ export function ProductsSection() {
|
|||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="text-center max-w-3xl mx-auto mb-16"
|
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">
|
<h2 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
|
||||||
我们的<span className="text-[#C41E3A]">产品</span>
|
我们的<span className="text-[#C41E3A]">产品</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ export function ServicesSection() {
|
|||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="text-center max-w-3xl mx-auto mb-16"
|
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">
|
<h2 className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
|
||||||
我们的 <span className="text-[#C41E3A]">核心服务</span>
|
我们的 <span className="text-[#C41E3A]">核心服务</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user