需求: 在開發(fā)過程中,公司有購買阿里云、騰訊云等云存儲服務(wù)且為了解決后端服務(wù)器的壓力,可采用前端直接傳輸文件到對象存儲服務(wù)器(怎么開通對象存儲我這里就不贅述了,直接上代碼~~~)
傳輸流程應(yīng)該是:選取文件---> 向后臺請求接口獲取臨時(shí)密鑰---> 上傳文件
1、在項(xiàng)目中安裝對象存儲的相關(guān)依賴
我這里用的是騰訊云對象存儲
cnpm install cos-js-sdk-v5
2、在utils目錄下創(chuàng)建upload.js
import COS from 'cos-js-sdk-v5'
import { Message } from 'element-ui'
import voteApi from "@/api/vote";
const config = {
Bucket: 'xxx',
Region: 'xxx'
}
// 上傳到騰訊云cos
/*
* file 選取的文件,
* fileCallback 文件上傳過程中返回上傳速度、進(jìn)度以及文件名的方法,
* callback 文件上傳成功后的回調(diào)方法,
* setBigFile 大文件上傳初始化設(shè)置回調(diào)方法
*
* */
export function uploadObject (file, fileCallback, setBigFile, callback) {
/*
1.獲取臨時(shí)秘鑰data
2.初始化
3.判斷上傳文件的類型
4.判斷文件大小 是否需要分片上傳
*/
let fileName = file.name || ""
const origin_file_name = fileName.split(".").slice(0, fileName.split(".").length - 1).join('.') // 獲取文件名稱
// console.log('origin_file_name', origin_file_name)
// 獲取當(dāng)前時(shí)間戳 與文件類型拼接 為cos.putObject里的參數(shù)Key
const upload_file_name = new Date().getTime() + '.' + fileName.split(".")[fileName.split(".").length - 1];
let userId = file.userId
let date = new Date()
let year = date.getFullYear()
let month = date.getMonth() + 1
let strDate = date.getDate()
let uploadDay = `${year}${month}${strDate}`
// 獲取密鑰
voteApi.fileUploadGetAuth({filePath: `/zhjyone/${userId}`}).then(response => { // 后臺接口返回 密鑰相關(guān)信息
const data = response.data
var credentials = data && data.credentials
if (!data || !credentials) return console.error('未獲取到參數(shù)')
// 初始化
var cos = new COS({
getAuthorization: (options, callback) => {
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
XCosSecurityToken: credentials.sessionToken,
StartTime: data.startTime,
ExpiredTime: data.expiredTime,
expiration: data.expiration,
requestId: data.requestId,
})
},
})
// 獲取上傳文件大小
let size = file.size
let key = `/zhjyone/${uploadDay}/${userId}/${upload_file_name}`
// console.log('size', size)
// console.log(size / (1024 * 2024))
if (size / (1024 * 1024) < 5) { // 文件小于5M走普通上傳
console.log('文件普通上傳')
cos.putObject(
{
Bucket: config.Bucket, // 存儲桶名稱
Region: config.Region, // 存儲桶所在地域,必須字段
Key: key, // 文件名稱
StorageClass: 'STANDARD',
Body: file, // 上傳文件對象
// onHashProgress: (progressData) => {
// console.log('校驗(yàn)中', JSON.stringify(progressData))
// },
onProgress: (progressData) => {
const percent = parseInt(progressData.percent * 10000) / 100;
const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
// console.log('進(jìn)度:' + percent + '%; 速度:' + speed + 'Mb/s;');
fileCallback(percent,speed,origin_file_name)
},
},
(err, data) => {
if (err) {
console.log('err', err)
Message({ message: '文件上傳失敗,請重新上傳', type: 'error' })
let fileUrl = null
callback(fileUrl, origin_file_name)
} else {
let fileUrl = 'https://' + data.Location
callback(fileUrl, origin_file_name) // 返回文件鏈接地址和視頻的原始名稱 上傳完成后的回調(diào)
}
}
)
} else {
console.log('文件分塊上傳')
// 上傳分塊
cos.sliceUploadFile(
{
Bucket: config.Bucket, // 存儲桶名稱
Region: config.Region, // 存儲桶所在地域,必須字段
Key: key /* 必須 */,
Body: file,
onTaskReady: (taskId) => {
/* 非必須 */
setBigFile && setBigFile(cos, taskId, origin_file_name)
},
// onHashProgress: (progressData) => {
// /* 非必須 */
// // console.log(JSON.stringify(progressData))
// },
onProgress: function (progressData) {
const percent = parseInt(progressData.percent * 10000) / 100;
const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
// console.log('進(jìn)度:' + percent + '%; 速度:' + speed + 'Mb/s;');
fileCallback(percent, speed, origin_file_name)
},
},
(err, data) => {
if (err) {
// console.log(err)
Message({ message: '文件上傳失敗,請重新上傳', type: 'error' })
let fileUrl = null
callback(fileUrl, origin_file_name)
} else {
let fileUrl = 'https://' + data.Location
callback(fileUrl, origin_file_name) // 返回文件鏈接地址和視頻的原始名稱 上傳完成后的回調(diào)
}
}
)
}
})
}
export default {
uploadObject
}
3、在components目錄下創(chuàng)建上傳文件的公共組件以及上傳完成后顯示文件列表的組件
文件上傳組件
<template>
<div class="upload">
<div class="file-upload">
<el-upload
ref="upload"
:userId = "userId"
:disabled="disabled"
:accept="accept"
action="#"
:show-file-list="false"
:http-request="uploadToCos"
:before-upload="beforeImageUpload"
:on-change="onChangeHandle">
<el-button v-if="value.length < limit" :disabled="disabled" size="small" type="primary">點(diǎn)擊上傳</el-button>
</el-upload>
<div class="file-list">
<file-item
v-for="file in value"
:key="file.id || file.fileUrl"
:file="file"
:disableDel="disabled"
showDownBtn
@remove="removeFile(file)"
/>
</div>
</div>
</div>
</template>
<script>
import { uploadObject } from '@/utils/uploadObject'
import { Message } from 'element-ui'
import FileItem from './FileItem'
export default {
name: 'MyUploadPlus',
components: { FileItem },
data () {
return {
userId: this.$route.query.userId,
imgWidth: 0,
imgHeight: 0,
picIndex: -1,
dialogImageUrl: '',
dialogVisibleShow: false,
fileList: [],
isUpload: true,
fileName: ''
}
},
props: {
disabled: {
type: Boolean,
default: () => false
},
value: {
type: Array,
default: () => []
},
accept: {
type: String,
default: ''
},
limit: {
type: Number,
default : 100
}
},
created () {
},
methods: {
removeFile (file) {
if (file) {
// remark: 這里是根據(jù)文件名來刪除的,因?yàn)榭赡艹霈F(xiàn)沒有上傳完的大文件臨時(shí)刪除,這時(shí)候沒有url就會造成刪不掉的情況
const fileIndex = this.value.findIndex(i => i.name === file.name)
// remark: 這里是終止上傳大文件的操作
if(file.taskId){
let taskId = this.value[fileIndex].taskId
file.cos.cancelTask(taskId)
}
this.value.splice(fileIndex, 1)
this.$emit('input', this.value)
// console.log(this.value)
file.cancel && file.cancel()
} else {
this.value.length = 0
this.$emit('input', this.value)
}
},
onChangeHandle (file, fileList) {
this.fileList = [file]
console.log('onChangeHandle file, fileList', fileList);
this.$refs.upload.$refs['upload-inner'].handleClick()
},
beforeImageUpload (file) {
let fileName = this.getFileName(file.name)
let idx = this.value.findIndex(e => e.name === fileName);
if(idx != -1){
this.fileName = this.value[idx].name;
}
this.isUpload = idx === -1 ? true : false;
file.userId = this.userId;
},
// 獲取選取文件的文件名
getFileName (name) {
return name.substring(0, name.lastIndexOf("."))
},
// 上傳文件
uploadToCos () {
if(this.isUpload){
uploadObject(this.fileList[0].raw, this.fileProgressCallback, this.setBigFile, (url, fileName) => {
// console.log('files', this.fileList[0].raw)
let index = this.value.findIndex(e => e.name === fileName)
if(url){
this.value[index].url = url
this.$emit('input', this.value)
Message.success({
message:`${fileName}已上傳完成`,
offset: 300
})
}else{
this.value.splice(index, 1); // 文件上傳失敗刪除fileitem列表顯示文件
}
})
}else{
Message.error({
message:`${this.fileName}已存在,請勿重復(fù)上傳`,
offset: 300
})
}
},
// 文件上傳更新進(jìn)度和單文件上傳初始化
fileProgressCallback(progress,speed,name){
/*
* progress 進(jìn)度
* speed 傳輸速度
* name 文件名稱
* */
console.log('speed=====>',speed)
let file = {
name: name,
uploadProgress: progress
}
if(this.value && this.value.length > 0){
let index = this.value.findIndex(e => e.name === name)
if(index >= 0){
this.value[index].uploadProgress = progress
} else {
this.value.push(file)
}
} else {
this.value.push(file)
}
},
// 大文件上傳需要記錄文件所對應(yīng)的taskId和cos,這里我是直接給進(jìn)度條 uploadProgress 做了一個(gè)初始化
setBigFile(cos, taskId, fileName){
let file = {
name: fileName,
taskId: taskId,
uploadProgress: 0,
cos: cos
}
let index = this.value.findIndex(e => e.name === fileName)
if(index >= 0){
this.value[index].taskId = taskId
this.value[index].cos = cos
this.value[index].uploadProgress = 0
} else {
this.value.push(file)
}
}
}
}
</script>
<style lang='less'>
@small-size: 80px;
.file-upload{
max-width: 600px;
}
.file-list{
padding-top: 20px;
height: auto;
}
</style>
文件列表組件
<template>
<div class="file-item">
<div class="item-icon-wrap">
<yc-svg-icon
class="item-icon"
name="video"
v-if="disablePreview"
></yc-svg-icon>
<i v-else class="el-icon-document-remove el-icon"></i>
</div>
<div class="item-message-wrap">
<div class="item-message">
<div class="message-name" @click="preview">{{ file.name }}</div>
</div>
<el-progress
class="item-progress-bar"
:percentage="parseInt(file.uploadProgress)"
:show-text="true"
></el-progress>
</div>
<div v-if="showDownBtn" class="item-del" @click="preview">
<i class="el-icon-download el-icon"></i>
</div>
<div v-if="!disableDel" class="item-del" @click="remove">
<i class="el-icon-delete el-icon"></i>
</div>
</div>
</template>
<script>
// todo : icon圖標(biāo)隨文件變化
export default {
props: {
file: {
type: Object,
required: true
},
disablePreview: {
type: Boolean,
default: false
},
disableDel: {
type: Boolean,
default: false
},
showDownBtn: {
type: Boolean,
default: false
}
},
created(){
// console.log('fileItem.file', this.file)
},
methods: {
remove () {
this.$emit('remove', this.file)
},
preview () {
// todo 預(yù)覽,健壯性待完善
if (!this.disablePreview) {
window.open(this.file.url)
}
}
}
}
</script>
<style lang="less" scoped>
.file-item{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-flow: row;
flex-flow: row;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
height: 50px;
margin-bottom: 10px;
background: white;
border-radius: 5px;
border: 1px solid #eee;
.el-icon{
font-size: 20px;
}
&:last-child{
margin-bottom: 0;
}
.item-icon-wrap{
flex: none;
flex-shrink: 0;
width: 50px;
text-align: center;
border-right: 1px solid #f3f3f3;
}
.item-message-wrap{
flex: 1;
padding: 0 15px;
line-height: 1em;
margin-top: 9px;
.item-message{
//@include clearfix
.message-name {
display: inline-block;
text-align: left;
//width: 180px
//padding-right: 20px // 預(yù)留最小點(diǎn)擊位置
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
}
//@include text-overflow
.message-progress-text{
float: right;
color: #409eff;
}
.item-progress-bar{
margin-top: 5px;
}
}
.item-del{
flex: none;
width: 50px;
text-align: center;
cursor: pointer;
border-left: 1px solid #f3f3f3;
}
}
</style>
4、頁面使用
<MyUploadPlus v-model="form.videoUrls" :userId="userId" :accept="isVideo" :disabled="isDetail" />