feat: 添加预览效果页面并优化交互效果
refactor: 优化代码健壮性和类型安全 style: 更新字体样式和全局CSS fix: 修复IntersectionObserver潜在空引用问题 chore: 更新依赖和ESLint配置 build: 更新构建ID和路由配置
This commit is contained in:
@@ -44,7 +44,7 @@ export function InkCard({
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
if (!cardRef.current) {return;}
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
@@ -214,7 +214,7 @@ export function TiltCard({
|
||||
const [rotateY, setRotateY] = useState(0);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
if (!cardRef.current) {return;}
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
@@ -269,7 +269,7 @@ export function GlowCard({
|
||||
const [glowPosition, setGlowPosition] = useState({ x: 50, y: 50 });
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
if (!cardRef.current) {return;}
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
@@ -26,7 +26,7 @@ export function AnimatedNumber({
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView || hasAnimated.current) return;
|
||||
if (!isInView || hasAnimated.current) {return;}
|
||||
|
||||
hasAnimated.current = true;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function InkDrop({
|
||||
color = '#1C1C1C',
|
||||
blur = 0,
|
||||
delay = 0,
|
||||
duration = 8,
|
||||
duration: _duration = 8,
|
||||
className = ''
|
||||
}: InkDropProps) {
|
||||
return (
|
||||
@@ -251,7 +251,7 @@ export function FloatingInk({ count = 15, className = '' }: FloatingInkProps) {
|
||||
}, []);
|
||||
|
||||
const elements = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
if (!isMounted) {return [];}
|
||||
const items = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -351,12 +351,50 @@ interface InkDecorationProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InkDecoration({ variant = 'balanced', className = '' }: InkDecorationProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
interface DropPosition {
|
||||
left: string;
|
||||
top: string;
|
||||
size: number;
|
||||
opacity: number;
|
||||
blur: number;
|
||||
isRed: boolean;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
interface SplashPosition {
|
||||
left: string;
|
||||
top: string;
|
||||
size: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface SealPosition {
|
||||
left: string;
|
||||
top: string;
|
||||
size: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface StainPosition {
|
||||
left: string;
|
||||
top: string;
|
||||
size: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface StrokePosition {
|
||||
left: string;
|
||||
top: string;
|
||||
width: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export function InkDecoration({ variant = 'balanced', className = '' }: InkDecorationProps) {
|
||||
const [dropPositions, setDropPositions] = useState<DropPosition[]>([]);
|
||||
const [splashPositions, setSplashPositions] = useState<SplashPosition[]>([]);
|
||||
const [sealPositions, setSealPositions] = useState<SealPosition[]>([]);
|
||||
const [stainPositions, setStainPositions] = useState<StainPosition[]>([]);
|
||||
const [strokePositions, setStrokePositions] = useState<StrokePosition[]>([]);
|
||||
|
||||
const config = {
|
||||
minimal: { drops: 3, splashes: 1, seals: 1, stains: 1, strokes: 1 },
|
||||
@@ -366,53 +404,45 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
|
||||
const { drops, splashes, seals, stains, strokes } = config[variant];
|
||||
|
||||
const dropPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: drops }, (_, i) => ({
|
||||
useEffect(() => {
|
||||
setDropPositions(Array.from({ length: drops }, (_, i) => ({
|
||||
left: `${15 + (i * 70 / drops)}%`,
|
||||
top: `${20 + Math.random() * 60}%`,
|
||||
size: 6 + Math.random() * 14,
|
||||
opacity: 0.06 + Math.random() * 0.1,
|
||||
blur: Math.random() * 3,
|
||||
isRed: i % 3 === 0,
|
||||
}));
|
||||
}, [drops, isMounted]);
|
||||
|
||||
const splashPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: splashes }, (_, i) => ({
|
||||
duration: 5 + Math.random() * 3,
|
||||
})));
|
||||
|
||||
setSplashPositions(Array.from({ length: splashes }, (_, i) => ({
|
||||
left: `${20 + (i * 60 / splashes)}%`,
|
||||
top: `${15 + Math.random() * 70}%`,
|
||||
size: 40 + Math.random() * 40,
|
||||
}));
|
||||
}, [splashes, isMounted]);
|
||||
|
||||
const sealPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: seals }, (_, i) => ({
|
||||
duration: 7 + Math.random() * 3,
|
||||
})));
|
||||
|
||||
setSealPositions(Array.from({ length: seals }, (_, i) => ({
|
||||
left: `${25 + (i * 50 / seals)}%`,
|
||||
top: `${25 + Math.random() * 50}%`,
|
||||
size: 25 + Math.random() * 25,
|
||||
}));
|
||||
}, [seals, isMounted]);
|
||||
|
||||
const stainPositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: stains }, (_, i) => ({
|
||||
duration: 6 + Math.random() * 2,
|
||||
})));
|
||||
|
||||
setStainPositions(Array.from({ length: stains }, (_, i) => ({
|
||||
left: `${10 + (i * 80 / stains)}%`,
|
||||
top: `${30 + Math.random() * 40}%`,
|
||||
size: 80 + Math.random() * 60,
|
||||
}));
|
||||
}, [stains, isMounted]);
|
||||
|
||||
const strokePositions = useMemo(() => {
|
||||
if (!isMounted) return [];
|
||||
return Array.from({ length: strokes }, (_, i) => ({
|
||||
duration: 8 + Math.random() * 4,
|
||||
})));
|
||||
|
||||
setStrokePositions(Array.from({ length: strokes }, (_, i) => ({
|
||||
left: `${15 + (i * 70 / strokes)}%`,
|
||||
top: `${40 + Math.random() * 30}%`,
|
||||
width: 100 + Math.random() * 100,
|
||||
}));
|
||||
}, [strokes, isMounted]);
|
||||
duration: 6 + Math.random() * 3,
|
||||
})));
|
||||
}, [drops, splashes, seals, stains, strokes]);
|
||||
|
||||
return (
|
||||
<div className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}>
|
||||
@@ -426,7 +456,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
scale: [1, 1.1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 5 + Math.random() * 3,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.2,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -452,7 +482,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
rotate: [0, 5, -5, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 7 + Math.random() * 3,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.3,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -472,7 +502,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
rotate: [-8, -5, -10, -8],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6 + Math.random() * 2,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.25,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -492,7 +522,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
opacity: [0.04, 0.06, 0.04],
|
||||
}}
|
||||
transition={{
|
||||
duration: 8 + Math.random() * 4,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.35,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
@@ -512,7 +542,7 @@ export function InkDecoration({ variant = 'balanced', className = '' }: InkDecor
|
||||
opacity: [0.08, 0.12, 0.08],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6 + Math.random() * 3,
|
||||
duration: pos.duration,
|
||||
delay: i * 0.3,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
|
||||
@@ -24,10 +24,10 @@ export function ParticleBackground({
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
if (!canvas) {return;}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
if (!ctx) {return;}
|
||||
|
||||
let animationFrameId: number;
|
||||
let particles: Particle[] = [];
|
||||
@@ -58,8 +58,8 @@ export function ParticleBackground({
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
|
||||
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
|
||||
if (particle.x < 0 || particle.x > canvas.width) {particle.vx *= -1;}
|
||||
if (particle.y < 0 || particle.y > canvas.height) {particle.vy *= -1;}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||
@@ -67,7 +67,7 @@ export function ParticleBackground({
|
||||
ctx.fill();
|
||||
|
||||
particles.forEach((otherParticle, j) => {
|
||||
if (i === j) return;
|
||||
if (i === j) {return;}
|
||||
const dx = particle.x - otherParticle.x;
|
||||
const dy = particle.y - otherParticle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion, AnimatePresence, type HTMLMotionProps } from 'framer-motion';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface RippleButtonProps
|
||||
rippleDuration?: number;
|
||||
children?: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -60,7 +61,7 @@ const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
|
||||
const [ripples, setRipples] = React.useState<Ripple[]>([]);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) return;
|
||||
if (disabled) {return;}
|
||||
|
||||
const button = e.currentTarget;
|
||||
const rect = button.getBoundingClientRect();
|
||||
@@ -78,7 +79,7 @@ const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
|
||||
};
|
||||
|
||||
const getRippleColor = () => {
|
||||
if (rippleColor) return rippleColor;
|
||||
if (rippleColor) {return rippleColor;}
|
||||
if (variant === 'outline' || variant === 'ghost' || variant === 'link') {
|
||||
return 'rgba(196, 30, 58, 0.2)';
|
||||
}
|
||||
@@ -123,6 +124,7 @@ RippleButton.displayName = 'RippleButton';
|
||||
export interface SealButtonProps extends VariantProps<typeof rippleButtonVariants> {
|
||||
children?: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -133,7 +135,7 @@ const SealButton = React.forwardRef<HTMLButtonElement, SealButtonProps>(
|
||||
const [showInk, setShowInk] = React.useState(false);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) return;
|
||||
if (disabled) {return;}
|
||||
setIsPressed(true);
|
||||
setShowInk(true);
|
||||
setTimeout(() => setIsPressed(false), 600);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useScroll, useTransform, useSpring, type MotionValue, type Variants, type HTMLMotionProps } from 'framer-motion';
|
||||
import { motion, useScroll, useTransform, useSpring, type Variants, type HTMLMotionProps } from 'framer-motion';
|
||||
import { useRef, type ReactNode } from 'react';
|
||||
|
||||
export const scrollRevealVariants: Variants = {
|
||||
@@ -80,7 +80,7 @@ export function ScrollReveal({
|
||||
children,
|
||||
className = '',
|
||||
delay = 0,
|
||||
variants = scrollRevealVariants,
|
||||
variants: _variants = scrollRevealVariants,
|
||||
once = true,
|
||||
threshold = 0.1,
|
||||
...props
|
||||
@@ -230,18 +230,23 @@ export function FadeOnScroll({
|
||||
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 useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
return transformUp;
|
||||
case 'down':
|
||||
return useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
return transformDown;
|
||||
case 'left':
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
return transformLeft;
|
||||
case 'right':
|
||||
return useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
return transformRight;
|
||||
default:
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
return transformUp;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -414,7 +419,7 @@ interface ScrollTriggeredCounterProps {
|
||||
|
||||
export function ScrollTriggeredCounter({
|
||||
end,
|
||||
duration = 2,
|
||||
duration: _duration = 2,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
className = '',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
||||
import { useState, useRef, type ReactNode } from 'react';
|
||||
|
||||
interface TouchSwipeProps {
|
||||
children: ReactNode;
|
||||
@@ -23,15 +23,21 @@ export function TouchSwipe({
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
setTouchEnd(null);
|
||||
setTouchStart(e.targetTouches[0].clientX);
|
||||
const touch = e.targetTouches[0];
|
||||
if (touch) {
|
||||
setTouchStart(touch.clientX);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
setTouchEnd(e.targetTouches[0].clientX);
|
||||
const touch = e.targetTouches[0];
|
||||
if (touch) {
|
||||
setTouchEnd(touch.clientX);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!touchStart || !touchEnd) return;
|
||||
if (!touchStart || !touchEnd) {return;}
|
||||
|
||||
const distance = touchStart - touchEnd;
|
||||
const isLeftSwipe = distance > threshold;
|
||||
|
||||
Reference in New Issue
Block a user