簡(jiǎn)述: 后臺(tái)系統(tǒng)基本的上傳功能,oss 的web直傳, 因oss 獲取音視頻時(shí)長(zhǎng)需要收費(fèi),所以采用的前端本地獲取上傳音視頻資源的時(shí)長(zhǎng),并配了上傳成功音視頻預(yù)覽功能, oss 直傳官網(wǎng)文檔
1:oss上傳流程,需要拿到后端返回的oss 簽名信息
// 獲取后端返回的簽名信息
export function client(data) {
//data后端提供數(shù)據(jù)
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"
>預(yù)覽</el-button
>
</el-upload>
<!-- 上傳進(jìn)度條 -->
<el-progress
v-show="showProgress"
:text-inside="false"
:stroke-width="5"
:percentage="progress"
></el-progress>
<!-- 音頻預(yù)覽 -->
<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>
<!-- 視頻預(yù)覽 -->
<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>
<!-- 視頻獲取時(shí)長(zhǎng) -->
<video id="myVideo" class="video" controls preload="load" :src="getVideoInfo"> </video>
<!-- 音頻獲取時(shí)長(zhǎng) -->
<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'
},
// 音視頻時(shí)長(zhǎng)
duration: {
type: [String, Number],
default: () => {}
},
name: {
type: [String, Number, Object],
default: () => ({})
},
unClick: {
type: Boolean,
default: false
},
// 上傳是否為歌詞
isLyc: {
type: Boolean,
default: false
},
// 業(yè)務(wù)名
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, //進(jìn)度條的顯示
dataObj: {}, //存簽名信息
progress: 0, //進(jìn)度條數(shù)據(jù)
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) {
// 監(jiān)聽父組件清空url的時(shí)候清空文件
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()
},
// 文件超出個(gè)數(shù)限制時(shí)的鉤子
handleExceed(files, fileList) {
this.$message.warning(`每次只能上傳 ${this.limit} 個(gè)文件`)
},
// 點(diǎn)擊文件列表中已上傳的文件時(shí)的鉤子
handlePreview(file) {
// 圖片預(yù)覽
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},請(qǐng)重新上傳`)
},
// 刪除文件之前的鉤子
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()
}
},
// 文件列表移除文件時(shí)的鉤子
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)
// 空數(shù)組 將imagePathList 轉(zhuǎn)為為字符串
if (imagePathList.length == 0) {
imagePathList = ''
} else if (imagePathList.length == 1) {
imagePathList = imagePathList[0]
}
this.urlClone = []
this.$emit('update:value', imagePathList)
// 嵌套多層的情況手動(dòng)更新數(shù)據(jù)
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)
}
},
//文件上傳前的校驗(yàn)
beforeAvatarUpload(file) {
// 獲取文件的md5 ,
// var fileReader = new FileReader()
// var spark = new SparkMD5() // 創(chuàng)建md5對(duì)象(基于SparkMD5)
// fileReader.readAsBinaryString(file) // file 對(duì)應(yīng)上傳的文件
// // 文件讀取完畢之后的處理
// fileReader.onload = (e) => {
// console.log('獲取文件的md5')
// spark.appendBinary(e.target.result)
// const md5 = spark.end()
// console.log(md5)
// }
const that = this
// 校驗(yàn)文件類型
if (this.type && !file.type.startsWith(this.type) && !this.isLyc) {
this.$message.error('格式不正確,請(qǐng)重新上傳!')
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}大小不對(duì),請(qǐng)重新上傳!`)
return false
}
}
// 上傳本地獲取視頻時(shí)長(zhǎng)
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)
})
}
// 歌詞 格式校驗(yàn)
if (this.isLyc) {
this.showProgress = true
if (file.name.split('.')[1].toLowerCase() != 'lrc') {
this.$message.error('請(qǐng)上傳格式正確的歌詞文件')
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}尺寸不對(duì),請(qǐng)重新上傳!`)
reject('圖片尺寸不對(duì)')
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}尺寸不對(duì),請(qǐng)重新上傳!`)
reject('圖片尺寸不對(duì)')
}
} else {
if (size.height == this.height) {
file.height = size.height
resolve(file)
} else {
that.$message.error(`${file.name}尺寸不對(duì),請(qǐng)重新上傳!`)
reject('圖片尺寸不對(duì)')
}
}
}
}
}
})
},
// http-request屬性來覆蓋默認(rèn)的上傳行為,自定義上傳的實(shí)現(xiàn)
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() {
//增加時(shí)間戳和隨機(jī)數(shù) 防止文件覆蓋
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
//回調(diào),可以使用element 的上傳成功鉤子函數(shù)
file.onSuccess(url)
}
// 上傳成功 賦值 音視頻預(yù)覽
that.type == 'video' ? (that.video_url = url) : (that.audio_info.resource_url = url)
// 上傳成功 隱藏進(jìn)度條
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
// 捕獲超時(shí)異常。
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) => {
// 初始化預(yù)覽音視頻賦值
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)
// 組件銷毀 釋放對(duì)象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 覆蓋, 對(duì)文件名處理拼接(看公司需求要求)
// 隨機(jī)字符串
export function randomString(len) {
len = len || 32
const $chars =
'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默認(rèn)去掉了容易混淆的字符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
}