"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { variantURL } from "@/lib/responsiveImage"; /** 顶部横版槽位的最小门槛:自然宽度低于该值的横版图视为过小,不占顶部布局、归入左栏 */ const MIN_TOP_WIDTH = 600; /** 由自然宽高判定当前槽位是否该显示(与 measure() 内逻辑保持一致) */ function slotMatches( orientation: "portrait" | "landscape", w: number, h: number ): boolean { const landscape = w > h; // 仅"横版且足够大"才配得上顶部槽位;过小的横版图随竖版走左栏 const topWorthy = landscape && w >= MIN_TOP_WIDTH; return orientation === "landscape" ? topWorthy : !topWorthy; } /** * 封面自适应槽位:同一封面在两个位置各放一个槽位(左栏=竖版、顶部全宽=横版), * 组件按方向决定是否渲染自身内容。 * * 已知尺寸(naturalWidth/naturalHeight > 0)时同步判定,SSR 首帧即正确方向—— * 避免"先竖版占位、hydration 后探测再切横版"的闪烁与布局抖动。 * 尺寸未知(外链/存量未存尺寸)时回退到隐藏探测图运行时测量。 */ export default function AdaptiveCoverSlot({ src, orientation, naturalWidth, naturalHeight, children, }: { src: string; orientation: "portrait" | "landscape"; /** 封面自然宽;>0 时跳过探测直接判定 */ naturalWidth?: number; /** 封面自然高;>0 时跳过探测直接判定 */ naturalHeight?: number; children: ReactNode; }) { const known = typeof naturalWidth === "number" && naturalWidth > 0 && typeof naturalHeight === "number" && naturalHeight > 0; const ref = useRef(null); // 已知尺寸 → 同步初值;未知 → 仍默认竖版显示(书封多为竖版),探测后纠正 const [match, setMatch] = useState( known ? slotMatches(orientation, naturalWidth!, naturalHeight!) : orientation === "portrait" ); // 仅未知尺寸时需要探测 useEffect(() => { if (known) return; // 缓存图可能在 hydration 前就完成加载而错过 onLoad if (ref.current?.complete) { const img = ref.current; if (img.naturalWidth && img.naturalHeight) { setMatch(slotMatches(orientation, img.naturalWidth, img.naturalHeight)); } } }, [known, orientation]); // 已知尺寸时不渲染探测图:省一次请求,且 SSR 输出即稳定 if (known) { return <>{match ? children : null}; } return ( <> {/* 探测只需尺寸信息:取 768 档变体而非原图(600~767 宽的源后端透传原尺寸, ≥768 的源探测宽 768 仍 ≥MIN_TOP_WIDTH,横竖版与门槛判定均不失真) */} {/* eslint-disable-next-line @next/next/no-img-element */} { const img = e.currentTarget; if (img.naturalWidth && img.naturalHeight) { setMatch(slotMatches(orientation, img.naturalWidth, img.naturalHeight)); } }} /> {match ? children : null} ); }