67 lines
2.0 KiB
TypeScript
67 lines
2.0 KiB
TypeScript
"use client";
|
||
|
||
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
||
|
||
/** 恒定线速度:px/s,图标多少只影响时长 */
|
||
const SCROLL_PX_PER_SEC = 36;
|
||
|
||
type AdIconMarqueeProps = {
|
||
children: ReactNode;
|
||
className?: string;
|
||
/** 段内间距,需与尾部 spacer 对齐以无缝衔接 */
|
||
gapClassName?: string;
|
||
/** 段尾 spacer 宽度,应等于 gap(gap-2→w-2,gap-3→w-3) */
|
||
spacerClassName?: string;
|
||
/** 内容签名:列表变化时重算时长 */
|
||
contentKey?: string;
|
||
};
|
||
|
||
/** Icon 广告从右向左无缝自动滚动(内容复制两段,translateX -50%) */
|
||
export function AdIconMarquee({
|
||
children,
|
||
className = "",
|
||
gapClassName = "gap-3",
|
||
spacerClassName = "w-3",
|
||
contentKey = "",
|
||
}: AdIconMarqueeProps) {
|
||
const segmentRef = useRef<HTMLDivElement>(null);
|
||
const [durationSec, setDurationSec] = useState(20);
|
||
|
||
useLayoutEffect(() => {
|
||
const el = segmentRef.current;
|
||
if (!el) return;
|
||
|
||
const update = () => {
|
||
const width = el.getBoundingClientRect().width;
|
||
if (width <= 0) return;
|
||
setDurationSec(Math.max(width / SCROLL_PX_PER_SEC, 8));
|
||
};
|
||
|
||
update();
|
||
const observer = new ResizeObserver(update);
|
||
observer.observe(el);
|
||
return () => observer.disconnect();
|
||
}, [contentKey]);
|
||
|
||
return (
|
||
<div
|
||
data-component="ad-icon-marquee"
|
||
className={`overflow-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${className}`}
|
||
>
|
||
<div
|
||
className="ad-icon-marquee-track flex w-max items-start"
|
||
style={{ animationDuration: `${durationSec}s` }}
|
||
>
|
||
<div ref={segmentRef} className={`flex shrink-0 items-start ${gapClassName}`}>
|
||
{children}
|
||
<span className={`${spacerClassName} shrink-0`} aria-hidden />
|
||
</div>
|
||
<div className={`flex shrink-0 items-start ${gapClassName}`} aria-hidden>
|
||
{children}
|
||
<span className={`${spacerClassName} shrink-0`} aria-hidden />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|