'use client' import Image from 'next/image' import { type ReactNode, useEffect, useId, useMemo, useRef, useState, } from 'react' import { createPortal } from 'react-dom' import { uploadWorkReviewImages } from '@/lib/work-review-client' import type { WorkReviewDecision, WorkReviewProject, WorkReviewReviewInput, WorkReviewRole, WorkReviewSubmissionInput, WorkReviewVersion, } from '@/types/work-review' import { WorkReviewImageDropzone } from './WorkReviewImageDropzone' function todayValue() { const now = new Date() const local = new Date(now.getTime() - now.getTimezoneOffset() * 60_000) return local.toISOString().slice(0, 10) } function proxyImageUrl(value: string) { if (!/^https?:\/\//i.test(value)) return value try { const parsed = new URL(value) if (['s3.jiansun.vip', 's3.yydd.ai', 's3.1080p.hk'].includes(parsed.hostname.toLowerCase())) { return `/api/proxy-media?url=${encodeURIComponent(value)}` } } catch { // 保留原地址。 } return value } function WorkReviewDialogShell({ title, subtitle, busy, onClose, children, footer, widthClass = 'max-w-2xl', }: { title: string subtitle?: string busy: boolean onClose: () => void children: ReactNode footer: ReactNode widthClass?: string }) { const [ready, setReady] = useState(false) const titleId = useId() const dialogRef = useRef(null) const closeRef = useRef(null) useEffect(() => setReady(true), []) useEffect(() => { if (!ready) 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 handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape' && !busy) { event.preventDefault() onClose() return } if (event.key !== 'Tab') return const elements = Array.from(dialogRef.current?.querySelectorAll( 'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])', ) || []) if (elements.length === 0) return const first = elements[0] const last = elements[elements.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', handleKeyDown) return () => { window.cancelAnimationFrame(frame) window.removeEventListener('keydown', handleKeyDown) document.body.style.overflow = previousOverflow previousFocus?.focus() } }, [busy, onClose, ready]) if (!ready) return null return createPortal(
{ if (!busy && event.target === event.currentTarget) onClose() }} >

{title}

{subtitle &&

{subtitle}

}
{children}
{footer}
, document.body, ) } export function WorkReviewSubmissionDialog({ mode, project, onClose, onSubmit, }: { mode: 'create' | 'resubmit' project?: WorkReviewProject | null onClose: () => void onSubmit: (input: WorkReviewSubmissionInput) => Promise }) { const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [workDate, setWorkDate] = useState(todayValue) const [submissionNote, setSubmissionNote] = useState('') const [files, setFiles] = useState([]) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const [progress, setProgress] = useState<{ message: string; percent?: number } | null>(null) const isCreate = mode === 'create' const submit = async () => { if (isCreate && !title.trim()) { setError('请填写作品名称') return } if (isCreate && title.trim().length > 120) { setError('作品名称不能超过 120 个字') return } if (isCreate && !workDate) { setError('请选择作品日期') return } if (files.length === 0) { setError('请至少选择 1 张作品图片') return } setBusy(true) setError(null) setProgress({ message: '正在准备图片' }) try { const processedImages = await uploadWorkReviewImages(files, (message, percent) => { setProgress({ message, percent }) }) await onSubmit({ ...(isCreate ? { title: title.trim(), description: description.trim(), workDate, } : {}), submissionNote: submissionNote.trim(), processedImages, }) } catch (submitError) { setError(submitError instanceof Error ? submitError.message : '提交失败,请重试') } finally { setBusy(false) } } return ( {progress && (
{progress.message} {typeof progress.percent === 'number' && {progress.percent}%}
)}
)} >
{isCreate ? ( <>