feat(review): 重构项目级单方案验收协作
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Annotation, AuditLogEntry, CurrentUser, CustomerAccessState, ManagedApiKey, ManagedUser, Note, NoteDetail, NoteListQuery, OperationGroup, Project, ReviewStatus, StorageConfig, TextAnnotation, UserRole, WorkCollection, WorkComment } from '@shared/types';
|
||||
import type { Annotation, AuditLogEntry, CurrentUser, CustomerAccessState, FeedbackReply, FeedbackType, ManagedApiKey, ManagedUser, Note, NoteDetail, NoteListQuery, OperationGroup, Project, ReviewStatus, StorageConfig, TextAnnotation, UserRole, WorkComment, WorkFeedbackBundle } from '@shared/types';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public status: number, message: string) { super(message); this.name = 'ApiError'; }
|
||||
@@ -44,49 +44,47 @@ export const api = {
|
||||
createProject: (data: { name: string; slug: string; client_description: string; groupId?: number }) => request<Project>('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
updateProject: (id: number, data: { name: string; client_description: string }) => request<Project>(`/api/projects/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
updateCustomerAccess: (id: number, data: { enabled: boolean; password?: string; expires_at?: string | null }) => request<Project>(`/api/projects/${id}/customer-access`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
listCollections: (projectId: number) => request<WorkCollection[]>(`/api/projects/${projectId}/collections`),
|
||||
createCollection: (projectId: number, data: { name: string; client_description: string }) => request<WorkCollection>(`/api/projects/${projectId}/collections`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
updateCollection: (projectId: number, id: number, data: { name: string; client_description: string }) => request<WorkCollection>(`/api/projects/${projectId}/collections/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
listNotes: (query: NoteListQuery = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(query).forEach(([key, value]) => value != null && params.set(key, String(value)));
|
||||
return request<Note[]>(`/api/notes?${params}`);
|
||||
},
|
||||
getNote: (id: number, version?: number) => request<NoteDetail>(`/api/notes/${id}${version ? `?version=${version}` : ''}`),
|
||||
createNote: (payload: { collectionId: number; title: string; description: string; tags: string[]; images: File[] }) => {
|
||||
const form = new FormData();
|
||||
form.append('collectionId', String(payload.collectionId));
|
||||
form.append('title', payload.title);
|
||||
form.append('description', payload.description);
|
||||
form.append('tags', payload.tags.join(','));
|
||||
payload.images.forEach((file) => form.append('images', file));
|
||||
return request<Note>('/api/notes', { method: 'POST', body: form });
|
||||
listProjectWorks: (projectId: number, query: NoteListQuery = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(query).forEach(([key, value]) => value != null && params.set(key, String(value)));
|
||||
return request<Note[]>(`/api/projects/${projectId}/works?${params}`);
|
||||
},
|
||||
createWorkVersion: (noteId: number, payload: { title: string; description: string; tags: string[]; images: File[] }) => {
|
||||
getWork: (id: number, round?: number) => request<NoteDetail>(`/api/works/${id}${round ? `?round=${round}` : ''}`),
|
||||
getWorkFeedback: (id: number) => request<WorkFeedbackBundle>(`/api/works/${id}/annotations`),
|
||||
createWork: (projectId: number, payload: { title: string; description: string; tags: string[]; images: File[] }) => {
|
||||
const form = new FormData();
|
||||
form.append('title', payload.title); form.append('description', payload.description); form.append('tags', payload.tags.join(','));
|
||||
payload.images.forEach((file) => form.append('images', file));
|
||||
return request<Note>(`/api/notes/${noteId}/versions`, { method: 'POST', body: form });
|
||||
return request<Note>(`/api/projects/${projectId}/works`, { method: 'POST', body: form });
|
||||
},
|
||||
createReviewRound: (noteId: number, candidates: Array<{ candidate_name: string; title: string; description: string; tags: string[]; images: File[] }>) => {
|
||||
createRound: (workId: number, payload: { title: string; description: string; tags: string[]; images: File[] }) => {
|
||||
const form = new FormData();
|
||||
form.append('candidates', JSON.stringify(candidates.map((candidate) => ({ ...candidate, images: undefined, image_count: candidate.images.length }))));
|
||||
candidates.forEach((candidate) => candidate.images.forEach((file) => form.append('images', file)));
|
||||
return request<Note>(`/api/notes/${noteId}/review-rounds`, { method: 'POST', body: form });
|
||||
form.append('title', payload.title); form.append('description', payload.description); form.append('tags', payload.tags.join(','));
|
||||
payload.images.forEach((file) => form.append('images', file));
|
||||
return request<Note>(`/api/works/${workId}/rounds`, { method: 'POST', body: form });
|
||||
},
|
||||
setReviewStatus: (id: number, status: ReviewStatus) => request<{ success: true; status: ReviewStatus }>(`/api/notes/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }),
|
||||
reopenWork: (id: number, reason: string) => request<{ success: true; status: ReviewStatus }>(`/api/notes/${id}/reopen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason }) }),
|
||||
addAnnotation: (imageId: number, data: { x: number; y: number; content: string; author_name?: string }) => request<Annotation>(`/api/images/${imageId}/annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
addTextAnnotation: (noteId: number, data: { version_number: number; target: 'title' | 'description'; content: string }) => request<TextAnnotation>(`/api/notes/${noteId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
addTextSelectionAnnotation: (workId: number, data: { round_number: number; target: 'title' | 'description' | 'tags'; start_offset: number; end_offset: number; selected_text: string; content: string }) => request<TextAnnotation>(`/api/works/${workId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
replyToFeedback: (workId: number, type: FeedbackType, feedbackId: number, content: string) => request<FeedbackReply>(`/api/works/${workId}/feedback/${type}/${feedbackId}/replies`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }),
|
||||
withdrawFeedback: (workId: number, type: FeedbackType, feedbackId: number) => request<{ success: true }>(`/api/works/${workId}/feedback/${type}/${feedbackId}/withdraw`, { method: 'POST' }),
|
||||
deleteAnnotation: (id: number) => request<void>(`/api/annotations/${id}`, { method: 'DELETE' }),
|
||||
addComment: (noteId: number, data: { content: string; author_name: string; author_role?: 'client' | 'operator' }) => request<WorkComment>(`/api/notes/${noteId}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
getCustomerAccess: (slug: string) => request<CustomerAccessState>(`/api/review/${slug}/access`),
|
||||
customerLogin: (slug: string, data: { reviewer_name: string; password: string }) => request<{ success: true; reviewer_name: string }>(`/api/review/${slug}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
getCustomerProject: (slug: string) => request<{ project: Pick<Project, 'id' | 'name' | 'slug' | 'client_description' | 'status'>; collections: WorkCollection[]; reviewer_name: string }>(`/api/review/${slug}/project`),
|
||||
getCustomerCollection: (slug: string, collectionId: number) => request<{ collection: WorkCollection; works: Note[] }>(`/api/review/${slug}/collections/${collectionId}/works`),
|
||||
getCustomerWork: (slug: string, noteId: number, version?: number) => request<NoteDetail>(`/api/review/${slug}/works/${noteId}${version ? `?version=${version}` : ''}`),
|
||||
getCustomerProject: (slug: string) => request<{ project: Pick<Project, 'id' | 'name' | 'slug' | 'client_description' | 'status' | 'review_status'>; works: Note[]; reviewer_name: string }>(`/api/review/${slug}/project`),
|
||||
getCustomerWorkRound: (slug: string, workId: number, round?: number) => request<NoteDetail>(`/api/review/${slug}/works/${workId}${round ? `?round=${round}` : ''}`),
|
||||
getCustomerWorkFeedback: (slug: string, workId: number) => request<WorkFeedbackBundle>(`/api/review/${slug}/works/${workId}/annotations`),
|
||||
addCustomerComment: (slug: string, noteId: number, content: string) => request<WorkComment>(`/api/review/${slug}/works/${noteId}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }),
|
||||
addCustomerAnnotation: (slug: string, imageId: number, data: { x: number; y: number; content: string }) => request<Annotation>(`/api/review/${slug}/images/${imageId}/annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
addCustomerTextAnnotation: (slug: string, noteId: number, data: { version_number: number; target: 'title' | 'description'; content: string }) => request<TextAnnotation>(`/api/review/${slug}/works/${noteId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
submitCustomerDecision: (slug: string, noteId: number, versionNumber: number, decision: 'approved' | 'changes_requested', reason?: string) => request<{ success: true; status: ReviewStatus; version_number: number }>(`/api/review/${slug}/works/${noteId}/decision`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version_number: versionNumber, decision, reason }) }),
|
||||
addCustomerTextSelectionAnnotation: (slug: string, workId: number, data: { round_number: number; target: 'title' | 'description' | 'tags'; start_offset: number; end_offset: number; selected_text: string; content: string }) => request<TextAnnotation>(`/api/review/${slug}/works/${workId}/text-annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }),
|
||||
replyToCustomerFeedback: (slug: string, workId: number, type: FeedbackType, feedbackId: number, content: string) => request<FeedbackReply>(`/api/review/${slug}/works/${workId}/feedback/${type}/${feedbackId}/replies`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }),
|
||||
withdrawCustomerFeedback: (slug: string, workId: number, type: FeedbackType, feedbackId: number) => request<{ success: true }>(`/api/review/${slug}/works/${workId}/feedback/${type}/${feedbackId}/withdraw`, { method: 'POST' }),
|
||||
submitCustomerRoundDecision: (slug: string, workId: number, roundNumber: number, decision: 'approved' | 'changes_requested', reason?: string) => request<{ success: true; status: ReviewStatus; version_number: number }>(`/api/review/${slug}/works/${workId}/decision`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ round_number: roundNumber, decision, reason }) }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user