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

View File

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

View File

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

View File

@ -1,8 +1,68 @@
import type { ButtonProps } from 'ant-design-vue';
import type Cropper from 'cropperjs'; import type Cropper from 'cropperjs';
import type { CSSProperties } from 'vue';
export interface apiFunParams {
file: Blob;
filename: string;
name: string;
}
export interface CropendResult { export interface CropendResult {
imgBase64: string; imgBase64: string;
imgInfo: Cropper.Data; 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" :show-btn="false"
:upload-api="handelUpload" :upload-api="handelUpload"
:value="avatar" :value="avatar"
width="120" :width="120"
@change="emit('success')" @change="emit('success')"
/> />
</Tooltip> </Tooltip>