diff --git a/desktop-client/src-tauri/Cargo.lock b/desktop-client/src-tauri/Cargo.lock index d67d112..539effd 100644 --- a/desktop-client/src-tauri/Cargo.lock +++ b/desktop-client/src-tauri/Cargo.lock @@ -3116,12 +3116,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams 0.4.2", "web-sys", "webpki-roots", ] @@ -3156,7 +3158,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -4716,6 +4718,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" diff --git a/desktop-client/src-tauri/Cargo.toml b/desktop-client/src-tauri/Cargo.toml index 15ee5bc..08e3c94 100644 --- a/desktop-client/src-tauri/Cargo.toml +++ b/desktop-client/src-tauri/Cargo.toml @@ -22,5 +22,5 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -reqwest = { version = "0.12", default-features = false, features = ["json", "cookies", "multipart", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "cookies", "multipart", "stream", "rustls-tls"] } urlencoding = "2.1" diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index 73c15f5..4df5698 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -620,8 +620,6 @@ async fn api_upload_file( .and_then(|name| name.to_str()) .map(|name| name.to_string()) .ok_or_else(|| "无法识别文件名".to_string())?; - - let file_bytes = fs::read(&source_path).map_err(|err| format!("读取文件失败: {}", err))?; let normalized_target = if target_path.trim().is_empty() { "/".to_string() } else { @@ -634,9 +632,15 @@ async fn api_upload_file( return Err("API 地址不能为空".to_string()); } + // 使用流式 multipart 上传,避免大文件整块读入内存导致占用暴涨。 + let file_part = reqwest::multipart::Part::file(&source_path) + .await + .map_err(|err| format!("读取文件失败: {}", err))? + .file_name(file_name); + let multipart = reqwest::multipart::Form::new() .text("path", normalized_target) - .part("file", reqwest::multipart::Part::bytes(file_bytes).file_name(file_name)); + .part("file", file_part); let mut request = state .client diff --git a/desktop-client/src/App.vue b/desktop-client/src/App.vue index e27bd34..2f6ba7b 100644 --- a/desktop-client/src/App.vue +++ b/desktop-client/src/App.vue @@ -70,7 +70,7 @@ const selectedFileName = ref(""); const searchKeyword = ref(""); const shares = ref([]); -const transferTasks = ref<{ id: string; name: string; speed: string; progress: number; status: string }[]>([]); +const transferTasks = ref<{ id: string; name: string; speed: string; progress: number; status: string; note?: string }[]>([]); const sharesLoading = ref(false); const contextMenu = reactive({ visible: false, @@ -280,17 +280,25 @@ function showToast(message: string, type = "info") { }, 2500); } -function prependTransferTask(task: { id: string; name: string; speed: string; progress: number; status: string }) { +function prependTransferTask(task: { id: string; name: string; speed: string; progress: number; status: string; note?: string }) { transferTasks.value = [task, ...transferTasks.value.slice(0, 49)]; } function updateTransferTask( id: string, - patch: Partial<{ name: string; speed: string; progress: number; status: string }>, + patch: Partial<{ name: string; speed: string; progress: number; status: string; note?: string }>, ) { transferTasks.value = transferTasks.value.map((task) => (task.id === id ? { ...task, ...patch } : task)); } +function getTaskStatusLabel(status: string) { + if (status === "uploading") return "上传中"; + if (status === "downloading") return "下载中"; + if (status === "done") return "已完成"; + if (status === "failed") return "失败"; + return status; +} + async function invokeBridge(command: string, payload: Record) { try { return await invoke(command, payload); @@ -624,14 +632,15 @@ async function downloadSelected(target?: FileItem | null) { }); if (nativeResponse.ok && nativeResponse.data?.success) { - updateTransferTask(taskId, { speed: "-", progress: 100, status: "done" }); + updateTransferTask(taskId, { speed: "-", progress: 100, status: "done", note: "下载成功" }); const savedPath = nativeResponse.data?.savePath ? `\n${nativeResponse.data.savePath}` : ""; showToast(`下载完成${savedPath}`, "success"); return; } - updateTransferTask(taskId, { speed: "-", progress: 0, status: "failed" }); - showToast(nativeResponse.data?.message || "原生下载失败", "error"); + const message = String(nativeResponse.data?.message || "原生下载失败"); + updateTransferTask(taskId, { speed: "-", progress: 0, status: "failed", note: message }); + showToast(message, "error"); } function selectFile(item: FileItem) { @@ -764,13 +773,16 @@ async function uploadDroppedFiles(paths: string[]) { speed: "-", progress: 100, status: "done", + note: "上传成功", }); } else { dropState.failed += 1; + const message = String(response.data?.message || "上传失败"); updateTransferTask(taskId, { speed: "-", progress: 0, status: "failed", + note: message, }); } } @@ -802,7 +814,12 @@ async function registerDragDropListener() { if (payload.type === "drop") { dropState.active = false; - if (!canUseDragUpload()) return; + if (!canUseDragUpload()) { + if (authenticated.value) { + showToast("请先切换到“全部文件”页面再上传", "info"); + } + return; + } void uploadDroppedFiles(payload.paths || []); } }); @@ -995,7 +1012,8 @@ onBeforeUnmount(() => {
{{ task.name }} - {{ task.id }} · {{ task.status }} + {{ task.id }} · {{ getTaskStatusLabel(task.status) }} + {{ task.note }}
{{ task.speed }} @@ -1639,9 +1657,19 @@ select:focus { } .task-row small { + display: block; color: #647b94; } +.task-note { + margin-top: 2px; + color: #5f7895; +} + +.task-note.error { + color: #c24747; +} + .task-right { display: grid; gap: 6px;