diff --git a/components/image/demo/preview-src.vue b/components/image/demo/preview-src.vue
index 2b2d4629d..ce4570bf8 100644
--- a/components/image/demo/preview-src.vue
+++ b/components/image/demo/preview-src.vue
@@ -8,11 +8,11 @@ title:
## zh-CN
-可以设置不同的预览图片。
+可以设置不同的预览图片。可以自定义预览图的加载占位符(placeholder)。
## 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.
@@ -21,7 +21,31 @@ You can set different preview image.
:width="200"
src="https://aliyuncdn.antdv.com/logo.png"
: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),
+ }"
+ />
+
+
+
diff --git a/components/image/index.zh-CN.md b/components/image/index.zh-CN.md
index b5a2f148f..e0b3fe22a 100644
--- a/components/image/index.zh-CN.md
+++ b/components/image/index.zh-CN.md
@@ -43,6 +43,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA
src?: string;
maskClassName?: string;
current?: number;
+ placeholder?: VNode | boolean;
}
```
diff --git a/components/image/style/index.ts b/components/image/style/index.ts
index 8433f90dd..cdf0d00a6 100644
--- a/components/image/style/index.ts
+++ b/components/image/style/index.ts
@@ -193,6 +193,9 @@ export const genImagePreviewStyle: GenerateStyle = (token: ImageToke
transition: `transform ${motionDurationSlow} ${motionEaseOut} 0s`,
userSelect: 'none',
pointerEvents: 'auto',
+ '&-placeholder': {
+ position: 'absolute',
+ },
'&-wrapper': {
...genBoxStyle(),
@@ -214,7 +217,6 @@ export const genImagePreviewStyle: GenerateStyle = (token: ImageToke
},
},
},
-
[`${previewCls}-moving`]: {
[`${previewCls}-preview-img`]: {
cursor: 'grabbing',
diff --git a/components/vc-image/src/Image.tsx b/components/vc-image/src/Image.tsx
index 643708dc5..5757c5282 100644
--- a/components/vc-image/src/Image.tsx
+++ b/components/vc-image/src/Image.tsx
@@ -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 isNumber from 'lodash-es/isNumber';
import cn from '../../_util/classNames';
@@ -19,6 +19,8 @@ export type ImagePreviewType = Omit<
> & {
src?: string;
visible?: boolean;
+ fallback?: string;
+ placeholder?: VNode | boolean;
onVisibleChange?: (value: boolean, prevValue: boolean) => void;
getContainer?: GetContainer | false;
maskClassName?: string;
@@ -79,6 +81,7 @@ const ImageInternal = defineComponent({
onVisibleChange: () => {},
getContainer: undefined,
};
+
return typeof props.preview === 'object'
? mergeDefaultValue(props.preview, defaultValues)
: defaultValues;
@@ -288,6 +291,7 @@ const ImageInternal = defineComponent({
onClose={onPreviewClose}
mousePosition={mousePosition.value}
src={mergedSrc}
+ placeholder={preview.value.placeholder}
alt={alt}
getContainer={getPreviewContainer.value}
icons={icons}
diff --git a/components/vc-image/src/Preview.tsx b/components/vc-image/src/Preview.tsx
index 80d03fc70..0ab3dac66 100644
--- a/components/vc-image/src/Preview.tsx
+++ b/components/vc-image/src/Preview.tsx
@@ -7,11 +7,15 @@ import {
shallowRef,
watch,
cloneVNode,
+ ref,
+ isVNode,
+ nextTick,
} 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 Dialog from '../../vc-dialog';
+import Spin from '../../spin';
import { type IDialogChildProps, dialogPropTypes } from '../../vc-dialog/IDialogPropTypes';
import { getOffset } from '../../vc-util/Dom/css';
import addEventListener from '../../vc-util/Dom/addEventListener';
@@ -49,6 +53,11 @@ export const previewProps = {
...dialogPropTypes(),
src: String,
alt: String,
+ fallback: String,
+ placeholder: {
+ type: Object as PropType,
+ default: () => true,
+ },
rootClassName: String,
icons: {
type: Object as PropType,
@@ -65,7 +74,20 @@ const Preview = defineComponent({
const { rotateLeft, rotateRight, zoomIn, zoomOut, close, left, right, flipX, flipY } = reactive(
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 = ref(hasPlaceholder.value ? 'loading' : 'normal');
+ watch(
+ () => props.src,
+ () => {
+ status.value = 'loading';
+ },
+ );
const scale = shallowRef(1);
const rotate = shallowRef(0);
const flip = reactive({ x: 1, y: 1 });
@@ -386,6 +408,11 @@ const Preview = defineComponent({
{
+ nextTick(() => {
+ status.value = 'normal';
+ });
+ }}
ref={imgRef}
class={`${props.prefixCls}-img`}
src={combinationSrc.value}
@@ -396,6 +423,12 @@ const Preview = defineComponent({
}deg)`,
}}
/>
+ {isDefaultPlaceholder.value && status.value === 'loading' && (
+
+ )}
+ {isCustomPlaceholder.value && status.value === 'loading' && (
+ {props.placeholder}
+ )}
{showLeftOrRightSwitches.value && (