feat: ImageGallery支持工具栏 (#5005)

This commit is contained in:
RUNZE LU 2022-07-29 16:13:15 +08:00 committed by GitHub
parent 4be87d2373
commit 9b8e212a9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 235 additions and 13 deletions

View File

@ -59,6 +59,7 @@
display: block; display: block;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s;
} }
} }
@ -203,3 +204,64 @@
} }
} }
} }
.#{$ns}ImageGallery-toolbar {
background-color: var(--white);
border-radius: px2rem(4px);
display: flex;
align-items: flex-start;
padding: px2rem(4px) px2rem(16px);
position: absolute;
bottom: px2rem(20px);
left: 50%;
transform: translateX(-50%);
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
&-action {
padding: px2rem(13px);
border-radius: px2rem(4px);
width: px2rem(40px);
height: px2rem(40px);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&-icon {
display: block;
color: var(--black);
& > svg {
fill: var(--black);
}
}
&.is-disabled {
cursor: not-allowed;
color: var(--icon-onDisabled-color);
.#{$ns}ImageGallery-toolbar-action-icon {
color: var(--icon-onDisabled-color);
& > svg {
color: var(--icon-onDisabled-color);
}
}
}
&:hover {
background-color: #f2f3f5;
.#{$ns}ImageGallery-toolbar-action-icon {
color: var(--primary);
& > svg {
fill: var(--primary);
}
}
}
}
}

View File

@ -1,13 +1,37 @@
import React from 'react'; import React from 'react';
import debounce from 'lodash/debounce';
import {themeable, ClassNamesFn, ThemeProps} from 'amis-core'; import {themeable, ClassNamesFn, ThemeProps} from 'amis-core';
import {autobind} from 'amis-core'; import {autobind} from 'amis-core';
import Modal from './Modal'; import Modal from './Modal';
import {Icon} from './icons'; import {Icon} from './icons';
import {LocaleProps, localeable} from 'amis-core'; import {LocaleProps, localeable} from 'amis-core';
export enum ImageActionKey {
/** 右旋转 */
ROTATE_RIGHT = 'rotateRight',
/** 左旋转 */
ROTATE_LEFT = 'rotateLeft',
/** 等比例放大 */
ZOOM_IN = 'zoomIn',
/** 等比例缩小 */
ZOOM_OUT = 'zoomOut',
/** 恢复原图缩放比例尺 */
SCALE_ORIGIN = 'scaleOrigin'
}
export interface ImageAction {
key: ImageActionKey;
label?: string;
icon?: string | React.ReactNode;
iconClassName?: string;
disabled?: boolean;
onClick?: (context: ImageGallery) => void;
}
export interface ImageGalleryProps extends ThemeProps, LocaleProps { export interface ImageGalleryProps extends ThemeProps, LocaleProps {
children: React.ReactNode; children: React.ReactNode;
modalContainer?: () => HTMLElement; modalContainer?: () => HTMLElement;
actions?: ImageAction[];
} }
export interface ImageGalleryState { export interface ImageGalleryState {
@ -19,16 +43,44 @@ export interface ImageGalleryState {
title?: string; title?: string;
caption?: string; caption?: string;
}>; }>;
/** 图片缩放比例尺 */
scale: number;
/** 图片旋转角度 */
rotate: number;
} }
export class ImageGallery extends React.Component< export class ImageGallery extends React.Component<
ImageGalleryProps, ImageGalleryProps,
ImageGalleryState ImageGalleryState
> { > {
static defaultProps: Pick<ImageGalleryProps, 'actions'> = {
actions: [
{
key: ImageActionKey.ROTATE_LEFT,
icon: 'rotate-left',
label: 'rotate.left'
},
{
key: ImageActionKey.ROTATE_RIGHT,
icon: 'rotate-right',
label: 'rotate.right'
},
{key: ImageActionKey.ZOOM_IN, icon: 'zoom-in', label: 'zoomIn'},
{key: ImageActionKey.ZOOM_OUT, icon: 'zoom-out', label: 'zoomOut'},
{
key: ImageActionKey.SCALE_ORIGIN,
icon: 'scale-origin',
label: 'scale.origin'
}
]
};
state: ImageGalleryState = { state: ImageGalleryState = {
isOpened: false, isOpened: false,
index: -1, index: -1,
items: [] items: [],
scale: 1,
rotate: 0
}; };
@autobind @autobind
@ -52,40 +104,116 @@ export class ImageGallery extends React.Component<
}); });
} }
resetImageAction() {
this.setState({scale: 1, rotate: 0});
}
@autobind @autobind
close() { close() {
this.setState({ this.setState({
isOpened: false isOpened: false
}); });
this.resetImageAction();
} }
@autobind @autobind
prev() { prev() {
const index = this.state.index; const index = this.state.index;
this.setState({ this.setState({index: index - 1});
index: index - 1 this.resetImageAction();
});
} }
@autobind @autobind
next() { next() {
const index = this.state.index; const index = this.state.index;
this.setState({ this.setState({index: index + 1});
index: index + 1 this.resetImageAction();
});
} }
@autobind @autobind
handleItemClick(e: React.MouseEvent<HTMLDivElement>) { handleItemClick(e: React.MouseEvent<HTMLDivElement>) {
const index = parseInt(e.currentTarget.getAttribute('data-index')!, 10); const index = parseInt(e.currentTarget.getAttribute('data-index')!, 10);
this.setState({ this.setState({index});
index this.resetImageAction();
}
handleToolbarAction = debounce(
(action: ImageAction) => {
if (action.disabled) {
return;
}
switch (action.key) {
case ImageActionKey.ROTATE_LEFT:
this.setState(prevState => ({rotate: prevState.rotate - 90}));
break;
case ImageActionKey.ROTATE_RIGHT:
this.setState(prevState => ({rotate: prevState.rotate + 90}));
break;
case ImageActionKey.ZOOM_IN:
this.setState(prevState => ({scale: prevState.scale + 0.5}));
break;
case ImageActionKey.ZOOM_OUT:
this.setState(prevState => {
return prevState.scale - 0.5 > 0
? {scale: prevState.scale - 0.5}
: null;
}); });
break;
case ImageActionKey.SCALE_ORIGIN:
this.setState(() => ({scale: 1}));
break;
}
if (action.onClick && typeof action.onClick === 'function') {
action.onClick(this);
}
},
250,
{leading: true, trailing: false}
);
renderToolbar(actions: ImageAction[]) {
const {classnames: cx, translate: __, className} = this.props;
const scale = this.state.scale;
return (
<div className={cx('ImageGallery-toolbar', className)}>
{actions.map(action => (
<div
className={cx('ImageGallery-toolbar-action', {
'is-disabled':
action.disabled ||
(action.key === ImageActionKey.ZOOM_OUT && scale - 0.5 <= 0)
})}
key={action.key}
onClick={() => this.handleToolbarAction(action)}
>
<a
className={cx('ImageGallery-toolbar-action-icon')}
data-tooltip={__(action.label)}
data-position="top"
>
{React.isValidElement(action.icon) ? (
React.cloneElement(action.icon, {
className: cx('icon', action.iconClassName)
})
) : (
<Icon
icon={action.icon}
className={cx('icon', action.iconClassName)}
/>
)}
</a>
</div>
))}
</div>
);
} }
render() { render() {
const {children, classnames: cx, modalContainer} = this.props; const {children, classnames: cx, modalContainer, actions} = this.props;
const {index, items} = this.state; const {index, items, rotate, scale} = this.state;
const __ = this.props.translate; const __ = this.props.translate;
return ( return (
@ -116,7 +244,14 @@ export class ImageGallery extends React.Component<
{items[index].title} {items[index].title}
</div> </div>
<div className={cx('ImageGallery-main')}> <div className={cx('ImageGallery-main')}>
<img src={items[index].originalSrc} /> <img
src={items[index].originalSrc}
style={{transform: `scale(${scale}) rotate(${rotate}deg)`}}
/>
{Array.isArray(actions) && actions.length > 0
? this.renderToolbar(actions)
: null}
{items.length > 1 ? ( {items.length > 1 ? (
<> <>
@ -143,6 +278,7 @@ export class ImageGallery extends React.Component<
</div> </div>
</> </>
) : null} ) : null}
{items.length > 1 ? ( {items.length > 1 ? (
<div className={cx('ImageGallery-footer')}> <div className={cx('ImageGallery-footer')}>
<a className={cx('ImageGallery-prevList is-disabled')}> <a className={cx('ImageGallery-prevList is-disabled')}>

View File

@ -103,6 +103,9 @@ import InvisibleIcon from '../icons/invisible.svg';
import DownIcon from '../icons/down.svg'; import DownIcon from '../icons/down.svg';
import RightDoubleArrowIcon from '../icons/right-double-arrow.svg'; import RightDoubleArrowIcon from '../icons/right-double-arrow.svg';
import NewEdit from '../icons/new-edit.svg'; import NewEdit from '../icons/new-edit.svg';
import RotateLeft from '../icons/rotate-left.svg';
import RotateRight from '../icons/rotate-right.svg';
import ScaleOrigin from '../icons/scale-origin.svg';
// 兼容原来的用法,后续不直接试用。 // 兼容原来的用法,后续不直接试用。
@ -233,6 +236,9 @@ registerIcon('invisible', InvisibleIcon);
registerIcon('down', DownIcon); registerIcon('down', DownIcon);
registerIcon('right-double-arrow', RightDoubleArrowIcon); registerIcon('right-double-arrow', RightDoubleArrowIcon);
registerIcon('new-edit', NewEdit); registerIcon('new-edit', NewEdit);
registerIcon('rotate-left', RotateLeft);
registerIcon('rotate-right', RotateRight);
registerIcon('scale-origin', ScaleOrigin);
export function Icon({ export function Icon({
icon, icon,

View File

@ -0,0 +1 @@
<svg t="1658923319458" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9742" width="200" height="200"><path d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32z m-44 402H188V494h440v326z" p-id="9743"></path><path d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-0.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7 0.4 12.6-6.1v-63.9c12.9 0.1 25.9 0.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-0.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3z" p-id="9744"></path></svg>

After

Width:  |  Height:  |  Size: 701 B

View File

@ -0,0 +1 @@
<svg t="1658923268348" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9594" width="200" height="200"><path d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-0.4-12.6 6.1l-0.2 64c-118.6 0.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-0.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z" p-id="9595"></path><path d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32z m-44 402H396V494h440v326z" p-id="9596"></path></svg>

After

Width:  |  Height:  |  Size: 701 B

View File

@ -0,0 +1 @@
<svg t="1658924156367" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12727" width="200" height="200"><path d="M316 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8zM512 622c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39zM512 482c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39z" p-id="12728"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" p-id="12729"></path><path d="M648 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8z" p-id="12730"></path></svg>

After

Width:  |  Height:  |  Size: 735 B

View File

@ -290,6 +290,11 @@ register('de-DE', {
'Year.placeholder': 'Wählen Sie ein Jahr', 'Year.placeholder': 'Wählen Sie ein Jahr',
'reload': 'Neu laden', 'reload': 'Neu laden',
'rotate': 'Drehen', 'rotate': 'Drehen',
'rotate.left': 'Nach links drehen',
'rotate.right': 'Drehe nach rechts',
'zoomIn': 'Vergrößern',
'zoomOut': 'Verkleinern',
'scale.origin': 'Originalmaße',
'Editor.fullscreen': 'Schirmfüllend Modus', 'Editor.fullscreen': 'Schirmfüllend Modus',
'Editor.exitFullscreen': 'Zurücktreten Schirmfüllend Modus', 'Editor.exitFullscreen': 'Zurücktreten Schirmfüllend Modus',
'Condition.not': 'nicht', 'Condition.not': 'nicht',

View File

@ -280,6 +280,11 @@ register('en-US', {
'Year.placeholder': 'Select a Year', 'Year.placeholder': 'Select a Year',
'reload': 'Reload', 'reload': 'Reload',
'rotate': 'Rotate', 'rotate': 'Rotate',
'rotate.left': 'Rotate left',
'rotate.right': 'Rotate right',
'zoomIn': 'Zoom in',
'zoomOut': 'Zoom out',
'scale.origin': 'Original scale',
'Editor.fullscreen': 'full screen', 'Editor.fullscreen': 'full screen',
'Editor.exitFullscreen': 'exit fullscreen mode', 'Editor.exitFullscreen': 'exit fullscreen mode',
'Condition.not': 'not', 'Condition.not': 'not',

View File

@ -282,6 +282,11 @@ register('zh-CN', {
'Year.placeholder': '请选择年', 'Year.placeholder': '请选择年',
'reload': '刷新', 'reload': '刷新',
'rotate': '旋转', 'rotate': '旋转',
'rotate.left': '向左旋转',
'rotate.right': '向右旋转',
'zoomIn': '放大',
'zoomOut': '缩小',
'scale.origin': '原始尺寸',
'Editor.fullscreen': '全屏', 'Editor.fullscreen': '全屏',
'Editor.exitFullscreen': '退出全屏', 'Editor.exitFullscreen': '退出全屏',
'Condition.not': '非', 'Condition.not': '非',