diff --git a/frontend/src/components/FileManager/ServerFilePanel.tsx b/frontend/src/components/FileManager/ServerFilePanel.tsx index 34268bf1..bfe300fa 100644 --- a/frontend/src/components/FileManager/ServerFilePanel.tsx +++ b/frontend/src/components/FileManager/ServerFilePanel.tsx @@ -5,6 +5,16 @@ import React, { useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Skeleton } from '@/components/ui/skeleton'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { ChevronRight, Folder, File, Home, ArrowLeft, Plus, Trash2, RefreshCw, Link } from 'lucide-react'; import { browseDirectory, createDirectory, removeFile, pathUtils, type RemoteFileInfo } from '@/services/fileService'; import { message } from 'antd'; @@ -20,6 +30,8 @@ export const ServerFilePanel: React.FC = ({ const [loading, setLoading] = useState(false); const [showNewFolder, setShowNewFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(''); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + const [pendingDeleteFile, setPendingDeleteFile] = useState(null); // 加载文件列表 const loadFiles = async () => { @@ -81,24 +93,34 @@ export const ServerFilePanel: React.FC = ({ } }; - // 删除文件/目录 - const handleDelete = async (file: RemoteFileInfo) => { - const confirmed = window.confirm( - file.isDirectory - ? `确定删除目录 "${file.name}" 及其所有内容吗?` - : `确定删除文件 "${file.name}" 吗?` - ); - if (!confirmed) return; + // 请求删除文件/目录(弹出确认框) + const handleDelete = (file: RemoteFileInfo) => { + setPendingDeleteFile(file); + setDeleteConfirmOpen(true); + }; + + // 确认删除 + const handleConfirmDelete = async () => { + setDeleteConfirmOpen(false); + if (!pendingDeleteFile) return; try { - await removeFile(serverId, file.path, file.isDirectory); + await removeFile(serverId, pendingDeleteFile.path, pendingDeleteFile.isDirectory); message.success('删除成功'); loadFiles(); } catch (error: any) { message.error(error.response?.data?.message || '删除失败'); + } finally { + setPendingDeleteFile(null); } }; + // 取消删除 + const handleCancelDelete = () => { + setDeleteConfirmOpen(false); + setPendingDeleteFile(null); + }; + // 面包屑导航 const breadcrumbs = currentPath.split('/').filter(Boolean); @@ -299,6 +321,36 @@ export const ServerFilePanel: React.FC = ({ )} + + {/* 删除确认对话框 */} + + + + 确认删除 + + {pendingDeleteFile?.isDirectory ? ( + <> + 确定删除目录 "{pendingDeleteFile.name}" 及其所有内容吗? +
+ 此操作不可恢复! + + ) : ( + <> + 确定删除文件 "{pendingDeleteFile?.name}" 吗? + + )} +
+
+ + + 取消 + + + 删除 + + +
+
); }; diff --git a/frontend/src/components/FileManager/UploadPanel.tsx b/frontend/src/components/FileManager/UploadPanel.tsx index de4c5059..1a87d1cf 100644 --- a/frontend/src/components/FileManager/UploadPanel.tsx +++ b/frontend/src/components/FileManager/UploadPanel.tsx @@ -4,8 +4,18 @@ import React, { useState, useRef, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { Upload, X, File as FileIcon, CheckCircle, AlertCircle } from 'lucide-react'; -import { submitAsyncUpload, getUploadTaskStatus, cancelUploadTask, pathUtils } from '@/services/fileService'; +import { submitAsyncUpload, getUploadTaskStatus, cancelUploadTask, pathUtils, browseDirectory } from '@/services/fileService'; import type { UploadTaskStatus } from '@/services/fileService'; import { message } from 'antd'; import type { UploadPanelProps } from './types'; @@ -27,6 +37,11 @@ export const UploadPanel: React.FC = ({ }) => { const [uploadFiles, setUploadFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); + const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); + const [pendingOverwriteItem, setPendingOverwriteItem] = useState(null); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + const [pendingDeleteId, setPendingDeleteId] = useState(null); + const [clearConfirmOpen, setClearConfirmOpen] = useState(false); const fileInputRef = useRef(null); const pollIntervalsRef = useRef>(new Map()); // 保存所有轮询定时器 @@ -61,22 +76,82 @@ export const UploadPanel: React.FC = ({ setUploadFiles(prev => [...prev, ...newFiles]); }; - // 从列表移除文件 - const removeFile = (id: string) => { + // 请求删除文件(弹出确认框) + const requestRemoveFile = (id: string) => { + const item = uploadFiles.find(f => f.id === id); + // 正在上传的需要确认 + if (item?.status === 'uploading') { + setPendingDeleteId(id); + setDeleteConfirmOpen(true); + } else { + // 其他状态直接删除 + doRemoveFile(id); + } + }; + + // 实际执行删除文件或取消上传 + const doRemoveFile = async (id: string) => { + const item = uploadFiles.find(f => f.id === id); + + // 如果正在上传,先取消任务 + if (item?.status === 'uploading' && item.taskId) { + try { + await cancelUploadTask(serverId, item.taskId); + message.success('上传已取消'); + } catch (error) { + console.error('取消上传失败:', error); + } + } + // 停止轮询 const interval = pollIntervalsRef.current.get(id); if (interval) { clearInterval(interval); pollIntervalsRef.current.delete(id); } + // 移除文件 setUploadFiles(prev => prev.filter(f => f.id !== id)); }; + // 确认删除 + const handleConfirmDelete = async () => { + setDeleteConfirmOpen(false); + if (pendingDeleteId) { + await doRemoveFile(pendingDeleteId); + setPendingDeleteId(null); + } + }; + + // 取消删除 + const handleCancelDelete = () => { + setDeleteConfirmOpen(false); + setPendingDeleteId(null); + }; + // 上传单个文件(异步上传+轮询) - const uploadSingleFile = async (item: UploadFileItem) => { + const uploadSingleFile = async (item: UploadFileItem, overwrite: boolean = false, skipCheck: boolean = false) => { const remotePath = pathUtils.join(currentPath, item.file.name); + // 如果不是覆盖模式且未跳过检查,先检查文件是否已存在 + if (!overwrite && !skipCheck) { + try { + // 使用browseDirectory API检查文件是否存在 + const files = await browseDirectory(serverId, currentPath); + const fileExists = files.some(f => f.name === item.file.name && !f.isDirectory); + + if (fileExists) { + // 文件已存在,弹出确认对话框 + setPendingOverwriteItem(item); + setConfirmDialogOpen(true); + return; // 暂停上传,等待用户决定 + } + } catch (error) { + console.log('检查文件是否存在时出错:', error); + // 检查失败,继续上传 + } + } + try { // 1. 提交异步上传任务 setUploadFiles(prev => @@ -87,7 +162,7 @@ export const UploadPanel: React.FC = ({ ) ); - const result = await submitAsyncUpload(serverId, item.file, remotePath, false); + const result = await submitAsyncUpload(serverId, item.file, remotePath, overwrite); const taskId = result.taskId; // 2. 保存taskId @@ -101,6 +176,8 @@ export const UploadPanel: React.FC = ({ startPolling(item.id, taskId); } catch (error: any) { const errorMsg = error.response?.data?.message || '提交上传任务失败'; + + // 直接显示后端返回的错误信息 setUploadFiles(prev => prev.map(f => f.id === item.id @@ -112,6 +189,32 @@ export const UploadPanel: React.FC = ({ } }; + // 处理用户确认覆盖 + const handleConfirmOverwrite = async () => { + setConfirmDialogOpen(false); + if (pendingOverwriteItem) { + // 以覆盖模式重新上传,跳过检查 + await uploadSingleFile(pendingOverwriteItem, true, true); + setPendingOverwriteItem(null); + } + }; + + // 处理用户取消覆盖 + const handleCancelOverwrite = () => { + setConfirmDialogOpen(false); + if (pendingOverwriteItem) { + // 标记为错误状态 + setUploadFiles(prev => + prev.map(f => + f.id === pendingOverwriteItem.id + ? { ...f, status: 'error', errorMessage: '用户取消覆盖' } + : f + ) + ); + setPendingOverwriteItem(null); + } + }; + // 轮询查询上传状态 const startPolling = (fileId: string, taskId: string) => { const pollInterval = setInterval(async () => { @@ -143,6 +246,8 @@ export const UploadPanel: React.FC = ({ clearInterval(pollInterval); pollIntervalsRef.current.delete(fileId); const errorMsg = taskInfo.errorMessage || '上传失败'; + + // 直接显示后端返回的错误信息 setUploadFiles(prev => prev.map(f => f.id === fileId @@ -150,7 +255,7 @@ export const UploadPanel: React.FC = ({ : f ) ); - message.error(`${taskInfo.fileName}: ${errorMsg}`); + message.error(`上传失败: ${errorMsg}`); } else if (taskInfo.status === 'CANCELLED') { clearInterval(pollInterval); pollIntervalsRef.current.delete(fileId); @@ -181,9 +286,21 @@ export const UploadPanel: React.FC = ({ } }; - // 清空列表 + // 请求清空列表(弹出确认框) const handleClearList = () => { + setClearConfirmOpen(true); + }; + + // 确认清空列表 + const handleConfirmClear = () => { + setClearConfirmOpen(false); setUploadFiles([]); + message.success('列表已清空'); + }; + + // 取消清空 + const handleCancelClear = () => { + setClearConfirmOpen(false); }; // 拖拽事件处理 @@ -311,16 +428,22 @@ export const UploadPanel: React.FC = ({ {item.status === 'error' && ( )} - {item.status === 'pending' && ( - - )} + {/* 所有状态都显示X按钮 */} +
@@ -353,6 +476,66 @@ export const UploadPanel: React.FC = ({
)} + + {/* 覆盖确认对话框 */} + + + + 文件已存在 + + 文件 {pendingOverwriteItem?.file.name} 已存在,是否覆盖? + + + + + 取消 + + + 覆盖 + + + + + + {/* 删除确认对话框 */} + + + + 取消上传 + + 确定要取消正在上传的文件吗? + + + + + 取消 + + + 确定 + + + + + + {/* 清空列表确认对话框 */} + + + + 清空列表 + + 确定要清空所有上传记录吗?此操作不可恢复。 + + + + + 取消 + + + 确定清空 + + + + ); }; diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index 27a5c63b..c1ad5b63 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -16,9 +16,10 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( @@ -34,9 +35,10 @@ const AlertDialogContent = React.forwardRef<