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 上傳代碼接口返回: