在项目中,前端开发经常会遇到有图片上传的需求,而别人的组件大多都满足不了当下产品的需求,这是往往我们得去依靠组件自己自定义一个项目通用的裁剪组件,废话不多说,上货…
-
安装依赖:
vue2:vue2:npm install vue-cropper 或 yarn add vue-cropper
vue3:
npm install vue-cropper@next 或 yarn add vue-cropper@next
-
引入相关文件
// main.ts import 'vue-cropper/dist/index.css' // vue3才需要引入样式,vue2不要
注意:这个样式一定记得引啊,我就是忘引入,裁剪框一直弄不出来,白白浪费好几个小时,还烧脑
-
自定义封装裁剪组件
vue2
-
父组件
// 表单中 <template slot="imageUrlForm" slot-scope="scope"> <Cropper :getPropsUrl="getPropsUrl" :imageUrl="imageUrl" :cropOption="cropOption"/> </template>
import Cropper from '@/components/cropper/cropper' export default { // 其他... data() { return { imageUrl:'', // 上传图片地址 cropOption:{ // 如果只是单组件使用的话,可以直接写在cropper组件中,这里是多组件使用,如:头像上传,广告上传,头像与广告的上传框宽高不一样,还有查看弹窗禁用上传功能等 autoCropWidth: '', autoCropHeight: '', width: 448, deleteLeft:230, updateLeft:180, disabled:false } } }, components:{Cropper}, methods: { /** * @函数描述: 父组件获取子组件的值 - 【截图组件】 **/ getPropsUrl(data){ this.imageUrl = data }, } }
-
cropper组件:
<template> <div> <div v-if="imageUrl!==''" class="btnUp1" :style="{'width': cropOption.width + 'px'}"> <img :src="imageUrl" alt=""/> <span v-show="!cropOption.disabled" class="deleteBtn el-icon-delete" @click.stop="deleteFn" :style="{'left': cropOption.deleteLeft + 'px'}"></span> <span class="updateBtn el-icon-zoom-in" @click.stop="()=>updateVisible=true" :style="{'left': cropOption.updateLeft + 'px'}"></span> </div> <div v-else class="btnUp"> <label for="uploads"><i class="el-icon-plus" style="cursor: pointer"></i></label> <input :disabled="cropOption.disabled" type="file" id="uploads" accept="image/png, image/jpeg, image/gif, image/jpg" @change="uploadImg($event, 1)"> </div> <!-- 若有点击图片放大的需求可使用 --> <el-dialog :visible.sync="updateVisible" :close-on-click-modal="false" append-to-body> <img width="100%" :src="imageUrl" alt=""/> </el-dialog> <el-dialog id="appBox" v-if="show" :visible.sync="show" width="800px" :close-on-click-modal="true" append-to-body> <div style="display: flex"> <div class="cut"> <vue-cropper ref="cropper" :img="option.img" :output-size="option.size" :output-type="option.outputType" :info="true" :full="option.full" :fixed="fixed" :fixed-number="fixedNumber" :can-move="option.canMove" :can-move-box="option.canMoveBox" :fixed-box="option.fixedBox" :original="option.original" :auto-crop="option.autoCrop" :auto-crop-width="cropOption.autoCropWidth" :auto-crop-height="cropOption.autoCropHeight" :center-box="option.centerBox" @real-time="realTime" :high="option.high" mode="cover" :max-img-size="option.max" > </vue-cropper> </div> <div class="show-preview" :style="{'width': previews.w + 'px', 'height': previews.h + 'px', 'overflow': 'hidden', 'margin-left': '5px'}"> <div :style="previews.div"> <img :src="previews.url" :style="previews.img" alt=""> </div> </div> </div> <div class="menuBox"> <div class="menuBtn el-icon-zoom-in" @click="changeScale(1)"></div> <div class="menuBtn el-icon-zoom-out" @click="changeScale(-1)"></div> <div class="menuBtn el-icon-back" @click="rotateLeft"></div> <div class="menuBtn el-icon-right" @click="rotateRight"></div> </div> <div class="test-button"> <el-button v-if="!isLoading" class="sureBtn" @click="clickSure" type="primary">确 定</el-button> <el-button v-else class="sureBtn" type="primary" :loading="isLoading">上传中</el-button> <el-button class="closeBtn" @click="closeShow">取 消</el-button> </div> </el-dialog> </div> </template>
import {VueCropper} from 'vue-cropper' import {imageUpload} from "@/components/cropper/api/cropper"; // 上传图片的接口 生成url地址 export default { name: "cropper", components: { VueCropper }, data() { return { previews: {}, option: { img: '', // 上传图片地址 size: 1, // 输出图片大小 full: true, // 是否输出原图比例 outputType: 'png', // 输出图片格式 canMove: true, // 图片是否可移动 fixedBox: true, // 截图框是否固定 original: true, // 图片是否按比例 canMoveBox: false, // 截图框是否可移动 autoCrop: true, // 是否开启截图 // 只有自动截图开启 宽度高度才生效 autoCropWidth: '', // 输出图片宽高 autoCropHeight: '', // 输出图片宽高 centerBox: true, // 是否开启截图框固定宽高 high: true, // 是否开启图片移动 max: 99999 // 输出图片大小限制 }, show: false, // 显示裁剪弹窗 fixed: false, // 上传图片时的过渡效果 fixedNumber: [16, 9], // 截图框固定宽高 imageUrl: '', // 图片地址 updateVisible: false, // 显示预览弹窗 isLoading: false, // 是否在上传 } }, props: ['getPropsUrl', 'imageUrl', 'cropOption'], methods: { closeShow() { this.option.img = '' this.show = false this.getPropsUrl('') document.getElementById('uploads').value = '' }, deleteFn() { this.imageUrl = '' this.getPropsUrl('') }, 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 }, clickSure() { this.isLoading = true // 上传图片时的过渡效果 this.$refs.cropper.getCropBlob(async (data) => { var img = window.URL.createObjectURL(data) const response = await fetch(img); const blobData = await response.blob(); const formData = new FormData(); formData.append('file', blobData, 'filename.png'); imageUpload(formData).then(res => { // 调用图片上传接口,获取url地址 this.isLoading = false this.show = false this.imageUrl = res.data.data.url this.getPropsUrl(res.data.data.url) document.getElementById('uploads').value = '' }); }) }, uploadImg(e, num) { //上传图片 var file = e.target.files[0] this.show = true if (!/\.(gif|jpg|jpeg|png|bmp|GIF|JPG|PNG)$/.test(e.target.value)) { alert('图片类型必须是.gif,jpeg,jpg,png,bmp中的一种,请重新选择') return false } var reader = new FileReader() // reader.onload = (e) => { let data if (typeof e.target.result === 'object') { // 把Array Buffer转化为blob 如果是base64不需要 data = window.URL.createObjectURL(new Blob([e.target.result])) } else { data = e.target.result } if (num === 1) { this.option.img = data } else if (num === 2) { this.example2.img = data } } // 转化为base64 reader.readAsDataURL(file) // 转化为blob // reader.readAsArrayBuffer(file) }, }, } </script>
vue3
-
父组件(html与vue2一样)
<script setup lang="ts"> import {ref, reactive} from "vue"; import Cropper from "@/components/cropper.vue"; /** * 截图配置 */ const cropOption = reactive({ autoCropWidth: 250, autoCropHeight: 250, width: 140, deleteLeft: 80, updateLeft: 28, disabled: false }) const imageUrl= ref<any>('') // 头像 /** * @函数描述: 父组件获取子组件的值 - 【截图组件】 **/ const getPropsUrl = (data: any) => { imageUrl.value = data }
-
cropper组件:
<template> <div id="cropper-container"> <div v-if="imageUrl!==''" class="showImg" :style="{'width': props.cropOption.width + 'px'}"> <img :src="imageUrl" alt=""/> <span v-show="!props.cropOption.disabled" class="powerBtn" @click.stop="deleteFn" :style="{'left': props.cropOption.deleteLeft + 'px'}"> <el-icon> <Delete/> </el-icon> </span> <span class="powerBtn" @click.stop="()=> updateVisible=true" :style="{'left': props.cropOption.updateLeft + 'px'}"> <el-icon> <ZoomIn/> </el-icon> </span> </div> <div v-else class="btnUp"> <label for="uploads"> <el-icon style="cursor: pointer"> <Plus/> </el-icon> </label> <input :disabled="props.cropOption.disabled" type="file" id="uploads" accept="image/png, image/jpeg, image/gif, image/jpg" @change="uploadImg($event, 1)"> </div> <div class="tip">像素长为{{ props.cropOption.autoCropWidth }}px,宽{{ props.cropOption.autoCropHeight }}px</div> <!-- 若有点击图片放大的需求可使用 --> <el-dialog v-if="updateVisible" v-model="updateVisible" :close-on-click-modal="false" append-to-body> <img width="100%" :src="imageUrl" alt=""/> </el-dialog> <el-dialog id="appBox" v-model="show" width="800px" :close-on-click-modal="false" :close-on-press-escape="false" :append-to-body="true"> <div style="display: flex"> <div class="cut"> <vue-cropper ref="cropper" :img="option.img" :output-size="option.size" :output-type="option.outputType" :info="true" :full="option.full" :fixed="fixed" :fixed-number="fixedNumber" :can-move="option.canMove" :can-move-box="option.canMoveBox" :fixed-box="option.fixedBox" :original="option.original" :auto-crop="option.autoCrop" :auto-crop-width="props.cropOption.autoCropWidth" :auto-crop-height="props.cropOption.autoCropHeight" :center-box="option.centerBox" :high="option.high" mode="cover" :max-img-size="option.max" @real-time="realTime" > </vue-cropper> </div> <div class="show-preview" :style="{'width': previews.w + 'px', 'height': previews.h + 'px', 'overflow': 'hidden', 'margin-left': '5px'}"> <div :style="previews.div"> <img :src="previews.url" :style="previews.img" alt=""> </div> </div> </div> <div class="menuBox"> <div class="menuBtn" @click="changeScale(1)"> <el-icon> <ZoomIn/> </el-icon> </div> <div class="menuBtn" @click="changeScale(-1)"> <el-icon> <ZoomOut/> </el-icon> </div> <div class="menuBtn" @click="rotateLeft"> <el-icon> <RefreshLeft/> </el-icon> </div> <div class="menuBtn" @click="rotateRight"> <el-icon> <RefreshRight/> </el-icon> </div> </div> <div class="test-button"> <el-button v-if="!isLoading" class="sureBtn" @click="clickSure" type="primary">确 定</el-button> <el-button v-else class="sureBtn" type="primary" :loading="isLoading">上传中</el-button> <el-button class="closeBtn" @click="closeShow">取 消</el-button> </div> </el-dialog> </div> </template>
<script setup lang="ts"> import {VueCropper} from "vue-cropper" import {Plus, ZoomIn, ZoomOut, RefreshRight, RefreshLeft, Delete} from '@element-plus/icons-vue' import {ref, reactive, computed} from 'vue' import {imageUpload} from "@/components/cropper/api/cropper"; // 上传图片的接口 生成url地址 const props = defineProps(['getPropsUrl', 'imageUrl', 'cropOption']) const cropper = ref(VueCropper) let previews = ref({}) // 即时预览 const option = reactive({ img: '', // 上传图片地址 size: 1, // 输出图片大小 full: true, // 是否输出原图比例 outputType: 'png', // 输出图片格式 canMove: true, // 图片是否可移动 fixedBox: true, // 截图框是否固定 original: true, // 图片是否按比例 canMoveBox: false, // 截图框是否可移动 autoCrop: true, // 是否开启截图 // 只有自动截图开启 宽度高度才生效 autoCropWidth: '', // 输出图片宽高 autoCropHeight: '', // 输出图片宽高 centerBox: true, // 是否开启截图框固定宽高 high: true, // 是否开启图片移动 max: 99999 // 输出图片大小限制 }) const show = ref(false) // 显示裁剪弹窗 const isLoading = ref(false) // 上传图片时的过渡效果 const updateVisible = ref(false) // 显示预览弹窗 const fixed = ref(false) // 是否开启截图框固定宽高 const imageUrl = computed(() => props.imageUrl) // 图片地址 const fixedNumber = ref([16, 9]) // 截图框固定宽高 /** * 关闭弹窗 */ const closeShow = () => { option.img = '' show.value = false props.getPropsUrl('') } /** * 删除图片 */ const deleteFn = () => { props.getPropsUrl('') } /** * 图片缩放 * @param num */ const changeScale = (num: any) => { num = num || 1 cropper.value.changeScale(num) } /** * 图片旋转 - 顺时针 */ const rotateLeft = () => { cropper.value.rotateLeft() } /** * 图片旋转 - 逆时针 */ const rotateRight = () => { cropper.value.rotateRight() } /** * 实时预览函数 * @param data */ const realTime = (data: any) => { previews.value = data } /** * 确定按钮 */ const clickSure = () => { isLoading.value = true // 上传图片时的过渡效果 cropper.value.getCropBlob(async (data: any) => { const imgUrl = window.URL.createObjectURL(data); const response = await fetch(imgUrl); const blobData = await response.blob(); const formData = new FormData(); formData.append('file', blobData, 'filename.png'); imageUpload(formData).then(res => { isLoading.value = false show.value = false props.getPropsUrl(res.data.data.url) }); }) } /** * 上传图片 * @param e * @param num */ const uploadImg = (e: any, num: any) => { var file = e.target.files[0] show.value = true console.log(file, show.value) if (!/\.(gif|jpg|jpeg|png|bmp|GIF|JPG|PNG)$/.test(e.target.value)) { alert('图片类型必须是.gif,jpeg,jpg,png,bmp中的一种,请重新选择') return false } var reader = new FileReader() reader.onload = (e: any) => { let data if (typeof e.target.result === 'object') { // 把Array Buffer转化为blob 如果是base64不需要 data = window.URL.createObjectURL(new Blob([e.target.result])) } else { data = e.target.result } if (num === 1) { option.img = data } else if (num === 2) { example2.value.img = data } } // 转化为base64 // reader.readAsDataURL(file) // 转化为blob reader.readAsArrayBuffer(file); } </script>
vue2与vue3样式一样
<style scoped> * { margin: 0; padding: 0; } .cut { width: 480px; height: 300px; flex-shrink: 0; display: flex; margin-bottom: 20px; box-sizing: border-box; overflow: hidden; } .cropperModal { width: 4636px; height: 300px; } .c-item { max-width: 800px; margin: 20px auto 10px; } .content { max-width: 1200px; margin: auto auto 100px; } .test-button { display: flex; align-content: center; justify-content: flex-end; } .btnUp { width: 200px; height: 140px; display: inline-block; border-radius: 5px; border: 1px solid #DCDFE6; box-sizing: border-box; background-color: #fbfdff; cursor: pointer; text-align: center; line-height: 140px; font-size: 20px; color: #DCDFE6; position: relative; } .btnUp i { width: 100%; height: 100%; display: flex; position: absolute; top: 0; left: 0; align-items: center; justify-content: center; } .showImg { height: 140px; display: inline-block; border-radius: 5px; border: 1px solid #DCDFE6; box-sizing: border-box; background-color: #fbfdff; cursor: pointer; text-align: center; line-height: 140px; font-size: 20px; color: #DCDFE6; position: relative; } .showImg img { width: 100%; height: 100%; } .showImg:hover .powerBtn { visibility: visible; } #uploads { position: absolute; clip: rect(0 0 0 0) } .tip { color: #8c939d; display: flex; justify-content: center; font-size: 12px; } .powerBtn { visibility: hidden; position: absolute; font-size: 24px; width: 24px; height: 24px; color: #fefeff; cursor: pointer; } .sureBtn { display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; text-align: center; box-sizing: border-box; outline: none; margin: 20px 10px 0 0; padding: 9px 15px; font-size: 14px; border-radius: 4px; color: #fff; background-color: #409EFF; border-color: #409EFF; transition: all .2s ease; text-decoration: none; user-select: none; } .closeBtn { display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; background: #fff; border: 1px solid #c0ccda; color: #1f2d3d; text-align: center; box-sizing: border-box; outline: none; margin: 20px 10px 0 0; padding: 9px 15px; font-size: 14px; border-radius: 4px; transition: all .2s ease; text-decoration: none; user-select: none; } .menuBox { display: flex; box-sizing: border-box; align-items: center; height: 30px; width: 460px; justify-content: center; } .menuBtn { white-space: nowrap; cursor: pointer; background: #ff4d51; color: #fff; text-align: center; box-sizing: border-box; outline: none; margin-right: 10px; padding: 9px 15px; font-size: 14px; border-radius: 4px; transition: all .2s ease; text-decoration: none; user-select: none; height: 30px; width: 50px; } .des { line-height: 30px; } code.language-html { padding: 10px 20px; margin: 10px 0; display: block; background-color: #333; color: #fff; overflow-x: auto; font-family: Consolas, Monaco, Droid, Sans, Mono, Source, Code, Pro, Menlo, Lucida, Sans, Type, Writer, Ubuntu, Mono; border-radius: 5px; white-space: pre; } .show-info { margin-bottom: 50px; } .show-info h2 { line-height: 50px; } .title { display: block; text-decoration: none; text-align: center; line-height: 1.5; margin: 20px 0px; background-image: -webkit-linear-gradient(left, #3498db, #f47920 10%, #d71345 20%, #f7acbc 30%, #ffd400 40%, #3498db 50%, #f47920 60%, #d71345 70%, #f7acbc 80%, #ffd400 90%, #3498db); color: transparent; -webkit-background-clip: text; background-size: 200% 100%; animation: slide 5s infinite linear; font-size: 40px; } .test { height: 500px; } .model { position: fixed; z-index: 10; width: 100vw; height: 100vh; overflow: auto; top: 0; left: 0; background: rgba(0, 0, 0, 0.8); } .model-show { display: flex; justify-content: center; align-items: center; width: 100vw; height: 100vh; } .model img { display: block; margin: auto; max-width: 80%; user-select: none; background-position: 0 0, 10px 10px; background-size: 20px 20px; background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee 100%), linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%); } .c-item { display: block; user-select: none; } @keyframes slide { 0% { background-position: 0 0; } 100% { background-position: -100% 0; } } </style>
好了,以上就是vue2与vue3结合vue-cropper的图片裁剪组件的封装,下次结合饿了么el-upload再进行封装,欢迎评论留言!!