UPD
This commit is contained in:
parent
f3c97db5f8
commit
d22ef62afb
|
|
@ -293,6 +293,35 @@ const RightBrand = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
function usePreloadImages(urls: string[]) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!urls?.length) return;
|
||||||
|
const unique = Array.from(new Set(urls.filter(Boolean)));
|
||||||
|
|
||||||
|
unique.forEach((url) => {
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
// Проверяем, есть ли уже preload в <head>
|
||||||
|
const exists = document.querySelector<HTMLLinkElement>(
|
||||||
|
`link[rel="preload"][as="image"][href="${url}"]`
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "preload";
|
||||||
|
link.as = "image";
|
||||||
|
link.href = url;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дополнительно сразу грузим в memory cache
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}, [urls]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ===================== Component ===================== */
|
/* ===================== Component ===================== */
|
||||||
|
|
||||||
export default function KeyBusinessSegments() {
|
export default function KeyBusinessSegments() {
|
||||||
|
|
@ -304,20 +333,19 @@ export default function KeyBusinessSegments() {
|
||||||
const segments = useMemo(() => mapSegments(data), [data]);
|
const segments = useMemo(() => mapSegments(data), [data]);
|
||||||
|
|
||||||
// --- предзагрузка иконок ---
|
// --- предзагрузка иконок ---
|
||||||
useEffect(() => {
|
// собираем все картинки для предзагрузки
|
||||||
if (!segments?.length) return;
|
const preloadUrls = useMemo(() => {
|
||||||
const urls = segments.flatMap((seg) =>
|
if (!segments?.length) return [];
|
||||||
seg.info.flatMap((block) =>
|
return segments.flatMap((seg) =>
|
||||||
block.items
|
seg.info.flatMap((block) => [
|
||||||
.map((item) => item.infoLogo)
|
block.brandLogo, // ← бренд
|
||||||
.filter((u): u is string => Boolean(u))
|
...block.items.map((item) => item.infoLogo), // ← иконки
|
||||||
)
|
])
|
||||||
);
|
).filter((u): u is string => Boolean(u));
|
||||||
urls.forEach((url) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = url;
|
|
||||||
});
|
|
||||||
}, [segments]);
|
}, [segments]);
|
||||||
|
|
||||||
|
// предзагрузка всех логотипов
|
||||||
|
usePreloadImages(preloadUrls);
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -328,11 +356,42 @@ export default function KeyBusinessSegments() {
|
||||||
const denom = Math.max(1, N - 1);
|
const denom = Math.max(1, N - 1);
|
||||||
const defaultCenterIndex = useMemo(() => Math.floor(N / 2), [N]);
|
const defaultCenterIndex = useMemo(() => Math.floor(N / 2), [N]);
|
||||||
|
|
||||||
const [activeIndex, setActiveIndex] = useState<number>(defaultCenterIndex);
|
const [activeIndex, _setActiveIndex] = useState<number>(defaultCenterIndex);
|
||||||
const [hoverIndex, setHoverIndex] = useState<number | null>(null);
|
const [hoverIndex, setHoverIndex] = useState<number | null>(null);
|
||||||
const [isOverflow, setIsOverflow] = useState(false);
|
const [isOverflow, setIsOverflow] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
|
const lockTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// обёртка для смены индекса
|
||||||
|
const setActiveIndex = (idx: number) => {
|
||||||
|
if (idx === activeIndex) return;
|
||||||
|
_setActiveIndex(idx);
|
||||||
|
setIsLocked(true);
|
||||||
|
if (lockTimer.current) clearTimeout(lockTimer.current);
|
||||||
|
lockTimer.current = window.setTimeout(() => {
|
||||||
|
setIsLocked(false);
|
||||||
|
}, 250); // чуть больше transition
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = scrollerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
let raf = 0;
|
||||||
|
const onScroll = () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
raf = requestAnimationFrame(() => {
|
||||||
|
if (isLocked) return; // 🔒 во время анимации не пересчитываем
|
||||||
|
setActiveIndex(indexFromScroll(el.scrollLeft));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
el.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
return () => el.removeEventListener("scroll", onScroll);
|
||||||
|
}, [denom, isLocked]);
|
||||||
|
|
||||||
|
|
||||||
/* ===== reserve-height measuring ===== */
|
/* ===== reserve-height measuring ===== */
|
||||||
const gridWrapRef = useRef<HTMLDivElement>(null);
|
const gridWrapRef = useRef<HTMLDivElement>(null);
|
||||||
const [gridMinHeight, setGridMinHeight] = useState(0);
|
const [gridMinHeight, setGridMinHeight] = useState(0);
|
||||||
|
|
@ -393,20 +452,6 @@ export default function KeyBusinessSegments() {
|
||||||
return () => window.removeEventListener("resize", onResize);
|
return () => window.removeEventListener("resize", onResize);
|
||||||
}, [defaultCenterIndex, N]);
|
}, [defaultCenterIndex, N]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = scrollerRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
let raf = 0;
|
|
||||||
const onScroll = () => {
|
|
||||||
cancelAnimationFrame(raf);
|
|
||||||
raf = requestAnimationFrame(() => {
|
|
||||||
setActiveIndex(indexFromScroll(el.scrollLeft));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
el.addEventListener("scroll", onScroll, { passive: true });
|
|
||||||
return () => el.removeEventListener("scroll", onScroll);
|
|
||||||
}, [denom]);
|
|
||||||
|
|
||||||
const realActiveIndex = hoverIndex ?? activeIndex;
|
const realActiveIndex = hoverIndex ?? activeIndex;
|
||||||
const activeSegment = segments[Math.min(Math.max(realActiveIndex, 0), N - 1)];
|
const activeSegment = segments[Math.min(Math.max(realActiveIndex, 0), N - 1)];
|
||||||
|
|
||||||
|
|
@ -471,9 +516,16 @@ export default function KeyBusinessSegments() {
|
||||||
{block.items.map((item) => (
|
{block.items.map((item) => (
|
||||||
<BulletItem key={item.id} $span={item.span}>
|
<BulletItem key={item.id} $span={item.span}>
|
||||||
<ItemRow>
|
<ItemRow>
|
||||||
<CheckDot
|
<img
|
||||||
|
src={item.infoLogo}
|
||||||
|
alt=""
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: item.infoLogo ? `url(${item.infoLogo})` : undefined,
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 4,
|
||||||
|
objectFit: "contain",
|
||||||
|
background: "#16a34a",
|
||||||
|
padding: 4,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text18_400_dom
|
<Text18_400_dom
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,7 @@ const Row = styled.div<{ $gap: number; $hidden?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${(p) => p.$gap}px;
|
gap: ${(p) => p.$gap}px;
|
||||||
visibility: ${(p) => (p.$hidden ? "hidden" : "visible")};
|
visibility: ${(p) => (p.$hidden ? "hidden" : "visible")};
|
||||||
padding: 0px 10px;
|
margin-left:10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Frame = styled.div`
|
const Frame = styled.div`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue