316 lines
11 KiB
JavaScript
316 lines
11 KiB
JavaScript
|
|
'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>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
}
|