upload上传是前端开发很常用的一个功能,在Vue开发中常用的Element组件库也提供了非常好用的upload组件。本文采用scoped-slot
去设置缩略图模版,并且实现了oss粘贴上传功能。
一、html代码
使用插槽scoped-slot获取当前上传的状态file。通过file.status的状态判断是否上传成功。file.status为uploading代表正在上传中,为success代表上传成功。上传中hover到文件显示上传的文件大小、进度、速度。
<template>
<div class="base-upload">
<el-upload
:id="id"
ref="baseUpload"
:action="OSS_URL"
list-type="picture-card"
:class="{ 'picture-card--disabled': disabled }"
:disabled="disabled"
drag
multiple
:file-list="myValue"
:accept="accept"
:headers="headers"
:limit="limit"
:data="tokens"
:before-upload="beforeUpload"
:on-progress="handleProgress"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<i slot="default" class="el-icon-plus"></i>
<div slot="file" slot-scope="{ file }">
<!-- 提示上传进度 -->
<el-popover ref="popover" placement="right" :title="file.status === 'uploading' ? '上传中,请耐心等待' : '上传成功'" width="200" trigger="hover">
<ul v-if="file.status === 'uploading'">
<li class="target-size" :class="{ 'target-size--success': file.status === 'success' }">文件大小:{{ showSize(file.size) }}</li>
<li class="target-size" :class="{ 'target-size--success': file.status === 'success' }">已上传:{{ showSize(file.size * (file.percentage / 100)) }}</li>
<li class="target-size" :class="{ 'target-size--success': file.status === 'success' }">上传速度:{{ showSize((file.size * (file.percentage / 100)) / file.speed) }}/s</li>
</ul>
</el-popover>
<!-- 上传主题 -->
<el-image v-if="file.status === 'success' && file.url" class="el-upload-list__item-thumbnail" :src="getThumbnailUrl(file)" fit="fit" />
<el-progress v-if="file.status === 'uploading'" type="circle" :percentage="parseInt(file.percentage)" />
<span v-popover:popover class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<i class="el-icon-zoom-in"></i>
</span>
<span v-if="download" class="el-upload-list__item-download" @click="handleDownload(file)">
<i class="el-icon-download"></i>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
<i class="el-icon-delete"></i>
</span>
</span>
</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible" :lock-scroll="false" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</div>
</template>
二、js代码
1.首先声明data参数
data() {
return {
thumbnailMaxSize: 1048576 * 20, // 缩略图(最大20M)
id: `baseUpload_${new Date().getTime()}`,
uploadStartTimeList: [], // 存储开始上传的事件
OSS_URL: defaultSettings.OSS_URL, // oss上传地址
myValue: this.value,
tokens: {},
dialogImageUrl: '',
dialogVisible: false,
headers: {
Authorization: getToken()
}
}
},
2.计算属性设置,这里有一个需要注意的点,oss缩略图限制图片最大为20M。如果上传图片过大,可以直接显示图片,另外判断大于20M显示原图片。
computed: {
// 缩略图地址
getThumbnailUrl(file) {
return (file) => {
return validURL(file.url) && file.size < this.thumbnailMaxSize ? file.url + `?x-oss-process=image/resize,w_${this.width}` : file.url
}
}
},
3.文件上传前钩子。前面说到的oss缩略图最大20M,在这里进行判断。之后通过后台接口设置签名,钩子返回Promise reject(false)则回调结束,
generateTokens(key, file) {
return new Promise((resolve, reject) => {
getOssLiuParamsApi().then((res) => {
file.startTime = new Date().getTime()
this.uploadStartTimeList.push(file)
this.tokens.key = res.dir + key
this.tokens.policy = res.policy
this.tokens.OSSAccessKeyId = res.accessid
this.tokens.success_action_status = 200
this.tokens.callback = res.callback
this.tokens.signature = res.signature
resolve(true)
})
}, () => {
// eslint-disable-next-line no-undef
reject(false)
})
},
// 上传文件之前的钩子 {Object} file 文件信息
beforeUpload(file) {
const isBeyond = file.size < this.maxSize
if (!isBeyond) {
const beyondTip = '文件大小超过' + showSize(this.maxSize) + ',请重新上传。'
this.$message.error(beyondTip)
return isBeyond
} else {
const arr = file.name.split('.')
const un_name = `${file.uid}.${arr[arr.length - 1]}`
return this.generateTokens(un_name, file)
}
},
4.文件上传时钩子。
handleProgress(event, file, fileList) {
this.$store.dispatch('tool/toggleBeUploading', true)
const currFile = this.uploadStartTimeList.filter(v => v.uid === file.uid)
if (currFile && currFile[0]) {
file.speed = (new Date().getTime() - currFile[0].startTime) / 1000
}
this.myValue = fileList
},
5.文件上传成功钩子。设置图片路径,为避免前面设置上传进度的时候数据错乱,这里重新遍历设置,有其他更好的方法可以自行修改。
handleUploadSuccess(res, file, fileList) {
fileList.forEach(fileListItem => {
const obj = this.setImg(file)
const index = this.myValue.findIndex(item => item.uid === file.uid)
this.myValue.splice(index, 1, obj)
})
console.log(this.myValue, 'myValue')
this.$store.dispatch('tool/toggleBeUploading', false)
this.$message({ type: 'success', message: '上传成功' })
},
6.最后来说说截屏上传
原理:借助QQ、微信等截屏工具截屏,同时监听浏览器粘贴和鼠标移动事件, ctrl+v调用后台接口进行上传并且同步到upload组件
核心代码
mixins: [pasteUploadMixin],
onMousemove(e) {
const el = e.target
const elWrappend = el.closest(`div#${this.id}`)
const elCurrent = elWrappend.parentNode.parentNode.parentNode.firstElementChild
if (elCurrent.getAttributeNode('for') && elCurrent.getAttributeNode('for').value) {
this.pasteUploadKey = elCurrent.getAttributeNode('for').value
} else {
this.pasteUploadKey = ''
}
},
// 初始化粘贴
initPaste() {
var handleAction = e => {
var cbd = e.clipboardData
var ua = window.navigator.userAgent
// 如果是 Safari 直接 return
if (!(e.clipboardData && e.clipboardData.items)) {
return
}
// Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
if (cbd.items && cbd.items.length === 2 && cbd.items[0].kind === 'string' && cbd.items[1].kind === 'file' && cbd.types && cbd.types.length === 2 && cbd.types[0] === 'text/plain' && cbd.types[1] === 'Files' && ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49) {
return
}
for (var i = 0; i < cbd.items.length; i++) {
var item = cbd.items[i]
if (item.kind === 'file') {
var blob = item.getAsFile()
if (blob.size === 0) {
return
}
var reader = new FileReader()
reader.readAsDataURL(blob)
reader.onload = readerE => {
this.submitUpload(readerE.target.result)
}
}
}
}
this.newHandle = event => {
handleAction(event)
}
document.getElementById(this.id).removeEventListener('paste', this.newHandle, false)
document.getElementById(this.id).addEventListener('paste', this.newHandle, false)
},
三、调用
<template>
<base-upload v-model="value" accept="image/*" />
</template>
<script>
import BaseUpload from '@/components/BaseUpload'
export default {
components: { BaseUpload },
data() {
return {
value: ''
}
},
methods: {
}
}
</script>
完整代码
代码存放目录
BaseUpload
mixin
paste-upload.js
index.vue
对应的JS方法
import { uploadingPicture } from '@/utils/dict'
// 上传默认图片
export const uploadingPicture = {
zip: 'http://xxxx.aliyuncs.com/1546836641143_4273.png',
xls: 'http://xxxx.aliyuncs.com/1550486132435_7782.png',
doc: 'http://xxxx.aliyuncs.com/1550486135954_2036.png',
txt: 'http://xxxx.aliyuncs.com/1550486134063_8220.png',
mp4: 'http://xxxx.aliyuncs.com/1550653591085_6150.png',
other: 'http://xxxx.aliyuncs.com/1550653593890_4662.png'
}
import { showSize } from '@/utils'
/**
* @description 将字节转化为MB GB T
* @param {Number} size 字节
* @return {string} data 转化后的大小
*/
export const showSize = size => {
let data = ''
if (size < 1048576) {
data = Number((size / 1024).toFixed(2)) + 'KB'
} else if (size === 1048576) {
data = '1MB'
} else if (size > 1048576 && size < 1073741824) {
data = Number((size / (1024 * 1024)).toFixed(2)) + 'MB'
} else if (size > 1048576 && size === 1073741824) {
data = '1GB'
} else if (size > 1073741824 && size < 1099511627776) {
data = Number((size / (1024 * 1024 * 1024)).toFixed(2)) + 'GB'
} else {
data = '文件超过1TB'
}
return data
}
import { validURL } from '@/utils/validate'
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return reg.test(url)
}
import { getToken } from '@/utils/auth'
export function getToken() {
return Cookies.get(TokenKey)
}
import { pasteUploadMixin } from './mixin/paste-upload'
import { getOssLiuParamsApi } from '@/api/utils'
// 前端直传参数接口
export function getOssLiuParamsApi(data) {
return request({
url: 'order/ossLiu',
method: 'post',
data
})
}
import defaultSettings from '@/settings'
module.exports = {
OSS_URL: 'https://xxxx/', // oss下载地址(测试)
BASE_API: 'https://xxxx/' // 测试环境
}
/**
* @param {Array} str
* @returns {Boolean}
*/
export function isEmpty(str) {
if (str === '' || str === null || str === undefined || str.length === 0) {
return true
}
return false
}
// 上传粘贴的图片到oss
export function uploadingUpFile(data) {
return request({
url: 'order/screenshotImg',
method: 'post',
data
})
}
<!-- index.vue -->
<template>
<div class="base-upload" @mousemove="onMousemove">
<el-upload
:id="id"
ref="baseUpload"
:action="OSS_URL"
list-type="picture-card"
:class="{ 'picture-card--disabled': disabled }"
:disabled="disabled"
drag
multiple
:file-list="myValue"
:accept="accept"
:headers="headers"
:limit="limit"
:data="tokens"
:before-upload="beforeUpload"
:on-progress="handleProgress"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<i slot="default" class="el-icon-plus"></i>
<div slot="file" slot-scope="{ file }">
<!-- 提示上传进度 -->
<el-popover ref="popover" placement="right" :title="file.status === 'uploading' ? '上传中,请耐心等待' : '上传成功'" width="200" trigger="hover">
<ul v-if="file.status === 'uploading'">
<li class="target-size" :class="{ 'target-size--success': file.status === 'success' }">文件大小:{{ showSize(file.size) }}</li>
<li class="target-size" :class="{ 'target-size--success': file.status === 'success' }">已上传:{{ showSize(file.size * (file.percentage / 100)) }}</li>
<li class="target-size" :class="{ 'target-size--success': file.status === 'success' }">上传速度:{{ showSize((file.size * (file.percentage / 100)) / file.speed) }}/s</li>
</ul>
</el-popover>
<!-- 上传主题 -->
<el-image v-if="file.status === 'success' && file.url" class="el-upload-list__item-thumbnail" :src="getThumbnailUrl(file)" fit="fit" />
<el-progress v-if="file.status === 'uploading'" type="circle" :percentage="parseInt(file.percentage)" />
<!-- <el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor(item.percentage * 100) / 100"></el-progress> -->
<span v-popover:popover class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<i class="el-icon-zoom-in"></i>
</span>
<span v-if="download" class="el-upload-list__item-download" @click="handleDownload(file)">
<i class="el-icon-download"></i>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
<i class="el-icon-delete"></i>
</span>
</span>
</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible" :lock-scroll="false" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</div>
</template>
<script>
import { uploadingPicture } from '@/utils/dict'
import { showSize } from '@/utils'
import { validURL } from '@/utils/validate'
import { getToken } from '@/utils/auth'
import { pasteUploadMixin } from './mixin/paste-upload'
import { getOssLiuParamsApi } from '@/api/utils'
import defaultSettings from '@/settings'
export default {
name: 'BaseUpload',
mixins: [pasteUploadMixin],
props: {
// 接受上传的文件类型
accept: {
type: String,
default: null
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否允许下载
download: {
type: Boolean,
default: true
},
// 上传个数
limit: {
type: Number,
default: 10
},
// 文件大小限制,单位为byte,默认1 * 10G(1M=1024k=1048576字节)
maxSize: {
type: Number,
default: (1048576 * 10240)
},
value: {
type: Array,
default: () => []
}
},
data() {
return {
thumbnailMaxSize: 1048576 * 20, // 缩略图(最大20M)
id: `baseUpload_${new Date().getTime()}`,
uploadStartTimeList: [],
OSS_URL: defaultSettings.OSS_URL,
myValue: this.value,
tokens: {},
dialogImageUrl: '',
dialogVisible: false,
headers: {
Authorization: getToken()
}
}
},
computed: {
// 缩略图地址
getThumbnailUrl(file) {
return (file) => {
return validURL(file.url) && file.size < this.thumbnailMaxSize ? file.url + `?x-oss-process=image/resize,w_${this.width}` : file.url
}
}
},
watch: {
value(newVal) {
this.myValue = newVal
},
myValue(newVal) {
this.$emit('input', newVal)
}
},
created() {
},
methods: {
showSize,
/**
* @description 正在上传
* @param {Object} event
* @param {Object} file 文件信息
* @param {Array} fileList 当前数组
* @result void
*/
handleProgress(event, file, fileList) {
this.$store.dispatch('tool/toggleBeUploading', true)
const currFile = this.uploadStartTimeList.filter(v => v.uid === file.uid)
if (currFile && currFile[0]) {
file.speed = (new Date().getTime() - currFile[0].startTime) / 1000
}
this.myValue = fileList
},
/**
* @description 图片数量限制
* @param {Object} file 文件信息
* @param {Array} fileList 当前数组
* @result void
*/
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择 ${this.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
},
generateTokens(key, file) {
return new Promise((resolve, reject) => {
getOssLiuParamsApi().then((res) => {
file.startTime = new Date().getTime()
this.uploadStartTimeList.push(file)
this.tokens.key = res.dir + key
this.tokens.policy = res.policy
this.tokens.OSSAccessKeyId = res.accessid
this.tokens.success_action_status = 200
this.tokens.callback = res.callback
this.tokens.signature = res.signature
resolve(true)
})
}, () => {
// eslint-disable-next-line no-undef
reject(false)
})
},
// 上传文件之前的钩子 {Object} file 文件信息
beforeUpload(file) {
const isBeyond = file.size < this.maxSize
if (!isBeyond) {
const beyondTip = '文件大小超过' + showSize(this.maxSize) + ',请重新上传。'
this.$message.error(beyondTip)
return isBeyond
} else {
const arr = file.name.split('.')
const un_name = `${file.uid}.${arr[arr.length - 1]}`
return this.generateTokens(un_name, file)
}
},
/**
* @description 文件上传成功时的钩子
* @param {Object} res 成功返回值
* @param {Object} file 文件信息
* @param {Array} fileList 当前数组
* @result void
*/
handleUploadSuccess(res, file, fileList) {
fileList.forEach(fileListItem => {
const obj = this.setImg(file)
const index = this.myValue.findIndex(item => item.uid === file.uid)
this.myValue.splice(index, 1, obj)
})
this.$store.dispatch('tool/toggleBeUploading', false)
this.$message({ type: 'success', message: '上传成功' })
},
/**
* @desc 重新设置图片地址
* @param {Object} file 文件信息
* @return {Object} img
*/
setImg(file) {
const arr = file.name.split('.')
const un_name = `${file.uid}.${arr[arr.length - 1]}`
var fileAddress = 'design' + un_name
const img = {
uid: file.uid,
real_url: this.OSS_URL + fileAddress,
name: file.name,
size: file.size
}
if (fileAddress.indexOf('.zip') > -1 || fileAddress.indexOf('.rar') > -1) {
file.url = uploadingPicture.zip
img.url = uploadingPicture.zip
} else if (fileAddress.indexOf('.xls') > -1) {
file.url = uploadingPicture.xls
img.url = uploadingPicture.xls
} else if (fileAddress.indexOf('.doc') > -1) {
file.url = uploadingPicture.doc
img.url = uploadingPicture.doc
} else if (fileAddress.indexOf('.txt') > -1) {
file.url = uploadingPicture.txt
img.url = uploadingPicture.txt
} else if (fileAddress.indexOf('.mp4') > -1) {
file.url = uploadingPicture.mp4
img.url = uploadingPicture.mp4
} else if (fileAddress.indexOf('.jpg') > -1 || fileAddress.indexOf('.png') > -1 || fileAddress.indexOf('.jpeg') > -1 || fileAddress.indexOf('.gif') > -1 || fileAddress.indexOf('.JPG') > -1 || fileAddress.indexOf('.PNG') > -1 || fileAddress.indexOf('.JPEG') > -1 || fileAddress.indexOf('.GIF') > -1) {
file.url = this.OSS_URL + fileAddress
img.url = this.OSS_URL + fileAddress
} else {
file.url = uploadingPicture.other
img.url = uploadingPicture.other
}
return img
},
// 文件上传失败时的钩子
handleUploadError() {
this.$message({ type: 'error', message: '上传失败' })
},
// 下载 {Object} file 文件信息
handleDownload(file) {
if (this.download) {
if (file.real_url) {
window.open(file.real_url)
} else {
window.open(file.url)
}
}
},
// 图片查看 {Object} file 文件信息
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url
this.dialogVisible = true
},
// 删除 {Object} file 文件信息
handleRemove(file) {
this.$refs.baseUpload.abort(file)
this.myValue = this.myValue.filter(v => v.uid !== file.uid)
}
}
}
</script>
<style lang="scss">
.base-upload {
// 图片大小
@include upload-width(78px, 78px);
.picture-card--disabled {
.el-upload--picture-card {
display: none;
}
}
}
</style>
<style lang="scss" scoped>
.target-size {
@include fc(12px, red);
line-height: 25px;
}
.target-size--success {
color: #91b493;
}
</style>
// paste-upload.js
import { uploadingUpFile } from '@/api/utils'
import { mapGetters } from 'vuex'
import { isEmpty } from '@/utils/validate'
export const pasteUploadMixin = {
data() {
return {
newHandle: null,
pasteUploadKey: ''
}
},
mounted() {
this.$nextTick(() => {
this.initPaste()
})
},
computed: {
...mapGetters(['pastPanel']),
// 获取激活项的地址(默认粘贴板的地址)
getBase64() {
return function(base64) {
let result = base64
if (!isEmpty(this.pastPanel.active)) {
const newResult = this.pastPanel.list.filter(v => v.id === this.pastPanel.active)
result = newResult[0].src
}
return result
}
},
// 返回粘贴限制key对应的type
getPasteUploadType() {
return function(key) {
const keyMap = {
model_img: 1,
reference_img: 2,
material_img: 3
}
return keyMap[key]
}
}
},
methods: {
onMousemove(e) {
const el = e.target
const elWrappend = el.closest(`div#${this.id}`)
const elCurrent = elWrappend.parentNode.parentNode.parentNode.firstElementChild
if (elCurrent.getAttributeNode('for') && elCurrent.getAttributeNode('for').value) {
this.pasteUploadKey = elCurrent.getAttributeNode('for').value
} else {
this.pasteUploadKey = ''
}
},
// 初始化粘贴
initPaste() {
var handleAction = e => {
var cbd = e.clipboardData
var ua = window.navigator.userAgent
// 如果是 Safari 直接 return
if (!(e.clipboardData && e.clipboardData.items)) {
return
}
// Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
if (cbd.items && cbd.items.length === 2 && cbd.items[0].kind === 'string' && cbd.items[1].kind === 'file' && cbd.types && cbd.types.length === 2 && cbd.types[0] === 'text/plain' && cbd.types[1] === 'Files' && ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49) {
return
}
for (var i = 0; i < cbd.items.length; i++) {
var item = cbd.items[i]
if (item.kind === 'file') {
var blob = item.getAsFile()
if (blob.size === 0) {
return
}
var reader = new FileReader()
reader.readAsDataURL(blob)
reader.onload = readerE => {
this.submitUpload(readerE.target.result)
}
}
}
}
this.newHandle = event => {
handleAction(event)
}
document.getElementById(this.id).removeEventListener('paste', this.newHandle, false)
document.getElementById(this.id).addEventListener('paste', this.newHandle, false)
},
submitUpload(base64) {
const params = {
type: this.getPasteUploadType(this.pasteUploadKey),
data: this.getBase64(base64)
}
this.$message.info('正在上传中,勿刷新页面')
this.$store.dispatch('tool/toggleBeUploading', true)
uploadingUpFile(params)
.then(res => {
this.myValue.push({ url: res.data, name: new Date().getTime() + '.jpg' })
this.$message({ type: 'success', message: '上传成功' })
this.$store.dispatch('tool/toggleBeUploading', false)
})
.catch(() => {
this.$message({ type: 'error', message: '上传失败' })
this.$store.dispatch('tool/toggleBeUploading', false)
})
}
},
destroyed() {
document.getElementById(this.id) && document.getElementById(this.id).removeEventListener('paste', this.newHandle, false)
}
}
喜欢的给个👍哦,欢迎留言