feat(image): 添加预览图加载占位符支持。Add preview image loading placeholder support.
支持通过 placeholder 属性自定义预览图的加载占位符,可以是 VNode 或布尔值。当设置为 true 时使用默认的 Spin 组件,设置为 VNode 时则渲染自定义内容。 Support customizing preview image loading placeholder through the placeholder property, which can be a VNode or boolean value. When set to true, it uses the default Spin component; when set to a VNode, it renders custom content.pull/8322/head
parent
35c1ad9c80
commit
9f465b25a4
|
|
@ -8,11 +8,11 @@ title:
|
||||||
|
|
||||||
## zh-CN
|
## zh-CN
|
||||||
|
|
||||||
可以设置不同的预览图片。
|
可以设置不同的预览图片。可以自定义预览图的加载占位符(placeholder)。
|
||||||
|
|
||||||
## en-US
|
## en-US
|
||||||
|
|
||||||
You can set different preview image.
|
You can set different preview image. You can also customize the loading placeholder of preview image with VNode.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
|
|
@ -21,7 +21,31 @@ You can set different preview image.
|
||||||
:width="200"
|
:width="200"
|
||||||
src="https://aliyuncdn.antdv.com/logo.png"
|
src="https://aliyuncdn.antdv.com/logo.png"
|
||||||
:preview="{
|
:preview="{
|
||||||
src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
src: 'http://47.100.102.7:11501/admin-api/infra/file/24/get//algorithm/execute/2025/08/20/20210628_iPhoneSE_YL_42_1755674307938_1755674703040.jpg',
|
||||||
|
placeholder: h(CustomLoadingComp),
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<a-image
|
||||||
|
:width="200"
|
||||||
|
src="https://aliyuncdn.antdv.com/logo.png"
|
||||||
|
:preview="{
|
||||||
|
src: 'http://47.100.102.7:11501/admin-api/infra/file/24/get//algorithm/execute/2025/08/20/20210628_iPhoneSE_YL_42_1755674307938_1755674703040.jpg',
|
||||||
|
placeholder: true,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { h } from 'vue';
|
||||||
|
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
|
||||||
|
import { theme } from 'ant-design-vue';
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
|
const CustomLoadingComp = h(LoadingOutlined, {
|
||||||
|
style: {
|
||||||
|
fontSize: '256px',
|
||||||
|
color: token.value.colorPrimary,
|
||||||
|
},
|
||||||
|
spin: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA
|
||||||
src?: string;
|
src?: string;
|
||||||
maskClassName?: string;
|
maskClassName?: string;
|
||||||
current?: number;
|
current?: number;
|
||||||
|
placeholder?: VNode | boolean;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,9 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
|
||||||
transition: `transform ${motionDurationSlow} ${motionEaseOut} 0s`,
|
transition: `transform ${motionDurationSlow} ${motionEaseOut} 0s`,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
|
'&-placeholder': {
|
||||||
|
position: 'absolute',
|
||||||
|
},
|
||||||
|
|
||||||
'&-wrapper': {
|
'&-wrapper': {
|
||||||
...genBoxStyle(),
|
...genBoxStyle(),
|
||||||
|
|
@ -214,7 +217,6 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
[`${previewCls}-moving`]: {
|
[`${previewCls}-moving`]: {
|
||||||
[`${previewCls}-preview-img`]: {
|
[`${previewCls}-preview-img`]: {
|
||||||
cursor: 'grabbing',
|
cursor: 'grabbing',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { CSSProperties, PropType } from 'vue';
|
import type { CSSProperties, PropType, VNode } from 'vue';
|
||||||
import { ref, watch, defineComponent, computed, onMounted, onUnmounted } from 'vue';
|
import { ref, watch, defineComponent, computed, onMounted, onUnmounted } from 'vue';
|
||||||
import isNumber from 'lodash-es/isNumber';
|
import isNumber from 'lodash-es/isNumber';
|
||||||
import cn from '../../_util/classNames';
|
import cn from '../../_util/classNames';
|
||||||
|
|
@ -19,6 +19,8 @@ export type ImagePreviewType = Omit<
|
||||||
> & {
|
> & {
|
||||||
src?: string;
|
src?: string;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
fallback?: string;
|
||||||
|
placeholder?: VNode | boolean;
|
||||||
onVisibleChange?: (value: boolean, prevValue: boolean) => void;
|
onVisibleChange?: (value: boolean, prevValue: boolean) => void;
|
||||||
getContainer?: GetContainer | false;
|
getContainer?: GetContainer | false;
|
||||||
maskClassName?: string;
|
maskClassName?: string;
|
||||||
|
|
@ -79,6 +81,7 @@ const ImageInternal = defineComponent({
|
||||||
onVisibleChange: () => {},
|
onVisibleChange: () => {},
|
||||||
getContainer: undefined,
|
getContainer: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return typeof props.preview === 'object'
|
return typeof props.preview === 'object'
|
||||||
? mergeDefaultValue(props.preview, defaultValues)
|
? mergeDefaultValue(props.preview, defaultValues)
|
||||||
: defaultValues;
|
: defaultValues;
|
||||||
|
|
@ -288,6 +291,7 @@ const ImageInternal = defineComponent({
|
||||||
onClose={onPreviewClose}
|
onClose={onPreviewClose}
|
||||||
mousePosition={mousePosition.value}
|
mousePosition={mousePosition.value}
|
||||||
src={mergedSrc}
|
src={mergedSrc}
|
||||||
|
placeholder={preview.value.placeholder}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
getContainer={getPreviewContainer.value}
|
getContainer={getPreviewContainer.value}
|
||||||
icons={icons}
|
icons={icons}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,15 @@ import {
|
||||||
shallowRef,
|
shallowRef,
|
||||||
watch,
|
watch,
|
||||||
cloneVNode,
|
cloneVNode,
|
||||||
|
ref,
|
||||||
|
isVNode,
|
||||||
|
nextTick,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { VNode, PropType } from 'vue';
|
import type { VNode, Ref, PropType } from 'vue';
|
||||||
|
import type { ImageStatus } from './Image';
|
||||||
import classnames from '../../_util/classNames';
|
import classnames from '../../_util/classNames';
|
||||||
import Dialog from '../../vc-dialog';
|
import Dialog from '../../vc-dialog';
|
||||||
|
import Spin from '../../spin';
|
||||||
import { type IDialogChildProps, dialogPropTypes } from '../../vc-dialog/IDialogPropTypes';
|
import { type IDialogChildProps, dialogPropTypes } from '../../vc-dialog/IDialogPropTypes';
|
||||||
import { getOffset } from '../../vc-util/Dom/css';
|
import { getOffset } from '../../vc-util/Dom/css';
|
||||||
import addEventListener from '../../vc-util/Dom/addEventListener';
|
import addEventListener from '../../vc-util/Dom/addEventListener';
|
||||||
|
|
@ -49,6 +53,11 @@ export const previewProps = {
|
||||||
...dialogPropTypes(),
|
...dialogPropTypes(),
|
||||||
src: String,
|
src: String,
|
||||||
alt: String,
|
alt: String,
|
||||||
|
fallback: String,
|
||||||
|
placeholder: {
|
||||||
|
type: Object as PropType<VNode | boolean>,
|
||||||
|
default: () => true,
|
||||||
|
},
|
||||||
rootClassName: String,
|
rootClassName: String,
|
||||||
icons: {
|
icons: {
|
||||||
type: Object as PropType<PreviewProps['icons']>,
|
type: Object as PropType<PreviewProps['icons']>,
|
||||||
|
|
@ -65,7 +74,20 @@ const Preview = defineComponent({
|
||||||
const { rotateLeft, rotateRight, zoomIn, zoomOut, close, left, right, flipX, flipY } = reactive(
|
const { rotateLeft, rotateRight, zoomIn, zoomOut, close, left, right, flipX, flipY } = reactive(
|
||||||
props.icons,
|
props.icons,
|
||||||
);
|
);
|
||||||
|
// 判断是否是自定义placeholder
|
||||||
|
const isCustomPlaceholder = computed(() => isVNode(props.placeholder));
|
||||||
|
const isDefaultPlaceholder = computed(() => {
|
||||||
|
return props.placeholder === true;
|
||||||
|
});
|
||||||
|
const hasPlaceholder = computed(() => isCustomPlaceholder.value || isDefaultPlaceholder.value);
|
||||||
|
const status: Ref<ImageStatus> = ref(hasPlaceholder.value ? 'loading' : 'normal');
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.src,
|
||||||
|
() => {
|
||||||
|
status.value = 'loading';
|
||||||
|
},
|
||||||
|
);
|
||||||
const scale = shallowRef(1);
|
const scale = shallowRef(1);
|
||||||
const rotate = shallowRef(0);
|
const rotate = shallowRef(0);
|
||||||
const flip = reactive({ x: 1, y: 1 });
|
const flip = reactive({ x: 1, y: 1 });
|
||||||
|
|
@ -386,6 +408,11 @@ const Preview = defineComponent({
|
||||||
<img
|
<img
|
||||||
onMousedown={onMouseDown}
|
onMousedown={onMouseDown}
|
||||||
onDblclick={onDoubleClick}
|
onDblclick={onDoubleClick}
|
||||||
|
onLoad={() => {
|
||||||
|
nextTick(() => {
|
||||||
|
status.value = 'normal';
|
||||||
|
});
|
||||||
|
}}
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
class={`${props.prefixCls}-img`}
|
class={`${props.prefixCls}-img`}
|
||||||
src={combinationSrc.value}
|
src={combinationSrc.value}
|
||||||
|
|
@ -396,6 +423,12 @@ const Preview = defineComponent({
|
||||||
}deg)`,
|
}deg)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{isDefaultPlaceholder.value && status.value === 'loading' && (
|
||||||
|
<Spin size="large" class={`${props.prefixCls}-img-placeholder`}></Spin>
|
||||||
|
)}
|
||||||
|
{isCustomPlaceholder.value && status.value === 'loading' && (
|
||||||
|
<div class={`${props.prefixCls}-img-placeholder`}>{props.placeholder}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showLeftOrRightSwitches.value && (
|
{showLeftOrRightSwitches.value && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue