mirror of
https://gitee.com/ant-design-vue/ant-design-vue.git
synced 2024-11-29 18:48:32 +08:00
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>
This commit is contained in:
parent
c6b189b583
commit
5913cf9c5c
@ -1 +1 @@
|
||||
Subproject commit ab88ac19de8ce0d3c8a559ad7bcd5fc04532c184
|
||||
Subproject commit db458a2276cd9156a7824f4e876de5702efd9ff7
|
25
components/image/PreviewGroup.tsx
Normal file
25
components/image/PreviewGroup.tsx
Normal file
@ -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;
|
3
components/image/__tests__/demo.test.js
Normal file
3
components/image/__tests__/demo.test.js
Normal file
@ -0,0 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('image');
|
32
components/image/__tests__/index.test.js
Normal file
32
components/image/__tests__/index.test.js
Normal file
@ -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');
|
||||
});
|
||||
});
|
35
components/image/index.tsx
Normal file
35
components/image/index.tsx
Normal file
@ -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;
|
||||
};
|
141
components/image/style/index.less
Normal file
141
components/image/style/index.less
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
2
components/image/style/index.ts
Normal file
2
components/image/style/index.ts
Normal file
@ -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';
|
||||
|
7
components/style/mixins/box.less
Normal file
7
components/style/mixins/box.less
Normal file
@ -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';
|
||||
|
31
components/style/mixins/modal-mask.less
Normal file
31
components/style/mixins/modal-mask.less
Normal file
@ -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%);
|
||||
|
313
components/vc-image/assets/index.less
Normal file
313
components/vc-image/assets/index.less
Normal file
@ -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;
|
||||
}
|
||||
}
|
4
components/vc-image/index.ts
Normal file
4
components/vc-image/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// based on rc-image 4.3.2
|
||||
import Image from './src/Image';
|
||||
export * from './src/Image';
|
||||
export default Image;
|
291
components/vc-image/src/Image.tsx
Normal file
291
components/vc-image/src/Image.tsx
Normal file
@ -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;
|
||||
};
|
314
components/vc-image/src/Preview.tsx
Normal file
314
components/vc-image/src/Preview.tsx
Normal file
@ -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;
|
97
components/vc-image/src/PreviewGroup.tsx
Normal file
97
components/vc-image/src/PreviewGroup.tsx
Normal file
@ -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;
|
61
components/vc-image/src/getFixScaleEleTransPosition.ts
Normal file
61
components/vc-image/src/getFixScaleEleTransPosition.ts
Normal file
@ -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;
|
||||
}
|
31
components/vc-image/src/hooks/useFrameSetState.ts
Normal file
31
components/vc-image/src/hooks/useFrameSetState.ts
Normal file
@ -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];
|
||||
}
|
115
components/vc-util/Dom/css.ts
Normal file
115
components/vc-util/Dom/css.ts
Normal file
@ -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
Block a user