cropperjs vue3.2 +ts +elment-plus实现图片裁剪上传功能 (复制可用)

特别鸣谢

插件github地址:https://github.com/fengyuanchen/cropper/blob/master/README.md
插件在线演示:https://fengyuanchen.github.io/cropperjs/
我是基于这个作者代码改的:https://blog.csdn.net/weixin_41897680/article/details/123606376
这位哥文章有中文文档:https://blog.csdn.net/weixin_46212682/article/details/122339262
cropperjs上传文件时文件名称问题:https://blog.csdn.net/weixin_43856603/article/details/103993434

最近产品提了一个需求,就是实现图片裁剪上传。去找了几个裁剪插件
最多用的是vue-cropper@next 但是发现有点bug 图片裁剪的时候只能拖动一边
image
只有这个角落能够拖动 太鸡肋了。
遂换了一个 能够满足需求 cropper.js
接下来看一下我的实现效果
image

elmentui-plus有公司统一特别定制,所以你们复制代码后弹窗样式可能会不太一样,不过也大差不差

公司上传的文件是二进制文件 如果是其他格式 转码一下即可

npm install cropperjs
<template>
    <div>
        <el-image class="upload" v-if="imgUrl" :src="imgUrl" fit="contain" @click="openUpload"></el-image>
        <el-icon v-else class="avatar-uploader-icon" @click="openUpload"><Plus /></el-icon>
        <!-- 上传弹窗 -->
        <el-dialog v-model="uploadDialog" :title="title" width="800px" :before-close="handleClose">
            <div class="container">
                <!-- 左侧裁剪区 -->
                <div class="left">
                    <!-- 大图显示区 -->
                    <!-- :style="{ 'background-image': 'url(' + imgUrl + ')' }" -->
                    <div class="big-image-preview">
                        <img v-if="imgUrl" :src="imgUrl" class="big-image" ref="imageRef" />
                        <div v-else class="big-image" @click="chooseImage" />
                    </div>
                    <div class="tool">
                        <p>{{ tips }}</p>
                        <el-button @click="chooseImage">上传图片</el-button>
                        <el-button @click="chooseImage">更换图片</el-button>
                        <el-button @click="zoomImage(0.2)">放大</el-button>
                        <el-button @click="zoomImage(-0.2)">缩小</el-button>
                        <el-button @click="rotateImage(90)">左转90°</el-button>
                        <el-button @click="rotateImage(90)">右转90°</el-button>
                    </div>
                </div>
                <!-- 右侧预览区 -->
                <div class="right">
                    <!-- 小图预览区域 -->
                    <div class="right-top">
                        <div class="image-view"></div>
                        <div class="view-info">
                            <div style="margin-bottom: 20px">
                                <span>比例:</span>
                                <el-input-number
                                    :controls="false"
                                    v-model="heightScale"
                                    style="width: 50px"
                                    @change="proportionFn"
                                    placeholder="长"
                                ></el-input-number>
                                -
                                <el-input-number
                                    :controls="false"
                                    v-model="widthScale"
                                    style="width: 50px; margin-right: 20px"
                                    @change="proportionFn"
                                    placeholder="宽"
                                ></el-input-number>
                                <el-button @click="proportionCloseFn">X</el-button>
                            </div>
                            <div style="margin-bottom: 20px">
                                <span>宽度:</span>
                                <el-input-number
                                    v-model="cropperWidth"
                                    :min="1"
                                    @change="setcropperHW"
                                ></el-input-number>
                                px
                            </div>
                            <div>
                                <span>高度:</span>
                                <el-input-number
                                    v-model="cropperHeight"
                                    :min="1"
                                    @change="setcropperHW"
                                ></el-input-number>
                                px
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <!-- 只用input来实现上传,但是不显示input -->
            <input v-show="false" ref="fileRef" type="file" accept="image/png, image/jpeg" @change="getImageInfo" />
            <template #footer>
                <el-button @click="handleClose">取消</el-button>
                <el-button type="primary" @click="submitImage">确定</el-button>
            </template>
        </el-dialog>
    </div>
</template>

<script setup>
import { nextTick, ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const props = defineProps({
    imgUrl: { // 回显需求
        type: String,
        default: ''
    },
    title: { //弹框标题
        type: String,
        default: '上传图片'
    },
    tips: {  //图片下的提示语
        type: String,
        default: ''
    },
    api: { //上传的api
        type: String,
        default: '/api/apps/common/Index/file'
    },
    size: {  //允许上传最大图片MB
        type: Number,
        default: 10000
    }
})
// 默认显示的图片
const imgUrl = ref(props.imgUrl)
const tips = ref(props.tips)
const isCreate = ref(false)
// 裁剪对象
const cropper = ref(null)
const title = ref(props.title)
const api = ref(props.api)
const size = ref(props.size)
const http = axios.create()
const env = import.meta.env
const info = ref()
const imageRef = ref(null)
const cropperHeight = ref()
const cropperWidth = ref()
const widthScale = ref()
const heightScale = ref()
const uploadDialog = ref(false)
const fileRef = ref(null)
const fileName = ref()
const emit = defineEmits(['upload'])
// 打开弹窗方法
const openUpload = () => {
    uploadDialog.value = true
    if (imgUrl.value) {
        cropper.value.setData(info.value)
    }
}
// 关闭弹窗
const handleClose = () => {
    uploadDialog.value = false
}
// 选择图片
const chooseImage = () => {
    // 当input的type属性值为file时,点击方法可以进行选取文件
    fileRef.value.click()
}
// 上传头像
const submitImage = () => {
    const cas = cropper.value.getCroppedCanvas()
    // const base64url = cas.toDataURL('image/jpeg')
    cas.toBlob(function (e) {
        imgUrl.value = window.URL.createObjectURL(e)
    })
    info.value = cropper.value.getData()
    uploadDialog.value = false
}
// 上传
const uploadImage = () => {
    const ca = cropper.value.getCroppedCanvas()
    const formData = new FormData()
    ca.toBlob(function (e) {
        formData.append('file', e ,fileName.vallue)
        http.post(api.value, formData, {
            baseURL: env.VITE_BASE_URL, // 基础路径
            headers: {
                'Content-Type': 'multipart/form-data', // 关键
                token: sessionStorage.getItem('token')
            }
        })
            .then((res) => {
                if (res.status == 200) {
                    emit('upload', res.data)
                }
            })
            .catch((error) => {
                console.log(error)
            })
    })
}
// 获取文件信息
const getImageInfo = (e) => {
    // 上传的文件
    const file = e.target.files[0]
    const fileSize = (file.size / 1024).toFixed(2)
    if (fileSize > size.value * 1024) {
        ElMessage.warning(`'图片大小必须在${size.value}MB以内!'`)
        return false
    }
    fileName.vallue = file.name
    // 获取 window 的 URL 工具
    const URL = window.URL || window.webkitURL
    // 通过 file 生成目标 url
    imgUrl.value = URL.createObjectURL(file)
    // console.log('图片:', imgUrl);
    nextTick(() => {
        // 判定裁剪对象是否存在
        // 存在销毁重新创建(这里不替换图片,图片不一样大时会变形),不存在创建对象
        if (cropper.value) {
            cropper.value.destroy()
            cropImage()
        } else {
            cropImage()
        }
        isCreate.value = true
    })
}
// 裁剪图片
const cropImage = () => {
    if (imageRef.value) {
        cropper.value = new Cropper(imageRef.value, {
            // 宽高比
            aspectRatio: widthScale.value / heightScale.value, //设置裁剪框为固定的宽高比
            // initialAspectRatio: [100, 200],
            viewMode: 1,
            // 预览
            preview: '.image-view',
            // cropBoxResizable: false,
            background: false,
            crop(event) {
                cropperHeight.value = parseInt(event.detail.height)
                cropperWidth.value = parseInt(event.detail.width)
            }
        })
        isCreate.value = true
    }
}
// 旋转
const rotateImage = (angle) => {
    if (isCreate.value) {
        cropper.value.rotate(angle)
    }
}
// 缩放
const zoomImage = (num) => {
    if (isCreate.value) {
        cropper.value.zoom(num)
    }
}
const setcropperHW = () => {
    if (isCreate.value) {
        cropper.value.setData({
            height: cropperHeight.value ? +cropperHeight.value : 0,
            width: cropperWidth.value ? +cropperWidth.value : 0
        })
    }
}
const proportionFn = () => {
    if (isCreate.value) {
        if (widthScale.value && heightScale.value) {
            cropper.value.destroy()
            cropImage()
        } else if (!widthScale.value && !heightScale.value) {
            cropper.value.destroy()
            cropImage()
        }
    }
}
const proportionCloseFn = () => {
    if (isCreate.value) {
        widthScale.value = null
        heightScale.value = null
        cropper.value.destroy()
        cropImage()
    }
}
defineExpose({
    uploadImage
})
</script>

<style scoped lang="scss">
//上传的基本样式
.upload {
    width: 142px;
    height: 142px;
    // border: 5px solid #eeeeee;
    box-sizing: border-box;
    cursor: pointer;
    background-position: center center;
    background-size: 100%;
    border-radius: 6px;
    background: #eee;
}

//hover的基本样式
.base-hover {
    position: absolute;
    width: 100%;
    height: 100%;
    content: '更换头像';
    background: black;
    color: #ffffff;
    display: flex;
    justify-content: center;
    align-items: center;
    opacity: 0.6;
}

.container {
    width: 800px;
    height: 500px;
    display: flex;
    margin: 20px 20px 0px;
    .left {
        width: 65%;
        height: 100%;
        .big-image-preview {
            width: 100%;
            height: 85%;
            background-size: 100% 100%;
            background-repeat: no-repeat;
            background-position: center center;
            border: 1px solid #999;
        }
        .tool {
            width: 100%;
            height: 15%;
            font-size: 10px;
            display: flex;
            justify-content: center;
            align-items: center;
            span {
                margin: 0px 10px;
                cursor: pointer;
            }
        }
        .big-image {
            width: 100%;
            height: 100%;
            display: block;
            max-width: 100%;
        }
    }
    .right {
        width: 240px;
        height: 100%;
        font-size: 14px;
        .right-top {
            width: 100%;
            height: 70%;
            display: flex;
            flex-direction: column;
            align-items: center;
            .image-view {
                // border: 1px solid gray;
                overflow: hidden;
                min-width: 200px;
                height: 200px;
            }
            .view-info {
                position: absolute;
                top: 340px;
            }
        }
        .right-bottom {
            width: 100%;
            height: 30%;
            display: flex;
            flex-direction: column-reverse;
            align-items: center;
        }
    }
}
:deep(.el-icon.avatar-uploader-icon) {
    font-size: 28px;
    color: #8c939d;
    width: 142px;
    height: 142px;
    text-align: center;
    border: 1px dashed var(--el-border-color);
    border-radius: 6px;
}
:deep(.cropper-point.point-se) {
    height: 5px;
    width: 5px;
}
</style>

引用

<template>
    <image-cropper ref="imageCropperRef" @upload="uploadBack"></image-cropper>
    <el-button @click="upload">上传</el-button>
</template>

<script setup>
import { ref } from 'vue'
import imageCropper from 'base/src/components/imageCropper.vue'
const imageCropperRef = ref(null)
const upload = () => {
    imageCropperRef.value.uploadImage() //上传钩子
}
const uploadBack = (val) => {
    console.log(val) //上传完成回传
}
</script>

<style scoped lang="scss"></style>
posted @ 2023-01-16 11:27  Chaplink  阅读(1220)  评论(1编辑  收藏  举报