同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
158 lines
6.2 KiB
TypeScript
158 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useEffect } from 'react';
|
|
import { ChevronDown, Lock, Sparkles } from 'lucide-react';
|
|
import { StaticLink } from '@/components/ui/static-link';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import type { MegaDropdownGroup } from '@/lib/site-config';
|
|
|
|
interface MegaDropdownProps {
|
|
label: string;
|
|
groups: MegaDropdownGroup[];
|
|
isOpen: boolean;
|
|
isActive?: boolean;
|
|
onToggle: () => void;
|
|
onOpen: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function MegaDropdown({ label, groups, isOpen, isActive, onToggle, onOpen, onClose }: MegaDropdownProps) {
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
|
|
const handleMouseEnter = () => {
|
|
clearTimeout(timeoutRef.current);
|
|
onOpen();
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
timeoutRef.current = setTimeout(onClose, 300);
|
|
};
|
|
|
|
useEffect(() => {
|
|
return () => clearTimeout(timeoutRef.current);
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={dropdownRef} className="relative" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
|
<div className="-mx-2 px-2 py-2">
|
|
<button
|
|
onClick={onToggle}
|
|
className={`
|
|
relative flex items-center gap-1 px-3 py-2 text-sm font-medium
|
|
transition-all duration-300 ease-out
|
|
${isOpen || isActive
|
|
? 'text-text-primary'
|
|
: 'text-text-secondary hover:text-text-primary hover:bg-bg-secondary'
|
|
}
|
|
`}
|
|
aria-expanded={isOpen}
|
|
aria-haspopup="true"
|
|
aria-current={isActive ? 'page' : undefined}
|
|
>
|
|
{label}
|
|
<ChevronDown
|
|
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
|
/>
|
|
<span
|
|
className={`
|
|
absolute -bottom-0.5 left-3 right-3 h-0.5 bg-brand
|
|
transition-all duration-300 ease-out
|
|
${isActive
|
|
? 'opacity-100 scale-x-100'
|
|
: 'opacity-0 scale-x-0'
|
|
}
|
|
`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -8 }}
|
|
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 pt-2 w-[640px] z-50"
|
|
>
|
|
<div className="bg-white backdrop-blur-xl rounded-none border border-border-primary p-6">
|
|
{groups.map((group, groupIndex) => (
|
|
<div key={group.id} className={groupIndex > 0 ? 'mt-6 pt-6 border-t border-border-primary' : ''}>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<h3 className="text-sm font-bold text-text-primary">{group.title}</h3>
|
|
{group.description && (
|
|
<span className="text-xs text-text-muted">{group.description}</span>
|
|
)}
|
|
{group.highlight && (
|
|
<Sparkles className="w-3.5 h-3.5 text-brand" />
|
|
)}
|
|
</div>
|
|
|
|
<div className={`grid gap-2 ${group.items.length > 4 ? 'grid-cols-3' : 'grid-cols-2'}`}>
|
|
{group.items.map((item) => {
|
|
const isPlaceholder = item.href === '#';
|
|
const content = (
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<div className="text-sm font-semibold text-text-primary">{item.title}</div>
|
|
{item.badge && (
|
|
<span className={`shrink-0 px-1.5 py-0.5 text-[10px] font-medium ${
|
|
item.badge === '敬请期待'
|
|
? 'bg-bg-tertiary text-text-muted'
|
|
: 'bg-brand/10 text-brand'
|
|
}`}>
|
|
{item.badge === '敬请期待' && <Lock className="w-2.5 h-2.5 inline mr-0.5" />}
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-text-secondary mt-1 leading-relaxed">{item.description}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (isPlaceholder) {
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className="block p-3 border border-border-primary bg-bg-secondary cursor-not-allowed opacity-50"
|
|
aria-disabled="true"
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<StaticLink
|
|
key={item.id}
|
|
href={item.href}
|
|
className="block p-3 border border-transparent transition-all duration-300 ease-out group hover:border-border-primary hover:bg-bg-secondary"
|
|
>
|
|
{content}
|
|
</StaticLink>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<div className="mt-4 pt-4 border-t border-border-primary">
|
|
<StaticLink
|
|
href={`/${label === '产品' ? 'products' : label === '解决方案' ? 'solutions' : 'services'}`}
|
|
className="flex items-center justify-center gap-1 text-xs font-medium text-text-muted hover:text-text-primary transition-colors"
|
|
>
|
|
查看全部{label}
|
|
<ChevronDown className="w-3 h-3 rotate-[-90deg]" />
|
|
</StaticLink>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}
|