288 lines
9.4 KiB
JavaScript
288 lines
9.4 KiB
JavaScript
|
|
import { useState } from 'react';
|
|||
|
|
import axios from 'axios';
|
|||
|
|
import { toast } from 'sonner';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
|
|||
|
|
// 深度遍历 JSON,将所有值设为空字符串
|
|||
|
|
function clearJsonValues(obj) {
|
|||
|
|
if (Array.isArray(obj)) {
|
|||
|
|
return obj.map(item => clearJsonValues(item));
|
|||
|
|
} else if (obj !== null && typeof obj === 'object') {
|
|||
|
|
const cleared = {};
|
|||
|
|
for (const key in obj) {
|
|||
|
|
cleared[key] = clearJsonValues(obj[key]);
|
|||
|
|
}
|
|||
|
|
return cleared;
|
|||
|
|
} else {
|
|||
|
|
return ''; // 所有基础类型值都变为空字符串
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function useAnnotation(projectId, onSuccess, onFindNextImage) {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [open, setOpen] = useState(false);
|
|||
|
|
const [saving, setSaving] = useState(false);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [currentImage, setCurrentImage] = useState(null);
|
|||
|
|
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
|||
|
|
const [answer, setAnswer] = useState('');
|
|||
|
|
|
|||
|
|
// 打开标注对话框
|
|||
|
|
const openAnnotation = async (image, template = null) => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
// 获取图片详情,包括已标注和未标注的问题
|
|||
|
|
const response = await axios.get(`/api/projects/${projectId}/images/${image.id}`);
|
|||
|
|
if (response.data.success) {
|
|||
|
|
const imageDetail = response.data.data;
|
|||
|
|
setCurrentImage(imageDetail);
|
|||
|
|
|
|||
|
|
// 如果没有指定模板,尝试选择第一个未标注的问题
|
|||
|
|
if (!template) {
|
|||
|
|
if (imageDetail.unansweredQuestions?.length > 0) {
|
|||
|
|
template = imageDetail.unansweredQuestions[0];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setSelectedTemplate(template);
|
|||
|
|
|
|||
|
|
// 根据问题类型初始化答案
|
|||
|
|
let initialAnswer = '';
|
|||
|
|
if (template?.answerType === 'label') {
|
|||
|
|
initialAnswer = [];
|
|||
|
|
} else if (template?.answerType === 'custom_format' && template?.customFormat) {
|
|||
|
|
// 为自定义格式提供默认值(所有字段值清空)
|
|||
|
|
try {
|
|||
|
|
let templateJson;
|
|||
|
|
if (typeof template.customFormat === 'string') {
|
|||
|
|
// 如果customFormat是字符串,尝试解析为JSON
|
|||
|
|
templateJson = JSON.parse(template.customFormat);
|
|||
|
|
} else {
|
|||
|
|
// 如果customFormat已经是对象,直接使用
|
|||
|
|
templateJson = template.customFormat;
|
|||
|
|
}
|
|||
|
|
// 深度遍历,将所有字段值清空
|
|||
|
|
const clearedJson = clearJsonValues(templateJson);
|
|||
|
|
initialAnswer = JSON.stringify(clearedJson, null, 2);
|
|||
|
|
} catch (error) {
|
|||
|
|
// 如枟解析失败,提供一个空的JSON对象
|
|||
|
|
initialAnswer = '{}';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setAnswer(initialAnswer);
|
|||
|
|
setOpen(true);
|
|||
|
|
} else {
|
|||
|
|
toast.error(t('images.loadImageDetailFailed', { defaultValue: '加载图片详情失败' }));
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取图片详情失败:', error);
|
|||
|
|
toast.error(t('images.loadImageDetailFailed', { defaultValue: '加载图片详情失败' }));
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 关闭对话框
|
|||
|
|
const closeAnnotation = () => {
|
|||
|
|
setOpen(false);
|
|||
|
|
setCurrentImage(null);
|
|||
|
|
setSelectedTemplate(null);
|
|||
|
|
setAnswer('');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 刷新当前图片的问题列表(创建问题后调用)
|
|||
|
|
const refreshCurrentImage = async () => {
|
|||
|
|
if (!currentImage) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await axios.get(`/api/projects/${projectId}/images/${currentImage.id}`);
|
|||
|
|
if (response.data.success) {
|
|||
|
|
const imageDetail = response.data.data;
|
|||
|
|
// 更新当前图片数据
|
|||
|
|
setCurrentImage(imageDetail);
|
|||
|
|
return imageDetail;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('刷新图片详情失败:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 查找下一个未标注的问题
|
|||
|
|
const findNextUnansweredQuestion = async () => {
|
|||
|
|
// 重新获取图片详情,获取最新的问题列表
|
|||
|
|
try {
|
|||
|
|
const response = await axios.get(`/api/projects/${projectId}/images/${currentImage.id}`);
|
|||
|
|
if (response.data.success) {
|
|||
|
|
const imageDetail = response.data.data;
|
|||
|
|
|
|||
|
|
// 更新当前图片数据
|
|||
|
|
setCurrentImage(imageDetail);
|
|||
|
|
|
|||
|
|
// 返回第一个未标注的问题
|
|||
|
|
if (imageDetail.unansweredQuestions?.length > 0) {
|
|||
|
|
return imageDetail.unansweredQuestions[0];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取下一个问题失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 保存标注
|
|||
|
|
const saveAnnotation = async (continueNext = false) => {
|
|||
|
|
if (!currentImage) {
|
|||
|
|
toast.error(t('images.noImageSelected', { defaultValue: '未选择图片' }));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!selectedTemplate) {
|
|||
|
|
toast.error(t('images.noTemplateSelected', { defaultValue: '请选择问题' }));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证答案
|
|||
|
|
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
|
|||
|
|
toast.error(t('images.answerRequired', { defaultValue: '请输入答案' }));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是自定义格式,验证 JSON 格式
|
|||
|
|
if (selectedTemplate.answerType === 'custom_format') {
|
|||
|
|
try {
|
|||
|
|
JSON.parse(answer);
|
|||
|
|
} catch (e) {
|
|||
|
|
toast.error(t('images.invalidJsonFormat', { defaultValue: 'JSON 格式不正确' }));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(999, answer);
|
|||
|
|
setSaving(true);
|
|||
|
|
try {
|
|||
|
|
const response = await axios.post(`/api/projects/${projectId}/images/annotations`, {
|
|||
|
|
imageId: currentImage.id,
|
|||
|
|
imageName: currentImage.imageName,
|
|||
|
|
questionId: selectedTemplate.id,
|
|||
|
|
question: selectedTemplate.question,
|
|||
|
|
templateId: selectedTemplate.id,
|
|||
|
|
answerType: selectedTemplate.answerType,
|
|||
|
|
answer
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.data.success) {
|
|||
|
|
toast.success(t('images.annotationSuccess', { defaultValue: '标注保存成功' }));
|
|||
|
|
|
|||
|
|
// 触发刷新回调
|
|||
|
|
if (onSuccess) {
|
|||
|
|
onSuccess();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (continueNext) {
|
|||
|
|
// 查找下一个未标注的问题
|
|||
|
|
const nextQuestion = await findNextUnansweredQuestion();
|
|||
|
|
|
|||
|
|
if (nextQuestion) {
|
|||
|
|
// 切换到下一个问题
|
|||
|
|
setSelectedTemplate(nextQuestion);
|
|||
|
|
|
|||
|
|
// 根据问题类型初始化答案
|
|||
|
|
let initialAnswer = '';
|
|||
|
|
if (nextQuestion.answerType === 'label') {
|
|||
|
|
initialAnswer = [];
|
|||
|
|
} else if (nextQuestion.answerType === 'custom_format' && nextQuestion.customFormat) {
|
|||
|
|
try {
|
|||
|
|
let templateJson;
|
|||
|
|
if (typeof nextQuestion.customFormat === 'string') {
|
|||
|
|
templateJson = JSON.parse(nextQuestion.customFormat);
|
|||
|
|
} else {
|
|||
|
|
templateJson = nextQuestion.customFormat;
|
|||
|
|
}
|
|||
|
|
const clearedJson = clearJsonValues(templateJson);
|
|||
|
|
initialAnswer = JSON.stringify(clearedJson, null, 2);
|
|||
|
|
} catch (error) {
|
|||
|
|
initialAnswer = '{}';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
setAnswer(initialAnswer);
|
|||
|
|
} else {
|
|||
|
|
// 没有更多未标注的问题了,尝试查找下一个有未标注问题的图片
|
|||
|
|
if (onFindNextImage) {
|
|||
|
|
const nextImage = await onFindNextImage();
|
|||
|
|
if (nextImage) {
|
|||
|
|
// 打开下一个图片的标注
|
|||
|
|
await openAnnotation(nextImage);
|
|||
|
|
} else {
|
|||
|
|
// 没有更多图片了
|
|||
|
|
toast.info(t('images.allImagesAnnotated', { defaultValue: '所有图片的问题都已标注完成' }));
|
|||
|
|
closeAnnotation();
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
toast.info(t('images.allQuestionsAnnotated', { defaultValue: '当前图片所有问题已标注完成' }));
|
|||
|
|
closeAnnotation();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
closeAnnotation();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('保存标注失败:', error);
|
|||
|
|
const errorMsg = error.response?.data?.error || t('images.annotationFailed', { defaultValue: '保存标注失败' });
|
|||
|
|
toast.error(errorMsg);
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理模板变更
|
|||
|
|
const handleTemplateChange = template => {
|
|||
|
|
setSelectedTemplate(template);
|
|||
|
|
|
|||
|
|
// 根据新模板类型初始化答案
|
|||
|
|
let initialAnswer = '';
|
|||
|
|
if (template?.answerType === 'label') {
|
|||
|
|
initialAnswer = [];
|
|||
|
|
} else if (template?.answerType === 'custom_format' && template?.customFormat) {
|
|||
|
|
// 为自定义格式提供默认值(所有字段值清空)
|
|||
|
|
try {
|
|||
|
|
let templateJson;
|
|||
|
|
if (typeof template.customFormat === 'string') {
|
|||
|
|
// 如果customFormat是字符串,尝试解析为JSON
|
|||
|
|
templateJson = JSON.parse(template.customFormat);
|
|||
|
|
} else {
|
|||
|
|
// 如果customFormat已经是对象,直接使用
|
|||
|
|
templateJson = template.customFormat;
|
|||
|
|
}
|
|||
|
|
// 深度遍历,将所有字段值清空
|
|||
|
|
const clearedJson = clearJsonValues(templateJson);
|
|||
|
|
initialAnswer = JSON.stringify(clearedJson, null, 2);
|
|||
|
|
} catch (error) {
|
|||
|
|
// 如枟解析失败,提供一个空的JSON对象
|
|||
|
|
initialAnswer = '{}';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setAnswer(initialAnswer);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
open,
|
|||
|
|
saving,
|
|||
|
|
loading,
|
|||
|
|
currentImage,
|
|||
|
|
selectedTemplate,
|
|||
|
|
answer,
|
|||
|
|
setSelectedTemplate,
|
|||
|
|
setAnswer,
|
|||
|
|
handleTemplateChange,
|
|||
|
|
openAnnotation,
|
|||
|
|
closeAnnotation,
|
|||
|
|
saveAnnotation,
|
|||
|
|
refreshCurrentImage
|
|||
|
|
};
|
|||
|
|
}
|