From f40fd6003c4fac0b9390d7dec5b21128564db940 Mon Sep 17 00:00:00 2001 From: 237899745 <237899745@users.noreply.git.workyai.cn> Date: Sat, 4 Apr 2026 00:19:20 +0800 Subject: [PATCH] feat: optimize file icons, UI, share direct links, and upload performance - Replace CSS-only file icons with SVG icons for each file type (folder, image, video, audio, archive, document, app) - Add navigation icons to left sidebar (Baidu Netdisk style) - Enlarge file cards (108px -> 120px) with smoother transitions - Add direct links section to share page with copy/delete actions - Add Rust bridge commands for fetching and deleting direct links - Optimize local storage put() to use rename-first strategy instead of copyFileSync for instant large file completion - Show "server processing" status during upload finalization instead of appearing stuck Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/storage.js | 21 ++- desktop-client/src-tauri/src/lib.rs | 35 ++++ desktop-client/src/App.vue | 256 +++++++++++++++++++++------- 3 files changed, 247 insertions(+), 65 deletions(-) diff --git a/backend/storage.js b/backend/storage.js index 67de8b2..b19ae32 100644 --- a/backend/storage.js +++ b/backend/storage.js @@ -244,16 +244,25 @@ class LocalStorageClient { const tempPath = `${destPath}.uploading_${Date.now()}`; try { - // 复制到临时文件 - fs.copyFileSync(localPath, tempPath); - // 如果目标文件存在,先删除 if (fs.existsSync(destPath)) { fs.unlinkSync(destPath); } - // 重命名临时文件为目标文件 - fs.renameSync(tempPath, destPath); + // 优先尝试 rename(同文件系统下瞬时完成,大文件不再需要逐字节复制) + let movedDirectly = false; + try { + fs.renameSync(localPath, destPath); + movedDirectly = true; + } catch (renameErr) { + if (renameErr.code === 'EXDEV') { + // 跨文件系统,回退到 copy + rename + fs.copyFileSync(localPath, tempPath); + fs.renameSync(tempPath, destPath); + } else { + throw renameErr; + } + } // 更新已使用空间(使用净增量) if (netIncrease !== 0) { @@ -262,7 +271,7 @@ class LocalStorageClient { } catch (error) { // 清理临时文件 if (fs.existsSync(tempPath)) { - fs.unlinkSync(tempPath); + try { fs.unlinkSync(tempPath); } catch (_) {} } throw error; } diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index f2626b8..7933ad2 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -1032,6 +1032,39 @@ async fn api_get_my_shares( .await } +#[tauri::command] +async fn api_get_my_direct_links( + state: tauri::State<'_, ApiState>, + base_url: String, +) -> Result { + request_with_optional_csrf( + &state.client, + Method::GET, + &base_url, + "/api/direct-link/my", + None, + false, + ) + .await +} + +#[tauri::command] +async fn api_delete_direct_link( + state: tauri::State<'_, ApiState>, + base_url: String, + link_id: i64, +) -> Result { + request_with_optional_csrf( + &state.client, + Method::DELETE, + &base_url, + &format!("/api/direct-link/{}", link_id), + None, + false, + ) + .await +} + #[tauri::command] async fn api_create_share( state: tauri::State<'_, ApiState>, @@ -2074,6 +2107,8 @@ pub fn run() { api_delete_file, api_get_download_url, api_get_my_shares, + api_get_my_direct_links, + api_delete_direct_link, api_create_share, api_delete_share, api_create_direct_link, diff --git a/desktop-client/src/App.vue b/desktop-client/src/App.vue index c001171..20e0d2f 100644 --- a/desktop-client/src/App.vue +++ b/desktop-client/src/App.vue @@ -35,6 +35,17 @@ type ShareItem = { storage_type?: string; }; +type DirectLinkItem = { + id: number; + link_code: string; + direct_url: string; + file_path: string; + file_name?: string; + storage_type?: string; + created_at?: string; + expires_at?: string | null; +}; + type OnlineDeviceItem = { session_id: string; client_type?: string; @@ -125,6 +136,8 @@ const files = ref([]); const selectedFileName = ref(""); const searchKeyword = ref(""); const shares = ref([]); +const directLinks = ref([]); +const directLinksLoading = ref(false); const batchMode = ref(false); const batchSelectedNames = ref([]); @@ -253,11 +266,11 @@ const toast = reactive({ let toastTimer: ReturnType | null = null; const navItems = computed(() => [ - { key: "files" as const, label: "全部文件", hint: `${files.value.length} 项` }, - { key: "transfers" as const, label: "传输列表", hint: `${transferTasks.value.length} 个任务` }, - { key: "shares" as const, label: "我的分享", hint: `${shares.value.length} 条` }, - { key: "sync" as const, label: "同步盘", hint: syncState.localDir ? "已配置" : "未配置" }, - { key: "settings" as const, label: "设置", hint: updateState.available ? "发现新版本" : "系统与更新" }, + { key: "files" as const, label: "全部文件", hint: `${files.value.length} 项`, icon: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" }, + { key: "transfers" as const, label: "传输列表", hint: `${transferTasks.value.length} 个任务`, icon: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" }, + { key: "shares" as const, label: "我的分享", hint: `${shares.value.length + directLinks.value.length} 条`, icon: "M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" }, + { key: "sync" as const, label: "同步盘", hint: syncState.localDir ? "已配置" : "未配置", icon: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }, + { key: "settings" as const, label: "设置", hint: updateState.available ? "发现新版本" : "系统与更新", icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z" }, ]); const sortedShares = computed(() => { @@ -546,10 +559,10 @@ function applyNativeUploadProgress(payload: NativeUploadProgressEvent) { } } updateTransferTask(taskId, { - status: "uploading", - speed: transferSpeed, + status: boundedProgress >= 99.5 && !payload?.done ? "processing" : "uploading", + speed: boundedProgress >= 99.5 && !payload?.done ? "服务器处理中" : transferSpeed, progress: Number(boundedProgress.toFixed(1)), - note: `${formatBytes(uploadedBytes)} / ${formatBytes(totalBytes)}`, + note: boundedProgress >= 99.5 && !payload?.done ? "分片已上传完成,等待服务器处理..." : `${formatBytes(uploadedBytes)} / ${formatBytes(totalBytes)}`, }); if (taskId === uploadRuntime.taskId) { @@ -593,6 +606,20 @@ function fileExtLabel(item: FileItem) { return ext.slice(0, 4); } +function fileIconSvg(kind: string): string { + const icons: Record = { + folder: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z", + image: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z", + video: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", + audio: "M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z", + archive: "M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4", + document: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z M9 13h6 M9 17h4", + app: "M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5", + file: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z", + }; + return icons[kind] || icons.file; +} + function matchFileTypeFilter(item: FileItem, type: string) { if (item.isDirectory || item.type === "directory") return false; const name = String(item.name || "").toLowerCase(); @@ -670,13 +697,14 @@ function getTaskStatusLabel(status: string) { if (status === "queued") return "排队中"; if (status === "uploading") return "上传中"; if (status === "downloading") return "下载中"; + if (status === "processing") return "服务器处理中"; if (status === "done") return "已完成"; if (status === "failed") return "失败"; return status; } function isTaskRunning(status: string) { - return status === "uploading" || status === "downloading"; + return status === "uploading" || status === "downloading" || status === "processing"; } function removeTransferTask(taskId: string) { @@ -1565,6 +1593,39 @@ async function loadShares(silent = false) { if (!silent) sharesLoading.value = false; } +async function loadDirectLinks(silent = false) { + if (!silent) directLinksLoading.value = true; + const response = await invokeBridge("api_get_my_direct_links", { + baseUrl: appConfig.baseUrl, + }); + if (response.ok && response.data?.success) { + directLinks.value = Array.isArray(response.data.links) ? response.data.links : []; + } else if (!silent) { + showToast(response.data?.message || "获取直链列表失败", "error"); + } + if (!silent) directLinksLoading.value = false; +} + +async function copyDirectLink(link: DirectLinkItem) { + const url = link.direct_url || ""; + if (url) { + await copyText(url, "直链已复制到剪贴板"); + } +} + +async function deleteDirectLink(link: DirectLinkItem) { + const response = await invokeBridge("api_delete_direct_link", { + baseUrl: appConfig.baseUrl, + linkId: link.id, + }); + if (response.ok && response.data?.success) { + showToast("直链已删除", "success"); + await loadDirectLinks(true); + } else { + showToast(response.data?.message || "删除直链失败", "error"); + } +} + async function getSignedUrlForItem(item: FileItem, mode: "download" | "preview") { const targetPath = buildItemPath(item); const response = await invokeBridge("api_get_download_url", { @@ -2524,6 +2585,7 @@ watch(nav, async (next) => { } if (next === "shares" && authenticated.value) { await loadShares(); + await loadDirectLinks(); return; } if (next === "settings" && authenticated.value) { @@ -2645,7 +2707,10 @@ onBeforeUnmount(() => { :class="{ active: nav === item.key }" @click="nav = item.key" > - {{ item.label }} + {{ item.hint }} @@ -2705,7 +2770,7 @@ onBeforeUnmount(() => {