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
+69
View File
@@ -0,0 +1,69 @@
'use client';
import { useEffect, useRef, useCallback } from 'react';
export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
const containerRef = useRef<T>(null);
const previousActiveElement = useRef<HTMLElement | null>(null);
const getFocusableElements = useCallback(() => {
if (!containerRef.current) return [];
const elements = containerRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
return Array.from(elements).filter(
(el) => !el.hasAttribute('disabled') && el.offsetParent !== null
);
}, []);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (!isActive || !containerRef.current) return;
if (event.key === 'Tab') {
const focusableElements = getFocusableElements();
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
}
} else {
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}
}
if (event.key === 'Escape') {
event.preventDefault();
previousActiveElement.current?.focus();
}
},
[isActive, getFocusableElements]
);
useEffect(() => {
if (isActive) {
previousActiveElement.current = document.activeElement as HTMLElement;
const focusableElements = getFocusableElements();
focusableElements[0]?.focus();
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset';
};
}, [isActive, handleKeyDown, getFocusableElements]);
return containerRef;
}
+5 -3
View File
@@ -19,13 +19,15 @@ export function useIntersectionObserver<T extends Element>(
useEffect(() => {
const element = elementRef.current;
if (!element) return;
if (!element) {return;}
if (freezeOnceVisible && isIntersecting) return;
if (freezeOnceVisible && isIntersecting) {return;}
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
if (entry) {
setIsIntersecting(entry.isIntersecting);
}
},
{ threshold, root, rootMargin }
);
+29 -21
View File
@@ -1,28 +1,36 @@
'use client';
import { useState, useEffect } from 'react';
import { useSyncExternalStore } from 'react';
function getMediaQuerySnapshot(query: string): boolean {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia(query).matches;
}
function getMediaQueryServerSnapshot(): boolean {
return false;
}
function subscribeToMediaQuery(query: string, callback: () => void): () => void {
if (typeof window === 'undefined') {
return () => {};
}
const media = window.matchMedia(query);
media.addEventListener('change', callback);
return () => {
media.removeEventListener('change', callback);
};
}
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = (event: MediaQueryListEvent) => {
setMatches(event.matches);
};
media.addEventListener('change', listener);
return () => {
media.removeEventListener('change', listener);
};
}, [matches, query]);
const matches = useSyncExternalStore(
(callback) => subscribeToMediaQuery(query, callback),
() => getMediaQuerySnapshot(query),
getMediaQueryServerSnapshot
);
return matches;
}
+6 -6
View File
@@ -16,11 +16,11 @@ export function useScrollReveal({
useEffect(() => {
const element = ref.current;
if (!element) return;
if (!element) {return;}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
if (entry?.isIntersecting) {
setIsVisible(true);
if (triggerOnce) {
observer.unobserve(element);
@@ -44,7 +44,7 @@ interface UseScrollProgressOptions {
threshold?: number;
}
export function useScrollProgress({ threshold = 0 }: UseScrollProgressOptions = {}) {
export function useScrollProgress({ threshold: _threshold = 0 }: UseScrollProgressOptions = {}) {
const [progress, setProgress] = useState(0);
useEffect(() => {
@@ -74,13 +74,13 @@ export function useParallax({ speed = 0.5 }: UseParallaxOptions = {}) {
useEffect(() => {
const handleScroll = () => {
if (!ref.current) return;
if (!ref.current) {return;}
const rect = ref.current.getBoundingClientRect();
const scrolled = window.pageYOffset;
const elementTop = rect.top + scrolled;
const parallaxOffset = (scrolled - elementTop) * speed;
setOffset(parallaxOffset);
};