浏览代码

多媒体分片直传

cldu 2 月之前
父节点
当前提交
f246837935
共有 4 个文件被更改,包括 170 次插入11 次删除
  1. 1 0
      package.json
  2. 10 1
      src/api/media/common.js
  3. 65 0
      src/views/media/common/uploadFile.js
  4. 94 10
      src/views/media/components/MediaUpload.vue

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@aws-sdk/client-s3": "^3.726.1",
     "@element-plus/icons-vue": "^2.3.1",
     "axios": "^1.6.8",
     "dayjs": "^1.11.12",

+ 10 - 1
src/api/media/common.js

@@ -7,5 +7,14 @@ export default{
      */
     getPermissionList: params => {
         return get('/chart_permission/list',params)
-    }
+    },
+    /**
+     * 获取minio上传签名
+     * @param {*} params 
+     * @param {String} params.FileExt
+     * @returns 
+     */
+    getMinioSign: params => {
+        return get('/media/minioSign',params)
+    },
 }

+ 65 - 0
src/views/media/common/uploadFile.js

@@ -0,0 +1,65 @@
+import { S3Client, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand , AbortMultipartUploadCommand } from "@aws-sdk/client-s3";
+//分片上传   minio
+export async function uploadFileToMinIO(file, options) {
+    const { endpoint, region, forcePathStyle, credentials, bucketName , chunkSize , imgHost , onProgress , prePath } = options;
+
+    const client = new S3Client({
+        endpoint,
+        region,
+        forcePathStyle,
+        credentials,
+    });
+    const fileKey = `${prePath || ''}${Date.now()}-${file.name}`;
+    let uploadId;
+    
+    try {
+        const createUploadCommand = new CreateMultipartUploadCommand({
+            Bucket: bucketName,
+            Key: fileKey,
+        });
+        const { UploadId } = await client.send(createUploadCommand);
+        uploadId = UploadId;
+        const parts = [];
+        const totalChunks = Math.ceil(file.size / chunkSize);
+
+        for (let index = 0; index < totalChunks; index++) {
+            const start = index * chunkSize;
+            const end = Math.min(start + chunkSize, file.size);
+            const chunk = file.slice(start, end);
+      
+            const uploadPartCommand = new UploadPartCommand({
+                Bucket: bucketName,
+                Key: fileKey,
+                UploadId: uploadId,
+                PartNumber: index + 1,
+                Body: chunk,
+            });
+      
+            const { ETag } = await client.send(uploadPartCommand);
+            parts.push({ PartNumber: index + 1, ETag });
+            onProgress && onProgress(Math.round(((index + 1) / totalChunks) * 100));
+        }
+
+        const completeCommand = new CompleteMultipartUploadCommand({
+            Bucket: bucketName,
+            Key: fileKey,
+            UploadId: uploadId,
+            MultipartUpload: { Parts: parts },
+        });
+        await client.send(completeCommand);
+        return  `${imgHost}${fileKey}`;
+        
+    } catch (error) {
+        if(uploadId){
+            await client.send(
+                new AbortMultipartUploadCommand({
+                    Bucket: bucketName,
+                    Key: fileKey,
+                    UploadId: uploadId,
+                })
+            );
+        }
+        throw error;
+    }
+
+}

+ 94 - 10
src/views/media/components/MediaUpload.vue

@@ -3,9 +3,12 @@ import { ref, reactive, computed, watch } from 'vue'
 import {apiAudio,apiVideo,apiMediaCommon} from '@/api/media'
 import { apiAuthor } from '@/api/author'
 import { ElMessage } from 'element-plus'
+import { uploadFileToMinIO } from "../common/uploadFile"
 
 const emits = defineEmits('save')
 const show = defineModel('show', { type: Boolean, default: false })
+const showProgress = ref(false);
+const percentage = ref(0);
 const props = defineProps({
     mediaType:{
         type:String,
@@ -75,6 +78,8 @@ watch(show,(newval)=>{
             mediaCover:'',
             duration:0
         })
+        showProgress.value = false;
+        percentage.value = 0;
     }
 })
 
@@ -107,6 +112,33 @@ const uploadRef = ref(null)
 function handleUpload(){
     uploadRef.value?.$el.getElementsByTagName('input')[0].click()
 }
+
+function getMediaDuration(file,callback,failback){
+    let durations = 0;
+    const url = URL.createObjectURL(file);
+    const mediaElement = document.createElement(file.type.startsWith("audio") ? "audio" : "video");
+    mediaElement.src = url;
+    mediaElement.preload = "metadata"; 
+    mediaElement.onloadedmetadata = () => {
+        durations = Math.ceil(mediaElement.duration * 1000);  //ms
+        URL.revokeObjectURL(url); 
+        callback(durations)
+    };
+    mediaElement.onerror = () => {
+        ElMessage.error('获取多媒体时长失败,请重新上传')
+        URL.revokeObjectURL(url);
+        failback()
+    };
+}
+
+function getPrePath(file){
+    const date = new Date();
+    const year = date.getFullYear();
+    const month = ((date.getMonth() + 1) < 10 ? '0' : '') + (date.getMonth() + 1);
+    const day = (date.getDate() < 10 ? '0' : '') + date.getDate();
+    return `ht/${file.type.startsWith("audio") ? "audio" : "video"}/${'' + year + month}/${'' + year + month + day}/`
+}
+
 async function handleUploadMedia(file){
     uploadLoading.value = true
     const {type} = file.file
@@ -115,17 +147,55 @@ async function handleUploadMedia(file){
         uploadLoading.value = false
         return 
     }
-    let form = new FormData();
-    form.append('File',file.file);
-    const res = props.mediaType==='audio'
-        ?await apiAudio.uploadAudioFile(form)
-        :await apiVideo.uploadVideoFile(form)
-    uploadLoading.value = false
-    if(res.Ret!==200) return 
-    mediaData.fileUrl = res.Data.Url||""
-    mediaData.duration = res.Data.DurationMillisecond||0
     
+    getMediaDuration(file.file,async (durations) => { //success
+        let res = await apiMediaCommon.getMinioSign();
+        if(res.Ret != 200) {
+            ElMessage.warning('获取minio临时签名错误')
+            uploadLoading.value = false
+            return
+        };
+        let { Data } = res;
+        percentage.value = 0;
+        showProgress.value = true;
+        try {
+            const fileUrl = await uploadFileToMinIO(file.file,{
+                    endpoint:Data.Endpoint,
+                    region: Data.RegionId,
+                    forcePathStyle: true,
+                    credentials: {
+                        accessKeyId:Data.AccessKeyId,
+                        secretAccessKey:Data.SecretKeyId,
+                    },
+                    chunkSize : 5 * 1024 * 1024,
+                    bucketName:Data.Bucketname,
+                    imgHost:Data.ImgHost,
+                    prePath:getPrePath(file.file),//存储路径,没有则不传
+                    onProgress: (progress) => {
+                        percentage.value = progress;
+                    }
+            });
+            setTimeout(() => {
+                ElMessage.success('上传成功');
+                uploadLoading.value = false;
+                showProgress.value = false;
+                percentage.value = 0;
+                mediaData.fileUrl = fileUrl||"";
+                mediaData.duration = durations||0;
+            }, 300);
+        } catch (error) {
+            uploadLoading.value = false
+            showProgress.value = false;
+            percentage.value = 0;
+            ElMessage.error('上传失败,请重试')
+        }
+    },() => { //fail
+        uploadLoading.value = false;
+        showProgress.value = false;
+        percentage.value = 0;
+    })
 }
+
 //获取音频长度
 function getAudioDuration(file){
     return new Promise((resolve,reject)=>{
@@ -209,7 +279,7 @@ function handleUploadImg(file){
     <el-dialog v-model="show" :title="dialogTitle" width="530px" draggable>
         <div class="content-wrap">
             <el-form label-width="95px" label-position="left" :model="mediaData" :rules="rules" ref="formRef">
-                <el-form-item prop="fileUrl" class="upload-form-item">
+                <el-form-item prop="fileUrl" class="upload-form-item" style="position: relative;">
                     <el-input :placeholder="props.mediaType==='audio'?'音频格式限制mp3、m4a':'视频格式限制mp4,编码器为H.264'" v-model="mediaData.fileUrl" disabled>
                         <template #append>
                             <el-button 
@@ -229,6 +299,7 @@ function handleUploadImg(file){
                             </el-upload>
                         </template>
                     </el-input>
+                    <el-progress v-if="showProgress" type="circle" :percentage="percentage" :width="50" class="progress-postion"/>
                 </el-form-item>
                 <el-form-item prop="mediaCover" label="照片" v-if="props.mediaType==='video'">
                     <ImageUpload
@@ -283,5 +354,18 @@ function handleUploadImg(file){
     .upload-form-item{
         margin-left: -95px;
     }
+    .progress-postion{
+        position: absolute;
+        right: -50px;
+        top: 50%;
+        transform: translateY(-50%);
+    }
+}
+</style>
+<style lang="scss">
+.content-wrap{
+    .el-progress__text{
+        font-size: 12px !important;
+    }
 }
 </style>