417 lines
14 KiB
JavaScript
417 lines
14 KiB
JavaScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useState } from 'react';
|
|||
|
|
import {
|
|||
|
|
Dialog,
|
|||
|
|
DialogTitle,
|
|||
|
|
DialogContent,
|
|||
|
|
DialogActions,
|
|||
|
|
Button,
|
|||
|
|
Box,
|
|||
|
|
Typography,
|
|||
|
|
List,
|
|||
|
|
ListItem,
|
|||
|
|
ListItemText,
|
|||
|
|
IconButton,
|
|||
|
|
CircularProgress,
|
|||
|
|
Alert,
|
|||
|
|
TextField,
|
|||
|
|
Tabs,
|
|||
|
|
Tab,
|
|||
|
|
Paper,
|
|||
|
|
Chip,
|
|||
|
|
Card
|
|||
|
|
} from '@mui/material';
|
|||
|
|
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
|||
|
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
|||
|
|
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
|||
|
|
import FolderZipIcon from '@mui/icons-material/FolderZip';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import { toast } from 'sonner';
|
|||
|
|
import axios from 'axios';
|
|||
|
|
|
|||
|
|
export default function ImportDialog({ open, projectId, onClose, onSuccess }) {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [mode, setMode] = useState(0); // 0: 目录导入, 1: PDF 导入, 2: 压缩包导入
|
|||
|
|
const [directories, setDirectories] = useState([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [inputPath, setInputPath] = useState('');
|
|||
|
|
const [selectedPdf, setSelectedPdf] = useState(null);
|
|||
|
|
const [selectedZip, setSelectedZip] = useState(null);
|
|||
|
|
|
|||
|
|
const handleAddDirectory = () => {
|
|||
|
|
if (inputPath.trim() && !directories.includes(inputPath.trim())) {
|
|||
|
|
setDirectories([...directories, inputPath.trim()]);
|
|||
|
|
setInputPath('');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleRemoveDirectory = index => {
|
|||
|
|
setDirectories(directories.filter((_, i) => i !== index));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleImport = async () => {
|
|||
|
|
if (directories.length === 0) {
|
|||
|
|
toast.error(t('images.selectAtLeastOne'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const response = await axios.post(`/api/projects/${projectId}/images`, {
|
|||
|
|
directories
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
toast.success(t('images.importSuccess', { count: response.data.count }));
|
|||
|
|
setDirectories([]);
|
|||
|
|
onSuccess?.();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to import images:', error);
|
|||
|
|
toast.error(error.response?.data?.error || t('images.importFailed'));
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePdfSelect = event => {
|
|||
|
|
const file = event.target.files?.[0];
|
|||
|
|
if (file && file.type === 'application/pdf') {
|
|||
|
|
setSelectedPdf(file);
|
|||
|
|
} else {
|
|||
|
|
toast.error(t('images.invalidPdfFile', { defaultValue: '请选择有效的 PDF 文件' }));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePdfImport = async () => {
|
|||
|
|
if (!selectedPdf) {
|
|||
|
|
toast.error(t('images.selectPdfFile', { defaultValue: '请选择 PDF 文件' }));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const formData = new FormData();
|
|||
|
|
formData.append('file', selectedPdf);
|
|||
|
|
|
|||
|
|
// 调用 PDF 转换 API
|
|||
|
|
const response = await axios.post(`/api/projects/${projectId}/images/pdf-convert`, formData, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'multipart/form-data'
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
toast.success(
|
|||
|
|
t('images.pdfImportSuccess', {
|
|||
|
|
defaultValue: `成功从 PDF "${response.data.pdfName}" 导入 ${response.data.count} 张图片`,
|
|||
|
|
count: response.data.count,
|
|||
|
|
name: response.data.pdfName
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
setSelectedPdf(null);
|
|||
|
|
onSuccess?.();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to import PDF:', error);
|
|||
|
|
toast.error(error.response?.data?.error || t('images.pdfImportFailed', { defaultValue: 'PDF 导入失败' }));
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleZipSelect = event => {
|
|||
|
|
const file = event.target.files?.[0];
|
|||
|
|
if (file && file.name.toLowerCase().endsWith('.zip')) {
|
|||
|
|
setSelectedZip(file);
|
|||
|
|
} else {
|
|||
|
|
toast.error(t('images.invalidZipFile', { defaultValue: '请选择有效的 ZIP 文件' }));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleZipImport = async () => {
|
|||
|
|
if (!selectedZip) {
|
|||
|
|
toast.error(t('images.selectZipFile', { defaultValue: '请选择 ZIP 文件' }));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const formData = new FormData();
|
|||
|
|
formData.append('file', selectedZip);
|
|||
|
|
|
|||
|
|
// 调用压缩包导入 API
|
|||
|
|
const response = await axios.post(`/api/projects/${projectId}/images/zip-import`, formData, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'multipart/form-data'
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
toast.success(
|
|||
|
|
t('images.zipImportSuccess', {
|
|||
|
|
defaultValue: `成功从压缩包 "${response.data.zipName}" 导入 ${response.data.count} 张图片`,
|
|||
|
|
count: response.data.count,
|
|||
|
|
name: response.data.zipName
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
setSelectedZip(null);
|
|||
|
|
onSuccess?.();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to import ZIP:', error);
|
|||
|
|
toast.error(error.response?.data?.error || t('images.zipImportFailed', { defaultValue: '压缩包导入失败' }));
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleClose = () => {
|
|||
|
|
if (!loading) {
|
|||
|
|
setDirectories([]);
|
|||
|
|
setSelectedPdf(null);
|
|||
|
|
setSelectedZip(null);
|
|||
|
|
setMode(0);
|
|||
|
|
onClose();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
|||
|
|
<DialogTitle>{t('images.importImages')}</DialogTitle>
|
|||
|
|
<DialogContent sx={{ pt: 2 }}>
|
|||
|
|
<Tabs
|
|||
|
|
value={mode}
|
|||
|
|
onChange={(e, newValue) => setMode(newValue)}
|
|||
|
|
sx={{ mb: 3, borderBottom: 1, borderColor: 'divider' }}
|
|||
|
|
>
|
|||
|
|
<Tab
|
|||
|
|
label={t('images.importFromDirectory', { defaultValue: '从目录导入' })}
|
|||
|
|
icon={<FolderOpenIcon />}
|
|||
|
|
iconPosition="start"
|
|||
|
|
/>
|
|||
|
|
<Tab
|
|||
|
|
label={t('images.importFromPdf', { defaultValue: '从 PDF 导入' })}
|
|||
|
|
icon={<PictureAsPdfIcon />}
|
|||
|
|
iconPosition="start"
|
|||
|
|
/>
|
|||
|
|
<Tab
|
|||
|
|
label={t('images.importFromZip', { defaultValue: '从压缩包导入' })}
|
|||
|
|
icon={<FolderZipIcon />}
|
|||
|
|
iconPosition="start"
|
|||
|
|
/>
|
|||
|
|
</Tabs>
|
|||
|
|
|
|||
|
|
{mode === 0 ? (
|
|||
|
|
<>
|
|||
|
|
<Alert severity="info" sx={{ mb: 2 }}>
|
|||
|
|
{t('images.importTip')}
|
|||
|
|
</Alert>
|
|||
|
|
|
|||
|
|
<Box sx={{ display: 'flex', gap: 1.5, mb: 3 }}>
|
|||
|
|
<TextField
|
|||
|
|
fullWidth
|
|||
|
|
size="small"
|
|||
|
|
label={t('images.directoryPath')}
|
|||
|
|
placeholder={t('images.enterDirectoryPath')}
|
|||
|
|
value={inputPath}
|
|||
|
|
onChange={e => setInputPath(e.target.value)}
|
|||
|
|
onKeyPress={e => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
handleAddDirectory();
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
disabled={loading}
|
|||
|
|
sx={{
|
|||
|
|
'& .MuiOutlinedInput-root': {
|
|||
|
|
borderRadius: 2
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<Button
|
|||
|
|
variant="contained"
|
|||
|
|
startIcon={<FolderOpenIcon />}
|
|||
|
|
onClick={handleAddDirectory}
|
|||
|
|
disabled={loading || !inputPath.trim()}
|
|||
|
|
sx={{
|
|||
|
|
borderRadius: 2,
|
|||
|
|
px: 2.5,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
textTransform: 'none',
|
|||
|
|
whiteSpace: 'nowrap',
|
|||
|
|
boxShadow: 1,
|
|||
|
|
transition: 'all 0.2s',
|
|||
|
|
'&:hover:not(:disabled)': {
|
|||
|
|
boxShadow: 2,
|
|||
|
|
transform: 'translateY(-1px)'
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{t('images.addDirectory', { defaultValue: '添加目录' })}
|
|||
|
|
</Button>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{directories.length > 0 && (
|
|||
|
|
<Card
|
|||
|
|
sx={{
|
|||
|
|
p: 2.5,
|
|||
|
|
bgcolor: 'background.paper',
|
|||
|
|
border: '1px solid',
|
|||
|
|
borderColor: 'divider',
|
|||
|
|
borderRadius: 2
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|||
|
|
<FolderOpenIcon sx={{ mr: 1, color: 'primary.main' }} />
|
|||
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
|||
|
|
{t('images.selectedDirectories')} ({directories.length})
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
|||
|
|
{directories.map((dir, index) => (
|
|||
|
|
<Chip
|
|||
|
|
key={index}
|
|||
|
|
label={dir}
|
|||
|
|
onDelete={() => handleRemoveDirectory(index)}
|
|||
|
|
disabled={loading}
|
|||
|
|
icon={<FolderOpenIcon />}
|
|||
|
|
sx={{
|
|||
|
|
borderRadius: 1.5,
|
|||
|
|
fontWeight: 500,
|
|||
|
|
maxWidth: '100%',
|
|||
|
|
'& .MuiChip-label': {
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
textOverflow: 'ellipsis',
|
|||
|
|
whiteSpace: 'nowrap'
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</Box>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
) : mode === 1 ? (
|
|||
|
|
<>
|
|||
|
|
<Alert severity="info" sx={{ mb: 2 }}>
|
|||
|
|
{t('images.pdfImportTip', { defaultValue: '选择 PDF 文件,系统会自动将其转换为图片并导入' })}
|
|||
|
|
</Alert>
|
|||
|
|
|
|||
|
|
<Paper
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{
|
|||
|
|
p: 4,
|
|||
|
|
textAlign: 'center',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
bgcolor: 'background.default',
|
|||
|
|
border: '2px dashed',
|
|||
|
|
borderColor: selectedPdf ? 'primary.main' : 'divider',
|
|||
|
|
transition: 'all 0.3s',
|
|||
|
|
'&:hover': {
|
|||
|
|
bgcolor: 'action.hover',
|
|||
|
|
borderColor: 'primary.main'
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
onClick={() => document.getElementById('pdf-file-input').click()}
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
id="pdf-file-input"
|
|||
|
|
type="file"
|
|||
|
|
accept=".pdf,application/pdf"
|
|||
|
|
style={{ display: 'none' }}
|
|||
|
|
onChange={handlePdfSelect}
|
|||
|
|
disabled={loading}
|
|||
|
|
/>
|
|||
|
|
<UploadFileIcon sx={{ fontSize: 64, color: selectedPdf ? 'primary.main' : 'text.secondary', mb: 2 }} />
|
|||
|
|
<Typography variant="h6" gutterBottom>
|
|||
|
|
{selectedPdf ? selectedPdf.name : t('images.clickToSelectPdf', { defaultValue: '点击选择 PDF 文件' })}
|
|||
|
|
</Typography>
|
|||
|
|
<Typography variant="body2" color="text.secondary">
|
|||
|
|
{t('images.supportedFormat', { defaultValue: '支持格式:PDF' })}
|
|||
|
|
</Typography>
|
|||
|
|
{selectedPdf && (
|
|||
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
|||
|
|
{t('images.fileSize', { defaultValue: '文件大小' })}: {(selectedPdf.size / 1024 / 1024).toFixed(2)} MB
|
|||
|
|
</Typography>
|
|||
|
|
)}
|
|||
|
|
</Paper>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<Alert severity="info" sx={{ mb: 2 }}>
|
|||
|
|
{t('images.zipImportTip', { defaultValue: '选择 ZIP 压缩包文件,系统会自动解压并导入其中的图片' })}
|
|||
|
|
</Alert>
|
|||
|
|
|
|||
|
|
<Paper
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{
|
|||
|
|
p: 4,
|
|||
|
|
textAlign: 'center',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
bgcolor: 'background.default',
|
|||
|
|
border: '2px dashed',
|
|||
|
|
borderColor: selectedZip ? 'primary.main' : 'divider',
|
|||
|
|
transition: 'all 0.3s',
|
|||
|
|
'&:hover': {
|
|||
|
|
bgcolor: 'action.hover',
|
|||
|
|
borderColor: 'primary.main'
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
onClick={() => document.getElementById('zip-file-input').click()}
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
id="zip-file-input"
|
|||
|
|
type="file"
|
|||
|
|
accept=".zip,application/zip,application/x-zip-compressed"
|
|||
|
|
style={{ display: 'none' }}
|
|||
|
|
onChange={handleZipSelect}
|
|||
|
|
disabled={loading}
|
|||
|
|
/>
|
|||
|
|
<FolderZipIcon sx={{ fontSize: 64, color: selectedZip ? 'primary.main' : 'text.secondary', mb: 2 }} />
|
|||
|
|
<Typography variant="h6" gutterBottom>
|
|||
|
|
{selectedZip ? selectedZip.name : t('images.clickToSelectZip', { defaultValue: '点击选择 ZIP 文件' })}
|
|||
|
|
</Typography>
|
|||
|
|
<Typography variant="body2" color="text.secondary">
|
|||
|
|
{t('images.supportedZipFormat', { defaultValue: '支持格式:ZIP' })}
|
|||
|
|
</Typography>
|
|||
|
|
{selectedZip && (
|
|||
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
|||
|
|
{t('images.fileSize', { defaultValue: '文件大小' })}: {(selectedZip.size / 1024 / 1024).toFixed(2)} MB
|
|||
|
|
</Typography>
|
|||
|
|
)}
|
|||
|
|
</Paper>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</DialogContent>
|
|||
|
|
<DialogActions>
|
|||
|
|
<Button onClick={handleClose} disabled={loading}>
|
|||
|
|
{t('common.cancel')}
|
|||
|
|
</Button>
|
|||
|
|
{mode === 0 ? (
|
|||
|
|
<Button
|
|||
|
|
onClick={handleImport}
|
|||
|
|
variant="contained"
|
|||
|
|
disabled={loading || directories.length === 0}
|
|||
|
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
|||
|
|
>
|
|||
|
|
{t('images.startImport')}
|
|||
|
|
</Button>
|
|||
|
|
) : mode === 1 ? (
|
|||
|
|
<Button
|
|||
|
|
onClick={handlePdfImport}
|
|||
|
|
variant="contained"
|
|||
|
|
disabled={loading || !selectedPdf}
|
|||
|
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
|||
|
|
>
|
|||
|
|
{t('images.convertAndImport', { defaultValue: '转换并导入' })}
|
|||
|
|
</Button>
|
|||
|
|
) : (
|
|||
|
|
<Button
|
|||
|
|
onClick={handleZipImport}
|
|||
|
|
variant="contained"
|
|||
|
|
disabled={loading || !selectedZip}
|
|||
|
|
startIcon={loading ? <CircularProgress size={20} /> : null}
|
|||
|
|
>
|
|||
|
|
{t('images.extractAndImport', { defaultValue: '解压并导入' })}
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</DialogActions>
|
|||
|
|
</Dialog>
|
|||
|
|
);
|
|||
|
|
}
|