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
shuoxian 2025-08-22 12:24:52 +08:00
parent 35c1ad9c80
commit 9f465b25a4
5 changed files with 71 additions and 7 deletions

View File

@ -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.
</docs>
@ -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),
}"
/>
<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>
<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>

View File

@ -43,6 +43,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA
src?: string;
maskClassName?: string;
current?: number;
placeholder?: VNode | boolean;
}
```

View File

@ -193,6 +193,9 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
transition: `transform ${motionDurationSlow} ${motionEaseOut} 0s`,
userSelect: 'none',
pointerEvents: 'auto',
'&-placeholder': {
position: 'absolute',
},
'&-wrapper': {
...genBoxStyle(),
@ -214,7 +217,6 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
},
},
},
[`${previewCls}-moving`]: {
[`${previewCls}-preview-img`]: {
cursor: 'grabbing',

View File

@ -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}

View File

@ -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<VNode | boolean>,
default: () => true,
},
rootClassName: String,
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(
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 rotate = shallowRef(0);
const flip = reactive({ x: 1, y: 1 });
@ -386,6 +408,11 @@ const Preview = defineComponent({
<img
onMousedown={onMouseDown}
onDblclick={onDoubleClick}
onLoad={() => {
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' && (
<Spin size="large" class={`${props.prefixCls}-img-placeholder`}></Spin>
)}
{isCustomPlaceholder.value && status.value === 'loading' && (
<div class={`${props.prefixCls}-img-placeholder`}>{props.placeholder}</div>
)}
</div>
{showLeftOrRightSwitches.value && (
<div