重写ssh前端组件,通用化
This commit is contained in:
parent
b8412314a8
commit
7309d05bcb
@ -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<ServerFilePanelProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showNewFolder, setShowNewFolder] = useState(false);
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [pendingDeleteFile, setPendingDeleteFile] = useState<RemoteFileInfo | null>(null);
|
||||
|
||||
// 加载文件列表
|
||||
const loadFiles = async () => {
|
||||
@ -81,24 +93,34 @@ export const ServerFilePanel: React.FC<ServerFilePanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件/目录
|
||||
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<ServerFilePanelProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{pendingDeleteFile?.isDirectory ? (
|
||||
<>
|
||||
确定删除目录 <span className="font-semibold text-foreground">"{pendingDeleteFile.name}"</span> 及其所有内容吗?
|
||||
<br />
|
||||
<span className="text-red-600">此操作不可恢复!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
确定删除文件 <span className="font-semibold text-foreground">"{pendingDeleteFile?.name}"</span> 吗?
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancelDelete}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete} className="bg-red-600 hover:bg-red-700">
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<UploadPanelProps> = ({
|
||||
}) => {
|
||||
const [uploadFiles, setUploadFiles] = useState<UploadFileItem[]>([]);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [pendingOverwriteItem, setPendingOverwriteItem] = useState<UploadFileItem | null>(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const pollIntervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map()); // 保存所有轮询定时器
|
||||
|
||||
@ -61,22 +76,82 @@ export const UploadPanel: React.FC<UploadPanelProps> = ({
|
||||
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<UploadPanelProps> = ({
|
||||
)
|
||||
);
|
||||
|
||||
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<UploadPanelProps> = ({
|
||||
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<UploadPanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理用户确认覆盖
|
||||
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<UploadPanelProps> = ({
|
||||
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<UploadPanelProps> = ({
|
||||
: 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<UploadPanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 清空列表
|
||||
// 请求清空列表(弹出确认框)
|
||||
const handleClearList = () => {
|
||||
setClearConfirmOpen(true);
|
||||
};
|
||||
|
||||
// 确认清空列表
|
||||
const handleConfirmClear = () => {
|
||||
setClearConfirmOpen(false);
|
||||
setUploadFiles([]);
|
||||
message.success('列表已清空');
|
||||
};
|
||||
|
||||
// 取消清空
|
||||
const handleCancelClear = () => {
|
||||
setClearConfirmOpen(false);
|
||||
};
|
||||
|
||||
// 拖拽事件处理
|
||||
@ -311,16 +428,22 @@ export const UploadPanel: React.FC<UploadPanelProps> = ({
|
||||
{item.status === 'error' && (
|
||||
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
{item.status === 'pending' && (
|
||||
{/* 所有状态都显示X按钮 */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => removeFile(item.id)}
|
||||
onClick={() => requestRemoveFile(item.id)}
|
||||
title={
|
||||
item.status === 'uploading'
|
||||
? '取消上传'
|
||||
: item.status === 'pending'
|
||||
? '删除'
|
||||
: '删除记录'
|
||||
}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
@ -353,6 +476,66 @@ export const UploadPanel: React.FC<UploadPanelProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 覆盖确认对话框 */}
|
||||
<AlertDialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>文件已存在</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
文件 <span className="font-semibold text-foreground">{pendingOverwriteItem?.file.name}</span> 已存在,是否覆盖?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancelOverwrite}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmOverwrite}>
|
||||
覆盖
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>取消上传</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要取消正在上传的文件吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancelDelete}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete}>
|
||||
确定
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 清空列表确认对话框 */}
|
||||
<AlertDialog open={clearConfirmOpen} onOpenChange={setClearConfirmOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>清空列表</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要清空所有上传记录吗?此操作不可恢复。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancelClear}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmClear}>
|
||||
确定清空
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -16,9 +16,10 @@ const AlertDialogOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
style={{ zIndex: 1100 }}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
@ -34,9 +35,10 @@ const AlertDialogContent = React.forwardRef<
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
style={{ zIndex: 1101 }}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user