feat(website): 三轮视觉改造与页面过渡动画
改造概要(30项): - 第一轮:Hero重构/Section差异化/SocialProof强化/CTA对比度/About架构 - 第二轮:字体优化/背景交替/Solutions差异化/Footer五列/MegaDropdown修复 - 第三轮:卡片交互/表单层级/CTA统一/时间线标记/连接线/三列布局/移动导航/Button微交互/SEO Schema - P3-2:template.tsx+Framer Motion页面过渡/loading.tsx加载状态 - 清理:删除未用组件/hooks,修复重复移动导航,清理冗余CSS
This commit is contained in:
@@ -2,69 +2,42 @@
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import type { MegaDropdownItem } from '@/lib/constants';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
interface MegaDropdownItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface MegaDropdownProps {
|
||||
label: string;
|
||||
items: MegaDropdownItem[];
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const HOVER_DELAY = 150;
|
||||
|
||||
export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }: MegaDropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
if (isOpen) { onToggle(); }
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen, onToggle]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
if (!isOpen) {
|
||||
if (onOpen) {
|
||||
onOpen();
|
||||
} else {
|
||||
onToggle();
|
||||
}
|
||||
}
|
||||
clearTimeout(timeoutRef.current);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
if (isOpen) {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
} else {
|
||||
onToggle();
|
||||
}
|
||||
}
|
||||
}, HOVER_DELAY);
|
||||
timeoutRef.current = setTimeout(onClose, 150);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => clearTimeout(timeoutRef.current);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className="relative" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<button
|
||||
@@ -73,8 +46,8 @@ export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }
|
||||
flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md
|
||||
transition-all duration-200
|
||||
${isOpen
|
||||
? 'text-[#C41E3A] bg-[#FEF2F4]'
|
||||
: 'text-[#3D3D3D] hover:text-[#1C1C1C] hover:bg-[#F5F5F5]'
|
||||
? 'text-[var(--color-brand-primary)] bg-[var(--color-brand-primary-bg)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)]'
|
||||
}
|
||||
`}
|
||||
aria-expanded={isOpen}
|
||||
@@ -93,19 +66,21 @@ export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute top-full left-1/2 -translate-x-1/2 mt-3 w-[520px] bg-white rounded-xl border border-[#E5E5E5] shadow-lg p-5 z-50"
|
||||
className="absolute top-full left-1/2 -translate-x-1/2 pt-2 w-[520px] z-50"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-[var(--color-bg-primary)] rounded-xl border border-[var(--color-border-primary)] shadow-lg p-5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{items.map((item) => (
|
||||
<StaticLink
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
className="block p-4 rounded-lg border-l-[3px] border-l-[#C41E3A] hover:bg-[#FFFBF5] transition-colors duration-200"
|
||||
className="block p-4 rounded-lg border-l-[3px] border-l-[var(--color-brand-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors duration-200"
|
||||
>
|
||||
<div className="text-sm font-semibold text-[#1C1C1C]">{item.title}</div>
|
||||
<div className="text-xs text-[#5C5C5C] mt-1.5 leading-relaxed">{item.description}</div>
|
||||
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{item.title}</div>
|
||||
<div className="text-xs text-[var(--color-text-placeholder)] mt-1.5 leading-relaxed">{item.description}</div>
|
||||
</StaticLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user