Files
YG-Datasets/easy-dataset-main/app/projects/[projectId]/images/components/ImageList.js

316 lines
11 KiB
JavaScript
Raw Normal View History

2026-03-17 14:36:31 +08:00
'use client';
import { useState } from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Chip,
Box,
Pagination,
Tooltip,
IconButton,
Avatar,
Dialog,
DialogContent,
Typography,
Button,
Checkbox
} from '@mui/material';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import DatasetIcon from '@mui/icons-material/Dataset';
import DeleteIcon from '@mui/icons-material/Delete';
import EditNoteIcon from '@mui/icons-material/EditNote';
import PhotoLibraryIcon from '@mui/icons-material/PhotoLibrary';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { useTranslation } from 'react-i18next';
import { imageStyles } from '../styles/imageStyles';
export default function ImageList({
images,
total,
page,
pageSize,
onPageChange,
onGenerateQuestions,
onGenerateDataset,
onDelete,
onAnnotate,
selectedIds = [],
onSelectionChange
}) {
const { t } = useTranslation();
const [previewImage, setPreviewImage] = useState(null);
// 处理全选/取消全选
const handleSelectAll = event => {
if (event.target.checked) {
const allIds = images.map(img => img.id);
onSelectionChange?.(allIds);
} else {
onSelectionChange?.([]);
}
};
// 处理单个选择
const handleSelectOne = (imageId, checked) => {
if (checked) {
onSelectionChange?.([...selectedIds, imageId]);
} else {
onSelectionChange?.(selectedIds.filter(id => id !== imageId));
}
};
// 判断是否全选
const isAllSelected = images.length > 0 && selectedIds.length === images.length;
const isSomeSelected = selectedIds.length > 0 && selectedIds.length < images.length;
// 格式化日期
const formatDate = dateString => {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// 格式化文件大小
const formatSize = bytes => {
if (!bytes) return '-';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
};
if (!images || images.length === 0) {
return (
<Box sx={imageStyles.emptyState}>
<Box sx={imageStyles.emptyIcon}>
<PhotoLibraryIcon sx={{ fontSize: 60, color: 'primary.main' }} />
</Box>
<Typography variant="h5" sx={imageStyles.emptyTitle}>
{t('images.noImages', { defaultValue: '还没有图片' })}
</Typography>
<Typography variant="body2" sx={imageStyles.emptyDescription}>
{t('images.noImagesDescription', { defaultValue: '开始导入图片,创建您的第一个图片数据集' })}
</Typography>
</Box>
);
}
return (
<>
<TableContainer component={Paper} sx={{ borderRadius: 2, boxShadow: 1 }}>
<Table>
<TableHead>
<TableRow sx={{ bgcolor: 'grey.50' }}>
<TableCell padding="checkbox">
<Checkbox indeterminate={isSomeSelected} checked={isAllSelected} onChange={handleSelectAll} />
</TableCell>
<TableCell width="60">{t('images.preview', { defaultValue: '预览' })}</TableCell>
<TableCell>{t('images.fileName', { defaultValue: '文件名' })}</TableCell>
<TableCell width="120">{t('images.size', { defaultValue: '大小' })}</TableCell>
<TableCell width="120">{t('images.dimensions', { defaultValue: '尺寸' })}</TableCell>
<TableCell width="100">{t('images.questionCount', { defaultValue: '问题数' })}</TableCell>
<TableCell width="100">{t('images.datasetCount', { defaultValue: '数据集数' })}</TableCell>
<TableCell width="180">{t('images.uploadTime', { defaultValue: '上传时间' })}</TableCell>
<TableCell width="200" align="center">
{t('common.actions', { defaultValue: '操作' })}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{images.map(image => (
<TableRow
key={image.id}
hover
selected={selectedIds.includes(image.id)}
sx={{
'&:hover': {
bgcolor: 'action.hover'
}
}}
>
{/* 复选框 */}
<TableCell padding="checkbox">
<Checkbox
checked={selectedIds.includes(image.id)}
onChange={e => handleSelectOne(image.id, e.target.checked)}
/>
</TableCell>
{/* 预览缩略图 */}
<TableCell>
<Avatar
src={image.base64 || image.path}
alt={image.imageName}
variant="rounded"
sx={{
width: 48,
height: 48,
cursor: 'pointer',
'&:hover': {
opacity: 0.8
}
}}
onClick={() => setPreviewImage(image)}
/>
</TableCell>
{/* 文件名 */}
<TableCell>
<Tooltip title={image.imageName}>
<Typography variant="body2" noWrap sx={{ maxWidth: 300 }}>
{image.imageName}
</Typography>
</Tooltip>
</TableCell>
{/* 文件大小 */}
<TableCell>
<Typography variant="body2" color="text.secondary">
{formatSize(image.size)}
</Typography>
</TableCell>
{/* 尺寸 */}
<TableCell>
{image.width && image.height ? (
<Typography variant="body2" color="text.secondary">
{image.width} × {image.height}
</Typography>
) : (
<Typography variant="body2" color="text.disabled">
-
</Typography>
)}
</TableCell>
{/* 问题数 */}
<TableCell>
<Chip
label={image.questionCount || 0}
size="small"
color={image.questionCount > 0 ? 'primary' : 'default'}
variant="outlined"
/>
</TableCell>
{/* 数据集数 */}
<TableCell>
<Chip
label={image.datasetCount || 0}
size="small"
color={image.datasetCount > 0 ? 'success' : 'default'}
variant="outlined"
/>
</TableCell>
{/* 上传时间 */}
<TableCell>
<Typography variant="body2" color="text.secondary">
{formatDate(image.createAt)}
</Typography>
</TableCell>
{/* 操作按钮 */}
<TableCell>
<Box sx={{ display: 'flex', gap: 0.5, justifyContent: 'center' }}>
<Tooltip title={t('images.preview', { defaultValue: '预览' })}>
<IconButton size="small" onClick={() => setPreviewImage(image)}>
<VisibilityIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('images.annotate', { defaultValue: '标注' })}>
<IconButton size="small" color="primary" onClick={() => onAnnotate(image)}>
<EditNoteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('images.generateQuestions', { defaultValue: '生成问题' })}>
<IconButton size="small" onClick={() => onGenerateQuestions(image)}>
<QuestionMarkIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('images.generateDataset', { defaultValue: '生成数据集' })}>
<IconButton size="small" onClick={() => onGenerateDataset(image)}>
<DatasetIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('common.delete', { defaultValue: '删除' })}>
<IconButton size="small" color="error" onClick={() => onDelete(image.id)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{/* 分页 */}
{total > pageSize && (
<Box sx={imageStyles.pagination}>
<Pagination
count={Math.ceil(total / pageSize)}
page={page}
onChange={(_, newPage) => onPageChange(newPage)}
color="primary"
size="large"
showFirstButton
showLastButton
/>
</Box>
)}
{/* 图片预览对话框 */}
<Dialog
open={!!previewImage}
onClose={() => setPreviewImage(null)}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
bgcolor: 'transparent',
boxShadow: 'none',
overflow: 'hidden'
}
}}
>
<DialogContent
sx={{ p: 0, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
{previewImage && (
<Box sx={{ width: '100%', textAlign: 'center' }}>
<img
src={previewImage.base64 || previewImage.path}
alt={previewImage.imageName}
style={{
maxWidth: '100%',
maxHeight: '90vh',
objectFit: 'contain'
}}
/>
<Typography
variant="caption"
sx={{ display: 'block', mt: 2, color: 'white', textShadow: '0 0 4px rgba(0,0,0,0.8)' }}
>
{previewImage.imageName}
</Typography>
</Box>
)}
</DialogContent>
</Dialog>
</>
);
}