Cropper.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <template>
  2. <div :class="getClass" :style="getWrapperStyle">
  3. <img
  4. v-show="isReady"
  5. ref="imgElRef"
  6. :alt="alt"
  7. :crossorigin="crossorigin"
  8. :src="src"
  9. :style="getImageStyle"
  10. />
  11. </div>
  12. </template>
  13. <script lang="ts" name="Cropper" setup>
  14. import { CSSProperties, PropType } from 'vue'
  15. import Cropper from 'cropperjs'
  16. import 'cropperjs/dist/cropper.css'
  17. import { useDesign } from '@/hooks/web/useDesign'
  18. import { propTypes } from '@/utils/propTypes'
  19. import { useDebounceFn } from '@vueuse/core'
  20. type Options = Cropper.Options
  21. const defaultOptions: Options = {
  22. aspectRatio: 1,
  23. zoomable: true,
  24. zoomOnTouch: true,
  25. zoomOnWheel: true,
  26. cropBoxMovable: true,
  27. cropBoxResizable: true,
  28. toggleDragModeOnDblclick: true,
  29. autoCrop: true,
  30. background: true,
  31. highlight: true,
  32. center: true,
  33. responsive: true,
  34. restore: true,
  35. checkCrossOrigin: true,
  36. checkOrientation: true,
  37. scalable: true,
  38. modal: true,
  39. guides: true,
  40. movable: true,
  41. rotatable: true
  42. }
  43. const props = defineProps({
  44. src: propTypes.string.def(''),
  45. alt: propTypes.string.def(''),
  46. circled: propTypes.bool.def(false),
  47. realTimePreview: propTypes.bool.def(true),
  48. height: propTypes.string.def('360px'),
  49. crossorigin: {
  50. type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
  51. default: undefined
  52. },
  53. imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
  54. options: { type: Object as PropType<Options>, default: () => ({}) }
  55. })
  56. const emit = defineEmits(['cropend', 'ready', 'cropendError'])
  57. const attrs = useAttrs()
  58. const imgElRef = ref<ElRef<HTMLImageElement>>()
  59. const cropper = ref<Nullable<Cropper>>()
  60. const isReady = ref(false)
  61. const { getPrefixCls } = useDesign()
  62. const prefixCls = getPrefixCls('cropper-image')
  63. const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)
  64. const getImageStyle = computed((): CSSProperties => {
  65. return {
  66. height: props.height,
  67. maxWidth: '100%',
  68. ...props.imageStyle
  69. }
  70. })
  71. const getClass = computed(() => {
  72. return [
  73. prefixCls,
  74. attrs.class,
  75. {
  76. [`${prefixCls}--circled`]: props.circled
  77. }
  78. ]
  79. })
  80. const getWrapperStyle = computed((): CSSProperties => {
  81. return { height: `${props.height}`.replace(/px/, '') + 'px' }
  82. })
  83. onMounted(init)
  84. onUnmounted(() => {
  85. cropper.value?.destroy()
  86. })
  87. async function init() {
  88. const imgEl = unref(imgElRef)
  89. if (!imgEl) {
  90. return
  91. }
  92. cropper.value = new Cropper(imgEl, {
  93. ...defaultOptions,
  94. ready: () => {
  95. isReady.value = true
  96. realTimeCroppered()
  97. emit('ready', cropper.value)
  98. },
  99. crop() {
  100. debounceRealTimeCroppered()
  101. },
  102. zoom() {
  103. debounceRealTimeCroppered()
  104. },
  105. cropmove() {
  106. debounceRealTimeCroppered()
  107. },
  108. ...props.options
  109. })
  110. }
  111. // Real-time display preview
  112. function realTimeCroppered() {
  113. props.realTimePreview && croppered()
  114. }
  115. // event: return base64 and width and height information after cropping
  116. function croppered() {
  117. if (!cropper.value) {
  118. return
  119. }
  120. let imgInfo = cropper.value.getData()
  121. const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
  122. canvas.toBlob((blob) => {
  123. if (!blob) {
  124. return
  125. }
  126. let fileReader: FileReader = new FileReader()
  127. fileReader.readAsDataURL(blob)
  128. fileReader.onloadend = (e) => {
  129. emit('cropend', {
  130. imgBase64: e.target?.result ?? '',
  131. imgInfo
  132. })
  133. }
  134. fileReader.onerror = () => {
  135. emit('cropendError')
  136. }
  137. }, 'image/png')
  138. }
  139. // Get a circular picture canvas
  140. function getRoundedCanvas() {
  141. const sourceCanvas = cropper.value!.getCroppedCanvas()
  142. const canvas = document.createElement('canvas')
  143. const context = canvas.getContext('2d')!
  144. const width = sourceCanvas.width
  145. const height = sourceCanvas.height
  146. canvas.width = width
  147. canvas.height = height
  148. context.imageSmoothingEnabled = true
  149. context.drawImage(sourceCanvas, 0, 0, width, height)
  150. context.globalCompositeOperation = 'destination-in'
  151. context.beginPath()
  152. context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
  153. context.fill()
  154. return canvas
  155. }
  156. </script>
  157. <style lang="scss">
  158. $prefix-cls: #{$namespace}-cropper-image;
  159. .#{$prefix-cls} {
  160. &--circled {
  161. .cropper-view-box,
  162. .cropper-face {
  163. border-radius: 50%;
  164. }
  165. }
  166. }
  167. </style>