first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,486 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import {
Container,
Box,
Typography,
Button,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField
} from '@mui/material';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import DeleteIcon from '@mui/icons-material/Delete';
import { imageStyles } from './styles/imageStyles';
import { toast } from 'sonner';
import axios from 'axios';
import { useAtomValue } from 'jotai';
import { selectedModelInfoAtom } from '@/lib/store';
import ImageFilters from './components/ImageFilters';
import ImageGrid from './components/ImageGrid';
import ImageList from './components/ImageList';
import ImportDialog from './components/ImportDialog';
import QuestionDialog from './components/QuestionDialog';
import DatasetDialog from './components/DatasetDialog';
import AnnotationDialog from './components/annotation/AnnotationDialog';
import { useQuestionTemplates } from '../questions/hooks/useQuestionTemplates';
import { useAnnotation } from './hooks/useAnnotation';
import { useQuestionEdit } from '../questions/hooks/useQuestionEdit';
import QuestionEditDialog from '../questions/components/QuestionEditDialog';
import TemplateFormDialog from '../questions/components/template/TemplateFormDialog';
export default function ImagesPage() {
const { projectId } = useParams();
const router = useRouter();
const { t, i18n } = useTranslation();
const selectedModel = useAtomValue(selectedModelInfoAtom);
const [loading, setLoading] = useState(false);
const [images, setImages] = useState([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(8);
// 筛选条件
const [imageName, setImageName] = useState('');
const [hasQuestions, setHasQuestions] = useState('all');
const [hasDatasets, setHasDatasets] = useState('all');
// 视图模式
const [viewMode, setViewMode] = useState('grid');
// 选中状态(仅列表视图使用)
const [selectedIds, setSelectedIds] = useState([]);
// 对话框状态
const [importDialogOpen, setImportDialogOpen] = useState(false);
const [questionDialogOpen, setQuestionDialogOpen] = useState(false);
const [datasetDialogOpen, setDatasetDialogOpen] = useState(false);
const [selectedImage, setSelectedImage] = useState(null);
const [autoGenerateDialogOpen, setAutoGenerateDialogOpen] = useState(false);
const [questionCount, setQuestionCount] = useState(3);
// 问题模板和标注功能 (只获取图像类型的模板)
const { templates, createTemplate } = useQuestionTemplates(projectId, 'image');
// 问题编辑 Hook
const { editDialogOpen, editMode, editingQuestion, handleOpenCreateDialog, handleCloseDialog, handleSubmitQuestion } =
useQuestionEdit(projectId, async () => {
fetchImages();
if (annotationOpen && currentImage) {
await refreshCurrentImage();
}
toast.success(t('questions.operationSuccess'));
});
// 模板管理状态
const [templateDialogOpen, setTemplateDialogOpen] = useState(false);
// 获取图片列表
const fetchImages = async () => {
try {
setLoading(true);
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString()
});
if (imageName) params.append('imageName', imageName);
if (hasQuestions !== 'all') params.append('hasQuestions', hasQuestions);
if (hasDatasets !== 'all') params.append('hasDatasets', hasDatasets);
const response = await axios.get(`/api/projects/${projectId}/images?${params.toString()}`);
setImages(response.data.data);
setTotal(response.data.total);
} catch (error) {
console.error('Failed to fetch images:', error);
toast.error(t('common.fetchError'));
} finally {
setLoading(false);
}
};
// 查找下一个有未标注问题的图片
const handleFindNextImage = async () => {
try {
const response = await axios.get(`/api/projects/${projectId}/images/next-unanswered`);
return response.data.data || null;
} catch (error) {
console.error('查找下一个图片失败:', error);
return null;
}
};
const {
open: annotationOpen,
saving: annotationSaving,
loading: annotationLoading,
currentImage,
selectedTemplate,
answer,
setAnswer,
handleTemplateChange,
openAnnotation,
closeAnnotation,
saveAnnotation,
refreshCurrentImage
} = useAnnotation(projectId, fetchImages, handleFindNextImage);
useEffect(() => {
fetchImages();
}, [projectId, page, imageName, hasQuestions, hasDatasets]);
useEffect(() => {
setSelectedIds([]);
}, [viewMode]);
// 处理导入成功
const handleImportSuccess = () => {
setImportDialogOpen(false);
setPage(1);
fetchImages();
};
// 处理生成问题
const handleGenerateQuestions = image => {
setSelectedImage(image);
setQuestionDialogOpen(true);
};
// 处理生成数据集
const handleGenerateDataset = image => {
setSelectedImage(image);
setDatasetDialogOpen(true);
};
// 删除图片
const handleDeleteImage = async imageId => {
if (!confirm(t('images.deleteConfirm', { defaultValue: '确定要删除这张图片吗?' }))) {
return;
}
try {
await axios.delete(`/api/projects/${projectId}/images?imageId=${imageId}`);
toast.success(t('images.deleteSuccess', { defaultValue: '删除成功' }));
fetchImages();
} catch (error) {
console.error('Failed to delete image:', error);
toast.error(t('images.deleteFailed', { defaultValue: '删除失败' }));
}
};
// 批量删除图片
const handleBatchDelete = async () => {
if (selectedIds.length === 0) {
toast.error(t('images.selectImagesToDelete', { defaultValue: '请选择要删除的图片' }));
return;
}
if (
!confirm(
t('images.batchDeleteConfirm', {
defaultValue: `确定要删除选中的 ${selectedIds.length} 张图片吗?`,
count: selectedIds.length
})
)
) {
return;
}
try {
setLoading(true);
let successCount = 0;
let failCount = 0;
// 逐个调用删除接口
for (const imageId of selectedIds) {
try {
await axios.delete(`/api/projects/${projectId}/images?imageId=${imageId}`);
successCount++;
} catch (error) {
console.error(`Failed to delete image ${imageId}:`, error);
failCount++;
}
}
// 显示结果
if (failCount === 0) {
toast.success(
t('images.batchDeleteSuccess', {
defaultValue: `成功删除 ${successCount} 张图片`,
count: successCount
})
);
} else {
toast.warning(
t('images.batchDeletePartialSuccess', {
defaultValue: `成功删除 ${successCount} 张,失败 ${failCount}`,
success: successCount,
fail: failCount
})
);
}
// 清空选中状态并刷新列表
setSelectedIds([]);
fetchImages();
} catch (error) {
console.error('Batch delete failed:', error);
toast.error(t('images.batchDeleteFailed', { defaultValue: '批量删除失败' }));
} finally {
setLoading(false);
}
};
// 处理自动提取问题
const handleAutoGenerateQuestions = () => {
if (!selectedModel) {
toast.error(t('images.selectModelFirst'));
return;
}
if (selectedModel.type !== 'vision') {
toast.error(t('images.visionModelRequired'));
return;
}
setAutoGenerateDialogOpen(true);
};
// 确认创建自动提取任务
const handleConfirmAutoGenerate = async () => {
// 验证问题数量
if (questionCount < 1 || questionCount > 10) {
toast.error(t('images.countRange'));
return;
}
try {
setAutoGenerateDialogOpen(false);
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
taskType: 'image-question-generation',
modelInfo: selectedModel,
language: i18n.language,
note: { questionCount }
});
if (response.data.code === 0) {
toast.success(t('images.taskCreated'));
// 跳转到任务管理页面
router.push(`/projects/${projectId}/tasks`);
} else {
toast.error(response.data.error || t('images.taskCreateFailed'));
}
} catch (error) {
console.error('Failed to create auto-generate task:', error);
toast.error(t('images.taskCreateFailed'));
}
};
// 模板管理函数
const handleOpenCreateTemplateDialog = () => {
setTemplateDialogOpen(true);
};
const handleCloseTemplateDialog = () => {
setTemplateDialogOpen(false);
};
const handleSubmitTemplate = async data => {
try {
await createTemplate(data);
handleCloseTemplateDialog();
fetchImages();
if (annotationOpen && currentImage) {
await refreshCurrentImage();
}
toast.success(t('questions.operationSuccess'));
} catch (error) {
console.error('Failed to save template:', error);
}
};
return (
<Container maxWidth="xl" sx={imageStyles.pageContainer}>
{/* 页面头部 */}
<Box sx={imageStyles.header}>
<Box sx={imageStyles.headerTitle}>
<Typography variant="h4" component="h1" sx={imageStyles.title}>
{t('images.title', { defaultValue: '图片管理' })}
</Typography>
</Box>
<Box sx={imageStyles.headerActions}>
{viewMode === 'list' && selectedIds.length > 0 && (
<Button
variant="contained"
color="error"
startIcon={<DeleteIcon />}
onClick={handleBatchDelete}
sx={imageStyles.actionButton}
>
{t('images.batchDelete', { defaultValue: '批量删除' })} ({selectedIds.length})
</Button>
)}
<Button
variant="outlined"
startIcon={<AutoAwesomeIcon />}
onClick={handleAutoGenerateQuestions}
sx={imageStyles.actionButton}
>
{t('images.autoGenerateQuestions', { defaultValue: 'AI 批量生成问题' })}
</Button>
<Button
variant="contained"
startIcon={<AddPhotoAlternateIcon />}
onClick={() => setImportDialogOpen(true)}
sx={imageStyles.actionButton}
>
{t('images.importImages', { defaultValue: '导入图片' })}
</Button>
</Box>
</Box>
{/* 筛选区域 */}
<ImageFilters
imageName={imageName}
onImageNameChange={setImageName}
hasQuestions={hasQuestions}
onHasQuestionsChange={setHasQuestions}
hasDatasets={hasDatasets}
onHasDatasetsChange={setHasDatasets}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
{/* 图片列表 */}
{loading ? (
<Box sx={imageStyles.loadingContainer}>
<CircularProgress size={48} />
</Box>
) : viewMode === 'grid' ? (
<ImageGrid
images={images}
total={total}
page={page}
pageSize={pageSize}
onPageChange={setPage}
onGenerateQuestions={handleGenerateQuestions}
onGenerateDataset={handleGenerateDataset}
onDelete={handleDeleteImage}
onAnnotate={openAnnotation}
/>
) : (
<ImageList
images={images}
total={total}
page={page}
pageSize={pageSize}
onPageChange={setPage}
onGenerateQuestions={handleGenerateQuestions}
onGenerateDataset={handleGenerateDataset}
onDelete={handleDeleteImage}
onAnnotate={openAnnotation}
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
/>
)}
<ImportDialog
open={importDialogOpen}
projectId={projectId}
onClose={() => setImportDialogOpen(false)}
onSuccess={handleImportSuccess}
/>
<QuestionDialog
open={questionDialogOpen}
projectId={projectId}
image={selectedImage}
onClose={() => setQuestionDialogOpen(false)}
onSuccess={fetchImages}
/>
<DatasetDialog
open={datasetDialogOpen}
projectId={projectId}
image={selectedImage}
onClose={() => setDatasetDialogOpen(false)}
onSuccess={fetchImages}
/>
<Dialog open={autoGenerateDialogOpen} onClose={() => setAutoGenerateDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>{t('images.autoGenerateQuestions')}</DialogTitle>
<DialogContent>
<Typography sx={{ mb: 3 }}>{t('images.autoGenerateConfirm')}</Typography>
<TextField
fullWidth
type="number"
label={t('images.questionCount')}
value={questionCount}
onChange={e => setQuestionCount(parseInt(e.target.value) || 1)}
inputProps={{ min: 1, max: 10 }}
helperText={t('images.questionCountHelp')}
sx={{ mb: 2 }}
/>
<Typography variant="body2" color="text.secondary">
{t('images.currentModel')}: {selectedModel?.modelName || t('common.none')}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setAutoGenerateDialogOpen(false)}>{t('common.cancel')}</Button>
<Button onClick={handleConfirmAutoGenerate} variant="contained">
{t('common.confirm')}
</Button>
</DialogActions>
</Dialog>
<AnnotationDialog
open={annotationOpen}
onClose={closeAnnotation}
image={currentImage}
templates={templates}
selectedTemplate={selectedTemplate}
onTemplateChange={handleTemplateChange}
answer={answer}
onAnswerChange={setAnswer}
onSave={() => saveAnnotation(false)}
onSaveAndContinue={() => saveAnnotation(true)}
saving={annotationSaving}
loading={annotationLoading}
onOpenCreateQuestion={handleOpenCreateDialog}
onOpenCreateTemplate={handleOpenCreateTemplateDialog}
/>
{/* 问题编辑对话框 */}
<QuestionEditDialog
open={editDialogOpen}
mode={editMode}
question={editingQuestion}
onClose={handleCloseDialog}
onSubmit={handleSubmitQuestion}
projectId={projectId}
initialData={{ sourceType: 'image', imageId: currentImage?.id }}
/>
{/* 问题模板对话框 */}
<TemplateFormDialog
open={templateDialogOpen}
onClose={handleCloseTemplateDialog}
onSubmit={handleSubmitTemplate}
projectId={projectId}
template={{ sourceType: 'image' }}
/>
</Container>
);
}