perf: 优化ImageCropping
This commit is contained in:
parent
85f71d457b
commit
069777c880
|
@ -1,12 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { nextTick, unref, ref, watch, onBeforeUnmount, onMounted } from 'vue'
|
import { nextTick, unref, ref, watch, onBeforeUnmount, onMounted, computed } from 'vue'
|
||||||
import Cropper from 'cropperjs'
|
import Cropper from 'cropperjs'
|
||||||
import 'cropperjs/dist/cropper.min.css'
|
import 'cropperjs/dist/cropper.min.css'
|
||||||
|
import { ElDivider, ElUpload, UploadFile, ElMessage, ElTooltip } from 'element-plus'
|
||||||
// interface CropperOptions extends /* @vue-ignore */ Cropper.Options {
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
// imageUrl: string
|
import { BaseButton } from '@/components/Button'
|
||||||
// }
|
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
|
@ -25,9 +24,63 @@ const props = defineProps({
|
||||||
cropBoxHeight: {
|
cropBoxHeight: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 200
|
default: 200
|
||||||
|
},
|
||||||
|
boxWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 425
|
||||||
|
},
|
||||||
|
boxHeight: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 320
|
||||||
|
},
|
||||||
|
showResult: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getBase64 = useDebounceFn(() => {
|
||||||
|
imgBase64.value = unref(cropperRef)?.getCroppedCanvas()?.toDataURL() ?? ''
|
||||||
|
}, 80)
|
||||||
|
|
||||||
|
const resetCropBox = () => {
|
||||||
|
const containerData = unref(cropperRef)?.getContainerData()
|
||||||
|
unref(cropperRef)?.setCropBoxData({
|
||||||
|
width: props.cropBoxWidth,
|
||||||
|
height: props.cropBoxHeight,
|
||||||
|
left: (containerData?.width || 0) / 2 - 100,
|
||||||
|
top: (containerData?.height || 0) / 2 - 100
|
||||||
|
})
|
||||||
|
imgBase64.value = unref(cropperRef)?.getCroppedCanvas()?.toDataURL() ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBoxStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
width: `${props.boxWidth}px`,
|
||||||
|
height: `${props.boxHeight}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCropBoxStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
width: `${props.cropBoxWidth}px`,
|
||||||
|
height: `${props.cropBoxHeight}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取对应的缩小倍数的宽高
|
||||||
|
const getScaleSize = (scale: number) => {
|
||||||
|
return {
|
||||||
|
width: props.cropBoxWidth * scale + 'px',
|
||||||
|
height: props.cropBoxHeight * scale + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgBase64 = ref('')
|
||||||
const imgRef = ref<HTMLImageElement>()
|
const imgRef = ref<HTMLImageElement>()
|
||||||
const cropperRef = ref<Cropper>()
|
const cropperRef = ref<Cropper>()
|
||||||
const intiCropper = () => {
|
const intiCropper = () => {
|
||||||
|
@ -37,22 +90,61 @@ const intiCropper = () => {
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
viewMode: 1,
|
viewMode: 1,
|
||||||
dragMode: 'move',
|
dragMode: 'move',
|
||||||
cropBoxResizable: false,
|
// cropBoxResizable: false,
|
||||||
cropBoxMovable: false,
|
// cropBoxMovable: false,
|
||||||
toggleDragModeOnDblclick: false,
|
toggleDragModeOnDblclick: false,
|
||||||
checkCrossOrigin: false,
|
checkCrossOrigin: false,
|
||||||
ready() {
|
ready() {
|
||||||
const containerData = unref(cropperRef)?.getContainerData()
|
resetCropBox()
|
||||||
unref(cropperRef)?.setCropBoxData({
|
},
|
||||||
width: props.cropBoxWidth,
|
cropmove() {
|
||||||
height: props.cropBoxHeight,
|
getBase64()
|
||||||
left: (containerData?.width || 0) / 2 - 100,
|
},
|
||||||
top: (containerData?.height || 0) / 2 - 100
|
zoom() {
|
||||||
})
|
getBase64()
|
||||||
|
},
|
||||||
|
crop() {
|
||||||
|
getBase64()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadChange = (uploadFile: UploadFile) => {
|
||||||
|
// 判断是否是图片
|
||||||
|
if (uploadFile?.raw?.type.indexOf('image') === -1) {
|
||||||
|
ElMessage.error('请上传图片格式的文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!uploadFile.raw) return
|
||||||
|
// 获取图片的访问地址
|
||||||
|
const url = URL.createObjectURL(uploadFile.raw)
|
||||||
|
unref(cropperRef)?.replace(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
unref(cropperRef)?.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotate = (deg: number) => {
|
||||||
|
unref(cropperRef)?.rotate(deg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = ref(1)
|
||||||
|
const scaleY = ref(1)
|
||||||
|
const scale = (type: 'scaleX' | 'scaleY') => {
|
||||||
|
if (type === 'scaleX') {
|
||||||
|
scaleX.value = scaleX.value === 1 ? -1 : 1
|
||||||
|
unref(cropperRef)?.[type](unref(scaleX))
|
||||||
|
} else {
|
||||||
|
scaleY.value = scaleY.value === 1 ? -1 : 1
|
||||||
|
unref(cropperRef)?.[type](unref(scaleY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoom = (num: number) => {
|
||||||
|
unref(cropperRef)?.zoom(num)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
intiCropper()
|
intiCropper()
|
||||||
})
|
})
|
||||||
|
@ -60,9 +152,10 @@ onMounted(() => {
|
||||||
watch(
|
watch(
|
||||||
() => props.imageUrl,
|
() => props.imageUrl,
|
||||||
async (url) => {
|
async (url) => {
|
||||||
await nextTick()
|
|
||||||
if (url) {
|
if (url) {
|
||||||
unref(cropperRef)?.replace(url)
|
unref(cropperRef)?.replace(url)
|
||||||
|
await nextTick()
|
||||||
|
resetCropBox()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -72,19 +165,95 @@ onBeforeUnmount(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
cropperExpose: () => unref(cropperRef)
|
cropperExpose: cropperRef
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="prefixCls" class="flex justify-center items-center">
|
<div
|
||||||
<img
|
:class="{
|
||||||
ref="imgRef"
|
[prefixCls]: true,
|
||||||
:src="imageUrl"
|
'flex items-center': showResult
|
||||||
class="block max-w-full"
|
}"
|
||||||
crossorigin="anonymous"
|
>
|
||||||
alt=""
|
<div>
|
||||||
srcset=""
|
<div :style="getBoxStyle" class="flex justify-center items-center">
|
||||||
/>
|
<img
|
||||||
|
v-show="imageUrl"
|
||||||
|
ref="imgRef"
|
||||||
|
:src="imageUrl"
|
||||||
|
class="block max-w-full"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
alt=""
|
||||||
|
srcset=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="showActions" class="mt-10px flex items-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ElTooltip content="选择文件" placement="bottom">
|
||||||
|
<ElUpload
|
||||||
|
action="''"
|
||||||
|
accept="image/*"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="uploadChange"
|
||||||
|
>
|
||||||
|
<BaseButton size="small" type="primary" class="mt-2px"
|
||||||
|
><Icon icon="ep:upload-filled"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElUpload>
|
||||||
|
</ElTooltip>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end flex-1">
|
||||||
|
<ElTooltip content="重置" placement="bottom">
|
||||||
|
<BaseButton size="small" type="primary" @click="reset"
|
||||||
|
><Icon icon="ep:refresh"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="逆时针旋转" placement="bottom">
|
||||||
|
<BaseButton size="small" type="primary" @click="rotate(-45)"
|
||||||
|
><Icon icon="ant-design:rotate-left-outlined"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="顺时针旋转" placement="bottom">
|
||||||
|
<BaseButton size="small" type="primary" @click="rotate(45)"
|
||||||
|
><Icon icon="ant-design:rotate-right-outlined"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="水平翻转" placement="bottom">
|
||||||
|
<BaseButton size="small" type="primary" @click="scale('scaleX')"
|
||||||
|
><Icon icon="vaadin:arrows-long-h"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="垂直翻转" placement="bottom">
|
||||||
|
<BaseButton size="small" type="primary" @click="scale('scaleY')"
|
||||||
|
><Icon icon="vaadin:arrows-long-v"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="放大" placement="bottom">
|
||||||
|
<BaseButton size="small" type="primary" @click="zoom(0.1)"
|
||||||
|
><Icon icon="ant-design:zoom-in-outlined"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="缩小" placement="bottom">
|
||||||
|
<BaseButton size="small" type="primary" @click="zoom(-0.1)"
|
||||||
|
><Icon icon="ant-design:zoom-out-outlined"
|
||||||
|
/></BaseButton>
|
||||||
|
</ElTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="imgBase64 && showResult" class="ml-20px">
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<img :src="imgBase64" class="rounded-[50%]" :style="getCropBoxStyle" />
|
||||||
|
</div>
|
||||||
|
<ElDivider />
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<img :src="imgBase64" class="rounded-[50%]" :style="getScaleSize(0.2)" />
|
||||||
|
<img :src="imgBase64" class="rounded-[50%] ml-20px" :style="getScaleSize(0.25)" />
|
||||||
|
<img :src="imgBase64" class="rounded-[50%] ml-20px" :style="getScaleSize(0.3)" />
|
||||||
|
<img :src="imgBase64" class="rounded-[50%] ml-20px" :style="getScaleSize(0.35)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default {
|
||||||
sizeIcon: '尺寸图标',
|
sizeIcon: '尺寸图标',
|
||||||
localeIcon: '多语言图标',
|
localeIcon: '多语言图标',
|
||||||
tagsView: '标签页',
|
tagsView: '标签页',
|
||||||
logo: '标志',
|
logo: 'Logo',
|
||||||
greyMode: '灰色模式',
|
greyMode: '灰色模式',
|
||||||
fixedHeader: '固定头部',
|
fixedHeader: '固定头部',
|
||||||
headerTheme: '头部主题',
|
headerTheme: '头部主题',
|
||||||
|
|
|
@ -2,14 +2,22 @@
|
||||||
import { ContentWrap } from '@/components/ContentWrap'
|
import { ContentWrap } from '@/components/ContentWrap'
|
||||||
import { ImageCropping } from '@/components/ImageCropping'
|
import { ImageCropping } from '@/components/ImageCropping'
|
||||||
import { ref, unref } from 'vue'
|
import { ref, unref } from 'vue'
|
||||||
import { ElInput } from 'element-plus'
|
import { ElInput, ElDivider } from 'element-plus'
|
||||||
|
|
||||||
const cropperExpose = ref<InstanceType<typeof ImageCropping>>()
|
const cropperExpose = ref<InstanceType<typeof ImageCropping>>()
|
||||||
|
|
||||||
const base64 = ref('')
|
const base64 = ref('')
|
||||||
|
|
||||||
const getBase64 = () => {
|
const getBase64 = () => {
|
||||||
base64.value = unref(cropperExpose)?.cropperExpose()?.getCroppedCanvas()?.toDataURL() ?? ''
|
base64.value = unref(cropperExpose)?.cropperExpose?.getCroppedCanvas()?.toDataURL() ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const cropperExpose2 = ref<InstanceType<typeof ImageCropping>>()
|
||||||
|
|
||||||
|
const base642 = ref('')
|
||||||
|
|
||||||
|
const getBase642 = () => {
|
||||||
|
base642.value = unref(cropperExpose)?.cropperExpose?.getCroppedCanvas()?.toDataURL() ?? ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -21,5 +29,15 @@ const getBase64 = () => {
|
||||||
ref="cropperExpose"
|
ref="cropperExpose"
|
||||||
image-url="https://hips.hearstapps.com/hmg-prod/images/%E5%AE%8B%E6%99%BA%E5%AD%9D-1597774015.jpg?crop=0.500xw:1.00xh;0.500xw,0&resize=640:*"
|
image-url="https://hips.hearstapps.com/hmg-prod/images/%E5%AE%8B%E6%99%BA%E5%AD%9D-1597774015.jpg?crop=0.500xw:1.00xh;0.500xw,0&resize=640:*"
|
||||||
/>
|
/>
|
||||||
|
<ElDivider />
|
||||||
|
<BaseButton type="primary" class="mb-20px" @click="getBase642">裁剪</BaseButton>
|
||||||
|
<ElInput v-model="base642" class="mb-20px" type="textarea" />
|
||||||
|
<ImageCropping
|
||||||
|
ref="cropperExpose2"
|
||||||
|
:show-actions="false"
|
||||||
|
box-width="100%"
|
||||||
|
:show-result="false"
|
||||||
|
image-url="https://hips.hearstapps.com/hmg-prod/images/%E5%AE%8B%E6%99%BA%E5%AD%9D-1597774015.jpg?crop=0.500xw:1.00xh;0.500xw,0&resize=640:*"
|
||||||
|
/>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue