1f591fe2b4
- 完善产品页面布局与交互 - 优化服务详情页用户体验 - 增强新闻模块内容展示 - 改进团队页面设计 - 优化全局样式和响应式布局 - 添加分页组件支持 - 提升性能与SEO优化 - 修复已知问题与改进代码质量
444 lines
9.9 KiB
TypeScript
444 lines
9.9 KiB
TypeScript
'use client';
|
|
|
|
import { motion, useScroll, useTransform, useSpring, type Variants, type HTMLMotionProps } from 'framer-motion';
|
|
import { useRef, type ReactNode } from 'react';
|
|
|
|
export const scrollRevealVariants: Variants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
y: 50,
|
|
scale: 0.95,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
transition: {
|
|
duration: 0.8,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
export const inkRevealVariants: Variants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
scale: 0.8,
|
|
filter: 'blur(10px)',
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
filter: 'blur(0px)',
|
|
transition: {
|
|
duration: 1,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
export const slideInLeftVariants: Variants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
x: -50,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
x: 0,
|
|
transition: {
|
|
duration: 0.6,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
export const slideInRightVariants: Variants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
x: 50,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
x: 0,
|
|
transition: {
|
|
duration: 0.6,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
interface ScrollRevealProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
delay?: number;
|
|
variants?: Variants;
|
|
once?: boolean;
|
|
threshold?: number;
|
|
}
|
|
|
|
export function ScrollReveal({
|
|
children,
|
|
className = '',
|
|
delay = 0,
|
|
variants: _variants = scrollRevealVariants,
|
|
once = true,
|
|
threshold = 0.1,
|
|
...props
|
|
}: ScrollRevealProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ['start end', 'start center'],
|
|
});
|
|
|
|
const opacity = useTransform(scrollYProgress, [0, threshold], [0, 1]);
|
|
const y = useTransform(scrollYProgress, [0, threshold], [50, 0]);
|
|
const scale = useTransform(scrollYProgress, [0, threshold], [0.95, 1]);
|
|
|
|
const smoothOpacity = useSpring(opacity, { stiffness: 100, damping: 30 });
|
|
const smoothY = useSpring(y, { stiffness: 100, damping: 30 });
|
|
const smoothScale = useSpring(scale, { stiffness: 100, damping: 30 });
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
style={{
|
|
opacity: smoothOpacity,
|
|
y: smoothY,
|
|
scale: smoothScale,
|
|
}}
|
|
initial={{ opacity: 0, y: 50, scale: 0.95 }}
|
|
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
|
viewport={{ once, amount: threshold }}
|
|
transition={{
|
|
duration: 0.8,
|
|
delay,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
}}
|
|
className={className}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface ParallaxSectionProps extends HTMLMotionProps<'section'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
speed?: number;
|
|
}
|
|
|
|
export function ParallaxSection({
|
|
children,
|
|
className = '',
|
|
speed = 0.5,
|
|
...props
|
|
}: ParallaxSectionProps) {
|
|
const ref = useRef<HTMLElement>(null);
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ['start end', 'end start'],
|
|
});
|
|
|
|
const y = useTransform(scrollYProgress, [0, 1], ['0%', `${speed * 100}%`]);
|
|
const smoothY = useSpring(y, { stiffness: 100, damping: 30 });
|
|
|
|
return (
|
|
<motion.section ref={ref} style={{ y: smoothY }} className={className} {...props}>
|
|
{children}
|
|
</motion.section>
|
|
);
|
|
}
|
|
|
|
interface ParallaxImageProps {
|
|
src: string;
|
|
alt: string;
|
|
className?: string;
|
|
speed?: number;
|
|
}
|
|
|
|
export function ParallaxImage({ src, alt, className = '', speed = 0.3 }: ParallaxImageProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ['start end', 'end start'],
|
|
});
|
|
|
|
const y = useTransform(scrollYProgress, [0, 1], [`${-speed * 50}%`, `${speed * 50}%`]);
|
|
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [1.1, 1, 1.1]);
|
|
|
|
return (
|
|
<div ref={ref} className={`overflow-hidden ${className}`}>
|
|
<motion.img
|
|
src={src}
|
|
alt={alt}
|
|
style={{ y, scale }}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ScaleOnScrollProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
minScale?: number;
|
|
maxScale?: number;
|
|
}
|
|
|
|
export function ScaleOnScroll({
|
|
children,
|
|
className = '',
|
|
minScale = 0.8,
|
|
maxScale = 1,
|
|
...props
|
|
}: ScaleOnScrollProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ['start end', 'center center'],
|
|
});
|
|
|
|
const scale = useTransform(scrollYProgress, [0, 1], [minScale, maxScale]);
|
|
const smoothScale = useSpring(scale, { stiffness: 100, damping: 30 });
|
|
|
|
return (
|
|
<motion.div ref={ref} style={{ scale: smoothScale }} className={className} {...props}>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface FadeOnScrollProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
direction?: 'up' | 'down' | 'left' | 'right';
|
|
distance?: number;
|
|
}
|
|
|
|
export function FadeOnScroll({
|
|
children,
|
|
className = '',
|
|
direction = 'up',
|
|
distance = 50,
|
|
...props
|
|
}: FadeOnScrollProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ['start end', 'center center'],
|
|
});
|
|
|
|
const transformUp = useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
|
const transformDown = useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
|
const transformLeft = useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
|
const transformRight = useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
|
|
|
const getTransform = () => {
|
|
switch (direction) {
|
|
case 'up':
|
|
return transformUp;
|
|
case 'down':
|
|
return transformDown;
|
|
case 'left':
|
|
return transformLeft;
|
|
case 'right':
|
|
return transformRight;
|
|
default:
|
|
return transformUp;
|
|
}
|
|
};
|
|
|
|
const transform = getTransform();
|
|
const opacity = useTransform(scrollYProgress, [0, 0.5], [0, 1]);
|
|
|
|
const smoothTransform = useSpring(transform, { stiffness: 100, damping: 30 });
|
|
const smoothOpacity = useSpring(opacity, { stiffness: 100, damping: 30 });
|
|
|
|
const style =
|
|
direction === 'left' || direction === 'right' ? { x: smoothTransform, opacity: smoothOpacity } : { y: smoothTransform, opacity: smoothOpacity };
|
|
|
|
return (
|
|
<motion.div ref={ref} style={style} className={className} {...props}>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface StaggerRevealProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
staggerDelay?: number;
|
|
containerDelay?: number;
|
|
}
|
|
|
|
export function StaggerReveal({
|
|
children,
|
|
className = '',
|
|
staggerDelay = 0.1,
|
|
containerDelay = 0,
|
|
...props
|
|
}: StaggerRevealProps) {
|
|
const containerVariants: Variants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: staggerDelay,
|
|
delayChildren: containerDelay,
|
|
},
|
|
},
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, amount: 0.2 }}
|
|
variants={containerVariants}
|
|
className={className}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface StaggerItemProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function StaggerItem({ children, className = '', ...props }: StaggerItemProps) {
|
|
const itemVariants: Variants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
y: 30,
|
|
scale: 0.95,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
transition: {
|
|
duration: 0.6,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
return (
|
|
<motion.div variants={itemVariants} className={className} {...props}>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface TextRevealProps extends HTMLMotionProps<'div'> {
|
|
text: string;
|
|
className?: string;
|
|
delay?: number;
|
|
}
|
|
|
|
export function TextReveal({ text, className = '', delay = 0, ...props }: TextRevealProps) {
|
|
const words = text.split(' ');
|
|
|
|
const containerVariants: Variants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: 0.05,
|
|
delayChildren: delay,
|
|
},
|
|
},
|
|
};
|
|
|
|
const wordVariants: Variants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
y: 20,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.4,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true }}
|
|
variants={containerVariants}
|
|
className={className}
|
|
{...props}
|
|
>
|
|
{words.map((word, index) => (
|
|
<motion.span
|
|
key={index}
|
|
variants={wordVariants}
|
|
className="inline-block mr-2"
|
|
>
|
|
{word}
|
|
</motion.span>
|
|
))}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface ProgressIndicatorProps {
|
|
className?: string;
|
|
color?: string;
|
|
}
|
|
|
|
export function ProgressIndicator({ className = '', color = 'var(--color-brand-primary)' }: ProgressIndicatorProps) {
|
|
const { scrollYProgress } = useScroll();
|
|
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30 });
|
|
|
|
return (
|
|
<motion.div
|
|
className={`fixed top-0 left-0 right-0 h-1 origin-left z-50 ${className}`}
|
|
style={{ scaleX, backgroundColor: color }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
interface ScrollTriggeredCounterProps {
|
|
end: number;
|
|
duration?: number;
|
|
prefix?: string;
|
|
suffix?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function ScrollTriggeredCounter({
|
|
end,
|
|
duration: _duration = 2,
|
|
prefix = '',
|
|
suffix = '',
|
|
className = '',
|
|
}: ScrollTriggeredCounterProps) {
|
|
const ref = useRef<HTMLSpanElement>(null);
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ['start end', 'center center'],
|
|
});
|
|
|
|
const displayNumber = useTransform(scrollYProgress, [0, 1], [0, end]);
|
|
const rounded = useTransform(displayNumber, (latest) => Math.round(latest));
|
|
|
|
return (
|
|
<motion.span ref={ref} className={className}>
|
|
{prefix}
|
|
<motion.span>{rounded}</motion.span>
|
|
{suffix}
|
|
</motion.span>
|
|
);
|
|
}
|