vue +element-ui 实现的oss web直传文件
2021-07-12 本文已影响0人
好名字都让你们用了
简述: 后台系统基本的上传功能,oss 的web直传, 因oss 获取音视频时长需要收费,所以采用的前端本地获取上传音视频资源的时长,并配了上传成功音视频预览功能, oss 直传官网文档
1:oss上传流程,需要拿到后端返回的oss 签名信息
// 获取后端返回的签名信息
export function client(data) {
//data后端提供数据
return new OSS({
region: data.region,
accessKeyId: data.Credentials.AccessKeyId,
accessKeySecret: data.Credentials.AccessKeySecret,
stsToken: data.Credentials.SecurityToken,
bucket: data.bucket
})
}
2:主要上传组件ossUpload代码,用element 的自定义上传方法http-request ,
<template>
<div class="ossUpload">
<el-upload
ref="upload"
action
:class="{ has: hideUploadBtn, hidden: ModifyStyle }"
:http-request="Upload"
:before-upload="beforeAvatarUpload"
:on-preview="handlePreview"
:before-remove="beforeRemove"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:on-change="uploadChange"
:list-type="listType"
:limit="limit"
:file-list="fileList"
:accept="accept"
:on-error="uploadFail"
:on-success="uploadSuccess"
:multiple="multiple"
>
<template #trigger>
<i v-if="listType == 'picture-card'" class="el-icon-plus"></i>
<el-button v-else type="primary">{{ text }}</el-button>
</template>
<el-button
v-if="(type == 'audio' || type == 'video') && value != ''"
type="primary"
class="preview"
@click="preview"
>预览</el-button
>
</el-upload>
<!-- 上传进度条 -->
<el-progress
v-show="showProgress"
:text-inside="false"
:stroke-width="5"
:percentage="progress"
></el-progress>
<!-- 音频预览 -->
<el-dialog v-model="dialogAudioVisible" width="560px">
<div v-if="dialogAudioVisible" class="audio-con">
<audio-node :node="audio_info" image-height="250px"></audio-node>
</div>
</el-dialog>
<!-- 视频预览 -->
<el-dialog v-model="dialogVideoVisible" width="800px">
<div v-if="dialogVideoVisible" class="video-con">
<video class="videoPrview" controls controlsList="nodownload" preload="load">
<source :src="video_url" type="video/mp4" />
</video>
</div>
</el-dialog>
<!-- 视频获取时长 -->
<video id="myVideo" class="video" controls preload="load" :src="getVideoInfo"> </video>
<!-- 音频获取时长 -->
<audio id="myAudio" class="audio" preload="load" :src="getAudioInfo"> </audio>
</div>
</template>
<script>
import {
client,
isArrayFn,
formatTime,
formatMin,
randomString,
loadPreviewPlugin,
previewImage
} from '@/utils'
import AudioNode from '@/components/viewComponent/audio.vue'
import { createAjax } from '@/utils/ajax'
import { deleteParame } from '@/utils'
// import SparkMD5 from 'spark-md5'
const Ajax = createAjax('common', 'v1', 'restapi')
export default {
name: 'Upload',
components: {
AudioNode
},
props: {
limit: {
type: Number,
default: 1
},
text: {
type: String,
default: '选择图片'
},
listType: {
type: String,
default: 'picture-card'
},
// 上传文件大小
measure: {
type: String,
default: '500'
},
type: {
type: String,
default: 'image'
},
accept: {
type: String,
default: 'image/*'
},
value: {
type: [String, Object],
default: () => {}
},
oss: {
type: String,
default: 'oss'
},
address: {
type: String,
default: 'admin/images'
},
// 音视频时长
duration: {
type: [String, Number],
default: () => {}
},
name: {
type: [String, Number, Object],
default: () => ({})
},
unClick: {
type: Boolean,
default: false
},
// 上传是否为歌词
isLyc: {
type: Boolean,
default: false
},
// 业务名
business: {
type: String,
default: 'lesson'
},
// 图片尺寸
imageSize: {
type: [String, Object],
default: () => {}
},
modality: {
type: String,
default: 'form'
},
isShow: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
},
check: {
type: Boolean,
default: false
},
imageShow: {
type: Boolean,
default: true
}
},
emits: [
'update:value',
'update:size',
'update:duration',
'update:name',
'removeChange',
'uploadChange',
'update:num'
],
data() {
return {
// reviewShow: this.isShow,
urlClone: this.value,
videoName: this.name,
fileList: [], //文件列
showProgress: false, //进度条的显示
dataObj: {}, //存签名信息
progress: 0, //进度条数据
id: this.$route.params.id,
code: 0,
message: '',
fileCurrentLen: 0,
getVideoInfo: '',
getAudioInfo: '',
dialogAudioVisible: false,
dialogVideoVisible: false,
uploadInfo: {},
audio_info: {},
err: '',
isDelete: false,
video_url: '',
isSuccess: false
}
},
computed: {
isShowHasClass() {
// 多张不去掉上传按钮
if (this.multiple) return false
// 单张判断是否已生成url
if (!this.value) return false
return this.value.length || this.urlClone.length
},
hideUploadBtn() {
return this.fileCurrentLen >= this.limit && this.type === 'image' && this.imageShow
},
// 控制表格 表单 上传样式
ModifyStyle() {
return this.type === 'image' && this.modality == 'table'
}
},
watch: {
value(nVal) {
// 监听父组件清空url的时候清空文件
if (nVal) return
this.clearFiles()
}
},
beforeMount() {
loadPreviewPlugin()
this.initFieldList()
},
methods: {
preview() {
this.type == 'video' ? (this.dialogVideoVisible = true) : (this.dialogAudioVisible = true)
},
getImageList(fileList) {
return fileList.map((item) => {
if (item.status != 'success') return ''
if (item.response) {
// 新上传的图
return deleteParame(item.response)
} else {
// 以前上传的图
return deleteParame(item.url)
}
})
},
// 清空已上传的文件列表
clearFiles() {
this.$refs.upload.clearFiles()
},
// 文件超出个数限制时的钩子
handleExceed(files, fileList) {
this.$message.warning(`每次只能上传 ${this.limit} 个文件`)
},
// 点击文件列表中已上传的文件时的钩子
handlePreview(file) {
// 图片预览
if (this.unClick || this.type != 'image') return
previewImage(file.path || file.url)
return false
},
uploadChange(file, fileList) {
this.fileCurrentLen = fileList.length
this.$emit('uploadChange', file)
},
uploadFail(err) {
this.$message.error(`${JSON.parse(err.message).message},请重新上传`)
},
// 删除文件之前的钩子
beforeRemove(file, fileList) {
this.fileCurrentLen = fileList.length
// this.showProgress = false
const that = this
if (
(!this.isDelete && this.type == 'audio') ||
this.type == 'video' ||
this.type == 'text'
) {
async function abortMultipartUpload() {
const name = that.uploadInfo.name // Object所在Bucket的完整路径。
const uploadId = that.uploadInfo.uploadId // 分片上传uploadId。
client(that.dataObj)
.abortMultipartUpload(name, uploadId)
.then((result) => {
this.progress = 0
this.showProgress = false
})
}
abortMultipartUpload()
}
},
// 文件列表移除文件时的钩子
handleRemove(file, fileList) {
this.progress = 0
this.fileCurrentLen = fileList.length
let imagePathList = []
fileList.map((item, index) => {
if (item.uid == file.uid) {
fileList.splice(index, 1)
}
})
imagePathList = this.getImageList(fileList)
// 空数组 将imagePathList 转为为字符串
if (imagePathList.length == 0) {
imagePathList = ''
} else if (imagePathList.length == 1) {
imagePathList = imagePathList[0]
}
this.urlClone = []
this.$emit('update:value', imagePathList)
// 嵌套多层的情况手动更新数据
this.$emit('removeChange', imagePathList)
},
// 上传成功
uploadSuccess(response, file, fileList) {
if (this.type == 'image' && this.isSuccess) {
// 图片类
let imageList = this.getImageList(fileList)
let imagePath
if (this.limit == 1) {
// 单张直接返回图片地址
imagePath = imageList[0]
} else {
// 多张返回图片地址列表
imagePath = imageList
}
this.$emit('update:value', imagePath)
}
},
//文件上传前的校验
beforeAvatarUpload(file) {
// 获取文件的md5 ,
// var fileReader = new FileReader()
// var spark = new SparkMD5() // 创建md5对象(基于SparkMD5)
// fileReader.readAsBinaryString(file) // file 对应上传的文件
// // 文件读取完毕之后的处理
// fileReader.onload = (e) => {
// console.log('获取文件的md5')
// spark.appendBinary(e.target.result)
// const md5 = spark.end()
// console.log(md5)
// }
const that = this
// 校验文件类型
if (this.type && !file.type.startsWith(this.type) && !this.isLyc) {
this.$message.error('格式不正确,请重新上传!')
return false
}
if (this.type == 'audio') {
this.showProgress = true
//音频类型限制上传大小
if (Number((file.size / 1024 / 1024).toFixed(3)) > this.measure) {
this.$message.error(`音频不能超过${this.measure}M`)
return false
}
this.getAudioInfo = URL.createObjectURL(file)
document.getElementById('myAudio').addEventListener('canplaythrough', function (e) {
const audioTime = formatMin(e.target.duration)
that.$emit('update:duration', audioTime)
})
}
if (this.type == 'image') {
if (Number((file.size / 1024 / 1024).toFixed(3)) > this.measure) {
that.$message.error(`${file.name}大小不对,请重新上传!`)
return false
}
}
// 上传本地获取视频时长
if (this.type == 'video') {
this.showProgress = true
// 视频类限制上传大小
if (Number((file.size / 1024 / 1024).toFixed(3)) > this.measure) {
this.$message.error(`视频不能超过${this.measure}M`)
return false
}
this.getVideoInfo = URL.createObjectURL(file)
document.getElementById('myVideo').addEventListener('canplaythrough', function (e) {
const time = formatTime(e.target.duration)
that.$emit('update:duration', time)
})
}
// 歌词 格式校验
if (this.isLyc) {
this.showProgress = true
if (file.name.split('.')[1].toLowerCase() != 'lrc') {
this.$message.error('请上传格式正确的歌词文件')
return false
}
}
if (this.code) {
this.$message.error(this.message)
return false
}
if (!that.imageSize) {
return true
}
return new Promise(function (resolve, reject) {
let reader = new FileReader()
let size = JSON.parse(that.imageSize)
reader.readAsDataURL(file)
reader.onload = function (theFile) {
let image = new Image()
image.src = theFile.target.result
image.onload = function () {
if (size.width && size.height) {
const noSizeLimit = !this.height || !this.width
const rightSize = size.width == this.width && size.height == this.height
if (noSizeLimit || rightSize) {
file.width = size.width
file.height = size.height
resolve(file)
return
} else {
that.$message.error(`${file.name}尺寸不对,请重新上传!`)
reject('图片尺寸不对')
return
}
}
if (!size.width || !size.height) {
if (size.width) {
if (size.width == this.width) {
file.width = size.width
resolve(file)
} else {
that.$message.error(`${file.name}尺寸不对,请重新上传!`)
reject('图片尺寸不对')
}
} else {
if (size.height == this.height) {
file.height = size.height
resolve(file)
} else {
that.$message.error(`${file.name}尺寸不对,请重新上传!`)
reject('图片尺寸不对')
}
}
}
}
}
})
},
// http-request属性来覆盖默认的上传行为,自定义上传的实现
async Upload(file) {
await Ajax.get('/aliyun/oss', {
params: {
oss: this.oss
}
})
.then((res) => {
if (res.code == 500) {
;(this.code = 1), this.$message.error(res.message)
return false
}
this.dataObj = res
})
.catch((err) => {
this.$message.error(err.message)
})
let fileName = '.' + file.file.name.substring(file.file.name.lastIndexOf('.') + 1)
const that = this
that.$emit('update:size', (file.file.size / 1024 / 1024).toFixed(2))
async function multipartUpload() {
//增加时间戳和随机数 防止文件覆盖
let temporary = new Date().getTime() + randomString(6) + fileName
let date = new Date()
let year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
let fileAddress =
that.type == 'audio'
? 'admin/audio'
: that.type == 'video'
? 'admin/video'
: that.type == 'image'
? 'admin/images'
: 'admin/lyrics'
client(that.dataObj)
.multipartUpload(
`${fileAddress}/${that.business}/${year}${month}${day}/${temporary}`,
file.file,
{
progress: function (p, _checkpoint) {
Object.assign(that.uploadInfo, _checkpoint)
if (that.type != 'image') {
that.showProgress = true
that.progress = Math.floor(p * 100)
}
}
}
)
.then((result) => {
let url = `${that.dataObj.domain}/${fileAddress}/${that.business}/${year}${month}${day}/${temporary}`
if (result) {
that.isSuccess = true
//回调,可以使用element 的上传成功钩子函数
file.onSuccess(url)
}
// 上传成功 赋值 音视频预览
that.type == 'video' ? (that.video_url = url) : (that.audio_info.resource_url = url)
// 上传成功 隐藏进度条
if (Number(that.progress) == 100) that.showProgress = false
that.isDelete = true
if (!that.multiple) that.$emit('update:value', url)
that.$emit('update:name', temporary)
})
.catch((err) => {
that.showProgress = false
// 捕获超时异常。
if (err.code === 'ConnectionTimeoutError') {
that.$message.error('TimeoutError')
}
})
}
multipartUpload()
},
initFieldList() {
this.urlClone = isArrayFn(this.value) || !this.value ? this.value : [this.value]
// 初始化
if (this.urlClone && this.urlClone.length) {
this.fileList = this.urlClone.map((item) => {
// 初始化预览音视频赋值
this.type == 'video'
? (this.video_url = deleteParame(item))
: (this.audio_info.resource_url = deleteParame(item))
// 返回音视频地址显示处理
let name = ''
const audioPathArr = deleteParame(item).split('/')
name = audioPathArr[audioPathArr.length - 1]
return {
name:
this.type == 'audio' || this.type == 'video' || this.isLyc
? name || item.name || item.audio_name || item
: item.name || item.audio_name || item,
url: item
}
})
this.fileCurrentLen = this.fileList.length
}
},
// getFieldName(item) {
// if (this.videoName && Object.getOwnPropertyNames(this.videoName).length !== 0) {
// return this.videoName
// }
// if (this.name && Object.getOwnPropertyNames(this.name).length !== 0) {
// return this.name
// }
// if (this.audio_name && Object.getOwnPropertyNames(this.audio_name).length !== 0) {
// return this.audio_name
// }
// return item
// },
beforeDestroy() {
document.getElementById('myVideo').addEventListener('canplaythrough', true),
document.getElementById('myAudio').addEventListener('canplaythrough', true)
// 组件销毁 释放对象url
URL.revokeObjectURL(this.getVideoInfo)
URL.revokeObjectURL(this.getAudioInfo)
}
}
}
</script>
<style lang="less" scoped>
.ossUpload {
/deep/ .el-upload-list__item {
transition: none !important;
}
.el-progress {
width: 55%;
}
.el-progress-bar {
width: 55%;
}
.el-upload-list__item {
width: 30% !important;
}
.video {
display: none;
}
.upload {
line-height: 1.4;
}
.el-upload-list .focusing {
border: 1px solid #c0ccda;
outline: none;
}
.has {
/deep/ .el-upload--picture-card:last-child {
display: none;
}
:global(.el-upload--) {
display: none;
}
}
.unshow {
:global(.el-upload-list--) {
display: none;
}
:global(.el-upload-list--picture) {
display: none;
}
}
.audio {
display: none;
}
.preview {
margin-left: 10px;
}
.video-con {
display: flex;
align-items: center;
justify-content: center;
}
.videoPrview {
width: 100%;
height: auto;
max-height: 60vh;
}
.hidden {
/deep/ .el-upload-list--picture-card .el-upload-list__item {
height: 100px;
width: 100px;
}
/deep/ .el-upload-list--picture-card .el-upload-list__item-actions {
font-size: 14px;
}
/deep/.el-upload--picture-card {
height: 100px;
width: 100px;
line-height: 100px;
}
}
}
</style>
3:防止不同文件相同名字上传oss 覆盖, 对文件名处理拼接(看公司需求要求)
// 随机字符串
export function randomString(len) {
len = len || 32
const $chars =
'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
const maxPos = $chars.length
let pwd = ''
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
}
return pwd
}