Files

316 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
</>
);
}