"use client"; import { ExternalLinkIcon, RefreshCwIcon, RotateCcwIcon, RotateCwIcon, XIcon, ZoomInIcon, ZoomOutIcon, } from "lucide-react"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import type { ReactZoomPanPinchContentRef } from "react-zoom-pan-pinch"; import { TransformComponent, TransformWrapper, } from "react-zoom-pan-pinch"; import { cn } from "@/lib/utils"; type ImageLightboxItem = { src: string; alt?: string; }; type ImageLightboxContextValue = { open: (src: string, alt?: string) => void; close: () => void; }; const ImageLightboxContext = createContext( null, ); const iconBtnClass = "inline-flex size-7 shrink-0 items-center justify-center rounded-md text-white outline-none transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-white/40"; export function useImageLightbox(): ImageLightboxContextValue { const ctx = useContext(ImageLightboxContext); if (!ctx) { throw new Error("useImageLightbox 必须在 ImageLightboxProvider 内使用"); } return ctx; } export function useImageLightboxOptional(): ImageLightboxContextValue | null { return useContext(ImageLightboxContext); } export type ImageLightboxProps = { open: boolean; onOpenChange: (open: boolean) => void; src: string | null; alt?: string; }; function canOpenInNewTab(url: string): boolean { if (!url) { return false; } if (url.startsWith("/")) { return true; } try { const parsed = new URL(url); return ( parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "blob:" ); } catch { return false; } } function LightboxImageBody({ src, alt, pinchRef, rotationDeg, }: { src: string; alt?: string; pinchRef: React.RefObject; rotationDeg: number; }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const showOpenTab = canOpenInNewTab(src); useEffect(() => { requestAnimationFrame(() => { pinchRef.current?.centerView(1, 0); }); }, [rotationDeg, pinchRef]); return (
{loading && !error ? (
) : null} {error ? (

图片加载失败

{showOpenTab ? ( 在新标签页打开 ) : null}
) : ( {/* eslint-disable-next-line @next/next/no-img-element -- 外链与任意尺寸大图预览 */} {alt { setLoading(false); setError(false); requestAnimationFrame(() => { pinchRef.current?.centerView(1, 0); }); }} onError={() => { setLoading(false); setError(true); }} /> )}
); } function ImageLightboxDialogContent({ src, alt, onRequestClose, }: { src: string; alt?: string; onRequestClose: () => void; }) { const pinchRef = useRef(null); const [rotationDeg, setRotationDeg] = useState(0); const showOpenTab = canOpenInNewTab(src); const titleText = alt?.trim() || "图片预览"; const rotateLeft = useCallback(() => { setRotationDeg((d) => (d - 90 + 360) % 360); }, []); const rotateRight = useCallback(() => { setRotationDeg((d) => (d + 90) % 360); }, []); return (

{titleText}

{showOpenTab ? ( ) : null}

使用滚轮或双指缩放,按住拖拽可平移图片;工具栏可向左或向右旋转。

); } export function ImageLightboxView({ open, onOpenChange, src, alt, }: ImageLightboxProps) { const dialogRef = useRef(null); useEffect(() => { const el = dialogRef.current; if (!el) { return; } if (open && src) { if (!el.open) { el.showModal(); } } else if (el.open) { el.close(); } }, [open, src]); return ( onOpenChange(false)} > {src ? ( onOpenChange(false)} /> ) : null} ); } export function ImageLightboxProvider({ children }: { children: ReactNode }) { const [state, setState] = useState(null); const open = useCallback((src: string, alt?: string) => { const trimmed = src?.trim(); if (!trimmed) { return; } setState({ src: trimmed, alt }); }, []); const close = useCallback(() => { setState(null); }, []); const contextValue = useMemo( () => ({ open, close, }), [open, close], ); return ( {children} { if (!next) { setState(null); } }} src={state?.src ?? null} alt={state?.alt} /> ); }