图片上传并剪裁
2020-11-03 本文已影响0人
McDu
图片上传组件
支持功能
- 图片上传、图片预览、图片剪裁、图片删除
- 默认上传时图片尺寸不符合 750 * 500 大小,自动弹出剪裁框剪裁,如不需要可设置
cropSize=false
关闭此功能 - 默认限制上传图片最多50张
- 默认限制图片大小最大1M
- 图片上传成功会有绿色的打勾角标
file.status = 'success'
使用场景
-
上传产品头图,特点:限制图片大小、有设置主图的插槽(
$scopedSlots.mainImg
)imgList: [{url:'xxx'}]
<image-upload :img-list="imgList" tip="图片限1M以下,图片宽度750,高度500,数量限50张" @image-change="handleImageChange" > <el-radio slot="mainImg" slot-scope="{file}" v-if="file.status === 'success'" v-model="form.main_image" :label="file.url" @change="handleMainImageChange(file.url)" > 主图 </el-radio> </image-upload>
handleImageChange(data) { this.imgList = data; }
-
上传产品图片,不限制尺寸,无主图插槽
<image-upload :img-list="imgList" :cropSize="false" @image-change="handleImageChange" ></image-upload>
ImageUpload.js
<!--
https://github.com/xyxiao001/vue-cropper 组件详细信息
description: 图片上传 支持批量上传,图片大小、尺寸校验 (主图插槽 -> mainImg)
@param imgList Array eg: [{url: imgUrl}] 必传
@param limit Number 上传数量限制 50张
@param cropSize Boolean 是否限制图片尺寸 限制
@param cropOptions Object 截图选项
@param limitSize Number 最大尺寸 1024kb
@param tip String 上传说明 ''
-->
<template>
<div>
<el-upload
:file-list="imgList"
multiple
ref="upload"
:limit="limit"
list-type="picture-card"
:accept="accept"
:auto-upload="false"
:on-exceed="handleExceed"
:on-change="debounce(handleChange, 2)"
:http-request="httpRequest"
:before-upload="beforeUpload"
>
<i slot="default" class="el-icon-plus"></i>
<div slot="file" slot-scope="{ file }">
<div :class="$scopedSlots.mainImg ? 'main-wrap' : ''">
<img class="el-upload-list__item-thumbnail" :src="file.url" alt />
<label
class="el-upload-list__item-status-label"
v-if="file.status === 'success'"
>
<i class="el-icon-upload-success el-icon-check"></i>
</label>
<span class="el-upload-list__item-actions" v-if="showHandle">
<span
class="el-upload-list__item-preview"
@click="showPreviewDialog(file)"
>
<i class="el-icon-zoom-in"></i>
</span>
<span
class="el-upload-list__item-delete"
@click="showCropDialog(file)"
>
<i class="el-icon-scissors"></i>
</span>
<span
class="el-upload-list__item-delete"
@click="handleRemove(file)"
>
<i class="el-icon-delete"></i>
</span>
</span>
</div>
<div class="main-img-box">
<slot name="mainImg" :file="file"></slot>
</div>
</div>
<div slot="tip" class="el-upload__tip">{{tip}}</div>
</el-upload>
<slot name="width-tips" :files="widthTipFiles"></slot>
<slot name="submit-btn" v-if="showSubmitBtn">
<el-button type="primary" size="small" @click="submitUpload">
确认上传图片
</el-button>
</slot>
<el-dialog :visible.sync="dialogPreviewVisible" append-to-body>
<img width="100%" :src="dialogPreviewUrl" alt />
</el-dialog>
<div v-for="(item, index) in needCropFiles" :key="item.uid">
<el-dialog :visible.sync="item.visible" width="875px" append-to-body>
<div style="width: 100%; height: 550px">
<vue-cropper
ref="cropper"
:auto-crop="cropOptions.autoCrop"
:fixed="cropOptions.fixed"
:fixed-box="cropOptions.fixedBox"
:high="cropOptions.high"
:center-box="cropOptions.centerBox"
:auto-crop-width="cropOptions.autoCropWidth"
:auto-crop-height="cropOptions.autoCropHeight"
:img="item.url"
/>
<div style="margin-top: 10px; color: red">
*滚动鼠标可以对图片进行缩放操作
</div>
</div>
<span slot="footer">
<el-button type="primary" @click="cropFinish(item, index)">剪裁</el-button>
<el-button @click="closeCropDialog(item)">取消</el-button>
</span>
</el-dialog>
</div>
</div>
</template>
<script>
import {VueCropper} from 'vue-cropper';
import request from 'lib/utils/request';
import Tools from './tools';
export default {
data() {
return {
// 预览弹框
dialogPreviewUrl: '',
dialogPreviewVisible: false,
// 需要剪裁的图片
needCropFiles: [],
// 图片宽度不是 750px
widthTipFiles: []
};
},
props: {
// 图片列表
imgList: {
type: Array,
default: () => []
},
// 图片张数限制
limit: {
type: Number,
default: 50
},
limitSize: {
type: Number,
default: 1
},
// 截图选项
cropOptions: {
type: Object,
default: () => {
return {
// 是否开启截图框宽高固定比例
fixed: false,
// 固定截图框大小 不允许改变
fixedBox: false,
// 是否默认生成截图框
autoCrop: true,
autoCropWidth: 750,
autoCropHeight: 500,
// 是否按照设备的dpr输出等比例图片
high: false,
// 截图框是否被限制在图片里面
centerBox: false
};
}
},
// 是否限制尺寸大小
cropSize: {
type: Boolean,
default: true
},
showHandle: {
type: Boolean,
default: true
},
// 是否展示『确认上传图片』按钮
showSubmitBtn: {
type: Boolean,
default: true
},
tip: {
type: String,
default: ''
},
showWidthTips: {
type: Boolean,
default: false
},
accept: {
type: String,
default: 'image/*'
}
},
computed: {
uploadFiles() {
return this.$refs.upload.uploadFiles;
},
autoW() {
return this.cropOptions.autoCropWidth;
},
autoH() {
return this.cropOptions.autoCropHeight;
}
},
watch: {
imgList: {
handler(val) {
if (!val.length) {
this.widthTipFiles = [];
}
},
immediate: true
}
},
methods: {
// 判断图片大小是否超过限制
judgeSizeLt_nM(size) {
return size < this.limitSize * 1024 * 1024;
},
// 错误提示信息
tipsInfo(file = {}) {
const {width, height, name = ''} = file;
return {
sizeTip: `大小超过 ${this.limitSize}M, 请压缩或剪裁后上传`,
widthTip: `您上传的 ${name} 图片尺寸为 ${width}px*${height}px, 要求 ${this.autoW}px*${this.autoH}px`,
typeTip: `文件类型必须为图片`
};
},
// 上传前校验
async beforeUpload(file) {
const {typeTip} = this.tipsInfo();
// 从 uploadFiles 取出的 file 具有更多信息
let cFile = this.uploadFiles.find((v) => v.uid === file.uid);
const isNeedCrop = this.judgeNeedCrop(cFile);
if (isNeedCrop) {
await this.getCroppedFile(cFile);
}
// 图片类型判断
if (!Tools.isImageType(file.type)) {
this.$message.error(typeTip);
return false;
} else {
return true;
}
},
// 手动上传图片
async httpRequest(item) {
const fd = new FormData();
const cFile = this.uploadFiles.find((v) => v.uid === item.file.uid);
fd.append('file', cFile.raw);
const [err, res] = await this.uploadImg(fd);
if (err) {
this.$message.warning('上传失败');
return false;
}
this.handleSuccess(res, cFile, this.uploadFiles);
},
uploadImg(formData) {
return request
.post('/admin/image/plainUpload.json', formData)
.then((res) => [null, res])
.catch((err) => [err, null]);
},
// 文件上传成功时的钩子
handleSuccess(res, file, fileList) {
if (res && res.ret) {
file.url = res.msg;
this.$emit('image-uploaded', fileList);
this.$emit('image-change', fileList);
}
},
// 添加文件、上传成功和上传失败时都会被调用
async handleChange(file, fileList) {
// file 状态为 success,是上传成功的状态
if (file.status === 'success') {
// fix 图片成功角标未更新
this.$forceUpdate();
return;
}
const readyFiles = fileList.filter(file => file.status === 'ready');
for(let file of readyFiles) {
const fileInfo = await Tools.getFileInfo(file);
const isNeedCrop = this.judgeNeedCrop(fileInfo);
if (isNeedCrop) {
file = await this.getCroppedFile(file);
}
}
},
debounce(fn, delay) {
let timer = null;
return function() {
// eslint-disable-next-line no-invalid-this
const context = this,
// eslint-disable-next-line prefer-rest-params
args = arguments;
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
};
},
// 是否需要剪裁(不需要剪裁或宽高固定的剪裁)
judgeNeedCrop(fileInfo) {
const {width, height} = fileInfo;
if (!this.cropSize) {
return false;
}
return width !== this.autoW || height !== this.autoH;
},
judgeWidth750(fileInfo) {
const index = this.getIndex(this.widthTipFiles, fileInfo);
if (fileInfo.width !== 750) {
if (index < 0) {
this.widthTipFiles.push(fileInfo);
}
} else {
if(index >= 0) {
this.widthTipFiles.splice(index, 1);
}
}
},
// 弹框确认
handleConfirmDialog(file) {
const {widthTip} = this.tipsInfo(file);
return new Promise((resolve, reject) => {
this.$confirm(widthTip, {
confirmButtonText: '去剪裁',
cancelButtonText: '取消上传'
}).then(
() => {
resolve(true);
},
() => {
this.uploadFiles.pop();
resolve(false);
}
);
});
},
// 获取剪裁后的图片
async getCroppedFile(file) {
const fileInfo = await Tools.getFileInfo(file);
const isGoCrop = await this.handleConfirmDialog(fileInfo);
// 去剪裁
if (isGoCrop) {
return new Promise((resolve, reject) => {
this.showCropDialog(file);
this.$on('cropFinish', (croppedFile) => {
resolve(croppedFile);
});
});
} else {
return Promise.resolve(null);
}
},
submitUpload() {
this.$refs.upload.submit();
},
clearFiles() {
this.$refs.upload.clearFiles();
},
// 剪裁
cropFinish(file, index) {
const $cropper = this.$refs.cropper[index];
const $cropW = $cropper.cropW,
$cropH = $cropper.cropH;
$cropper.getCropBlob((data) => {
const imgUrl = window.URL.createObjectURL(data);
// 更新图片 raw、size、url 等
file.raw = new Blob([data], {
type: data.type
});
file.url = imgUrl;
file.size = data.size;
// 这里设置 uid,为了 beforeUpload 时的 filter
file.raw.uid = file.uid;
file.status = 'ready';
file.width = $cropW;
file.height = $cropH;
this.$emit('cropFinish', file);
this.closeCropDialog(file);
if(this.showWidthTips) {
this.judgeWidth750(file);
}
});
},
// 弹出剪裁框剪裁
showCropDialog(file) {
file.visible = true;
const index = this.getIndex(this.needCropFiles, file);
if (index < 0) {
this.needCropFiles.push(file);
}
},
getIndex(arr, file) {
return arr.findIndex((v) => v.uid === file.uid);
},
// 关闭剪裁框
closeCropDialog(file) {
file.visible = false;
this.needCropFiles = this.needCropFiles.filter((v) => v.visible);
// Fix:$emit 一次 cropFinish 事件,$on 回调执行多次
this.$off('cropFinish');
},
// 上传数量超出限制
handleExceed(files, fileList) {
const limit = this.limit,
left = limit - fileList.length;
this.$message.error(`上传数量限制${limit}张,还可上传${left}张`);
},
// 删除图片
handleRemove(file) {
const index = this.getIndex(this.uploadFiles, file);
this.uploadFiles.splice(index, 1);
if(this.showWidthTips) {
const tId = this.getIndex(this.widthTipFiles, file);
if(tId >= 0) {
this.widthTipFiles.splice(tId, 1);
}
}
this.$emit('image-change', this.uploadFiles);
this.$emit('image-removed', this.uploadFiles);
},
// 显示预览框
showPreviewDialog(file) {
this.dialogPreviewUrl = file.url;
this.dialogPreviewVisible = true;
}
},
components: {
VueCropper
}
};
</script>
<style lang="scss">
.error-tips {
margin-bottom: 10px;
line-height: 20px;
font-size: 12px;
color: #f56c6c;
}
.el-upload-list--picture-card {
.el-upload-list__item-actions {
height: 148px;
}
// 有主图插槽,图片高度为100px
.main-wrap {
height: 100px;
overflow: hidden;
.el-upload-list__item-thumbnail {
width: 100%;
height: auto;
}
.el-upload-list__item-actions {
height: 100px;
}
}
.main-img-box {
line-height: 48px;
text-align: center;
}
}
</style>
tools.js
const Tools = {
isImageType(type) {
const reg = /^image\//;
return reg.test(type);
},
// 获取 file 信息:width、height、url 等
getFileInfo(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.src = e.target.result;
img.onload = () => {
file.width = img.width;
file.height = img.height;
file.url = img.src;
resolve(file);
};
img.onerror = () => {
reject();
};
};
reader.readAsDataURL(file.raw);
});
}
};
export default Tools;