web-admin/src/components/upload/cropperImg.vue

369 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="gva-cropper-btn">
<label :class="`btn btn-${props.btnType}`" :for="`uploads_${props.name}`">
<el-icon>
<Crop />
</el-icon>
<span>裁剪上传</span>
</label>
<input type="file" :id="`uploads_${props.name}`" style="display: none;"
accept="image/png, image/jpeg, image/gif, image/jpg, image/webp" @change="handleSelectImg($event)">
</div>
<el-drawer v-model="showDrawer" title="上传头像" direction="ltr" :show-close="false" :close-on-press-escape="false"
:close-on-click-modal="false" destroy-on-close append-to-body
:size="Math.max(800, Math.min(1500, props.imgWidth + 140))">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">裁剪上传图片</span>
<div>
<el-button type="primary" @click="handleConfirm('blob')">确 定</el-button>
<el-button @click="showDrawer = false">取 消</el-button>
</div>
</div>
</template>
<div class="gva-cropper-content flex">
<div class="cropper"
:style="{ 'width': Math.max(760, Math.min(1500, props.imgWidth + 100)) + 'px', 'min-height': Math.max(Math.min(props.imgHeight + 200, 900), 500) + 'px' }">
<div class="gva-btn-list">
<el-button @click="changeScale(1)">
<el-icon>
<ZoomIn />
</el-icon>
</el-button>
<el-button @click="changeScale(-1)">
<el-icon>
<ZoomOut />
</el-icon>
</el-button>
<el-button @click="rotateLeft">
<el-icon>
<RefreshLeft />
</el-icon>
</el-button>
<el-button @click="rotateRight">
<el-icon>
<RefreshRight />
</el-icon>
</el-button>
</div>
<vue-cropper v-if="showDrawer" ref="vueCropperRef" :outputSize="1" outputType="png" :img="vueCropperImg"
:info="true" :full="true" autoCrop :centerBox="props.cropType === 'fixed' ? true : false"
:autoCropWidth="props.imgWidth" :autoCropHeight="props.imgHeight"
:fixedBox="props.cropType === 'fixed' ? true : false" :fixedNumber="props.fixedNumber" :maxImgSize="2000" />
</div>
</div>
</el-drawer>
</template>
<script setup>
import { ref } from 'vue'
import VueCropper from "@/components/vueCropper";
import { ElMessage } from 'element-plus'
import axios from 'axios'
defineOptions({
name: 'CropperImg',
})
const showDrawer = ref(false)
const vueCropperImg = ref(null)
const vueCropperRef = ref(null)
const props = defineProps({
imgWidth: {
required: true,
type: Number
},
imgHeight: {
required: true,
type: Number
},
category: {
required: true,
type: String
},
name: {
type: String,
default: 'media'
},
cropType: {
type: String,
default: 'fixed'
},
outputSize: {
type: Number,
default: 1
},
fixed: {
type: Boolean,
default: true
},
fixedNumber: {
type: Array,
default: function () {
return [1, 1]
}
},
btnType: {
type: String,
default: 'default'
},
})
const emits = defineEmits(['on-success', 'update:modelValue'])
const changeScale = (num) => {
num = num || 1
vueCropperRef.value.changeScale(num)
}
const rotateLeft = () => {
vueCropperRef.value.rotateLeft()
}
const rotateRight = () => {
vueCropperRef.value.rotateRight()
}
// 实时预览函数
// const realTime = (data) => {
// previews.value = data
// }
const handleConfirm = (type) => {
// 输出
if (type === 'blob') {
vueCropperRef.value.getCropBlob(async (data) => {
doConfirm(data)
})
} else {
vueCropperRef.getCropData(async (data) => {
doConfirm(data)
})
}
}
// 判断是哪种缩放方式
const doConfirm = (cropData) => {
const originalBlob = cropData // 原始 Blob 对象
const desiredWidth = props.imgWidth// 所需的宽度
const desiredHeight = props.imgHeight // 所需的高度
if (props.cropType == 'fixed') {
// 如果是需要固定宽高的图片,直接调用方法缩放图片
handleResizeBlob(originalBlob, desiredWidth, desiredHeight)
} else if (props.cropType == 'max') {
// 如果是需要不超过xxx宽高的图片先做判断
var blob = new Blob([cropData], { type: 'image/png' })
var img = new Image()
var url = window.URL.createObjectURL(blob)
img.src = url
img.onload = function () {
// 获取图像的宽度和高度
var width = img.width
var height = img.height
if (width <= desiredWidth && height <= desiredHeight) {
// 如果裁剪完未超过限制,则直接去上传
uploadImage(cropData)
} else {
// 如果裁剪完超过限制,则调用方法缩放图片
handleResizeBlob(originalBlob, desiredWidth, desiredHeight)
}
// 清理 URL 对象
window.URL.revokeObjectURL(url)
}
}
}
// 缩放图片
const resizeBlob = (blob, desiredWidth, desiredHeight) => {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 计算缩放比例
const scaleX = desiredWidth / img.width
const scaleY = desiredHeight / img.height
const scale = Math.min(scaleX, scaleY)
// 设置 Canvas 的宽度和高度
canvas.width = desiredWidth
canvas.height = desiredHeight
// 绘制图片到 Canvas 上,并进行缩放
ctx.drawImage(img, 0, 0, img.width * scale, img.height * scale)
// 将 Canvas 中的图像转换为 Blob 对象
canvas.toBlob((resizedBlob) => {
resolve(resizedBlob)
}, blob.type)
}
img.onerror = (error) => {
reject(error)
}
img.src = window.URL.createObjectURL(blob)
})
}
// 缩放后上传
const handleResizeBlob = (originalBlob, desiredWidth, desiredHeight) => {
resizeBlob(originalBlob, desiredWidth, desiredHeight)
.then((resizedBlob) => {
uploadImage(resizedBlob)
})
.catch((error) => {
console.error('Error resizing Blob:', error)
})
}
const uploadImage = (data) => {
const formData = new FormData();
formData.append('file', data, props.name + '_image.png'); // 假设使用'image.jpg'作为上传的文件名
const path = import.meta.env.VITE_BASE_API + '/cms/mediaFile/upload?category=' + props.category
axios.post(path, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(res => {
// 处理上传成功的响应
if (res.data.code === 0) {
emits('on-success', res.data)
emits('update:modelValue', res.data.data.url)
showDrawer.value = false
} else {
ElMessage({
type: 'error',
message: '上传失败'
})
}
})
.catch(error => {
// 处理上传失败的错误
console.error('上传失败', error);
ElMessage({
type: 'error',
message: '上传失败'
})
})
}
const handleSelectImg = (e) => {
var file = e.target.files[0]
if (! /\.(gif|jpg|jpeg|png|bmp|webp|GIF|JPG|PNG)$/.test(e.target.value)) {
ElMessage({
type: 'error',
message: '图片类型必须是.gif,jpeg,jpg,png,bmp中的一种'
})
return false
}
var reader = new FileReader()
reader.onloadend = (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
}
vueCropperImg.value = data
showDrawer.value = true
}
reader.readAsArrayBuffer(file)
e.target.value = ''
}
// 初始化方法
const open = async (data) => {
showDrawer.value = true
vueCropperImg.value = data
}
// 对外暴露方法
defineExpose({ open })
</script>
<style lang="scss">
.gva-cropper-content {
justify-content: flex-start;
-webkit-justify-content: flex-start;
.cropper {
width: 565px;
height: 600px;
.gva-btn-list {
justify-content: center;
-webkit-justify-content: center;
}
}
.show-preview {
margin-right: 15px;
.preview-title {
line-height: 42px;
}
.preview {
box-sizing: border-box;
overflow: hidden;
/*border-radius: 50%;*/
border: 2px solid var(--el-border-color-dark);
background: #cccccc;
margin-right: 20px;
}
}
}
.gva-cropper-btn {
display: inline-flex;
.btn {
min-width: 108px;
display: inline-flex;
box-sizing: border-box;
line-height: 1;
height: 32px;
padding: 8px 15px;
border-radius: var(--el-border-radius-base);
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
vertical-align: middle;
justify-content: center;
align-items: center;
outline: none;
>span {
margin-left: 6px;
}
}
.btn-default {
border: 1px solid var(--el-border-color);
color: var(--el-text-color-regular);
background-color: var(--el-fill-color-blank);
}
.btn-default:hover {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.btn-primary {
border: 1px solid var(--el-color-primary-light-5);
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.btn-primary:hover {
border: 1px solid var(--el-color-primary);
color: var(--el-color-white);
background-color: var(--el-color-primary);
}
}
</style>