Files
jiang13-bbs/frontend/components/AdaptiveCoverSlot.tsx
freefire ff2ab286fb feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

95 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<HTMLImageElement>(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 */}
<img
ref={ref}
src={variantURL(src, 768)}
alt=""
aria-hidden
className="hidden"
onLoad={(e) => {
const img = e.currentTarget;
if (img.naturalWidth && img.naturalHeight) {
setMatch(slotMatches(orientation, img.naturalWidth, img.naturalHeight));
}
}}
/>
{match ? children : null}
</>
);
}