diff --git a/components/image/demo/preview-src.vue b/components/image/demo/preview-src.vue index 2b2d4629d9..f74bde67eb 100644 --- a/components/image/demo/preview-src.vue +++ b/components/image/demo/preview-src.vue @@ -8,20 +8,44 @@ 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. + + diff --git a/components/image/index.en-US.md b/components/image/index.en-US.md index 5200aba318..34642b8bde 100644 --- a/components/image/index.en-US.md +++ b/components/image/index.en-US.md @@ -19,12 +19,12 @@ Previewable image. | --- | --- | --- | --- | --- | | alt | Image description | string | - | 2.0.0 | | fallback | Load failure fault-tolerant src | string | - | 2.0.0 | -| height | Image height | string \| number | - | 2.0.0 | -| placeholder | Load placeholder, use default placeholder when set `true` | boolean \| slot | - | 2.0.0 | -| preview | preview config, disabled when `false` | boolean \| [previewType](#previewtype) | true | 2.0.0 | +| height | Image height | string\| number | - | 2.0.0 | +| placeholder | Load placeholder, use default placeholder when set `true` | boolean\| slot | - | 2.0.0 | +| preview | preview config, disabled when `false` | boolean\| [previewType](#previewtype) | true | 2.0.0 | | src | Image path | string | - | 2.0.0 | -| previewMask | custom mask | false \| function \| slot | - | 3.2.0 | -| width | Image width | string \| number | - | 2.0.0 | +| previewMask | custom mask | false\| function \| slot | - | 3.2.0 | +| width | Image width | string\| number | - | 2.0.0 | ### events @@ -38,11 +38,12 @@ Previewable image. { visible?: boolean; onVisibleChange?: (visible, prevVisible) => void; - getContainer?: string | HTMLElement | (() => HTMLElement); + getContainer: string | HTMLElement | (() => HTMLElement); src?: string; maskClassName?: string; current?: number; + placeholder?: (() => vNode) | boolean; } ``` -Other attributes [<img>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes) +Other attributes [<img>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes) diff --git a/components/image/index.zh-CN.md b/components/image/index.zh-CN.md index b5a2f148f7..28bc91f113 100644 --- a/components/image/index.zh-CN.md +++ b/components/image/index.zh-CN.md @@ -20,12 +20,12 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA | --- | --- | --- | --- | --- | | alt | 图像描述 | string | - | 2.0.0 | | fallback | 加载失败容错地址 | string | - | 2.0.0 | -| height | 图像高度 | string \| number | - | 2.0.0 | -| placeholder | 加载占位, 为 `true` 时使用默认占位 | boolean \| slot | - | 2.0.0 | -| preview | 预览参数,为 `false` 时禁用 | boolean \| [previewType](#previewtype) | true | 2.0.0 | +| height | 图像高度 | string\| number | - | 2.0.0 | +| placeholder | 加载占位, 为 `true` 时使用默认占位 | boolean\| slot | - | 2.0.0 | +| preview | 预览参数,为 `false` 时禁用 | boolean\| [previewType](#previewtype) | true | 2.0.0 | | src | 图片地址 | string | - | 2.0.0 | -| previewMask | 自定义 mask | false \| function \| slot | - | 3.2.0 | -| width | 图像宽度 | string \| number | - | 2.0.0 | +| previewMask | 自定义 mask | false\| function \| slot | - | 3.2.0 | +| width | 图像宽度 | string\| number | - | 2.0.0 | ### 事件 @@ -43,7 +43,8 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*LVQ3R5JjjJEAAA src?: string; maskClassName?: string; current?: number; + placeholder?: (() => vNode) | boolean; } ``` -其他属性见 [<img>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes) +其他属性见 [<img>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes) diff --git a/components/image/style/index.ts b/components/image/style/index.ts index 8433f90dd7..cdf0d00a63 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 643708dc52..0aa53c1bb8 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?: boolean | (() => VNode); 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 80d03fc70e..36efc1b15a 100644 --- a/components/vc-image/src/Preview.tsx +++ b/components/vc-image/src/Preview.tsx @@ -7,11 +7,14 @@ import { shallowRef, watch, cloneVNode, + ref, + 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'; @@ -28,6 +31,7 @@ export interface PreviewProps extends Omit VNode); icons?: { rotateLeft?: VNode; rotateRight?: VNode; @@ -49,6 +53,11 @@ export const previewProps = { ...dialogPropTypes(), src: String, alt: String, + fallback: String, + placeholder: { + type: [Boolean, Function] as PropType VNode)>, + default: () => true, + }, rootClassName: String, icons: { type: Object as PropType, @@ -65,7 +74,25 @@ const Preview = defineComponent({ const { rotateLeft, rotateRight, zoomIn, zoomOut, close, left, right, flipX, flipY } = reactive( props.icons, ); + // 判断是否是自定义placeholder + const isCustomPlaceholder = computed(() => typeof props.placeholder === 'function'); + + 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 renderPlaceholder = () => { + const { placeholder } = props; + return typeof placeholder === 'function' ? placeholder() : placeholder; + }; const scale = shallowRef(1); const rotate = shallowRef(0); const flip = reactive({ x: 1, y: 1 }); @@ -386,6 +413,11 @@ const Preview = defineComponent({ { + nextTick(() => { + status.value = 'normal'; + }); + }} ref={imgRef} class={`${props.prefixCls}-img`} src={combinationSrc.value} @@ -396,6 +428,12 @@ const Preview = defineComponent({ }deg)`, }} /> + {isDefaultPlaceholder.value && status.value === 'loading' && ( + + )} + {isCustomPlaceholder.value && status.value === 'loading' && ( +
{renderPlaceholder()}
+ )} {showLeftOrRightSwitches.value && (