基于element-ui的el-upload和vue3.2+vue-cropper封装一个上传图片的组件

983 阅读1分钟

这里看效果图

Snipaste_2022-07-13_10-50-28.png

Snipaste_2022-07-13_10-50-56.png

代码实现my-scopper组件

```<!-- eslint-disable vue/no-mutating-props -->
<!--
 * @description: 
 * @Author: Song_Bing_Yan
 * @Date: 2022-07-12 13:24:00
 * @LastEditors: Song_Bing_Yan
 * @LastEditTime: 2022-07-13 09:28:47
-->
<!--
   $prop 
   isShow: 弹窗的显示
   imgSrc: 图片的url
 -->
<!-- 
  $emit
  'update-is-show':修改弹窗显示状态,
  'upload-img':最终上传的图片
   -->
<script setup lang="ts">
import { ref, reactive, defineProps, defineEmits } from 'vue'
import {
    Plus,
    Sort,
    Switch,
    Refresh,
    ZoomIn,
    RefreshRight,
    ZoomOut,
    ArrowLeft,
    ArrowRight,
    ArrowUp,
    ArrowDown,
    RefreshLeft,
} from '@element-plus/icons-vue'
import uuid from '@/libs/uuid'
/*
 *variable
 */
const $props = defineProps({
    isShow: { type: Boolean, default: false },
    imgSrc: { type: String, default: '' },
})
const cropperParam = reactive({
    imgSrc: $props.imgSrc,
    cropImg: '',
})
const cropper = ref()
const FlipY = ref()
const FlipX = ref()
const $emit = defineEmits(['update-is-show', 'upload-img'])
/*
 *lifeCircle
 */
/*
 *function
 */

/**
 * @LastEditors: Song_Bing_Yan
 * @description: 设置翻转
 * @returns {*}
 */
const flipX = () => {
    const dom = FlipX.value
    let scale = dom.getAttribute('data-scale')
    scale = scale ? -scale : -1
    cropper.value.scaleX(scale)
    dom.setAttribute('data-scale', scale)
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 设置翻转
 * @returns {*}
 */
const flipY = () => {
    const dom = FlipY.value
    let scale = dom.getAttribute('data-scale')
    scale = scale ? -scale : -1
    cropper.value.scaleY(scale)
    dom.setAttribute('data-scale', scale)
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 移动图片
 * @param {*} offsetX
 * @param {*} offsetY
 * @returns {*}
 */
const move = (offsetX: number, offsetY: number) => {
    cropper.value.move(offsetX, offsetY)
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 重置
 * @returns {*}
 */
const reset = () => {
    cropper.value.reset()
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 角度旋转
 * @param {*} deg 角度
 * @returns {*}
 */
const rotate = (deg: number) => {
    cropper.value.rotate(deg)
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 图片放大
 * @param {*} percent
 * @returns {*}
 */
const zoom = (percent: number) => {
    cropper.value.relativeZoom(percent)
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 剪切好的图片
 * @returns {*}
 */
const cropImage = () => {
    cropperParam.cropImg = cropper.value.getCroppedCanvas().toDataURL()
}

/**
 * @LastEditors: Song_Bing_Yan
 * @description: 处理完成
 * @returns {*}
 */
const handleFinish = () => {
    // 获取截图的base64 数据
    cropperParam.cropImg = cropper.value.getCroppedCanvas().toDataURL()
    function dataURLtoFile(dataurl: string, filename: string) {
        let arr = dataurl.split(',')
        let mime = arr[0].match(/:(.*?);/)![1]
        let suffix = mime.split('/')[1]
        let bstr = window.atob(arr[1])
        let n = bstr.length
        let u8arr = new Uint8Array(n)
        while (n--) {
            u8arr[n] = bstr.charCodeAt(n)
        }
        return new File([u8arr], `${filename}.${suffix}`, {
            type: mime,
        })
        //将base64转换为文件
    }
    const file = dataURLtoFile(cropperParam.cropImg, uuid(10))
    $emit('upload-img', file)
    // $emit('upload-img', URL.createObjectURL(file))
    $emit('update-is-show', false)
}
</script>
<template>
    <el-dialog :model-value="$props.isShow" width="50%" title="图片裁剪" @close="$emit('update-is-show', false)">
        <div class="cropper-container">
            <div class="content">
                <section class="cropper-area">
                    <div class="img-cropper">
                        <vue-cropper
                            ref="cropper"
                            :ontainer-style="{ width: '400px', height: '400px' }"
                            output-type="jpeg"
                            overflow-hidden
                            :src="cropperParam.imgSrc"
                            preview=".preview"
                            :min-container-height="400"
                            background
                        />
                        <el-button-group class="btnGroup">
                            <el-button size="large" :icon="ZoomIn" @click="zoom(0.2)" />
                            <el-button size="large" :icon="ZoomOut" @click="zoom(-0.2)" />
                            <el-button size="large" :icon="ArrowLeft" @click="move(-10, 0)" />
                            <el-button size="large" :icon="ArrowRight" @click="move(10, 0)" />
                            <el-button size="large" :icon="ArrowUp" @click="move(0, -10)" />
                            <el-button size="large" :icon="ArrowDown" @click="move(0, 10)" />
                            <el-button size="large" :icon="Switch" class="flipX" @click="flipX" />
                            <el-button size="large" :icon="Sort" @click="flipY" />
                            <a ref="FlipX" href="#" class="inita" />
                            <a ref="FlipY" href="#" class="inita" />
                            <el-button size="large" :icon="RefreshLeft" @click="rotate(90)" />
                            <el-button size="large" :icon="RefreshRight" @click="rotate(-90)" />
                        </el-button-group>
                    </div>
                </section>
                <!-- 预览框 s  -->
                <section class="preview-area">
                    <p>预览</p>
                    <div class="preview" />
                    <!-- <p>效果展示</p>
                    <div class="cropped-image">
                        <img v-if="cropperParam.cropImg" :src="cropperParam.cropImg" alt="裁剪图片" />
                        <div v-else class="crop-placeholder" />
                    </div> -->
                </section>
            </div>
        </div>

        <template #footer>
            <span class="dialog-footer">
                <el-button size="large" :icon="Refresh" @click="reset">重置</el-button>
                <el-button size="large" @click="$emit('update-is-show', false)">取消</el-button>
                <el-button size="large" type="primary" @click="handleFinish">确定</el-button>
            </span>
        </template>
    </el-dialog>
</template>
<style>
.cropper-container {
    width: 900px;
    margin: 0 auto;
}
.content {
    display: flex;
    justify-content: space-between;
}
.cropper-area {
    width: 614px;
    margin-right: 30px;
}

.btnGroup {
    width: 100%;
    display: flex;
    justify-content: center;
    margin-top: 20px;
}
.preview-area {
    width: 307px;
}

.preview-area p {
    font-size: 1.25rem;
    color: #000;
    font-weight: 700;
    margin: 0;
    margin-bottom: 1rem;
}

.preview-area p:last-of-type {
    margin-top: 1rem;
}

.preview {
    width: 100%;
    height: calc(372px * (9 / 16));
    overflow: hidden;
}

.crop-placeholder {
    width: 100%;
    height: 200px;
    background: #ccc;
}

.cropped-image img {
    max-width: 100%;
}
</style>

校验函数用于upload组件上传前校验onchange+befor-apload钩子都可用 '@/utils/Upload'

/*
 * @description:
 * @Author: Song_Bing_Yan
 * @Date: 2022-07-09 16:30:43
 * @LastEditors: Song_Bing_Yan
 * @LastEditTime: 2022-07-13 11:04:06
 */
import { ElMessage } from 'element-plus'
import axios from 'axios'

interface UploadObj {
    width?: number
    height?: number
    types?: Array<keyof typeof ImageType>
    size?: number
}
interface T {
    width: number
    height: number
    types: Array<string>
    size: number
}
enum ImageType {
    'image/bmp',
    'image/jpg',
    'image/png',
    'image/tif',
    'image/gif',
    'image/pcx',
    'image/tga',
    'image/exif',
    'image/fpx',
    'image/svg',
    'image/psd',
    'image/cdr',
    'image/pcd',
    'image/dxf',
    'image/ufo',
    'image/eps',
    'image/ai',
    'image/raw',
    'image/WMF',
    'image/webp',
    'image/avif',
    'image/apng',
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 检验是否合法
 * @param {UploadObj} sizeWH
 * @returns {*}
 */
export function handleChangeUpload(sizeWH: UploadObj, file?: any) {
    let uploadObj = {
        width: 1000,
        height: 1000,
        types: ['image/png', 'image/jpg', 'image/gif'],
        size: 2,
    }
    // 不管什么情况下 rawFile 永远都是文件对象
    // eslint-disable-next-line prefer-rest-params
    const rawFile = arguments[arguments.length - 1].size ? arguments[arguments.length - 1].raw : arguments[arguments.length - 2].raw
    if (rawFile === sizeWH) {
        //  说明没传参数
        return validation(uploadObj)
    } else {
        // 说明传le参数
        return validation(Object.assign(uploadObj, sizeWH))
    }
    async function validation(obj: T) {
        let success = false
        let src = ''
        const isType = obj.types.includes(rawFile.type) // 上传的文件类型在不在types中
        const isLt2M = rawFile.size / 1024 / 1024 < obj.size // 上传的文件大小在不在size范围
        if (!isType) {
            ElMessage.error(`请上传${obj.types.join(',')}格式`)
            return { success, src }
        }
        if (!isLt2M) {
            ElMessage.error(`传输文件大小在${obj.size}MB以内!`)
            return { success, src }
        }
        let img = new Image()
        const isSize = await new Promise(function (resolve) {
            let _URL = window.URL || window.webkitURL
            img.src = _URL.createObjectURL(rawFile)
            img.onload = function () {
                let valid = img.width < obj.width && img.height < obj.height
                valid ? resolve(valid) : resolve(valid)
            }
        })
        if (!isSize) {
            ElMessage.error(`上传图片像素要小于${obj.width}*${obj.height}!`)
            return { success, src }
        }
        success = isType && (isSize as boolean) && isLt2M
        src = img.src
        return { success, src }
    }
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 手动上传
 * @param {any} request
 * @param {any} cuttingObject 裁剪后的文件
 * @returns {*}  ImgUrl
 */
export const httpRequest = async (request: any, cuttingObject: File) => {
    const { action, file, filename } = request
    let formData = new FormData()
    formData.append(filename, cuttingObject, file.name)
    // 上传中的loading
    const {
        data: { data, code },
    } = await axios({
        headers: {
            contentType: 'multipart/form-data', // 需要指定上传的方式
        },
        url: action,
        method: 'post',
        data: formData,
        timeout: Number(import.meta.env.VITE_REQUEST_TIME_OUT), // 防止文件过大超时
    })
    if (code) {
        ElMessage.success('上传成功')
        return data
    }
}

upload组件上面使用示例

 <el-upload
          ref="licenseUploadRef"
        :on-change="handleChangeUplicense"
         :action="uploadUrl"
        class="avatar-uploader"
       :auto-upload="false"
    :http-request="httpRequestLicense"
      :show-file-list="false"
                >
                    <img v-if="submitForm.license" :src="submitForm.license" class="avatar" />
                    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
    </el-upload>
                ---------裁切组件----------
    <my-cropper
        v-if="cropperShow"
        :img-src="cropperSrc"
        :is-show="cropperShow"
        @update-is-show="cropperShow = $event"
        @upload-img="uploadImg"
    >
    </my-cropper>
    <script setup lang="ts">
    import { handleChangeUpload, httpRequest } from '@/utils/Upload'
import { reactive, inject, ref } from 'vue'
import MyCropper from '@/components/my-cropper/my-cropper.vue'
// 裁切组件显示
const cropperShow = ref(false)
// 裁切组件Src
const cropperSrc = ref('')
// (许可证)上传组件实例
const licenseUploadRef = ref()
// 裁剪后的文件对象
const licenseUploadFile = ref('')
defineExpose({
    currentFormRef,
    componentFlag: 'info',
})

/**
 * @LastEditors: Song_Bing_Yan
 * @description: 添加校验许可证(许可证)
 * @param {*} file 图片文件
 * @returns {*}
 */
const handleChangeUplicense = async (file: File) => {
    if (file.status !== 'ready') return
    const { src, success } = await handleChangeUpload({}, file)
    if (success && src) {
        cropperSrc.value = src
        cropperShow.value = true
    }
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: submit触发此方法(许可证)
 * @returns {*}
 */
const httpRequestLicense = async (request: any) => {
// 后端返回的url回显到页面
    submitForm.value.license = await httpRequest(request, licenseUploadFile.value as unknown as File)
}
/**
 * @LastEditors: Song_Bing_Yan
 * @description: 上传图片(许可证)
 * @param {*} uploadFile
 * @returns {*}
 */
const uploadImg = (uploadFile: string) => {
    licenseUploadFile.value = uploadFile
    licenseUploadRef.value.submit()
}
</script>