perf: 优化 cropper 组件的类型

pull/90/MERGE
xingyu4j 2025-05-06 16:15:56 +08:00
parent bd02645e26
commit 5af8a3c40c
5 changed files with 102 additions and 86 deletions

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ButtonProps } from 'ant-design-vue';
import type { CSSProperties } from 'vue';
import type { CSSProperties, PropType } from 'vue';
import type { CropperAvatarProps } from './typing';
import { computed, ref, unref, watch, watchEffect } from 'vue';
@ -14,27 +14,14 @@ import cropperModal from './cropper-modal.vue';
defineOptions({ name: 'CropperAvatar' });
const props = defineProps({
width: { default: '200px', type: [String, Number] },
value: { default: '', type: String },
showBtn: { default: true, type: Boolean },
btnProps: { default: () => ({}), type: Object as PropType<ButtonProps> },
btnText: { default: '', type: String },
uploadApi: {
required: true,
type: Function as PropType<
({
file,
filename,
name,
}: {
file: Blob;
filename: string;
name: string;
}) => Promise<any>
>,
},
size: { default: 5, type: Number },
const props = withDefaults(defineProps<CropperAvatarProps>(), {
width: 200,
value: '',
showBtn: true,
btnProps: () => ({}),
btnText: '',
uploadApi: () => Promise.resolve(),
size: 5,
});
const emit = defineEmits(['update:value', 'change']);

View File

@ -1,7 +1,5 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { CropendResult, Cropper } from './typing';
import type { CropendResult, CropperModalProps, CropperType } from './typing';
import { ref } from 'vue';
@ -20,18 +18,13 @@ import {
import CropperImage from './cropper.vue';
type apiFunParams = { file: Blob; filename: string; name: string };
defineOptions({ name: 'CropperModal' });
const props = defineProps({
circled: { default: true, type: Boolean },
uploadApi: {
required: true,
type: Function as PropType<(params: apiFunParams) => Promise<any>>,
},
src: { default: '', type: String },
size: { default: 0, type: Number },
const props = withDefaults(defineProps<CropperModalProps>(), {
circled: true,
size: 0,
src: '',
uploadApi: () => Promise.resolve(),
});
const emit = defineEmits(['uploadSuccess', 'uploadError', 'register']);
@ -39,7 +32,7 @@ const emit = defineEmits(['uploadSuccess', 'uploadError', 'register']);
let filename = '';
const src = ref(props.src || '');
const previewSource = ref('');
const cropper = ref<Cropper>();
const cropper = ref<CropperType>();
let scaleX = 1;
let scaleY = 1;
@ -83,7 +76,7 @@ function handleCropend({ imgBase64 }: CropendResult) {
previewSource.value = imgBase64;
}
function handleReady(cropperInstance: Cropper) {
function handleReady(cropperInstance: CropperType) {
cropper.value = cropperInstance;
// loading
modalLoading(false);

View File

@ -1,64 +1,40 @@
<script lang="ts" setup>
import type { CSSProperties, PropType } from 'vue';
import type { CSSProperties } from 'vue';
import type { CropperProps } from './typing';
import { computed, onMounted, onUnmounted, ref, unref, useAttrs } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
import { defaultOptions } from './typing';
type Options = Cropper.Options;
import 'cropperjs/dist/cropper.css';
defineOptions({ name: 'CropperImage' });
const props = defineProps({
src: { required: true, type: String },
alt: { default: '', type: String },
circled: { default: false, type: Boolean },
realTimePreview: { default: true, type: Boolean },
height: { default: '360px', type: [String, Number] },
crossorigin: {
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
default: undefined,
},
imageStyle: { default: () => ({}), type: Object as PropType<CSSProperties> },
options: { default: () => ({}), type: Object as PropType<Options> },
const props = withDefaults(defineProps<CropperProps>(), {
src: '',
alt: '',
circled: false,
realTimePreview: true,
height: '360px',
crossorigin: undefined,
imageStyle: () => ({}),
options: () => ({}),
});
const emit = defineEmits(['cropend', 'ready', 'cropendError']);
const attrs = useAttrs();
const defaultOptions: Options = {
aspectRatio: 1,
zoomable: true,
zoomOnTouch: true,
zoomOnWheel: true,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: true,
autoCrop: true,
background: true,
highlight: true,
center: true,
responsive: true,
restore: true,
checkCrossOrigin: true,
checkOrientation: true,
scalable: true,
modal: true,
guides: true,
movable: true,
rotatable: true,
};
type ElRef<T extends HTMLElement = HTMLDivElement> = null | T;
const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Cropper | null>();
const isReady = ref(false);
const prefixCls = 'cropper-image';
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80);
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
const getImageStyle = computed((): CSSProperties => {
return {
@ -97,29 +73,29 @@ async function init() {
...defaultOptions,
ready: () => {
isReady.value = true;
realTimeCroppered();
realTimeCropped();
emit('ready', cropper.value);
},
crop() {
debounceRealTimeCroppered();
debounceRealTimeCropped();
},
zoom() {
debounceRealTimeCroppered();
debounceRealTimeCropped();
},
cropmove() {
debounceRealTimeCroppered();
debounceRealTimeCropped();
},
...props.options,
});
}
// Real-time display preview
function realTimeCroppered() {
props.realTimePreview && croppered();
function realTimeCropped() {
props.realTimePreview && cropped();
}
// event: return base64 and width and height information after cropping
function croppered() {
function cropped() {
if (!cropper.value) {
return;
}

View File

@ -1,8 +1,68 @@
import type { ButtonProps } from 'ant-design-vue';
import type Cropper from 'cropperjs';
import type { CSSProperties } from 'vue';
export interface apiFunParams {
file: Blob;
filename: string;
name: string;
}
export interface CropendResult {
imgBase64: string;
imgInfo: Cropper.Data;
}
export type { Cropper };
export interface CropperProps {
src?: string;
alt?: string;
circled?: boolean;
realTimePreview?: boolean;
height?: number | string;
crossorigin?: '' | 'anonymous' | 'use-credentials' | undefined;
imageStyle?: CSSProperties;
options?: Cropper.Options;
}
export interface CropperAvatarProps {
width?: number | string;
value?: string;
showBtn?: boolean;
btnProps?: ButtonProps;
btnText?: string;
uploadApi?: (params: apiFunParams) => Promise<any>;
size?: number;
}
export interface CropperModalProps {
circled?: boolean;
uploadApi?: (params: apiFunParams) => Promise<any>;
src?: string;
size?: number;
}
export const defaultOptions: Cropper.Options = {
aspectRatio: 1,
zoomable: true,
zoomOnTouch: true,
zoomOnWheel: true,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: true,
autoCrop: true,
background: true,
highlight: true,
center: true,
responsive: true,
restore: true,
checkCrossOrigin: true,
checkOrientation: true,
scalable: true,
modal: true,
guides: true,
movable: true,
rotatable: true,
};
export type { Cropper as CropperType };

View File

@ -50,7 +50,7 @@ async function handelUpload({
:show-btn="false"
:upload-api="handelUpload"
:value="avatar"
width="120"
:width="120"
@change="emit('success')"
/>
</Tooltip>