element-ui + vue-cropper 图片裁剪后上传

本文最后更新于:2022年7月6日 上午

兵来将挡,水来土掩

前提

  • vue-cropper
  • element-ui upload
  • 整体分为两个组件来做
    • cropper.vue 主要负责图片裁剪,将裁剪之后的数据传递出来
    • crop-upload.vue,引入 cropper.vue 作为子组件,将裁剪后的数据上传、显示上传列表等功能

cropper.vue

这一步没什么难度,单独抽出来一个组件只是为了项目结构能清晰一点。
按照文档中的 demo 来设置即可,其中部分属性项目 demo 中没有提及,直接去看源码即可。

要注意的是,组件的外部一定要设置固定的宽高

<!--crop-box 设置固定宽高-->
<div class="crop-box">
	<vue-cropper
		:autoCrop="cropperOption.autoCrop"
		:autoCropHeight="cropperOption.autoCropHeight"
		:autoCropWidth="cropperOption.autoCropWidth"
		:canMove="cropperOption.canMove"
		:canMoveBox="cropperOption.canMoveBox"
		:canScale="cropperOption.canScale"
		:centerBox="cropperOption.centerBox"
		:enlarge="cropperOption.enlarge"
		:fixedBox="cropperOption.fixedBox"
		:fixed="cropperOption.fixed"
		:full="cropperOption.full"
		:high="cropperOption.high"
		:img="cropperOption.img"
		:info="true"
		:infoTrue="cropperOption.infoTrue"
		:limitMinSize="cropperOption.limitMinSize"
		:maxImgSize="cropperOption.maxImgSize"
		:mode="cropperOption.mode"
		:original="cropperOption.original"
		:outputSize="cropperOption.size"
		:outputType="cropperOption.outputType"
		@cropMoving="onCropMoving"
		@imgLoad="onImgLoad"
		@realTime="onRealTime"
		ref="cropper"
	></vue-cropper>
</div>

组件中各个属性的含义如下:

cropperOption: {
	img: '', // 需要裁剪的图片
	size: 1, // 输出图片压缩比, 默认 1
	full: false, // 是否输出原图比例的截图
	infoTrue: false, // 截图信息展示是否时真实的输出宽高
	outputType: 'png', // 输出的图片格式
	canScale: false, // 是否开启滚轮缩放大小
	canMove: false, // 能否拖动图片
	canMoveBox: true, // 能否拖动截图框
	fixed: true, // 项目要求方形裁剪,因此固定宽高比,默认宽高比是 1:1
	fixedBox: false, // 截图框固定大小
	original: false, // 上传图片时,是否显示原始宽高
	autoCrop: true, // 是否自动生成截图框
	// 只有自动截图开启 宽度高度才生效
	autoCropWidth: 200,
	autoCropHeight: 200,
	centerBox: true, // 截图框是否限制在图片里(只有在自动生成截图框时才生效)
	high: false, // 是否根据 dpr 生成适合屏幕的高清图片
	cropData: {},
	enlarge: 1, // 按照截图框比例输出,默认 1
	mode: 'contain', // 图片的默认渲染方式
	maxImgSize: 2000, // 上传图片时图片最大大小(默认会压缩尺寸到这个大小)
	limitMinSize: [200, 200] // 截图框最小限制
}

最终确定裁剪时,调用下面的方法获取裁剪后的数据

// 获取 blob
this.$refs.cropper.getCropBlob((blob) => {
	console.log('crop onConfirm -> blob', blob);
});

// 获取 base64
this.$refs.cropper.getCropData((blob) => {
	console.log('crop onConfirm -> blob', blob);
});

cropper.vue 最终代码

<!--cropper.vue-->
<template>
	<div>
		<el-dialog :visible.sync="visible" title="图片裁剪" width="800px">
			<div class="crop-container" v-show="!showPreview">
				<div class="crop-box">
					<vue-cropper
						:autoCrop="cropperOption.autoCrop"
						:autoCropHeight="cropperOption.autoCropHeight"
						:autoCropWidth="cropperOption.autoCropWidth"
						:canMove="cropperOption.canMove"
						:canMoveBox="cropperOption.canMoveBox"
						:canScale="cropperOption.canScale"
						:centerBox="cropperOption.centerBox"
						:enlarge="cropperOption.enlarge"
						:fixedBox="cropperOption.fixedBox"
						:fixed="cropperOption.fixed"
						:full="cropperOption.full"
						:high="cropperOption.high"
						:img="cropperOption.img"
						:info="true"
						:infoTrue="cropperOption.infoTrue"
						:limitMinSize="cropperOption.limitMinSize"
						:maxImgSize="cropperOption.maxImgSize"
						:mode="cropperOption.mode"
						:original="cropperOption.original"
						:outputSize="cropperOption.size"
						:outputType="cropperOption.outputType"
						@cropMoving="onCropMoving"
						@imgLoad="onImgLoad"
						@realTime="onRealTime"
						ref="cropper"
					></vue-cropper>
				</div>
				<div class="crop-action">
          <el-button @click="onConfirm" type="primary">确 定</el-button>
					<el-button @click="showPreview = true">预 览</el-button>
					<el-button @click="onCancel">取 消</el-button>
				</div>
			</div>
			<div class="crop-container" v-show="showPreview">
				<div :style="previews.div" class="crop-preview">
					<img :src="previews.url" :style="previews.img" alt />
				</div>
				<div class="crop-action">
					<el-button @click="onConfirm" type="primary">确 定</el-button>
					<el-button @click="showPreview = false">取消预览</el-button>
				</div>
			</div>
		</el-dialog>
	</div>
</template>

<script>
import { VueCropper } from 'vue-cropper';
export default {
	name: 'cropper-upload',
	data() {
		return {
			visible: false,
			previews: {},
			showPreview: false,
			crap: false,
			cropperOption: {
				img: '',
				size: 1, // 输出图片压缩比, 默认 1
				full: false, // 是否输出原图比例的截图
				infoTrue: false, // 截图信息展示是否时真实的输出宽高
				outputType: 'png',
				canScale: false, // 是否开启滚轮缩放大小
				canMove: false, // 能否拖动图片
				canMoveBox: true, // 能否拖动截图框
				fixed: true, // 固定宽高比
				fixedBox: false, // 截图框固定大小
				original: false, // 上传图片时,是否显示原始宽高
				autoCrop: true, // 是否自动生成截图框
				// 只有自动截图开启 宽度高度才生效
				autoCropWidth: 200,
				autoCropHeight: 200,
				centerBox: true, // 截图框是否限制在图片里(只有在自动生成截图框时才生效)
				high: false, // 是否根据 dpr 生成适合屏幕的高清图片
				cropData: {},
				enlarge: 1, // 按照截图框比例输出,默认 1
				mode: 'contain', // 图片的默认渲染方式
				maxImgSize: 2000, // 上传图片时图片最大大小(默认会压缩尺寸到这个大小)
				limitMinSize: [200, 200] // 截图框最小限制
			}
		};
  },
  computed: {
    cropper() {
      return this.$refs.cropper;
    },
  },
	methods: {
		show(file) {
			this.cropperOption.img = file.url;
			this.$nextTick(() => {
				this.visible = true;
			});
		},
		hide() {
			this.visible = false;
		},
		// 实时预览函数
		// data 中保存了需要预览的样式及 url,直接用就行了
		onRealTime(data) {
			// console.log('onRealTime -> data', data);
			this.previews = data;
		},
		onImgLoad(msg) {
      console.log('onImgLoad -> msg', msg);
			// 图片加载完成后,获取图片的真实宽高
			// 以最小的那个值作为裁剪框默认大小
      const { trueWidth, trueHeight } = this.cropper;
      const width = Math.min(trueWidth, trueHeight);
      this.cropperOption.autoCropWidth = width;
      this.cropperOption.autoCropHeight = width;
		},
		onCropMoving(data) {
			this.cropperOption.cropData = data;
		},
		onConfirm() {
			// 获取裁剪后的 blob 数据,传递到外部
			this.$refs.cropper.getCropBlob(blob => {
        console.log('crop onConfirm -> blob', blob);
        this.hide();
        this.$emit('on-finish', blob);
			});
		},
		onCancel() {
			this.hide();
			this.$emit('on-cancel');
		}
	},
	components: {
		VueCropper
	}
};
</script>

<style lang="stylus" scoped>
.crop-container {
  display: flex;
  align-items: center;
}
.crop-box {
  margin: 0 auto;
  width: 700px;
  height: 600px;
}
.crop-preview {
  margin: auto;
  border: 1px dotted #e4e4e4;
  overflow: hidden;
}
.crop-action {
  width: 100px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.el-button {
  width: 98px;
  margin-bottom: 20px;
  margin-left: 10px;
}
</style>

crop-upload.vue

思路

  • 因为需要先裁剪再上传,因此需要关闭自动上传设置:auto-upload 设置为 false
  • 之后,利用上一步裁剪后的数据进行 FormData 上传即可
  • 还有就是,没有用 el-upload 自带的 file-list,主要是为了能获取到当前裁剪的是哪个图片,方便进行对应的更新

最终代码

<template>
	<div class="crop-upload">
		<div class="crop-upload-list">
			<ul class="el-upload-list el-upload-list--picture-card">
				<li
					:key="item.url"
					:tabindex="index"
					class="el-upload-list__item is-success"
					v-for="(item, index) in fileList"
				>
					<img :src="item.url" alt class="el-upload-list__item-thumbnail" />
					<a class="el-upload-list__item-name">
						<i class="el-icon-document"></i>
					</a>
					<label class="el-upload-list__item-status-label">
						<i class="el-icon-upload-success el-icon-check"></i>
					</label>
					<i class="el-icon-close"></i>
					<i class="el-icon-close-tip">按 delete 键可删除</i>
					<!---->
					<span class="el-upload-list__item-actions">
						<span @click="onEdit(index)" class="el-upload-list__item-preview">
							<i class="el-icon-edit"></i>
						</span>
						<span @click="onPreview(item)" class="el-upload-list__item-preview">
							<i class="el-icon-zoom-in"></i>
						</span>
						<span @click="onRemove(index)" class="el-upload-list__item-delete">
							<i class="el-icon-delete"></i>
						</span>
					</span>
				</li>
			</ul>
			<el-upload
				:action="uploadUrl"
				:auto-upload="false"
				:data="uploadData"
				:headers="headers"
				:limit="limit"
				:on-change="onChange"
				:on-exceed="onExceed"
				:show-file-list="false"
				list-type="picture-card"
				ref="upload"
			>
				<i class="el-icon-plus"></i>
			</el-upload>
		</div>
		<cropper
			:visible.sync="cropperVisible"
			@on-cancel="onCropCancel"
			@on-finish="onCropFinish"
			ref="cropper"
		></cropper>
		<el-dialog :visible.sync="dialogVisible">
			<img :src="dialogImageUrl" alt width="100%" />
		</el-dialog>
	</div>
</template>

<script>
import Cropper from './cropper.vue';

export default {
	props: {
		limit: {
			type: Number,
			default: 10
		},
		fileList: {
			type: Array,
			default: () => []
		}
	},
	data() {
		return {
			headers: {
				// your headers
			},
			uploadUrl: 'upload url',
			uploadData: {},
			dialogVisible: false,
			dialogImageUrl: '',
			cropperVisible: false,
			selectedCropIndex: -1
		};
	},
	computed: {
		upload() {
			return this.$refs.upload;
		},
		cropper() {
			return this.$refs.cropper;
		}
	},
	created() {},
	methods: {
    // 删除
		onRemove(index) {
			const newFileList = this.fileList;
			newFileList.splice(index, 1);
			this.$emit('update:fileList', newFileList);
		},
    // 预览
		onPreview(item) {
			this.dialogImageUrl = item.url;
			this.dialogVisible = true;
		},
    // 弹出裁剪组件
		onEdit(index) {
			this.selectedCropIndex = index;
			const file = this.fileList[index];
			this.cropper.show(file);
		},
		onExceed() {
			this.$message({
				type: 'error',
				message: '超出上传数量限制,无法继续上传'
			});
		},
		// 文件状态改变时的钩子,添加文件,弹出裁剪组件
		onChange(file, fileList) {
			this.cropper.show(file);
		},
    // 监听裁剪结束,进行上传
		onCropFinish(data) {
			const loading = this.$loading({
				lock: true,
				text: 'Loading',
				spinner: 'el-icon-loading',
				background: 'rgba(0, 0, 0, 0.7)'
			});
			this.doUpload(data)
				.then(url => {
					// 更新对应 index 的 url
					const newFileList = this.fileList;
					const item = { url, };
					if (this.selectedCropIndex === -1) {
						newFileList.push(item);
					} else {
						newFileList[this.selectedCropIndex] = item;
					}
					this.$emit('update:fileList', newFileList);
				})
				.finally(() => {
					this.reset();
					loading.close();
				});
		},
    // 取消裁剪
		onCropCancel() {
			this.reset();
		},
		doUpload(data) {
			// your custom upload
		},
		reset() {
			this.selectedCropIndex = -1;
		}
	},
	components: {
		Cropper
	}
};
</script>

<style lang="stylus" scoped>
.crop-upload-list {
  display: flex;
}
</style>

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

 目录