需求: 在开发过程中,公司有购买阿里云、腾讯云等云存储服务且为了解决后端服务器的压力,可采用前端直接传输文件到对象存储服务器(怎么开通对象存储我这里就不赘述了,直接上代码~~~)
传输流程应该是:选取文件---> 向后台请求接口获取临时密钥---> 上传文件
1、在项目中安装对象存储的相关依赖
我这里用的是腾讯云对象存储
cnpm install cos-js-sdk-v5
2、在utils目录下创建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 文件上传过程中返回上传速度、进度以及文件名的方法,
* callback 文件上传成功后的回调方法,
* setBigFile 大文件上传初始化设置回调方法
*
* */
export function uploadObject (file, fileCallback, setBigFile, callback) {
/*
1.获取临时秘钥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)
// 获取当前时间戳 与文件类型拼接 为cos.putObject里的参数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 => { // 后台接口返回 密钥相关信息
const data = response.data
var credentials = data && data.credentials
if (!data || !credentials) return console.error('未获取到参数')
// 初始化
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('校验中', JSON.stringify(progressData))
// },
onProgress: (progressData) => {
const percent = parseInt(progressData.percent * 10000) / 100;
const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
// console.log('进度:' + 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) // 返回文件链接地址和视频的原始名称 上传完成后的回调
}
}
)
} 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('进度:' + 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) // 返回文件链接地址和视频的原始名称 上传完成后的回调
}
}
)
}
})
}
export default {
uploadObject
}
3、在components目录下创建上传文件的公共组件以及上传完成后显示文件列表的组件
文件上传组件
<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">点击上传</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: 这里是根据文件名来删除的,因为可能出现没有上传完的大文件临时删除,这时候没有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}已存在,请勿重复上传`,
offset: 300
})
}
},
// 文件上传更新进度和单文件上传初始化
fileProgressCallback(progress,speed,name){
/*
* progress 进度
* 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)
}
},
// 大文件上传需要记录文件所对应的taskId和cos,这里我是直接给进度条 uploadProgress 做了一个初始化
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图标随文件变化
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 预览,健壮性待完善
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 // 预留最小点击位置
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" />