From 42b9910df04b997047bea9dfa469725c5851e5a8 Mon Sep 17 00:00:00 2001 From: 2betop <2betop.cn@gmail.com> Date: Tue, 7 Jan 2020 19:42:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90=E5=9B=BE=E7=89=87=E9=9B=86?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/components/CRUD/Fields.jsx | 11 +- examples/components/Form/Static.jsx | 44 +++++- scss/components/_image-gallery.scss | 204 ++++++++++++++++++++++++++++ scss/components/_modal.scss | 24 ++-- scss/components/_tooltip.scss | 2 +- scss/themes/cxd.scss | 1 + scss/themes/dark.scss | 1 + scss/themes/default.scss | 1 + src/components/ImageGallery.tsx | 177 ++++++++++++++++++++++++ src/components/Modal.tsx | 11 +- src/components/icons.tsx | 2 + src/factory.tsx | 43 +++--- src/renderers/Dialog.tsx | 1 + src/renderers/Image.tsx | 80 +++++------ src/renderers/Images.tsx | 67 ++++++++- src/renderers/Table.tsx | 46 ++++++- 16 files changed, 635 insertions(+), 80 deletions(-) create mode 100644 scss/components/_image-gallery.scss create mode 100644 src/components/ImageGallery.tsx diff --git a/examples/components/CRUD/Fields.jsx b/examples/components/CRUD/Fields.jsx index 89ca40a3..8ed7d612 100644 --- a/examples/components/CRUD/Fields.jsx +++ b/examples/components/CRUD/Fields.jsx @@ -30,10 +30,13 @@ export default { type: 'image', label: '图片', name: 'image', - popOver: { - title: '查看大图', - body: '
' - } + enlargeAble: true, + title: '233', + thumbMode: 'cover' + // popOver: { + // title: '查看大图', + // body: '
' + // } }, { name: 'date', diff --git a/examples/components/Form/Static.jsx b/examples/components/Form/Static.jsx index ee0be700..4636c5a8 100644 --- a/examples/components/Form/Static.jsx +++ b/examples/components/Form/Static.jsx @@ -4,7 +4,39 @@ export default { data: { id: 1, image: - 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg' + 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg', + images: [ + { + image: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80', + src: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg' + }, + { + image: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692942/d8e4992057f9.jpeg@s_0,w_216,l_1,f_jpg,q_80', + src: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692942/d8e4992057f9.jpeg' + }, + { + image: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693148/1314a2a3d3f6.jpeg@s_0,w_216,l_1,f_jpg,q_80', + src: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693148/1314a2a3d3f6.jpeg' + }, + { + image: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693379/8f2e79f82be0.jpeg@s_0,w_216,l_1,f_jpg,q_80', + src: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693379/8f2e79f82be0.jpeg' + }, + { + image: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693566/552b175ef11d.jpeg@s_0,w_216,l_1,f_jpg,q_80', + src: + 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693566/552b175ef11d.jpeg' + } + ] }, body: [ { @@ -112,6 +144,16 @@ export default { originalSrc: '${image}' }, + { + type: 'static-images', + label: '图片集', + name: 'images', + thumbMode: 'cover', + thumbRatio: '4:3', + enlargeAble: true, + originalSrc: '${src}' // 注意这个取变量是想对数组成员取的。 + }, + { type: 'divider' }, diff --git a/scss/components/_image-gallery.scss b/scss/components/_image-gallery.scss new file mode 100644 index 00000000..15f34aba --- /dev/null +++ b/scss/components/_image-gallery.scss @@ -0,0 +1,204 @@ +@keyframes disappear { + to { + opacity: 0; + } +} + +@keyframes appear { + from { + opacity: 0; + } +} + +.#{$ns}ImageGallery { + display: flex; + flex-direction: column; + background: transparent; + border: none; + border-radius: 0; + max-width: 1010px !important; + + &-close { + position: absolute; + right: 0; + top: 0; + color: rgba(255, 255, 255, 0.8); + cursor: pointer; + + &:hover { + color: #fff; + } + + > svg { + width: px2rem(16px); + height: px2rem(16px); + } + } + + &-title { + height: px2rem(30px); + vertical-align: top; + line-height: px2rem(30px); + font-size: px2rem(12px); + color: $white; + text-align: center; + } + + &-main { + background: #000; + flex-basis: 0; + flex-grow: 1; + position: relative; + display: flex; + justify-content: center; + align-items: center; + user-select: none; + + > img { + display: block; + max-width: 100%; + max-height: 100%; + } + } + + &-prevBtn, + &-nextBtn { + > svg { + width: px2rem(48px); + height: px2rem(48px); + } + + position: absolute; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + color: #999; + text-shadow: rgba(0, 0, 0, 0.3) 0px 0px 4px; + + &:hover { + color: #fff; + text-shadow: rgba(0, 0, 0, 0.5) 0px 0px 4px; + } + animation-name: disappear; + animation-delay: 3s; + animation-duration: 0.35s; + animation-fill-mode: both; + + &.is-disabled { + pointer-events: none; + } + } + + &-main:hover &-prevBtn, + &-main:hover &-nextBtn { + animation-name: appear; + animation-delay: 0s; + animation-duration: 0.35s; + } + + &-prevBtn { + left: px2rem(20px); + } + + &-nextBtn { + right: px2rem(20px); + } + + &-footer { + height: px2rem(74px); + background: #222; + display: flex; + flex-direction: row; + user-select: none; + } + + &-prevList, + &-nextList { + width: px2rem(20px); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + background: rgba(0, 0, 0, 0.3); + color: #fff; + + &.is-disabled { + background: rgba(0, 0, 0, 0.3); + color: rgba(255, 255, 255, 0.1); + pointer-events: none; + } + + &:hover { + background: rgba(0, 0, 0, 1); + color: #fff; + } + } + + &-itemsWrap { + flex-grow: 1; + flex-basis: 0; + width: 0; + overflow: hidden; + align-items: center; + justify-content: center; + display: flex; + } + + &-items { + display: inline-block; + white-space: nowrap; + } + + &-item { + margin: 10px 5px; + width: 54px; + height: 54px; + display: inline-flex; + position: relative; + border: 1px solid #666; + justify-content: center; + align-items: center; + cursor: pointer; + + > img { + display: block; + max-width: 100%; + max-height: 100%; + } + + @supports (object-fit: cover) { + > img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + &:after { + position: absolute; + content: ''; + display: block; + background: rgba(0, 0, 0, 0.5); + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + } + + &:hover { + border: 1px solid #e5e5e5; + &:after { + display: none; + } + } + + &.is-active { + border: 1px solid #108cee; + &:after { + display: none; + } + } + } +} diff --git a/scss/components/_modal.scss b/scss/components/_modal.scss index 8d99d8a2..dfa3f14a 100644 --- a/scss/components/_modal.scss +++ b/scss/components/_modal.scss @@ -219,14 +219,22 @@ color: $danger; } -.#{$ns}Modal--full .#{$ns}Modal-content { - width: calc(100% - 60px); - height: calc(100% - 60px); - max-width: unset; - margin: px2rem(30px); +.#{$ns}Modal--full { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; - > .#{$ns}Modal-body { - height: 0; - overflow: auto; + > .#{$ns}Modal-content { + flex-basis: 0; + flex-grow: 1; + margin: 30px; + width: calc(100% - 60px); + max-width: unset; + + > .#{$ns}Modal-body { + height: 0; + overflow: auto; + } } } diff --git a/scss/components/_tooltip.scss b/scss/components/_tooltip.scss index a92a4f90..e3c9d9ae 100644 --- a/scss/components/_tooltip.scss +++ b/scss/components/_tooltip.scss @@ -230,7 +230,7 @@ } &[data-position='left']:hover:after { - margin: 0 0 0 $Tooltip--attr-gap; + margin: 0 $Tooltip--attr-gap 0 0; } &[data-position='top']:after { diff --git a/scss/themes/cxd.scss b/scss/themes/cxd.scss index 844b2d36..ef007581 100644 --- a/scss/themes/cxd.scss +++ b/scss/themes/cxd.scss @@ -534,6 +534,7 @@ $Card-actions-onChecked-onHover-bg: $white; @import '../components/wrapper'; @import '../components/status'; @import '../components/carousel'; +@import '../components/image-gallery'; @import '../components/images'; @import '../components/form/fieldset'; diff --git a/scss/themes/dark.scss b/scss/themes/dark.scss index cb44c895..34447b70 100644 --- a/scss/themes/dark.scss +++ b/scss/themes/dark.scss @@ -200,6 +200,7 @@ pre { @import '../components/wrapper'; @import '../components/status'; @import '../components/carousel'; +@import '../components/image-gallery'; @import '../components/images'; @import '../components/form/fieldset'; diff --git a/scss/themes/default.scss b/scss/themes/default.scss index 8c8dcde9..1f887442 100644 --- a/scss/themes/default.scss +++ b/scss/themes/default.scss @@ -65,6 +65,7 @@ $Form-input-borderColor: #cfdadd; @import '../components/wrapper'; @import '../components/status'; @import '../components/carousel'; +@import '../components/image-gallery'; @import '../components/images'; @import '../components/form/fieldset'; diff --git a/src/components/ImageGallery.tsx b/src/components/ImageGallery.tsx new file mode 100644 index 00000000..fd36d23a --- /dev/null +++ b/src/components/ImageGallery.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import {themeable, ClassNamesFn} from '../theme'; +import {autobind} from '../utils/helper'; +import Modal from './Modal'; +import {Icon} from './icons'; + +export interface ImageGalleryProps { + classnames: ClassNamesFn; + classPrefix: string; + children: React.ReactNode; +} + +export interface ImageGalleryState { + isOpened: boolean; + index: number; + items: Array<{ + src: string; + originalSrc: string; + title?: string; + caption?: string; + }>; +} + +export class ImageGallery extends React.Component< + ImageGalleryProps, + ImageGalleryState +> { + state: ImageGalleryState = { + isOpened: false, + index: -1, + items: [] + }; + + @autobind + handleImageEnlarge(info: { + src: string; + originalSrc: string; + list?: Array<{ + src: string; + originalSrc: string; + title?: string; + caption?: string; + }>; + title?: string; + caption?: string; + index?: number; + }) { + this.setState({ + isOpened: true, + items: info.list ? info.list : [info], + index: info.index || 0 + }); + } + + @autobind + close() { + this.setState({ + isOpened: false + }); + } + + @autobind + prev() { + const index = this.state.index; + this.setState({ + index: index - 1 + }); + } + + @autobind + next() { + const index = this.state.index; + this.setState({ + index: index + 1 + }); + } + + @autobind + handleItemClick(e: React.MouseEvent) { + const index = parseInt(e.currentTarget.getAttribute('data-index')!, 10); + this.setState({ + index + }); + } + + render() { + const {children, classnames: cx} = this.props; + const {index, items} = this.state; + + return ( + <> + {React.cloneElement(children as any, { + onImageEnlarge: this.handleImageEnlarge + })} + + + + + + {~index && items[index] ? ( + <> +
+ {items[index].title} +
+
+ + + {items.length > 1 ? ( + <> + + + + = items.length - 1 ? 'is-disabled' : '' + )} + onClick={this.next} + > + + + + ) : null} +
+ + ) : null} + {items.length > 1 ? ( +
+ + + +
+
+ {items.map((item, i) => ( +
+ +
+ ))} +
+
+ + + +
+ ) : null} +
+ + ); + } +} + +export default themeable(ImageGallery); diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index a729c190..0afb4c9e 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -17,6 +17,7 @@ import {ClassNamesFn, themeable} from '../theme'; export interface ModalProps { className?: string; + contentClassName?: string; size?: any; overlay?: boolean; onHide: () => void; @@ -83,6 +84,7 @@ export class Modal extends React.Component { render() { const { className, + contentClassName, children, container, show, @@ -92,6 +94,7 @@ export class Modal extends React.Component { } = this.props; return ( + // @ts-ignore { {overlay ? (
) : null} -
+
{children}
diff --git a/src/components/icons.tsx b/src/components/icons.tsx index c1458eb2..65f621f8 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -104,6 +104,8 @@ registerIcon('play', PlayIcon); registerIcon('pause', PauseIcon); registerIcon('left-arrow', LeftArrowIcon); registerIcon('right-arrow', RightArrowIcon); +registerIcon('prev', LeftArrowIcon); +registerIcon('next', RightArrowIcon); registerIcon('check', CheckIcon); registerIcon('plus', PlusIcon); registerIcon('minus', MinusIcon); diff --git a/src/factory.tsx b/src/factory.tsx index d7488d1e..49286f80 100644 --- a/src/factory.tsx +++ b/src/factory.tsx @@ -38,6 +38,7 @@ import {getTheme, ThemeInstance, ClassNamesFn, ThemeContext} from './theme'; import find = require('lodash/find'); import Alert from './components/Alert2'; import {LazyComponent} from './components'; +import ImageGallery from './components/ImageGallery'; export interface TestFunc { ( @@ -383,26 +384,28 @@ export class RootRenderer extends React.Component { return ( - { - renderChild( - pathPrefix || '', - isPlainObject(schema) - ? { - type: 'page', - ...(schema as Schema) - } - : schema, - { - ...rest, - resolveDefinitions: this.resolveDefinitions, - location: location, - data: finalData, - env, - classnames: theme.classnames, - classPrefix: theme.classPrefix - } - ) as JSX.Element - } + + { + renderChild( + pathPrefix || '', + isPlainObject(schema) + ? { + type: 'page', + ...(schema as Schema) + } + : schema, + { + ...rest, + resolveDefinitions: this.resolveDefinitions, + location: location, + data: finalData, + env, + classnames: theme.classnames, + classPrefix: theme.classPrefix + } + ) as JSX.Element + } + ); diff --git a/src/renderers/Dialog.tsx b/src/renderers/Dialog.tsx index ac6cad12..5391ed5a 100644 --- a/src/renderers/Dialog.tsx +++ b/src/renderers/Dialog.tsx @@ -403,6 +403,7 @@ export default class Dialog extends React.Component { {showCloseButton !== false && !store.loading ? ( diff --git a/src/renderers/Image.tsx b/src/renderers/Image.tsx index c62938a1..e7adde18 100644 --- a/src/renderers/Image.tsx +++ b/src/renderers/Image.tsx @@ -9,17 +9,11 @@ export interface ImageThumbProps { src: string; originalSrc?: string; // 原图 enlargeAble?: boolean; - onEnlarge?: (info: { - src: string; - originalSrc: string; - title?: string; - caption?: string; - thumbMode?: 'w-full' | 'h-full' | 'contain' | 'cover'; - thumbRatio?: '1:1' | '4:3' | '16:9'; - }) => void; + onEnlarge?: (info: ImageThumbProps) => void; showDimensions?: boolean; title?: string; alt?: string; + index?: number; className?: string; imageClassName?: string; caption?: string; @@ -33,26 +27,8 @@ export interface ImageThumbProps { export class ImageThumb extends React.Component { @autobind handleEnlarge() { - const { - onEnlarge, - src, - originalSrc, - title, - caption, - thumbMode, - thumbRatio - } = this.props; - - onEnlarge && - originalSrc && - onEnlarge({ - src, - originalSrc, - title, - caption, - thumbMode, - thumbRatio - }); + const {onEnlarge, ...rest} = this.props; + onEnlarge && onEnlarge(rest); } render() { @@ -123,14 +99,17 @@ export interface ImageFieldProps extends RendererProps { thumbRatio: '1:1' | '4:3' | '16:9'; originalSrc?: string; // 原图 enlargeAble?: boolean; - onEnlarge?: (info: { - src: string; - originalSrc: string; - title?: string; - caption?: string; - thumbMode?: 'w-full' | 'h-full' | 'contain' | 'cover'; - thumbRatio?: '1:1' | '4:3' | '16:9'; - }) => void; + onImageEnlarge?: ( + info: { + src: string; + originalSrc: string; + title?: string; + caption?: string; + thumbMode?: 'w-full' | 'h-full' | 'contain' | 'cover'; + thumbRatio?: '1:1' | '4:3' | '16:9'; + }, + target: any + ) => void; showDimensions?: boolean; } @@ -146,6 +125,31 @@ export class ImageField extends React.Component { placeholder: '-' }; + @autobind + handleEnlarge({ + src, + originalSrc, + title, + caption, + thumbMode, + thumbRatio + }: ImageThumbProps) { + const {onImageEnlarge} = this.props; + + onImageEnlarge && + onImageEnlarge( + { + src, + originalSrc: originalSrc || src, + title, + caption, + thumbMode, + thumbRatio + }, + this.props + ); + } + render() { const { className, @@ -178,9 +182,9 @@ export class ImageField extends React.Component { caption={filter(imageCaption, data)} thumbMode={thumbMode} thumbRatio={thumbRatio} - originalSrc={originalSrc} + originalSrc={filter(originalSrc, data, '| raw')} enlargeAble={enlargeAble} - onEnlarge={onEnlarge} + onEnlarge={this.handleEnlarge} showDimensions={showDimensions} /> ) : ( diff --git a/src/renderers/Images.tsx b/src/renderers/Images.tsx index 3dce0f61..719bedc7 100644 --- a/src/renderers/Images.tsx +++ b/src/renderers/Images.tsx @@ -2,7 +2,8 @@ import React from 'react'; import {Renderer, RendererProps} from '../factory'; import {filter} from '../utils/tpl'; import {resolveVariable, isPureVariable} from '../utils/tpl-builtin'; -import Image from './Image'; +import Image, {ImageThumbProps} from './Image'; +import {autobind} from '../utils/helper'; export interface ImagesProps extends RendererProps { className: string; @@ -10,11 +11,22 @@ export interface ImagesProps extends RendererProps { placeholder: string; delimiter: string; thumbMode: 'w-full' | 'h-full' | 'contain' | 'cover'; - thumbRatio: '1-1' | '4-3' | '16-9'; + thumbRatio: '1:1' | '4:3' | '16:9'; name?: string; value?: any; source?: string; + src?: string; + originalSrc?: string; // 原图 + enlargeAble?: boolean; + onEnlarge?: ( + info: ImageThumbProps & { + list?: Array< + Pick + >; + } + ) => void; + showDimensions?: boolean; } export class ImagesField extends React.Component { @@ -33,9 +45,35 @@ export class ImagesField extends React.Component { 'https://fex.bdstatic.com/n/static/amis/renderers/crud/field/placeholder_cfad9b1.png', placehoder: '-', thumbMode: 'contain', - thumbRatio: '1-1' + thumbRatio: '1:1' }; + list: Array = []; + + @autobind + handleEnlarge(info: ImageThumbProps) { + const {onImageEnlarge, src, originalSrc} = this.props; + + onImageEnlarge && + onImageEnlarge( + { + ...info, + originalSrc: info.originalSrc || info.src, + list: this.list.map(item => ({ + src: src + ? filter(src, item, '| raw') + : (item && item.image) || item, + originalSrc: originalSrc + ? filter(originalSrc, item, '| raw') + : item && item.src, + title: item && item.title, + caption: item && (item.description || item.caption) + })) + }, + this.props + ); + } + render() { const { className, @@ -48,7 +86,10 @@ export class ImagesField extends React.Component { placeholder, classnames: cx, source, - delimiter + delimiter, + enlargeAble, + src, + originalSrc } = this.props; let list: any; @@ -67,19 +108,33 @@ export class ImagesField extends React.Component { list = [list]; } + this.list = list; + return (
{Array.isArray(list) ? (
{list.map((item: any, index: number) => ( ))}
diff --git a/src/renderers/Table.tsx b/src/renderers/Table.tsx index 663ec213..ea72d2ae 100644 --- a/src/renderers/Table.tsx +++ b/src/renderers/Table.tsx @@ -89,6 +89,7 @@ export interface TableProps extends RendererProps { ) => void; onSaveOrder?: (moved: Array, items: Array) => void; onQuery: (values: object) => void; + onImageEnlarge?: (data: any, target: any) => void; buildItemProps?: (item: any, index: number) => any; checkOnItemClick?: boolean; hideCheckToggler?: boolean; @@ -860,6 +861,48 @@ export default class Table extends React.Component { ); } + @autobind + handleImageEnlarge(info: any, target: {rowIndex: number; colIndex: number}) { + const onImageEnlarge = this.props.onImageEnlarge; + + // 如果已经是多张了,直接跳过 + if (Array.isArray(info.list)) { + return onImageEnlarge && onImageEnlarge(info, target); + } + + // 从列表中收集所有图片,然后作为一个图片集合派送出去。 + const store = this.props.store; + const column = store.filteredColumns[target.colIndex].pristine; + + const list: Array = []; + store.rows.forEach(row => { + const src = resolveVariable(column.name, row.data); + + list.push({ + src, + originalSrc: column.originalSrc + ? filter(column.originalSrc, row.data) + : src, + title: column.title ? filter(column.title, row.data) : undefined, + caption: column.caption ? filter(column.caption, row.data) : undefined + }); + }); + + if (list.length > 1) { + onImageEnlarge && + onImageEnlarge( + { + ...info, + list, + index: target.rowIndex + }, + target + ); + } else { + onImageEnlarge && onImageEnlarge(info, target); + } + } + renderHeading() { let { title, @@ -1186,7 +1229,8 @@ export default class Table extends React.Component { popOverContainer: this.getPopOverContainer, rowSpan: item.rowSpans[column.name as string], quickEditFormRef: this.subFormRef, - prefix + prefix, + onImageEnlarge: this.handleImageEnlarge }; delete subProps.label;