feat: 添加预览效果页面并优化交互效果

refactor: 优化代码健壮性和类型安全

style: 更新字体样式和全局CSS

fix: 修复IntersectionObserver潜在空引用问题

chore: 更新依赖和ESLint配置

build: 更新构建ID和路由配置
This commit is contained in:
张翔
2026-02-24 10:24:05 +08:00
parent 64165c4499
commit fecbfd1990
239 changed files with 3403 additions and 5181 deletions
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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;
+71 -41
View File
@@ -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',
+5 -5
View File
@@ -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);
+6 -4
View File
@@ -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);
+13 -8
View File
@@ -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 = '',
+10 -4
View File
@@ -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;