'use client'
// LEGACY_READ_ONLY: 旧域名仅用于展示历史审核素材。
import Image from 'next/image'
import { useEffect, useMemo, useState } from 'react'
import type {
WorkReviewProject,
WorkReviewReview,
WorkReviewRole,
WorkReviewVersion,
} from '@/types/work-review'
import { WorkReviewImageGallery } from './WorkReviewImageGallery'
import type { WorkReviewDisplayAnnotation } from './WorkReviewAnnotationMarker'
import { WorkReviewDecisionBadge, WorkReviewStatusBadge } from './WorkReviewStatusBadge'
import { canResubmitWorkReviewProject } from '@/lib/work-review-policy'
function formatDateOnly(value: string) {
const raw = String(value || '')
const datePart = raw.match(/^\d{4}-\d{2}-\d{2}/)?.[0]
return datePart || '-'
}
function formatDateTime(value?: string | null) {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '-'
return new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(date)
}
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 getVersionReview(version: WorkReviewVersion, role: WorkReviewRole) {
return version.reviews.find((review) => review.reviewerRole === role) || null
}
function getRequiredReviewRoles(project: WorkReviewProject): WorkReviewRole[] {
return project.requiredReviewRoles?.length
? project.requiredReviewRoles
: ['manager', 'final_reviewer']
}
function getVersionAnnotations(
version: WorkReviewVersion,
requiredRoles: WorkReviewRole[],
): WorkReviewDisplayAnnotation[][] {
const imageCount = version.imageGroups?.length || version.imageUrls?.length || 0
const result = Array.from({ length: imageCount }, () => [] as WorkReviewDisplayAnnotation[])
for (const review of version.reviews || []) {
if (!requiredRoles.includes(review.reviewerRole)) continue
for (const imageComment of review.imageComments || []) {
if (!result[imageComment.imageIndex]) continue
result[imageComment.imageIndex].push(...(imageComment.annotations || []).map((annotation, index) => ({
annotation,
markerNumber: index + 1,
reviewerName: review.reviewerName,
reviewerRole: review.reviewerRole,
})))
}
}
return result
}
function ReviewProgress({
project,
version,
}: {
project: WorkReviewProject
version: WorkReviewVersion
}) {
const requiredRoles = getRequiredReviewRoles(project)
const allRoles: Array<{ role: WorkReviewRole; label: string; name: string }> = [
{ role: 'manager', label: '评审人', name: project.manager.name },
{ role: 'final_reviewer', label: '元泽', name: project.finalReviewer.name },
]
const roles = allRoles.filter((item) => requiredRoles.includes(item.role))
return (
{roles.map((item) => {
const review = getVersionReview(version, item.role)
return (
{item.label}{review ? '已评' : '待评'}
)
})}
)
}
export function WorkReviewProjectCard({
project,
expanded,
canDelete,
deleteBusy,
onToggle,
onDelete,
onResubmit,
onReview,
}: {
project: WorkReviewProject
expanded: boolean
canDelete: boolean
deleteBusy?: boolean
onToggle: () => void
onDelete: () => void
onResubmit: () => void
onReview: () => void
}) {
const latestVersion = project.versions[0]
const [selectedVersionId, setSelectedVersionId] = useState(latestVersion?.id || '')
useEffect(() => {
if (!project.versions.some((version) => version.id === selectedVersionId)) {
setSelectedVersionId(project.versions[0]?.id || '')
}
}, [project.versions, selectedVersionId])
const selectedVersion = useMemo(
() => project.versions.find((version) => version.id === selectedVersionId) || latestVersion,
[latestVersion, project.versions, selectedVersionId],
)
const cover = latestVersion?.imageGroups?.[0]?.previewUrl || latestVersion?.imageUrls?.[0] || ''
const isLatestSelected = selectedVersion?.id === latestVersion?.id
const canResubmitLatestVersion = canResubmitWorkReviewProject(project)
const requiredReviewRoles = getRequiredReviewRoles(project)
const singleReviewerTest = project.reviewMode === 'single_reviewer_test'
const annotationsByImageIndex = useMemo(
() => selectedVersion ? getVersionAnnotations(selectedVersion, requiredReviewRoles) : [],
[requiredReviewRoles, selectedVersion],
)
if (!latestVersion || !selectedVersion) return null
return (
{canDelete && (
)}
{expanded && (
V{selectedVersion.version} 作品内容
{!isLatestSelected && 历史版本 · 只读}
提交于 {formatDateTime(selectedVersion.submittedAt)}
{selectedVersion.completedAt ? ` · 评审完成于 ${formatDateTime(selectedVersion.completedAt)}` : ''}
{isLatestSelected && canResubmitLatestVersion && (
)}
{project.description && (
作品说明
{project.description}
)}
{selectedVersion.submissionNote && (
本次提交说明
{selectedVersion.submissionNote}
)}
0 ? 'xl:grid-cols-[minmax(0,2fr)_minmax(240px,1fr)]' : ''}`}>
{(selectedVersion.referenceImageGroups || []).length > 0 && (
)}
{singleReviewerTest ? '评审意见' : '双评审意见'}
{requiredReviewRoles.includes('manager') && (
)}
{requiredReviewRoles.includes('final_reviewer') && (
)}
)}
)
}
function ReviewPanel({
label,
reviewerName,
review,
canReview,
onReview,
}: {
label: string
reviewerName: string
review: WorkReviewReview | null
canReview: boolean
onReview: () => void
}) {
return (
{label}
{review?.reviewerName || reviewerName}
{review ? (
) : (
待评审
)}
{review ? (
{(review.imageComments || []).map((item) => (
图 {item.imageIndex + 1}{item.sourceName ? ` · ${item.sourceName}` : ''}
{item.comment}
))}
{review.generalComment && (
总体意见
{review.generalComment}
)}
提交于 {formatDateTime(review.reviewedAt)}
) : (
{canReview ? '当前版本等待你填写逐图评审建议' : '等待评审人提交'}
{canReview && (
)}
)}
)
}