78 lines
2.4 KiB
TypeScript
78 lines
2.4 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 containerRef = useRef<HTMLDivElement>(null);
|
||
const segmentRef = useRef<HTMLDivElement>(null);
|
||
const [durationSec, setDurationSec] = useState(20);
|
||
const [overflowing, setOverflowing] = useState(false);
|
||
|
||
useLayoutEffect(() => {
|
||
const container = containerRef.current;
|
||
const el = segmentRef.current;
|
||
if (!container || !el) return;
|
||
|
||
const update = () => {
|
||
const width = el.getBoundingClientRect().width;
|
||
if (width <= 0) return;
|
||
setOverflowing(width > container.clientWidth + 1);
|
||
setDurationSec(Math.max(width / SCROLL_PX_PER_SEC, 8));
|
||
};
|
||
|
||
update();
|
||
const observer = new ResizeObserver(update);
|
||
observer.observe(container);
|
||
observer.observe(el);
|
||
return () => observer.disconnect();
|
||
}, [contentKey]);
|
||
|
||
return (
|
||
<div
|
||
ref={containerRef}
|
||
data-component="ad-icon-marquee"
|
||
className={`overflow-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${className}`}
|
||
>
|
||
<div
|
||
className={`flex w-max items-start ${overflowing ? "ad-icon-marquee-track" : ""}`}
|
||
style={overflowing ? { animationDuration: `${durationSec}s` } : undefined}
|
||
>
|
||
<div ref={segmentRef} className={`flex shrink-0 items-start ${gapClassName}`}>
|
||
{children}
|
||
<span className={`${spacerClassName} shrink-0`} aria-hidden />
|
||
</div>
|
||
{overflowing ? (
|
||
<div className={`flex shrink-0 items-start ${gapClassName}`} aria-hidden>
|
||
{children}
|
||
<span className={`${spacerClassName} shrink-0`} aria-hidden />
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|