yintai-company-home/src/components/layout/RowAccordion/index.tsx

116 lines
4.7 KiB
TypeScript

// 横向手风琴组件
import { useEffect, useRef, useState } from 'react';
import { motion, useInView, type Variants } from 'motion/react';
import styles from './index.module.css';
import { Link } from 'react-router-dom';
import { scrollToWithLenis } from '@/utils/lenis';
const contentItemVariants: Variants = {
hidden: { opacity: 0, x: 80 },
visible: { opacity: 1, x: 0 },
};
const FALLBACK_GRADIENT = "linear-gradient(0deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%)";
type Data = {
title?: string;
items: {
title: string;
subtitle?: string;
content?: string;
links?: {
text: string;
path: string;
}[];
/** 以 mockData 为准,优先使用 backgroundImage */
backgroundImage?: string;
image?: string;
}[];
}
type Props = {
data: Data;
placement?: 'top' | 'bottom';
}
export default function RowAccordion({ data, placement='bottom' }: Props) {
const [activeIndex, setActiveIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const isInView = useInView(containerRef, { once: true, margin: "0px 0px -30% 0px" });
useEffect(() => {
if (isInView && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
scrollToWithLenis(window.scrollY + rect.top, { duration: 3 });
}
}, [isInView]);
const getToPath = (link: { text: string; path: string }) => {
return link.path.includes("{id}") ? link.path.replace("{id}", link.text) : link.path;
}
return (
<div ref={containerRef} className={styles.rowAccordion}>
<div className={styles.rowAccordionBgContainer}>
{data.items.map((item, index) => (
<div
key={index}
className={styles.rowAccordionBgLayer}
style={{
backgroundImage: `url(${item.backgroundImage ?? item.image ?? ""}), ${FALLBACK_GRADIENT}`,
opacity: activeIndex === index ? 1 : 0,
}}
/>
))}
</div>
{
data.title && (
<div className={styles.headerRow}>
<div className={styles.title}>{data.title}</div>
</div>
)
}
<div className={styles.contentRow} style={{ height: data.title ? 'calc(100% - 15.625rem)' : '100%' }}>
{data.items.map((item, index) => (
<motion.div
className={`${styles.contentItem} ${activeIndex === index && styles.active}`}
key={item.title}
style={{ justifyContent: placement === 'top' ? 'flex-start' : 'flex-end' }}
onMouseEnter={() => setActiveIndex(index)}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={contentItemVariants}
transition={{
duration: 0.6,
delay: 0.3 + index * 0.12,
ease: [0.25, 0.46, 0.45, 0.94],
}}
>
<div className={styles.contentItemContainer}>
<div className={styles.contentItemTitle}>{item.title}</div>
{item.subtitle && <div className={styles.contentItemSubtitle}>{item.subtitle}</div>}
{
(item.content || (item.links || []).length > 0) && (
<div className={styles.contentItemContentWrapper}>
{item.content && (
<div className={styles.contentItemContent}>{item.content}</div>
)}
{item.links && (
<div className={styles.contentItemLinks}>
{item.links?.map((link) => (
<Link key={link.text} to={getToPath(link)}>{link.text}</Link>
))}
</div>
)}
</div>
)
}
</div>
</motion.div>
))}
</div>
</div>
);
}