fix: 优化附件显示 (#1545)

Co-authored-by: sealday <zhanglin@daoyoucloud.com>
Reviewed-on: daoyoucloud/tachybase#1545
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: wjh <wwwjh0710@163.com>
Co-committed-by: wjh <wwwjh0710@163.com>
This commit is contained in:
wjh 2024-09-24 18:23:40 +08:00 committed by sealday
parent 2b698c6c3d
commit 2e15125445
10 changed files with 528 additions and 396 deletions

View File

@ -18,6 +18,7 @@ import { i18n } from '../i18n';
import { CSSVariableProvider } from '../style/css-variable'; import { CSSVariableProvider } from '../style/css-variable';
import { AntdAppProvider, GlobalThemeProvider } from '../style/theme'; import { AntdAppProvider, GlobalThemeProvider } from '../style/theme';
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
import { AttachmentPreviewManager, PluginAttachmentItemsOptions } from './AttachmentPreviewManager';
import { AppComponent, BlankComponent, defaultAppComponents } from './components'; import { AppComponent, BlankComponent, defaultAppComponents } from './components';
import { NoticeManager } from './NoticesManager'; import { NoticeManager } from './NoticesManager';
import type { Plugin } from './Plugin'; import type { Plugin } from './Plugin';
@ -63,6 +64,7 @@ export interface ApplicationOptions {
devDynamicImport?: DevDynamicImport; devDynamicImport?: DevDynamicImport;
dataSourceManager?: DataSourceManagerOptions; dataSourceManager?: DataSourceManagerOptions;
pluginMenuItems?: Record<string, PluginItemsOptions>; pluginMenuItems?: Record<string, PluginItemsOptions>;
attachmentItem?: Record<string, PluginAttachmentItemsOptions>;
} }
export class Application { export class Application {
@ -89,7 +91,7 @@ export class Application {
public dataSourceManager: DataSourceManager; public dataSourceManager: DataSourceManager;
public noticeManager: NoticeManager; public noticeManager: NoticeManager;
public pluginContextMenu: PluginContextMenu; public pluginContextMenu: PluginContextMenu;
public AttachmentPreviewManager: AttachmentPreviewManager;
public name: string; public name: string;
loading = true; loading = true;
@ -137,6 +139,7 @@ export class Application {
this.addRoutes(); this.addRoutes();
this.name = this.options.name || getSubAppName(options.publicPath) || 'main'; this.name = this.options.name || getSubAppName(options.publicPath) || 'main';
this.pluginContextMenu = new PluginContextMenu(options.pluginMenuItems, this); this.pluginContextMenu = new PluginContextMenu(options.pluginMenuItems, this);
this.AttachmentPreviewManager = new AttachmentPreviewManager(options.attachmentItem, this);
} }
private initRequireJs() { private initRequireJs() {

View File

@ -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<string, PluginAttachmentItemsOptions> = {};
public app: Application;
constructor(_pluginAttachmentItems: Record<string, PluginAttachmentItemsOptions>, 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;
}
}

View File

@ -43,6 +43,10 @@ export class Plugin<T = any> {
return this.app.dataSourceManager; return this.app.dataSourceManager;
} }
get AttachmentPreviewManager() {
return this.app.AttachmentPreviewManager;
}
async afterAdd() {} async afterAdd() {}
async beforeLoad() {} async beforeLoad() {}

View File

@ -12,3 +12,4 @@ export * from './NoticesManager';
export * from './hoc'; export * from './hoc';
export { ApplicationContext } from './context'; export { ApplicationContext } from './context';
export * from './PluginContextMenu'; export * from './PluginContextMenu';
export * from './AttachmentPreviewManager';

View File

@ -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 && (
<img
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
style={{ width: '100%', height: '100%' }}
alt={file.title}
className={`${prefixCls}-list-item-image`}
/>
)
);
},
checkedComponent: (props) => {
const { file } = props;
return (
file.imageUrl && (
<img
{...props}
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
alt={file.title}
style={{
width: '90%',
height: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
/>
)
);
},
};
export const imagePng = {
key: 'image/png',
type: 'image/png',
viewComponet: (props) => {
const { file, prefixCls } = props;
return (
file.imageUrl && (
<img
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
style={{ width: '100%', height: '100%' }}
alt={file.title}
className={`${prefixCls}-list-item-image`}
/>
)
);
},
checkedComponent: (props) => {
const { file } = props;
return (
file.imageUrl && (
<img
{...props}
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
alt={file.title}
style={{
width: '90%',
height: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
/>
)
);
},
};
export const fileDef = {
key: 'default',
type: 'default',
viewComponet: (props) => {
const { prefixCls } = props;
return (
<FileFilled
className={`${prefixCls}-list-item-image`}
style={{ width: '100%', height: '100%', lineHeight: '100%', color: '#000000' }}
/>
);
},
checkedComponent: (props) => {
return (
<div {...props}>
<FileFilled style={{ color: '#ffffff', fontSize: '30rem', display: 'block', lineHeight: '100vh' }} />
</div>
);
},
};
export const filePdf = {
key: 'application/pdf',
type: 'application/pdf',
viewComponet: (props) => {
const { file, prefixCls } = props;
return (
file.imageUrl && (
<img
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
style={{ width: '100%', height: '100%' }}
alt={file.title}
className={`${prefixCls}-list-item-image`}
/>
)
);
},
checkedComponent: (props) => {
const { file } = props;
return (
<div
{...props}
style={{
maxWidth: '100%',
maxHeight: '100%',
height: '100%',
width: '100%',
background: 'white',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
overflowY: 'auto',
}}
>
<iframe
src={file.url}
style={{
width: '100%',
maxHeight: '90vh',
height: '90vh',
flex: '1 1 auto',
}}
/>
</div>
);
},
};

View File

@ -0,0 +1,11 @@
import { Plugin } from '../../application/Plugin';
import { fileDef, filePdf, imagejpeg, imagePng } from './AttachmentItems';
export class AttachmentPreviewPlugin extends Plugin {
async load() {
this.app.AttachmentPreviewManager.add(imagejpeg);
this.app.AttachmentPreviewManager.add(fileDef);
this.app.AttachmentPreviewManager.add(imagePng);
this.app.AttachmentPreviewManager.add(filePdf);
}
}

View File

@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import { Navigate, useNavigate } from 'react-router-dom'; import { Navigate, useNavigate } from 'react-router-dom';
import { useAPIClient } from '../api-client'; import { useAPIClient } from '../api-client';
import { Application, NoticeLevel, useApp } from '../application'; import { Application, AttachmentPreviewManager, NoticeLevel, useApp } from '../application';
import { Plugin } from '../application/Plugin'; import { Plugin } from '../application/Plugin';
import { BlockSchemaComponentPlugin } from '../block-provider'; import { BlockSchemaComponentPlugin } from '../block-provider';
import { CollectionPlugin } from '../collection-manager'; import { CollectionPlugin } from '../collection-manager';
@ -23,6 +23,7 @@ import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user';
import { ACLPlugin } from './acl'; import { ACLPlugin } from './acl';
import { AdminLayoutPlugin } from './admin-layout'; import { AdminLayoutPlugin } from './admin-layout';
import { PluginAssistant } from './assistant'; import { PluginAssistant } from './assistant';
import { AttachmentPreviewPlugin } from './attachment-preview';
import { PluginBuiltInCollections } from './built-in-collections'; import { PluginBuiltInCollections } from './built-in-collections';
import { PluginContextMenu } from './context-menu'; import { PluginContextMenu } from './context-menu';
import { RemoteDocumentTitlePlugin } from './document-title'; import { RemoteDocumentTitlePlugin } from './document-title';
@ -357,5 +358,6 @@ export class BuiltInPlugin extends Plugin {
await this.app.pm.add(PluginContextMenu, { name: 'context-menu' }); await this.app.pm.add(PluginContextMenu, { name: 'context-menu' });
await this.app.pm.add(PluginAssistant, { name: 'drag-assistant' }); await this.app.pm.add(PluginAssistant, { name: 'drag-assistant' });
await this.app.pm.add(UserSettingsPlugin, { name: 'user-settings' }); await this.app.pm.add(UserSettingsPlugin, { name: 'user-settings' });
await this.app.pm.add(AttachmentPreviewPlugin, { name: 'attachment-preview' });
} }
} }

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Lightbox } from '@tachybase/components'; import { Lightbox } from '@tachybase/components';
import { Field, useField } from '@tachybase/schema'; import { Field, useField } from '@tachybase/schema';
import { isString } from '@tachybase/utils/client'; import { isString } from '@tachybase/utils/client';
@ -10,8 +10,10 @@ import cls from 'classnames';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaComponent } from '../..';
import { useApp } from '../../../application/hooks';
import { useRecord } from '../../../record-provider'; import { useRecord } from '../../../record-provider';
import { isImage, isPdf, toArr, toImages } from './shared'; import { isImage, isPdf, toArr, toFileList, toImages } from './shared';
import { useStyles } from './style'; import { useStyles } from './style';
import type { UploadProps } from './type'; import type { UploadProps } from './type';
@ -24,23 +26,17 @@ export const ReadPretty: Composed = () => null;
ReadPretty.File = function File(props: UploadProps) { ReadPretty.File = function File(props: UploadProps) {
const { size, showCount = 0 } = props; const { size, showCount = 0 } = props;
const { t } = useTranslation();
const record = useRecord(); const record = useRecord();
const field = useField<Field>(); const field = useField<Field>();
const value = isString(field.value) ? record : field.value; const images = isString(field.value) ? record : toFileList(field.value);
const app = useApp();
const images = toImages(showCount === 0 ? toArr(value) : toArr(value).slice(0, showCount));
const [fileIndex, setFileIndex] = useState(0); const [fileIndex, setFileIndex] = useState(0);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [fileType, setFileType] = useState<'image' | 'pdf'>();
const { wrapSSR, hashId, componentCls: prefixCls } = useStyles(); const { wrapSSR, hashId, componentCls: prefixCls } = useStyles();
const useUploadStyleVal = (useUploadStyle as any).default ? (useUploadStyle as any).default : useUploadStyle; const useUploadStyleVal = (useUploadStyle as any).default ? (useUploadStyle as any).default : useUploadStyle;
const previewList = app.AttachmentPreviewManager.get();
// 加载 antd 的样式 // 加载 antd 的样式
useUploadStyleVal(prefixCls); useUploadStyleVal(prefixCls);
function closeIFrameModal() {
setVisible(false);
}
return wrapSSR( return wrapSSR(
<div> <div>
<div <div
@ -53,95 +49,49 @@ ReadPretty.File = function File(props: UploadProps) {
)} )}
> >
<div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}> <div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}>
{images.map((file) => { {images.map((file, index) => {
const handleClick = (e) => { if (size === 'small') {
const index = images.indexOf(file); return (
if (isImage(file.extname)) { index === 0 && (
e.preventDefault(); <ReadFile
e.stopPropagation(); file={file}
setVisible(true); prefixCls={prefixCls}
setFileIndex(index); // handleClick={handleClick}
setFileType('image'); size={size}
} else if (isPdf(file.extname)) { images={images}
e.preventDefault(); setFileIndex={setFileIndex}
e.stopPropagation(); setVisible={setVisible}
setVisible(true); preview={previewList[file?.mimetype] || previewList['default']}
setFileIndex(index); key={index}
setFileType('pdf'); />
} )
// else { );
// saveAs(file.url, `${file.title}${file.extname}`); }
// }
};
return ( return (
<div <ReadFile
key={file.name} file={file}
className={cls(`${prefixCls}-list-picture-card-container`, `${prefixCls}-list-item-container`)} prefixCls={prefixCls}
> size={size}
<div images={images}
className={cls( setFileIndex={setFileIndex}
`${prefixCls}-list-item`, setVisible={setVisible}
`${prefixCls}-list-item-done`, preview={previewList['image/jpeg']}
`${prefixCls}-list-item-list-type-picture-card`, key={index}
)} />
>
<div className={`${prefixCls}-list-item-info`}>
<span className={`${prefixCls}-span`}>
<a
className={`${prefixCls}-list-item-thumbnail`}
href={file.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
>
{file.imageUrl && (
<img
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
alt={file.title}
className={`${prefixCls}-list-item-image`}
/>
)}
</a>
<a
target="_blank"
rel="noopener noreferrer"
className={`${prefixCls}-list-item-name`}
title={file.title}
href={file.url}
onClick={handleClick}
>
{file.title}
</a>
</span>
</div>
{size !== 'small' && (
<span className={`${prefixCls}-list-item-actions`}>
<Space size={3}>
<Button
size={'small'}
type={'text'}
icon={<DownloadOutlined />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
saveAs(file.url, `${file.title}${file.extname}`);
}}
/>
</Space>
</span>
)}
</div>
</div>
); );
})} })}
</div> </div>
</div> </div>
{visible && fileType === 'image' && ( {visible && (
<Lightbox <Lightbox
// discourageDownloads={true} // discourageDownloads={true}
mainSrc={images[fileIndex]?.imageUrl} mainSrc={images[fileIndex]?.imageUrl}
nextSrc={images[(fileIndex + 1) % images.length]?.imageUrl} nextSrc={images[(fileIndex + 1) % images.length]?.imageUrl}
prevSrc={images[(fileIndex + images.length - 1) % images.length]?.imageUrl} prevSrc={images[(fileIndex + images.length - 1) % images.length]?.imageUrl}
mainFile={images[fileIndex]}
nextFile={images[(fileIndex + 1) % images.length]}
prevFile={images[(fileIndex + images.length - 1) % images.length]}
previewList={previewList}
// @ts-ignore // @ts-ignore
onCloseRequest={(e) => { onCloseRequest={(e) => {
e.preventDefault(); e.preventDefault();
@ -171,59 +121,6 @@ ReadPretty.File = function File(props: UploadProps) {
]} ]}
/> />
)} )}
{visible && fileType === 'pdf' && (
<Modal
open={visible}
title={'PDF - ' + images[fileIndex].title}
onCancel={closeIFrameModal}
footer={[
<Button
key={'download'}
style={{
textTransform: 'capitalize',
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const file = images[fileIndex];
saveAs(file.url, `${file.title}${file.extname}`);
}}
>
{t('download')}
</Button>,
<Button key={'close'} onClick={closeIFrameModal} style={{ textTransform: 'capitalize' }}>
{t('close')}
</Button>,
]}
width={'85vw'}
centered={true}
>
<div
style={{
padding: '8px',
maxWidth: '100%',
maxHeight: 'calc(100vh - 256px)',
height: '90vh',
width: '100%',
background: 'white',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
overflowY: 'auto',
}}
>
<iframe
src={images[fileIndex].url}
style={{
width: '100%',
maxHeight: '90vh',
flex: '1 1 auto',
}}
></iframe>
</div>
</Modal>
)}
</div>, </div>,
); );
}; };
@ -242,3 +139,87 @@ ReadPretty.Upload = function Upload() {
</div> </div>
)); ));
}; };
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 (
<div
key={file.name}
className={cls(`${prefixCls}-list-picture-card-container`, `${prefixCls}-list-item-container`)}
style={{ position: 'relative' }}
>
<div
className={cls(
`${prefixCls}-list-item`,
`${prefixCls}-list-item-done`,
`${prefixCls}-list-item-list-type-picture-card`,
)}
>
<div className={`${prefixCls}-list-item-info`}>
<span className={`${prefixCls}-span`}>
<a
className={`${prefixCls}-list-item-thumbnail`}
href={file.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
style={{ lineHeight: '100%' }}
>
{viewComponet({ images, size, prefixCls, file, setFileIndex, setVisible })}
</a>
<a
target="_blank"
rel="noopener noreferrer"
className={`${prefixCls}-list-item-name`}
title={file.title}
href={file.url}
onClick={handleClick}
>
{file.title}
</a>
</span>
</div>
{size !== 'small' && (
<span className={`${prefixCls}-list-item-actions`}>
<Space size={3}>
<Button
size={'small'}
type={'text'}
icon={<DownloadOutlined />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
saveAs(file.url, `${file.title}${file.extname}`);
}}
/>
</Space>
</span>
)}
</div>
{images.length > 1 && size === 'small' && (
<div
style={{
position: 'absolute',
bottom: '0',
right: '0',
backgroundColor: '#9b999992',
width: '50%',
height: '30%',
lineHeight: '30%',
borderRadius: '40%',
textAlign: 'center',
}}
>
...
</div>
)}
</div>
);
};

View File

@ -8,6 +8,7 @@ import cls from 'classnames';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useApp } from '../../../application';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
@ -29,18 +30,15 @@ Upload.Attachment = connect((props: UploadProps) => {
const { disabled, multiple, value, onChange } = props; const { disabled, multiple, value, onChange } = props;
const [fileList, setFileList] = useState<any[]>([]); const [fileList, setFileList] = useState<any[]>([]);
const [sync, setSync] = useState(true); const [sync, setSync] = useState(true);
const app = useApp();
const previewList = app.AttachmentPreviewManager.get();
const images = fileList; const images = fileList;
const [fileIndex, setFileIndex] = useState(0); const [fileIndex, setFileIndex] = useState(0);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [fileType, setFileType] = useState<'image' | 'pdf'>();
const { t } = useTranslation(); const { t } = useTranslation();
const uploadProps = useUploadProps({ ...props }); const uploadProps = useUploadProps({ ...props });
const { wrapSSR, hashId, componentCls: prefixCls } = useStyles(); const { wrapSSR, hashId, componentCls: prefixCls } = useStyles();
const internalFileList = useRef([]); const internalFileList = useRef([]);
function closeIFrameModal() {
setVisible(false);
}
useEffect(() => { useEffect(() => {
if (sync) { if (sync) {
const fileList = toFileList(value); const fileList = toFileList(value);
@ -53,104 +51,23 @@ Upload.Attachment = connect((props: UploadProps) => {
<div> <div>
<div className={cls(`${prefixCls}-wrapper`, `${prefixCls}-picture-card-wrapper`, 'nb-upload', hashId)}> <div className={cls(`${prefixCls}-wrapper`, `${prefixCls}-picture-card-wrapper`, 'nb-upload', hashId)}>
<div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}> <div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}>
{fileList.map((file) => { {fileList.map((file, index) => {
const handleClick = (e) => { const fileProps = {
e.preventDefault(); file,
e.stopPropagation(); prefixCls,
const index = fileList.indexOf(file); fileList,
if (isImage(file.extname)) { setFileIndex,
setFileType('image'); setVisible,
setVisible(true); disabled,
setFileIndex(index); t,
} else if (isPdf(file.extname)) { setSync,
setVisible(true); setFileList,
setFileIndex(index); multiple,
setFileType('pdf'); onChange,
} else { internalFileList,
saveAs(file.url, `${file.title}${file.extname}`); preview: previewList[file?.mimetype] || previewList['default'],
}
}; };
return ( return <UploadReadFile {...fileProps} key={index} />;
<div
key={file.uid || file.id}
className={`${prefixCls}-list-picture-card-container ${prefixCls}-list-item-container`}
>
<div
className={cls(
`${prefixCls}-list-item`,
`${prefixCls}-list-item-done`,
`${prefixCls}-list-item-list-type-picture-card`,
)}
>
<div className={`${prefixCls}-list-item-info`}>
<span className={`${prefixCls}-span`}>
<a
className={`${prefixCls}-list-item-thumbnail`}
href={file.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
>
{file.imageUrl && (
<img src={file.imageUrl} alt={file.title} className={`${prefixCls}-list-item-image`} />
)}
</a>
<a
target="_blank"
rel="noopener noreferrer"
className={`${prefixCls}-list-item-name`}
title={file.title}
href={file.url}
onClick={handleClick}
>
{file.status === 'uploading' ? t('Uploading') : file.title}
</a>
</span>
</div>
<span className={`${prefixCls}-list-item-actions`}>
<Space size={3}>
<Button
size={'small'}
type={'text'}
icon={<DownloadOutlined />}
onClick={() => {
saveAs(file.url, `${file.title}${file.extname}`);
}}
/>
{!disabled && (
<Button
size={'small'}
type={'text'}
icon={<DeleteOutlined />}
onClick={() => {
setSync(false);
setFileList((prevFileList) => {
if (!multiple) {
onChange?.(null as any);
setSync(true);
return [];
}
const index = prevFileList.indexOf(file);
prevFileList.splice(index, 1);
internalFileList.current = internalFileList.current.filter(
(item) => item.uid !== file.uid,
);
onChange?.(toValue([...prevFileList]));
return [...prevFileList];
});
}}
/>
)}
</Space>
</span>
{file.status === 'uploading' && (
<div className={`${prefixCls}-list-item-progress`}>
<Progress strokeWidth={2} type={'line'} showInfo={false} percent={file.percent} />
</div>
)}
</div>
</div>
);
})} })}
{!disabled && (multiple || toArr(value).length < 1) && ( {!disabled && (multiple || toArr(value).length < 1) && (
<div className={cls(`${prefixCls}-list-picture-card-container`, `${prefixCls}-list-item-container`)}> <div className={cls(`${prefixCls}-list-picture-card-container`, `${prefixCls}-list-item-container`)}>
@ -198,12 +115,16 @@ Upload.Attachment = connect((props: UploadProps) => {
</div> </div>
</div> </div>
{/* 预览图片的弹框 */} {/* 预览图片的弹框 */}
{visible && fileType === 'image' && ( {visible && (
<Lightbox <Lightbox
// discourageDownloads={true} // discourageDownloads={true}
mainSrc={images[fileIndex]?.imageUrl} mainSrc={images[fileIndex]?.imageUrl}
nextSrc={images[(fileIndex + 1) % images.length]?.imageUrl} nextSrc={images[(fileIndex + 1) % images.length]?.imageUrl}
prevSrc={images[(fileIndex + images.length - 1) % images.length]?.imageUrl} prevSrc={images[(fileIndex + images.length - 1) % images.length]?.imageUrl}
mainFile={images[fileIndex]}
nextFile={images[(fileIndex + 1) % images.length]}
prevFile={images[(fileIndex + images.length - 1) % images.length]}
previewList={previewList}
onCloseRequest={() => setVisible(false)} onCloseRequest={() => setVisible(false)}
onMovePrevRequest={() => setFileIndex((fileIndex + images.length - 1) % images.length)} onMovePrevRequest={() => setFileIndex((fileIndex + images.length - 1) % images.length)}
onMoveNextRequest={() => setFileIndex((fileIndex + 1) % images.length)} onMoveNextRequest={() => setFileIndex((fileIndex + 1) % images.length)}
@ -227,59 +148,6 @@ Upload.Attachment = connect((props: UploadProps) => {
]} ]}
/> />
)} )}
{visible && fileType === 'pdf' && (
<Modal
open={visible}
title={'PDF - ' + images[fileIndex].title}
onCancel={closeIFrameModal}
footer={[
<Button
key="download"
style={{
textTransform: 'capitalize',
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const file = images[fileIndex];
saveAs(file.url, `${file.title}${file.extname}`);
}}
>
{t('download')}
</Button>,
<Button key="close" onClick={closeIFrameModal} style={{ textTransform: 'capitalize' }}>
{t('close')}
</Button>,
]}
width={'85vw'}
centered={true}
>
<div
style={{
padding: '8px',
maxWidth: '100%',
maxHeight: 'calc(100vh - 256px)',
height: '90vh',
width: '100%',
background: 'white',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
overflowY: 'auto',
}}
>
<iframe
src={images[fileIndex].url}
style={{
width: '100%',
maxHeight: '90vh',
flex: '1 1 auto',
}}
></iframe>
</div>
</Modal>
)}
</div>, </div>,
); );
}, mapReadPretty(ReadPretty.File)); }, mapReadPretty(ReadPretty.File));
@ -355,3 +223,107 @@ function updateFileList(file: UploadFile, fileList: (UploadFile | Readonly<Uploa
} }
return nextFileList; return nextFileList;
} }
export const UploadReadFile = (props) => {
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 (
<div
key={file.uid || file.id}
className={`${prefixCls}-list-picture-card-container ${prefixCls}-list-item-container`}
>
<div
className={cls(
`${prefixCls}-list-item`,
`${prefixCls}-list-item-done`,
`${prefixCls}-list-item-list-type-picture-card`,
)}
>
<div className={`${prefixCls}-list-item-info`}>
<span className={`${prefixCls}-span`}>
<a
className={`${prefixCls}-list-item-thumbnail`}
href={file.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
style={{ lineHeight: '100%' }}
>
{viewComponet({ prefixCls, file, setFileIndex, setVisible })}
</a>
<a
target="_blank"
rel="noopener noreferrer"
className={`${prefixCls}-list-item-name`}
title={file.title}
href={file.url}
onClick={handleClick}
>
{file.status === 'uploading' ? t('Uploading') : file.title}
</a>
</span>
</div>
<span className={`${prefixCls}-list-item-actions`}>
<Space size={3}>
<Button
size={'small'}
type={'text'}
icon={<DownloadOutlined />}
onClick={() => {
saveAs(file.url, `${file.title}${file.extname}`);
}}
/>
{!disabled && (
<Button
size={'small'}
type={'text'}
icon={<DeleteOutlined />}
onClick={() => {
setSync(false);
setFileList((prevFileList) => {
if (!multiple) {
onChange?.(null as any);
setSync(true);
return [];
}
const index = prevFileList.indexOf(file);
prevFileList.splice(index, 1);
internalFileList.current = internalFileList.current.filter((item) => item.uid !== file.uid);
onChange?.(toValue([...prevFileList]));
return [...prevFileList];
});
}}
/>
)}
</Space>
</span>
{file.status === 'uploading' && (
<div className={`${prefixCls}-list-item-progress`}>
<Progress strokeWidth={2} type={'line'} showInfo={false} percent={file.percent} />
</div>
)}
</div>
</div>
);
};

View File

@ -25,6 +25,8 @@ import { getHighestSafeWindowContext, getWindowHeight, getWindowWidth, translate
import './style.css'; import './style.css';
import { css } from '@tachybase/client';
class ReactImageLightbox extends Component { class ReactImageLightbox extends Component {
static isTargetMatchImage(target) { static isTargetMatchImage(target) {
return target && /ril-image-current/.test(target.className); 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 // Get info for the best suited image to display with the given srcType
getBestImageForType(srcType) { getBestImageForType(srcType, fileSrc?) {
let imageSrc = this.props[srcType]; let imageSrc = fileSrc || this.props[srcType];
let fitSizes = {}; let fitSizes = {};
if (this.isImageLoaded(imageSrc)) { if (this.isImageLoaded(imageSrc)) {
// Use full-size image if available // Use full-size image if available
fitSizes = this.getFitSizes(this.imageCache[imageSrc].width, this.imageCache[imageSrc].height); fitSizes = this.getFitSizes(this.imageCache[imageSrc].width, this.imageCache[imageSrc].height);
@ -394,6 +395,30 @@ class ReactImageLightbox extends Component {
name: 'prevSrcThumbnail', name: 'prevSrcThumbnail',
keyEnding: `t${this.keyCounter - 1}`, 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, imageTitle,
nextSrc, nextSrc,
prevSrc, prevSrc,
mainFile,
nextFile,
prevFile,
previewList,
toolbarButtons, toolbarButtons,
reactModalStyle, reactModalStyle,
onAfterOpen, onAfterOpen,
@ -1203,17 +1232,21 @@ class ReactImageLightbox extends Component {
// Images to be displayed // Images to be displayed
const images = []; 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 = { const imageStyle = {
...transitionStyle, ...transitionStyle,
...ReactImageLightbox.getTransform({ ...ReactImageLightbox.getTransform({
...transforms, ...transforms,
height: boxSize.height - 20,
width: boxSize.width - 20,
targetWidth: boxSize.width - 50,
targeHeight: boxSize.height - 50,
...bestImageInfo, ...bestImageInfo,
}), }),
}; };
@ -1221,99 +1254,43 @@ class ReactImageLightbox extends Component {
if (zoomLevel > MIN_ZOOM_LEVEL) { if (zoomLevel > MIN_ZOOM_LEVEL) {
imageStyle.cursor = 'move'; imageStyle.cursor = 'move';
} }
images.push(
// support IE 9 and 11 <div
const hasTrueValue = (object) => Object.keys(object).some((key) => object[key]); className={`${imageClass} ril__image`}
style={{
// when error on one of the loads then push custom error stuff ...imageStyle,
if (bestImageInfo === null && hasTrueValue(loadErrorStatus)) { width: bestImageInfo?.width || '90%',
images.push( height: '90%',
<div display: 'flex',
className={`${imageClass} ril__image ril-errored`} justifyContent: 'center',
style={imageStyle} alignItems: 'center',
key={this.props[srcType] + keyEndings[srcType]} }}
> >
<div className="ril__errorContainer">{this.props.imageLoadErrorMessage}</div> {checkedComponent({
</div>, key: `${fieldType + keyEndings[srcType]}`,
); onDoubleClick: this.handleImageDoubleClick,
onWheel: this.handleImageMouseWheel,
return; onDragStart: (e) => e.preventDefault(),
} file: this.props[srcType],
if (bestImageInfo === null) { bestImageInfo,
const loadingIcon = })}
loader !== undefined ? ( </div>,
loader );
) : (
<div className="ril-loading-circle ril__loadingCircle ril__loadingContainer__icon">
{[...new Array(12)].map((_, index) => (
<div
// eslint-disable-next-line react/no-array-index-key
key={index}
className="ril-loading-circle-point ril__loadingCirclePoint"
/>
))}
</div>
);
// Fall back to loading icon if the thumbnail has not been loaded
images.push(
<div
className={`${imageClass} ril__image ril-not-loaded`}
style={imageStyle}
key={this.props[srcType] + keyEndings[srcType]}
>
<div className="ril__loadingContainer">{loadingIcon}</div>
</div>,
);
return;
}
const imageSrc = bestImageInfo.src;
if (discourageDownloads) {
imageStyle.backgroundImage = `url('${imageSrc}')`;
images.push(
<div
className={`${imageClass} ril__image ril__imageDiscourager`}
onDoubleClick={this.handleImageDoubleClick}
onWheel={this.handleImageMouseWheel}
style={imageStyle}
key={imageSrc + keyEndings[srcType]}
>
<div className="ril-download-blocker ril__downloadBlocker" />
</div>,
);
} else {
images.push(
<img
{...(imageCrossOrigin ? { crossOrigin: imageCrossOrigin } : {})}
className={`${imageClass} ril__image`}
onDoubleClick={this.handleImageDoubleClick}
onWheel={this.handleImageMouseWheel}
onDragStart={(e) => e.preventDefault()}
style={imageStyle}
src={imageSrc}
key={imageSrc + keyEndings[srcType]}
alt={typeof imageTitle === 'string' ? imageTitle : translate('Image')}
draggable={false}
/>,
);
}
}; };
const zoomMultiplier = this.getZoomMultiplier(); const zoomMultiplier = this.getZoomMultiplier();
// Next Image (displayed on the right) // Next Image (displayed on the right)
addImage('nextSrc', 'ril-image-next ril__imageNext', { addImage('nextFile', 'ril-image-next ril__imageNext', {
x: boxSize.width, x: boxSize.width,
}); });
// Main Image // Main Image
addImage('mainSrc', 'ril-image-current', { addImage('mainFile', 'ril-image-current', {
x: -1 * offsetX, x: -1 * offsetX,
y: -1 * offsetY, y: -1 * offsetY,
zoom: zoomMultiplier, zoom: zoomMultiplier,
}); });
// Previous Image (displayed on the left) // Previous Image (displayed on the left)
addImage('prevSrc', 'ril-image-prev ril__imagePrev', { addImage('prevFile', 'ril-image-prev ril__imagePrev', {
x: -1 * boxSize.width, x: -1 * boxSize.width,
}); });
@ -1336,7 +1313,6 @@ class ReactImageLightbox extends Component {
...reactModalStyle.content, // Allow style overrides via props ...reactModalStyle.content, // Allow style overrides via props
}, },
}; };
return ( return (
<Modal <Modal
isOpen isOpen
@ -1497,33 +1473,37 @@ class ReactImageLightbox extends Component {
} }
ReactImageLightbox.propTypes = { ReactImageLightbox.propTypes = {
previewList: PropTypes.object,
//----------------------------- //-----------------------------
// Image sources // Image sources
//----------------------------- //-----------------------------
// Main display image url // Main display image url
mainSrc: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types mainSrc: PropTypes.string.isRequired,
mainFile: PropTypes.object,
// Previous display image url (displayed to the left) // Previous display image url (displayed to the left)
// If left undefined, movePrev actions will not be performed, and the button not displayed // If left undefined, movePrev actions will not be performed, and the button not displayed
prevSrc: PropTypes.string, prevSrc: PropTypes.string,
prevFile: PropTypes.object,
// Next display image url (displayed to the right) // Next display image url (displayed to the right)
// If left undefined, moveNext actions will not be performed, and the button not displayed // If left undefined, moveNext actions will not be performed, and the button not displayed
nextSrc: PropTypes.string, nextSrc: PropTypes.string,
nextFile: PropTypes.object,
//----------------------------- //-----------------------------
// Image thumbnail sources // Image thumbnail sources
//----------------------------- //-----------------------------
// Thumbnail image url corresponding to props.mainSrc // Thumbnail image url corresponding to props.mainSrc
mainSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types mainSrcThumbnail: PropTypes.string,
// Thumbnail image url corresponding to props.prevSrc // Thumbnail image url corresponding to props.prevSrc
prevSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types prevSrcThumbnail: PropTypes.string,
// Thumbnail image url corresponding to props.nextSrc // Thumbnail image url corresponding to props.nextSrc
nextSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types nextSrcThumbnail: PropTypes.string,
//----------------------------- //-----------------------------
// Event Handlers // Event Handlers