"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { EmptyState } from "@/components/ui/empty-state"; import { MetricChip } from "@/components/ui/metric-chip"; import { StatusBadge } from "@/components/ui/status-badge"; import { ReviewActionPanel } from "@/features/reviews/components/review-action-panel"; import { ReviewImagePanel } from "@/features/reviews/components/review-image-panel"; import { ReviewRevisionPanel } from "@/features/reviews/components/review-revision-panel"; import { ReviewWorkflowSummary } from "@/features/reviews/components/review-workflow-summary"; import type { ReviewDecision } from "@/lib/types/backend"; import type { AssetViewModel, OrderDetailVM, ReviewSubmissionVM, RevisionRegistrationVM, WorkflowDetailVM, } from "@/lib/types/view-models"; type ApiEnvelope = { data?: T; message?: string; }; type ReviewWorkbenchDetailScreenProps = { orderId: number; }; const REVIEWER_ID = 1; function isRerunDecision(decision: ReviewDecision) { return ( decision === "rerun_scene" || decision === "rerun_face" || decision === "rerun_fusion" ); } function getPreferredAsset(order: OrderDetailVM) { return order.finalAsset ?? order.assets[0] ?? null; } async function parseEnvelope(response: Response): Promise> { return (await response.json()) as ApiEnvelope; } function formatTimestamp(timestamp: string) { return timestamp.replace("T", " ").replace("Z", " UTC"); } export function ReviewWorkbenchDetailScreen({ orderId, }: ReviewWorkbenchDetailScreenProps) { const router = useRouter(); const [orderDetail, setOrderDetail] = useState(null); const [workflowDetail, setWorkflowDetail] = useState( null, ); const [selectedAssetId, setSelectedAssetId] = useState(null); const [contextError, setContextError] = useState(null); const [submissionError, setSubmissionError] = useState(null); const [submissionResult, setSubmissionResult] = useState(null); const [revisionError, setRevisionError] = useState(null); const [revisionResult, setRevisionResult] = useState(null); const [isLoadingContext, setIsLoadingContext] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { let active = true; async function loadReviewContext() { setIsLoadingContext(true); try { const [orderResponse, workflowResponse] = await Promise.all([ fetch(`/api/orders/${orderId}`), fetch(`/api/workflows/${orderId}`), ]); const [orderPayload, workflowPayload] = await Promise.all([ parseEnvelope(orderResponse), parseEnvelope(workflowResponse), ]); const nextOrder = orderPayload.data; const nextWorkflow = workflowPayload.data; if ( !orderResponse.ok || !workflowResponse.ok || !nextOrder || !nextWorkflow ) { throw new Error("CONTEXT_LOAD_FAILED"); } if (!active) { return; } const preferredAsset = getPreferredAsset(nextOrder); setOrderDetail(nextOrder); setWorkflowDetail(nextWorkflow); setContextError(null); setSelectedAssetId((current) => { if ( current && [ ...(nextOrder.finalAsset ? [nextOrder.finalAsset] : []), ...nextOrder.assets, ].some((asset) => asset.id === current) ) { return current; } return preferredAsset?.id ?? null; }); } catch { if (!active) { return; } setOrderDetail(null); setWorkflowDetail(null); setSelectedAssetId(null); setContextError("订单详情或流程摘要加载失败,请稍后重试。"); } finally { if (active) { setIsLoadingContext(false); } } } void loadReviewContext(); return () => { active = false; }; }, [orderId]); const selectedAsset: AssetViewModel | null = (orderDetail?.finalAsset?.id === selectedAssetId ? orderDetail.finalAsset : orderDetail?.assets.find((asset) => asset.id === selectedAssetId)) ?? orderDetail?.finalAsset ?? orderDetail?.assets[0] ?? null; const handleSubmit = async (decision: ReviewDecision, comment: string) => { if (!orderDetail) { return; } if (isRerunDecision(decision) && !comment.trim()) { setSubmissionError("请填写审核备注"); setSubmissionResult(null); return; } setIsSubmitting(true); setSubmissionError(null); setSubmissionResult(null); try { const response = await fetch(`/api/reviews/${orderDetail.orderId}/submit`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ decision, reviewer_id: REVIEWER_ID, selected_asset_id: selectedAsset?.id ?? null, comment: comment.trim() ? comment : null, }), }); const payload = await parseEnvelope(response); if (!response.ok || !payload.data) { setSubmissionError(payload.message ?? "审核动作提交失败,请稍后重试。"); return; } setSubmissionResult(payload.data); router.push("/reviews/workbench"); } catch { setSubmissionError("审核动作提交失败,请检查网络后重试。"); } finally { setIsSubmitting(false); } }; const handleRegisterRevision = async (payload: { uploadedUri: string; comment: string; }) => { if (!orderDetail || !selectedAsset) { return; } if (!payload.uploadedUri.trim()) { setRevisionError("请填写修订稿 URI"); setRevisionResult(null); return; } setIsSubmitting(true); setRevisionError(null); setRevisionResult(null); setSubmissionResult(null); try { const response = await fetch(`/api/orders/${orderDetail.orderId}/revisions`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ parent_asset_id: selectedAsset.id, uploaded_uri: payload.uploadedUri.trim(), reviewer_id: REVIEWER_ID, comment: payload.comment.trim() ? payload.comment : null, }), }); const revisionPayload = await parseEnvelope(response); if (!response.ok || !revisionPayload.data) { setRevisionError(revisionPayload.message ?? "人工修订稿登记失败,请稍后重试。"); return; } const nextRevision = revisionPayload.data; setRevisionResult(nextRevision); setOrderDetail((current) => current ? { ...current, currentRevisionAssetId: nextRevision.assetId, currentRevisionVersion: nextRevision.versionNo, latestRevisionAssetId: nextRevision.latestRevisionAssetId, latestRevisionVersion: nextRevision.versionNo, revisionCount: nextRevision.revisionCount, reviewTaskStatus: nextRevision.reviewTaskStatus, pendingManualConfirm: true, } : current, ); setWorkflowDetail((current) => current ? { ...current, currentRevisionAssetId: nextRevision.assetId, currentRevisionVersion: nextRevision.versionNo, latestRevisionAssetId: nextRevision.latestRevisionAssetId, latestRevisionVersion: nextRevision.versionNo, revisionCount: nextRevision.revisionCount, reviewTaskStatus: nextRevision.reviewTaskStatus, pendingManualConfirm: true, } : current, ); } catch { setRevisionError("人工修订稿登记失败,请检查网络后重试。"); } finally { setIsSubmitting(false); } }; const handleConfirmRevision = async (comment: string) => { if (!orderDetail) { return; } setIsSubmitting(true); setRevisionError(null); setSubmissionError(null); setSubmissionResult(null); try { const response = await fetch( `/api/reviews/${orderDetail.orderId}/confirm-revision`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ reviewer_id: REVIEWER_ID, comment: comment.trim() ? comment : null, }), }, ); const payload = await parseEnvelope(response); if (!response.ok || !payload.data) { setRevisionError(payload.message ?? "确认修订失败,请稍后重试。"); return; } setSubmissionResult(payload.data); router.push("/reviews/workbench"); } catch { setRevisionError("确认修订失败,请检查网络后重试。"); } finally { setIsSubmitting(false); } }; if (isLoadingContext) { return (
正在加载审核详情…
); } if (contextError || !orderDetail || !workflowDetail) { return ( 返回审核列表 } /> ); } return (

Review detail

订单 #{orderDetail.orderId}

更新于 {formatTimestamp(orderDetail.updatedAt)}

返回审核列表
); }