|
|
|
@ -10,9 +10,9 @@ |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
<script lang="ts"> |
|
|
|
<script lang="ts" setup> |
|
|
|
import type { CSSProperties } from 'vue'; |
|
|
|
import { defineComponent, onMounted, ref, unref, computed, onUnmounted } from 'vue'; |
|
|
|
import { onMounted, ref, unref, computed, onUnmounted, useAttrs } from 'vue'; |
|
|
|
import Cropper from 'cropperjs'; |
|
|
|
import 'cropperjs/dist/cropper.css'; |
|
|
|
import { useDesign } from '/@/hooks/web/useDesign'; |
|
|
|
@ -20,6 +20,7 @@ |
|
|
|
|
|
|
|
type Options = Cropper.Options; |
|
|
|
|
|
|
|
const emits = defineEmits(['cropend', 'ready', 'cropendError']); |
|
|
|
const defaultOptions: Options = { |
|
|
|
aspectRatio: 1, |
|
|
|
zoomable: true, |
|
|
|
@ -43,7 +44,7 @@ |
|
|
|
rotatable: true, |
|
|
|
}; |
|
|
|
|
|
|
|
const props = { |
|
|
|
const props = defineProps({ |
|
|
|
src: { type: String, required: true }, |
|
|
|
alt: { type: String }, |
|
|
|
circled: { type: Boolean, default: false }, |
|
|
|
@ -55,124 +56,117 @@ |
|
|
|
}, |
|
|
|
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) }, |
|
|
|
options: { type: Object as PropType<Options>, default: () => ({}) }, |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
export default defineComponent({ |
|
|
|
name: 'CropperImage', |
|
|
|
props, |
|
|
|
emits: ['cropend', 'ready', 'cropendError'], |
|
|
|
setup(props, { attrs, emit }) { |
|
|
|
const imgElRef = ref<ElRef<HTMLImageElement>>(); |
|
|
|
const cropper = ref<Nullable<Cropper>>(); |
|
|
|
const isReady = ref(false); |
|
|
|
|
|
|
|
const { prefixCls } = useDesign('cropper-image'); |
|
|
|
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80); |
|
|
|
|
|
|
|
const getImageStyle = computed((): CSSProperties => { |
|
|
|
return { |
|
|
|
height: props.height, |
|
|
|
maxWidth: '100%', |
|
|
|
...props.imageStyle, |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
const getClass = computed(() => { |
|
|
|
return [ |
|
|
|
prefixCls, |
|
|
|
attrs.class, |
|
|
|
{ |
|
|
|
[`${prefixCls}--circled`]: props.circled, |
|
|
|
}, |
|
|
|
]; |
|
|
|
}); |
|
|
|
|
|
|
|
const getWrapperStyle = computed((): CSSProperties => { |
|
|
|
return { height: `${props.height}`.replace(/px/, '') + 'px' }; |
|
|
|
}); |
|
|
|
|
|
|
|
onMounted(init); |
|
|
|
|
|
|
|
onUnmounted(() => { |
|
|
|
cropper.value?.destroy(); |
|
|
|
}); |
|
|
|
|
|
|
|
async function init() { |
|
|
|
const imgEl = unref(imgElRef); |
|
|
|
if (!imgEl) { |
|
|
|
return; |
|
|
|
} |
|
|
|
cropper.value = new Cropper(imgEl, { |
|
|
|
...defaultOptions, |
|
|
|
ready: () => { |
|
|
|
isReady.value = true; |
|
|
|
realTimeCroppered(); |
|
|
|
emit('ready', cropper.value); |
|
|
|
}, |
|
|
|
crop() { |
|
|
|
debounceRealTimeCroppered(); |
|
|
|
}, |
|
|
|
zoom() { |
|
|
|
debounceRealTimeCroppered(); |
|
|
|
}, |
|
|
|
cropmove() { |
|
|
|
debounceRealTimeCroppered(); |
|
|
|
}, |
|
|
|
...props.options, |
|
|
|
}); |
|
|
|
} |
|
|
|
const attrs = useAttrs(); |
|
|
|
|
|
|
|
// Real-time display preview |
|
|
|
function realTimeCroppered() { |
|
|
|
props.realTimePreview && croppered(); |
|
|
|
} |
|
|
|
const imgElRef = ref<ElRef<HTMLImageElement>>(); |
|
|
|
const cropper = ref<Nullable<Cropper>>(); |
|
|
|
const isReady = ref(false); |
|
|
|
|
|
|
|
// event: return base64 and width and height information after cropping |
|
|
|
function croppered() { |
|
|
|
if (!cropper.value) { |
|
|
|
return; |
|
|
|
} |
|
|
|
let imgInfo = cropper.value.getData(); |
|
|
|
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas(); |
|
|
|
canvas.toBlob((blob) => { |
|
|
|
if (!blob) { |
|
|
|
return; |
|
|
|
} |
|
|
|
let fileReader: FileReader = new FileReader(); |
|
|
|
fileReader.readAsDataURL(blob); |
|
|
|
fileReader.onloadend = (e) => { |
|
|
|
emit('cropend', { |
|
|
|
imgBase64: e.target?.result ?? '', |
|
|
|
imgInfo, |
|
|
|
}); |
|
|
|
}; |
|
|
|
fileReader.onerror = () => { |
|
|
|
emit('cropendError'); |
|
|
|
}; |
|
|
|
}, 'image/png'); |
|
|
|
} |
|
|
|
const { prefixCls } = useDesign('cropper-image'); |
|
|
|
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80); |
|
|
|
|
|
|
|
// Get a circular picture canvas |
|
|
|
function getRoundedCanvas() { |
|
|
|
const sourceCanvas = cropper.value!.getCroppedCanvas(); |
|
|
|
const canvas = document.createElement('canvas'); |
|
|
|
const context = canvas.getContext('2d')!; |
|
|
|
const width = sourceCanvas.width; |
|
|
|
const height = sourceCanvas.height; |
|
|
|
canvas.width = width; |
|
|
|
canvas.height = height; |
|
|
|
context.imageSmoothingEnabled = true; |
|
|
|
context.drawImage(sourceCanvas, 0, 0, width, height); |
|
|
|
context.globalCompositeOperation = 'destination-in'; |
|
|
|
context.beginPath(); |
|
|
|
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true); |
|
|
|
context.fill(); |
|
|
|
return canvas; |
|
|
|
} |
|
|
|
const getImageStyle = computed((): CSSProperties => { |
|
|
|
return { |
|
|
|
height: props.height, |
|
|
|
maxWidth: '100%', |
|
|
|
...props.imageStyle, |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered }; |
|
|
|
}, |
|
|
|
const getClass = computed(() => { |
|
|
|
return [ |
|
|
|
prefixCls, |
|
|
|
attrs.class, |
|
|
|
{ |
|
|
|
[`${prefixCls}--circled`]: props.circled, |
|
|
|
}, |
|
|
|
]; |
|
|
|
}); |
|
|
|
|
|
|
|
const getWrapperStyle = computed((): CSSProperties => { |
|
|
|
return { height: `${props.height}`.replace(/px/, '') + 'px' }; |
|
|
|
}); |
|
|
|
|
|
|
|
onMounted(init); |
|
|
|
|
|
|
|
onUnmounted(() => { |
|
|
|
cropper.value?.destroy(); |
|
|
|
}); |
|
|
|
|
|
|
|
async function init() { |
|
|
|
const imgEl = unref(imgElRef); |
|
|
|
if (!imgEl) { |
|
|
|
return; |
|
|
|
} |
|
|
|
cropper.value = new Cropper(imgEl, { |
|
|
|
...defaultOptions, |
|
|
|
ready: () => { |
|
|
|
isReady.value = true; |
|
|
|
realTimeCroppered(); |
|
|
|
emits('ready', cropper.value); |
|
|
|
}, |
|
|
|
crop() { |
|
|
|
debounceRealTimeCroppered(); |
|
|
|
}, |
|
|
|
zoom() { |
|
|
|
debounceRealTimeCroppered(); |
|
|
|
}, |
|
|
|
cropmove() { |
|
|
|
debounceRealTimeCroppered(); |
|
|
|
}, |
|
|
|
...props.options, |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Real-time display preview |
|
|
|
function realTimeCroppered() { |
|
|
|
props.realTimePreview && croppered(); |
|
|
|
} |
|
|
|
|
|
|
|
// event: return base64 and width and height information after cropping |
|
|
|
function croppered() { |
|
|
|
if (!cropper.value) { |
|
|
|
return; |
|
|
|
} |
|
|
|
let imgInfo = cropper.value.getData(); |
|
|
|
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas(); |
|
|
|
canvas.toBlob((blob) => { |
|
|
|
if (!blob) { |
|
|
|
return; |
|
|
|
} |
|
|
|
let fileReader: FileReader = new FileReader(); |
|
|
|
fileReader.readAsDataURL(blob); |
|
|
|
fileReader.onloadend = (e) => { |
|
|
|
emits('cropend', { |
|
|
|
imgBase64: e.target?.result ?? '', |
|
|
|
imgInfo, |
|
|
|
}); |
|
|
|
}; |
|
|
|
fileReader.onerror = () => { |
|
|
|
emits('cropendError'); |
|
|
|
}; |
|
|
|
}, 'image/png'); |
|
|
|
} |
|
|
|
|
|
|
|
// Get a circular picture canvas |
|
|
|
function getRoundedCanvas() { |
|
|
|
const sourceCanvas = cropper.value!.getCroppedCanvas(); |
|
|
|
const canvas = document.createElement('canvas'); |
|
|
|
const context = canvas.getContext('2d')!; |
|
|
|
const width = sourceCanvas.width; |
|
|
|
const height = sourceCanvas.height; |
|
|
|
canvas.width = width; |
|
|
|
canvas.height = height; |
|
|
|
context.imageSmoothingEnabled = true; |
|
|
|
context.drawImage(sourceCanvas, 0, 0, width, height); |
|
|
|
context.globalCompositeOperation = 'destination-in'; |
|
|
|
context.beginPath(); |
|
|
|
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true); |
|
|
|
context.fill(); |
|
|
|
return canvas; |
|
|
|
} |
|
|
|
</script> |
|
|
|
<style lang="less"> |
|
|
|
@prefix-cls: ~'@{namespace}-cropper-image'; |
|
|
|
|