368 lines
9.4 KiB
Vue
368 lines
9.4 KiB
Vue
<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" :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>
|