import { useCallback, useEffect, useState } from 'react'; import { Link, useParams, useSearchParams } from 'react-router-dom'; import { ArrowLeft, ArrowRight, Check, KeyRound, MessageSquareText, Send } from 'lucide-react'; import type { CustomerAccessState, Note, NoteDetail, Project, WorkCollection } from '@shared/types'; import { api, ApiError } from '@/api/client'; import AnnotatableImage from '@/components/AnnotatableImage'; import AnnotatableText from '@/components/AnnotatableText'; import StatusBadge from '@/components/StatusBadge'; import CollectionStatusBadge from '@/components/CollectionStatusBadge'; type ProjectPayload = { project: Pick; collections: WorkCollection[]; reviewer_name: string }; export default function CustomerReviewPage() { const { slug = '', collectionId, noteId } = useParams(); const [search] = useSearchParams(); const selectedVersion = search.get('version'); const [access, setAccess] = useState(null); const [projectData, setProjectData] = useState(null); const [collectionData, setCollectionData] = useState<{ collection: WorkCollection; works: Note[] } | null>(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) { const result = await api.getCustomerWork(slug, Number(noteId), selectedVersion ? Number(selectedVersion) : undefined); setWork({ ...result, text_annotations: result.text_annotations ?? [] }); } else if (collectionId) setCollectionData(await api.getCustomerCollection(slug, Number(collectionId))); else setProjectData(await api.getCustomerProject(slug)); } catch (reason) { setError(reason instanceof ApiError ? reason.message : '页面加载失败'); } }, [slug, collectionId, noteId, selectedVersion]); 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 (!access && !error) return ; if (!access) return ; if (!access.enabled || access.expired) return ; if (!access.authenticated) return ; if (noteId) return work ? : ; if (collectionId) return collectionData ? : ; return projectData ? : ; } function AccessGate({ access, name, password, error, busy, setName, setPassword, submit }: { access: CustomerAccessState; name: string; password: string; error: string; busy: boolean; setName: (v: string) => void; setPassword: (v: string) => void; submit: (e: React.FormEvent) => void }) { return

Private review

{access.project_name}

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

{error&&

{error}

}

身份将在此设备保留 7 天

; } function ProjectReview({ slug, data }: { slug: string; data: ProjectPayload }) { return

{data.project.client_description}

{data.collections.map((item, index)=>
{String(index+1).padStart(2,'0')}

{item.name}

{item.client_description || '作品交付集验收'}

{item.approved_count}/{item.work_count} 已通过
)}
; } function CollectionReview({ slug, data }: { slug: string; data: { collection: WorkCollection; works: Note[] } }) { return
返回项目
{data.collection.status==='completed'&&
本作品交付集已全部通过验收,当前内容为只读状态。
}
{data.works.map((item)=>

{item.title}

)}
; } function WorkReview({ slug, work, reviewer, reload }: { slug: string; work: NoteDetail; reviewer: string; reload: () => Promise }) { const [comment,setComment]=useState(''); const [reason,setReason]=useState(''); const [busy,setBusy]=useState(false); const readOnly=work.collection.status==='completed'; const send=async()=>{if(!comment.trim())return;setBusy(true);await api.addCustomerComment(slug,work.id,comment.trim());setComment('');await reload();setBusy(false)}; const decide=async(decision:'approved'|'changes_requested')=>{if(decision==='changes_requested'&&!reason.trim())return;if(decision==='approved'&&!window.confirm('确认通过这个版本吗?通过后将记录你的验收决定。'))return;setBusy(true);await api.submitCustomerDecision(slug,work.id,decision,reason.trim());setReason('');await reload();setBusy(false)}; return
{work.collection.name}
{work.versions.map((item)=>V{item.version_number})}
{readOnly&&
验收已经完成。你仍可查看全部作品与历史批注,但不能继续添加反馈。
}

{work.project.name} / {work.collection.name} / WORK {String(work.id).padStart(3,'0')}

{work.images.map((img)=>{await api.addCustomerAnnotation(slug,img.id,{x,y,content});await reload()}}/>)}
annotation.target==='title')} readOnly={readOnly} onAdd={async(content)=>{await api.addCustomerTextAnnotation(slug,work.id,{version_number:work.version_number,target:'title',content});await reload()}}>

{work.title}

{work.description&&
annotation.target==='description')} readOnly={readOnly} onAdd={async(content)=>{await api.addCustomerTextAnnotation(slug,work.id,{version_number:work.version_number,target:'description',content});await reload()}}>

{work.description}

}{work.tags.length>0&&

{work.tags.join(' ')}

}