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,30 +49,111 @@ 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) => {
if (size === 'small') {
return (
index === 0 && (
<ReadFile
file={file}
prefixCls={prefixCls}
// handleClick={handleClick}
size={size}
images={images}
setFileIndex={setFileIndex}
setVisible={setVisible}
preview={previewList[file?.mimetype] || previewList['default']}
key={index}
/>
)
);
}
return (
<ReadFile
file={file}
prefixCls={prefixCls}
size={size}
images={images}
setFileIndex={setFileIndex}
setVisible={setVisible}
preview={previewList['image/jpeg']}
key={index}
/>
);
})}
</div>
</div>
{visible && (
<Lightbox
// discourageDownloads={true}
mainSrc={images[fileIndex]?.imageUrl}
nextSrc={images[(fileIndex + 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
onCloseRequest={(e) => {
e.preventDefault();
e.stopPropagation();
setVisible(false);
}}
onMovePrevRequest={() => setFileIndex((fileIndex + images.length - 1) % images.length)}
onMoveNextRequest={() => setFileIndex((fileIndex + 1) % images.length)}
imageTitle={images[fileIndex]?.title}
toolbarButtons={[
<button
key={'download'}
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
type="button"
aria-label="Zoom in"
title="Zoom in"
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const file = images[fileIndex];
saveAs(file.url, `${file.title}${file.extname}`);
}}
>
<DownloadOutlined />
</button>,
]}
/>
)}
</div>,
);
};
ReadPretty.Upload = function Upload() {
const field = useField<Field>();
return (field.value || []).map((item) => (
<div key={item.name}>
{item.url ? (
<a target={'_blank'} href={item.url} rel="noreferrer">
{item.name}
</a>
) : (
<span>{item.name}</span>
)}
</div>
));
};
export const ReadFile = ({ file, prefixCls, size, images, setFileIndex, setVisible, preview }) => {
const { viewComponet } = preview;
const handleClick = (e) => { const handleClick = (e) => {
const index = images.indexOf(file); const index = images.indexOf(file);
if (isImage(file.extname)) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setVisible(true); setVisible(true);
setFileIndex(index); setFileIndex(index);
setFileType('image');
} else if (isPdf(file.extname)) {
e.preventDefault();
e.stopPropagation();
setVisible(true);
setFileIndex(index);
setFileType('pdf');
}
// else {
// saveAs(file.url, `${file.title}${file.extname}`);
// }
}; };
return ( return (
<div <div
key={file.name} key={file.name}
className={cls(`${prefixCls}-list-picture-card-container`, `${prefixCls}-list-item-container`)} className={cls(`${prefixCls}-list-picture-card-container`, `${prefixCls}-list-item-container`)}
style={{ position: 'relative' }}
> >
<div <div
className={cls( className={cls(
@ -93,14 +170,9 @@ ReadPretty.File = function File(props: UploadProps) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={handleClick} onClick={handleClick}
style={{ lineHeight: '100%' }}
> >
{file.imageUrl && ( {viewComponet({ images, size, prefixCls, file, setFileIndex, setVisible })}
<img
src={`${file.imageUrl}${file.thumbnailRule || ''}`}
alt={file.title}
className={`${prefixCls}-list-item-image`}
/>
)}
</a> </a>
<a <a
target="_blank" target="_blank"
@ -131,114 +203,23 @@ ReadPretty.File = function File(props: UploadProps) {
</span> </span>
)} )}
</div> </div>
</div> {images.length > 1 && size === 'small' && (
);
})}
</div>
</div>
{visible && fileType === 'image' && (
<Lightbox
// discourageDownloads={true}
mainSrc={images[fileIndex]?.imageUrl}
nextSrc={images[(fileIndex + 1) % images.length]?.imageUrl}
prevSrc={images[(fileIndex + images.length - 1) % images.length]?.imageUrl}
// @ts-ignore
onCloseRequest={(e) => {
e.preventDefault();
e.stopPropagation();
setVisible(false);
}}
onMovePrevRequest={() => setFileIndex((fileIndex + images.length - 1) % images.length)}
onMoveNextRequest={() => setFileIndex((fileIndex + 1) % images.length)}
imageTitle={images[fileIndex]?.title}
toolbarButtons={[
<button
key={'download'}
style={{ fontSize: 22, background: 'none', lineHeight: 1 }}
type="button"
aria-label="Zoom in"
title="Zoom in"
className="ril-zoom-in ril__toolbarItemChild ril__builtinButton"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const file = images[fileIndex];
saveAs(file.url, `${file.title}${file.extname}`);
}}
>
<DownloadOutlined />
</button>,
]}
/>
)}
{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 <div
style={{ style={{
padding: '8px', position: 'absolute',
maxWidth: '100%', bottom: '0',
maxHeight: 'calc(100vh - 256px)', right: '0',
height: '90vh', backgroundColor: '#9b999992',
width: '100%', width: '50%',
background: 'white', height: '30%',
display: 'flex', lineHeight: '30%',
flexDirection: 'column', borderRadius: '40%',
alignItems: 'center', textAlign: 'center',
overflowY: 'auto',
}} }}
> >
<iframe ...
src={images[fileIndex].url}
style={{
width: '100%',
maxHeight: '90vh',
flex: '1 1 auto',
}}
></iframe>
</div> </div>
</Modal>
)} )}
</div>, </div>
); );
}; };
ReadPretty.Upload = function Upload() {
const field = useField<Field>();
return (field.value || []).map((item) => (
<div key={item.name}>
{item.url ? (
<a target={'_blank'} href={item.url} rel="noreferrer">
{item.name}
</a>
) : (
<span>{item.name}</span>
)}
</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';
} }
// 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( images.push(
<div <div
className={`${imageClass} ril__image ril-errored`}
style={imageStyle}
key={this.props[srcType] + keyEndings[srcType]}
>
<div className="ril__errorContainer">{this.props.imageLoadErrorMessage}</div>
</div>,
);
return;
}
if (bestImageInfo === null) {
const loadingIcon =
loader !== undefined ? (
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`} className={`${imageClass} ril__image`}
onDoubleClick={this.handleImageDoubleClick} style={{
onWheel={this.handleImageMouseWheel} ...imageStyle,
onDragStart={(e) => e.preventDefault()} width: bestImageInfo?.width || '90%',
style={imageStyle} height: '90%',
src={imageSrc} display: 'flex',
key={imageSrc + keyEndings[srcType]} justifyContent: 'center',
alt={typeof imageTitle === 'string' ? imageTitle : translate('Image')} alignItems: 'center',
draggable={false} }}
/>, >
{checkedComponent({
key: `${fieldType + keyEndings[srcType]}`,
onDoubleClick: this.handleImageDoubleClick,
onWheel: this.handleImageMouseWheel,
onDragStart: (e) => e.preventDefault(),
file: this.props[srcType],
bestImageInfo,
})}
</div>,
); );
}
}; };
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