import { useCallback, useEffect, useState } from 'react'; import { Link, Navigate, useParams, useSearchParams } from 'react-router-dom'; import { ArrowLeft, ArrowRight, Check, KeyRound } from 'lucide-react'; import type { CustomerAccessState, Note, NoteDetail, Project } from '@shared/types'; import { api, ApiError } from '@/api/client'; import AnnotatableImage from '@/components/AnnotatableImage'; import AnnotatableText, { type TextSelectionDraft } from '@/components/AnnotatableText'; import CollaborationDrawer, { type DrawerTextDraft } from '@/components/CollaborationDrawer'; import ImageReviewModal from '@/components/ImageReviewModal'; import StatusBadge from '@/components/StatusBadge'; type ProjectPayload = { project: Pick; works: Note[]; reviewer_name: string }; export default function CustomerReviewPage() { const { slug = '', collectionId, noteId } = useParams(); const [search] = useSearchParams(); const selectedRound = search.get('round') ? Number(search.get('round')) : undefined; const [access, setAccess] = useState(null); const [projectData, setProjectData] = useState(null); const [work, setWork] = useState(null); const [name, setName] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const load = useCallback(async () => { setError(''); try { const state = await api.getCustomerAccess(slug); setAccess(state); if (!state.authenticated) return; if (noteId) setWork(await api.getCustomerWorkRound(slug, Number(noteId), selectedRound)); else setProjectData(await api.getCustomerProject(slug)); } catch (reason) { setError(reason instanceof ApiError ? reason.message : '页面加载失败'); } }, [slug, noteId, selectedRound]); useEffect(() => { void load(); }, [load]); const login = async (event: React.FormEvent) => { event.preventDefault(); setBusy(true); setError(''); try { await api.customerLogin(slug, { reviewer_name: name, password }); await load(); } catch (reason) { setError(reason instanceof ApiError ? reason.message : '验证失败'); } finally { setBusy(false); } }; if (collectionId) return ; if (!access && !error) return ; if (!access) return ; if (!access.enabled || access.expired) return ; if (!access.authenticated) return ; if (noteId) return work ? : ; return projectData ? : ; } function AccessGate({ access, name, password, error, busy, setName, setPassword, submit }: { access: CustomerAccessState; name: string; password: string; error: string; busy: boolean; setName: (value: string) => void; setPassword: (value: string) => void; submit: (event: React.FormEvent) => void }) { return

Private review

{access.project_name}

{access.client_description || '请输入姓名与项目密码,进入作品验收。'}

{error &&

{error}

}
; } function ProjectReview({ slug, data }: { slug: string; data: ProjectPayload }) { const counts = { pending: data.works.filter((item) => item.review_status === 'pending').length, changes: data.works.filter((item) => item.review_status === 'changes_requested').length, approved: data.works.filter((item) => item.review_status === 'approved').length }; return

{data.project.client_description}

{data.project.review_status === 'completed' ? '验收完毕' : data.project.review_status === 'reviewing' ? '验收中' : '待提交'}
{data.project.review_status === 'completed' &&
本项目已全部通过验收,当前内容为只读状态。
}
{data.works.map((item) =>

{item.title}

第 {item.version_number} 轮

)}
; } function WorkReview({ slug, work, reviewer, reload }: { slug: string; work: NoteDetail; reviewer: string; reload: () => Promise }) { const [drawerOpen, setDrawerOpen] = useState(false); const [reason, setReason] = useState(''); const [busy, setBusy] = useState(false); const [actionError, setActionError] = useState(''); const [selectedImage, setSelectedImage] = useState<{ imageId: number } | null>(null); const [focusImageId, setFocusImageId] = useState(); const [textDraft, setTextDraft] = useState<(DrawerTextDraft & { target: 'title' | 'description' | 'tags' }) | null>(null); const [focusFeedback, setFocusFeedback] = useState<{ type: 'text_annotation' | 'image_annotation'; id: number } | null>(null); const viewed = work.rounds.find((round) => round.version_number === work.version_number); const current = Boolean(viewed && Number(viewed.review_round_id) === Number(work.active_round_id)); const readOnly = work.project.status !== 'active' || work.project.review_status === 'completed' || !current || viewed?.round_status !== 'reviewing'; const tagsText = work.tags.join(' '); const selectText = (target: 'title' | 'description' | 'tags', label: string, selection: TextSelectionDraft) => { setTextDraft({ target, label, ...selection }); setFocusImageId(undefined); setFocusFeedback(null); setDrawerOpen(true); }; const openTextAnnotation = (annotationId: number) => { setTextDraft(null); setFocusImageId(undefined); setFocusFeedback({ type: 'text_annotation', id: annotationId }); setDrawerOpen(true); }; const openImage = (imageId: number) => { setSelectedImage({ imageId }); setFocusImageId(imageId); setTextDraft(null); setFocusFeedback(null); }; const openImageAnnotation = (imageId: number, annotationId: number) => { setFocusImageId(imageId); setTextDraft(null); setFocusFeedback({ type: 'image_annotation', id: annotationId }); setDrawerOpen(true); }; const openFeedback = work.images.flatMap((image) => image.annotations).filter((item) => item.status === 'open').length + work.text_annotations.filter((item) => item.status === 'open').length + work.comments.filter((item) => item.status === 'open').length; const decide = async (decision: 'approved' | 'changes_requested') => { if (!viewed || decision === 'changes_requested' && !reason.trim()) return; if (decision === 'approved' && !window.confirm(openFeedback ? `当前还有 ${openFeedback} 条未处理反馈。确认通过并将其标记为“随本轮通过关闭”吗?` : '确认通过当前轮次吗?')) return; setBusy(true); setActionError(''); try { await api.submitCustomerRoundDecision(slug, work.id, viewed.round_number, decision, reason.trim()); setReason(''); await reload(); } catch (error) { setActionError(error instanceof Error ? error.message : '验收提交失败'); } finally { setBusy(false); } }; const footer = !readOnly ?