116 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|