这里看效果图


代码实现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'
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',
}
export function handleChangeUpload(sizeWH: UploadObj, file?: any) {
let uploadObj = {
width: 1000,
height: 1000,
types: ['image/png', 'image/jpg', 'image/gif'],
size: 2,
}
const rawFile = arguments[arguments.length - 1].size ? arguments[arguments.length - 1].raw : arguments[arguments.length - 2].raw
if (rawFile === sizeWH) {
return validation(uploadObj)
} else {
return validation(Object.assign(uploadObj, sizeWH))
}
async function validation(obj: T) {
let success = false
let src = ''
const isType = obj.types.includes(rawFile.type)
const isLt2M = rawFile.size / 1024 / 1024 < obj.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 }
}
}
export const httpRequest = async (request: any, cuttingObject: File) => {
const { action, file, filename } = request
let formData = new FormData()
formData.append(filename, cuttingObject, file.name)
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)
const cropperSrc = ref('')
const licenseUploadRef = ref()
const licenseUploadFile = ref('')
defineExpose({
currentFormRef,
componentFlag: 'info',
})
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
}
}
const httpRequestLicense = async (request: any) => {
submitForm.value.license = await httpRequest(request, licenseUploadFile.value as unknown as File)
}
const uploadImg = (uploadFile: string) => {
licenseUploadFile.value = uploadFile
licenseUploadRef.value.submit()
}
</script>