'use client' // LEGACY_READ_ONLY: 旧域名仅用于展示历史审核素材。 import Image from 'next/image' import { useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import type { WorkReviewImageGroup } from '@/types/work-review' import { WORK_REVIEW_IMAGE_LABELS } from '@/types/work-review' import { WorkReviewAnnotationMarker, type WorkReviewDisplayAnnotation, } from './WorkReviewAnnotationMarker' const PROXY_HOSTS = new Set(['s3.jiansun.vip', 's3.yydd.ai', 's3.1080p.hk']) function normalizeImageUrl(value: string) { const url = String(value || '').trim() if (!url || !/^https?:\/\//i.test(url)) return url try { const parsed = new URL(url) return PROXY_HOSTS.has(parsed.hostname.toLowerCase()) ? `/api/proxy-media?url=${encodeURIComponent(url)}` : url } catch { return url } } function normalizeGroups(groups: WorkReviewImageGroup[], imageUrls: string[]): WorkReviewImageGroup[] { if (groups.length > 0) { return groups.map((group, index) => ({ ...group, sourceName: group.sourceName || `作品图片 ${index + 1}`, previewUrl: normalizeImageUrl(group.previewUrl || group.slices?.[0]?.url || ''), slices: (group.slices || []).map((slice) => ({ ...slice, url: normalizeImageUrl(slice.url), })), })).filter((group) => Boolean(group.previewUrl)) } return imageUrls.map((url, index) => ({ sourceName: `作品图片 ${index + 1}`, previewUrl: normalizeImageUrl(url), previewWidth: 1200, previewHeight: 900, width: 1200, height: 900, slices: [{ url: normalizeImageUrl(url), width: 1200, height: 900 }], })) } export function WorkReviewImageGallery({ groups, imageUrls = [], compact = false, annotationsByImageIndex = [], }: { groups: WorkReviewImageGroup[] imageUrls?: string[] compact?: boolean annotationsByImageIndex?: WorkReviewDisplayAnnotation[][] }) { const [activeIndex, setActiveIndex] = useState(null) const displayGroups = useMemo(() => normalizeGroups(groups || [], imageUrls || []), [groups, imageUrls]) if (displayGroups.length === 0) { return (
暂无作品图片
) } return ( <>
{displayGroups.map((group, index) => ( ))}
setActiveIndex(null)} onPrevious={() => setActiveIndex((current) => ( current == null ? null : (current - 1 + displayGroups.length) % displayGroups.length ))} onNext={() => setActiveIndex((current) => ( current == null ? null : (current + 1) % displayGroups.length ))} /> ) } function WorkReviewImageViewer({ group, index, total, annotations, onClose, onPrevious, onNext, }: { group: WorkReviewImageGroup | null index: number | null total: number annotations: WorkReviewDisplayAnnotation[] onClose: () => void onPrevious: () => void onNext: () => void }) { const [portalReady, setPortalReady] = useState(false) const dialogRef = useRef(null) const closeRef = useRef(null) useEffect(() => setPortalReady(true), []) useEffect(() => { if (!group) return const previousOverflow = document.body.style.overflow const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null document.body.style.overflow = 'hidden' const frame = window.requestAnimationFrame(() => closeRef.current?.focus()) const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault() onClose() } else if (event.key === 'ArrowLeft' && total > 1) { event.preventDefault() onPrevious() } else if (event.key === 'ArrowRight' && total > 1) { event.preventDefault() onNext() } else if (event.key === 'Tab') { const buttons = Array.from(dialogRef.current?.querySelectorAll('button:not([disabled])') || []) if (buttons.length === 0) return const first = buttons[0] const last = buttons[buttons.length - 1] if (event.shiftKey && document.activeElement === first) { event.preventDefault() last.focus() } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault() first.focus() } } } window.addEventListener('keydown', onKeyDown) return () => { window.cancelAnimationFrame(frame) window.removeEventListener('keydown', onKeyDown) document.body.style.overflow = previousOverflow previousFocus?.focus() } }, [group, onClose, onNext, onPrevious, total]) if (!group || index == null || !portalReady) return null return createPortal(
event.stopPropagation()}>
{group.sourceName}
{index + 1} / {total}
event.stopPropagation()} > {total > 1 && ( <> )}
{(group.slices.length > 0 ? group.slices : [{ url: group.previewUrl, width: group.previewWidth, height: group.previewHeight, }]).map((slice, sliceIndex) => ( {`${group.sourceName}${sliceIndex ))} {annotations.map((item) => ( ))}
, document.body, ) }