### What problem does this PR solve? fix: fix uploaded file time error #680 feat: support preview of word and excel #684 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.6.0
| import { api_host } from '@/utils/api'; | |||||
| import React from 'react'; | import React from 'react'; | ||||
| interface IProps extends React.PropsWithChildren { | interface IProps extends React.PropsWithChildren { | ||||
| documentId: string; | |||||
| link: string; | |||||
| preventDefault?: boolean; | preventDefault?: boolean; | ||||
| color?: string; | |||||
| } | } | ||||
| const NewDocumentLink = ({ | const NewDocumentLink = ({ | ||||
| children, | children, | ||||
| documentId, | |||||
| link, | |||||
| preventDefault = false, | preventDefault = false, | ||||
| color = 'rgb(15, 79, 170)', | |||||
| }: IProps) => { | }: IProps) => { | ||||
| return ( | return ( | ||||
| <a | <a | ||||
| target="_blank" | target="_blank" | ||||
| onClick={!preventDefault ? undefined : (e) => e.preventDefault()} | onClick={!preventDefault ? undefined : (e) => e.preventDefault()} | ||||
| href={`${api_host}/document/get/${documentId}`} | |||||
| href={link} | |||||
| rel="noreferrer" | rel="noreferrer" | ||||
| style={{ color }} | |||||
| > | > | ||||
| {children} | {children} | ||||
| </a> | </a> |
| xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | ||||
| mp4: 'video/mp4', | mp4: 'video/mp4', | ||||
| }; | }; | ||||
| //#region file preview | |||||
| export const Images = [ | |||||
| 'jpg', | |||||
| 'jpeg', | |||||
| 'png', | |||||
| 'gif', | |||||
| 'bmp', | |||||
| 'tif', | |||||
| 'tiff', | |||||
| 'webp', | |||||
| // 'svg', | |||||
| 'ico', | |||||
| ]; | |||||
| // Without FileViewer | |||||
| export const ExceptiveType = ['xlsx', 'xls', 'pdf', ...Images]; | |||||
| export const SupportedPreviewDocumentTypes = ['docx', 'csv', ...ExceptiveType]; | |||||
| //#endregion |
| import { useGetKnowledgeSearchParams } from './routeHook'; | import { useGetKnowledgeSearchParams } from './routeHook'; | ||||
| import { useOneNamespaceEffectsLoading } from './storeHooks'; | import { useOneNamespaceEffectsLoading } from './storeHooks'; | ||||
| export const useGetDocumentUrl = (documentId: string) => { | |||||
| const url = useMemo(() => { | |||||
| return `${api_host}/document/get/${documentId}`; | |||||
| }, [documentId]); | |||||
| export const useGetDocumentUrl = (documentId?: string) => { | |||||
| const getDocumentUrl = useCallback( | |||||
| (id?: string) => { | |||||
| return `${api_host}/document/get/${documentId || id}`; | |||||
| }, | |||||
| [documentId], | |||||
| ); | |||||
| return url; | |||||
| return getDocumentUrl; | |||||
| }; | }; | ||||
| export const useGetChunkHighlights = (selectedChunk: IChunk) => { | export const useGetChunkHighlights = (selectedChunk: IChunk) => { |
| 'Support for a single or bulk upload. Strictly prohibited from uploading company data or other banned files.', | 'Support for a single or bulk upload. Strictly prohibited from uploading company data or other banned files.', | ||||
| local: 'Local uploads', | local: 'Local uploads', | ||||
| s3: 'S3 uploads', | s3: 'S3 uploads', | ||||
| preview: 'Preview', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: 'All rights reserved @ React', | profile: 'All rights reserved @ React', |
| directory: '文件夾', | directory: '文件夾', | ||||
| local: '本地上傳', | local: '本地上傳', | ||||
| s3: 'S3 上傳', | s3: 'S3 上傳', | ||||
| preview: '預覽', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: '“保留所有權利 @ react”', | profile: '“保留所有權利 @ react”', |
| directory: '文件夹', | directory: '文件夹', | ||||
| local: '本地上传', | local: '本地上传', | ||||
| s3: 'S3 上传', | s3: 'S3 上传', | ||||
| preview: '预览', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: 'All rights reserved @ React', | profile: 'All rights reserved @ React', |
| }, | }, | ||||
| { | { | ||||
| title: t('uploadDate'), | title: t('uploadDate'), | ||||
| dataIndex: 'create_date', | |||||
| key: 'create_date', | |||||
| dataIndex: 'create_time', | |||||
| key: 'create_time', | |||||
| render(value) { | render(value) { | ||||
| return formatDate(value); | return formatDate(value); | ||||
| }, | }, |
| import { ReactComponent as NavigationPointerIcon } from '@/assets/svg/navigation-pointer.svg'; | import { ReactComponent as NavigationPointerIcon } from '@/assets/svg/navigation-pointer.svg'; | ||||
| import NewDocumentLink from '@/components/new-document-link'; | import NewDocumentLink from '@/components/new-document-link'; | ||||
| import { useGetDocumentUrl } from '@/hooks/documentHooks'; | |||||
| import { ITestingDocument } from '@/interfaces/database/knowledge'; | import { ITestingDocument } from '@/interfaces/database/knowledge'; | ||||
| import { isPdf } from '@/utils/documentUtils'; | import { isPdf } from '@/utils/documentUtils'; | ||||
| import { Table, TableProps } from 'antd'; | import { Table, TableProps } from 'antd'; | ||||
| ); | ); | ||||
| const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
| const getDocumentUrl = useGetDocumentUrl(); | |||||
| const columns: TableProps<ITestingDocument>['columns'] = [ | const columns: TableProps<ITestingDocument>['columns'] = [ | ||||
| { | { | ||||
| key: 'view', | key: 'view', | ||||
| width: 50, | width: 50, | ||||
| render: (_, { doc_id, doc_name }) => ( | render: (_, { doc_id, doc_name }) => ( | ||||
| <NewDocumentLink documentId={doc_id} preventDefault={!isPdf(doc_name)}> | |||||
| <NewDocumentLink | |||||
| link={getDocumentUrl(doc_id)} | |||||
| preventDefault={!isPdf(doc_name)} | |||||
| > | |||||
| <NavigationPointerIcon /> | <NavigationPointerIcon /> | ||||
| </NewDocumentLink> | </NewDocumentLink> | ||||
| ), | ), |
| import SvgIcon from '@/components/svg-icon'; | import SvgIcon from '@/components/svg-icon'; | ||||
| import { useTranslate } from '@/hooks/commonHooks'; | import { useTranslate } from '@/hooks/commonHooks'; | ||||
| import { useGetDocumentUrl } from '@/hooks/documentHooks'; | |||||
| import { getExtension, isPdf } from '@/utils/documentUtils'; | import { getExtension, isPdf } from '@/utils/documentUtils'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| }) => { | }) => { | ||||
| const userInfo = useSelectUserInfo(); | const userInfo = useSelectUserInfo(); | ||||
| const fileThumbnails = useSelectFileThumbnails(); | const fileThumbnails = useSelectFileThumbnails(); | ||||
| const getDocumentUrl = useGetDocumentUrl(); | |||||
| const isAssistant = item.role === MessageType.Assistant; | const isAssistant = item.role === MessageType.Assistant; | ||||
| )} | )} | ||||
| <NewDocumentLink | <NewDocumentLink | ||||
| documentId={item.doc_id} | |||||
| link={getDocumentUrl(item.doc_id)} | |||||
| preventDefault={!isPdf(item.doc_name)} | preventDefault={!isPdf(item.doc_name)} | ||||
| > | > | ||||
| {item.doc_name} | {item.doc_name} |
| .viewerWrapper { | .viewerWrapper { | ||||
| width: 100%; | width: 100%; | ||||
| height: 100%; | |||||
| :global { | :global { | ||||
| .pdf-canvas { | .pdf-canvas { | ||||
| text-align: center; | text-align: center; | ||||
| } | } | ||||
| } | } | ||||
| .image { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| } | |||||
| } | } |
| import { ExceptiveType, Images } from '@/constants/common'; | |||||
| import { api_host } from '@/utils/api'; | import { api_host } from '@/utils/api'; | ||||
| import { Flex, Image } from 'antd'; | |||||
| import FileViewer from 'react-file-viewer'; | import FileViewer from 'react-file-viewer'; | ||||
| import { useParams, useSearchParams } from 'umi'; | import { useParams, useSearchParams } from 'umi'; | ||||
| import Excel from './excel'; | import Excel from './excel'; | ||||
| import Pdf from './pdf'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // TODO: The interface returns an incorrect content-type for the SVG. | |||||
| const isNotExceptiveType = (ext: string) => ExceptiveType.indexOf(ext) === -1; | |||||
| const DocumentViewer = () => { | const DocumentViewer = () => { | ||||
| const { id: documentId } = useParams(); | const { id: documentId } = useParams(); | ||||
| const api = `${api_host}/file/get/${documentId}`; | const api = `${api_host}/file/get/${documentId}`; | ||||
| return ( | return ( | ||||
| <section className={styles.viewerWrapper}> | <section className={styles.viewerWrapper}> | ||||
| {ext === 'xlsx' && <Excel filePath={api}></Excel>} | |||||
| {ext !== 'xlsx' && ( | |||||
| {Images.includes(ext!) && ( | |||||
| <Flex className={styles.image} align="center" justify="center"> | |||||
| <Image src={api} preview={false}></Image> | |||||
| </Flex> | |||||
| )} | |||||
| {ext === 'pdf' && <Pdf url={api}></Pdf>} | |||||
| {(ext === 'xlsx' || ext === 'xls') && <Excel filePath={api}></Excel>} | |||||
| {isNotExceptiveType(ext!) && ( | |||||
| <FileViewer fileType={ext} filePath={api} onError={onError} /> | <FileViewer fileType={ext} filePath={api} onError={onError} /> | ||||
| )} | )} | ||||
| </section> | </section> |
| import { Skeleton } from 'antd'; | |||||
| import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter'; | |||||
| interface IProps { | |||||
| url: string; | |||||
| } | |||||
| const DocumentPreviewer = ({ url }: IProps) => { | |||||
| const resetHash = () => {}; | |||||
| return ( | |||||
| <div style={{ width: '100%' }}> | |||||
| <PdfLoader | |||||
| url={url} | |||||
| beforeLoad={<Skeleton active />} | |||||
| workerSrc="/pdfjs-dist/pdf.worker.min.js" | |||||
| > | |||||
| {(pdfDocument) => { | |||||
| return ( | |||||
| <PdfHighlighter | |||||
| pdfDocument={pdfDocument} | |||||
| enableAreaSelection={(event) => event.altKey} | |||||
| onScrollChange={resetHash} | |||||
| scrollRef={() => {}} | |||||
| onSelectionFinished={() => null} | |||||
| highlightTransform={() => { | |||||
| return <div></div>; | |||||
| }} | |||||
| highlights={[]} | |||||
| /> | |||||
| ); | |||||
| }} | |||||
| </PdfLoader> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default DocumentPreviewer; |
| DeleteOutlined, | DeleteOutlined, | ||||
| DownloadOutlined, | DownloadOutlined, | ||||
| EditOutlined, | EditOutlined, | ||||
| EyeOutlined, | |||||
| LinkOutlined, | LinkOutlined, | ||||
| } from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||
| import { Button, Space, Tooltip } from 'antd'; | import { Button, Space, Tooltip } from 'antd'; | ||||
| import { useHandleDeleteFile, useNavigateToDocument } from '../hooks'; | |||||
| import { useHandleDeleteFile } from '../hooks'; | |||||
| import NewDocumentLink from '@/components/new-document-link'; | |||||
| import { SupportedPreviewDocumentTypes } from '@/constants/common'; | |||||
| import { getExtension } from '@/utils/documentUtils'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const isSupportedPreviewDocumentType = (fileExtension: string) => { | |||||
| return SupportedPreviewDocumentTypes.includes(fileExtension); | |||||
| }; | |||||
| interface IProps { | interface IProps { | ||||
| record: IFile; | record: IFile; | ||||
| setCurrentRecord: (record: any) => void; | setCurrentRecord: (record: any) => void; | ||||
| [documentId], | [documentId], | ||||
| setSelectedRowKeys, | setSelectedRowKeys, | ||||
| ); | ); | ||||
| const navigateToDocument = useNavigateToDocument(record.id, record.name); | |||||
| const extension = getExtension(record.name); | |||||
| const onDownloadDocument = () => { | const onDownloadDocument = () => { | ||||
| downloadFile({ | downloadFile({ | ||||
| return ( | return ( | ||||
| <Space size={0}> | <Space size={0}> | ||||
| {/* <Tooltip title={t('addToKnowledge')}> | |||||
| <Button | |||||
| type="text" | |||||
| className={styles.iconButton} | |||||
| onClick={navigateToDocument} | |||||
| > | |||||
| <EyeOutlined size={20} /> | |||||
| </Button> | |||||
| </Tooltip> */} | |||||
| <Tooltip title={t('addToKnowledge')}> | <Tooltip title={t('addToKnowledge')}> | ||||
| <Button | <Button | ||||
| type="text" | type="text" | ||||
| </Button> | </Button> | ||||
| </Tooltip> | </Tooltip> | ||||
| )} | )} | ||||
| {isSupportedPreviewDocumentType(extension) && ( | |||||
| <NewDocumentLink | |||||
| color="black" | |||||
| link={`/document/${documentId}?ext=${extension}`} | |||||
| > | |||||
| <Tooltip title={t('preview')}> | |||||
| <Button type="text" className={styles.iconButton}> | |||||
| <EyeOutlined size={20} /> | |||||
| </Button> | |||||
| </Tooltip> | |||||
| </NewDocumentLink> | |||||
| )} | |||||
| </Space> | </Space> | ||||
| ); | ); | ||||
| }; | }; |
| import { useGetPagination, useSetPagination } from '@/hooks/logicHooks'; | import { useGetPagination, useSetPagination } from '@/hooks/logicHooks'; | ||||
| import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | ||||
| import { IFile } from '@/interfaces/database/file-manager'; | import { IFile } from '@/interfaces/database/file-manager'; | ||||
| import { getExtension } from '@/utils/documentUtils'; | |||||
| import { PaginationProps } from 'antd'; | import { PaginationProps } from 'antd'; | ||||
| import { UploadFile } from 'antd/lib'; | import { UploadFile } from 'antd/lib'; | ||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | import { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| return { handleBreadcrumbClick }; | return { handleBreadcrumbClick }; | ||||
| }; | }; | ||||
| export const useNavigateToDocument = (documentId: string, name: string) => { | |||||
| const navigate = useNavigate(); | |||||
| const navigateToDocument = () => { | |||||
| navigate(`/document/${documentId}?ext=${getExtension(name)}`); | |||||
| }; | |||||
| return navigateToDocument; | |||||
| }; |
| }, | }, | ||||
| { | { | ||||
| title: t('uploadDate'), | title: t('uploadDate'), | ||||
| dataIndex: 'create_date', | |||||
| key: 'create_date', | |||||
| dataIndex: 'create_time', | |||||
| key: 'create_time', | |||||
| render(text) { | render(text) { | ||||
| return formatDate(text); | return formatDate(text); | ||||
| }, | }, |
| <div className={styles.bottomLeft}> | <div className={styles.bottomLeft}> | ||||
| <CalendarOutlined className={styles.leftIcon} /> | <CalendarOutlined className={styles.leftIcon} /> | ||||
| <span className={styles.rightText}> | <span className={styles.rightText}> | ||||
| {formatDate(item.update_date)} | |||||
| {formatDate(item.update_time)} | |||||
| </span> | </span> | ||||
| </div> | </div> | ||||
| {/* <Avatar.Group size={25}> | {/* <Avatar.Group size={25}> |
| path: '/flow', | path: '/flow', | ||||
| component: '@/pages/flow', | component: '@/pages/flow', | ||||
| }, | }, | ||||
| { | |||||
| path: 'document/:id', | |||||
| component: '@/pages/document-viewer', | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| path: 'document/:id', | |||||
| component: '@/pages/document-viewer', | |||||
| layout: false, | |||||
| }, | |||||
| { | { | ||||
| path: '/*', | path: '/*', | ||||
| component: '@/pages/404', | component: '@/pages/404', |
| if (!date) { | if (!date) { | ||||
| return ''; | return ''; | ||||
| } | } | ||||
| return dayjs(date).format('DD/MM/YYYY'); | |||||
| return dayjs(date).format('DD/MM/YYYY HH:mm:ss'); | |||||
| } | } |