feat(image): add new features (#5479)

* feat(image): add new features

* fix: lint error

* update test

* update code

* update docs

* perf: reset currentIndex after close

* update code

* update code

* update code

* update code

* update code

* add rootClassName props

* fix lint
pull/5502/head
bqy_fe 2022-04-14 09:54:45 +08:00 committed by GitHub
parent 32a145a79f
commit bd87079e12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 413 additions and 100 deletions

View File

@ -4,6 +4,7 @@ export type KeyboardEventHandler = (e: KeyboardEvent) => void;
export type CompositionEventHandler = (e: CompositionEvent) => void; export type CompositionEventHandler = (e: CompositionEvent) => void;
export type ClipboardEventHandler = (e: ClipboardEvent) => void; export type ClipboardEventHandler = (e: ClipboardEvent) => void;
export type ChangeEventHandler = (e: ChangeEvent) => void; export type ChangeEventHandler = (e: ChangeEvent) => void;
export type WheelEventHandler = (e: WheelEvent) => void;
export type ChangeEvent = Event & { export type ChangeEvent = Event & {
target: { target: {
value?: string | undefined; value?: string | undefined;

View File

@ -2,6 +2,24 @@ import PreviewGroup from '../vc-image/src/PreviewGroup';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import useConfigInject from '../_util/hooks/useConfigInject'; import useConfigInject from '../_util/hooks/useConfigInject';
import RotateLeftOutlined from '@ant-design/icons-vue/RotateLeftOutlined';
import RotateRightOutlined from '@ant-design/icons-vue/RotateRightOutlined';
import ZoomInOutlined from '@ant-design/icons-vue/ZoomInOutlined';
import ZoomOutOutlined from '@ant-design/icons-vue/ZoomOutOutlined';
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
export const icons = {
rotateLeft: <RotateLeftOutlined />,
rotateRight: <RotateRightOutlined />,
zoomIn: <ZoomInOutlined />,
zoomOut: <ZoomOutOutlined />,
close: <CloseOutlined />,
left: <LeftOutlined />,
right: <RightOutlined />,
};
const InternalPreviewGroup = defineComponent({ const InternalPreviewGroup = defineComponent({
name: 'AImagePreviewGroup', name: 'AImagePreviewGroup',
inheritAttrs: false, inheritAttrs: false,
@ -13,6 +31,7 @@ const InternalPreviewGroup = defineComponent({
return ( return (
<PreviewGroup <PreviewGroup
{...{ ...attrs, ...props }} {...{ ...attrs, ...props }}
icons={icons}
previewPrefixCls={prefixCls.value} previewPrefixCls={prefixCls.value}
v-slots={slots} v-slots={slots}
></PreviewGroup> ></PreviewGroup>

View File

@ -3,8 +3,11 @@
exports[`renders ./components/image/demo/basic.vue correctly 1`] = ` exports[`renders ./components/image/demo/basic.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"> <div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div> </div>
</div>
<!---->
`; `;
exports[`renders ./components/image/demo/controlled-preview.vue correctly 1`] = ` exports[`renders ./components/image/demo/controlled-preview.vue correctly 1`] = `
@ -13,7 +16,9 @@ exports[`renders ./components/image/demo/controlled-preview.vue correctly 1`] =
</button> </button>
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" style="display: none;" src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"> <div class="ant-image" style="width: 200px;"><img class="ant-image-img" style="display: none;" src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div> </div>
<!----> <!---->
</div> </div>
@ -22,8 +27,11 @@ exports[`renders ./components/image/demo/controlled-preview.vue correctly 1`] =
exports[`renders ./components/image/demo/fallback.vue correctly 1`] = ` exports[`renders ./components/image/demo/fallback.vue correctly 1`] = `
<div class="ant-image" style="width: 200px; height: 200px;"><img class="ant-image-img" src="https://www.antdv.com/#error"> <div class="ant-image" style="width: 200px; height: 200px;"><img class="ant-image-img" src="https://www.antdv.com/#error">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div> </div>
</div>
<!---->
`; `;
exports[`renders ./components/image/demo/placeholder.vue correctly 1`] = ` exports[`renders ./components/image/demo/placeholder.vue correctly 1`] = `
@ -37,7 +45,9 @@ exports[`renders ./components/image/demo/placeholder.vue correctly 1`] = `
</div> </div>
<!----> <!---->
</div> </div>
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div> </div>
<!----> <!---->
</div> </div>
@ -52,35 +62,61 @@ exports[`renders ./components/image/demo/placeholder.vue correctly 1`] = `
exports[`renders ./components/image/demo/preview-group.vue correctly 1`] = ` exports[`renders ./components/image/demo/preview-group.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/vue.png"> <div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/vue.png">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div> </div>
</div>
<!---->
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/logo.png"> <div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/logo.png">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div> </div>
</div>
<!---->
<!---->
`; `;
exports[`renders ./components/image/demo/preview-group-visible.vue correctly 1`] = ` exports[`renders ./components/image/demo/preview-group-visible.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"> <div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div> </div>
</div>
<!---->
<div style="display: none;"> <div style="display: none;">
<div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"> <div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div> </div>
<!----> <!---->
<div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/cV16ZqzMjW/photo-1473091540282-9b846e7965e3.webp"> <div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/cV16ZqzMjW/photo-1473091540282-9b846e7965e3.webp">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div> </div>
<!----> <!---->
<div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/x43I27A55%26/photo-1438109491414-7198515b166b.webp"> <div class="ant-image"><img class="ant-image-img" src="https://gw.alipayobjects.com/zos/antfincdn/x43I27A55%26/photo-1438109491414-7198515b166b.webp">
<!----> <!---->
<!----> <div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div> </div>
<!----> <!---->
<!----> <!---->
</div> </div>
`; `;
exports[`renders ./components/image/demo/preview-src.vue correctly 1`] = `
<div class="ant-image" style="width: 200px;"><img class="ant-image-img" src="https://aliyuncdn.antdv.com/logo.png">
<!---->
<div class="ant-image-mask">
<div class="ant-image-mask-info"><span role="img" aria-label="eye" class="anticon anticon-eye"><svg focusable="false" class="" data-icon="eye" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg></span>Preview</div>
</div>
</div>
<!---->
`;

View File

@ -4,8 +4,9 @@
<fallback /> <fallback />
<placeholder /> <placeholder />
<preview-group /> <preview-group />
<controlled-preview />
<previewGroupVisibleVue /> <previewGroupVisibleVue />
<previewSrc />
<controlled-preview />
</demo-sort> </demo-sort>
</template> </template>
@ -13,6 +14,7 @@
import Basic from './basic.vue'; import Basic from './basic.vue';
import Fallback from './fallback.vue'; import Fallback from './fallback.vue';
import Placeholder from './placeholder.vue'; import Placeholder from './placeholder.vue';
import previewSrc from './preview-src.vue';
import PreviewGroup from './preview-group.vue'; import PreviewGroup from './preview-group.vue';
import ControlledPreview from './controlled-preview.vue'; import ControlledPreview from './controlled-preview.vue';
import previewGroupVisibleVue from './preview-group-visible.vue'; import previewGroupVisibleVue from './preview-group-visible.vue';
@ -26,6 +28,7 @@ export default defineComponent({
components: { components: {
Basic, Basic,
Fallback, Fallback,
previewSrc,
Placeholder, Placeholder,
PreviewGroup, PreviewGroup,
ControlledPreview, ControlledPreview,

View File

@ -18,7 +18,7 @@ Preview a collection from one image.
<template> <template>
<a-image <a-image
:preview="{ visible }" :preview="{ visible: false }"
:width="200" :width="200"
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp" src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"
@click="visible = true" @click="visible = true"

View File

@ -0,0 +1,31 @@
<docs>
---
order: 4
title:
zh-CN: 自定义预览图片
en-US: Custom preview image
---
## zh-CN
可以设置不同的预览图片
## en-US
You can set different preview image.
</docs>
<template>
<a-image
:width="200"
src="https://aliyuncdn.antdv.com/logo.png"
:preview="{
src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
}"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>

View File

@ -22,7 +22,9 @@ Previewable image.
| placeholder | Load placeholder, use default placeholder when set `true` | boolean \| slot | - | 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 | | preview | preview config, disabled when `false` | boolean \| [previewType](#previewType) | true | 2.0.0 |
| src | Image path | string | - | 2.0.0 | | src | Image path | string | - | 2.0.0 |
| previewMask | custom mask | slot | - | 3.2.0 |
| width | Image width | string \| number | - | 2.0.0 | | width | Image width | string \| number | - | 2.0.0 |
| onError | Load failed callback | (event: Event) => void | - | 3.2.0 |
### previewType ### previewType
@ -31,6 +33,9 @@ Previewable image.
visible?: boolean; visible?: boolean;
onVisibleChange?: (visible, prevVisible) => void; onVisibleChange?: (visible, prevVisible) => void;
getContainer?: string | HTMLElement | (() => HTMLElement); getContainer?: string | HTMLElement | (() => HTMLElement);
src?: string;
maskClassName?: string;
current?: number;
} }
``` ```

View File

@ -1,9 +1,12 @@
import type { App, ExtractPropTypes, ImgHTMLAttributes, Plugin } from 'vue'; import type { App, ExtractPropTypes, ImgHTMLAttributes, Plugin } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent, computed } from 'vue';
import ImageInternal from '../vc-image'; import ImageInternal from '../vc-image';
import { imageProps } from '../vc-image/src/Image'; import { imageProps } from '../vc-image/src/Image';
import defaultLocale from '../locale/en_US';
import useConfigInject from '../_util/hooks/useConfigInject'; import useConfigInject from '../_util/hooks/useConfigInject';
import PreviewGroup from './PreviewGroup'; import PreviewGroup, { icons } from './PreviewGroup';
import EyeOutlined from '@ant-design/icons-vue/EyeOutlined';
import { getTransitionName } from '../_util/transition';
export type ImageProps = Partial< export type ImageProps = Partial<
ExtractPropTypes<ReturnType<typeof imageProps>> & ExtractPropTypes<ReturnType<typeof imageProps>> &
@ -14,12 +17,46 @@ const Image = defineComponent<ImageProps>({
inheritAttrs: false, inheritAttrs: false,
props: imageProps() as any, props: imageProps() as any,
setup(props, { slots, attrs }) { setup(props, { slots, attrs }) {
const { prefixCls } = useConfigInject('image', props); const { prefixCls, rootPrefixCls, configProvider } = useConfigInject('image', props);
const mergedPreview = computed(() => {
const { preview } = props;
if (preview === false) {
return preview;
}
const _preview = typeof preview === 'object' ? preview : {};
return {
icons,
..._preview,
transitionName: getTransitionName(rootPrefixCls.value, 'zoom', _preview.transitionName),
maskTransitionName: getTransitionName(
rootPrefixCls.value,
'fade',
_preview.maskTransitionName,
),
};
});
return () => { return () => {
const imageLocale = configProvider.locale?.Image || defaultLocale.Image;
return ( return (
<ImageInternal <ImageInternal
{...{ ...attrs, ...props, prefixCls: prefixCls.value }} {...{ ...attrs, ...props, prefixCls: prefixCls.value }}
v-slots={slots} preview={mergedPreview.value}
v-slots={{
...slots,
previewMask:
slots.previewMask ??
(() => (
<div class={`${prefixCls.value}-mask-info`}>
<EyeOutlined />
{imageLocale?.preview}
</div>
)),
}}
></ImageInternal> ></ImageInternal>
); );
}; };

View File

@ -23,7 +23,9 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/D1dXz9PZqa/image.svg
| placeholder | 加载占位, 为 `true` 时使用默认占位 | boolean \| slot | - | 2.0.0 | | placeholder | 加载占位, 为 `true` 时使用默认占位 | boolean \| slot | - | 2.0.0 |
| preview | 预览参数,为 `false` 时禁用 | boolean \| [previewType](#previewType) | true | 2.0.0 | | preview | 预览参数,为 `false` 时禁用 | boolean \| [previewType](#previewType) | true | 2.0.0 |
| src | 图片地址 | string | - | 2.0.0 | | src | 图片地址 | string | - | 2.0.0 |
| previewMask | 自定义 mask | slot | - | 3.2.0 |
| width | 图像宽度 | string \| number | - | 2.0.0 | | width | 图像宽度 | string \| number | - | 2.0.0 |
| onError | 加载错误回调 | (event: Event) => void | - | 3.2.0 |
### previewType ### previewType
@ -32,6 +34,9 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/D1dXz9PZqa/image.svg
visible?: boolean; visible?: boolean;
onVisibleChange?: (visible, prevVisible) => void; onVisibleChange?: (visible, prevVisible) => void;
getContainer: string | HTMLElement | (() => HTMLElement); getContainer: string | HTMLElement | (() => HTMLElement);
src?: string;
maskClassName?: string;
current?: number;
} }
``` ```

View File

@ -143,6 +143,7 @@ export default defineComponent({
maskAnimation, maskAnimation,
zIndex, zIndex,
wrapClassName, wrapClassName,
rootClassName,
wrapStyle, wrapStyle,
closable, closable,
maskProps, maskProps,
@ -154,7 +155,7 @@ export default defineComponent({
} = props; } = props;
const { style, class: className } = attrs; const { style, class: className } = attrs;
return ( return (
<div class={`${prefixCls}-root`} {...pickAttrs(props, { data: true })}> <div class={[`${prefixCls}-root`, rootClassName]} {...pickAttrs(props, { data: true })}>
<Mask <Mask
prefixCls={prefixCls} prefixCls={prefixCls}
visible={mask && visible} visible={mask && visible}

View File

@ -1,7 +1,7 @@
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
function dialogPropTypes() { export function dialogPropTypes() {
return { return {
keyboard: { type: Boolean, default: undefined }, keyboard: { type: Boolean, default: undefined },
mask: { type: Boolean, default: undefined }, mask: { type: Boolean, default: undefined },
@ -25,6 +25,7 @@ function dialogPropTypes() {
maskStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties }, maskStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
prefixCls: String, prefixCls: String,
wrapClassName: String, wrapClassName: String,
rootClassName: String,
width: [String, Number], width: [String, Number],
height: [String, Number], height: [String, Number],
zIndex: Number, zIndex: Number,

View File

@ -1,21 +1,29 @@
import type { ImgHTMLAttributes, CSSProperties, PropType } from 'vue'; import type { ImgHTMLAttributes, CSSProperties, PropType } from 'vue';
import { ref, watch, defineComponent, computed, onMounted } 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';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { getOffset } from '../../vc-util/Dom/css'; import { getOffset } from '../../vc-util/Dom/css';
import useMergedState from '../../_util/hooks/useMergedState';
import type { MouseEventHandler } from './Preview';
import Preview from './Preview'; import Preview from './Preview';
import type { MouseEventHandler } from '../../_util/EventInterface';
import PreviewGroup, { context } from './PreviewGroup'; import PreviewGroup, { context } from './PreviewGroup';
import type { IDialogChildProps } from '../../vc-dialog/IDialogPropTypes';
export type GetContainer = string | HTMLElement | (() => HTMLElement); export type GetContainer = string | HTMLElement | (() => HTMLElement);
export interface ImagePreviewType { import type { PreviewProps } from './Preview';
export type ImagePreviewType = Omit<
IDialogChildProps,
'mask' | 'visible' | 'closable' | 'prefixCls' | 'onClose' | 'afterClose' | 'wrapClassName'
> & {
src?: string;
visible?: boolean; visible?: boolean;
onVisibleChange?: (value: boolean, prevValue: boolean) => void; onVisibleChange?: (value: boolean, prevValue: boolean) => void;
getContainer?: GetContainer | false; getContainer?: GetContainer | false;
} maskClassName?: string;
icons?: PreviewProps['icons'];
};
export interface ImagePropsType extends Omit<ImgHTMLAttributes, 'placeholder' | 'onClick'> { export interface ImagePropsType extends Omit<ImgHTMLAttributes, 'placeholder' | 'onClick'> {
// Original // Original
@ -27,11 +35,14 @@ export interface ImagePropsType extends Omit<ImgHTMLAttributes, 'placeholder' |
placeholder?: boolean; placeholder?: boolean;
fallback?: string; fallback?: string;
preview?: boolean | ImagePreviewType; preview?: boolean | ImagePreviewType;
onClick?: MouseEventHandler;
onError?: HTMLImageElement['onerror'];
} }
export const imageProps = () => ({ export const imageProps = () => ({
src: String, src: String,
wrapperClassName: String, wrapperClassName: String,
wrapperStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties }, wrapperStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
rootClassName: String,
prefixCls: String, prefixCls: String,
previewPrefixCls: String, previewPrefixCls: String,
placeholder: PropTypes.any, placeholder: PropTypes.any,
@ -40,10 +51,17 @@ export const imageProps = () => ({
type: [Boolean, Object] as PropType<boolean | ImagePreviewType>, type: [Boolean, Object] as PropType<boolean | ImagePreviewType>,
default: true as boolean | ImagePreviewType, default: true as boolean | ImagePreviewType,
}, },
onClick: {
type: Function as PropType<MouseEventHandler>,
},
onError: {
type: Function as PropType<HTMLImageElement['onerror']>,
},
}); });
type ImageStatus = 'normal' | 'error' | 'loading'; export type ImageProps = Partial<ReturnType<typeof imageProps>>;
export type ImageStatus = 'normal' | 'error' | 'loading';
const mergeDefaultValue = <T extends object>(obj: T, defaultValues: object): T => { export const mergeDefaultValue = <T extends object>(obj: T, defaultValues: object): T => {
const res = { ...obj }; const res = { ...obj };
Object.keys(defaultValues).forEach(key => { Object.keys(defaultValues).forEach(key => {
if (obj[key] === undefined) { if (obj[key] === undefined) {
@ -57,11 +75,11 @@ const ImageInternal = defineComponent({
name: 'Image', name: 'Image',
inheritAttrs: false, inheritAttrs: false,
props: imageProps(), props: imageProps(),
emits: ['click'], emits: ['click', 'error'],
setup(props, { attrs, slots, emit }) { setup(props, { attrs, slots, emit }) {
const prefixCls = computed(() => props.prefixCls); const prefixCls = computed(() => props.prefixCls);
const previewPrefixCls = computed(() => `${prefixCls.value}-preview`); const previewPrefixCls = computed(() => `${prefixCls.value}-preview`);
const preview = computed(() => { const preview = computed<ImagePreviewType>(() => {
const defaultValues = { const defaultValues = {
visible: undefined, visible: undefined,
onVisibleChange: () => {}, onVisibleChange: () => {},
@ -75,16 +93,21 @@ const ImageInternal = defineComponent({
() => (props.placeholder && props.placeholder !== true) || slots.placeholder, () => (props.placeholder && props.placeholder !== true) || slots.placeholder,
); );
const previewVisible = computed(() => preview.value.visible); const previewVisible = computed(() => preview.value.visible);
const onPreviewVisibleChange = computed(() => preview.value.onVisibleChange);
const getPreviewContainer = computed(() => preview.value.getContainer); const getPreviewContainer = computed(() => preview.value.getContainer);
const isControlled = computed(() => previewVisible.value !== undefined); const isControlled = computed(() => previewVisible.value !== undefined);
const isShowPreview = ref(!!previewVisible.value);
watch(previewVisible, () => { const onPreviewVisibleChange = (val, preval) => {
isShowPreview.value = !!previewVisible.value; preview.value.onVisibleChange?.(val, preval);
};
const [isShowPreview, setShowPreview] = useMergedState(!!previewVisible.value, {
value: previewVisible,
onChange: onPreviewVisibleChange,
});
watch(previewVisible, val => {
setShowPreview(Boolean(val));
}); });
watch(isShowPreview, (val, preVal) => { watch(isShowPreview, (val, preVal) => {
onPreviewVisibleChange.value(val, preVal); onPreviewVisibleChange(val, preVal);
}); });
const status = ref<ImageStatus>(isCustomPlaceholder.value ? 'loading' : 'normal'); const status = ref<ImageStatus>(isCustomPlaceholder.value ? 'loading' : 'normal');
watch( watch(
@ -108,13 +131,15 @@ const ImageInternal = defineComponent({
const onLoad = () => { const onLoad = () => {
status.value = 'normal'; status.value = 'normal';
}; };
const onError = () => { const onError = (e: Event) => {
status.value = 'error'; status.value = 'error';
emit('error', e);
}; };
const onPreview: MouseEventHandler = e => { const onPreview: MouseEventHandler = e => {
if (!isControlled.value) { if (!isControlled.value) {
const { left, top } = getOffset(e.target); const { left, top } = getOffset(e.target);
if (isPreviewGroup.value) { if (isPreviewGroup.value) {
setCurrent(currentId.value); setCurrent(currentId.value);
setGroupMousePosition({ setGroupMousePosition({
@ -131,13 +156,13 @@ const ImageInternal = defineComponent({
if (isPreviewGroup.value) { if (isPreviewGroup.value) {
setGroupShowPreview(true); setGroupShowPreview(true);
} else { } else {
isShowPreview.value = true; setShowPreview(true);
} }
emit('click', e); emit('click', e);
}; };
const onPreviewClose = () => { const onPreviewClose = () => {
isShowPreview.value = false; setShowPreview(false);
if (!isControlled.value) { if (!isControlled.value) {
mousePosition.value = null; mousePosition.value = null;
} }
@ -163,7 +188,7 @@ const ImageInternal = defineComponent({
return () => {}; return () => {};
} }
unRegister = registerImage(currentId.value, props.src); unRegister = registerImage(currentId.value, props.src, canPreview.value);
if (!canPreview.value) { if (!canPreview.value) {
unRegister(); unRegister();
@ -172,13 +197,21 @@ const ImageInternal = defineComponent({
{ flush: 'post', immediate: true }, { flush: 'post', immediate: true },
); );
}); });
onUnmounted(unRegister);
const toSizePx = (l: number | string) => { const toSizePx = (l: number | string) => {
if (isNumber(l)) return l + 'px'; if (isNumber(l)) return l + 'px';
return l; return l;
}; };
return () => { return () => {
const { prefixCls, wrapperClassName, fallback, src, preview, placeholder, wrapperStyle } = const {
props; prefixCls,
wrapperClassName,
fallback,
src: imgSrc,
placeholder,
wrapperStyle,
rootClassName,
} = props;
const { const {
width, width,
height, height,
@ -191,11 +224,12 @@ const ImageInternal = defineComponent({
class: cls, class: cls,
style, style,
} = attrs as ImgHTMLAttributes; } = attrs as ImgHTMLAttributes;
const wrappperClass = cn(prefixCls, wrapperClassName, { const { icons, maskClassName, src: previewSrc, ...dialogProps } = preview.value;
const wrappperClass = cn(prefixCls, wrapperClassName, rootClassName, {
[`${prefixCls}-error`]: isError.value, [`${prefixCls}-error`]: isError.value,
}); });
const mergedSrc = isError.value && fallback ? fallback : src; const mergedSrc = isError.value && fallback ? fallback : previewSrc ?? imgSrc;
const previewMask = slots.previewMask && slots.previewMask();
const imgCommonProps = { const imgCommonProps = {
crossorigin, crossorigin,
decoding, decoding,
@ -215,12 +249,13 @@ const ImageInternal = defineComponent({
...(style as CSSProperties), ...(style as CSSProperties),
}, },
}; };
return ( return (
<> <>
<div <div
class={wrappperClass} class={wrappperClass}
onClick={ onClick={
preview && !isError.value canPreview.value
? onPreview ? onPreview
: e => { : e => {
emit('click', e); emit('click', e);
@ -238,7 +273,7 @@ const ImageInternal = defineComponent({
? { ? {
src: fallback, src: fallback,
} }
: { onLoad, onError, src })} : { onLoad, onError, src: imgSrc })}
ref={img} ref={img}
/> />
@ -248,12 +283,13 @@ const ImageInternal = defineComponent({
</div> </div>
)} )}
{/* Preview Click Mask */} {/* Preview Click Mask */}
{previewMask && canPreview.value && ( {slots.previewMask && canPreview.value && (
<div class={`${prefixCls}-mask`}>{previewMask}</div> <div class={[`${prefixCls}-mask`, maskClassName]}>{slots.previewMask()}</div>
)} )}
</div> </div>
{!isPreviewGroup.value && canPreview.value && ( {!isPreviewGroup.value && canPreview.value && (
<Preview <Preview
{...dialogProps}
aria-hidden={!isShowPreview.value} aria-hidden={!isShowPreview.value}
visible={isShowPreview.value} visible={isShowPreview.value}
prefixCls={previewPrefixCls.value} prefixCls={previewPrefixCls.value}
@ -262,6 +298,8 @@ const ImageInternal = defineComponent({
src={mergedSrc} src={mergedSrc}
alt={alt} alt={alt}
getContainer={getPreviewContainer.value} getContainer={getPreviewContainer.value}
icons={icons}
rootClassName={rootClassName}
/> />
)} )}
</> </>

View File

@ -1,47 +1,65 @@
import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'; import {
import RotateLeftOutlined from '@ant-design/icons-vue/RotateLeftOutlined'; computed,
import RotateRightOutlined from '@ant-design/icons-vue/RotateRightOutlined'; defineComponent,
import ZoomInOutlined from '@ant-design/icons-vue/ZoomInOutlined'; onMounted,
import ZoomOutOutlined from '@ant-design/icons-vue/ZoomOutOutlined'; onUnmounted,
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; reactive,
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined'; ref,
import RightOutlined from '@ant-design/icons-vue/RightOutlined'; watch,
cloneVNode,
} from 'vue';
import type { VNode, PropType } from 'vue';
import classnames from '../../_util/classNames'; import classnames from '../../_util/classNames';
import Dialog from '../../vc-dialog'; import Dialog from '../../vc-dialog';
import getIDialogPropTypes 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';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import useFrameSetState from './hooks/useFrameSetState'; import useFrameSetState from './hooks/useFrameSetState';
import getFixScaleEleTransPosition from './getFixScaleEleTransPosition'; import getFixScaleEleTransPosition from './getFixScaleEleTransPosition';
import type { MouseEventHandler, WheelEventHandler } from '../../_util/EventInterface';
import { context } from './PreviewGroup'; import { context } from './PreviewGroup';
const IDialogPropTypes = getIDialogPropTypes(); export interface PreviewProps extends Omit<IDialogChildProps, 'onClose' | 'mask'> {
export type MouseEventHandler = (payload: MouseEvent) => void;
export interface PreviewProps extends Omit<typeof IDialogPropTypes, 'onClose'> {
onClose?: (e: Element) => void; onClose?: (e: Element) => void;
src?: string; src?: string;
alt?: string; alt?: string;
rootClassName?: string;
icons?: {
rotateLeft?: VNode;
rotateRight?: VNode;
zoomIn?: VNode;
zoomOut?: VNode;
close?: VNode;
left?: VNode;
right?: VNode;
};
} }
const initialPosition = { const initialPosition = {
x: 0, x: 0,
y: 0, y: 0,
}; };
const PreviewType = { export const previewProps = {
...dialogPropTypes(),
src: String, src: String,
alt: String, alt: String,
...IDialogPropTypes, rootClassName: String,
icons: {
type: Object as PropType<PreviewProps['icons']>,
default: () => ({} as PreviewProps['icons']),
},
}; };
const Preview = defineComponent({ const Preview = defineComponent({
name: 'Preview', name: 'Preview',
inheritAttrs: false, inheritAttrs: false,
props: PreviewType, props: previewProps,
emits: ['close', 'afterClose'], emits: ['close', 'afterClose'],
setup(props, { emit, attrs }) { setup(props, { emit, attrs }) {
const { rotateLeft, rotateRight, zoomIn, zoomOut, close, left, right } = reactive(props.icons);
const scale = ref(1); const scale = ref(1);
const rotate = ref(0); const rotate = ref(0);
const [position, setPosition] = useFrameSetState<{ const [position, setPosition] = useFrameSetState<{
@ -65,22 +83,22 @@ const Preview = defineComponent({
const isMoving = ref(false); const isMoving = ref(false);
const groupContext = context.inject(); const groupContext = context.inject();
const { previewUrls, current, isPreviewGroup, setCurrent } = groupContext; const { previewUrls, current, isPreviewGroup, setCurrent } = groupContext;
const previewGroupCount = computed(() => Object.keys(previewUrls).length); const previewGroupCount = computed(() => previewUrls.value.size);
const previewUrlsKeys = computed(() => Object.keys(previewUrls)); const previewUrlsKeys = computed(() => Array.from(previewUrls.value.keys()));
const currentPreviewIndex = computed(() => const currentPreviewIndex = computed(() => previewUrlsKeys.value.indexOf(current.value));
previewUrlsKeys.value.indexOf(String(current.value)), const combinationSrc = computed(() => {
); return isPreviewGroup.value ? previewUrls.value.get(current.value) : props.src;
const combinationSrc = computed(() => });
isPreviewGroup.value ? previewUrls[current.value] : props.src,
);
const showLeftOrRightSwitches = computed( const showLeftOrRightSwitches = computed(
() => isPreviewGroup.value && previewGroupCount.value > 1, () => isPreviewGroup.value && previewGroupCount.value > 1,
); );
const lastWheelZoomDirection = ref({ wheelDirection: 0 });
const onAfterClose = () => { const onAfterClose = () => {
scale.value = 1; scale.value = 1;
rotate.value = 0; rotate.value = 0;
setPosition(initialPosition); setPosition(initialPosition);
emit('afterClose');
}; };
const onZoomIn = () => { const onZoomIn = () => {
@ -106,7 +124,7 @@ const Preview = defineComponent({
// Without this mask close will abnormal // Without this mask close will abnormal
event.stopPropagation(); event.stopPropagation();
if (currentPreviewIndex.value > 0) { if (currentPreviewIndex.value > 0) {
setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value - 1)]); setCurrent(previewUrlsKeys.value[currentPreviewIndex.value - 1]);
} }
}; };
@ -115,7 +133,7 @@ const Preview = defineComponent({
// Without this mask close will abnormal // Without this mask close will abnormal
event.stopPropagation(); event.stopPropagation();
if (currentPreviewIndex.value < previewGroupCount.value - 1) { if (currentPreviewIndex.value < previewGroupCount.value - 1) {
setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value + 1)]); setCurrent(previewUrlsKeys.value[currentPreviewIndex.value + 1]);
} }
}; };
@ -126,28 +144,28 @@ const Preview = defineComponent({
const iconClassName = `${props.prefixCls}-operations-icon`; const iconClassName = `${props.prefixCls}-operations-icon`;
const tools = [ const tools = [
{ {
icon: CloseOutlined, icon: close,
onClick: onClose, onClick: onClose,
type: 'close', type: 'close',
}, },
{ {
icon: ZoomInOutlined, icon: zoomIn,
onClick: onZoomIn, onClick: onZoomIn,
type: 'zoomIn', type: 'zoomIn',
}, },
{ {
icon: ZoomOutOutlined, icon: zoomOut,
onClick: onZoomOut, onClick: onZoomOut,
type: 'zoomOut', type: 'zoomOut',
disabled: computed(() => scale.value === 1), disabled: computed(() => scale.value === 1),
}, },
{ {
icon: RotateRightOutlined, icon: rotateRight,
onClick: onRotateRight, onClick: onRotateRight,
type: 'rotateRight', type: 'rotateRight',
}, },
{ {
icon: RotateLeftOutlined, icon: rotateLeft,
onClick: onRotateLeft, onClick: onRotateLeft,
type: 'rotateLeft', type: 'rotateLeft',
}, },
@ -175,6 +193,8 @@ const Preview = defineComponent({
}; };
const onMouseDown: MouseEventHandler = event => { const onMouseDown: MouseEventHandler = event => {
// Only allow main button
if (event.button !== 0) return;
event.preventDefault(); event.preventDefault();
// Without this mask close will abnormal // Without this mask close will abnormal
event.stopPropagation(); event.stopPropagation();
@ -193,6 +213,14 @@ const Preview = defineComponent({
}); });
} }
}; };
const onWheelMove: WheelEventHandler = event => {
if (!props.visible) return;
event.preventDefault();
const wheelDirection = event.deltaY;
lastWheelZoomDirection.value = { wheelDirection };
};
let removeListeners = () => {}; let removeListeners = () => {};
onMounted(() => { onMounted(() => {
watch( watch(
@ -204,6 +232,9 @@ const Preview = defineComponent({
const onMouseUpListener = addEventListener(window, 'mouseup', onMouseUp, false); const onMouseUpListener = addEventListener(window, 'mouseup', onMouseUp, false);
const onMouseMoveListener = addEventListener(window, 'mousemove', onMouseMove, false); const onMouseMoveListener = addEventListener(window, 'mousemove', onMouseMove, false);
const onScrollWheelListener = addEventListener(window, 'wheel', onWheelMove, {
passive: false,
});
try { try {
// Resolve if in iframe lost event // Resolve if in iframe lost event
@ -225,6 +256,7 @@ const Preview = defineComponent({
removeListeners = () => { removeListeners = () => {
onMouseUpListener.remove(); onMouseUpListener.remove();
onMouseMoveListener.remove(); onMouseMoveListener.remove();
onScrollWheelListener.remove();
/* istanbul ignore next */ /* istanbul ignore next */
if (onTopMouseUpListener) onTopMouseUpListener.remove(); if (onTopMouseUpListener) onTopMouseUpListener.remove();
@ -240,6 +272,8 @@ const Preview = defineComponent({
}); });
return () => { return () => {
const { visible, prefixCls, rootClassName } = props;
return ( return (
<Dialog <Dialog
{...attrs} {...attrs}
@ -247,11 +281,12 @@ const Preview = defineComponent({
maskTransitionName="fade" maskTransitionName="fade"
closable={false} closable={false}
keyboard keyboard
prefixCls={props.prefixCls} prefixCls={prefixCls}
onClose={onClose} onClose={onClose}
afterClose={onAfterClose} afterClose={onAfterClose}
visible={props.visible} visible={visible}
wrapClassName={wrapClassName} wrapClassName={wrapClassName}
rootClassName={rootClassName}
getContainer={props.getContainer} getContainer={props.getContainer}
> >
<ul class={`${props.prefixCls}-operations`}> <ul class={`${props.prefixCls}-operations`}>
@ -263,7 +298,7 @@ const Preview = defineComponent({
onClick={onClick} onClick={onClick}
key={type} key={type}
> >
<IconType class={iconClassName} /> {cloneVNode(IconType, { class: iconClassName })}
</li> </li>
))} ))}
</ul> </ul>
@ -291,7 +326,7 @@ const Preview = defineComponent({
})} })}
onClick={onSwitchLeft} onClick={onSwitchLeft}
> >
<LeftOutlined /> {left}
</div> </div>
)} )}
{showLeftOrRightSwitches.value && ( {showLeftOrRightSwitches.value && (
@ -302,7 +337,7 @@ const Preview = defineComponent({
})} })}
onClick={onSwitchRight} onClick={onSwitchRight}
> >
<RightOutlined /> {right}
</div> </div>
)} )}
</Dialog> </Dialog>

View File

@ -1,19 +1,39 @@
import type { Ref } from 'vue'; import type { PropType, Ref, ComputedRef } from 'vue';
import { ref, provide, defineComponent, inject, reactive } from 'vue'; import { ref, provide, defineComponent, inject, watch, reactive, computed, watchEffect } from 'vue';
import { type ImagePreviewType, mergeDefaultValue } from './Image';
import Preview from './Preview'; import Preview from './Preview';
import type { PreviewProps } from './Preview';
export interface PreviewGroupPreview
extends Omit<ImagePreviewType, 'icons' | 'mask' | 'maskClassName'> {
/**
* If Preview the show img index
* @default 0
*/
current?: number;
}
export interface GroupConsumerProps { export interface GroupConsumerProps {
previewPrefixCls?: string; previewPrefixCls?: string;
icons?: PreviewProps['icons'];
preview?: boolean | PreviewGroupPreview;
} }
interface PreviewUrl {
url: string;
canPreview: boolean;
}
export interface GroupConsumerValue extends GroupConsumerProps { export interface GroupConsumerValue extends GroupConsumerProps {
isPreviewGroup?: Ref<boolean | undefined>; isPreviewGroup?: Ref<boolean | undefined>;
previewUrls: Record<number, string>; previewUrls: ComputedRef<Map<number, string>>;
setPreviewUrls: (previewUrls: Record<number, string>) => void; setPreviewUrls: (id: number, url: string, canPreview?: boolean) => void;
current: Ref<number>; current: Ref<number>;
setCurrent: (current: number) => void; setCurrent: (current: number) => void;
setShowPreview: (isShowPreview: boolean) => void; setShowPreview: (isShowPreview: boolean) => void;
setMousePosition: (mousePosition: null | { x: number; y: number }) => void; setMousePosition: (mousePosition: null | { x: number; y: number }) => void;
registerImage: (id: number, url: string) => () => void; registerImage: (id: number, url: string, canPreview?: boolean) => () => void;
rootClassName?: string;
} }
const previewGroupContext = Symbol('previewGroupContext'); const previewGroupContext = Symbol('previewGroupContext');
export const context = { export const context = {
@ -23,13 +43,14 @@ export const context = {
inject: () => { inject: () => {
return inject<GroupConsumerValue>(previewGroupContext, { return inject<GroupConsumerValue>(previewGroupContext, {
isPreviewGroup: ref(false), isPreviewGroup: ref(false),
previewUrls: reactive({}), previewUrls: computed(() => new Map()),
setPreviewUrls: () => {}, setPreviewUrls: () => {},
current: ref(null), current: ref(null),
setCurrent: () => {}, setCurrent: () => {},
setShowPreview: () => {}, setShowPreview: () => {},
setMousePosition: () => {}, setMousePosition: () => {},
registerImage: null, registerImage: null,
rootClassName: '',
}); });
}, },
}; };
@ -37,14 +58,56 @@ export const context = {
const Group = defineComponent({ const Group = defineComponent({
name: 'PreviewGroup', name: 'PreviewGroup',
inheritAttrs: false, inheritAttrs: false,
props: { previewPrefixCls: String }, props: {
previewPrefixCls: String,
preview: {
type: [Boolean, Object] as PropType<boolean | ImagePreviewType>,
default: true as boolean | ImagePreviewType,
},
icons: {
type: Object as PropType<PreviewProps['icons']>,
default: () => ({}),
},
},
setup(props, { slots }) { setup(props, { slots }) {
const previewUrls = reactive<Record<number, string>>({}); const preview = computed<PreviewGroupPreview>(() => {
const defaultValues = {
visible: undefined,
onVisibleChange: () => {},
getContainer: undefined,
current: 0,
};
return typeof props.preview === 'object'
? mergeDefaultValue(props.preview, defaultValues)
: defaultValues;
});
const previewUrls = reactive<Map<number, PreviewUrl>>(new Map());
const current = ref<number>(); const current = ref<number>();
const isShowPreview = ref<boolean>(false);
const previewVisible = computed(() => preview.value.visible);
const onPreviewVisibleChange = computed(() => preview.value.onVisibleChange);
const getPreviewContainer = computed(() => preview.value.getContainer);
const isShowPreview = ref(!!previewVisible.value);
const mousePosition = ref<{ x: number; y: number }>(null); const mousePosition = ref<{ x: number; y: number }>(null);
const setPreviewUrls = (val: Record<number, string>) => { const isControlled = computed(() => previewVisible.value !== undefined);
Object.assign(previewUrls, val); const previewUrlsKeys = computed(() => Array.from(previewUrls.keys()));
const currentControlledKey = computed(() => previewUrlsKeys.value[preview.value.current]);
const canPreviewUrls = computed(
() =>
new Map<number, string>(
Array.from(previewUrls)
.filter(([, { canPreview }]) => !!canPreview)
.map(([id, { url }]) => [id, url]),
),
);
const setPreviewUrls = (id: number, url: string, canPreview = true) => {
previewUrls.set(id, {
url,
canPreview,
});
}; };
const setCurrent = (val: number) => { const setCurrent = (val: number) => {
current.value = val; current.value = val;
@ -55,21 +118,54 @@ const Group = defineComponent({
const setShowPreview = (val: boolean) => { const setShowPreview = (val: boolean) => {
isShowPreview.value = val; isShowPreview.value = val;
}; };
const registerImage = (id: number, url: string) => {
previewUrls[id] = url;
return () => { const registerImage = (id: number, url: string, canPreview = true) => {
delete previewUrls[id]; const unRegister = () => {
previewUrls.delete(id);
}; };
previewUrls.set(id, {
url,
canPreview,
});
return unRegister;
}; };
const onPreviewClose = (e: any) => { const onPreviewClose = (e: any) => {
e?.stopPropagation(); e?.stopPropagation();
isShowPreview.value = false; isShowPreview.value = false;
mousePosition.value = null; mousePosition.value = null;
}; };
watch(previewVisible, () => {
isShowPreview.value = !!previewVisible.value;
});
watch(isShowPreview, (val, preVal) => {
onPreviewVisibleChange.value(val, preVal);
});
watch(
currentControlledKey,
val => {
setCurrent(val);
},
{
immediate: true,
flush: 'post',
},
);
watchEffect(
() => {
if (!isShowPreview.value && isControlled.value) {
setCurrent(currentControlledKey.value);
}
},
{
flush: 'post',
},
);
context.provide({ context.provide({
isPreviewGroup: ref(true), isPreviewGroup: ref(true),
previewUrls, previewUrls: canPreviewUrls,
setPreviewUrls, setPreviewUrls,
current, current,
setCurrent, setCurrent,
@ -77,17 +173,22 @@ const Group = defineComponent({
setMousePosition, setMousePosition,
registerImage, registerImage,
}); });
return () => { return () => {
const { ...dialogProps } = preview.value;
return ( return (
<> <>
{slots.default && slots.default()} {slots.default && slots.default()}
<Preview <Preview
{...dialogProps}
ria-hidden={!isShowPreview.value} ria-hidden={!isShowPreview.value}
visible={isShowPreview.value} visible={isShowPreview.value}
prefixCls={props.previewPrefixCls} prefixCls={props.previewPrefixCls}
onClose={onPreviewClose} onClose={onPreviewClose}
mousePosition={mousePosition.value} mousePosition={mousePosition.value}
src={previewUrls[current.value]} src={canPreviewUrls.value.get(current.value)}
icons={props.icons}
getContainer={getPreviewContainer.value}
/> />
</> </>
); );