feat: add image (#3235)
* feat: 🆕 image组件 - 添加样式 * feat: 🆕 增加 AImage 组件 * feat: 🆕 参考 ant-design 增加 相关 Image 组件的less定义 * feat: 🆕 Image placeholder 的修改 * fix: 🐞 去除 onPreviewClose 相关 * test: 🧪 image width/height 以及相关测试 * fix: 🐞 fix lint no-setup-props-destructure * test: 🧪 image test snap file * refactor: 💡 去掉多加了的文件,重构文件以使逻辑清晰 * feat: 🆕 rc-image 相关内容 列入 vc-image 文件夹中 * feat: 🆕 antd 4.9.1 增加 image-preview-group * feat: 🆕 add ImagePropsType * feat: udpate image components * feat: update image * feat: update image Co-authored-by: tanjinzhou <415800467@qq.com>pull/3433/head
							parent
							
								
									c6b189b583
								
							
						
					
					
						commit
						5913cf9c5c
					
				|  | @ -1 +1 @@ | |||
| Subproject commit ab88ac19de8ce0d3c8a559ad7bcd5fc04532c184 | ||||
| Subproject commit db458a2276cd9156a7824f4e876de5702efd9ff7 | ||||
|  | @ -0,0 +1,25 @@ | |||
| import PreviewGroup from '../vc-image/src/PreviewGroup'; | ||||
| import { defineComponent, inject } from 'vue'; | ||||
| import { defaultConfigProvider } from '../config-provider'; | ||||
| import PropTypes from '../_util/vue-types'; | ||||
| 
 | ||||
| const InternalPreviewGroup = defineComponent({ | ||||
|   name: 'AImagePreviewGroup', | ||||
|   inheritAttrs: false, | ||||
|   props: { previewPrefixCls: PropTypes.string }, | ||||
|   setup(props, { attrs, slots }) { | ||||
|     const configProvider = inject('configProvider', defaultConfigProvider); | ||||
|     return () => { | ||||
|       const { getPrefixCls } = configProvider; | ||||
|       const prefixCls = getPrefixCls('image-preview', props.previewPrefixCls); | ||||
|       return ( | ||||
|         <PreviewGroup | ||||
|           previewPrefixCls={prefixCls} | ||||
|           {...{ ...attrs, ...props }} | ||||
|           v-slots={slots} | ||||
|         ></PreviewGroup> | ||||
|       ); | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| export default InternalPreviewGroup; | ||||
|  | @ -0,0 +1,3 @@ | |||
| import demoTest from '../../../tests/shared/demoTest'; | ||||
| 
 | ||||
| demoTest('image'); | ||||
|  | @ -0,0 +1,32 @@ | |||
| import Image from '..'; | ||||
| import mountTest from '../../../tests/shared/mountTest'; | ||||
| import { mount } from '@vue/test-utils'; | ||||
| describe('Image', () => { | ||||
|   mountTest(Image); | ||||
|   it('image size', () => { | ||||
|     const wrapper = mount({ | ||||
|       render() { | ||||
|         return ( | ||||
|           <Image | ||||
|             width="200px" | ||||
|             src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" | ||||
|           /> | ||||
|         ); | ||||
|       }, | ||||
|     }); | ||||
|     expect(wrapper.find('.ant-image').element.style.width).toBe('200px'); | ||||
|   }); | ||||
|   it('image size number', () => { | ||||
|     const wrapper = mount({ | ||||
|       render() { | ||||
|         return ( | ||||
|           <Image | ||||
|             width={200} | ||||
|             src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" | ||||
|           /> | ||||
|         ); | ||||
|       }, | ||||
|     }); | ||||
|     expect(wrapper.find('.ant-image').element.style.width).toBe('200px'); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,35 @@ | |||
| import { App, defineComponent, inject, Plugin } from 'vue'; | ||||
| import { defaultConfigProvider } from '../config-provider'; | ||||
| import ImageInternal from '../vc-image'; | ||||
| import { ImageProps, ImagePropsType } from '../vc-image/src/Image'; | ||||
| 
 | ||||
| import PreviewGroup from './PreviewGroup'; | ||||
| const Image = defineComponent({ | ||||
|   name: 'AImage', | ||||
|   inheritAttrs: false, | ||||
|   props: ImageProps, | ||||
|   setup(props, ctx) { | ||||
|     const { slots, attrs } = ctx; | ||||
|     const configProvider = inject('configProvider', defaultConfigProvider); | ||||
|     return () => { | ||||
|       const { getPrefixCls } = configProvider; | ||||
|       const prefixCls = getPrefixCls('image', props.prefixCls); | ||||
|       return <ImageInternal {...{ ...attrs, ...props, prefixCls }} v-slots={slots}></ImageInternal>; | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| export { ImageProps, ImagePropsType }; | ||||
| 
 | ||||
| Image.PreviewGroup = PreviewGroup; | ||||
| 
 | ||||
| Image.install = function(app: App) { | ||||
|   app.component(Image.name, Image); | ||||
|   app.component(Image.PreviewGroup.name, Image.PreviewGroup); | ||||
|   return app; | ||||
| }; | ||||
| 
 | ||||
| export default Image as typeof Image & | ||||
|   Plugin & { | ||||
|     readonly PreviewGroup: typeof PreviewGroup; | ||||
|   }; | ||||
|  | @ -0,0 +1,141 @@ | |||
| @import '../../style/themes/index'; | ||||
| @import '../../style/mixins/index'; | ||||
| 
 | ||||
| @image-prefix-cls: ~'@{ant-prefix}-image'; | ||||
| @image-preview-prefix-cls: ~'@{image-prefix-cls}-preview'; | ||||
| 
 | ||||
| .@{image-prefix-cls} { | ||||
|   position: relative; | ||||
|   display: inline-block; | ||||
|   &-img { | ||||
|     width: 100%; | ||||
|     height: auto; | ||||
|     &-placeholder { | ||||
|       background-color: @image-bg; | ||||
|       background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTQuNSAyLjVoLTEzQS41LjUgMCAwIDAgMSAzdjEwYS41LjUgMCAwIDAgLjUuNWgxM2EuNS41IDAgMCAwIC41LS41VjNhLjUuNSAwIDAgMC0uNS0uNXpNNS4yODEgNC43NWExIDEgMCAwIDEgMCAyIDEgMSAwIDAgMSAwLTJ6bTguMDMgNi44M2EuMTI3LjEyNyAwIDAgMS0uMDgxLjAzSDIuNzY5YS4xMjUuMTI1IDAgMCAxLS4wOTYtLjIwN2wyLjY2MS0zLjE1NmEuMTI2LjEyNiAwIDAgMSAuMTc3LS4wMTZsLjAxNi4wMTZMNy4wOCAxMC4wOWwyLjQ3LTIuOTNhLjEyNi4xMjYgMCAwIDEgLjE3Ny0uMDE2bC4wMTUuMDE2IDMuNTg4IDQuMjQ0YS4xMjcuMTI3IDAgMCAxLS4wMi4xNzV6IiBmaWxsPSIjOEM4QzhDIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4=); | ||||
|       background-repeat: no-repeat; | ||||
|       background-position: center center; | ||||
|       background-size: 30%; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &-placeholder { | ||||
|     .box; | ||||
|   } | ||||
| 
 | ||||
|   &-preview { | ||||
|     .modal-mask; | ||||
| 
 | ||||
|     height: 100%; | ||||
|     text-align: center; | ||||
| 
 | ||||
|     &-body { | ||||
|       .box; | ||||
|       overflow: hidden; | ||||
|     } | ||||
| 
 | ||||
|     &-img { | ||||
|       max-width: 100%; | ||||
|       max-height: 100%; | ||||
|       vertical-align: middle; | ||||
|       transform: scale3d(1, 1, 1); | ||||
|       cursor: grab; | ||||
|       transition: transform 0.3s @ease-out 0s; | ||||
|       user-select: none; | ||||
|       pointer-events: auto; | ||||
|       &-wrapper { | ||||
|         .box; | ||||
|         transition: transform 0.3s @ease-out 0s; | ||||
|         &::before { | ||||
|           display: inline-block; | ||||
|           width: 1px; | ||||
|           height: 50%; | ||||
|           margin-right: -1px; | ||||
|           content: ''; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-moving { | ||||
|       .@{image-prefix-cls}-preview-img { | ||||
|         cursor: grabbing; | ||||
|         &-wrapper { | ||||
|           transition-duration: 0s; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-wrap { | ||||
|       z-index: @zindex-image; | ||||
|     } | ||||
| 
 | ||||
|     &-operations { | ||||
|       .reset-component; | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       z-index: 1; | ||||
|       display: flex; | ||||
|       flex-direction: row-reverse; | ||||
|       align-items: center; | ||||
|       width: 100%; | ||||
|       color: @image-preview-operation-color; | ||||
|       list-style: none; | ||||
|       background: fade(@modal-mask-bg, 10%); | ||||
|       pointer-events: auto; | ||||
| 
 | ||||
|       &-operation { | ||||
|         margin-left: @control-padding-horizontal; | ||||
|         padding: @control-padding-horizontal; | ||||
|         cursor: pointer; | ||||
|         &-disabled { | ||||
|           color: @image-preview-operation-disabled-color; | ||||
|           pointer-events: none; | ||||
|         } | ||||
|         &:last-of-type { | ||||
|           margin-left: 0; | ||||
|         } | ||||
|       } | ||||
|       &-icon { | ||||
|         font-size: @image-preview-operation-size; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-switch-left, | ||||
|     &-switch-right { | ||||
|       position: absolute; | ||||
|       top: 50%; | ||||
|       right: 10px; | ||||
|       z-index: 1; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       width: 44px; | ||||
|       height: 44px; | ||||
|       margin-top: -22px; | ||||
|       color: @image-preview-operation-color; | ||||
|       background: fade(@modal-mask-bg, 10%); | ||||
|       border-radius: 50%; | ||||
|       cursor: pointer; | ||||
|       pointer-events: auto; | ||||
|       &-disabled { | ||||
|         color: @image-preview-operation-disabled-color; | ||||
|         cursor: not-allowed; | ||||
|         > .anticon { | ||||
|           cursor: not-allowed; | ||||
|         } | ||||
|       } | ||||
|       > .anticon { | ||||
|         font-size: 18px; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-switch-left { | ||||
|       left: 10px; | ||||
|     } | ||||
| 
 | ||||
|     &-switch-right { | ||||
|       right: 10px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,2 @@ | |||
| import '../../style/index.less'; | ||||
| import './index.less'; | ||||
|  | @ -133,7 +133,7 @@ import { default as Drawer } from './drawer'; | |||
| import { default as Skeleton } from './skeleton'; | ||||
| 
 | ||||
| import { default as Comment } from './comment'; | ||||
| 
 | ||||
| import { default as Image } from './image'; | ||||
| // import { default as ColorPicker } from './color-picker';
 | ||||
| 
 | ||||
| import { default as ConfigProvider } from './config-provider'; | ||||
|  | @ -209,6 +209,7 @@ const components = [ | |||
|   Descriptions, | ||||
|   PageHeader, | ||||
|   Space, | ||||
|   Image, | ||||
| ]; | ||||
| 
 | ||||
| const install = function(app: App) { | ||||
|  | @ -296,6 +297,7 @@ export { | |||
|   Descriptions, | ||||
|   PageHeader, | ||||
|   Space, | ||||
|   Image, | ||||
| }; | ||||
| 
 | ||||
| export default { | ||||
|  |  | |||
|  | @ -60,4 +60,5 @@ import './descriptions/style'; | |||
| import './page-header/style'; | ||||
| import './form/style'; | ||||
| import './space/style'; | ||||
| import './image/style'; | ||||
| // import './color-picker/style';
 | ||||
|  |  | |||
|  | @ -0,0 +1,7 @@ | |||
| .box(@position: absolute) { | ||||
|   position: @position; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   left: 0; | ||||
| } | ||||
|  | @ -8,3 +8,5 @@ | |||
| @import 'reset'; | ||||
| @import 'operation-unit'; | ||||
| @import 'typography'; | ||||
| @import 'box'; | ||||
| @import 'modal-mask'; | ||||
|  |  | |||
|  | @ -0,0 +1,31 @@ | |||
| @import 'box'; | ||||
| 
 | ||||
| .modal-mask() { | ||||
|   pointer-events: none; | ||||
| 
 | ||||
|   &.zoom-enter, | ||||
|   &.zoom-appear { | ||||
|     transform: none; // reset scale avoid mousePosition bug | ||||
|     opacity: 0; | ||||
|     animation-duration: @animation-duration-slow; | ||||
|     user-select: none; // https://github.com/ant-design/ant-design/issues/11777 | ||||
|   } | ||||
| 
 | ||||
|   &-mask { | ||||
|     .box(fixed); | ||||
|     z-index: @zindex-modal-mask; | ||||
|     height: 100%; | ||||
|     background-color: @modal-mask-bg; | ||||
| 
 | ||||
|     &-hidden { | ||||
|       display: none; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &-wrap { | ||||
|     .box(fixed); | ||||
|     overflow: auto; | ||||
|     outline: 0; | ||||
|     -webkit-overflow-scrolling: touch; | ||||
|   } | ||||
| } | ||||
|  | @ -288,6 +288,7 @@ | |||
| @zindex-dropdown: 1050; | ||||
| @zindex-picker: 1050; | ||||
| @zindex-tooltip: 1060; | ||||
| @zindex-image: 1080; | ||||
| 
 | ||||
| // Animation | ||||
| @animation-duration-slow: 0.3s; // Modal | ||||
|  | @ -720,3 +721,13 @@ | |||
| @typography-title-font-weight: 600; | ||||
| @typography-title-margin-top: 1.2em; | ||||
| @typography-title-margin-bottom: 0.5em; | ||||
| 
 | ||||
| // Image | ||||
| // --- | ||||
| @image-size-base: 48px; | ||||
| @image-font-size-base: 24px; | ||||
| @image-bg: #f5f5f5; | ||||
| @image-color: #fff; | ||||
| @image-preview-operation-size: 18px; | ||||
| @image-preview-operation-color: @text-color-dark; | ||||
| @image-preview-operation-disabled-color: fade(@image-preview-operation-color, 45%); | ||||
|  |  | |||
|  | @ -0,0 +1,313 @@ | |||
| @import '../../style/themes/index'; | ||||
| @import '../../style/mixins/index'; | ||||
| 
 | ||||
| @prefixCls: ~'@{ant-prefix}-image'; | ||||
| @zindex-preview-mask: 1000; | ||||
| @preview-mask-bg: fade(#000, 40%); | ||||
| @text-color: #bbb; | ||||
| @text-color-disabled: darken(@text-color, 30%); | ||||
| @background-color: #f3f3f3; | ||||
| 
 | ||||
| .reset() { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| .box() { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .@{prefixCls} { | ||||
|   display: inline-block; | ||||
|   position: relative; | ||||
|   &-img { | ||||
|     width: 100%; | ||||
|     height: auto; | ||||
|     &-placeholder { | ||||
|       background-color: @background-color; | ||||
|       background-repeat: no-repeat; | ||||
|       background-position: center center; | ||||
|       background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjhweCIgaGVpZ2h0PSIyMnB4IiB2aWV3Qm94PSIwIDAgMjggMjIiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDU1LjIgKDc4MTgxKSAtIGh0dHBzOi8vc2tldGNoYXBwLmNvbSAtLT4KICAgIDx0aXRsZT5pbWFnZS1maWxs5aSH5Lu9PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9Iuafpeeci+WbvueJh+S8mOWMljQuMCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IuWKoOi9veWbvueJhyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTU3Mi4wMDAwMDAsIC01MDYuMDAwMDAwKSI+CiAgICAgICAgICAgIDxnIGlkPSJpbWFnZS1maWxs5aSH5Lu9IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1NzAuMDAwMDAwLCA1MDEuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlIiBmaWxsPSIjMDAwMDAwIiBvcGFjaXR5PSIwIiB4PSIwIiB5PSIwIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjwvcmVjdD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0yOSw1IEwzLDUgQzIuNDQ2ODc1LDUgMiw1LjQ0Njg3NSAyLDYgTDIsMjYgQzIsMjYuNTUzMTI1IDIuNDQ2ODc1LDI3IDMsMjcgTDI5LDI3IEMyOS41NTMxMjUsMjcgMzAsMjYuNTUzMTI1IDMwLDI2IEwzMCw2IEMzMCw1LjQ0Njg3NSAyOS41NTMxMjUsNSAyOSw1IFogTTEwLjU2MjUsOS41IEMxMS42NjU2MjUsOS41IDEyLjU2MjUsMTAuMzk2ODc1IDEyLjU2MjUsMTEuNSBDMTIuNTYyNSwxMi42MDMxMjUgMTEuNjY1NjI1LDEzLjUgMTAuNTYyNSwxMy41IEM5LjQ1OTM3NSwxMy41IDguNTYyNSwxMi42MDMxMjUgOC41NjI1LDExLjUgQzguNTYyNSwxMC4zOTY4NzUgOS40NTkzNzUsOS41IDEwLjU2MjUsOS41IFogTTI2LjYyMTg3NSwyMy4xNTkzNzUgQzI2LjU3ODEyNSwyMy4xOTY4NzUgMjYuNTE4NzUsMjMuMjE4NzUgMjYuNDU5Mzc1LDIzLjIxODc1IEw1LjUzNzUsMjMuMjE4NzUgQzUuNCwyMy4yMTg3NSA1LjI4NzUsMjMuMTA2MjUgNS4yODc1LDIyLjk2ODc1IEM1LjI4NzUsMjIuOTA5Mzc1IDUuMzA5Mzc1LDIyLjg1MzEyNSA1LjM0Njg3NSwyMi44MDYyNSBMMTAuNjY4NzUsMTYuNDkzNzUgQzEwLjc1NjI1LDE2LjM4NzUgMTAuOTE1NjI1LDE2LjM3NSAxMS4wMjE4NzUsMTYuNDYyNSBDMTEuMDMxMjUsMTYuNDcxODc1IDExLjA0Mzc1LDE2LjQ4MTI1IDExLjA1MzEyNSwxNi40OTM3NSBMMTQuMTU5Mzc1LDIwLjE4MTI1IEwxOS4xLDE0LjMyMTg3NSBDMTkuMTg3NSwxNC4yMTU2MjUgMTkuMzQ2ODc1LDE0LjIwMzEyNSAxOS40NTMxMjUsMTQuMjkwNjI1IEMxOS40NjI1LDE0LjMgMTkuNDc1LDE0LjMwOTM3NSAxOS40ODQzNzUsMTQuMzIxODc1IEwyNi42NTkzNzUsMjIuODA5Mzc1IEMyNi43NDA2MjUsMjIuOTEyNSAyNi43MjgxMjUsMjMuMDcxODc1IDI2LjYyMTg3NSwyMy4xNTkzNzUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjRThFOEU4Ij48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &-placeholder { | ||||
|     .box; | ||||
|   } | ||||
| 
 | ||||
|   &-preview { | ||||
|     text-align: center; | ||||
|     height: 100%; | ||||
|     pointer-events: none; | ||||
| 
 | ||||
|     &-body { | ||||
|       .box; | ||||
|       overflow: hidden; | ||||
|     } | ||||
| 
 | ||||
|     &.zoom-enter, | ||||
|     &.zoom-appear { | ||||
|       transform: none; | ||||
|       opacity: 0; | ||||
|       animation-duration: 0.3s; | ||||
|     } | ||||
| 
 | ||||
|     &-mask { | ||||
|       position: fixed; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       bottom: 0; | ||||
|       left: 0; | ||||
|       z-index: @zindex-preview-mask; | ||||
|       height: 100%; | ||||
|       background-color: @preview-mask-bg; | ||||
|       filter: ~'alpha(opacity=50)'; | ||||
| 
 | ||||
|       &-hidden { | ||||
|         display: none; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-img { | ||||
|       cursor: grab; | ||||
|       transform: scale3d(1, 1, 1); | ||||
|       transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; | ||||
|       user-select: none; | ||||
|       vertical-align: middle; | ||||
|       max-width: 100%; | ||||
|       max-height: 100%; | ||||
|       pointer-events: auto; | ||||
|       &-wrapper { | ||||
|         .box; | ||||
|         transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; | ||||
|         &::before { | ||||
|           content: ''; | ||||
|           display: inline-block; | ||||
|           height: 50%; | ||||
|           width: 1px; | ||||
|           margin-right: -1px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-moving { | ||||
|       .@{prefixCls}-preview-img { | ||||
|         cursor: grabbing; | ||||
|         &-wrapper { | ||||
|           transition-duration: 0s; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-wrap { | ||||
|       position: fixed; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       bottom: 0; | ||||
|       left: 0; | ||||
|       z-index: @zindex-preview-mask; | ||||
|       overflow: auto; | ||||
|       outline: 0; | ||||
|       -webkit-overflow-scrolling: touch; | ||||
|     } | ||||
| 
 | ||||
|     &-operations { | ||||
|       .reset; | ||||
|       pointer-events: auto; | ||||
|       list-style: none; | ||||
|       position: absolute; | ||||
|       display: flex; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       width: 100%; | ||||
|       align-items: center; | ||||
|       flex-direction: row-reverse; | ||||
|       z-index: 1; | ||||
|       color: @text-color; | ||||
|       background: fade(@preview-mask-bg, 45%); | ||||
| 
 | ||||
|       &-operation { | ||||
|         padding: 10px; | ||||
|         cursor: pointer; | ||||
|         margin-left: 10px; | ||||
|         &-disabled { | ||||
|           pointer-events: none; | ||||
|           color: @text-color-disabled; | ||||
|         } | ||||
|         &:last-of-type { | ||||
|           margin-left: 0; | ||||
|         } | ||||
|       } | ||||
|       &-icon { | ||||
|         font-size: 18px; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     &-switch-left { | ||||
|       position: absolute; | ||||
|       left: 10px; | ||||
|       top: 50%; | ||||
|       width: 44px; | ||||
|       height: 44px; | ||||
|       margin-top: -22px; | ||||
|       background: fade(@text-color, 45%); | ||||
|       border-radius: 50%; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       z-index: 1; | ||||
|       pointer-events: auto; | ||||
|       color: @text-color; | ||||
|       &-disabled { | ||||
|         background: fade(@text-color, 30%); | ||||
|         color: @text-color-disabled; | ||||
|         cursor: not-allowed; | ||||
|         > .anticon { | ||||
|           cursor: not-allowed; | ||||
|         } | ||||
|       } | ||||
|       > .anticon { | ||||
|         font-size: 24px; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &-switch-right { | ||||
|       position: absolute; | ||||
|       right: 10px; | ||||
|       top: 50%; | ||||
|       width: 44px; | ||||
|       height: 44px; | ||||
|       margin-top: -22px; | ||||
|       background: fade(@text-color, 45%); | ||||
|       border-radius: 50%; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       z-index: 1; | ||||
|       pointer-events: auto; | ||||
|       color: @text-color; | ||||
|       &-disabled { | ||||
|         background: fade(@text-color, 20%); | ||||
|         color: @text-color-disabled; | ||||
|         cursor: not-allowed; | ||||
|         > .anticon { | ||||
|           cursor: not-allowed; | ||||
|         } | ||||
|       } | ||||
|       > .anticon { | ||||
|         font-size: 24px; | ||||
|       }  | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .fade-enter, | ||||
| .fade-appear { | ||||
|   animation-duration: 0.2s; | ||||
|   animation-fill-mode: both; | ||||
|   animation-play-state: paused; | ||||
|   opacity: 0; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| .fade-leave { | ||||
|   animation-duration: 0.2s; | ||||
|   animation-fill-mode: both; | ||||
|   animation-play-state: paused; | ||||
|   animation-timing-function: linear; | ||||
| } | ||||
| .fade-enter.fade-enter-active, | ||||
| .fade-appear.fade-appear-active { | ||||
|   animation-name: rcImageFadeIn; | ||||
|   animation-play-state: running; | ||||
| } | ||||
| .fade-leave.fade-leave-active { | ||||
|   animation-name: rcImageFadeOut; | ||||
|   animation-play-state: running; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| @keyframes rcImageFadeIn { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
|   100% { | ||||
|     opacity: 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes rcImageFadeOut { | ||||
|   0% { | ||||
|     opacity: 1; | ||||
|   } | ||||
|   100% { | ||||
|     opacity: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .zoom-enter, | ||||
| .zoom-appear { | ||||
|   -webkit-animation-duration: 0.2s; | ||||
|   animation-duration: 0.2s; | ||||
|   -webkit-animation-fill-mode: both; | ||||
|   animation-fill-mode: both; | ||||
|   -webkit-animation-play-state: paused; | ||||
|   animation-play-state: paused; | ||||
|   -webkit-transform: scale(0); | ||||
|   transform: scale(0); | ||||
|   opacity: 0; | ||||
|   -webkit-animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); | ||||
|   animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); | ||||
| } | ||||
| .zoom-leave { | ||||
|   -webkit-animation-duration: 0.2s; | ||||
|   animation-duration: 0.2s; | ||||
|   -webkit-animation-fill-mode: both; | ||||
|   animation-fill-mode: both; | ||||
|   -webkit-animation-play-state: paused; | ||||
|   animation-play-state: paused; | ||||
|   -webkit-animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86); | ||||
|   animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86); | ||||
| } | ||||
| .zoom-enter.zoom-enter-active, | ||||
| .zoom-appear.zoom-appear-active { | ||||
|   -webkit-animation-name: rcImageZoomIn; | ||||
|   animation-name: rcImageZoomIn; | ||||
|   -webkit-animation-play-state: running; | ||||
|   animation-play-state: running; | ||||
| } | ||||
| .zoom-leave.zoom-leave-active { | ||||
|   -webkit-animation-name: rcImageZoomOut; | ||||
|   animation-name: rcImageZoomOut; | ||||
|   -webkit-animation-play-state: running; | ||||
|   animation-play-state: running; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| @keyframes rcImageZoomIn { | ||||
|   0% { | ||||
|     -webkit-transform: scale(0.2); | ||||
|     transform: scale(0.2); | ||||
|     opacity: 0; | ||||
|   } | ||||
|   100% { | ||||
|     -webkit-transform: scale(1); | ||||
|     transform: scale(1); | ||||
|     opacity: 1; | ||||
|   } | ||||
| } | ||||
| @keyframes rcImageZoomOut { | ||||
|   0% { | ||||
|     -webkit-transform: scale(1); | ||||
|     transform: scale(1); | ||||
|   } | ||||
|   100% { | ||||
|     -webkit-transform: scale(0.2); | ||||
|     transform: scale(0.2); | ||||
|     opacity: 0; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,4 @@ | |||
| // based on rc-image 4.3.2
 | ||||
| import Image from './src/Image'; | ||||
| export * from './src/Image'; | ||||
| export default Image; | ||||
|  | @ -0,0 +1,291 @@ | |||
| import { | ||||
|   ImgHTMLAttributes, | ||||
|   CSSProperties, | ||||
|   ref, | ||||
|   watch, | ||||
|   defineComponent, | ||||
|   computed, | ||||
|   onMounted, | ||||
| } from 'vue'; | ||||
| import { isNumber } from 'lodash-es'; | ||||
| 
 | ||||
| import BaseMixin from '../../_util/BaseMixin'; | ||||
| import cn from '../../_util/classNames'; | ||||
| import PropTypes from '../../_util/vue-types'; | ||||
| import { getOffset } from '../../vc-util/Dom/css'; | ||||
| 
 | ||||
| import Preview, { MouseEventHandler } from './Preview'; | ||||
| 
 | ||||
| import PreviewGroup, { context } from './PreviewGroup'; | ||||
| 
 | ||||
| export type GetContainer = string | HTMLElement | (() => HTMLElement); | ||||
| export interface ImagePreviewType { | ||||
|   visible?: boolean; | ||||
|   onVisibleChange?: (value: boolean, prevValue: boolean) => void; | ||||
|   getContainer?: GetContainer | false; | ||||
| } | ||||
| 
 | ||||
| export interface ImagePropsType extends Omit<ImgHTMLAttributes, 'placeholder' | 'onClick'> { | ||||
|   // Original | ||||
|   src?: string; | ||||
|   wrapperClassName?: string; | ||||
|   wrapperStyle?: CSSProperties; | ||||
|   prefixCls?: string; | ||||
|   previewPrefixCls?: string; | ||||
|   placeholder?: boolean; | ||||
|   fallback?: string; | ||||
|   preview?: boolean | ImagePreviewType; | ||||
| } | ||||
| export const ImageProps = { | ||||
|   src: PropTypes.string, | ||||
|   wrapperClassName: PropTypes.string, | ||||
|   wrapperStyle: PropTypes.style, | ||||
|   prefixCls: PropTypes.string, | ||||
|   previewPrefixCls: PropTypes.string, | ||||
|   placeholder: PropTypes.VNodeChild, | ||||
|   fallback: PropTypes.string, | ||||
|   preview: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.shape<ImagePreviewType>({})]).def( | ||||
|     true, | ||||
|   ), | ||||
| }; | ||||
| type ImageStatus = 'normal' | 'error' | 'loading'; | ||||
| 
 | ||||
| const mergeDefaultValue = <T extends object>(obj: T, defaultValues: object): T => { | ||||
|   const res = { ...obj }; | ||||
|   Object.keys(defaultValues).forEach(key => { | ||||
|     if (obj[key] === undefined) { | ||||
|       res[key] = defaultValues[key]; | ||||
|     } | ||||
|   }); | ||||
|   return res; | ||||
| }; | ||||
| let uuid = 0; | ||||
| const ImageInternal = defineComponent({ | ||||
|   name: 'Image', | ||||
|   mixins: [BaseMixin], | ||||
|   inheritAttrs: false, | ||||
|   props: ImageProps, | ||||
|   emits: ['click'], | ||||
|   setup(props, { attrs, slots, emit }) { | ||||
|     const prefixCls = computed(() => props.prefixCls); | ||||
|     const previewPrefixCls = computed(() => `${prefixCls.value}-preview`); | ||||
|     const preview = computed(() => { | ||||
|       const defaultValues = { | ||||
|         visible: undefined, | ||||
|         onVisibleChange: () => {}, | ||||
|         getContainer: undefined, | ||||
|       }; | ||||
|       return typeof props.preview === 'object' | ||||
|         ? mergeDefaultValue(props.preview, defaultValues) | ||||
|         : defaultValues; | ||||
|     }); | ||||
|     const isCustomPlaceholder = computed( | ||||
|       () => (props.placeholder && props.placeholder !== true) || slots.placeholder, | ||||
|     ); | ||||
|     const previewVisible = computed(() => preview.value.visible); | ||||
|     const onPreviewVisibleChange = computed(() => preview.value.onVisibleChange); | ||||
|     const getPreviewContainer = computed(() => preview.value.getContainer); | ||||
| 
 | ||||
|     const isControlled = computed(() => previewVisible.value !== undefined); | ||||
|     const isShowPreview = ref(!!previewVisible.value); | ||||
|     watch(previewVisible, () => { | ||||
|       isShowPreview.value = !!previewVisible.value; | ||||
|     }); | ||||
|     watch(isShowPreview, (val, preVal) => { | ||||
|       onPreviewVisibleChange.value(val, preVal); | ||||
|     }); | ||||
|     const status = ref<ImageStatus>(isCustomPlaceholder.value ? 'loading' : 'normal'); | ||||
|     watch( | ||||
|       () => props.src, | ||||
|       () => { | ||||
|         status.value = isCustomPlaceholder.value ? 'loading' : 'normal'; | ||||
|       }, | ||||
|     ); | ||||
|     const mousePosition = ref<null | { x: number; y: number }>(null); | ||||
|     const isError = computed(() => status.value === 'error'); | ||||
|     const groupContext = context.inject(); | ||||
|     const { | ||||
|       isPreviewGroup, | ||||
|       setCurrent, | ||||
|       setShowPreview: setGroupShowPreview, | ||||
|       setMousePosition: setGroupMousePosition, | ||||
|       registerImage, | ||||
|     } = groupContext; | ||||
|     const currentId = ref(uuid++); | ||||
|     const canPreview = computed(() => props.preview && !isError.value); | ||||
|     const onLoad = () => { | ||||
|       status.value = 'normal'; | ||||
|     }; | ||||
|     const onError = () => { | ||||
|       status.value = 'error'; | ||||
|     }; | ||||
| 
 | ||||
|     const onPreview: MouseEventHandler = e => { | ||||
|       if (!isControlled.value) { | ||||
|         const { left, top } = getOffset(e.target); | ||||
|         if (isPreviewGroup.value) { | ||||
|           setCurrent(currentId.value); | ||||
|           setGroupMousePosition({ | ||||
|             x: left, | ||||
|             y: top, | ||||
|           }); | ||||
|         } else { | ||||
|           mousePosition.value = { | ||||
|             x: left, | ||||
|             y: top, | ||||
|           }; | ||||
|         } | ||||
|       } | ||||
|       if (isPreviewGroup.value) { | ||||
|         setGroupShowPreview(true); | ||||
|       } else { | ||||
|         isShowPreview.value = true; | ||||
|       } | ||||
|       emit('click', e); | ||||
|     }; | ||||
| 
 | ||||
|     const onPreviewClose = () => { | ||||
|       isShowPreview.value = false; | ||||
|       if (!isControlled.value) { | ||||
|         mousePosition.value = null; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     const img = ref<HTMLImageElement>(null); | ||||
|     watch( | ||||
|       () => img, | ||||
|       () => { | ||||
|         if (status.value !== 'loading') return; | ||||
|         if (img.value.complete && (img.value.naturalWidth || img.value.naturalHeight)) { | ||||
|           onLoad(); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|     let unRegister = () => {}; | ||||
|     onMounted(() => { | ||||
|       watch( | ||||
|         [() => props.src, canPreview], | ||||
|         () => { | ||||
|           unRegister(); | ||||
|           if (!isPreviewGroup.value) { | ||||
|             return () => {}; | ||||
|           } | ||||
| 
 | ||||
|           unRegister = registerImage(currentId.value, props.src); | ||||
| 
 | ||||
|           if (!canPreview.value) { | ||||
|             unRegister(); | ||||
|           } | ||||
|         }, | ||||
|         { flush: 'post', immediate: true }, | ||||
|       ); | ||||
|     }); | ||||
|     const toSizePx = (l: number | string) => { | ||||
|       if (isNumber(l)) return l + 'px'; | ||||
|       return l; | ||||
|     }; | ||||
|     return () => { | ||||
|       const { | ||||
|         prefixCls, | ||||
|         wrapperClassName, | ||||
|         fallback, | ||||
|         src, | ||||
|         preview, | ||||
|         placeholder, | ||||
|         wrapperStyle, | ||||
|       } = props; | ||||
|       const { | ||||
|         width, | ||||
|         height, | ||||
|         crossorigin, | ||||
|         decoding, | ||||
|         alt, | ||||
|         sizes, | ||||
|         srcset, | ||||
|         usemap, | ||||
|         class: cls, | ||||
|         style, | ||||
|       } = attrs as ImgHTMLAttributes; | ||||
|       const wrappperClass = cn(prefixCls, wrapperClassName, { | ||||
|         [`${prefixCls}-error`]: isError.value, | ||||
|       }); | ||||
|       const mergedSrc = isError.value && fallback ? fallback : src; | ||||
|       const previewMask = slots.previewMask && slots.previewMask(); | ||||
|       const imgCommonProps = { | ||||
|         crossorigin, | ||||
|         decoding, | ||||
|         alt, | ||||
|         sizes, | ||||
|         srcset, | ||||
|         usemap, | ||||
|         class: cn( | ||||
|           `${prefixCls}-img`, | ||||
|           { | ||||
|             [`${prefixCls}-img-placeholder`]: placeholder === true, | ||||
|           }, | ||||
|           cls, | ||||
|         ), | ||||
|         style: { | ||||
|           height, | ||||
|           ...(style as CSSProperties), | ||||
|         }, | ||||
|       }; | ||||
|       return ( | ||||
|         <> | ||||
|           <div | ||||
|             class={wrappperClass} | ||||
|             onClick={ | ||||
|               preview && !isError.value | ||||
|                 ? onPreview | ||||
|                 : e => { | ||||
|                     emit('click', e); | ||||
|                   } | ||||
|             } | ||||
|             style={{ | ||||
|               width: toSizePx(width), | ||||
|               height: toSizePx(height), | ||||
|               ...wrapperStyle, | ||||
|             }} | ||||
|           > | ||||
|             <img | ||||
|               {...imgCommonProps} | ||||
|               {...(isError.value && fallback | ||||
|                 ? { | ||||
|                     src: fallback, | ||||
|                   } | ||||
|                 : { onLoad, onError, src })} | ||||
|               ref={img} | ||||
|             /> | ||||
| 
 | ||||
|             {status.value === 'loading' && ( | ||||
|               <div aria-hidden="true" class={`${prefixCls}-placeholder`}> | ||||
|                 {placeholder || (slots.placeholder && slots.placeholder())} | ||||
|               </div> | ||||
|             )} | ||||
|             {/* Preview Click Mask */} | ||||
|             {previewMask && canPreview.value && ( | ||||
|               <div class={`${prefixCls}-mask`}>{previewMask}</div> | ||||
|             )} | ||||
|           </div> | ||||
|           {!isPreviewGroup.value && canPreview.value && ( | ||||
|             <Preview | ||||
|               aria-hidden={!isShowPreview.value} | ||||
|               visible={isShowPreview.value} | ||||
|               prefixCls={previewPrefixCls.value} | ||||
|               onClose={onPreviewClose} | ||||
|               mousePosition={mousePosition.value} | ||||
|               src={mergedSrc} | ||||
|               alt={alt} | ||||
|               getContainer={getPreviewContainer} | ||||
|             /> | ||||
|           )} | ||||
|         </> | ||||
|       ); | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| ImageInternal.PreviewGroup = PreviewGroup; | ||||
| 
 | ||||
| export default ImageInternal as typeof ImageInternal & { | ||||
|   readonly PreviewGroup: typeof PreviewGroup; | ||||
| }; | ||||
|  | @ -0,0 +1,314 @@ | |||
| import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'; | ||||
| import { | ||||
|   RotateLeftOutlined, | ||||
|   RotateRightOutlined, | ||||
|   ZoomInOutlined, | ||||
|   ZoomOutOutlined, | ||||
|   CloseOutlined, | ||||
|   LeftOutlined, | ||||
|   RightOutlined, | ||||
| } from '@ant-design/icons-vue'; | ||||
| 
 | ||||
| import classnames from '../../_util/classNames'; | ||||
| import PropTypes from '../../_util/vue-types'; | ||||
| import Dialog from '../../vc-dialog'; | ||||
| import getIDialogPropTypes from '../../vc-dialog/IDialogPropTypes'; | ||||
| import { getOffset } from '../../vc-util/Dom/css'; | ||||
| import addEventListener from '../../vc-util/Dom/addEventListener'; | ||||
| import { warning } from '../../vc-util/warning'; | ||||
| import useFrameSetState from './hooks/useFrameSetState'; | ||||
| import getFixScaleEleTransPosition from './getFixScaleEleTransPosition'; | ||||
| 
 | ||||
| import { context } from './PreviewGroup'; | ||||
| 
 | ||||
| const IDialogPropTypes = getIDialogPropTypes(); | ||||
| export type MouseEventHandler = (payload: MouseEvent) => void; | ||||
| 
 | ||||
| export interface PreviewProps extends Omit<typeof IDialogPropTypes, 'onClose'> { | ||||
|   onClose?: (e: Element) => void; | ||||
|   src?: string; | ||||
|   alt?: string; | ||||
| } | ||||
| 
 | ||||
| const initialPosition = { | ||||
|   x: 0, | ||||
|   y: 0, | ||||
| }; | ||||
| const PreviewType = { | ||||
|   src: PropTypes.string, | ||||
|   alt: PropTypes.string, | ||||
|   ...IDialogPropTypes, | ||||
| }; | ||||
| const Preview = defineComponent({ | ||||
|   name: 'Preview', | ||||
|   inheritAttrs: false, | ||||
|   props: PreviewType, | ||||
|   emits: ['close', 'afterClose'], | ||||
|   setup(props, { emit, attrs }) { | ||||
|     const scale = ref(1); | ||||
|     const rotate = ref(0); | ||||
|     const [position, setPosition] = useFrameSetState<{ | ||||
|       x: number; | ||||
|       y: number; | ||||
|     }>(initialPosition); | ||||
| 
 | ||||
|     const onClose = () => emit('close'); | ||||
|     const imgRef = ref<HTMLImageElement>(); | ||||
|     const originPositionRef = reactive<{ | ||||
|       originX: number; | ||||
|       originY: number; | ||||
|       deltaX: number; | ||||
|       deltaY: number; | ||||
|     }>({ | ||||
|       originX: 0, | ||||
|       originY: 0, | ||||
|       deltaX: 0, | ||||
|       deltaY: 0, | ||||
|     }); | ||||
|     const isMoving = ref(false); | ||||
|     const groupContext = context.inject(); | ||||
|     const { previewUrls, current, isPreviewGroup, setCurrent } = groupContext; | ||||
|     const previewGroupCount = computed(() => Object.keys(previewUrls).length); | ||||
|     const previewUrlsKeys = computed(() => Object.keys(previewUrls)); | ||||
|     const currentPreviewIndex = computed(() => | ||||
|       previewUrlsKeys.value.indexOf(String(current.value)), | ||||
|     ); | ||||
|     const combinationSrc = computed(() => | ||||
|       isPreviewGroup.value ? previewUrls[current.value] : props.src, | ||||
|     ); | ||||
|     const showLeftOrRightSwitches = computed( | ||||
|       () => isPreviewGroup.value && previewGroupCount.value > 1, | ||||
|     ); | ||||
| 
 | ||||
|     const onAfterClose = () => { | ||||
|       scale.value = 1; | ||||
|       rotate.value = 0; | ||||
|       setPosition(initialPosition); | ||||
|     }; | ||||
| 
 | ||||
|     const onZoomIn = () => { | ||||
|       scale.value++; | ||||
|       setPosition(initialPosition); | ||||
|     }; | ||||
|     const onZoomOut = () => { | ||||
|       if (scale.value > 1) { | ||||
|         scale.value--; | ||||
|       } | ||||
|       setPosition(initialPosition); | ||||
|     }; | ||||
| 
 | ||||
|     const onRotateRight = () => { | ||||
|       rotate.value += 90; | ||||
|     }; | ||||
| 
 | ||||
|     const onRotateLeft = () => { | ||||
|       rotate.value -= 90; | ||||
|     }; | ||||
|     const onSwitchLeft: MouseEventHandler = event => { | ||||
|       event.preventDefault(); | ||||
|       // Without this mask close will abnormal | ||||
|       event.stopPropagation(); | ||||
|       if (currentPreviewIndex.value > 0) { | ||||
|         setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value - 1)]); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     const onSwitchRight: MouseEventHandler = event => { | ||||
|       event.preventDefault(); | ||||
|       // Without this mask close will abnormal | ||||
|       event.stopPropagation(); | ||||
|       if (currentPreviewIndex.value < previewGroupCount.value - 1) { | ||||
|         setCurrent(previewUrlsKeys.value[String(currentPreviewIndex.value + 1)]); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     const wrapClassName = classnames({ | ||||
|       [`${props.prefixCls}-moving`]: isMoving.value, | ||||
|     }); | ||||
|     const toolClassName = `${props.prefixCls}-operations-operation`; | ||||
|     const iconClassName = `${props.prefixCls}-operations-icon`; | ||||
|     const tools = [ | ||||
|       { | ||||
|         icon: CloseOutlined, | ||||
|         onClick: onClose, | ||||
|         type: 'close', | ||||
|       }, | ||||
|       { | ||||
|         icon: ZoomInOutlined, | ||||
|         onClick: onZoomIn, | ||||
|         type: 'zoomIn', | ||||
|       }, | ||||
|       { | ||||
|         icon: ZoomOutOutlined, | ||||
|         onClick: onZoomOut, | ||||
|         type: 'zoomOut', | ||||
|         disabled: computed(() => scale.value === 1), | ||||
|       }, | ||||
|       { | ||||
|         icon: RotateRightOutlined, | ||||
|         onClick: onRotateRight, | ||||
|         type: 'rotateRight', | ||||
|       }, | ||||
|       { | ||||
|         icon: RotateLeftOutlined, | ||||
|         onClick: onRotateLeft, | ||||
|         type: 'rotateLeft', | ||||
|       }, | ||||
|     ]; | ||||
| 
 | ||||
|     const onMouseUp: MouseEventHandler = () => { | ||||
|       if (props.visible && isMoving.value) { | ||||
|         const width = imgRef.value.offsetWidth * scale.value; | ||||
|         const height = imgRef.value.offsetHeight * scale.value; | ||||
|         const { left, top } = getOffset(imgRef.value); | ||||
|         const isRotate = rotate.value % 180 !== 0; | ||||
| 
 | ||||
|         isMoving.value = false; | ||||
| 
 | ||||
|         const fixState = getFixScaleEleTransPosition( | ||||
|           isRotate ? height : width, | ||||
|           isRotate ? width : height, | ||||
|           left, | ||||
|           top, | ||||
|         ); | ||||
|         if (fixState) { | ||||
|           setPosition({ ...fixState }); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     const onMouseDown: MouseEventHandler = event => { | ||||
|       event.preventDefault(); | ||||
|       // Without this mask close will abnormal | ||||
|       event.stopPropagation(); | ||||
|       originPositionRef.deltaX = event.pageX - position.x; | ||||
|       originPositionRef.deltaY = event.pageY - position.y; | ||||
|       originPositionRef.originX = position.x; | ||||
|       originPositionRef.originY = position.y; | ||||
|       isMoving.value = true; | ||||
|     }; | ||||
| 
 | ||||
|     const onMouseMove: MouseEventHandler = event => { | ||||
|       if (props.visible && isMoving.value) { | ||||
|         setPosition({ | ||||
|           x: event.pageX - originPositionRef.deltaX, | ||||
|           y: event.pageY - originPositionRef.deltaY, | ||||
|         }); | ||||
|       } | ||||
|     }; | ||||
|     let removeListeners = () => {}; | ||||
|     onMounted(() => { | ||||
|       watch( | ||||
|         [() => props.visible, isMoving], | ||||
|         () => { | ||||
|           removeListeners(); | ||||
|           let onTopMouseUpListener: { remove: any }; | ||||
|           let onTopMouseMoveListener: { remove: any }; | ||||
| 
 | ||||
|           const onMouseUpListener = addEventListener(window, 'mouseup', onMouseUp, false); | ||||
|           const onMouseMoveListener = addEventListener(window, 'mousemove', onMouseMove, false); | ||||
| 
 | ||||
|           try { | ||||
|             // Resolve if in iframe lost event | ||||
|             /* istanbul ignore next */ | ||||
|             if (window.top !== window.self) { | ||||
|               onTopMouseUpListener = addEventListener(window.top, 'mouseup', onMouseUp, false); | ||||
|               onTopMouseMoveListener = addEventListener( | ||||
|                 window.top, | ||||
|                 'mousemove', | ||||
|                 onMouseMove, | ||||
|                 false, | ||||
|               ); | ||||
|             } | ||||
|           } catch (error) { | ||||
|             /* istanbul ignore next */ | ||||
|             warning(false, `[vc-image] ${error}`); | ||||
|           } | ||||
| 
 | ||||
|           removeListeners = () => { | ||||
|             onMouseUpListener.remove(); | ||||
|             onMouseMoveListener.remove(); | ||||
| 
 | ||||
|             /* istanbul ignore next */ | ||||
|             if (onTopMouseUpListener) onTopMouseUpListener.remove(); | ||||
|             /* istanbul ignore next */ | ||||
|             if (onTopMouseMoveListener) onTopMouseMoveListener.remove(); | ||||
|           }; | ||||
|         }, | ||||
|         { flush: 'post', immediate: true }, | ||||
|       ); | ||||
|     }); | ||||
|     onUnmounted(() => { | ||||
|       removeListeners(); | ||||
|     }); | ||||
| 
 | ||||
|     return () => ( | ||||
|       <Dialog | ||||
|         {...attrs} | ||||
|         transitionName="zoom" | ||||
|         maskTransitionName="fade" | ||||
|         closable={false} | ||||
|         keyboard | ||||
|         prefixCls={props.prefixCls} | ||||
|         onClose={onClose} | ||||
|         afterClose={onAfterClose} | ||||
|         visible={props.visible} | ||||
|         wrapClassName={wrapClassName} | ||||
|       > | ||||
|         <ul class={`${props.prefixCls}-operations`}> | ||||
|           {tools.map(({ icon: IconType, onClick, type, disabled }) => ( | ||||
|             <li | ||||
|               class={classnames(toolClassName, { | ||||
|                 [`${props.prefixCls}-operations-operation-disabled`]: disabled && disabled?.value, | ||||
|               })} | ||||
|               onClick={onClick} | ||||
|               key={type} | ||||
|             > | ||||
|               <IconType class={iconClassName} /> | ||||
|             </li> | ||||
|           ))} | ||||
|         </ul> | ||||
|         <div | ||||
|           class={`${props.prefixCls}-img-wrapper`} | ||||
|           style={{ | ||||
|             transform: `translate3d(${position.x}px, ${position.y}px, 0)`, | ||||
|           }} | ||||
|         > | ||||
|           <img | ||||
|             onMousedown={onMouseDown} | ||||
|             ref={imgRef} | ||||
|             class={`${props.prefixCls}-img`} | ||||
|             src={combinationSrc.value} | ||||
|             alt={props.alt} | ||||
|             style={{ | ||||
|               transform: `scale3d(${scale.value}, ${scale.value}, 1) rotate(${rotate.value}deg)`, | ||||
|             }} | ||||
|           /> | ||||
|         </div> | ||||
|         {showLeftOrRightSwitches.value && ( | ||||
|           <div | ||||
|             class={classnames(`${props.prefixCls}-switch-left`, { | ||||
|               [`${props.prefixCls}-switch-left-disabled`]: currentPreviewIndex.value <= 0, | ||||
|             })} | ||||
|             onClick={onSwitchLeft} | ||||
|           > | ||||
|             <LeftOutlined /> | ||||
|           </div> | ||||
|         )} | ||||
|         {showLeftOrRightSwitches.value && ( | ||||
|           <div | ||||
|             class={classnames(`${props.prefixCls}-switch-right`, { | ||||
|               [`${props.prefixCls}-switch-right-disabled`]: | ||||
|                 currentPreviewIndex.value >= previewGroupCount.value - 1, | ||||
|             })} | ||||
|             onClick={onSwitchRight} | ||||
|           > | ||||
|             <RightOutlined /> | ||||
|           </div> | ||||
|         )} | ||||
|       </Dialog> | ||||
|     ); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| export default Preview; | ||||
|  | @ -0,0 +1,97 @@ | |||
| import { ref, provide, defineComponent, inject, Ref, reactive } from 'vue'; | ||||
| import Preview from './Preview'; | ||||
| 
 | ||||
| export interface GroupConsumerProps { | ||||
|   previewPrefixCls?: string; | ||||
| } | ||||
| export interface GroupConsumerValue extends GroupConsumerProps { | ||||
|   isPreviewGroup?: Ref<boolean | undefined>; | ||||
|   previewUrls: Record<number, string>; | ||||
|   setPreviewUrls: (previewUrls: Record<number, string>) => void; | ||||
|   current: Ref<number>; | ||||
|   setCurrent: (current: number) => void; | ||||
|   setShowPreview: (isShowPreview: boolean) => void; | ||||
|   setMousePosition: (mousePosition: null | { x: number; y: number }) => void; | ||||
|   registerImage: (id: number, url: string) => () => void; | ||||
| } | ||||
| const previewGroupContext = Symbol('previewGroupContext'); | ||||
| export const context = { | ||||
|   provide: (val: GroupConsumerValue) => { | ||||
|     provide(previewGroupContext, val); | ||||
|   }, | ||||
|   inject: () => { | ||||
|     return inject<GroupConsumerValue>(previewGroupContext, { | ||||
|       isPreviewGroup: ref(false), | ||||
|       previewUrls: reactive({}), | ||||
|       setPreviewUrls: () => {}, | ||||
|       current: ref(null), | ||||
|       setCurrent: () => {}, | ||||
|       setShowPreview: () => {}, | ||||
|       setMousePosition: () => {}, | ||||
|       registerImage: null, | ||||
|     }); | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| const Group = defineComponent({ | ||||
|   name: 'PreviewGroup', | ||||
|   inheritAttrs: false, | ||||
|   props: { previewPrefixCls: String }, | ||||
|   setup(props, { slots }) { | ||||
|     const previewUrls = reactive<Record<number, string>>({}); | ||||
|     const current = ref<number>(); | ||||
|     const isShowPreview = ref<boolean>(false); | ||||
|     const mousePosition = ref<{ x: number; y: number }>(null); | ||||
|     const setPreviewUrls = (val: Record<number, string>) => { | ||||
|       Object.assign(previewUrls, val); | ||||
|     }; | ||||
|     const setCurrent = (val: number) => { | ||||
|       current.value = val; | ||||
|     }; | ||||
|     const setMousePosition = (val: null | { x: number; y: number }) => { | ||||
|       mousePosition.value = val; | ||||
|     }; | ||||
|     const setShowPreview = (val: boolean) => { | ||||
|       isShowPreview.value = val; | ||||
|     }; | ||||
|     const registerImage = (id: number, url: string) => { | ||||
|       previewUrls[id] = url; | ||||
| 
 | ||||
|       return () => { | ||||
|         delete previewUrls[id]; | ||||
|       }; | ||||
|     }; | ||||
|     const onPreviewClose = (e: any) => { | ||||
|       e?.stopPropagation(); | ||||
|       isShowPreview.value = false; | ||||
|       mousePosition.value = null; | ||||
|     }; | ||||
|     context.provide({ | ||||
|       isPreviewGroup: ref(true), | ||||
|       previewUrls, | ||||
|       setPreviewUrls, | ||||
|       current, | ||||
|       setCurrent, | ||||
|       setShowPreview, | ||||
|       setMousePosition, | ||||
|       registerImage, | ||||
|     }); | ||||
|     return () => { | ||||
|       return ( | ||||
|         <> | ||||
|           {slots.default && slots.default()} | ||||
|           <Preview | ||||
|             ria-hidden={!isShowPreview.value} | ||||
|             visible={isShowPreview.value} | ||||
|             prefixCls={props.previewPrefixCls} | ||||
|             onClose={onPreviewClose} | ||||
|             mousePosition={mousePosition.value} | ||||
|             src={previewUrls[current.value]} | ||||
|           /> | ||||
|         </> | ||||
|       ); | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| export default Group; | ||||
|  | @ -0,0 +1,61 @@ | |||
| import { getClientSize } from '../../vc-util/Dom/css'; | ||||
| 
 | ||||
| function fixPoint(key: 'x' | 'y', start: number, width: number, clientWidth: number) { | ||||
|   const startAddWidth = start + width; | ||||
|   const offsetStart = (width - clientWidth) / 2; | ||||
| 
 | ||||
|   if (width > clientWidth) { | ||||
|     if (start > 0) { | ||||
|       return { | ||||
|         [key]: offsetStart, | ||||
|       }; | ||||
|     } | ||||
|     if (start < 0 && startAddWidth < clientWidth) { | ||||
|       return { | ||||
|         [key]: -offsetStart, | ||||
|       }; | ||||
|     } | ||||
|   } else if (start < 0 || startAddWidth > clientWidth) { | ||||
|     return { | ||||
|       [key]: start < 0 ? offsetStart : -offsetStart, | ||||
|     }; | ||||
|   } | ||||
|   return {}; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fix positon x,y point when | ||||
|  * | ||||
|  * Ele width && height < client | ||||
|  * - Back origin | ||||
|  * | ||||
|  * - Ele width | height > clientWidth | clientHeight | ||||
|  * - left | top > 0 -> Back 0 | ||||
|  * - left | top + width | height < clientWidth | clientHeight -> Back left | top + width | height === clientWidth | clientHeight | ||||
|  * | ||||
|  * Regardless of other | ||||
|  */ | ||||
| export default function getFixScaleEleTransPosition( | ||||
|   width: number, | ||||
|   height: number, | ||||
|   left: number, | ||||
|   top: number, | ||||
| ): null | { x: number; y: number } { | ||||
|   const { width: clientWidth, height: clientHeight } = getClientSize(); | ||||
| 
 | ||||
|   let fixPos = null; | ||||
| 
 | ||||
|   if (width <= clientWidth && height <= clientHeight) { | ||||
|     fixPos = { | ||||
|       x: 0, | ||||
|       y: 0, | ||||
|     }; | ||||
|   } else if (width > clientWidth || height > clientHeight) { | ||||
|     fixPos = { | ||||
|       ...fixPoint('x', left, width, clientWidth), | ||||
|       ...fixPoint('y', top, height, clientHeight), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   return fixPos; | ||||
| } | ||||
|  | @ -0,0 +1,31 @@ | |||
| import raf from '../../../_util/raf'; | ||||
| import { onMounted, reactive, ref } from 'vue'; | ||||
| 
 | ||||
| type SetActionType<T> = Partial<T> | ((state: T) => Partial<T>); | ||||
| export default function useFrameSetState<T extends object>( | ||||
|   initial: T, | ||||
| ): [Record<string, any>, (newState: SetActionType<T>) => void] { | ||||
|   const frame = ref(null); | ||||
|   const state = reactive({ ...initial }); | ||||
|   const queue = ref<SetActionType<T>[]>([]); | ||||
| 
 | ||||
|   const setFrameState = (newState: SetActionType<T>) => { | ||||
|     if (frame.value === null) { | ||||
|       queue.value = []; | ||||
|       frame.value = raf(() => { | ||||
|         let memoState: any; | ||||
|         queue.value.forEach((queueState: object) => { | ||||
|           memoState = { ...memoState, ...queueState }; | ||||
|         }); | ||||
|         Object.assign(state, memoState); | ||||
|         frame.value = null; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     queue.value.push(newState as any); | ||||
|   }; | ||||
|   onMounted(() => { | ||||
|     frame.value && raf.cancel(frame.value); | ||||
|   }); | ||||
|   return [state, setFrameState]; | ||||
| } | ||||
|  | @ -0,0 +1,115 @@ | |||
| const PIXEL_PATTERN = /margin|padding|width|height|max|min|offset/; | ||||
| 
 | ||||
| const removePixel = { | ||||
|   left: true, | ||||
|   top: true, | ||||
| }; | ||||
| const floatMap = { | ||||
|   cssFloat: 1, | ||||
|   styleFloat: 1, | ||||
|   float: 1, | ||||
| }; | ||||
| 
 | ||||
| function getComputedStyle(node: HTMLElement) { | ||||
|   return node.nodeType === 1 ? node.ownerDocument.defaultView.getComputedStyle(node, null) : {}; | ||||
| } | ||||
| 
 | ||||
| function getStyleValue(node: HTMLElement, type: string, value: string) { | ||||
|   type = type.toLowerCase(); | ||||
|   if (value === 'auto') { | ||||
|     if (type === 'height') { | ||||
|       return node.offsetHeight; | ||||
|     } | ||||
|     if (type === 'width') { | ||||
|       return node.offsetWidth; | ||||
|     } | ||||
|   } | ||||
|   if (!(type in removePixel)) { | ||||
|     removePixel[type] = PIXEL_PATTERN.test(type); | ||||
|   } | ||||
|   return removePixel[type] ? parseFloat(value) || 0 : value; | ||||
| } | ||||
| 
 | ||||
| export function get(node: HTMLElement, name: any) { | ||||
|   const length = arguments.length; | ||||
|   const style = getComputedStyle(node); | ||||
| 
 | ||||
|   name = floatMap[name] ? ('cssFloat' in node.style ? 'cssFloat' : 'styleFloat') : name; | ||||
| 
 | ||||
|   return length === 1 ? style : getStyleValue(node, name, style[name] || node.style[name]); | ||||
| } | ||||
| 
 | ||||
| export function set(node: HTMLElement, name: any, value: string | number) { | ||||
|   const length = arguments.length; | ||||
|   name = floatMap[name] ? ('cssFloat' in node.style ? 'cssFloat' : 'styleFloat') : name; | ||||
|   if (length === 3) { | ||||
|     if (typeof value === 'number' && PIXEL_PATTERN.test(name)) { | ||||
|       value = `${value}px`; | ||||
|     } | ||||
|     node.style[name as string] = value; // Number
 | ||||
|     return value; | ||||
|   } | ||||
|   for (const x in name) { | ||||
|     if (name.hasOwnProperty(x)) { | ||||
|       set(node, x, name[x]); | ||||
|     } | ||||
|   } | ||||
|   return getComputedStyle(node); | ||||
| } | ||||
| 
 | ||||
| export function getOuterWidth(el: HTMLElement) { | ||||
|   if (el === document.body) { | ||||
|     return document.documentElement.clientWidth; | ||||
|   } | ||||
|   return el.offsetWidth; | ||||
| } | ||||
| 
 | ||||
| export function getOuterHeight(el: HTMLElement) { | ||||
|   if (el === document.body) { | ||||
|     return window.innerHeight || document.documentElement.clientHeight; | ||||
|   } | ||||
|   return el.offsetHeight; | ||||
| } | ||||
| 
 | ||||
| export function getDocSize() { | ||||
|   const width = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth); | ||||
|   const height = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); | ||||
| 
 | ||||
|   return { | ||||
|     width, | ||||
|     height, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function getClientSize() { | ||||
|   const width = document.documentElement.clientWidth; | ||||
|   const height = window.innerHeight || document.documentElement.clientHeight; | ||||
|   return { | ||||
|     width, | ||||
|     height, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function getScroll() { | ||||
|   return { | ||||
|     scrollLeft: Math.max(document.documentElement.scrollLeft, document.body.scrollLeft), | ||||
|     scrollTop: Math.max(document.documentElement.scrollTop, document.body.scrollTop), | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function getOffset(node: any) { | ||||
|   const box = node.getBoundingClientRect(); | ||||
|   const docElem = document.documentElement; | ||||
| 
 | ||||
|   // < ie8 不支持 win.pageXOffset, 则使用 docElem.scrollLeft
 | ||||
|   return { | ||||
|     left: | ||||
|       box.left + | ||||
|       (window.pageXOffset || docElem.scrollLeft) - | ||||
|       (docElem.clientLeft || document.body.clientLeft || 0), | ||||
|     top: | ||||
|       box.top + | ||||
|       (window.pageYOffset || docElem.scrollTop) - | ||||
|       (docElem.clientTop || document.body.clientTop || 0), | ||||
|   }; | ||||
| } | ||||
|  | @ -60,6 +60,7 @@ Array [ | |||
|   "Drawer", | ||||
|   "Skeleton", | ||||
|   "Comment", | ||||
|   "Image", | ||||
|   "ConfigProvider", | ||||
|   "Empty", | ||||
|   "Result", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Zou Jian
						Zou Jian