diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index f3f8d92d6..c41943e4d 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -18,6 +18,7 @@ import { i18n } from '../i18n'; import { CSSVariableProvider } from '../style/css-variable'; import { AntdAppProvider, GlobalThemeProvider } from '../style/theme'; import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; +import { AttachmentPreviewManager, PluginAttachmentItemsOptions } from './AttachmentPreviewManager'; import { AppComponent, BlankComponent, defaultAppComponents } from './components'; import { NoticeManager } from './NoticesManager'; import type { Plugin } from './Plugin'; @@ -63,6 +64,7 @@ export interface ApplicationOptions { devDynamicImport?: DevDynamicImport; dataSourceManager?: DataSourceManagerOptions; pluginMenuItems?: Record; + attachmentItem?: Record; } export class Application { @@ -89,7 +91,7 @@ export class Application { public dataSourceManager: DataSourceManager; public noticeManager: NoticeManager; public pluginContextMenu: PluginContextMenu; - + public AttachmentPreviewManager: AttachmentPreviewManager; public name: string; loading = true; @@ -137,6 +139,7 @@ export class Application { this.addRoutes(); this.name = this.options.name || getSubAppName(options.publicPath) || 'main'; this.pluginContextMenu = new PluginContextMenu(options.pluginMenuItems, this); + this.AttachmentPreviewManager = new AttachmentPreviewManager(options.attachmentItem, this); } private initRequireJs() { diff --git a/packages/core/client/src/application/AttachmentPreviewManager.ts b/packages/core/client/src/application/AttachmentPreviewManager.ts new file mode 100644 index 000000000..ee5bd731a --- /dev/null +++ b/packages/core/client/src/application/AttachmentPreviewManager.ts @@ -0,0 +1,32 @@ +import type { Application } from './Application'; + +export interface PluginAttachmentItemsOptions { + key: string; + type: string; + viewComponet: (any) => JSX.Element; + checkedComponent: (any) => JSX.Element; +} + +export class AttachmentPreviewManager { + protected attachmentItem: Record = {}; + public app: Application; + + constructor(_pluginAttachmentItems: Record, app: Application) { + this.app = app; + Object.entries(_pluginAttachmentItems || {}).forEach(([name, PluginItemsOptions]) => { + this.add(PluginItemsOptions); + }); + } + + get() { + return this.attachmentItem; + } + + getTypeItem(key) { + return this.attachmentItem[key]; + } + + add(options: PluginAttachmentItemsOptions) { + this.attachmentItem[options.key] = options; + } +} diff --git a/packages/core/client/src/application/Plugin.ts b/packages/core/client/src/application/Plugin.ts index 102d0c0cb..0f5ad17c8 100644 --- a/packages/core/client/src/application/Plugin.ts +++ b/packages/core/client/src/application/Plugin.ts @@ -43,6 +43,10 @@ export class Plugin { return this.app.dataSourceManager; } + get AttachmentPreviewManager() { + return this.app.AttachmentPreviewManager; + } + async afterAdd() {} async beforeLoad() {} diff --git a/packages/core/client/src/application/index.ts b/packages/core/client/src/application/index.ts index 3f3e73710..2365ade1e 100644 --- a/packages/core/client/src/application/index.ts +++ b/packages/core/client/src/application/index.ts @@ -12,3 +12,4 @@ export * from './NoticesManager'; export * from './hoc'; export { ApplicationContext } from './context'; export * from './PluginContextMenu'; +export * from './AttachmentPreviewManager'; diff --git a/packages/core/client/src/built-in/attachment-preview/AttachmentItems.tsx b/packages/core/client/src/built-in/attachment-preview/AttachmentItems.tsx new file mode 100644 index 000000000..2edfb1262 --- /dev/null +++ b/packages/core/client/src/built-in/attachment-preview/AttachmentItems.tsx @@ -0,0 +1,146 @@ +import React from 'react'; + +import { FileFilled } from '@ant-design/icons'; +import { saveAs } from 'file-saver'; + +export const imagejpeg = { + key: 'image/jpeg', + type: 'image/jpeg', + viewComponet: (props) => { + const { file, prefixCls } = props; + return ( + file.imageUrl && ( + {file.title} + ) + ); + }, + checkedComponent: (props) => { + const { file } = props; + return ( + file.imageUrl && ( + {file.title} + ) + ); + }, +}; + +export const imagePng = { + key: 'image/png', + type: 'image/png', + viewComponet: (props) => { + const { file, prefixCls } = props; + return ( + file.imageUrl && ( + {file.title} + ) + ); + }, + checkedComponent: (props) => { + const { file } = props; + return ( + file.imageUrl && ( + {file.title} + ) + ); + }, +}; + +export const fileDef = { + key: 'default', + type: 'default', + viewComponet: (props) => { + const { prefixCls } = props; + return ( + + ); + }, + checkedComponent: (props) => { + return ( +
+ +
+ ); + }, +}; + +export const filePdf = { + key: 'application/pdf', + type: 'application/pdf', + viewComponet: (props) => { + const { file, prefixCls } = props; + return ( + file.imageUrl && ( + {file.title} + ) + ); + }, + checkedComponent: (props) => { + const { file } = props; + return ( +
+ -
- - )} , ); }; @@ -242,3 +139,87 @@ ReadPretty.Upload = function Upload() { )); }; + +export const ReadFile = ({ file, prefixCls, size, images, setFileIndex, setVisible, preview }) => { + const { viewComponet } = preview; + const handleClick = (e) => { + const index = images.indexOf(file); + e.preventDefault(); + e.stopPropagation(); + setVisible(true); + setFileIndex(index); + }; + return ( +
+
+ + {size !== 'small' && ( + + +
+ {images.length > 1 && size === 'small' && ( +
+ ... +
+ )} +
+ ); +}; diff --git a/packages/core/client/src/schema-component/antd/upload/Upload.tsx b/packages/core/client/src/schema-component/antd/upload/Upload.tsx index 7c127d738..cf0a1ce37 100644 --- a/packages/core/client/src/schema-component/antd/upload/Upload.tsx +++ b/packages/core/client/src/schema-component/antd/upload/Upload.tsx @@ -8,6 +8,7 @@ import cls from 'classnames'; import { saveAs } from 'file-saver'; import { useTranslation } from 'react-i18next'; +import { useApp } from '../../../application'; import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; import { useProps } from '../../hooks/useProps'; import { ReadPretty } from './ReadPretty'; @@ -29,18 +30,15 @@ Upload.Attachment = connect((props: UploadProps) => { const { disabled, multiple, value, onChange } = props; const [fileList, setFileList] = useState([]); const [sync, setSync] = useState(true); + const app = useApp(); + const previewList = app.AttachmentPreviewManager.get(); const images = fileList; const [fileIndex, setFileIndex] = useState(0); const [visible, setVisible] = useState(false); - const [fileType, setFileType] = useState<'image' | 'pdf'>(); const { t } = useTranslation(); const uploadProps = useUploadProps({ ...props }); const { wrapSSR, hashId, componentCls: prefixCls } = useStyles(); const internalFileList = useRef([]); - - function closeIFrameModal() { - setVisible(false); - } useEffect(() => { if (sync) { const fileList = toFileList(value); @@ -53,104 +51,23 @@ Upload.Attachment = connect((props: UploadProps) => {
- {fileList.map((file) => { - const handleClick = (e) => { - e.preventDefault(); - e.stopPropagation(); - const index = fileList.indexOf(file); - if (isImage(file.extname)) { - setFileType('image'); - setVisible(true); - setFileIndex(index); - } else if (isPdf(file.extname)) { - setVisible(true); - setFileIndex(index); - setFileType('pdf'); - } else { - saveAs(file.url, `${file.title}${file.extname}`); - } + {fileList.map((file, index) => { + const fileProps = { + file, + prefixCls, + fileList, + setFileIndex, + setVisible, + disabled, + t, + setSync, + setFileList, + multiple, + onChange, + internalFileList, + preview: previewList[file?.mimetype] || previewList['default'], }; - return ( -
-
- - - -
-
- ); + return ; })} {!disabled && (multiple || toArr(value).length < 1) && (
@@ -198,12 +115,16 @@ Upload.Attachment = connect((props: UploadProps) => {
{/* 预览图片的弹框 */} - {visible && fileType === 'image' && ( + {visible && ( setVisible(false)} onMovePrevRequest={() => setFileIndex((fileIndex + images.length - 1) % images.length)} onMoveNextRequest={() => setFileIndex((fileIndex + 1) % images.length)} @@ -227,59 +148,6 @@ Upload.Attachment = connect((props: UploadProps) => { ]} /> )} - - {visible && fileType === 'pdf' && ( - { - e.preventDefault(); - e.stopPropagation(); - const file = images[fileIndex]; - saveAs(file.url, `${file.title}${file.extname}`); - }} - > - {t('download')} - , - , - ]} - width={'85vw'} - centered={true} - > -
- -
-
- )}
, ); }, mapReadPretty(ReadPretty.File)); @@ -355,3 +223,107 @@ function updateFileList(file: UploadFile, fileList: (UploadFile | Readonly { + const { + file, + prefixCls, + fileList, + setFileIndex, + setVisible, + disabled, + t, + setSync, + setFileList, + multiple, + onChange, + internalFileList, + preview, + } = props; + const handleClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + const index = fileList.indexOf(file); + setVisible(true); + setFileIndex(index); + }; + const { viewComponet } = preview; + return ( +
+
+ + + +
+
+ ); +}; diff --git a/packages/core/components/src/lightbox/react-image-lightbox.tsx b/packages/core/components/src/lightbox/react-image-lightbox.tsx index 49663663e..cff7b3a71 100644 --- a/packages/core/components/src/lightbox/react-image-lightbox.tsx +++ b/packages/core/components/src/lightbox/react-image-lightbox.tsx @@ -25,6 +25,8 @@ import { getHighestSafeWindowContext, getWindowHeight, getWindowWidth, translate import './style.css'; +import { css } from '@tachybase/client'; + class ReactImageLightbox extends Component { static isTargetMatchImage(target) { return target && /ril-image-current/.test(target.className); @@ -282,10 +284,9 @@ class ReactImageLightbox extends Component { } // Get info for the best suited image to display with the given srcType - getBestImageForType(srcType) { - let imageSrc = this.props[srcType]; + getBestImageForType(srcType, fileSrc?) { + let imageSrc = fileSrc || this.props[srcType]; let fitSizes = {}; - if (this.isImageLoaded(imageSrc)) { // Use full-size image if available fitSizes = this.getFitSizes(this.imageCache[imageSrc].width, this.imageCache[imageSrc].height); @@ -394,6 +395,30 @@ class ReactImageLightbox extends Component { name: 'prevSrcThumbnail', keyEnding: `t${this.keyCounter - 1}`, }, + { + name: 'mainFile', + keyEnding: `i${this.keyCounter}`, + }, + { + name: 'mainSrcThumbnail', + keyEnding: `t${this.keyCounter}`, + }, + { + name: 'nextFile', + keyEnding: `i${this.keyCounter + 1}`, + }, + { + name: 'nextFileThumbnail', + keyEnding: `t${this.keyCounter + 1}`, + }, + { + name: 'prevFile', + keyEnding: `i${this.keyCounter - 1}`, + }, + { + name: 'prevFileThumbnail', + keyEnding: `t${this.keyCounter - 1}`, + }, ]; } @@ -1175,6 +1200,10 @@ class ReactImageLightbox extends Component { imageTitle, nextSrc, prevSrc, + mainFile, + nextFile, + prevFile, + previewList, toolbarButtons, reactModalStyle, onAfterOpen, @@ -1203,17 +1232,21 @@ class ReactImageLightbox extends Component { // Images to be displayed const images = []; - const addImage = (srcType, imageClass, transforms) => { - // Ignore types that have no source defined for their full size image - if (!this.props[srcType]) { - return; - } - const bestImageInfo = this.getBestImageForType(srcType); + const addImage = (srcType, imageClass, transforms) => { + const fieldType = this.props[srcType]?.mimetype; + const { checkedComponent } = previewList[fieldType] || previewList['default']; + const bestImageInfo = (this.props[srcType]?.mimetype as string).includes('image') + ? this.getBestImageForType(srcType, this.props[srcType]?.imageUrl) + : {}; const imageStyle = { ...transitionStyle, ...ReactImageLightbox.getTransform({ ...transforms, + height: boxSize.height - 20, + width: boxSize.width - 20, + targetWidth: boxSize.width - 50, + targeHeight: boxSize.height - 50, ...bestImageInfo, }), }; @@ -1221,99 +1254,43 @@ class ReactImageLightbox extends Component { if (zoomLevel > MIN_ZOOM_LEVEL) { imageStyle.cursor = 'move'; } - - // support IE 9 and 11 - const hasTrueValue = (object) => Object.keys(object).some((key) => object[key]); - - // when error on one of the loads then push custom error stuff - if (bestImageInfo === null && hasTrueValue(loadErrorStatus)) { - images.push( -
-
{this.props.imageLoadErrorMessage}
-
, - ); - - return; - } - if (bestImageInfo === null) { - const loadingIcon = - loader !== undefined ? ( - loader - ) : ( -
- {[...new Array(12)].map((_, index) => ( -
- ))} -
- ); - - // Fall back to loading icon if the thumbnail has not been loaded - images.push( -
-
{loadingIcon}
-
, - ); - - return; - } - - const imageSrc = bestImageInfo.src; - if (discourageDownloads) { - imageStyle.backgroundImage = `url('${imageSrc}')`; - images.push( -
-
-
, - ); - } else { - images.push( - e.preventDefault()} - style={imageStyle} - src={imageSrc} - key={imageSrc + keyEndings[srcType]} - alt={typeof imageTitle === 'string' ? imageTitle : translate('Image')} - draggable={false} - />, - ); - } + images.push( +
+ {checkedComponent({ + key: `${fieldType + keyEndings[srcType]}`, + onDoubleClick: this.handleImageDoubleClick, + onWheel: this.handleImageMouseWheel, + onDragStart: (e) => e.preventDefault(), + file: this.props[srcType], + bestImageInfo, + })} +
, + ); }; const zoomMultiplier = this.getZoomMultiplier(); // Next Image (displayed on the right) - addImage('nextSrc', 'ril-image-next ril__imageNext', { + addImage('nextFile', 'ril-image-next ril__imageNext', { x: boxSize.width, }); // Main Image - addImage('mainSrc', 'ril-image-current', { + addImage('mainFile', 'ril-image-current', { x: -1 * offsetX, y: -1 * offsetY, zoom: zoomMultiplier, }); // Previous Image (displayed on the left) - addImage('prevSrc', 'ril-image-prev ril__imagePrev', { + addImage('prevFile', 'ril-image-prev ril__imagePrev', { x: -1 * boxSize.width, }); @@ -1336,7 +1313,6 @@ class ReactImageLightbox extends Component { ...reactModalStyle.content, // Allow style overrides via props }, }; - return (