vue+Minio實(shí)現(xiàn)多文件進(jìn)度上傳的詳細(xì)步驟
背景
最近突然接到了一個(gè)產(chǎn)品的需求,有點(diǎn)特別,在這里給大家分享一下,需求如下
- 提交表單,同時(shí)要上傳模型資源
- 模型文件是大文件,要顯示上傳進(jìn)度,同時(shí)可以刪除
- 模型文件要上傳到服務(wù)器,表單數(shù)據(jù)同步到數(shù)據(jù)庫(kù)
- 同時(shí)要同步上傳后的模型地址到數(shù)據(jù)庫(kù)
- 后端使用Minio做文件管理
設(shè)計(jì)圖如下
一開(kāi)始以為是一個(gè)簡(jiǎn)單的表單上傳,發(fā)現(xiàn)并不是,這是大文件上傳啊,但很快又發(fā)現(xiàn),不單單是上傳大文件,還有將文件信息關(guān)聯(lián)到表單。
基于這個(gè)奇葩的情況,我和后端兄弟商量了一下,決定使用如下方案
實(shí)現(xiàn)方案
分2步走
- 點(diǎn)擊上傳時(shí),先提交表單信息到數(shù)據(jù)庫(kù),接著后端返回一個(gè)表單的id給我
- 當(dāng)所有文件上傳完成后,再調(diào)用另外一個(gè)服務(wù),將上傳完成后的地址和表單id發(fā)送給后端
如此,便完成了上面的需求
了解一下Mino
這里大家先了解一下Minio的js SDK文檔
里面有2個(gè)很重要的接口,今天要用到
一個(gè)是給文件生成用于put方法上傳的地址 |
---|
![]() |
一個(gè)是獲取已經(jīng)上傳完成后的文件的get下載地址 |
---|
實(shí)現(xiàn)步驟
這里是使用原生的 ajax請(qǐng)求進(jìn)行上傳的,至于為什么,后面會(huì)有說(shuō)到
1.創(chuàng)建存儲(chǔ)桶
創(chuàng)建一個(gè)Minio上傳實(shí)例
var Minio = require('minio') this.minioClient = new Minio.Client({ endPoint: '192.168.172.162', //后端提供 port: 9000, //端口號(hào)默認(rèn)9000 useSSL: true, accessKey: 'Q3AM3UQ867SPQQA43P2F', //后端提供 secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' }); this.userBucket = 'yourBucketName' //這里后端需要提供給你,一個(gè)存儲(chǔ)桶名字
2.選擇文件
這里使用input標(biāo)簽選擇文件,點(diǎn)擊選擇文件的時(shí)候,調(diào)用一下input的click方法,就可以打開(kāi)本地文件夾
<el-form-item label="資源文件"> <el-button style="marginRight:10px;" @click="selectFile()" size="mini" >選擇文件</el-button> <input :accept="acceptFileType" multiple="multiple" type="file" id="uploadInput" ref="uploadInput" v-show="false" @change="getAndFormatFile()" > <i class="tip">僅支持.gbl、.gltf、.fbx、.obj、.mtl、.hdr、.png、.jpg格式的文件</i> <i class="tip">單個(gè)文件的大小限制為128MB</i> </el-form-item>
selectFile() { let inputDOM = this.$refs.uploadInput inputDOM.click(); },
接著就是對(duì)文件進(jìn)行格式化
//格式化文件并創(chuàng)建上傳隊(duì)列 getAndFormatFile(){ let files = this.$refs.uploadInput.files const userBucket = this.userBucket if(files.length > 6) { this.$message({ message: `最大只能上傳6個(gè)文件`, type: 'warning' }) return } files.forEach((file, index) => { if ((file.size / 1024 / 1024).toFixed(2) > 128) { //單個(gè)文件限制大小為128MB this.$message({ message: `文件大小不能超過(guò)128MB`, type: 'warning' }) return } //創(chuàng)建文件的put方法的url this.minioClient.presignedPutObject(userBucket, file.name, 24 * 60 * 60, (err, presignedUrl) => { if (err) { this.$message({ message: `服務(wù)器連接超時(shí)`, type: 'error' }) return err } let fileIcon = this.getFileIcon(file) let fileUploadProgress = '0%' //文件上傳進(jìn)度 this.fileInfoList.push({ file, //文件 fileIcon, //文件對(duì)應(yīng)的圖標(biāo) className fileUploadProgress, //文件上傳進(jìn)度 filePutUrl: presignedUrl, //文件上傳put方法的url fileGetUrl: '', //文件下載的url }) }) }) this.fileList = [...this.fileInfoList] },
3.創(chuàng)建上傳隊(duì)列
這里定義了一個(gè)創(chuàng)建文件上傳請(qǐng)求的方法,使用原生的XMLHttpRequest,它接受以下參數(shù)
- file:要上傳的文件
- filePutUrl:文件上傳的put方法地址
- customHeader: 自定義的頭信息
- onUploadProgress:文件上傳的進(jìn)度監(jiān)聽(tīng)函數(shù)
- onUploaded:文件上傳完成的監(jiān)聽(tīng)函數(shù)
- onError:文件上傳出錯(cuò)的監(jiān)聽(tīng)函數(shù)
//創(chuàng)建上傳文件的http createUploadHttp(config){ const {file, filePutUrl, customHeader, onUploadProgress, onUploaded, onError} = config let fileName = file.name let http = new XMLHttpRequest(); http.upload.addEventListener("progress", (e) => { //監(jiān)聽(tīng)http的進(jìn)度。并執(zhí)行進(jìn)度監(jiān)聽(tīng)函數(shù) onUploadProgress({ progressEvent: e, uploadingFile: file }) }, false) http.onload = () => { if (http.status === 200 && http.status < 300 || http.status === 304) { try { //監(jiān)聽(tīng)http的完成事件,并執(zhí)行上傳完成的監(jiān)聽(tīng)函數(shù) const result = http.responseURL onUploaded({ result, uploadedFile: file}) } catch(error) { //監(jiān)聽(tīng)錯(cuò)誤 onError({ error, errorFile: file}) } } } http.open("PUT", filePutUrl, true); //加入頭信息 Object.keys(customHeader).forEach((key, index) =>{ http.setRequestHeader(key, customHeader[key]) }) http.send(file); return http //返回該http實(shí)例 }
4.開(kāi)始上傳
//上傳文件到存儲(chǔ)桶 async handleUplaod(){ let _this = this if(this.fileInfoList.length < 1) { this.$message({ message: `請(qǐng)先選擇文件`, type: 'warning' }) return } //先上傳文件的基本表單信息,獲取表單信息的id try{ const {remark, alias} = _this.uploadFormData let res = await uploadModelSourceInfo({remark, serviceName: alias}) _this.modelSourceInfoId = res.message }catch(error){ if(error) { _this.$message({ message: `上傳失敗,請(qǐng)檢查服務(wù)`, type: 'error' }) return } } //開(kāi)始將模型資源上傳到遠(yuǎn)程的存儲(chǔ)桶 this.fileList.forEach((item, index) => { const {file, filePutUrl} = item let config = { file, filePutUrl, customHeader:{ "X-FILENAME": encodeURIComponent(file.name), "X-Access-Token": getToken() }, onUploadProgress: ({progressEvent, uploadingFile}) => { let progress = (progressEvent.loaded / progressEvent.total).toFixed(2) this.updateFileUploadProgress(uploadingFile, progress) }, onUploaded: ({result, uploadedFile}) => { this.updateFileDownloadUrl(uploadedFile) }, onError: ({error, errorFile}) => { } } let httpInstance = this.createUploadHttp(config) //創(chuàng)建http請(qǐng)求實(shí)例 this.httpQueue.push(httpInstance) //將http請(qǐng)求保存到隊(duì)列中 }) }, //更新對(duì)應(yīng)文件的上傳進(jìn)度 updateFileUploadProgress(uploadingFile, progress) { this.fileInfoList.forEach((item, index) => { if(item.file.name === uploadingFile.name){ item.fileUploadProgress = (Number(progress)*100).toFixed(2) + '%' } }) }, //更新上傳完成文件的下載地址 updateFileDownloadUrl(uploadedFile){ const userBucket = this.userBucket this.fileInfoList.forEach((item, index) => { if(item.file.name === uploadedFile.name){ this.minioClient.presignedGetObject(userBucket, uploadedFile.name, 24*60*60, (err, presignedUrl) => { if (err) return console.log(err) item.fileGetUrl = presignedUrl }) } }) },
5 上傳完成后,同步文件地址給后端
在watch里監(jiān)聽(tīng)文件列表,當(dāng)所有的文件進(jìn)度都是100%時(shí),表示上傳完成,接著就可以同步文件信息
watch:{ fileInfoList: { handler(val){ //1.3所有文件都上傳到存儲(chǔ)桶后,將上傳完成后的文件地址、文件名字同步后端 if(val.length < 1) return let allFileHasUpload = val.every((item, index) => { return item.fileGetUrl.length > 1 }) if(allFileHasUpload) { this.allFileHasUpload = allFileHasUpload const {modelSourceInfoId} = this if(modelSourceInfoId.length < 1) { return } const url = process.env.VUE_APP_BASE_API + "/vector-map/threeDimensionalModelService/invokeMapService" const files = val.map((ite, idx) => { return { fileName: ite.file.name, fileUrl: ite.fileGetUrl } }) this.syncAllUploadedFile(url, files, modelSourceInfoId) } }, deep: true } }, //同步已上傳的文件到后端 syncAllUploadedFile(url, files, modelSourceInfoId){ let xhr = new XMLHttpRequest() xhr.onload = () => { if (xhr.status === 200 && xhr.status < 300 || xhr.status === 304) { try { const res = JSON.parse(xhr.responseText) if(res && res.code === 200){ this.$message({ message: '上傳完成', type: 'success' }) this.$emit('close') this.fileInfoList = [] this.fileList = [] this.httpQueue = [] } } catch(error) { this.$message({ message: '上傳失敗,請(qǐng)檢查服務(wù)', type: 'error' }) } } } xhr.open("post", url, true) xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('X-Access-Token', getToken()) //將前面1.1獲取文件信息的id作為頭信息傳遞到后端 xhr.setRequestHeader('ThreeDimensionalModel-ServiceID', modelSourceInfoId) xhr.send(JSON.stringify(files)) },
6.刪除文件
刪除文件時(shí)要注意
- 刪除本地的文件緩存
- 刪除存儲(chǔ)桶里面的文件
- 停止當(dāng)前文件對(duì)應(yīng)的http請(qǐng)求
//刪除文件,并取消正在文件的上傳 deleteFile(fileInfo, index){ this.httpQueue[index] && this.httpQueue[index].abort() this.httpQueue[index] && this.httpQueue.splice(index, 1) this.fileInfoList.splice(index, 1) this.fileList.splice(index, 1) this.removeRemoteFile(fileInfo) }, //清空文件并取消上傳隊(duì)列 clearFile() { this.fileInfoList.forEach((item, index) => { this.httpQueue[index] && this.httpQueue[index].abort() this.httpQueue[index] && this.httpQueue.splice(index, 1) this.removeRemoteFile(item) }) this.fileInfoList = [] this.httpQueue = [] this.fileList = [] }, //刪除遠(yuǎn)程文件 removeRemoteFile(fileInfo){ const userBucket = this.userBucket const { fileUploadProgress, file} = fileInfo const fileName = file.name const complete = fileUploadProgress === '100.00%' ? true : false if(complete){ this. minioClient.removeObject(userBucket, fileName, function(err) { if (err) { return console.log('Unable to remove object', err) } console.log('Removed the object') }) }else{ this.minioClient.removeIncompleteUpload(userBucket, fileName, function(err) { if (err) { return console.log('Unable to remove incomplete object', err) } console.log('Incomplete object removed successfully.') }) } },
完整代碼
這里的完整代碼是我直接從工程里拷貝出來(lái)的,里面用到了一些自己封裝的服務(wù)和方法 比如 后端的接口、AES解密、獲取Token、表單驗(yàn)證等
import{uploadModelSourceInfo, uploadModelSource, getMinioConfig} from '@/api/map' import AES from '@/utils/AES.js' import { getToken } from '@/utils/auth' import * as myValiDate from "@/utils/formValidate";
/** * 文件說(shuō)明 * @Author: zhuds * @Description: 模型資源上傳彈窗 分為3個(gè)步驟 1.先將文件的基本表單信息上傳給后端,獲取文件信息的ID 2.然后將文件上傳存儲(chǔ)桶 3.等所有文件都上傳完成后,再將上傳完成后的文件信息傳遞給后端,注意,此時(shí)的請(qǐng)求頭要戴上第1步獲取的文件信息id * @Date: 2/28/2022, 1:13:20 PM * @LastEditDate: 2/28/2022, 1:13:20 PM * @LastEditor: */ <template> <div class="upload-model"> <el-dialog :visible.sync="isVisible" @close="close()" :show-close ="false" :close-on-click-modal="false" top="10vh" v-if="isVisible" :destroy-on-close="true" > <div slot="title" class="header-title"> <div class="icon"></div> <span>上傳模型資源</span> <i class="el-icon-close" @click="close()"></i> </div> <el-form :label-position="labelPosition" label-width="80px" :model="uploadFormData" ref="form" :rules="rules" > <el-form-item label="別名"> <el-input size="small" v-model="uploadFormData.alias"></el-input> </el-form-item> <el-form-item label="備注"> <el-input type="textarea" v-model="uploadFormData.remark" size="small"></el-input> </el-form-item> <el-form-item label="資源文件"> <el-button style="marginRight:10px;" @click="selectFile()" size="mini" >選擇文件</el-button> <input :accept="acceptFileType" multiple="multiple" type="file" id="uploadInput" ref="uploadInput" v-show="false" @change="getAndFormatFile()" > <i class="tip">僅支持.gbl、.gltf、.fbx、.obj、.mtl、.hdr、.png、.jpg格式的文件</i> <i class="tip">單個(gè)文件的大小限制為128MB</i> </el-form-item> </el-form> <div class="file-list" v-show="fileInfoList.length > 0"> <div class="file-item" v-for="(item, index) in fileInfoList" :key="index"> <div class="icon"></div> <div class="name">{{item.file.name}}</div> <div class="size">{{(item.file.size/1024/1024).toFixed(2)}}MB </div> <div class="progress"> <div class="bar" :style="{width: item.fileUploadProgress}"></div> </div> <div class="rate">{{item.fileUploadProgress}}</div> <div class="delete-btn" @click="deleteFile(item, index)">x</div> </div> </div> <div class="custom-footer"> <button class="info" @click="close()">取 消</button> <button class="success" @click="handleUplaod()">上傳</button> </div> </el-dialog> </div> </template> <script> import{uploadModelSourceInfo, uploadModelSource, getMinioConfig} from '@/api/map' import AES from '@/utils/AES.js' import { getToken } from '@/utils/auth' import * as myValiDate from "@/utils/formValidate"; let Minio = require('minio') export default { name: 'UploadModelDialog', props: { isVisible: { type: Boolean, default: false }, }, data(){ return { labelPosition: 'right', uploadFormData: { alias: '', //服務(wù)名稱(chēng) remark: '', //備注 }, rules: { serviceName: [{ validator: myValiDate.validateServiceName, trigger: "blur", required: true, }], }, acceptFileType:".glb,.gltf,.fbx,.obj,.mtl,.hdr,.png,.jpg, .mp4", fileList:[], //待上傳的文件列表 fileInfoList: [], //格式化后的文件信息列表 userBucket: null, httpQueue: [], //上傳文件的http隊(duì)列 allFileHasUpload: false, //是否完成上傳 modelSourceInfoId: '', //模型資源基本信息的id } }, watch:{ fileInfoList: { handler(val){ //1.3所有文件都上傳到存儲(chǔ)桶后,將上傳完成后的文件地址、文件名字同步后端 if(val.length < 1) return let allFileHasUpload = val.every((item, index) => { return item.fileGetUrl.length > 1 }) if(allFileHasUpload) { this.allFileHasUpload = allFileHasUpload const {modelSourceInfoId} = this if(modelSourceInfoId.length < 1) { return } const url = process.env.VUE_APP_BASE_API + "/vector-map/threeDimensionalModelService/invokeMapService" const files = val.map((ite, idx) => { return { fileName: ite.file.name, fileUrl: ite.fileGetUrl } }) this.syncAllUploadedFile(url, files, modelSourceInfoId) } }, deep: true } }, created() { this.initMinioClient() }, beforeDestroy() { if(!this.allFileHasUpload) { this.clearFile() } }, methods:{ //創(chuàng)建存儲(chǔ)桶 async initMinioClient(){ const { code, result, message } = AES.decryptToJSON(await getMinioConfig({})) if(!result || code !== 200) { this.$customMessage.error({message: '獲取存儲(chǔ)桶配置信息出錯(cuò)'}) return false } let {accessKey, bucketName, endPoint, secretKey} = result //console.log({accessKey, bucketName, endPoint, secretKey}) let endPointStr = endPoint.split(":")[1] let formatPort = Number(endPoint.split(":")[2]) let formatEndPoint = endPointStr.split('//')[1] this.userBucket = bucketName this.minioClient = new Minio.Client({ useSSL: false, partSize: '20M', port: formatPort, endPoint: formatEndPoint, accessKey, secretKey }); let userBucket = this.userBucket //userBucket只能作為字符串變量傳入,不能作為其他變量的屬性或者函數(shù)返回值,屬于Minio的一個(gè)規(guī)定 this.minioClient.bucketExists(userBucket, (err)=> { if (err && err.code == 'NoSuchBucket') { this.minioClient.makeBucket(userBucket, 'us-east-1', (err)=> { if (err) { return console.log('創(chuàng)建存儲(chǔ)桶失敗', err) } // console.log('Bucket created successfully in "us-east-1".') }) }else{ //console.log('存儲(chǔ)桶存在') } }) }, close(flag = false) { this.$emit('close', flag) //關(guān)閉彈窗時(shí),如果文件沒(méi)有上傳完成,則清空文件 if(!this.allFileHasUpload) { this.clearFile() } }, selectFile() { let inputDOM = this.$refs.uploadInput inputDOM.click(); }, getFileSize(file){ let fileSize = '' if(file.size / 1024 < 1){ fileSize = file.size + 'B' }else if(file.size / 1024 /1024 < 1){ fileSize = file.size + 'KB' }else if(file.size / 1024 /1024 >=1){ fileSize = file.size + 'MB' }else{ } return fileSize }, //刪除文件,并取消正在文件的上傳 deleteFile(fileInfo, index){ this.httpQueue[index] && this.httpQueue[index].abort() this.httpQueue[index] && this.httpQueue.splice(index, 1) this.fileInfoList.splice(index, 1) this.fileList.splice(index, 1) this.removeRemoteFile(fileInfo) }, //清空文件并取消上傳隊(duì)列 clearFile() { this.fileInfoList.forEach((item, index) => { this.httpQueue[index] && this.httpQueue[index].abort() this.httpQueue[index] && this.httpQueue.splice(index, 1) this.removeRemoteFile(item) }) this.fileInfoList = [] this.httpQueue = [] this.fileList = [] }, //刪除遠(yuǎn)程文件 removeRemoteFile(fileInfo){ const userBucket = this.userBucket const { fileUploadProgress, file} = fileInfo const fileName = file.name const complete = fileUploadProgress === '100.00%' ? true : false if(complete){ this. minioClient.removeObject(userBucket, fileName, function(err) { if (err) { return console.log('Unable to remove object', err) } console.log('Removed the object') }) }else{ this.minioClient.removeIncompleteUpload(userBucket, fileName, function(err) { if (err) { return console.log('Unable to remove incomplete object', err) } console.log('Incomplete object removed successfully.') }) } }, //格式化文件并創(chuàng)建上傳隊(duì)列 getAndFormatFile(){ let files = this.$refs.uploadInput.files const userBucket = this.userBucket if(files.length > 6) { this.$message({ message: `最大只能上傳6個(gè)文件`, type: 'warning' }) return } files.forEach((file, index) => { if ((file.size / 1024 / 1024).toFixed(2) > 128) { //單個(gè)文件限制大小為128MB this.$message({ message: `文件大小不能超過(guò)128MB`, type: 'warning' }) return } //創(chuàng)建文件上傳的url并格式化文件信息 this.minioClient.presignedPutObject(userBucket, file.name, 24 * 60 * 60, (err, presignedUrl) => { if (err) { this.$message({ message: `服務(wù)器連接超時(shí)`, type: 'error' }) return err } let fileIcon = this.getFileIcon(file) let fileUploadProgress = '0%' this.fileInfoList.push({ file, //文件 fileIcon, //文件對(duì)應(yīng)的圖標(biāo) className fileUploadProgress, //文件上傳進(jìn)度 filePutUrl: presignedUrl, //文件上傳put方法的url fileGetUrl: '', //文件下載的url }) }) }) this.fileList = [...this.fileInfoList] }, //1.上傳文件到存儲(chǔ)桶 async handleUplaod(){ let _this = this if(this.fileInfoList.length < 1) { this.$message({ message: `請(qǐng)先選擇文件`, type: 'warning' }) return } //1.1先上傳文件的基本表單信息,獲取文件信息的id try{ const {remark, alias} = _this.uploadFormData let res = await uploadModelSourceInfo({remark, serviceName: alias}) _this.modelSourceInfoId = res.message }catch(error){ if(error) { _this.$message({ message: `上傳失敗,請(qǐng)檢查服務(wù)`, type: 'error' }) return } } //1.2開(kāi)始將模型資源上傳到遠(yuǎn)程的存儲(chǔ)桶 this.fileList.forEach((item, index) => { const {file, filePutUrl} = item let config = { file, filePutUrl, customHeader:{ "X-FILENAME": encodeURIComponent(file.name), "X-Access-Token": getToken() }, onUploadProgress: ({progressEvent, uploadingFile}) => { let progress = (progressEvent.loaded / progressEvent.total).toFixed(2) this.updateFileUploadProgress(uploadingFile, progress) }, onUploaded: ({result, uploadedFile}) => { this.updateFileDownloadUrl(uploadedFile) }, onError: ({error, errorFile}) => { } } let httpInstance = this.createUploadHttp(config) this.httpQueue.push(httpInstance) }) }, //1更新對(duì)應(yīng)文件的上傳進(jìn)度 updateFileUploadProgress(uploadingFile, progress) { //console.log({uploadingFile, progress}) this.fileInfoList.forEach((item, index) => { if(item.file.name === uploadingFile.name){ item.fileUploadProgress = (Number(progress)*100).toFixed(2) + '%' } }) }, //更新上傳完成文件的下載地址 updateFileDownloadUrl(uploadedFile){ const userBucket = this.userBucket this.fileInfoList.forEach((item, index) => { if(item.file.name === uploadedFile.name){ this.minioClient.presignedGetObject(userBucket, uploadedFile.name, 24*60*60, (err, presignedUrl) => { if (err) return console.log(err) item.fileGetUrl = presignedUrl // console.log(presignedUrl) }) } }) }, //同步已上傳的文件到后端 syncAllUploadedFile(url, files, modelSourceInfoId){ let xhr = new XMLHttpRequest() xhr.onload = () => { if (xhr.status === 200 && xhr.status < 300 || xhr.status === 304) { try { const res = JSON.parse(xhr.responseText) if(res && res.code === 200){ this.$message({ message: '上傳完成', type: 'success' }) // setTimeout(() => { this.$emit('close') this.fileInfoList = [] this.fileList = [] this.httpQueue = [] // }, 1000) } } catch(error) { this.$message({ message: '上傳失敗,請(qǐng)檢查服務(wù)', type: 'error' }) } } } xhr.open("post", url, true) xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('X-Access-Token', getToken()) //將前面1.1獲取文件信息的id作為頭信息傳遞到后端 xhr.setRequestHeader('ThreeDimensionalModel-ServiceID', modelSourceInfoId) xhr.send(JSON.stringify(files)) }, //獲取文件類(lèi)型圖標(biāo)class getFileIcon(file) { const { type } = file let icon = '' return icon }, //創(chuàng)建上傳文件的http createUploadHttp(config){ const {file, filePutUrl, customHeader, onUploadProgress, onUploaded, onError} = config let fileName = file.name let http = new XMLHttpRequest(); http.upload.addEventListener("progress", (e) => { onUploadProgress({ progressEvent: e, uploadingFile: file }) }, false) http.onload = () => { if (http.status === 200 && http.status < 300 || http.status === 304) { try { const result = http.responseURL onUploaded({ result, uploadedFile: file}) } catch(error) { onError({ error, errorFile: file}) } } } http.open("PUT", filePutUrl, true); Object.keys(customHeader).forEach((key, index) =>{ http.setRequestHeader(key, customHeader[key]) }) http.send(file); return http } } } </script> <style scoped lang="scss"> .header-title { // height: 24px; border-bottom: 1px solid #EFEFEF; padding-bottom: 20px; //outline: 1px solid red; .icon { width: 26px; height: 26px; background-image: url(../images/icon_upload.png); float: left; background-size: 100% 100%; margin-right: 18px; margin-left: 10px; } span { font-size: 23px; font-family: Source Han Sans CN; font-weight: 500; color: #333333; } i { float: right; font-size: 16px; color: rgba(176, 176, 176, 1); cursor: pointer; &:hover { color: #0069D5 ; } } } .tip { font-size: 12px; display: block; } .file-list { box-sizing: border-box; padding: 5px; // border: 1px solid red; // width: 840px; margin: 5px auto; margin-left: 80px; .file-item { min-height: 32px; display: flex; justify-content: flex-start; align-items: center; font-size: 12px; div { margin-right: 15px; text-align: left; } .name { width: 200px; } .size { width: 60px; } .progress { width: 180px; height: 8px; border-radius: 4px; background-color: #E2E2E2; .bar { width: 50%; height: 8px; border-radius: 4px; background-color: #13A763; } } .rate { width: 60px; // border: 1px solid red; } .delete-btn { cursor: pointer; font-size: 16px; } } } .custom-footer { // border: 1px solid red; display: flex; justify-content: space-evenly; align-items: center; width: 100%; height: 80px; background-color: #fff; //box-shadow: 0px 1px 0px 0px red; border-top: 1px solid #efefef; z-index: 10; button { width: 90px; height: 40px; border-radius: 4px; border: 0; font-size: 16px; font-family: Source Han Sans CN; &:focus { border: 0; outline: 0; } &.info { color: #8f8f8f; background: #e2e2e2; &:hover{ background-color: #A6A9AD; cursor: pointer; color: #fff; } } &.success { background: #12a763; color: #fff; &:hover{ cursor: pointer; background-color: #73C132; } } } } </style>
源碼分享
其實(shí)一開(kāi)始我是想提供一個(gè)demo的,發(fā)現(xiàn)這個(gè)東西與產(chǎn)品功能強(qiáng)綁定,沒(méi)有測(cè)試的服務(wù)地址和存儲(chǔ)桶,也無(wú)法做出一個(gè)開(kāi)放的案例
所以上面的代碼只是給大家提供一種實(shí)現(xiàn)方式和思路,里面具體的細(xì)節(jié)處理我做的比較復(fù)雜
總結(jié)
到此這篇關(guān)于vue+Minio實(shí)現(xiàn)多文件進(jìn)度上傳的文章就介紹到這了,更多相關(guān)vue+Minio多文件進(jìn)度上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue3.x對(duì)echarts的二次封裝之按需加載過(guò)程詳解
echarts是我們后臺(tái)系統(tǒng)中最常用的數(shù)據(jù)統(tǒng)計(jì)圖形展示,外界對(duì)它的二次封裝也不計(jì)層數(shù),這篇文章主要介紹了vue3.x對(duì)echarts的二次封裝之按需加載,需要的朋友可以參考下2023-09-09Vue實(shí)現(xiàn)動(dòng)態(tài)路由導(dǎo)航的示例
本文主要介紹了Vue實(shí)現(xiàn)動(dòng)態(tài)路由導(dǎo)航的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02解決vue打包項(xiàng)目后刷新404的問(wèn)題
下面小編就為大家整理了一篇解決vue打包項(xiàng)目后刷新404的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03vue使用axios跨域請(qǐng)求數(shù)據(jù)問(wèn)題詳解
這篇文章主要為大家詳細(xì)介紹了vue使用axios跨域請(qǐng)求數(shù)據(jù)的問(wèn)題,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10VueX瀏覽器刷新如何實(shí)現(xiàn)保存數(shù)據(jù)
這篇文章主要介紹了VueX瀏覽器刷新如何實(shí)現(xiàn)保存數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07vue quill editor 使用富文本添加上傳音頻功能
vue-quill-editor 是vue項(xiàng)目中常用的富文本插件,其功能能滿(mǎn)足大部分的項(xiàng)目需求。這篇文章主要介紹了vue-quill-editor 富文本添加上傳音頻功能,需要的朋友可以參考下2020-01-01