Files

301 lines
9.1 KiB
JavaScript
Raw Permalink Normal View History

2026-03-17 14:36:31 +08:00
'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>
);
}