> 技术文档 > Softhub软件下载站实战开发(十三):软件管理前端分片上传实现

Softhub软件下载站实战开发(十三):软件管理前端分片上传实现


文章目录

  • Softhub软件下载站实战开发(十三):软件管理前端分片上传实现 🚀
    • 前言
    • 前端分片上传架构设计 🏗️
    • 核心组件实现 💻
      • 1. 上传资源组件 (`uploadResource.vue`)
      • 2. 分片上传逻辑实现
      • 3. 资源管理组件 (`editResource.vue`)
    • 文件大小格式化组件 📏
    • 大整数处理方案 🔢
    • 总结 🎯

Softhub软件下载站实战开发(十三):软件管理前端分片上传实现 🚀

前言

在之前文章中,我们实现了软件分片上传的后端接口。本文将聚焦于前端分片上传的实现,详细介绍如何使用Vue3和Element Plus构建一个高效、稳定的文件上传组件,并与后端分片上传接口进行对接。

前端分片上传架构设计 🏗️

前端分片上传主要分为三个步骤:

  1. 文件选择与分片准备
  2. 分片上传过程
  3. 上传完成与合并

#mermaid-svg-iYh43qT0gL5NNWWF {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-iYh43qT0gL5NNWWF .error-icon{fill:#552222;}#mermaid-svg-iYh43qT0gL5NNWWF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iYh43qT0gL5NNWWF .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-iYh43qT0gL5NNWWF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iYh43qT0gL5NNWWF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iYh43qT0gL5NNWWF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iYh43qT0gL5NNWWF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iYh43qT0gL5NNWWF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iYh43qT0gL5NNWWF .marker.cross{stroke:#333333;}#mermaid-svg-iYh43qT0gL5NNWWF svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iYh43qT0gL5NNWWF .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iYh43qT0gL5NNWWF .cluster-label text{fill:#333;}#mermaid-svg-iYh43qT0gL5NNWWF .cluster-label span{color:#333;}#mermaid-svg-iYh43qT0gL5NNWWF .label text,#mermaid-svg-iYh43qT0gL5NNWWF span{fill:#333;color:#333;}#mermaid-svg-iYh43qT0gL5NNWWF .node rect,#mermaid-svg-iYh43qT0gL5NNWWF .node circle,#mermaid-svg-iYh43qT0gL5NNWWF .node ellipse,#mermaid-svg-iYh43qT0gL5NNWWF .node polygon,#mermaid-svg-iYh43qT0gL5NNWWF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iYh43qT0gL5NNWWF .node .label{text-align:center;}#mermaid-svg-iYh43qT0gL5NNWWF .node.clickable{cursor:pointer;}#mermaid-svg-iYh43qT0gL5NNWWF .arrowheadPath{fill:#333333;}#mermaid-svg-iYh43qT0gL5NNWWF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iYh43qT0gL5NNWWF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iYh43qT0gL5NNWWF .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-iYh43qT0gL5NNWWF .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-iYh43qT0gL5NNWWF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iYh43qT0gL5NNWWF .cluster text{fill:#333;}#mermaid-svg-iYh43qT0gL5NNWWF .cluster span{color:#333;}#mermaid-svg-iYh43qT0gL5NNWWF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iYh43qT0gL5NNWWF :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 用户选择文件 初始化上传 分片文件 上传分片 所有分片上传完成? 合并分片 上传完成

核心组件实现 💻

1. 上传资源组件 (uploadResource.vue)

这是用户上传文件的入口组件,主要功能是文件选择和初步信息收集。

   
将文件拖到此处,或点击上传
const handleFileChange = (file) => { // 从文件名中自动提取版本号 const versionMatch = file.name.match(/[vV]?(\\d+\\.\\d+\\.\\d+)/); formData.version = versionMatch ? versionMatch[1] : \'\'; // 设置默认资源名称 formData.resourceName = file.name; formData.file = file.raw;}

2. 分片上传逻辑实现

在确认上传后,组件会执行以下分片上传流程:

#mermaid-svg-0rKoZO4W5SbnNSdL {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-0rKoZO4W5SbnNSdL .error-icon{fill:#552222;}#mermaid-svg-0rKoZO4W5SbnNSdL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0rKoZO4W5SbnNSdL .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-0rKoZO4W5SbnNSdL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0rKoZO4W5SbnNSdL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0rKoZO4W5SbnNSdL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0rKoZO4W5SbnNSdL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0rKoZO4W5SbnNSdL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0rKoZO4W5SbnNSdL .marker.cross{stroke:#333333;}#mermaid-svg-0rKoZO4W5SbnNSdL svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0rKoZO4W5SbnNSdL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0rKoZO4W5SbnNSdL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0rKoZO4W5SbnNSdL .actor-line{stroke:grey;}#mermaid-svg-0rKoZO4W5SbnNSdL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0rKoZO4W5SbnNSdL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0rKoZO4W5SbnNSdL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0rKoZO4W5SbnNSdL .sequenceNumber{fill:white;}#mermaid-svg-0rKoZO4W5SbnNSdL #sequencenumber{fill:#333;}#mermaid-svg-0rKoZO4W5SbnNSdL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0rKoZO4W5SbnNSdL .messageText{fill:#333;stroke:#333;}#mermaid-svg-0rKoZO4W5SbnNSdL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0rKoZO4W5SbnNSdL .labelText,#mermaid-svg-0rKoZO4W5SbnNSdL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0rKoZO4W5SbnNSdL .loopText,#mermaid-svg-0rKoZO4W5SbnNSdL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0rKoZO4W5SbnNSdL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0rKoZO4W5SbnNSdL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0rKoZO4W5SbnNSdL .noteText,#mermaid-svg-0rKoZO4W5SbnNSdL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0rKoZO4W5SbnNSdL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0rKoZO4W5SbnNSdL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0rKoZO4W5SbnNSdL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0rKoZO4W5SbnNSdL .actorPopupMenu{position:absolute;}#mermaid-svg-0rKoZO4W5SbnNSdL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0rKoZO4W5SbnNSdL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0rKoZO4W5SbnNSdL .actor-man circle,#mermaid-svg-0rKoZO4W5SbnNSdL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0rKoZO4W5SbnNSdL :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 用户界面 前端代码 后端API 点击上传按钮 初始化上传(文件信息) 返回uploadId 分割文件为5MB分片 上传分片n 返回结果 更新进度条 loop [分片上传] 合并分片请求 返回最终结果 显示上传成功 用户界面 前端代码 后端API

关键代码实现:

// 创建文件分片const createFileChunks = (file: File) => { const chunks = []; let cur = 0; while (cur < file.size) { chunks.push({ index: chunks.length, file: file.slice(cur, cur + CHUNK_SIZE) }); cur += CHUNK_SIZE; } return chunks;};// 上传分片const uploadChunkFile = async (chunk, uploadId) => { const formDataObj = new FormData(); formDataObj.append(\'file\', chunk.file); formDataObj.append(\'uploadId\', uploadId); formDataObj.append(\'chunkIndex\', String(chunk.index)); formDataObj.append(\'fileName\', formData.file!.name); formDataObj.append(\'softwareId\', String(props.softwareId)); await uploadChunk(formDataObj);};// 完整上传流程const onConfirm = async () => { // 初始化上传 const initResponse = await initChunkUpload({ softwareId: props.softwareId, fileName: formData.file.name, fileSize: formData.file.size, chunkSize: CHUNK_SIZE }); const uploadId = initResponse.data.data.uploadId; // 创建分片 const chunks = createFileChunks(formData.file); // 显示进度条 const loadingInstance = ElLoading.service({ text: \'正在上传文件... 0%\' }); // 上传所有分片 for (let i = 0; i < chunks.length; i++) { await uploadChunkFile(chunks[i], uploadId); const progress = Math.round(((i + 1) / chunks.length) * 100); loadingInstance.setText(`正在上传文件... ${progress}%`); } // 合并分片 await mergeChunks({ uploadId, softwareId: props.softwareId, resourceName: formData.resourceName, version: formData.version, remark: formData.remark }); loadingInstance.close(); ElMessage.success(\'上传成功\');};

3. 资源管理组件 (editResource.vue)

这个组件提供了已上传资源的管理界面,包括:

  • 资源列表展示
  • 设置默认版本
  • 资源删除
  • 资源下载
               下载 修改 删除   

文件大小格式化组件 📏

为了方便显示文件大小,我们创建了一个专用的格式化组件:

 {{ formattedSize }}const formattedSize = computed(() => { const bytes = sizeInBytes.value; if (bytes === 0) return \'0 Bytes\'; const k = 1024; const sizes = [\'Bytes\', \'KB\', \'MB\', \'GB\', \'TB\']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \' \' + sizes[i];});

大整数处理方案 🔢

由于我们使用雪花算法生成ID,前端需要特殊处理大整数:

/** * 将大整数转换为字符串,避免精度丢失 * @param value 要转换的值 * @returns 字符串形式的大整数 */export function toBigIntString(value) { if (value === null || value === undefined) { return \'0\'; } // 如果已经是字符串,直接返回 if (typeof value === \'string\') { return value; } // 如果是数字,检查是否超过安全整数范围 if (typeof value === \'number\') { if (Number.isSafeInteger(value)) { return value.toString(); } else { // 超过安全整数范围,使用BigInt try { return BigInt(value).toString(); } catch (error) { console.error(\'BigInt转换失败:\', error); return value.toString(); } } } // 其他类型,尝试转换为字符串 try { return BigInt(value).toString(); } catch (error) { console.error(\'BigInt转换失败:\', error); return String(value); }}

总结 🎯

通过本文的实现,我们完成了Softhub软件下载站的前端分片上传功能。关键点包括:

  • 使用Element Plus构建友好的上传界面
  • 实现高效的分片上传逻辑
  • 处理大整数ID问题
  • 提供完善的进度反馈和错误处理

这套方案不仅适用于软件下载站,也可以应用于任何需要大文件上传的场景,如视频平台、云存储服务等。