前端逻辑更新
This commit is contained in:
		
							parent
							
								
									a1e5554823
								
							
						
					
					
						commit
						9b1a2f51e8
					
				
							
								
								
									
										41
									
								
								minio-admin/src/main/java/com/mmg/types/JsonResponse.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								minio-admin/src/main/java/com/mmg/types/JsonResponse.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | package com.mmg.types; | ||||||
|  | 
 | ||||||
|  | import lombok.Data; | ||||||
|  | 
 | ||||||
|  | @Data | ||||||
|  | public class JsonResponse { | ||||||
|  | 
 | ||||||
|  |     private Integer code; | ||||||
|  |     private String msg; | ||||||
|  |     private Object data; | ||||||
|  | 
 | ||||||
|  |     public JsonResponse(Integer code, String msg, Object data) { | ||||||
|  |         this.code = code; | ||||||
|  |         this.msg = msg; | ||||||
|  |         this.data = data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static JsonResponse success(String msg) { | ||||||
|  |         return new JsonResponse(0, msg, null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static JsonResponse success() { | ||||||
|  |         return new JsonResponse(0, "", null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static JsonResponse data(Object data) { | ||||||
|  |         return new JsonResponse(0, "", data); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static JsonResponse error(String msg, Integer code) { | ||||||
|  |         return new JsonResponse(code, msg, null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static JsonResponse error(String msg) { | ||||||
|  |         return new JsonResponse(-1, msg, null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static JsonResponse error(String msg, Object data) { | ||||||
|  |         return new JsonResponse(-1, msg, data); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -7,4 +7,10 @@ import {RouterLink, RouterView} from 'vue-router' | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  | body { | ||||||
|  |   font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; | ||||||
|  |   background-color: #f5f5f5; | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								minio-fornt/src/api/resource.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								minio-fornt/src/api/resource.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | // src/api/resource.js
 | ||||||
|  | import axios from '../utils/request'; | ||||||
|  | 
 | ||||||
|  | export const resource = { | ||||||
|  |     resourceList: (page, size, title, type, categoryIds) => | ||||||
|  |         axios.get('/resource/list', { | ||||||
|  |             params: { page, size, title, type, categoryIds }, | ||||||
|  |         }), | ||||||
|  | 
 | ||||||
|  |     destroyResource: (id) => axios.delete(`/resource/${id}`), | ||||||
|  | 
 | ||||||
|  |     destroyResourceMulti: (ids) => axios.post('/resource/batch-delete', { ids }), | ||||||
|  | 
 | ||||||
|  |     videoDetail: (id) => axios.get(`/resource/${id}`), | ||||||
|  | 
 | ||||||
|  |     videoUpdate: (id, data) => axios.put(`/resource/${id}`, data), | ||||||
|  | }; | ||||||
							
								
								
									
										6
									
								
								minio-fornt/src/api/resourceCategory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								minio-fornt/src/api/resourceCategory.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | // src/api/resourceCategory.js
 | ||||||
|  | import axios from '../utils/request'; | ||||||
|  | 
 | ||||||
|  | export const resourceCategory = { | ||||||
|  |     resourceCategoryList: () => axios.get('/resource-category/list'), | ||||||
|  | }; | ||||||
| @ -1,49 +1,66 @@ | |||||||
| import request from '@/utils/request' | // src/api/upload.js
 | ||||||
|  | import axios from '../utils/request'; | ||||||
| 
 | 
 | ||||||
| //上传信息
 | /** | ||||||
| export function uploadScreenshot(data){ |  * 获取上传中的文件信息 | ||||||
|     return request({ |  * @param {string} fileMD5 - 文件的 MD5 值 | ||||||
|         url:'upload/multipart/uploadScreenshot', |  * @returns {Promise} | ||||||
|         method:'post', |  */ | ||||||
|         data | export const getUploadingFile = (fileMD5) => { | ||||||
|     }) |     return axios.get(`/upload/getUploadingFile/${fileMD5}`); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| //上传信息
 |  | ||||||
| export function uploadFileInfo(data){ |  | ||||||
|     return request({ |  | ||||||
|         url:'upload/multipart/uploadFileInfo', |  | ||||||
|         method:'post', |  | ||||||
|         data |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 上传校验
 |  | ||||||
| export function checkUpload(MD5) { |  | ||||||
|     return request({ |  | ||||||
|         url: `upload/multipart/check?md5=${MD5}`, |  | ||||||
|         method: 'get', |  | ||||||
|     }) |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 
 | /** | ||||||
| // 初始化上传
 |  * 校验文件是否已经上传 | ||||||
| export function initUpload(data) { |  * @param {string} md5 - 文件的 MD5 值 | ||||||
|     return request({ |  * @returns {Promise} | ||||||
|         url: `upload/multipart/init`, |  */ | ||||||
|         method: 'post', | export const checkFileUploadedByMd5 = (md5) => { | ||||||
|         data |     return axios.get('/upload/multipart/check', { | ||||||
|     }) |         params: { md5 }, | ||||||
|  |     }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 
 | /** | ||||||
| // 初始化上传
 |  * 初始化分片上传 | ||||||
| export function mergeUpload(data) { |  * @param {Object} fileUploadInfo - 文件上传信息 | ||||||
|     return request({ |  * @returns {Promise} | ||||||
|         url: `upload/multipart/merge`, |  */ | ||||||
|         method: 'post', | export const initMultiPartUpload = (fileUploadInfo) => { | ||||||
|         data |     return axios.post('/upload/multipart/init', fileUploadInfo); | ||||||
|     }) |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * 完成分片上传 | ||||||
|  |  * @param {Object} fileUploadInfo - 文件上传信息 | ||||||
|  |  * @returns {Promise} | ||||||
|  |  */ | ||||||
|  | export const completeMultiPartUpload = (fileUploadInfo) => { | ||||||
|  |     return axios.post('/upload/multipart/merge', fileUploadInfo); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * 上传截图 | ||||||
|  |  * @param {FormData} formData - 包含截图文件的 FormData | ||||||
|  |  * @param {string} bucketName - Bucket 名称 | ||||||
|  |  * @returns {Promise} | ||||||
|  |  */ | ||||||
|  | export const uploadScreenshot = (formData, bucketName) => { | ||||||
|  |     return axios.post('/upload/multipart/uploadScreenshot', formData, { | ||||||
|  |         params: { bucketName }, | ||||||
|  |         headers: { | ||||||
|  |             'Content-Type': 'multipart/form-data', | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 创建 Bucket | ||||||
|  |  * @param {string} bucketName - Bucket 名称 | ||||||
|  |  * @returns {Promise} | ||||||
|  |  */ | ||||||
|  | export const createBucket = (bucketName) => { | ||||||
|  |     return axios.post('/upload/createBucket', null, { | ||||||
|  |         params: { bucketName }, | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | |||||||
							
								
								
									
										260
									
								
								minio-fornt/src/components/Resource/FileUploader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								minio-fornt/src/components/Resource/FileUploader.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,260 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="file-uploader"> | ||||||
|  |     <el-upload | ||||||
|  |         ref="upload" | ||||||
|  |         :file-list="fileList" | ||||||
|  |         :before-upload="beforeUpload" | ||||||
|  |         :auto-upload="false" | ||||||
|  |         multiple | ||||||
|  |         drag | ||||||
|  |         accept=".doc,.docx,.ppt,.pptx,.pdf,.txt,.rar,.zip,.jpg,.jpeg,.png" | ||||||
|  |         @change="handleChange" | ||||||
|  |     > | ||||||
|  |       <i class="el-icon-upload"></i> | ||||||
|  |       <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> | ||||||
|  |       <div slot="tip" class="el-upload__tip">支持上传的格式: DOC, DOCX, PPT, PPTX, PDF, TXT, RAR, ZIP</div> | ||||||
|  |     </el-upload> | ||||||
|  |     <el-button | ||||||
|  |         type="primary" | ||||||
|  |         :disabled="!fileList.length" | ||||||
|  |         @click="uploadFiles" | ||||||
|  |         class="upload-button" | ||||||
|  |     > | ||||||
|  |       上传 | ||||||
|  |     </el-button> | ||||||
|  | 
 | ||||||
|  |     <el-progress v-if="uploadProgress > 0" :percentage="uploadProgress" class="upload-progress" /> | ||||||
|  | 
 | ||||||
|  |     <div v-if="uploadResult" class="upload-result"> | ||||||
|  |       <h3>上传结果</h3> | ||||||
|  |       <p> | ||||||
|  |         文件 URL: | ||||||
|  |         <a :href="uploadResult.url" target="_blank">{{ uploadResult.url }}</a> | ||||||
|  |       </p> | ||||||
|  |       <p>文件名: {{ uploadResult.resource.originalName }}</p> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div v-if="errorMessage" class="error-message"> | ||||||
|  |       <el-alert :title="errorMessage" type="error" show-icon /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, ref } from 'vue'; | ||||||
|  | import axios, { AxiosRequestConfig } from 'axios'; | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
|  | import { | ||||||
|  |   checkFileUploadedByMd5, | ||||||
|  |   getUploadingFile, | ||||||
|  |   initMultiPartUpload, | ||||||
|  |   completeMultiPartUpload, | ||||||
|  | } from '../../api/upload.js'; | ||||||
|  | import SparkMD5 from 'spark-md5'; | ||||||
|  | 
 | ||||||
|  | interface FileUploadInfo { | ||||||
|  |   adminId?: string; | ||||||
|  |   categoryIds?: string[]; | ||||||
|  |   resourceType: string; | ||||||
|  |   originalName: string; | ||||||
|  |   extension: string; | ||||||
|  |   size: number; | ||||||
|  |   savePath: string; | ||||||
|  |   uploadId?: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Resource { | ||||||
|  |   id: string; | ||||||
|  |   originalName: string; | ||||||
|  |   extension: string; | ||||||
|  |   size: number; | ||||||
|  |   md5: string; | ||||||
|  |   url: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface UploadResult { | ||||||
|  |   url: string; | ||||||
|  |   resource: Resource; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  |   name: 'FileUploader', | ||||||
|  |   emits: ['upload-success'], | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const fileList = ref<any[]>([]); | ||||||
|  |     const uploadProgress = ref<number>(0); | ||||||
|  |     const uploadResult = ref<UploadResult | null>(null); | ||||||
|  |     const errorMessage = ref<string>(''); | ||||||
|  | 
 | ||||||
|  |     const beforeUpload = (file: File) => { | ||||||
|  |       return true; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleChange = (info: any) => { | ||||||
|  |       fileList.value = info.fileList; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const calculateMD5 = (file: File): Promise<string> => { | ||||||
|  |       return new Promise((resolve, reject) => { | ||||||
|  |         const chunkSize = 2097152; // 2MB | ||||||
|  |         const chunks = Math.ceil(file.size / chunkSize); | ||||||
|  |         let currentChunk = 0; | ||||||
|  |         const spark = new SparkMD5.ArrayBuffer(); | ||||||
|  |         const fileReader = new FileReader(); | ||||||
|  | 
 | ||||||
|  |         fileReader.onload = (e) => { | ||||||
|  |           if (e.target && e.target.result) { | ||||||
|  |             spark.append(e.target.result as ArrayBuffer); | ||||||
|  |             currentChunk++; | ||||||
|  |             if (currentChunk < chunks) { | ||||||
|  |               loadNext(); | ||||||
|  |             } else { | ||||||
|  |               const md5 = spark.end(); | ||||||
|  |               resolve(md5); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         fileReader.onerror = () => { | ||||||
|  |           reject('文件读取错误'); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const loadNext = () => { | ||||||
|  |           const start = currentChunk * chunkSize; | ||||||
|  |           const end = Math.min(start + chunkSize, file.size); | ||||||
|  |           fileReader.readAsArrayBuffer(file.slice(start, end)); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         loadNext(); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const uploadFiles = async () => { | ||||||
|  |       if (fileList.value.length === 0) return; | ||||||
|  | 
 | ||||||
|  |       uploadProgress.value = 0; | ||||||
|  |       uploadResult.value = null; | ||||||
|  |       errorMessage.value = ''; | ||||||
|  | 
 | ||||||
|  |       try { | ||||||
|  |         const file = fileList.value[0].raw as File; | ||||||
|  |         const fileMD5 = await calculateMD5(file); | ||||||
|  |         console.log('文件 MD5:', fileMD5); | ||||||
|  | 
 | ||||||
|  |         // 检查文件是否已上传 | ||||||
|  |         const checkResponse = await checkFileUploadedByMd5(fileMD5); | ||||||
|  |         if (checkResponse.data.exists) { | ||||||
|  |           // 文件已存在,获取文件信息 | ||||||
|  |           const getFileResponse = await getUploadingFile(fileMD5); | ||||||
|  |           uploadResult.value = getFileResponse.data.file; | ||||||
|  |           ElMessage.success('文件已存在,已获取文件信息'); | ||||||
|  |           emit('upload-success'); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 初始化分片上传 | ||||||
|  |         const initInfo: FileUploadInfo = { | ||||||
|  |           resourceType: 'file', | ||||||
|  |           originalName: file.name, | ||||||
|  |           extension: getFileExtension(file.name), | ||||||
|  |           size: file.size, | ||||||
|  |           savePath: `uploads/${file.name}`, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const initResponse = await initMultiPartUpload(initInfo); | ||||||
|  |         const { uploadId, preSignedUrls, partCount } = initResponse.data.data; | ||||||
|  | 
 | ||||||
|  |         // 分片上传 | ||||||
|  |         const partSize = 5 * 1024 * 1024; // 5MB | ||||||
|  |         let uploadedBytes = 0; | ||||||
|  | 
 | ||||||
|  |         for (let i = 0; i < partCount; i++) { | ||||||
|  |           const start = i * partSize; | ||||||
|  |           const end = Math.min(start + partSize, file.size); | ||||||
|  |           const blob = file.slice(start, end); | ||||||
|  |           const preSignedUrl = preSignedUrls[i]; | ||||||
|  | 
 | ||||||
|  |           // 正确声明配置类型为 AxiosRequestConfig<Blob> | ||||||
|  |           const config: AxiosRequestConfig = { | ||||||
|  |             headers: { | ||||||
|  |               'Content-Type': file.type, // 设置文件类型 | ||||||
|  |             }, | ||||||
|  |             onUploadProgress: (progressEvent: ProgressEvent) => { | ||||||
|  |               if (progressEvent.total) { | ||||||
|  |                 const percentCompleted = Math.round( | ||||||
|  |                     (progressEvent.loaded * 100) / progressEvent.total | ||||||
|  |                 ); | ||||||
|  |                 uploadProgress.value = Math.min( | ||||||
|  |                     percentCompleted + (i * 100) / partCount, | ||||||
|  |                     100 | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |           }; | ||||||
|  | 
 | ||||||
|  |           // 调用 PUT 请求,上传文件 | ||||||
|  |           await axios.put(preSignedUrl, blob, config); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 完成分片上传 | ||||||
|  |         const completeInfo: FileUploadInfo = { | ||||||
|  |           resourceType: 'file', | ||||||
|  |           originalName: file.name, | ||||||
|  |           extension: getFileExtension(file.name), | ||||||
|  |           size: file.size, | ||||||
|  |           savePath: `uploads/${file.name}`, | ||||||
|  |           uploadId: uploadId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const completeResponse = await completeMultiPartUpload(completeInfo); | ||||||
|  |         uploadResult.value = completeResponse.data.data; | ||||||
|  |         ElMessage.success('文件上传成功'); | ||||||
|  |         emit('upload-success'); | ||||||
|  |       } catch (error: any) { | ||||||
|  |         console.error(error); | ||||||
|  |         errorMessage.value = error.response?.data?.message || '上传失败'; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const getFileExtension = (filename: string): string => { | ||||||
|  |       const index = filename.lastIndexOf('.'); | ||||||
|  |       if (index === -1) return ''; | ||||||
|  |       return filename.substring(index + 1); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       fileList, | ||||||
|  |       uploadProgress, | ||||||
|  |       uploadResult, | ||||||
|  |       errorMessage, | ||||||
|  |       beforeUpload, | ||||||
|  |       handleChange, | ||||||
|  |       uploadFiles, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .file-uploader { | ||||||
|  |   max-width: 600px; | ||||||
|  |   margin: 0 auto; | ||||||
|  |   padding: 20px; | ||||||
|  |   border: 1px dashed #d9d9d9; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  | .upload-button { | ||||||
|  |   margin-top: 16px; | ||||||
|  | } | ||||||
|  | .upload-progress { | ||||||
|  |   margin-top: 16px; | ||||||
|  | } | ||||||
|  | .upload-result { | ||||||
|  |   margin-top: 16px; | ||||||
|  |   text-align: left; | ||||||
|  | } | ||||||
|  | .error-message { | ||||||
|  |   margin-top: 16px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										285
									
								
								minio-fornt/src/components/Resource/UploadCoursewareButton.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								minio-fornt/src/components/Resource/UploadCoursewareButton.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,285 @@ | |||||||
|  | <!-- src/components/UploadCoursewareButton.vue --> | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <el-button type="primary" @click="showModal = true"> | ||||||
|  |       上传课件 | ||||||
|  |     </el-button> | ||||||
|  | 
 | ||||||
|  |     <el-dialog | ||||||
|  |         title="上传课件" | ||||||
|  |         :visible.sync="showModal" | ||||||
|  |         width="50%" | ||||||
|  |         @close="handleCancel" | ||||||
|  |     > | ||||||
|  |       <el-upload | ||||||
|  |           class="upload-demo" | ||||||
|  |           drag | ||||||
|  |           action="" | ||||||
|  |       :before-upload="beforeUpload" | ||||||
|  |       :file-list="[]" | ||||||
|  |       multiple | ||||||
|  |       :auto-upload="false" | ||||||
|  |       :on-change="handleChange" | ||||||
|  |       > | ||||||
|  |       <i class="el-icon-upload"></i> | ||||||
|  |       <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> | ||||||
|  |       <div slot="tip" class="el-upload__tip">支持多文件上传,支持word、ppt、pdf、zip、rar、txt格式文件</div> | ||||||
|  |       </el-upload> | ||||||
|  | 
 | ||||||
|  |       <el-table | ||||||
|  |           :data="fileList" | ||||||
|  |           style="width: 100%; margin-top: 20px;" | ||||||
|  |           :show-header="false" | ||||||
|  |           row-key="id" | ||||||
|  |       > | ||||||
|  |         <el-table-column prop="name"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <span>{{ scope.row.file.name }}</span> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |         <el-table-column prop="size"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <span>{{ (scope.row.file.size / 1024 / 1024).toFixed(2) }} MB</span> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |         <el-table-column prop="progress"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <el-progress v-if="scope.row.upload.status === 'uploading'" :percentage="scope.row.upload.progress" /> | ||||||
|  |             <span v-else-if="scope.row.upload.status === 'waiting'">等待上传</span> | ||||||
|  |             <span v-else-if="scope.row.upload.status === 'success'" style="color: green;">上传成功</span> | ||||||
|  |             <span v-else-if="scope.row.upload.status === 'error'" style="color: red;">{{ scope.row.upload.remark }}</span> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |       </el-table> | ||||||
|  | 
 | ||||||
|  |       <span slot="footer" class="dialog-footer"> | ||||||
|  |         <el-button @click="handleCancel">取消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleUpload">上传</el-button> | ||||||
|  |       </span> | ||||||
|  |     </el-dialog> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  | import { ref } from 'vue'; | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: 'UploadCoursewareButton', | ||||||
|  |   props: { | ||||||
|  |     categoryIds: { | ||||||
|  |       type: Array, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   emits: ['update'], | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const showModal = ref(false); | ||||||
|  |     const fileList = ref([]); | ||||||
|  | 
 | ||||||
|  |     // 生成唯一ID | ||||||
|  |     const generateUUID = () => { | ||||||
|  |       return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { | ||||||
|  |         const r = (Math.random() * 16) | 0, | ||||||
|  |             v = c === 'x' ? r : (r & 0x3) | 0x8; | ||||||
|  |         return v.toString(16); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // 文件类型验证 | ||||||
|  |     const allowedTypes = ['doc', 'docx', 'ppt', 'pptx', 'pdf', 'zip', 'rar', 'txt']; | ||||||
|  | 
 | ||||||
|  |     const beforeUpload = (file) => { | ||||||
|  |       const extension = getFileExtension(file.name); | ||||||
|  |       if (!allowedTypes.includes(extension)) { | ||||||
|  |         ElMessage.error(`${file.name} 是不支持的文件类型`); | ||||||
|  |         return false; // 阻止上传 | ||||||
|  |       } | ||||||
|  |       // 添加到 fileList | ||||||
|  |       const newFileItem = { | ||||||
|  |         id: generateUUID(), | ||||||
|  |         file: file, | ||||||
|  |         upload: { | ||||||
|  |           progress: 0, | ||||||
|  |           status: 'waiting', // 'waiting', 'uploading', 'success', 'error' | ||||||
|  |           remark: '', | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |       fileList.value.push(newFileItem); | ||||||
|  |       console.log('文件添加到待上传列表:', newFileItem); | ||||||
|  |       return false; // 阻止自动上传 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleChange = (info) => { | ||||||
|  |       // 由于使用自定义上传,这里无需处理 | ||||||
|  |       console.log('文件变化:', info); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleUpload = async () => { | ||||||
|  |       console.log('开始上传文件列表:', fileList.value); | ||||||
|  |       for (const fileItem of fileList.value) { | ||||||
|  |         if (fileItem.upload.status === 'waiting') { | ||||||
|  |           await uploadFile(fileItem); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       emit('update'); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const uploadFile = async (fileItem) => { | ||||||
|  |       fileItem.upload.status = 'uploading'; | ||||||
|  |       console.log(`开始上传文件: ${fileItem.file.name}`); | ||||||
|  |       try { | ||||||
|  |         // 获取上传ID | ||||||
|  |         const uploadIdResponse = await minioUploadId(getFileExtension(fileItem.file.name)); | ||||||
|  |         const { resource_type, upload_id, filename } = uploadIdResponse.data; | ||||||
|  |         console.log('获取上传ID成功:', uploadIdResponse.data); | ||||||
|  | 
 | ||||||
|  |         const chunkSize = 5 * 1024 * 1024; // 5MB | ||||||
|  |         const totalChunks = Math.ceil(fileItem.file.size / chunkSize); | ||||||
|  |         let currentChunk = 0; | ||||||
|  | 
 | ||||||
|  |         while (currentChunk < totalChunks) { | ||||||
|  |           const start = currentChunk * chunkSize; | ||||||
|  |           const end = Math.min(start + chunkSize, fileItem.file.size); | ||||||
|  |           const blob = fileItem.file.slice(start, end); | ||||||
|  | 
 | ||||||
|  |           // 获取预签名URL | ||||||
|  |           const preSignUrlResponse = await minioPreSignUrl(upload_id, filename, currentChunk + 1); | ||||||
|  |           const { url } = preSignUrlResponse.data; | ||||||
|  |           console.log(`获取预签名URL成功 (分片 ${currentChunk + 1}):`, url); | ||||||
|  | 
 | ||||||
|  |           // 上传分片 | ||||||
|  |           const uploadResponse = await fetch(url, { | ||||||
|  |             method: 'PUT', | ||||||
|  |             headers: { | ||||||
|  |               'Content-Type': fileItem.file.type, | ||||||
|  |             }, | ||||||
|  |             body: blob, | ||||||
|  |           }); | ||||||
|  | 
 | ||||||
|  |           if (!uploadResponse.ok) { | ||||||
|  |             throw new Error(`分片 ${currentChunk + 1} 上传失败`); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           currentChunk++; | ||||||
|  |           fileItem.upload.progress = Math.floor((currentChunk / totalChunks) * 100); | ||||||
|  |           console.log(`分片 ${currentChunk} 上传成功,进度: ${fileItem.upload.progress}%`); | ||||||
|  |           // 触发响应式更新 | ||||||
|  |           fileList.value = [...fileList.value]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 合并分片 | ||||||
|  |         const mergeData = { | ||||||
|  |           extension: getFileExtension(fileItem.file.name), | ||||||
|  |           original_filename: fileItem.file.name, | ||||||
|  |           category_ids: props.categoryIds.join(','), | ||||||
|  |           size: fileItem.file.size, | ||||||
|  |           upload_id: upload_id, | ||||||
|  |           filename: filename, | ||||||
|  |           poster: '', | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const mergeResponse = await minioMergeFile(mergeData); | ||||||
|  |         const { url: mergedUrl } = mergeResponse.data; | ||||||
|  |         console.log('合并分片成功:', mergeResponse.data); | ||||||
|  | 
 | ||||||
|  |         fileItem.upload.progress = 100; | ||||||
|  |         fileItem.upload.status = 'success'; | ||||||
|  |         ElMessage.success(`${fileItem.file.name} 上传成功`); | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error(`上传文件失败 (${fileItem.file.name}):`, error); | ||||||
|  |         fileItem.upload.status = 'error'; | ||||||
|  |         fileItem.upload.remark = error.message || '上传失败'; | ||||||
|  |         ElMessage.error(`${fileItem.file.name} 上传失败: ${error.message}`); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleCancel = () => { | ||||||
|  |       // 取消上传中或等待上传的文件 | ||||||
|  |       fileList.value.forEach((item) => { | ||||||
|  |         if (item.upload.status !== 'success') { | ||||||
|  |           item.upload.status = 'error'; | ||||||
|  |           item.upload.remark = '上传被取消'; | ||||||
|  |           console.log(`上传被取消: ${item.file.name}`); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       fileList.value = []; | ||||||
|  |       showModal.value = false; | ||||||
|  |       emit('update'); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const getFileExtension = (filename) => { | ||||||
|  |       const index = filename.lastIndexOf('.'); | ||||||
|  |       if (index === -1) return ''; | ||||||
|  |       return filename.substring(index + 1).toLowerCase(); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // 模拟API调用 | ||||||
|  |     const minioUploadId = async (extension) => { | ||||||
|  |       // 替换为实际的API调用 | ||||||
|  |       console.log('请求获取上传ID'); | ||||||
|  |       return new Promise((resolve) => { | ||||||
|  |         setTimeout(() => { | ||||||
|  |           resolve({ | ||||||
|  |             data: { | ||||||
|  |               resource_type: 'courseware', | ||||||
|  |               upload_id: 'unique-upload-id', | ||||||
|  |               filename: 'uploaded-file-name.' + extension, | ||||||
|  |             }, | ||||||
|  |           }); | ||||||
|  |         }, 500); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const minioPreSignUrl = async (upload_id, filename, chunkNumber) => { | ||||||
|  |       // 替换为实际的API调用 | ||||||
|  |       console.log(`请求获取预签名URL (分片 ${chunkNumber})`); | ||||||
|  |       return new Promise((resolve) => { | ||||||
|  |         setTimeout(() => { | ||||||
|  |           resolve({ | ||||||
|  |             data: { | ||||||
|  |               url: `https://example.com/upload/${upload_id}/${filename}/chunk-${chunkNumber}`, | ||||||
|  |             }, | ||||||
|  |           }); | ||||||
|  |         }, 500); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const minioMergeFile = async (mergeData) => { | ||||||
|  |       // 替换为实际的API调用 | ||||||
|  |       console.log('请求合并分片'); | ||||||
|  |       return new Promise((resolve) => { | ||||||
|  |         setTimeout(() => { | ||||||
|  |           resolve({ | ||||||
|  |             data: { | ||||||
|  |               url: 'https://example.com/upload/complete', | ||||||
|  |             }, | ||||||
|  |           }); | ||||||
|  |         }, 500); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       showModal, | ||||||
|  |       fileList, | ||||||
|  |       beforeUpload, | ||||||
|  |       handleChange, | ||||||
|  |       handleUpload, | ||||||
|  |       handleCancel, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style scoped> | ||||||
|  | .upload-demo i { | ||||||
|  |   font-size: 28px; | ||||||
|  |   color: #409EFF; | ||||||
|  | } | ||||||
|  | .upload-demo .el-upload__text { | ||||||
|  |   margin-top: 10px; | ||||||
|  |   font-size: 16px; | ||||||
|  | } | ||||||
|  | .upload-demo .el-upload__tip { | ||||||
|  |   font-size: 12px; | ||||||
|  |   color: #c0c4cc; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @ -1,13 +1,19 @@ | |||||||
| import {createApp} from 'vue' | // src/main.js
 | ||||||
| import App from './App.vue' | import { createApp } from 'vue'; | ||||||
| import router from './router' | import App from './App.vue'; | ||||||
| import ElementPlus from 'element-plus' | import router from './router'; | ||||||
| import 'element-plus/dist/index.css' |  | ||||||
| 
 | 
 | ||||||
| const app = createApp(App) | // 引入 Element Plus 及其样式
 | ||||||
|  | import ElementPlus from 'element-plus'; | ||||||
|  | import 'element-plus/dist/index.css'; | ||||||
| 
 | 
 | ||||||
|  | const app = createApp(App); | ||||||
| 
 | 
 | ||||||
| app.use(router) | // 使用 Element Plus
 | ||||||
| app.use(ElementPlus) | app.use(ElementPlus); | ||||||
| 
 | 
 | ||||||
| app.mount('#app') | // 使用路由
 | ||||||
|  | app.use(router); | ||||||
|  | 
 | ||||||
|  | // 挂载应用
 | ||||||
|  | app.mount('#app'); | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								minio-fornt/src/utils/dateFormat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								minio-fornt/src/utils/dateFormat.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | // src/utils/dateFormat.js
 | ||||||
|  | export const dateFormat = (dateStr) => { | ||||||
|  |     const date = new Date(dateStr); | ||||||
|  |     const year = date.getFullYear(); | ||||||
|  |     const month = (`0${date.getMonth() + 1}`).slice(-2); | ||||||
|  |     const day = (`0${date.getDate()}`).slice(-2); | ||||||
|  |     const hours = (`0${date.getHours()}`).slice(-2); | ||||||
|  |     const minutes = (`0${date.getMinutes()}`).slice(-2); | ||||||
|  |     const seconds = (`0${date.getSeconds()}`).slice(-2); | ||||||
|  |     return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; | ||||||
|  | }; | ||||||
| @ -1,42 +1,34 @@ | |||||||
| import axios from 'axios' | // src/utils/request.js
 | ||||||
|  | import axios from 'axios'; | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
| 
 | 
 | ||||||
| const request = axios.create({ | // 创建 axios 实例
 | ||||||
|     baseURL: `http://localhost:9090`, | const instance = axios.create({ | ||||||
|     timeout: 30000 |     baseURL: 'http://localhost:9090', // 替换为您的后端地址
 | ||||||
| }) |     timeout: 10000, | ||||||
| 
 |  | ||||||
| // request 拦截器
 |  | ||||||
| // 可以自请求发送前对请求做一些处理
 |  | ||||||
| // 比如统一加token,对请求参数统一加密
 |  | ||||||
| request.interceptors.request.use(config => { |  | ||||||
|     config.headers['Content-Type'] = 'application/json;charset=utf-8'; |  | ||||||
|     return config |  | ||||||
| }, error => { |  | ||||||
|     return Promise.reject(error) |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| // response 拦截器
 | // 请求拦截器
 | ||||||
| // 可以在接口响应后统一处理结果
 | instance.interceptors.request.use( | ||||||
| request.interceptors.response.use( |     (config) => { | ||||||
|     response => { |         // 可以在这里添加请求头,例如 token
 | ||||||
|         let res = response.data; |         // config.headers['Authorization'] = 'Bearer token';
 | ||||||
|         // 如果是返回的文件
 |         return config; | ||||||
|         if (response.headers === 'blob') { |  | ||||||
|             return res |  | ||||||
|         } |  | ||||||
|         // 兼容服务端返回的字符串数据
 |  | ||||||
|         if (typeof res === 'string') { |  | ||||||
|             res = res ? JSON.parse(res) : res |  | ||||||
|             console.log(res) |  | ||||||
|         } |  | ||||||
|         return res; |  | ||||||
|     }, |     }, | ||||||
|     error => { |     (error) => { | ||||||
|         console.log('err' + error) // for debug
 |         return Promise.reject(error); | ||||||
|         return Promise.reject(error) |  | ||||||
|     } |     } | ||||||
| ) | ); | ||||||
| 
 | 
 | ||||||
|  | // 响应拦截器
 | ||||||
|  | instance.interceptors.response.use( | ||||||
|  |     (response) => { | ||||||
|  |         return response; | ||||||
|  |     }, | ||||||
|  |     (error) => { | ||||||
|  |         ElMessage.error(error.response?.data?.message || '请求失败'); | ||||||
|  |         return Promise.reject(error); | ||||||
|  |     } | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| export default request | export default instance; | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -1,338 +1,59 @@ | |||||||
|  | <!-- src/views/FileView.vue --> | ||||||
| <template> | <template> | ||||||
|   <div class="container"> |   <div class="file-view"> | ||||||
|     <div style="display:none;"> |     <h2>文件上传</h2> | ||||||
|       <video width="500" height="240" controls id="upvideo"> |     <FileUploader @upload-success="handleUploadSuccess" /> | ||||||
|       </video> |     <el-button type="primary" class="mt-16" @click="createBucket"> | ||||||
|     </div> |       创建 Bucket | ||||||
|     <h2>上传示例</h2> |     </el-button> | ||||||
|     <div class="upload-demo"> |  | ||||||
|       <el-upload ref="upload" action="https://jsonplaceholder.typicode.com/posts/" |  | ||||||
|                  :on-remove="handleRemove" :on-change="handleFileChange" :file-list="data.uploadFileList" |  | ||||||
|                  :show-file-list="false" |  | ||||||
|                  :auto-upload="false" multiple> |  | ||||||
|         <el-button slot="trigger" type="primary" plain>选择文件</el-button> |  | ||||||
|       </el-upload> |  | ||||||
|       <el-button style="margin: 5px;" type="success" @click="handler">上传</el-button> |  | ||||||
|       <el-button type="danger" @click="clearFileHandler">清空</el-button> |  | ||||||
|     </div> |  | ||||||
|     <table style="margin-top: 20px"> |  | ||||||
|       <th> |  | ||||||
|         文件名 |  | ||||||
|       </th> |  | ||||||
|       <th> |  | ||||||
|         文件大小 |  | ||||||
|       </th> |  | ||||||
|       <th> |  | ||||||
|         上传进度 |  | ||||||
|       </th> |  | ||||||
|       <th> |  | ||||||
|         状态 |  | ||||||
|       </th> |  | ||||||
|     </table> |  | ||||||
|     <!-- 文件列表 --> |  | ||||||
|     <div class="file-list-wrapper"> |  | ||||||
|       <el-collapse> |  | ||||||
|         <el-collapse-item v-for="item in data.uploadFileList"> |  | ||||||
|           <template #title> |  | ||||||
|             <div class="upload-file-item"> |  | ||||||
|               <div class="file-info-item file-name" :title="item.name">{{ item.name }}</div> |  | ||||||
|               <div class="file-info-item file-size">{{ item.size }}</div> |  | ||||||
|               <div class="file-info-item file-progress"> |  | ||||||
|                 <span class="file-progress-label"></span> |  | ||||||
|                 <el-progress :percentage="item.uploadProgress" class="file-progress-value"/> |  | ||||||
|               </div> |  | ||||||
|               <div class="file-info-item file-size"><span></span> |  | ||||||
|                 <el-tag v-if="item.status === '等待上传'" size="small" type="info">等待上传</el-tag> |  | ||||||
|                 <el-tag v-else-if="item.status === '校验MD5'" size="small" type="warning">校验MD5</el-tag> |  | ||||||
|                 <el-tag v-else-if="item.status === '正在上传'" size="small">正在上传</el-tag> |  | ||||||
|                 <el-tag v-else-if="item.status === '上传成功'" size="small" type="success">上传完成</el-tag> |  | ||||||
|                 <el-tag v-else size="small">正在上传</el-tag> |  | ||||||
|                 <!--                                <el-tag v-else size="medium" type="danger">上传错误</el-tag>--> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|           </template> |  | ||||||
|           <div class="file-chunk-list-wrapper"> |  | ||||||
|             <!-- 分片列表 --> |  | ||||||
|             <el-table :data="item.chunkList" max-height="400" style="width: 100%"> |  | ||||||
|               <el-table-column prop="chunkNumber" label="分片序号" width="180"> |  | ||||||
|               </el-table-column> |  | ||||||
|               <el-table-column prop="progress" label="上传进度"> |  | ||||||
|                 <template v-slot="{ row }"> |  | ||||||
|                   <el-progress v-if="!row.status || row.progressStatus === 'normal'" |  | ||||||
|                                :percentage="row.progress"/> |  | ||||||
|                   <el-progress v-else :percentage="row.progress" :status="row.progressStatus" |  | ||||||
|                                :text-inside="true" :stroke-width="16"/> |  | ||||||
|                 </template> |  | ||||||
|               </el-table-column> |  | ||||||
|               <el-table-column prop="status" label="状态" width="180"> |  | ||||||
|               </el-table-column> |  | ||||||
|             </el-table> |  | ||||||
|           </div> |  | ||||||
|         </el-collapse-item> |  | ||||||
|       </el-collapse> |  | ||||||
|     </div> |  | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup> | <script lang="ts"> | ||||||
| import {ref, reactive} from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import {checkUpload, initUpload, mergeUpload, uploadFileInfo} from '@/api/upload'; | import FileUploader from '../components/Resource/FileUploader.vue'; | ||||||
| import {fileSuffixTypeUtil} from '@/utils/FileUtil'; | import { createBucket } from '../api/upload.js'; | ||||||
| import axios from 'axios'; | import { ElInput, ElButton, ElMessage } from 'element-plus'; | ||||||
| import SparkMD5 from 'spark-md5'; |  | ||||||
| import {ElMessageBox} from "element-plus"; |  | ||||||
| 
 | 
 | ||||||
| const FILE_UPLOAD_ID_KEY = 'file_upload_id'; | export default defineComponent({ | ||||||
| const chunkSize = 10 * 1024 * 1024; // 10MB |   name: 'FileView', | ||||||
| let currentFileIndex = 0; |   components: { | ||||||
| 
 |     FileUploader, | ||||||
| const FileStatus = { |   }, | ||||||
|   wait: '等待上传', |   setup() { | ||||||
|   getMd5: '校验MD5', |     const handleUploadSuccess = () => { | ||||||
|   chip: '正在创建序列', |       ElMessage.success('文件上传成功!'); | ||||||
|   uploading: '正在上传', |       // 可以在这里执行其他操作,如刷新列表等 | ||||||
|   success: '上传成功', |  | ||||||
|   error: '上传错误' |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const simultaneousUploads = ref(3); |  | ||||||
| const data = reactive({ |  | ||||||
|   uploadFileList: [] |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const handler = () => { |  | ||||||
|   if (data.uploadFileList.length === 0) { |  | ||||||
|     ElMessageBox.alert('请先选择文件') |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|   if (currentFileIndex >= data.uploadFileList.length) { |  | ||||||
|     ElMessageBox.alert('文件上传完成') |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|   const currentFile = data.uploadFileList[currentFileIndex]; |  | ||||||
|   currentFile.status = FileStatus.getMd5; |  | ||||||
|   currentFile.chunkUploadedList = []; |  | ||||||
| 
 |  | ||||||
|   getFileMd5(currentFile.raw, async (md5, totalChunks) => { |  | ||||||
|     const checkResult = await checkFileUploadedByMd5(md5); |  | ||||||
|     if (checkResult.code === 1) { |  | ||||||
|       currentFile.status = FileStatus.success; |  | ||||||
|       currentFile.uploadProgress = 100; |  | ||||||
|       currentFileIndex++; |  | ||||||
|       handler(); |  | ||||||
|       return; |  | ||||||
|     } else if (checkResult.code === 2) { |  | ||||||
|       currentFile.status = FileStatus.uploading; |  | ||||||
|       currentFile.chunkUploadedList = checkResult.data.chunkUploadedList; |  | ||||||
|     } else { |  | ||||||
|       console.log('未上传'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     currentFile.status = FileStatus.chip; |  | ||||||
|     let fileChunks = createFileChunk(currentFile.raw, chunkSize); |  | ||||||
|     let type = fileSuffixTypeUtil(currentFile.name); |  | ||||||
|     let param = { |  | ||||||
|       fileName: currentFile.name, |  | ||||||
|       fileSize: currentFile.size, |  | ||||||
|       chunkSize: chunkSize, |  | ||||||
|       chunkNum: totalChunks, |  | ||||||
|       fileMd5: md5, |  | ||||||
|       contentType: 'application/octet-stream', |  | ||||||
|       fileType: type, |  | ||||||
|       chunkUploadedList: currentFile.chunkUploadedList |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let uploadIdInfoResult = await getFileUploadUrls(param); |     const createBucket = async () => { | ||||||
|     let uploadIdInfo = uploadIdInfoResult.data; |       const bucketName = prompt('请输入要创建的 Bucket 名称'); | ||||||
|     let uploadUrls = uploadIdInfo.urlList; |       if (!bucketName) { | ||||||
| 
 |         ElMessage.warning('Bucket 名称不能为空'); | ||||||
|     currentFile.chunkList = []; |         return; | ||||||
|     if (uploadUrls && fileChunks.length !== uploadUrls.length) { |  | ||||||
|       await ElMessageBox.alert('文件上传完成') |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fileChunks.map((chunkItem, index) => { |  | ||||||
|       if (currentFile.chunkUploadedList.indexOf(index + 1) !== -1) { |  | ||||||
|         currentFile.chunkList.push({ |  | ||||||
|           chunkNumber: index + 1, |  | ||||||
|           chunk: chunkItem, |  | ||||||
|           uploadUrl: uploadUrls[index], |  | ||||||
|           progress: 100, |  | ||||||
|           progressStatus: 'success', |  | ||||||
|           status: '上传成功' |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         currentFile.chunkList.push({ |  | ||||||
|           chunkNumber: index + 1, |  | ||||||
|           chunk: chunkItem, |  | ||||||
|           uploadUrl: uploadUrls[index], |  | ||||||
|           progress: 0, |  | ||||||
|           status: '—' |  | ||||||
|         }); |  | ||||||
|       } |       } | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     let tempFileChunks = []; |       try { | ||||||
|     currentFile.chunkList.forEach((item) => { |         await createBucket(); | ||||||
|       tempFileChunks.push(item); |         ElMessage.success('Bucket 创建成功'); | ||||||
|     }); |       } catch (error: any) { | ||||||
| 
 |         ElMessage.error(error.response?.data?.message || 'Bucket 创建失败'); | ||||||
|     currentFile.status = FileStatus.uploading; |  | ||||||
|     tempFileChunks = processUploadChunkList(tempFileChunks); |  | ||||||
|     await uploadChunkBase(tempFileChunks); |  | ||||||
| 
 |  | ||||||
|     if (uploadIdInfo.uploadId === "SingleFileUpload") { |  | ||||||
|       currentFile.status = FileStatus.success; |  | ||||||
|       currentFileIndex++; |  | ||||||
|       handler(); |  | ||||||
|       return; |  | ||||||
|     } else { |  | ||||||
|       const mergeResult = await mergeFile({ |  | ||||||
|         uploadId: uploadIdInfo.uploadId, |  | ||||||
|         fileName: currentFile.name, |  | ||||||
|         fileMd5: md5, |  | ||||||
|         fileType: type, |  | ||||||
|         chunkNum: uploadIdInfo.urlList.length, |  | ||||||
|         chunkSize: chunkSize, |  | ||||||
|         fileSize: currentFile.size |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       if (!mergeResult.data) { |  | ||||||
|         currentFile.status = FileStatus.error; |  | ||||||
|         this.$message.error(mergeResult.error); |  | ||||||
|       } else { |  | ||||||
|         localStorage.removeItem(FILE_UPLOAD_ID_KEY); |  | ||||||
|         currentFile.status = FileStatus.success; |  | ||||||
|         currentFileIndex++; |  | ||||||
|         handler(); |  | ||||||
|       } |       } | ||||||
|     } |     }; | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const clearFileHandler = () => { |     return { | ||||||
|   data.uploadFileList.splice(0, data.uploadFileList.length); |       handleUploadSuccess, | ||||||
|   currentFileIndex = 0; |       createBucket, | ||||||
| }; |     }; | ||||||
| 
 |   }, | ||||||
| const handleFileChange = (file, fileList) => { | }); | ||||||
|   initFileProperties(file); |  | ||||||
|   data.uploadFileList.splice(0, data.uploadFileList.length, ...fileList); |  | ||||||
|   console.log("data.uploadFileList", data.uploadFileList) |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const initFileProperties = (file) => { |  | ||||||
|   file.chunkList = []; |  | ||||||
|   file.status = FileStatus.wait; |  | ||||||
|   file.progressStatus = 'warning'; |  | ||||||
|   file.uploadProgress = 0; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const handleRemove = (file, fileList) => { |  | ||||||
|   data.uploadFileList.splice(0, data.uploadFileList.length, ...fileList); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const getFileMd5 = (file, callback) => { |  | ||||||
|   let fileReader = new FileReader(); |  | ||||||
|   fileReader.readAsArrayBuffer(file); |  | ||||||
|   fileReader.onload = function (e) { |  | ||||||
|     let spark = new SparkMD5.ArrayBuffer(); |  | ||||||
|     spark.append(e.target.result); |  | ||||||
|     callback(spark.end(), Math.ceil(file.size / chunkSize)); |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const createFileChunk = (file, size) => { |  | ||||||
|   let fileChunks = []; |  | ||||||
|   let cur = 0; |  | ||||||
|   while (cur < file.size) { |  | ||||||
|     fileChunks.push({file: file.slice(cur, cur + size)}); |  | ||||||
|     cur += size; |  | ||||||
|   } |  | ||||||
|   return fileChunks; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const checkFileUploadedByMd5 = async (md5) => { |  | ||||||
|   const response = await checkUpload(md5); |  | ||||||
|   return response; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const getFileUploadUrls = async (param) => { |  | ||||||
|   const response = await initUpload(param); |  | ||||||
|   return response; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const processUploadChunkList = (chunkList) => { |  | ||||||
|   const temp = []; |  | ||||||
|   chunkList.forEach((chunk) => { |  | ||||||
|     temp.push(chunk); |  | ||||||
|   }); |  | ||||||
|   return temp; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const uploadChunkBase = async (chunkList) => { |  | ||||||
|   const uploadPromiseList = []; |  | ||||||
|   const limit = simultaneousUploads.value; |  | ||||||
|   for (let i = 0; i < chunkList.length; i++) { |  | ||||||
|     if (chunkList[i].progress !== 100) { |  | ||||||
|       chunkList[i].status = FileStatus.uploading; |  | ||||||
|       let params = chunkList[i]; |  | ||||||
|       uploadPromiseList.push( |  | ||||||
|           uploadFileChunk(params) |  | ||||||
|               .then(() => { |  | ||||||
|                 chunkList[i].progress = 100; |  | ||||||
|                 chunkList[i].progressStatus = 'success'; |  | ||||||
|                 chunkList[i].status = '上传成功'; |  | ||||||
|               }) |  | ||||||
|               .catch(() => { |  | ||||||
|                 chunkList[i].progress = 100; |  | ||||||
|                 chunkList[i].progressStatus = 'exception'; |  | ||||||
|                 chunkList[i].status = '上传失败'; |  | ||||||
|               }) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|     if (uploadPromiseList.length === limit || i === chunkList.length - 1) { |  | ||||||
|       await Promise.all(uploadPromiseList); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const uploadFileChunk = async (chunk) => { |  | ||||||
|   let formData = new FormData(); |  | ||||||
|   formData.append('file', chunk.chunk.file); |  | ||||||
|   await axios.put(chunk.uploadUrl, formData, { |  | ||||||
|     headers: {'Content-Type': 'application/octet-stream'} |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .container { | .file-view { | ||||||
|   padding: 20px; |   padding: 20px; | ||||||
| } | } | ||||||
| 
 | .mt-16 { | ||||||
| .file-list-wrapper { |   margin-top: 16px; | ||||||
|   margin-top: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .upload-file-item { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .file-info-item { |  | ||||||
|   flex: 1; |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .file-name { |  | ||||||
|   text-align: left; |  | ||||||
|   padding-left: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .file-progress { |  | ||||||
|   width: 200px; |  | ||||||
|   margin: 0 20px; |  | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user