Files
YG-Datasets/easy-dataset-main/components/home/MigrationDialog.js

301 lines
9.1 KiB
JavaScript
Raw 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 {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
CircularProgress,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Alert,
Paper,
useTheme,
Tooltip
} from '@mui/material';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';
/**
* 项目迁移对话框组件
* @param {Object} props - 组件属性
* @param {boolean} props.open - 对话框是否打开
* @param {Function} props.onClose - 关闭对话框的回调函数
* @param {Array<string>} props.projectIds - 需要迁移的项目ID列表
*/
export default function MigrationDialog({ open, onClose, projectIds = [] }) {
const { t } = useTranslation();
const theme = useTheme();
const [migrating, setMigrating] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
const [migratedCount, setMigratedCount] = useState(0);
const [taskId, setTaskId] = useState(null);
const [progress, setProgress] = useState(0);
const [statusText, setStatusText] = useState('');
const [processingIds, setProcessingIds] = useState([]);
// 打开项目目录
const handleOpenDirectory = async projectId => {
try {
setProcessingIds(prev => [...prev, projectId]);
const response = await fetch('/api/projects/open-directory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ projectId })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || t('migration.openDirectoryFailed'));
}
// 成功打开目录,不需要特别处理
} catch (err) {
console.error('打开目录错误:', err);
setError(err.message);
} finally {
setProcessingIds(prev => prev.filter(id => id !== projectId));
}
};
// 删除项目目录
const handleDeleteDirectory = async projectId => {
try {
if (!window.confirm(t('migration.confirmDelete'))) {
return;
}
setProcessingIds(prev => [...prev, projectId]);
const response = await fetch('/api/projects/delete-directory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ projectId })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || t('migration.deleteDirectoryFailed'));
}
// 从列表中移除已删除的项目
const updatedProjectIds = projectIds.filter(id => id !== projectId);
// 这里我们不能直接修改 projectIds因为它是从父组件传入的
// 但我们可以通知用户界面刷新
window.location.reload();
} catch (err) {
console.error('删除目录错误:', err);
setError(err.message);
} finally {
setProcessingIds(prev => prev.filter(id => id !== projectId));
}
};
// 处理迁移操作
const handleMigration = async () => {
try {
setMigrating(true);
setError(null);
setSuccess(false);
setProgress(0);
setStatusText(t('migration.starting'));
// 调用异步迁移接口启动迁移任务
const response = await fetch('/api/projects/migrate', {
method: 'POST'
});
if (!response.ok) {
throw new Error(t('migration.failed'));
}
const { success, taskId: newTaskId } = await response.json();
if (!success || !newTaskId) {
throw new Error(t('migration.startFailed'));
}
// 保存任务ID
setTaskId(newTaskId);
setStatusText(t('migration.processing'));
// 开始轮询任务状态
await pollMigrationStatus(newTaskId);
} catch (err) {
console.error('迁移错误:', err);
setError(err.message);
setMigrating(false);
}
};
// 轮询迁移任务状态
const pollMigrationStatus = async id => {
try {
// 定义轮询间隔(毫秒)
const pollInterval = 1000;
// 发送请求获取任务状态
const response = await fetch(`/api/projects/migrate?taskId=${id}`);
if (!response.ok) {
throw new Error(t('migration.statusFailed'));
}
const { success, task } = await response.json();
if (!success || !task) {
throw new Error(t('migration.taskNotFound'));
}
// 更新进度
setProgress(task.progress || 0);
// 根据任务状态更新UI
if (task.status === 'completed') {
// 任务完成
setMigratedCount(task.completed);
setSuccess(true);
setMigrating(false);
setStatusText(t('migration.completed'));
// 迁移成功后,延迟关闭对话框并刷新页面
setTimeout(() => {
onClose();
window.location.reload();
}, 2000);
} else if (task.status === 'failed') {
// 任务失败
throw new Error(task.error || t('migration.failed'));
} else {
// 任务仍在进行中,继续轮询
setTimeout(() => pollMigrationStatus(id), pollInterval);
// 更新状态文本
if (task.total > 0) {
setStatusText(
t('migration.progressStatus', {
completed: task.completed || 0,
total: task.total
})
);
}
}
} catch (err) {
console.error('获取迁移状态错误:', err);
setError(err.message);
setMigrating(false);
}
};
return (
<Dialog open={open} onClose={migrating ? undefined : onClose} maxWidth="sm" fullWidth>
<DialogTitle
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5
}}
>
<WarningAmberIcon color="warning" />
<Typography variant="h6">{t('migration.title')}</Typography>
</DialogTitle>
<DialogContent>
{success ? (
<Alert severity="success" sx={{ mb: 2 }}>
{t('migration.success', { count: migratedCount })}
</Alert>
) : error ? (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
) : null}
<Typography variant="body1" sx={{ mb: 2 }}>
{t('migration.description')}
</Typography>
{projectIds.length > 0 && (
<Box sx={{ mt: 2, mb: 2 }}>
<Typography variant="subtitle1" sx={{ mb: 1 }}>
{t('migration.projectsList')}:
</Typography>
<Paper variant="outlined" sx={{ maxHeight: 180, overflow: 'auto' }}>
<List dense>
{projectIds.map(id => (
<ListItem key={id}>
<ListItemText primary={id} />
<ListItemSecondaryAction>
<Tooltip title={t('migration.openDirectory')}>
<IconButton
edge="end"
aria-label="open"
onClick={() => handleOpenDirectory(id)}
disabled={processingIds.includes(id)}
size="small"
>
<FolderOpenIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('migration.deleteDirectory')}>
<IconButton
edge="end"
aria-label="delete"
onClick={() => handleDeleteDirectory(id)}
disabled={processingIds.includes(id)}
size="small"
sx={{ ml: 1, color: 'error.main' }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Paper>
</Box>
)}
{migrating && (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 3, gap: 1.5 }}>
<CircularProgress variant={progress > 0 ? 'determinate' : 'indeterminate'} value={progress} />
<Typography variant="body2" color="text.secondary">
{statusText || t('migration.migrating')}
</Typography>
{progress > 0 && (
<Typography variant="body2" color="text.secondary">
{progress}%
</Typography>
)}
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={migrating}>
{t('common.cancel')}
</Button>
<Button onClick={handleMigration} variant="contained" color="primary" disabled={migrating || success}>
{migrating ? t('migration.migrating') : t('migration.migrate')}
</Button>
</DialogActions>
</Dialog>
);
}