基于Vue+ElementUi的全能图片上传公用组件
集多图上传,图片大小格式限制,图片压缩,图片裁剪功能为一体
<template>
<!-- 图片上传公共组件 -->
<div id="uploadimg"
class="uploadimg"
:style="styleVar">
<ul class="imgList">
<li v-for="(item,i) in imgList"
:key="i"
:style="{'margin-bottom':limit>1?'16px':''}">
<img :src="item.url">
<el-progress v-show="!item.url"
type="circle"
:width="parseInt(imgShowWidth)-20"
:text-inside="false"
:percentage="progressList[i]"></el-progress>
<span><i class="el-icon-zoom-in"
@click="elImagSelect=i,elImageList=imgList.map(arry=>(arry.url)),$refs.previewImg.showViewer=true"></i>
<i class="el-icon-delete"
@click="imgDelete(i)"></i>
</span>
<p v-show="imgName">{{item.name}}</p>
</li>
<li v-show="imgList.length<limit">
<el-upload action=""
:multiple="limit>1&&!tailoring"
:accept="accept"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
:http-request="httpRequestImagePath">
<i class="el-icon-plus avatar-uploader-icon"></i>
<span v-show="limit>1">{{imgList.length+'/'+limit}}</span>
</el-upload>
</li>
</ul>
<el-dialog title="图片剪裁"
:visible.sync="dialogTailoring"
append-to-body
width="540px">
<div class="cropper-content">
<div class="cropper"
style="text-align:center">
<vueCropper ref="cropper"
:img="option.img"
:outputSize="option.outputSize"
:outputType="option.outputType"
:info="option.info"
:canScale="option.canScale"
:autoCrop="option.autoCrop"
:autoCropWidth="option.autoCropWidth"
:autoCropHeight="option.autoCropHeight"
:fixed="option.fixed"
:fixedBox="option.fixedBox"
:fixedNumber="option.fixedNumber"></vueCropper>
</div>
</div>
<div slot="footer"
class="dialog-footer">
<el-button @click="dialogTailoring = false">取 消</el-button>
<el-button type="primary"
@click="finishTailoring">确认</el-button>
</div>
</el-dialog>
<el-image ref="previewImg"
style="width:0;height:0"
:src="elImageList[elImagSelect]"
:preview-src-list="elImageList">
</el-image>
</div>
</template>
<script>
import { VueCropper } from 'vue-cropper'
import axios from 'axios'
import store from '@/store'
import { getToken } from '@/utils/auth'
export default {
components: { VueCropper },
name: 'UploadImg',
props: {
//图片类型
accept: {
type: String,
default: '.jpg,.png,.jpeg,.gif'
},
//图片个数
limit: {
type: Number,
default: 1
},
//图片大小
imgSize: {
type: Number,
default: 10
},
//组件传值
uploadImgFaToSo: {
type: Array,
default: () => []
},
//图片展示尺寸
imgShowWidth: {
type: String,
default: '155px'
},
//图片是否裁剪
tailoring: {
type: Boolean,
default: false
},
//是否显示图片名
imgName: {
type: Boolean,
default: false
},
},
data () {
return {
imgList: [],
elImageList: [],
elImagSelect: -1,
uploadState: [],//上传状态
progressList: [],//上传进度
fileName: '',
fileUid: '',
dialogTailoring: false,
option: {
img: '', // 裁剪图片的地址
info: true, // 裁剪框的大小信息
outputSize: 1, // 裁剪生成图片的质量
outputType: 'png', // 裁剪生成图片的格式
canScale: true, // 图片是否允许滚轮缩放
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 300, // 默认生成截图框宽度
autoCropHeight: 300, // 默认生成截图框高度
fixedBox: false, // 固定截图框大小 不允许改变
fixed: true, // 是否开启截图框宽高固定比例
fixedNumber: [1, 1], // 截图框的宽高比例
full: true, // 是否输出原图比例的截图
canMoveBox: false, // 截图框能否拖动
original: false, // 上传图片按照原始比例渲染
centerBox: true, // 截图框是否被限制在图片里面
infoTrue: false, // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
canMove: true,
},
}
},
computed: {
styleVar () {
return {
'--imgShowWidth': this.imgShowWidth,
}
}
},
mounted () {
//监听组件显示隐藏
const myObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
if (entry.contentRect.width == 0) {
this.imgList = []
this.uploadState = []
this.progressList = []
this.$emit('uploadImgSoToFa', [{ name: '', url: '' }])
}
})
})
myObserver.observe(document.querySelector('#uploadimg'))
},
watch: {
uploadImgFaToSo (val) {
val.forEach(arry => {
if (arry.url !== '' && arry.url !== null) {
this.imgList.push(arry)
this.uploadState.push(1)
this.progressList.push(100)
}
});
}
},
methods: {
httpRequestImagePath (options) {
if (this.tailoring) {
this.fileName = options.file.name.substring(0, options.file.name.lastIndexOf('.')) + '.png'
this.fileUid = options.file.uid
this.$nextTick(() => {
this.option.img = URL.createObjectURL(options.file);
this.dialogTailoring = true
})
} else {
this.imgList.push(options.file)
this.uploadState.push(0)
this.progressList.push(0)
//当图片大于300k时先压缩再上传
if (options.file.size > 307200) {
this.compressImg(new File([options.file], options.file.name, { type: options.file.type })).then(file => {
file.uid = options.file.uid
this.imgUpload(file)
});
} else {
this.imgUpload(options.file)
}
}
},
//图片上传
imgUpload (file) {
var form = new FormData()
form.append('file', file)
axios({
url: '/auth/file/upload',
method: 'post',
data: form,
baseURL: process.env.VUE_APP_SERVICE_URL,
headers: {
Authorization: getToken(),
tenant: store.getters.Tenant
},
//获取上传进度
onUploadProgress: progressEvent => {
let progress = parseInt(progressEvent.loaded / progressEvent.total * 100)
this.imgList.forEach((arry, index) => {
if (file.uid == arry.uid) this.progressList.splice(index, 1, progress == 100 ? 99 : progress)
});
},
}).then(res => {
this.imgList.forEach((arry, index) => {
if (file.uid == arry.uid) {
this.imgList[index].url = res.data.data.url
this.uploadState[index] = 1
this.progressList.splice(index, 1, 100)
}
});
//当所有文件上传完毕后再传给父组件
if (this.uploadState.indexOf(0) == -1) this.$emit('uploadImgSoToFa', this.imgList)
}).catch(err => {
this.$message.error(err.msg);
});
},
//文件删除
imgDelete (i) {
this.imgList.splice(i, 1)
this.uploadState.splice(i, 1)
this.progressList.splice(i, 1)
if (this.imgList.length == 0) {
this.$emit('uploadImgSoToFa', [{ name: '', url: '' }])
} else {
this.$emit('uploadImgSoToFa', this.imgList)
}
},
//图片裁剪
finishTailoring () {
this.$refs.cropper.getCropBlob((data) => {
let file = new File([data], this.fileName, { type: "image/png" })
file.uid = this.fileUid
this.dialogTailoring = false
this.imgList.push(file)
this.uploadState.push(0)
this.progressList.push(0)
this.imgUpload(file)
})
},
//图片选择校验
beforeAvatarUpload (file) {
let fileName = file.name
let fileType = fileName.substring(fileName.lastIndexOf('.'), fileName.length);
if (this.imgList.length >= this.limit) {
this.$message.warning('当前限制选择 ' + this.limit + ' 个文件');
return false
}
if (this.accept.indexOf(fileType) == -1) {
this.$message.error('上传图片只能是 ' + this.accept.replaceAll(',', '/').replaceAll('.', '') + ' 格式!');
return false
}
if (file.size / 1024 / 1024 > this.imgSize) {
this.$message.error('上传图片大小不能超过 ' + this.imgSize + 'MB!');
return false
}
},
//base64转file
base64ToFile (base64, name) {
var arr = base64.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], name, { type: mime });
},
//压缩图片
compressImg (file) {
var vm = this;
var files;
var fileSize = parseFloat(parseInt(file['size']) / 1024 / 1024).toFixed(2);
var read = new FileReader();
read.readAsDataURL(file);
return new Promise(function (resolve, reject) {
read.onload = function (e) {
var img = new Image();
img.src = e.target.result;
img.onload = function () {
//默认按比例压缩
var w = this.width,
h = this.height;
//生成canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var base64;
// 创建属性节点
canvas.setAttribute("width", w);
canvas.setAttribute("height", h);
ctx.drawImage(this, 0, 0, w, h);
base64 = canvas.toDataURL(file['type'], 0.8);
// 回调函数返回file的值(将base64编码转成file)
files = vm.base64ToFile(base64, file.name); //如果后台接收类型为base64的话这一步可以省略
resolve(files)
};
};
})
}
}
}
</script>
<style lang="scss">
.uploadimg {
ul {
display: inline-flex;
flex-wrap: wrap;
margin: 0;
padding: 0;
vertical-align: top;
.move {
-webkit-animation: fadeOutUp 1s 0.2s ease both;
-moz-animation: fadeOutUp 1s 0.2s ease both;
}
li:last-of-type {
animation: none;
}
li {
overflow: hidden;
position: relative;
width: var(--imgShowWidth);
height: var(--imgShowWidth);
margin-right: 10px;
padding: 1px;
list-style: none;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #c0ccda;
border-radius: 6px;
-webkit-animation: fadeInDown 1s 0.2s ease both;
-moz-animation: fadeInDown 1s 0.2s ease both;
img {
max-width: 100%;
max-height: 100%;
}
> span {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
display: flex;
justify-content: space-around;
color: #fff;
opacity: 0;
font-size: var(--imgShowWidth);
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s;
i {
cursor: pointer;
font-size: 0.25em;
line-height: var(--imgShowWidth);
}
}
p {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
margin: 0;
text-align: center;
font-size: 1em;
line-height: 2;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
li:hover span {
opacity: 1;
}
}
> div {
display: inline-block;
}
.el-upload {
position: relative;
width: var(--imgShowWidth);
height: var(--imgShowWidth);
border: 1px dashed #c0ccda;
border-radius: 6px;
.el-icon-plus {
width: 100% !important;
height: 100% !important;
font-size: 24px;
line-height: var(--imgShowWidth) !important;
}
span {
position: absolute;
bottom: 10px;
left: 0;
width: 100%;
line-height: 1;
text-align: center;
}
}
}
.cropper-content {
width: 500px;
height: 500px;
background: pink;
}
.cropper {
width: 500px;
height: 500px;
background: yellow;
}
@-webkit-keyframes fadeOutUp {
0% {
opacity: 1;
-webkit-transform: translateY(0);
}
100% {
opacity: 0;
-webkit-transform: translateY(-20px);
}
}
@-moz-keyframes fadeOutUp {
0% {
opacity: 1;
-moz-transform: translateY(0);
}
100% {
opacity: 0;
-moz-transform: translateY(-20px);
}
}
@-webkit-keyframes fadeInDown {
0% {
opacity: 0;
-webkit-transform: translateY(-20px);
}
100% {
opacity: 1;
-webkit-transform: translateY(0);
}
}
@-moz-keyframes fadeInDown {
0% {
opacity: 0;
-moz-transform: translateY(-20px);
}
100% {
opacity: 1;
-moz-transform: translateY(0);
}
}
</style>