From bb6985341f06865f0fd3fc320b51a2c0727cdbc8 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sun, 7 Dec 2025 02:21:22 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99ssh=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=80=9A=E7=94=A8=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FileManager/ServerFilePanel.tsx | 10 +- .../components/FileManager/UploadPanel.tsx | 134 ++++++++++++++---- frontend/src/services/fileService.ts | 95 ++++++++++++- 3 files changed, 212 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/FileManager/ServerFilePanel.tsx b/frontend/src/components/FileManager/ServerFilePanel.tsx index 9b791ea8..34268bf1 100644 --- a/frontend/src/components/FileManager/ServerFilePanel.tsx +++ b/frontend/src/components/FileManager/ServerFilePanel.tsx @@ -144,7 +144,13 @@ export const ServerFilePanel: React.FC = ({ {/* 路径导航 */}
- / + {breadcrumbs.map((name, index) => ( @@ -236,7 +242,7 @@ export const ServerFilePanel: React.FC = ({ > {/* 文件名 */}
handleEnterDirectory(file)} title={file.isSymlink ? `链接到: ${file.linkTarget || '未知'}` : file.name} > diff --git a/frontend/src/components/FileManager/UploadPanel.tsx b/frontend/src/components/FileManager/UploadPanel.tsx index 0af63393..de4c5059 100644 --- a/frontend/src/components/FileManager/UploadPanel.tsx +++ b/frontend/src/components/FileManager/UploadPanel.tsx @@ -1,13 +1,24 @@ /** - * 文件上传面板 + * 文件上传面板 - 异步上传 + 轮询方案 */ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Upload, X, File as FileIcon, CheckCircle, AlertCircle } from 'lucide-react'; -import { uploadFile, pathUtils } from '@/services/fileService'; +import { submitAsyncUpload, getUploadTaskStatus, cancelUploadTask, pathUtils } from '@/services/fileService'; +import type { UploadTaskStatus } from '@/services/fileService'; import { message } from 'antd'; -import type { UploadPanelProps, UploadFileItem } from './types'; +import type { UploadPanelProps } from './types'; + +// 本地扩展的上传文件项类型 +interface UploadFileItem { + file: File; + id: string; + status: 'pending' | 'uploading' | 'success' | 'error'; + progress: number; + taskId?: string; // 异步上传任务ID + errorMessage?: string; +} export const UploadPanel: React.FC = ({ serverId, @@ -17,6 +28,24 @@ export const UploadPanel: React.FC = ({ const [uploadFiles, setUploadFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); + const pollIntervalsRef = useRef>(new Map()); // 保存所有轮询定时器 + + // 组件卸载时清理所有定时器 + useEffect(() => { + return () => { + pollIntervalsRef.current.forEach(interval => clearInterval(interval)); + pollIntervalsRef.current.clear(); + }; + }, []); + + // 格式化文件大小 + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; + }; // 添加文件到上传列表 const addFiles = (files: FileList | null) => { @@ -34,14 +63,22 @@ export const UploadPanel: React.FC = ({ // 从列表移除文件 const removeFile = (id: string) => { + // 停止轮询 + const interval = pollIntervalsRef.current.get(id); + if (interval) { + clearInterval(interval); + pollIntervalsRef.current.delete(id); + } + // 移除文件 setUploadFiles(prev => prev.filter(f => f.id !== id)); }; - // 上传单个文件 + // 上传单个文件(异步上传+轮询) const uploadSingleFile = async (item: UploadFileItem) => { const remotePath = pathUtils.join(currentPath, item.file.name); try { + // 1. 提交异步上传任务 setUploadFiles(prev => prev.map(f => f.id === item.id @@ -50,31 +87,20 @@ export const UploadPanel: React.FC = ({ ) ); - await uploadFile( - serverId, - item.file, - remotePath, - false, - (progress) => { - setUploadFiles(prev => - prev.map(f => - f.id === item.id ? { ...f, progress } : f - ) - ); - } - ); + const result = await submitAsyncUpload(serverId, item.file, remotePath, false); + const taskId = result.taskId; + // 2. 保存taskId setUploadFiles(prev => prev.map(f => - f.id === item.id - ? { ...f, status: 'success', progress: 100 } - : f + f.id === item.id ? { ...f, taskId } : f ) ); - onUploadComplete(); + // 3. 开始轮询查询状态 + startPolling(item.id, taskId); } catch (error: any) { - const errorMsg = error.response?.data?.message || '上传失败'; + const errorMsg = error.response?.data?.message || '提交上传任务失败'; setUploadFiles(prev => prev.map(f => f.id === item.id @@ -86,6 +112,66 @@ export const UploadPanel: React.FC = ({ } }; + // 轮询查询上传状态 + const startPolling = (fileId: string, taskId: string) => { + const pollInterval = setInterval(async () => { + try { + const taskInfo = await getUploadTaskStatus(serverId, taskId); + + // 更新进度 + setUploadFiles(prev => + prev.map(f => + f.id === fileId + ? { ...f, progress: taskInfo.progress } + : f + ) + ); + + // 检查是否完成 + if (taskInfo.status === 'SUCCESS') { + clearInterval(pollInterval); + pollIntervalsRef.current.delete(fileId); + setUploadFiles(prev => + prev.map(f => + f.id === fileId + ? { ...f, status: 'success', progress: 100 } + : f + ) + ); + onUploadComplete(); + } else if (taskInfo.status === 'FAILED') { + clearInterval(pollInterval); + pollIntervalsRef.current.delete(fileId); + const errorMsg = taskInfo.errorMessage || '上传失败'; + setUploadFiles(prev => + prev.map(f => + f.id === fileId + ? { ...f, status: 'error', errorMessage: errorMsg } + : f + ) + ); + message.error(`${taskInfo.fileName}: ${errorMsg}`); + } else if (taskInfo.status === 'CANCELLED') { + clearInterval(pollInterval); + pollIntervalsRef.current.delete(fileId); + setUploadFiles(prev => + prev.map(f => + f.id === fileId + ? { ...f, status: 'error', errorMessage: '已取消' } + : f + ) + ); + } + } catch (error) { + // 查询失败,继续轮询(可能是网络问题) + console.error('轮询查询失败:', error); + } + }, 2000); // 每2秒查询一次 + + // 保存定时器到ref + pollIntervalsRef.current.set(fileId, pollInterval); + }; + // 上传所有待上传文件 const handleUploadAll = async () => { const pendingFiles = uploadFiles.filter(f => f.status === 'pending'); @@ -239,7 +325,7 @@ export const UploadPanel: React.FC = ({
- {(item.file.size / 1024).toFixed(1)} KB + {formatFileSize(item.file.size)} {item.status === 'uploading' && ( diff --git a/frontend/src/services/fileService.ts b/frontend/src/services/fileService.ts index 25217c52..e245d0d3 100644 --- a/frontend/src/services/fileService.ts +++ b/frontend/src/services/fileService.ts @@ -34,6 +34,45 @@ export interface FileUploadResult { errorMessage: string | null; } +// 异步上传相关类型 +export type UploadTaskStatus = 'PENDING' | 'UPLOADING' | 'SUCCESS' | 'FAILED' | 'CANCELLED'; + +export interface AsyncUploadResponse { + taskId: string; + message: string; +} + +export interface UploadTaskInfo { + taskId: string; + fileName: string; + fileSize: number; + fileSizeFormatted: string; + uploadedSize: number; + uploadedSizeFormatted: string; + progress: number; + status: UploadTaskStatus; + metadata?: { + serverId: number; + remotePath: string; + overwrite: boolean; + }; + errorMessage?: string; + createTime?: string; + updateTime?: string; +} + +export interface UploadProgressMessage { + taskId: string; + status: UploadTaskStatus; + progress: number; + uploaded: number; + total: number; + uploadedFormatted: string; + totalFormatted: string; + errorMessage?: string; + timestamp: number; +} + /** * 浏览目录 */ @@ -73,7 +112,7 @@ export const removeFile = async ( }; /** - * 上传文件 + * 上传文件(同步,适用于小文件) */ export const uploadFile = async ( serverId: number, @@ -109,6 +148,60 @@ export const uploadFile = async ( return response; // request拦截器已经提取了data字段 }; +/** + * 提交异步上传任务 + */ +export const submitAsyncUpload = async ( + serverId: number, + file: File, + remotePath: string, + overwrite?: boolean +): Promise => { + const formData = new FormData(); + formData.append('file', file); + formData.append('remotePath', remotePath); + if (overwrite !== undefined) { + formData.append('overwrite', String(overwrite)); + } + + const response = await request.post( + `${API_BASE}/${serverId}/files/upload-async`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + return response; +}; + +/** + * 查询上传任务状态 + */ +export const getUploadTaskStatus = async ( + serverId: number, + taskId: string +): Promise => { + const response = await request.get( + `${API_BASE}/${serverId}/files/upload-task/${taskId}` + ); + return response; +}; + +/** + * 取消上传任务 + */ +export const cancelUploadTask = async ( + serverId: number, + taskId: string +): Promise<{ cancelled: boolean }> => { + const response = await request.delete( + `${API_BASE}/${serverId}/files/upload-task/${taskId}` + ); + return response; +}; + /** * 重命名文件/目录 */