VUE 图片上传/剪切(vue-cropper)
1. 概述
1.1 说明
项目中为了保证图片展示效果以及分辨率高度匹配,需对图片的尺寸、大小、类型等进行控制;最大限度保证图片在网站、小程序、app端的展示效果保持一致。
1.2 思路
使用vue-cropper进行图片裁剪功能,使用iview组件Upload进行图片上传。
1.2.1 功能选择
使用下拉框进行功能选择,如图片直接上传、图片剪裁等
1.2.2 图片上传与展现
使用Upload进行图片上传,根据所需上传图片的大小、格式等进行图片验证;所有验证通过后再进行图片上传操作。
1.2.3 最终组件样式
1.2.4 组件使用样式
1.3 vue-cropper使用
- 安装
npm install vue-cropper
或者yarn add vue-cropper
- main.js引用注入
- import VueCropper from \'vue-cropper\'
- Vue.use(VueCropper)
- 页面使用 <vueCropper></vueCropper>
- 安装
1.4 图片规则
2. 代码
2.1 示例代码结构:
2.2 图片上传、剪裁、展现公用组件(common)
2.2.1 图片剪裁组件代码(imageCropper.vue)
<!-- 图片上传剪裁 --><template>
<div ref="refCommonImageCropper" class="image-cropper-wrapper">
<div class="cropper-content">
<div class="operate-content">
<div class="scope-btn">
<label class="btn" for="uploads">{{ options.imgUrl ? \'更换图片\' : \'上传图片\'}}</label>
<input
type="file"
id="uploads"
style="position:absolute; clip:rect(0 0 0 0);"
accept="image/png, image/jpeg, image/gif, image/jpg"
@change="uploadImg($event)"
/>
<Button @click="changeScale(1)" title="放大">+</Button>
<Button @click="changeScale(-1)" title="缩小">-</Button>
<Button @click="rotateLeft" title="左旋转">↺</Button>
<Button @click="rotateRight" title="右旋转">↻</Button>
<!-- <Button @click="imageCropperSubmit(\'blob\')">确定</Button> -->
</div>
</div>
<div :style="{\'width\': options.imgWidth + \'px\', \'height\': options.imgHeight + \'px\'}">
<vueCropper
ref="cropper"
:img="options.imgUrl"
:outputSize="options.size"
:outputType="option.outputType"
:info="true"
:full="option.full"
:canMove="option.canMove"
:canMoveBox="option.canMoveBox"
:original="option.original"
:autoCrop="option.autoCrop"
:autoCropWidth="options.cropWidth"
:autoCropHeight="options.cropHeight"
:fixedBox="option.fixedBox"
@realTime="realTime"
@imgLoad="imgLoad"
></vueCropper>
</div>
</div>
<div class="cropper-operate" v-if="isShowView">
<div style="height:42px;padding:6px;">剪裁图片预览</div>
<div
class="show-preview"
:style="{\'width\': options.cropWidth + \'px\', \'height\': options.cropHeight + \'px\', \'overflow\': \'hidden\', \'margin\': \'5px\'}"
>
<div :style="previews.div" class="preview">
<img :src="previews.url" :style="previews.img" />
</div>
</div>
</div>
</div>
</template>
<script>
import { uploadImage } from \'../../../api/account.js\'
export default {
name: \'image-cropper\',
props: {
/**
* 裁剪配置
*/
setting: {
type: Object,
default () {
return {
imgUrl: \'\',
cropWidth: 200,
cropHeight: 200,
imgWidth: 350,
imgHeight: 300,
size: 1
}
}
},
/**
* 预览区域是否展示
*/
isShowView: {
type: Boolean,
default: true
}
},
data () {
return {
crap: false,
previews: {},
option: {
full: false,
outputType: \'png\',
canMove: true,
original: false,
canMoveBox: true,
autoCrop: true,
fixedBox: true
},
fileName: \'\'
}
},
computed: {
options () {
return this.setting
}
},
watch: {
options: {
// eslint-disable-next-line
handler (newVal, oldVal) {
this.setting = newVal
},
deep: true
}
},
methods: {
/**
* 更改比例
*/
changeScale (num) {
num = num || 1
this.$refs.cropper.changeScale(num)
},
/**
* 左旋转
*/
rotateLeft () {
this.$refs.cropper.rotateLeft()
},
/**
* 右旋转
*/
rotateRight () {
this.$refs.cropper.rotateRight()
},
/**
* 实时预览函数
*/
realTime (data) {
this.previews = data
},
/**
* 被剪裁图片的上传以及更改
*/
uploadImg (e) {
// 上传图片
var file = e.target.files[0]
if (!/\.(jpg|jpeg|png|JPG|PNG)$/.test(e.target.value)) {
this.$Message.warning(\'图片类型必须是jpeg,jpg,png中的一种\')
return false
}
this.fileName = file.name
var reader = new FileReader()
let type = file.type
reader.onload = (e) => {
let data
if (typeof e.target.result === \'object\') {
// 把Array Buffer转化为blob 如果是base64不需要
data = URL.createObjectURL(new Blob([e.target.result], { type }))
} else {
data = e.target.result
}
this.options.imgUrl = data
}
// 转化为base64
reader.readAsDataURL(file)
// 转化为blob
// reader.readAsArrayBuffer(file)
},
imgLoad (msg) {
console.log(msg)
},
/**
* 剪裁图片上传
*/
imageCropperSubmit (type) {
if (this.options.imgUrl) {
if (type === \'blob\') {
this.$refs.cropper.getCropBlob(data => {
let formData = new FormData()
formData.append(\'file\', data, this.fileName)
uploadImage(formData).then(data => {
this.$Message.success(\'上传成功\')
let cropperData = {
httpOriginalFileUri: data.httpOriginalFileUri,
originalFileUri: data.originalFileUri,
converFileUri: data.converFileUri,
fileName: this.fileName
}
this.$emit(\'fileSuccess\', cropperData)
}).catch(err => {
console.log(err)
})
})
} else {
// 此处得到的是base64位图片数据,暂无用
this.$refs.cropper.getCropData(data => {
console.log(data)
})
}
} else {
this.$Message.warning(\'请上传需裁剪图片\')
}
}
}
}
</script>
<style lang="scss" scoped>
.image-cropper-wrapper{
display: flex;
flex-direction: row;
justify-content: center;
.cropper-content {
display: flex;
display: -webkit-flex;
flex-direction: column;
.operate-content {
margin-bottom: 10px;
display: flex;
display: -webkit-flex;
.scope-btn {
display: flex;
display: -webkit-flex;
>button {
margin-left: 6px;
}
}
.btn {
outline: none;
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
-webkit-appearance: none;
text-align: center;
-webkit-box-sizing: border-box;
box-sizing: border-box;
outline: 0;
margin: 0;
-webkit-transition: 0.1s;
transition: 0.1s;
font-weight: 500;
padding: 8px 15px;
font-size: 12px;
border-radius: 3px;
color: #fff;
background-color: #67c23a;
border-color: #67c23a;
}
}
}
.cropper-operate{
display: flex;
flex-direction: column;
padding-left: 20px;
}
.show-preview {
flex: 1;
-webkit-flex: 1;
display: flex;
display: -webkit-flex;
justify-content: center;
align-items: center;
-webkit-justify-content: center;
.preview {
overflow: hidden;
border: 1px solid #cccccc;
background: #cccccc;
}
}
}
</style>
2.2.1 图片上传、展现、裁剪组件代码(imageOperate.vue)
<!-- 图片上传 --><template>
<div class="image-upload-wrapper">
<div class="default-upload-list" v-if="formInfo.fileUrl">
<img :src="formInfo.fileUrl">
<div class="default-upload-list-cover">
<Icon type="ios-eye-outline" @click.native="avatarModal=true"></Icon>
<Icon v-if="!isDisable" type="ios-trash-outline" @click.native="handleFileRemove"></Icon>
</div>
</div>
<div v-else class="image-upload-item">
<Select
v-model="formInfo.uploadTypeSelected"
placeholder="请选择图片上传方式"
:disabled="`${imgHeight}`==\'0\'"
:title="`${imgHeight}`==\'0\'?\'尺寸不限时只能进行直接上传操作\':\'\'"
@on-change="onChangeImageUploadType">
<Option
v-for="item in imageUploadType"
:value="item.value"
:key="item.value"
>{{ item.label }}</Option>
</Select>
<div class="image-upload-content">
<div v-if="formInfo.uploadTypeSelected==\'cropperUpload\'" class="cut-img-wrapper">
<div style="width: 58px;height:58px;line-height: 58px;" @click="uploadImage">
<Icon
type="ios-cut"
size="20"
style="color: #3399ff"
></Icon>
</div>
</div>
<!-- 直接上传 -->
<Upload
v-else
ref="refWholeUpload"
type="drag"
action="/api/admin/attach/uploadAttach"
style="width: 58px;height:58px;"
:show-upload-list="false"
:on-success="fileSuccess"
:before-upload="handleBeforeUpload"
:on-progress="uploadProgress"
:on-error="uploaderror">
<div style="width: 58px;height:58px;line-height: 58px;">
<Icon
type="ios-cloud-upload"
size="20"
style="color: #3399ff"
></Icon>
</div>
</Upload>
<Alert
v-if="!formInfo.fileUrl"
:type="imageError?\'error\':\'info\'"
class="image-info">
<div>宽高[{{(`${imgWidth}`!=\'0\'?imgWidth:\'不限\')}}*{{(`${imgHeight}`!=\'0\'?imgHeight:\'不限\')}}] 格式[{{formInfo.uploadTypeSelected==\'IconUpload\'?\'.ico\':\'.png/.jpg/.jpeg\'}}] 最大[{{imgSizeTip}}]</div>
<div v-if="imageError" style="color:red;">{{imageError}}</div>
</Alert>
</div>
</div>
<Modal
v-model="avatarModal"
:mask-closable="false"
:closable=\'false\'
:z-index=\'1001\'>
<img :src="formInfo.fileUrl" style="width: 100%">
<div slot="footer">
<Button
type="primary"
@click="avatarModal = false"
>关闭</Button>
</div>
</Modal>
<Modal
:width="modalCropperWidth"
v-model="modalImageCropper"
title=\'图片剪切处理\'
:mask-closable="false"
:z-index=\'2001\'
class-name="vertical-center-modal"
draggable>
<image-cropper
ref="refImageCropper"
@fileSuccess="imageCropperSuccess"
:setting="setOptions"
:isShowView="isShowViewImg"
v-if="modalImageCropper" />
<div slot="footer">
<Button @click="modalImageCropper = false">取消</Button>
<Button type="primary" @click="submitImageCropper">确定</Button>
</div>
</Modal>
<Spin
fix
style="z-index:9999"
v-if=\'isLoading\'
class="spin"
>图片上传中...</Spin>
</div>
</template>
<script>
import imageCropper from \'./imageCropper\'
export default {
name: \'image-operate\',
props: {
// 图片路径
imageUrl: {
type: String,
default () {
return \'\'
}
},
// 图片大小
imageSize: {
type: Number,
default () {
return 500
}
},
// 图片尺寸——宽
imgWidth: {
type: String | Number,
default () {
return 100
}
},
// 图片尺寸——高
imgHeight: {
type: String | Number,
default () {
return 100
}
},
// 图片大小文字内容
imgSizeTip: {
type: String,
default () {
return \'500KB\'
}
},
// 是否启用
isDisable: {
type: Boolean,
default: false
},
// 是否支持icon直接上传
isIconUpload: {
type: Boolean,
default: false
}
},
components: {
\'image-cropper\': imageCropper
},
data () {
return {
isLoading: false,
// 图片上传类型选择
imageUploadType: [],
formInfo: {
uploadTypeSelected: \'wholeUpload\',
fileUrl: \'\',
submitUrl: \'\'
},
// 图片预览
avatarModal: false,
attach: {
fileName: \'\',
originalFileUri: \'\',
converFileUri: \'\'
},
// 图片直接上传错误
imageError: \'\',
// 图片裁剪
setOptions: {
imgUrl: \'\',
size: 1,
cropWidth: 200,
cropHeight: 200,
imgWidth: 350,
imgHeight: 300
},
isShowViewImg: true,
modalCropperWidth: 700,
modalImageCropper: false
}
},
mounted () {
if (this.isIconUpload) {
this.imageUploadType = [
{ label: \'图片直接上传\', value: \'wholeUpload\' },
{ label: \'图片剪裁上传\', value: \'cropperUpload\' },
{ label: \'ICON图片直接上传\', value: \'IconUpload\' }
]
} else {
this.imageUploadType = [
{ label: \'图片直接上传\', value: \'wholeUpload\' },
{ label: \'图片剪裁上传\', value: \'cropperUpload\' }
]
}
this.imageError = \'\'
this.formInfo.fileUrl = this.imageUrl
},
methods: {
/**
* 图片上传类型选择
*/
onChangeImageUploadType (val) {
this.imageError = \'\'
this.handleFileRemove()
},
/**
* 裁剪图片
*/
uploadImage () {
this.setOptions.imgUrl = \'\'
this.setOptions.size = 1
this.setOptions.cropWidth = this.imgWidth
this.setOptions.cropHeight = this.imgHeight
this.setOptions.imgWidth = this.imgWidth + 20
this.setOptions.imgHeight = this.imgHeight + 20
let screenWidth = document.body.clientWidth
let maxWidth = screenWidth / 2 - 100
if (maxWidth >= this.imgWidth + 50) {
this.isShowViewImg = true
this.modalCropperWidth = (this.imgWidth + 40) * 2 > 500 ? (this.imgWidth + 50) * 2 : 500
} else {
this.isShowViewImg = false
if ((this.imgWidth + 50) * 2 <= 500) {
this.modalCropperWidth = 500
} else {
this.modalCropperWidth = this.imgWidth + 100
}
}
this.modalImageCropper = true
},
/**
* 图片确定上传
*/
submitImageCropper () {
this.$refs.refImageCropper.imageCropperSubmit(\'blob\')
},
/**
* 图片上传成功后返回对应图片信息
*/
imageCropperSuccess (val) {
this.formInfo.fileUrl = val.httpOriginalFileUri
this.formInfo.submitUrl = val.httpOriginalFileUri
const data = {
fileName: val.fileName,
originalFileUri: val.originalFileUri,
converFileUri: val.converFileUri,
fileUrl: val.httpOriginalFileUri,
submitUrl: val.originalFileUri
}
this.$emit(\'imageUploadEmit\', data)
this.modalImageCropper = false
},
/**
* 删除图片
*/
handleFileRemove () {
this.attach = {
fileName: this.attach.fileName,
originalFileUri: \'\',
converFileUri: \'\'
}
this.formInfo.fileUrl = \'\'
this.formInfo.submitUrl = \'\'
const data = {
fileName: this.attach.fileName,
originalFileUri: \'\',
converFileUri: \'\',
fileUrl: \'\',
submitUrl: \'\'
}
this.$emit(\'imageUploadEmit\', data)
},
/**
* 附件上传成功返回值
*/
fileSuccess (res, file) {
if (res.result) {
const { httpOriginalFileUri, originalFileUri, converFileUri } = res.data
this.attach.fileName = file.name
this.attach.originalFileUri = originalFileUri
this.attach.converFileUri = converFileUri
this.formInfo.fileUrl = httpOriginalFileUri
this.formInfo.submitUrl = originalFileUri
const data = {
fileName: file.name,
originalFileUri: originalFileUri,
converFileUri: converFileUri,
fileUrl: httpOriginalFileUri,
submitUrl: originalFileUri
}
this.isLoading = false
this.$emit(\'imageUploadEmit\', data)
} else {
this.formInfo.fileUrl = \'\'
this.imageError = \'图片上传失败,请重新上传。\'
this.isLoading = false
}
},
uploadProgress (file) {
this.isLoading = true
},
/**
* 附件上传判断
*/
handleBeforeUpload (file) {
this.isLoading = true
this.imageError = \'\'
let check = this.$refs.refWholeUpload.fileList.length < 1
let isIcontype = file.type === \'image/x-icon\'
if (this.formInfo.uploadTypeSelected === \'IconUpload\') {
if (!isIcontype) {
this.imageError = \'请选择icon类型的图片上传。\'
this.isLoading = false
return false
}
}
if (!check) {
this.imageError = \'限制上传一张图片,请删除后重新上传。\'
this.isLoading = false
return false
} else {
return this.checkImageWH(file, this.imgWidth, this.imgHeight)
}
},
/**
* 判断上传文件类型
*/
judgeFileType (type) {
let typeList = [\'image/jpeg\', \'image/png\', \'image/jpg\', \'image/x-icon\']
let hasIndex = typeList.findIndex(item => item.indexOf(type) > -1)
if (hasIndex > -1) {
return true
} else return false
},
/**
* 返回一个promise 检测通过返回resolve 失败返回reject组织图片上传
*/
checkImageWH (file, width, height) {
let self = this
return new Promise(function (resolve, reject) {
let accordType = self.judgeFileType(file.type)
if (!accordType) {
self.imageError = \'上传的文件为非图片格式,请选择图片格式文件上传\'
self.isLoading = false
reject(new Error(self.imageError))
} else if (file.size / 1024 > self.imageSize) {
self.imageError = `上传图片不能超过${self.imgSizeTip}`
self.isLoading = false
reject(new Error(self.imageError))
} else {
if (`${width}` !== \'0\') {
let filereader = new FileReader()
filereader.onload = e => {
let src = e.target.result
const image = new Image()
image.onload = function () {
let errorInfo = \'\'
if ((width && this.width !== width)) {
errorInfo = `宽(${this.width})、`
}
if (height && `${height}` !== \'0\' && this.height !== height) {
errorInfo = `${errorInfo}高(${this.height})`
}
if (errorInfo) {
self.imageError = `上传图片错误:${errorInfo}`
self.isLoading = false
reject(new Error(self.imageError))
} else {
self.isLoading = false
resolve(true)
}
}
image.onerror = reject
image.src = src
}
filereader.readAsDataURL(file)
} else {
self.isLoading = false
resolve(true)
}
}
})
},
/**
* 附件上传失败
*/
uploaderror (file) {
this.isLoading = false
this.imageError = \'图片上传失败,请重新上传。\'
}
}
}
</script>
<style lang="scss" scoped>
.image-upload-wrapper .image-upload-item{
display:flex;
flex-direction: column;
}
.image-upload-wrapper .image-upload-content{
margin-top: 10px;
display: flex;
.image-info {
margin-left:10px;
text-align: left;
display:flex;
flex-direction:column;
justify-content:center;
padding: 0 10px;
font-size: 13px;
height: 60px;
}
}
.cut-img-wrapper{
background: #fff;
border: 1px dashed #dcdee2;
display: felx;
width: 58px;
height: 58px;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s;
}
.cut-img-wrapper:hover{
border: 1px dashed #2d8cf0;
}
.default-upload-list{
display: inline-block;
width: 60px;
height: 60px;
text-align: center;
line-height: 60px;
border: 1px solid transparent;
border-radius: 4px;
overflow: hidden;
background: #fff;
position: relative;
box-shadow: 0 1px 1px rgba(0,0,0,.2);
margin-right: 4px;
}
.default-upload-list img{
width: 100%;
height: 100%;
}
.default-upload-list-cover{
display: none;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,.6);
}
.default-upload-list:hover .default-upload-list-cover{
display: block;
}
.default-upload-list-cover i{
color: #fff;
font-size: 20px;
cursor: pointer;
margin: 0 2px;
}
</style>
2.3 使用示例(formOperate.vue/index.vue)
2.3.1 表单示例代码(formOperate.vue)
<template><Form ref="refForm" :model="formData" :rules="rules">
<FormItem label="图片" prop="fileUrl" label-for="fileUrl">
<Input v-show="false" v-model="formData.fileUrl" />
<image-upload
:imageUrl="formData.fileUrl"
:isIconUpload="true"
@imageUploadEmit="imageUploadFunction"
:imageSize="settingImageUpload.maxSize"
:imgWidth="settingImageUpload.width"
:imgHeight="settingImageUpload.height"
:imgSizeTip="settingImageUpload.sizeTip"
/>
</FormItem>
</Form>
</template>
<script>
import ImageUpload from "./common/imageOperate";
export default {
components: {
ImageUpload,
},
data() {
return {
formData: {
fileUrl: "", // 图片路径
},
settingImageUpload: {
width: 100,
height: 100,
maxSize: 100,
sizeTip: "100kb",
},
rules: {
fileUrl: { required: true, message: \'请上传图片\' }
}
};
},
methods: {
/**
* 图片上传成功
*/
imageUploadFunction(val) {
this.formData.originalFileUri = val.originalFileUri;
this.formData.obtainIconUrl = val.fileUrl;
}
},
};
</script>
2.3.2 入口示例代码(index.vue)
<template><div class="medo-wrapper">
<Button type="primary" @click="handleAdd">新建</Button>
<Drawer
:title="`${propOperateData.id ? \'修改\' : \'新增\'}表单`"
v-model="showInfoOperate"
:width="drawerStyles.width"
:styles="drawerStyles.style"
:mask-closable="false"
@on-close="showInfoOperate = false"
>
<form-operate
v-if="showInfoOperate"
:data="propOperateData"
ref="refFormOperate"
/>
<div class="default-drawer-footer">
<Button type="text" @click="showInfoOperate = false"> 关闭 </Button>
<Button type="primary" @click="handleSubmit"> 确定 </Button>
</div>
</Drawer>
</div>
</template>
<script>
import formOperate from \'./formOperate\'
export default {
components: {
formOperate
},
data() {
return {
showInfoOperate: false,
propOperateData: {
id: "",
fileUrl: "",
},
drawerStyles: {
// 内容样式
styles: {
height: \'calc(100% - 55px)\',
overflow: \'auto\',
paddingBottom: \'53px\',
position: \'static\'
},
width: 720
},
};
},
methods: {
handleAdd () {
this.showInfoOperate = true
},
/**
* 提交按钮
*/
handleSubmit() {
this.$refs.refFormOperate.$refs.refForm.validate(validate => {
if (validate) {
console.log(\'验证成功调用接口\')
}
})
}
}
};
</script>
<style lang="scss" scoped>
.default-drawer-footer{
width: 100%;
position: absolute;
bottom: 0;
left: 0;
border-top: 1px solid #e8e8e8;
padding: 10px 16px;
text-align: right;
background: #fff;
}
</style>
2.3 上传代码接口返回:
以上是 VUE 图片上传/剪切(vue-cropper) 的全部内容, 来源链接: utcz.com/z/380095.html