From 7d588a1f681ae037a610f9d58cf4980aadd8ea1b Mon Sep 17 00:00:00 2001 From: Linzp Date: Tue, 16 Jun 2026 12:14:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintignore | 6 + .gitignore | 2 + .husky/pre-commit | 3 +- README.md | 8 +- babel.config.js | 7 + doc/preview.js | 8 +- jest.config.js | 7 + package.json | 25 +- src/common/__tests__/utils.test.js | 31 ++ src/common/useStaticUrl.js | 2 +- src/components/Download/Download.js | 18 +- src/components/Download/downloadBlobFile.js | 4 +- src/components/Download/index.js | 2 +- src/components/Download/useDownload.js | 2 +- src/components/File/index.js | 5 +- src/components/FileButton/FileButton.js | 11 +- src/components/FileButton/FileModal.js | 52 ++- .../{style.modules.scss => style.module.scss} | 0 src/components/FileList/OptionButtons.js | 11 +- src/components/FileList/index.js | 30 +- src/components/FilePreview/AudioPreview.js | 7 +- src/components/FilePreview/DocxPreview.js | 136 ++++++ src/components/FilePreview/FilePreview.js | 11 +- src/components/FilePreview/HtmlPreview.js | 36 +- src/components/FilePreview/ImagePreview.js | 21 +- src/components/FilePreview/JsonPreview.js | 20 +- src/components/FilePreview/MarkdownPreview.js | 19 +- src/components/FilePreview/OSSFilePreview.js | 25 +- .../FilePreview/OSSFilePreviewInner.js | 30 ++ src/components/FilePreview/OfficePreview.js | 64 ++- src/components/FilePreview/PdfPreview.js | 215 ++++++---- src/components/FilePreview/PreviewHeader.js | 20 + src/components/FilePreview/PreviewShell.js | 27 ++ src/components/FilePreview/PreviewSuspense.js | 17 + .../FilePreview/PreviewZoomControls.js | 38 ++ src/components/FilePreview/TextPreview.js | 19 +- src/components/FilePreview/TypePreview.js | 11 +- src/components/FilePreview/UnknownPreview.js | 18 +- src/components/FilePreview/VideoPreview.js | 8 +- src/components/FilePreview/XlsxPreview.js | 112 +++++ src/components/FilePreview/ZipPreview.js | 275 ++++++------ .../__tests__/fileExtensions.test.js | 38 ++ .../FilePreview/createPreviewMapping.js | 29 ++ src/components/FilePreview/fileExtensions.js | 87 ++++ src/components/FilePreview/fileType.js | 1 + src/components/FilePreview/index.js | 5 +- .../FilePreview/innerTypePreview.js | 21 + src/components/FilePreview/previewMapping.js | 3 + src/components/FilePreview/style.module.scss | 103 ++++- src/components/FilePreview/typeFormat.js | 72 +--- src/components/FileSystem/EntryIcon.js | 25 ++ src/components/FileSystem/FileSystem.js | 402 ++++++++++++++++++ .../FileSystem/FileSystem.module.scss | 399 +++++++++++++++++ src/components/FileSystem/icons/folder.svg | 1 + src/components/FileSystem/index.js | 2 + src/components/FileSystem/utils.js | 125 ++++++ src/components/FileUpload/FileInput.js | 19 +- src/components/FileUpload/FileUpload.js | 32 +- src/components/FileUpload/useFileUpload.js | 20 +- src/components/Image/index.js | 153 ++++--- src/components/Image/style.module.scss | 2 - src/components/PrintButton/index.js | 8 +- src/hocs/withOSSFile.js | 1 - src/index.js | 21 +- src/locale/en-US.js | 60 ++- src/locale/zh-CN.js | 60 ++- src/withLocale.js | 8 + template-libs-example/public/mock/resume.xlsx | Bin 0 -> 66655 bytes 68 files changed, 2420 insertions(+), 640 deletions(-) create mode 100644 .eslintignore mode change 100644 => 100755 .husky/pre-commit create mode 100644 babel.config.js create mode 100644 jest.config.js create mode 100644 src/common/__tests__/utils.test.js rename src/components/FileButton/{style.modules.scss => style.module.scss} (100%) create mode 100644 src/components/FilePreview/DocxPreview.js create mode 100644 src/components/FilePreview/OSSFilePreviewInner.js create mode 100644 src/components/FilePreview/PreviewHeader.js create mode 100644 src/components/FilePreview/PreviewShell.js create mode 100644 src/components/FilePreview/PreviewSuspense.js create mode 100644 src/components/FilePreview/PreviewZoomControls.js create mode 100644 src/components/FilePreview/XlsxPreview.js create mode 100644 src/components/FilePreview/__tests__/fileExtensions.test.js create mode 100644 src/components/FilePreview/createPreviewMapping.js create mode 100644 src/components/FilePreview/fileExtensions.js create mode 100644 src/components/FilePreview/fileType.js create mode 100644 src/components/FilePreview/innerTypePreview.js create mode 100644 src/components/FilePreview/previewMapping.js create mode 100644 src/components/FileSystem/EntryIcon.js create mode 100644 src/components/FileSystem/FileSystem.js create mode 100644 src/components/FileSystem/FileSystem.module.scss create mode 100644 src/components/FileSystem/icons/folder.svg create mode 100644 src/components/FileSystem/index.js create mode 100644 src/components/FileSystem/utils.js create mode 100644 src/withLocale.js create mode 100644 template-libs-example/public/mock/resume.xlsx diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c830b7b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +dist +build +example +doc +node_modules +template-libs-example diff --git a/.gitignore b/.gitignore index caef309..3fcccf2 100644 --- a/.gitignore +++ b/.gitignore @@ -126,7 +126,9 @@ dist .yarn/install-state.gz .pnp.* .idea +.DS_Store build pnpm-lock.yaml package-lock.json example +prompts diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 index 7a76e76..2312dc5 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1 @@ -npm run build:md -npm run lint-staged +npx lint-staged diff --git a/README.md b/README.md index 91ba0f6..a8fd19e 100644 --- a/README.md +++ b/README.md @@ -435,8 +435,9 @@ const BaseExample = createWithRemoteLoader({ 3: '/mock/resume.html', 4: '/mock/resume.txt', 5: '/mock/audio.wav', - 6: 'http://ieee802.org:80/secmail/docIZSEwEqHFr.doc', - 7: '/mock/example.zip' + 6: '/mock/resume.docx', + 7: '/mock/example.zip', + 8: '/mock/resume.xlsx' }; return new Promise(resolve => { setTimeout(() => { @@ -465,7 +466,8 @@ const BaseExample = createWithRemoteLoader({ - + + diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..3e2f16c --- /dev/null +++ b/babel.config.js @@ -0,0 +1,7 @@ +module.exports = api => { + api.cache.using(() => process.env.NODE_ENV); + + return { + presets: [require.resolve('babel-preset-react-app')] + }; +}; diff --git a/doc/preview.js b/doc/preview.js index 6c27ecc..2ffb667 100644 --- a/doc/preview.js +++ b/doc/preview.js @@ -22,8 +22,9 @@ const BaseExample = createWithRemoteLoader({ 3: '/mock/resume.html', 4: '/mock/resume.txt', 5: '/mock/audio.wav', - 6: 'http://ieee802.org:80/secmail/docIZSEwEqHFr.doc', - 7: '/mock/example.zip' + 6: '/mock/resume.docx', + 7: '/mock/example.zip', + 8: '/mock/resume.xlsx' }; return new Promise(resolve => { setTimeout(() => { @@ -52,7 +53,8 @@ const BaseExample = createWithRemoteLoader({ - + + diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..dec7063 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + testEnvironment: 'node', + transform: { + '^.+\\.(js|jsx)$': 'babel-jest' + }, + modulePathIgnorePatterns: ['/dist/', '/example/', '/build/'] +}; diff --git a/package.json b/package.json index ca185e1..b82eb16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne/react-file", - "version": "0.1.36", + "version": "0.1.37", "description": "提供了文件上传,文件预览,文件批量管理等功能", "syntax": { "esmodules": true @@ -16,27 +16,22 @@ "build:md": "npx @kne/md-doc", "start:md": "npx @kne/md-doc --watch", "build:locale": "microbundle src/locale/*.js -o dist/locale --no-compress --format modern,cjs ", - "build:lib-main": "microbundle --no-compress --format modern,cjs --jsx React.createElement --jsxFragment React.Fragment", + "build:lib-main": "microbundle --no-compress --format modern,cjs --jsxImportSource react --jsx React.createElement --jsxFragment React.Fragment", "build:lib": "run-s build:locale build:lib-main", - "start:lib": "microbundle watch --no-compress --format modern,cjs --jsx React.createElement --jsxFragment React.Fragment", + "start:lib": "microbundle watch --no-compress --format modern,cjs --jsxImportSource react --jsx React.createElement --jsxFragment React.Fragment", "build:example": "cd example && npm run build", "start:example": "cd example && npm run start", "test:build": "run-s build", - "test:lint": "eslint .", - "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", - "test:watch": "react-scripts test --env=jsdom", + "test:lint": "cross-env NODE_ENV=development eslint src", + "test:unit": "cross-env NODE_ENV=test jest", + "test:watch": "jest --watch", "prettier": "prettier --config .prettierrc --write '{src/**/*,index,prompts}.{js,jsx,ts,tsx,json,css,scss}'", - "lint-staged": "npx lint-staged" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } + "lint-staged": "lint-staged", + "prepare": "husky" }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ - "prettier --config .prettierrc --write", - "git add" + "prettier --config .prettierrc --write" ] }, "files": [ @@ -88,6 +83,8 @@ }, "dependencies": { "@ant-design/icons": "^5.5.1", + "@extend-ai/react-docx": "^0.7.1", + "@extend-ai/react-xlsx": "^0.10.2", "@kne/button-group": "^0.1.3", "@kne/create-deferred": "^0.1.0", "@kne/global-context": "^1.3.2", diff --git a/src/common/__tests__/utils.test.js b/src/common/__tests__/utils.test.js new file mode 100644 index 0000000..5bb5720 --- /dev/null +++ b/src/common/__tests__/utils.test.js @@ -0,0 +1,31 @@ +import { formatStaticUrl } from '../useStaticUrl'; +import computedAccept from '../../components/FileUpload/computedAccept'; + +describe('formatStaticUrl', () => { + test('returns absolute urls unchanged', () => { + expect(formatStaticUrl({ url: 'https://cdn.example.com/a.pdf', staticUrl: '/static/' })).toBe('https://cdn.example.com/a.pdf'); + expect(formatStaticUrl({ url: 'blob:abc', staticUrl: '/static/' })).toBe('blob:abc'); + }); + + test('prefixes relative urls with staticUrl', () => { + expect(formatStaticUrl({ url: 'files/a.pdf', staticUrl: '/static/' })).toBe('/static/files/a.pdf'); + }); +}); + +describe('computedAccept', () => { + const file = (name, type) => ({ name, type }); + + test('matches extension accept rules', () => { + expect(computedAccept(file('a.PDF', 'application/pdf'), '.pdf,.png')).toBe(true); + expect(computedAccept(file('a.doc', 'application/msword'), '.pdf,.png')).toBe(false); + }); + + test('matches mime wildcard rules', () => { + expect(computedAccept(file('a.bin', 'image/png'), 'image/*')).toBe(true); + expect(computedAccept(file('a.bin', 'application/pdf'), 'image/*')).toBe(false); + }); + + test('allows all files when accept is empty', () => { + expect(computedAccept(file('a.bin', 'application/octet-stream'), null)).toBe(true); + }); +}); diff --git a/src/common/useStaticUrl.js b/src/common/useStaticUrl.js index 6c19491..aa85c57 100644 --- a/src/common/useStaticUrl.js +++ b/src/common/useStaticUrl.js @@ -1,7 +1,7 @@ import { usePreset } from '@kne/global-context'; export const formatStaticUrl = ({ url, staticUrl }) => { - return /^(blob:)?https?:\/\//.test(url) ? url : staticUrl + url; + return /^(blob:|https?:\/\/)/.test(url) ? url : staticUrl + url; }; const useStaticUrl = ({ url, staticUrl: staticUrlProps }) => { diff --git a/src/components/Download/Download.js b/src/components/Download/Download.js index 70e3b66..f165e2c 100644 --- a/src/components/Download/Download.js +++ b/src/components/Download/Download.js @@ -1,23 +1,18 @@ -import React from 'react'; import { Button } from 'antd'; import { DownloadOutlined } from '@ant-design/icons'; import useDownload from './useDownload'; import downloadAction from './downloadAction'; import downloadBlobFile from './downloadBlobFile'; import omit from 'lodash/omit'; -import { createWithIntlProvider, useIntl } from '@kne/react-intl'; -import zhCn from '../../locale/zh-CN'; +import withLocale from '../../withLocale'; +import { useIntl } from '@kne/react-intl'; -const Download = createWithIntlProvider( - 'zh-CN', - zhCn, - 'react-file' -)(p => { +const DownloadInner = p => { const { formatMessage } = useIntl(); const { id, src, filename, api, onSuccess, onError, onClick, ...props } = Object.assign( {}, { - filename: formatMessage({ id: 'unnamedDownloadFile' }) + filename: formatMessage({ id: 'Download.unnamedDownloadFile' }) }, p ); @@ -42,10 +37,13 @@ const Download = createWithIntlProvider( }} /> ); -}); +}; + +const Download = withLocale(DownloadInner); Download.useDownload = useDownload; Download.download = downloadAction; Download.downloadBlobFile = downloadBlobFile; +export { DownloadInner }; export default Download; diff --git a/src/components/Download/downloadBlobFile.js b/src/components/Download/downloadBlobFile.js index a3c89c5..5bc528f 100644 --- a/src/components/Download/downloadBlobFile.js +++ b/src/components/Download/downloadBlobFile.js @@ -1,9 +1,11 @@ import download from './downloadAction'; import { getAjax } from '@kne/react-fetch'; +import { createIntl } from '@kne/react-intl'; const downloadBlobFile = async (input, filename = 'file', locale) => { if (!input) { - throw new Error(locale?.notFoundFile || '未获取到下载的文件信息'); + const { formatMessage } = createIntl({ locale, namespace: 'react-file' }); + throw new Error(formatMessage({ id: 'Download.notFoundFile' })); } if (typeof input === 'string' && /blob:http(s)?:/.test(input)) { download(input, filename); diff --git a/src/components/Download/index.js b/src/components/Download/index.js index bc987cf..0e26371 100644 --- a/src/components/Download/index.js +++ b/src/components/Download/index.js @@ -1,4 +1,4 @@ -export { default } from './Download'; +export { default, DownloadInner } from './Download'; export { default as useDownload } from './useDownload'; export { default as downloadBlobFile } from './downloadBlobFile'; export { default as download } from './downloadAction'; diff --git a/src/components/Download/useDownload.js b/src/components/Download/useDownload.js index 740a886..2d50257 100644 --- a/src/components/Download/useDownload.js +++ b/src/components/Download/useDownload.js @@ -48,7 +48,7 @@ const useDownload = ({ id, src, filename, staticUrl: staticUrlProps, apis: curre downloadHandler(data).then(() => { setDownLoading(false); }); - }, [isLoading, error, data, showError]); + }, [isLoading, error, data, showError, downloadHandler]); return { ...otherProps, diff --git a/src/components/File/index.js b/src/components/File/index.js index d565e04..c8a026b 100644 --- a/src/components/File/index.js +++ b/src/components/File/index.js @@ -1,7 +1,10 @@ import withOSSFile from '../../hocs/withOSSFile'; +import withLocale from '../../withLocale'; -const File = withOSSFile(({ data, children, ...props }) => { +const FileInner = withOSSFile(({ data, children, ...props }) => { return children({ url: data, ...props }); }); +const File = withLocale(FileInner); + export default File; diff --git a/src/components/FileButton/FileButton.js b/src/components/FileButton/FileButton.js index 7ab3c49..3e10cf2 100644 --- a/src/components/FileButton/FileButton.js +++ b/src/components/FileButton/FileButton.js @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { LinkOutlined } from '@ant-design/icons'; import { Button } from 'antd'; -import FileModal from './FileModal'; +import { FileModalInner } from './FileModal'; +import withLocale from '../../withLocale'; -const FileButton = p => { +const FileButtonInner = p => { const [open, onOpenChange] = useState(false); const { filename, originName, id, src, title, modalProps, openDownload, openPrint, children, ...props } = Object.assign( {}, @@ -35,9 +36,11 @@ const FileButton = p => { > {typeof children === 'function' ? children(filename || originName) : children || filename || originName} - + ); }; +const FileButton = withLocale(FileButtonInner); + export default FileButton; diff --git a/src/components/FileButton/FileModal.js b/src/components/FileButton/FileModal.js index e750157..3c3bdd4 100644 --- a/src/components/FileButton/FileModal.js +++ b/src/components/FileButton/FileModal.js @@ -1,17 +1,16 @@ -import React, { useRef } from 'react'; +import { useRef } from 'react'; import { Modal, Space, App } from 'antd'; import { PrinterOutlined } from '@ant-design/icons'; -import Download from '../Download'; -import PrintButton from '../PrintButton'; -import FilePreview, { typeFormat } from '../FilePreview'; +import { DownloadInner } from '../Download'; +import { PrintButtonInner } from '../PrintButton'; +import { FilePreviewInner, typeFormat } from '../FilePreview'; import useControlValue from '@kne/use-control-value'; -import { createIntlProvider, useIntl } from '@kne/react-intl'; -import zhCn from '../../locale/zh-CN'; -import style from './style.modules.scss'; - -const IntlProvider = createIntlProvider('zh-CN', zhCn, 'react-file'); +import withLocale from '../../withLocale'; +import { useIntl } from '@kne/react-intl'; +import style from './style.module.scss'; export const useFileModalProps = p => { + const { formatMessage } = useIntl(); const { title, filename, originName, openDownload, openPrint, id, src, apis, ...props } = Object.assign( {}, { @@ -40,29 +39,25 @@ export const useFileModalProps = p => { {title || filename || originName} {openDownload && ( - - {({ formatMessage }) => ( - { - message.success(formatMessage({ id: 'downloadSuccess' })); - }} - /> - )} - + { + message.success(formatMessage({ id: 'Download.downloadSuccess' })); + }} + /> )} - {openPrint && ['txt', 'pdf', 'image', 'html'].indexOf(typeFormat(filename || originName)) > -1 && } />} + {openPrint && ['txt', 'pdf', 'image', 'html'].indexOf(typeFormat(filename || originName)) > -1 && } />} ), children: (
- +
) }; @@ -81,9 +76,10 @@ export const useFileModal = p => { return Object.assign({}, fileProps, { renderModal: props => renderModal(Object.assign({}, fileProps, props)) }); }; -const FileModal = p => { +const FileModalInner = p => { const { renderModal } = useFileModal(p); return renderModal(); }; -export default FileModal; +export { FileModalInner }; +export default withLocale(FileModalInner); diff --git a/src/components/FileButton/style.modules.scss b/src/components/FileButton/style.module.scss similarity index 100% rename from src/components/FileButton/style.modules.scss rename to src/components/FileButton/style.module.scss diff --git a/src/components/FileList/OptionButtons.js b/src/components/FileList/OptionButtons.js index f0d7702..d5d9dce 100644 --- a/src/components/FileList/OptionButtons.js +++ b/src/components/FileList/OptionButtons.js @@ -1,12 +1,12 @@ -import React from 'react'; import { Flex, Button, Modal } from 'antd'; import { ConfirmButton, LoadingButton } from '@kne/button-group'; import '@kne/button-group/dist/index.css'; -import DownloadButton from '../Download'; +import { DownloadInner } from '../Download'; import { useFileModal } from '../FileButton'; import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import withLocale from '../../withLocale'; -const OptionButtons = p => { +const OptionButtonsInner = p => { const { item, hasPreview, @@ -55,7 +55,7 @@ const OptionButtons = p => { }} /> )} - {getPermission('download', item) && } + {getPermission('download', item) && } {getPermission('delete', item) && ( { ); }; +const OptionButtons = withLocale(OptionButtonsInner); + +export { OptionButtonsInner }; export default OptionButtons; diff --git a/src/components/FileList/index.js b/src/components/FileList/index.js index 3ba82d0..483396e 100644 --- a/src/components/FileList/index.js +++ b/src/components/FileList/index.js @@ -1,18 +1,13 @@ -import React from 'react'; import { Col, List as AntdList, Modal, Row, Space, Spin, Typography } from 'antd'; import FileType from '@kne/react-file-type'; -import OptionButtons from './OptionButtons'; +import { OptionButtonsInner } from './OptionButtons'; import last from 'lodash/last'; import dayjs from 'dayjs'; import style from './style.module.scss'; -import { createWithIntlProvider, useIntl } from '@kne/react-intl'; -import zhCn from '../../locale/zh-CN'; +import withLocale from '../../withLocale'; +import { useIntl } from '@kne/react-intl'; -const List = createWithIntlProvider( - 'zh-CN', - zhCn, - 'react-file' -)(p => { +const ListInner = p => { const { formatMessage } = useIntl(); const { className, dataSource, getPermission, infoItemRenders, onDelete, onEdit, apis, renderModal } = Object.assign( {}, @@ -35,10 +30,7 @@ const List = createWithIntlProvider( return ( { - item.index = index; - return item; - })} + dataSource={dataSource.map((item, index) => ({ ...item, index }))} rowKey={item => `item_${(item.uuid && `uuid_${item.uuid}`) || (item.id && `id_${item.id}`) || (item.src && `src_${item.src}`)}`} renderItem={item => { const { type, filename } = item; @@ -63,11 +55,11 @@ const List = createWithIntlProvider( })} {type !== 'uploading' ? ( - + ) : ( - {formatMessage({ id: 'uploading' })} + {formatMessage({ id: 'FileList.uploading' })} )} @@ -78,8 +70,10 @@ const List = createWithIntlProvider( bordered /> ); -}); +}; -export default List; +const List = withLocale(ListInner); -export { OptionButtons }; +export { ListInner }; +export { default as OptionButtons } from './OptionButtons'; +export default List; diff --git a/src/components/FilePreview/AudioPreview.js b/src/components/FilePreview/AudioPreview.js index 11fcc43..44b4fa4 100644 --- a/src/components/FilePreview/AudioPreview.js +++ b/src/components/FilePreview/AudioPreview.js @@ -1,7 +1,7 @@ -import React from 'react'; import style from './style.module.scss'; +import withLocale from '../../withLocale'; -const AudioPreview = ({ url, maxWidth, ...props }) => { +const AudioPreviewInner = ({ url, maxWidth, ...props }) => { return (
{ ); }; +const AudioPreview = withLocale(AudioPreviewInner); + +export { AudioPreviewInner }; export default AudioPreview; diff --git a/src/components/FilePreview/DocxPreview.js b/src/components/FilePreview/DocxPreview.js new file mode 100644 index 0000000..ded10c5 --- /dev/null +++ b/src/components/FilePreview/DocxPreview.js @@ -0,0 +1,136 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Alert, Button, Spin } from 'antd'; +import { CloudOutlined } from '@ant-design/icons'; +import { DocxEditorViewer, useDocxEditor, useDocxPagination } from '@extend-ai/react-docx'; +import withLocale from '../../withLocale'; +import { useIntl } from '@kne/react-intl'; +import PreviewShell from './PreviewShell'; +import PreviewZoomControls from './PreviewZoomControls'; +import style from './style.module.scss'; + +const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + +const DocxPreviewInner = ({ url, filename, className, height = 600, showHeader = true, onRemotePreview }) => { + const { formatMessage } = useIntl(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [zoom, setZoom] = useState(100); + const [viewportElement, setViewportElement] = useState(null); + const displayFileName = useMemo(() => filename || 'document.docx', [filename]); + + const editor = useDocxEditor({ + initialDocumentTheme: 'light', + initialFileName: displayFileName + }); + const { importDocxFile } = editor; + const { pagination } = useDocxPagination(editor); + + const setViewportRef = useCallback(node => { + setViewportElement(node); + }, []); + + const zoomScale = zoom / 100; + + const pageVirtualization = useMemo( + () => ({ + enabled: true, + overscan: 1, + scrollElement: viewportElement, + zoomScale + }), + [viewportElement, zoomScale] + ); + + const viewerZoomStyle = useMemo( + () => ({ + zoom: zoomScale + }), + [zoomScale] + ); + + useEffect(() => { + let cancelled = false; + + async function load() { + if (!url) { + setLoading(false); + setError(formatMessage({ id: 'FilePreview.fileNotFoundError' })); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch DOCX (${response.status})`); + } + + const buffer = await response.arrayBuffer(); + const file = new File([buffer], displayFileName, { + type: DOCX_MIME + }); + + await importDocxFile(file); + + if (!cancelled) { + setLoading(false); + } + } catch (loadError) { + if (!cancelled) { + setError(loadError?.message || formatMessage({ id: 'FilePreview.fileLoadedError' })); + setLoading(false); + } + } + } + + load(); + + return () => { + cancelled = true; + }; + }, [url, displayFileName, importDocxFile, formatMessage]); + + const toolbarExtra = + pagination.totalPages > 0 ? ( + + {pagination.currentPage} / {pagination.totalPages} + + ) : null; + + const headerActions = useMemo(() => { + const items = []; + + if (onRemotePreview) { + items.push( + + ); + } + + return items; + }, [zoom, loading, error, onRemotePreview, formatMessage]); + + return ( + + {loading ? ( +
+ +
+ ) : null} + {error && !loading ? : null} + {!loading && !error ? ( +
+ +
+ ) : null} +
+ ); +}; + +const DocxPreview = withLocale(DocxPreviewInner); + +export { DocxPreviewInner }; +export default DocxPreview; diff --git a/src/components/FilePreview/FilePreview.js b/src/components/FilePreview/FilePreview.js index 379bc16..550d0e9 100644 --- a/src/components/FilePreview/FilePreview.js +++ b/src/components/FilePreview/FilePreview.js @@ -1,12 +1,15 @@ -import React from 'react'; -import OSSFilePreview from './OSSFilePreview'; +import OSSFilePreviewInner from './OSSFilePreviewInner'; import TypePreview from './TypePreview'; +import withLocale from '../../withLocale'; -const FilePreview = ({ id, src, originName, filename, ...props }) => { +const FilePreviewInner = ({ id, src, originName, filename, ...props }) => { if (src) { return ; } - return ; + return ; }; +const FilePreview = withLocale(FilePreviewInner); + +export { FilePreviewInner }; export default FilePreview; diff --git a/src/components/FilePreview/HtmlPreview.js b/src/components/FilePreview/HtmlPreview.js index 48f90ed..dcd27da 100644 --- a/src/components/FilePreview/HtmlPreview.js +++ b/src/components/FilePreview/HtmlPreview.js @@ -1,22 +1,17 @@ import iFrameResize from '@kne/iframe-resizer'; -import React, { useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import classnames from 'classnames'; import style from './style.module.scss'; import Fetch from '@kne/react-fetch'; import { usePreset } from '@kne/global-context'; -import { createWithIntlProvider, useIntl } from '@kne/react-intl'; -import zhCn from '../../locale/zh-CN'; +import withLocale from '../../withLocale'; +import { useIntl } from '@kne/react-intl'; -const HtmlInnerPreview = createWithIntlProvider( - 'zh-CN', - zhCn, - 'react-file' -)(({ data, apis: propsApis, contentWindowUrl: contentWindowUrlProps }) => { +const HtmlInnerPreviewInner = ({ data, apis: propsApis, contentWindowUrl: contentWindowUrlProps }) => { const ref = useRef(null); const { apis: baseApis } = usePreset(); const { formatMessage } = useIntl(); const apis = Object.assign({}, baseApis, propsApis); - // https://uc.fatalent.cn/packages/@kne/iframe-resizer/0.1.2/dist/contentWindow.js https://cdn.jsdelivr.net/npm/@kne/iframe-resizer@0.1.3/dist/contentWindow.js const contentWindowUrl = contentWindowUrlProps || apis.file?.contentWindowUrl || 'https://cdn.jsdelivr.net/npm/@kne/iframe-resizer@0.1.3/dist/contentWindow.js'; useEffect(() => { const parser = new DOMParser(); @@ -31,22 +26,18 @@ const HtmlInnerPreview = createWithIntlProvider( style.innerText = 'html,body{height:auto!important;}body{pointer-events: none;background: #FFFFFF;}'; domDocument.head.appendChild(style); ref.current.srcdoc = domDocument.documentElement.outerHTML; - }, [data]); + }, [data, contentWindowUrl]); useEffect(() => { iFrameResize({ checkOrigin: false }, ref.current); - }, []); - return