Ben Lin
2024-10-22 e4f79a7e36214e5836cb0e667b3ffbd3db45456d
src/components/Cropper/src/Cropper.vue
@@ -10,13 +10,14 @@
    />
  </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 } from 'vue';
  import Cropper from 'cropperjs';
  import 'cropperjs/dist/cropper.css';
  import { useDesign } from '/@/hooks/web/useDesign';
  import { useDebounceFn } from '@vueuse/shared';
  import { useDesign } from '@/hooks/web/useDesign';
  import { useDebounceFn } from '@vueuse/core';
  import { useAttrs } from '@vben/hooks';
  type Options = Cropper.Options;
@@ -43,7 +44,9 @@
    rotatable: true,
  };
  const props = {
  defineOptions({ name: 'CropperImage' });
  const props = defineProps({
    src: { type: String, required: true },
    alt: { type: String },
    circled: { type: Boolean, default: false },
@@ -55,124 +58,119 @@
    },
    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,
        });
      }
      // 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) => {
            emit('cropend', {
              imgBase64: e.target?.result ?? '',
              imgInfo,
            });
          };
          fileReader.onerror = () => {
            emit('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;
      }
      return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered };
    },
  });
  const emit = defineEmits(['cropend', 'ready', 'cropendError']);
  const attrs = useAttrs();
  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,
    });
  }
  // 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) => {
        emit('cropend', {
          imgBase64: e.target?.result ?? '',
          imgInfo,
        });
      };
      fileReader.onerror = () => {
        emit('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';