feat: 更新营销页面组件与交互优化

- 删除 .impeccable.md
- 新增 detail-swipe-nav, loading-state, skeleton, tooltip 组件
- 新增 use-keyboard-shortcuts, use-swipe-gesture hooks
- 更新 contact, news, products, services, solutions 等页面
- 优化 header, mobile-menu, input, page-transition 组件
- 添加 terms 与错误追踪测试页面
This commit is contained in:
张翔
2026-05-14 18:21:54 +08:00
parent b8ab1fd0e3
commit 7f6128a6ff
23 changed files with 1190 additions and 111 deletions
+90 -25
View File
@@ -2,47 +2,112 @@
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname } from 'next/navigation';
import { ReactNode, useSyncExternalStore } from 'react';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
interface PageTransitionProps {
children: ReactNode;
}
function useIsMounted() {
return useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
const pageVariants = {
initial: {
opacity: 0,
y: 8,
scale: 0.98,
},
animate: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.4,
ease: [0.25, 1, 0.5, 1] as const,
staggerChildren: 0.05,
},
() => true,
() => false
);
}
},
exit: {
opacity: 0,
y: -8,
scale: 0.98,
transition: {
duration: 0.2,
ease: [0.22, 1, 0.36, 1] as const,
},
},
};
export function PageTransition({ children }: PageTransitionProps) {
export function PageTransition({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const shouldReduceMotion = useReducedMotion();
const mounted = useIsMounted();
if (shouldReduceMotion || !mounted) {
if (shouldReduceMotion) {
return <>{children}</>;
}
return (
<AnimatePresence mode="wait">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={pathname}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{
duration: 0.3,
ease: [0.16, 1, 0.3, 1],
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
style={{
willChange: 'transform, opacity',
backfaceVisibility: 'hidden',
}}
>
{children}
</motion.div>
</AnimatePresence>
);
}
}
const sectionVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.08,
duration: 0.5,
ease: [0.25, 1, 0.5, 1] as const,
},
}),
};
export function StaggerChildren({
children,
className = '',
staggerCount = 4
}: {
children: React.ReactNode;
className?: string;
staggerCount?: number;
}) {
const shouldReduceMotion = useReducedMotion();
if (shouldReduceMotion) {
return <div className={className}>{children}</div>;
}
return (
<motion.div
className={className}
initial="hidden"
animate="visible"
variants={{
visible: {
transition: {
staggerChildren: 0.06,
delayChildren: 0.1,
},
},
}}
>
{Array.isArray(children)
? children.map((child, i) => (
<motion.div key={i} custom={Math.min(i, staggerCount)} variants={sectionVariants}>
{child}
</motion.div>
))
: children
}
</motion.div>
);
}