perf(desktop): stream drag-upload and improve transfer status UX

This commit is contained in:
2026-02-18 19:50:34 +08:00
parent 09043e8059
commit d4818a78d3
4 changed files with 60 additions and 13 deletions

View File

@@ -3116,12 +3116,14 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-util",
"tower", "tower",
"tower-http", "tower-http",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams 0.4.2",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots",
] ]
@@ -3156,7 +3158,7 @@ dependencies = [
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams 0.5.0",
"web-sys", "web-sys",
] ]
@@ -4716,6 +4718,19 @@ dependencies = [
"wasmparser", "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]] [[package]]
name = "wasm-streams" name = "wasm-streams"
version = "0.5.0" version = "0.5.0"

View File

@@ -22,5 +22,5 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" 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" urlencoding = "2.1"

View File

@@ -620,8 +620,6 @@ async fn api_upload_file(
.and_then(|name| name.to_str()) .and_then(|name| name.to_str())
.map(|name| name.to_string()) .map(|name| name.to_string())
.ok_or_else(|| "无法识别文件名".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() { let normalized_target = if target_path.trim().is_empty() {
"/".to_string() "/".to_string()
} else { } else {
@@ -634,9 +632,15 @@ async fn api_upload_file(
return Err("API 地址不能为空".to_string()); 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() let multipart = reqwest::multipart::Form::new()
.text("path", normalized_target) .text("path", normalized_target)
.part("file", reqwest::multipart::Part::bytes(file_bytes).file_name(file_name)); .part("file", file_part);
let mut request = state let mut request = state
.client .client

View File

@@ -70,7 +70,7 @@ const selectedFileName = ref("");
const searchKeyword = ref(""); const searchKeyword = ref("");
const shares = ref<ShareItem[]>([]); const shares = ref<ShareItem[]>([]);
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 sharesLoading = ref(false);
const contextMenu = reactive({ const contextMenu = reactive({
visible: false, visible: false,
@@ -280,17 +280,25 @@ function showToast(message: string, type = "info") {
}, 2500); }, 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)]; transferTasks.value = [task, ...transferTasks.value.slice(0, 49)];
} }
function updateTransferTask( function updateTransferTask(
id: string, 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)); 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<string, any>) { async function invokeBridge(command: string, payload: Record<string, any>) {
try { try {
return await invoke<BridgeResponse>(command, payload); return await invoke<BridgeResponse>(command, payload);
@@ -624,14 +632,15 @@ async function downloadSelected(target?: FileItem | null) {
}); });
if (nativeResponse.ok && nativeResponse.data?.success) { 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}` : ""; const savedPath = nativeResponse.data?.savePath ? `\n${nativeResponse.data.savePath}` : "";
showToast(`下载完成${savedPath}`, "success"); showToast(`下载完成${savedPath}`, "success");
return; return;
} }
updateTransferTask(taskId, { speed: "-", progress: 0, status: "failed" }); const message = String(nativeResponse.data?.message || "原生下载失败");
showToast(nativeResponse.data?.message || "原生下载失败", "error"); updateTransferTask(taskId, { speed: "-", progress: 0, status: "failed", note: message });
showToast(message, "error");
} }
function selectFile(item: FileItem) { function selectFile(item: FileItem) {
@@ -764,13 +773,16 @@ async function uploadDroppedFiles(paths: string[]) {
speed: "-", speed: "-",
progress: 100, progress: 100,
status: "done", status: "done",
note: "上传成功",
}); });
} else { } else {
dropState.failed += 1; dropState.failed += 1;
const message = String(response.data?.message || "上传失败");
updateTransferTask(taskId, { updateTransferTask(taskId, {
speed: "-", speed: "-",
progress: 0, progress: 0,
status: "failed", status: "failed",
note: message,
}); });
} }
} }
@@ -802,7 +814,12 @@ async function registerDragDropListener() {
if (payload.type === "drop") { if (payload.type === "drop") {
dropState.active = false; dropState.active = false;
if (!canUseDragUpload()) return; if (!canUseDragUpload()) {
if (authenticated.value) {
showToast("请先切换到“全部文件”页面再上传", "info");
}
return;
}
void uploadDroppedFiles(payload.paths || []); void uploadDroppedFiles(payload.paths || []);
} }
}); });
@@ -995,7 +1012,8 @@ onBeforeUnmount(() => {
<div v-for="task in transferTasks" :key="task.id" class="task-row"> <div v-for="task in transferTasks" :key="task.id" class="task-row">
<div> <div>
<strong>{{ task.name }}</strong> <strong>{{ task.name }}</strong>
<small>{{ task.id }} · {{ task.status }}</small> <small>{{ task.id }} · {{ getTaskStatusLabel(task.status) }}</small>
<small v-if="task.note" class="task-note" :class="{ error: task.status === 'failed' }">{{ task.note }}</small>
</div> </div>
<div class="task-right"> <div class="task-right">
<span>{{ task.speed }}</span> <span>{{ task.speed }}</span>
@@ -1639,9 +1657,19 @@ select:focus {
} }
.task-row small { .task-row small {
display: block;
color: #647b94; color: #647b94;
} }
.task-note {
margin-top: 2px;
color: #5f7895;
}
.task-note.error {
color: #c24747;
}
.task-right { .task-right {
display: grid; display: grid;
gap: 6px; gap: 6px;