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..5c6f991 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### 描述 -提供了文件上传,文件预览,文件批量管理等功能 +React文件操作组件库,提供文件上传、多格式预览、下载、列表管理及文件系统浏览,支持i18n ### 安装 @@ -16,86 +16,47 @@ npm i --save @kne/react-file ### 概述 -这是一个功能丰富的React文件操作组件库,提供了文件上传、预览、下载等完整的文件处理解决方案。 +React文件操作组件库,提供文件上传、多格式预览、下载、列表管理及文件系统浏览的完整解决方案,支持国际化。 ### 主要特性 -- 🚀 **文件上传** - 支持单文件和多文件上传,可自定义文件类型限制 -- 👀 **文件预览** - 支持多种文件格式的预览,包括: - - 图片(PNG, JPG, GIF等) - - 音频文件 - - 视频文件 - - PDF文档 - - Office文档(Word, Excel, PowerPoint) - - HTML文件 - - 文本文件 -- 📥 **文件下载** - 支持文件直接下载和blob数据下载 -- 📋 **文件列表** - 提供文件列表展示和管理功能 -- 🖼️ **图片处理** - 专门的图片组件,支持默认头像和加载失败处理 -- 🖨️ **打印功能** - 内置打印按钮组件 +- **文件上传** - 支持单文件/多文件上传,可自定义文件类型、大小限制和并发数 +- **文件预览** - 根据文件类型自动选择预览方式,支持图片、音频、视频、PDF、Office文档、HTML、Markdown、JSON、ZIP、文本等 +- **文件下载** - 支持OSS文件ID下载、直接URL下载和Blob数据下载 +- **文件列表** - 展示已上传文件列表,支持上传中状态、预览、下载、删除操作 +- **文件系统** - 提供文件系统浏览组件,支持图标、列表、分栏、画廊四种视图模式 +- **图片处理** - 支持默认头像、性别区分头像、加载骨架屏和失败占位图 +- **打印功能** - 内置打印按钮组件,支持打印前/后回调 +- **国际化** - 内置中英文语言包,支持多语言切换 ### 组件概览 +#### File +文件组件,通过OSS文件ID获取文件URL,使用render props模式。 + #### FileUpload 文件上传组件,支持单文件和多文件上传,可自定义接受的文件类型和上传限制。 #### FilePreview -文件预览组件,根据文件类型自动选择合适的预览方式。支持本地文件URL和OSS文件ID两种方式。 +文件预览组件,根据文件类型自动选择合适的预览方式。支持本地URL和OSS文件ID两种方式。 #### FileList -文件列表组件,用于展示和管理已上传的文件,支持预览、下载等操作。 +文件列表组件,用于展示和管理已上传的文件,支持上传中状态、预览、下载和删除操作。 + +#### FileButton +文件操作按钮组件,点击后弹出文件预览弹窗,支持下载和打印。 #### Download -文件下载组件,支持多种下载方式,包括直接下载和blob数据下载。 +文件下载按钮组件,支持OSS文件ID下载和直接URL下载。 #### Image 图片组件,支持默认头像、性别区分的头像显示,以及加载失败时的占位图。 -#### FileButton -文件操作按钮组件,提供文件相关操作的统一交互界面。 +#### FileSystem +文件系统浏览组件,提供图标、列表、分栏、画廊四种视图,支持导航历史和搜索。 #### PrintButton -打印按钮组件,用于触发打印功能。 - - -### 快速开始 - -```jsx -import { FileUpload, FileList, FilePreview } from '@your-org/react-file-components'; - -// 文件上传示例 -const UploadExample = () => ( - console.log('Uploaded files:', files)} - /> -); - -// 文件预览示例 -const PreviewExample = () => ( - -); - -// 文件列表示例 -const ListExample = () => ( - console.log('Delete file:', id)} - /> -); -``` - -## 注意事项 - -1. 确保服务器端支持相应的文件上传和处理功能 -2. 文件预览功能可能需要额外的依赖或服务器支持(如Office文档预览) +打印按钮组件,用于触发浏览器打印功能。 ### 示例(全屏) @@ -435,8 +396,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 +427,8 @@ const BaseExample = createWithRemoteLoader({ - + + @@ -554,9 +517,90 @@ render(); ``` +- FileSystem +- 文件系统浏览 +- _ReactFile(@kne/current-lib_react-file)[import * as _ReactFile from "@kne/react-file"],(@kne/current-lib_react-file/dist/index.css),antd(antd),remoteLoader(@kne/remote-loader) + +```jsx +const { FileSystem } = _ReactFile; +const { createWithRemoteLoader, getPublicPath } = remoteLoader; +const { Segmented } = antd; +const { useState } = React; + +const BaseExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:InfoPage'] +})(({ remoteModules }) => { + const [PureGlobal, InfoPage] = remoteModules; + const [items] = useState([ + { kind: 'folder', path: 'documents/', name: 'Documents' }, + { kind: 'folder', path: 'documents/reports/', name: 'Reports' }, + { kind: 'file', path: 'documents/reports/Q3-report.pdf', name: 'Q3-report.pdf', size: 1024000 }, + { kind: 'file', path: 'documents/reports/Q4-report.xlsx', name: 'Q4-report.xlsx', size: 512000 }, + { kind: 'file', path: 'documents/meeting-notes.docx', name: 'meeting-notes.docx', size: 256000 }, + { kind: 'folder', path: 'images/', name: 'Images' }, + { kind: 'file', path: 'images/logo.png', name: 'logo.png', size: 45000 }, + { kind: 'file', path: 'images/banner.jpg', name: 'banner.jpg', size: 89000 }, + { kind: 'folder', path: 'archives/', name: 'Archives' }, + { kind: 'file', path: 'archives/project-backup.zip', name: 'project-backup.zip', size: 15728640 }, + { kind: 'file', path: 'readme.md', name: 'readme.md', size: 3200 }, + { kind: 'file', path: 'config.json', name: 'config.json', size: 1200 } + ]); + + return ( + { + return { data: { code: 0, data: api.loader() } }; + }, + apis: { + file: { + staticUrl: getPublicPath('react-file') || window.PUBLIC_URL + } + } + }}> + + + { + console.log('Open file:', entry); + }} + onSelectionChange={entry => { + console.log('Selection changed:', entry); + }} + /> + + + + ); +}); + +render(); + +``` + ### API +### File + +文件组件,通过OSS文件ID获取文件URL,使用render props模式。内部使用`withOSSFile` HOC获取文件URL。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID,必填 | +| children | function({ url, ...props }) | - | render props函数,参数包含获取到的文件URL | +| apis | object | - | 自定义API配置,会与全局preset.apis合并 | +| loading | ReactNode | null | 加载中显示的内容 | +| error | ReactNode | - | 加载失败显示的内容 | +| ttl | number | 6000000 | 缓存过期时间(毫秒) | +| cacheName | string | 'file-cache' | 缓存名称 | + +--- + ### FileUpload 文件上传组件,支持单文件和多文件上传。 @@ -565,56 +609,97 @@ render(); | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| accept | string | undefined | 接受的文件类型,如 ".jpg,.png,.pdf" | -| multiple | boolean | false | 是否支持多文件上传 | -| onChange | function | - | 文件选择后的回调函数,参数为选择的文件数组 | -| disabled | boolean | false | 是否禁用上传功能 | -| maxSize | number | - | 单个文件最大尺寸(字节) | -| children | ReactNode | - | 自定义上传按钮内容 | +| value | array | [] | 已上传文件列表(受控) | +| defaultValue | array | [] | 默认文件列表 | +| onChange | function(list) | - | 文件列表变化回调 | +| accept | string[] | ['.pdf','.jpg','.png','.jpeg','.doc','.docx','.xls','.xlsx','.html','.msg','.eml','.zip'] | 接受的文件类型 | +| multiple | boolean | true | 是否支持多文件上传 | +| fileSize | number | 100 | 单个文件最大尺寸(MB) | +| maxLength | number | 10 | 最大文件数量 | +| concurrentCount | number | 10 | 上传并发数 | +| size | string | - | 按钮尺寸,同antd Button的size | +| showUploadList | boolean | true | 是否显示已上传文件列表 | +| children | ReactNode | '文件上传' | 上传按钮文字 | +| renderTips | function(defaultTips, { fileSize, maxLength, accept }) | v => v | 自定义提示信息渲染 | +| onSave | function(data, file, uuid) | - | 上传成功后数据处理回调,返回值将作为新的文件对象 | +| ossUpload | function | - | 自定义上传函数 | +| getPermission | function(type) | - | 文件列表操作权限控制函数,type为'preview'/'delete'等 | +| apis | object | - | API配置对象 | +| renderModal | function(modalProps) | props => Modal | 自定义弹窗渲染函数 | -### MarkdownPreview +--- + +### FileInput -Markdown文件预览组件,支持渲染Markdown格式的文档。 +文件输入组件,提供文件选择按钮。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | Markdown文件的URL地址 | -| className | string | - | 自定义容器类名 | -| maxWidth | string/number | - | 容器最大宽度 | +| accept | string[] | defaultAccept | 接受的文件类型 | +| multiple | boolean | true | 是否支持多选 | +| buttonText | string | '文件上传' | 按钮文字 | +| onChange | function(fileList) | - | 文件选择后的回调,参数为File数组 | +| children | function({ children, ...props }) | - | 自定义按钮渲染函数 | +| disabled | boolean | false | 是否禁用 | -### ZipPreview +--- -ZIP压缩包文件预览组件,支持查看压缩包内部的文件列表和目录结构。 +### FilePreview + +文件预览组件,根据文件类型自动选择合适的预览方式。提供`src`时使用`TypePreview`,提供`id`时使用`OSSFilePreview`。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | ZIP文件的URL地址 | -| className | string | - | 自定义容器类名 | -| maxWidth | string/number | - | 容器最大宽度 | +| id | string | - | OSS文件ID | +| src | string | - | 文件URL | +| filename | string | - | 文件名,用于确定文件类型 | +| originName | string | - | 原始文件名,作为filename的备选 | +| apis | object | - | API配置对象 | + +#### 预览子组件 + +以下子组件均可单独使用: + +| 子组件 | 描述 | +|--------|------| +| ImagePreview | 图片预览 | +| AudioPreview | 音频预览 | +| VideoPreview | 视频预览 | +| PdfPreview | PDF预览 | +| DocxPreview | Word文档预览 | +| XlsxPreview | Excel预览 | +| OfficePreview | Office文档预览(根据文件类型选择DocxPreview或XlsxPreview) | +| HtmlPreview | HTML预览 | +| MarkdownPreview | Markdown预览 | +| JsonPreview | JSON预览 | +| ZipPreview | ZIP压缩包预览 | +| TextPreview | 文本文件预览 | +| UnknownPreview | 未知类型文件预览 | +| OSSFilePreview | OSS文件预览(通过ID获取URL后自动选择预览方式) | + +--- + +### MarkdownPreview -#### 功能特性 +Markdown文件预览组件。 -- 支持解析ZIP压缩包内容 -- 以树形结构展示文件和目录 -- 显示文件大小和压缩后大小 -- 支持嵌套目录结构 -- 自动格式化文件大小(B、KB、MB、GB) +#### 属性 -#### 支持的压缩格式 +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| url | string | - | Markdown文件的URL地址 | +| className | string | - | 自定义容器类名 | +| maxWidth | string/number | - | 容器最大宽度 | -- `.zip` - ZIP压缩文件 -- `.rar` - RAR压缩文件 -- `.7z` - 7-Zip压缩文件 -- `.tar` - TAR归档文件 -- `.gz` - Gzip压缩文件 +--- ### JsonPreview -JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示。 +JSON文件预览组件,使用@kne/json-view提供友好的JSON数据展示。 #### 属性 @@ -630,28 +715,21 @@ JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示 | indentWidth | number | 20 | 缩进宽度(像素) | | showLineNumbers | boolean | true | 是否显示行号 | -#### 功能特性 - -- 语法高亮:不同数据类型使用不同颜色 -- 层级控制:支持展开/收起,可指定从第几级开始收起 -- 搜索功能:基于 Fuse.js 的模糊搜索,关键字高亮 -- 双主题支持:白色和黑色两种主题 -- 复制功能:一键复制格式化的 JSON 数据 -- 行号显示:左侧显示行号 +--- -### FilePreview +### ZipPreview -文件预览组件,支持多种文件格式的预览。 +ZIP压缩包文件预览组件,支持查看压缩包内部的文件列表和目录结构。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | 文件URL | -| filename | string | - | 文件名,用于确定文件类型 | -| type | string | - | 文件类型,可选,如不提供则根据文件名推断 | -| staticUrl | string | - | 静态资源URL前缀 | -| apis | object | - | API配置对象 | +| url | string | - | ZIP文件的URL地址 | +| className | string | - | 自定义容器类名 | +| maxWidth | string/number | - | 容器最大宽度 | + +--- ### FileList @@ -661,25 +739,116 @@ JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| files | array | [] | 文件列表数据 | -| onDelete | function | - | 删除文件的回调函数 | -| onPreview | function | - | 预览文件的回调函数 | -| onDownload | function | - | 下载文件的回调函数 | -| renderItem | function | - | 自定义渲染文件项的函数 | +| dataSource | array | [] | 文件列表数据,每项包含 id/filename/date/userName/type 等 | +| getPermission | function(type, item) | () => true | 操作权限控制函数,type为'preview'/'edit'/'download'/'delete' | +| infoItemRenders | array | [用户名, 日期] | 信息列渲染函数数组 | +| onDelete | function(item) | - | 删除文件的回调函数 | +| onEdit | function(item) | - | 编辑文件的回调函数 | +| apis | object | - | API配置对象 | +| renderModal | function(modalProps) | props => Modal | 自定义弹窗渲染函数 | + +#### dataSource 数据项 + +| 字段 | 类型 | 描述 | +|------|------|------| +| id | string | 文件ID | +| filename | string | 文件名 | +| src | string | 文件URL | +| date | string | 上传日期(ISO格式) | +| userName | string | 上传用户名 | +| type | string | 'uploading'表示上传中 | +| uuid | string | 上传中的临时标识 | + +--- + +### FileListOptionButtons + +文件列表操作按钮组件,提供预览、编辑、下载、删除操作。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| item | object | - | 文件数据项 | +| hasPreview | boolean | true | 是否显示预览按钮 | +| getPermission | function(type, item) | () => true | 操作权限控制函数 | +| apis | object | {} | API配置对象 | +| onDelete | function(item) | - | 删除回调 | +| onEdit | function(item) | - | 编辑回调 | +| onPreview | function(item) | - | 预览回调 | +| renderModal | function(modalProps) | props => Modal | 自定义弹窗渲染函数 | + +--- ### Download -文件下载组件。 +文件下载按钮组件,基于antd Button封装。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | 下载文件的URL | -| filename | string | - | 下载后的文件名 | -| children | ReactNode | - | 触发下载的元素 | +| id | string | - | OSS文件ID,通过API获取下载URL | +| src | string | - | 直接的下载URL | +| filename | string | '未命名下载文件' | 下载后的文件名 | +| api | object | - | 自定义API配置 | | onSuccess | function | - | 下载成功的回调函数 | | onError | function | - | 下载失败的回调函数 | +| onClick | function | - | 点击按钮的回调函数 | +| ...antd Button props | - | - | 支持所有antd Button属性 | + +#### 静态方法 + +| 方法 | 描述 | +|------|------| +| Download.useDownload | useDownload Hook | +| Download.download | download(url, filename) 下载工具函数 | +| Download.downloadBlobFile | downloadBlobFile(input, filename, locale) Blob下载函数 | + +--- + +### FileButton + +文件操作按钮组件,点击后弹出文件预览弹窗。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 文件URL | +| filename | string | - | 文件名 | +| originName | string | - | 原始文件名,作为filename的备选 | +| title | string | - | 弹窗标题,默认使用filename | +| modalProps | object | - | 传递给弹窗的属性,如{ width: 800 } | +| openDownload | boolean | false | 弹窗中是否显示下载按钮 | +| openPrint | boolean | false | 弹窗中是否显示打印按钮(仅txt/pdf/image/html类型支持) | +| children | ReactNode \| function(filename) | - | 按钮内容,支持函数返回 | +| ...antd Button props | - | - | 支持所有antd Button属性(默认icon为LinkOutlined) | + +--- + +### FileModal + +文件预览弹窗组件。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 文件URL | +| filename | string | - | 文件名 | +| title | string | - | 弹窗标题 | +| open | boolean | - | 是否显示(受控) | +| defaultOpen | boolean | - | 默认是否显示 | +| onOpenChange | function(open) | - | 显示状态变化回调 | +| openDownload | boolean | false | 是否显示下载按钮 | +| openPrint | boolean | false | 是否显示打印按钮 | +| footer | ReactNode | null | 弹窗底部内容 | +| apis | object | - | API配置对象 | + +--- ### Image @@ -689,107 +858,260 @@ JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| +| id | string | - | OSS文件ID,通过ID获取图片URL | | src | string | - | 图片源URL | | alt | string | - | 图片替代文本 | -| defaultAvatar | string | - | 默认头像类型('male'/'female'/'default') | -| onError | function | - | 图片加载失败的回调函数 | +| loading | ReactNode | Skeleton.Avatar | 加载中显示的内容 | +| error | ReactNode | PhotoFail图标 | 加载失败显示的内容 | | className | string | - | 自定义类名 | -| style | object | - | 自定义样式 | +| apis | object | - | API配置对象 | +| staticUrl | string | - | 静态资源URL前缀 | +| onClick | function | - | 点击事件 | +| children | function({ alt, className, src }) | - | 自定义渲染函数 | -### FileButton +#### Image.Avatar -文件操作按钮组件。 +头像子组件。 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 图片源URL | +| gender | string | - | 性别,'F'/'female'显示女性头像,'M'/'male'显示男性头像 | +| shape | 'circle' \| 'square' | 'circle' | 头像形状 | +| size | number | 100 | 头像大小 | +| width | number | - | 自定义宽度(与height一起使用时shape自动变为square) | +| height | number | - | 自定义高度 | +| gap | number | - | 头像与边框的间距 | +| icon | ReactNode | - | 自定义图标 | +| defaultAvatar | ReactNode | AvatarDefault图标 | 默认头像SVG | +| loading | ReactNode | Skeleton.Avatar | 加载中显示的内容 | +| error | ReactNode | PhotoFail图标 | 加载失败显示的内容 | +| apis | object | - | API配置对象 | + +--- + +### FileSystem + +文件系统浏览组件,提供图标、列表、分栏、画廊四种视图模式。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| onClick | function | - | 点击按钮的回调函数 | -| disabled | boolean | false | 是否禁用按钮 | -| children | ReactNode | - | 按钮内容 | +| items | array | - | 文件系统条目数组 | +| title | string | 'Files' | 标题,当路径为根目录时显示 | +| defaultView | 'icons' \| 'list' \| 'columns' \| 'gallery' | 'list' | 默认视图模式 | +| defaultPath | string | '' | 默认路径 | | className | string | - | 自定义类名 | +| onSelectionChange | function(entry) | - | 选中项变化回调 | +| onFileOpen | function(entry) | - | 打开文件回调 | +| renderFilePreview | function(entry) | - | 画廊视图中的文件预览渲染函数 | +| canPreviewFile | function(entry) | - | 判断文件是否可预览的函数 | + +#### items 数据项 + +| 字段 | 类型 | 描述 | +|------|------|------| +| kind | 'file' \| 'folder' | 条目类型 | +| path | string | 条目路径 | +| name | string | 显示名称(可选,默认从路径提取) | +| size | number | 文件大小(字节,仅file类型) | +| parentPath | string | 父路径(可选,默认从path提取) | + +--- ### PrintButton -打印按钮组件。 +打印按钮组件,触发浏览器打印功能。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| +| contentRef | Ref | - | 需要打印内容的ref引用 | +| content | ReactNode | - | 打印内容(备用) | | onBeforePrint | function | - | 打印前的回调函数 | -| onAfterPrint | function | - | 打印后的回调函数 | -| children | ReactNode | - | 按钮内容 | - -### 类型定义 - -#### FileType - -```typescript -interface FileType { - id: string; - name: string; - url?: string; - size?: number; - type?: string; - lastModified?: number; -} +| onSuccess | function | - | 打印成功的回调函数 | +| onError | function | - | 打印失败的回调函数 | +| printProps | object | - | 传递给react-to-print的属性 | +| ...antd Button props | - | - | 支持所有antd Button属性 | + +--- + +### withOSSFile + +高阶组件,通过OSS文件ID获取文件URL并注入到被包裹组件。 + +#### 用法 + +```jsx +const MyComponent = withOSSFile(({ data, id, ...props }) => { + // data 为获取到的文件URL + return
{data}
; +}); ``` -#### APIConfig +#### 被注入的属性 + +| 属性 | 类型 | 描述 | +|------|------|------| +| data | string | 获取到的文件URL | +| fetchApi | object | react-fetch的API对象 | +| id | string | 格式化后的文件ID(去除了查询参数) | + +--- + +### useFileUpload + +文件上传Hook,处理文件上传逻辑。 + +#### 参数 + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| multiple | boolean | true | 是否支持多文件 | +| fileSize | number | 100 | 单个文件最大尺寸(MB) | +| maxLength | number | 10 | 最大文件数量 | +| value | array | [] | 已上传文件列表 | +| concurrentCount | number | 1 | 上传并发数 | +| onAdd | function(file) | - | 文件添加后回调(上传前) | +| onError | function({ file, error, errMsg }) | - | 上传失败回调 | +| onSave | function(data, file, uuid) | - | 上传成功后数据处理回调 | +| onChange | function(list) | - | 文件列表变化回调 | +| onUpload | function({ file }) | - | 自定义上传函数 | + +#### 返回值 + +| 字段 | 类型 | 描述 | +|------|------|------| +| fileList | array | 正在上传中的文件列表 | +| onFileSelected | function(fileList) | 文件选择处理函数 | + +--- + +### useDownload + +文件下载Hook。 + +#### 参数 + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 直接的下载URL | +| filename | string | - | 下载后的文件名 | +| staticUrl | string | - | 静态资源URL前缀 | +| apis | object | - | 自定义API配置 | +| onError | function(error) | - | 下载失败回调 | +| onSuccess | function | - | 下载成功回调 | + +#### 返回值 + +| 字段 | 类型 | 描述 | +|------|------|------| +| isLoading | boolean | 是否正在下载 | +| download | function() | 触发下载 | +| ...react-fetch返回值 | - | 包含refresh等其他属性 | + +--- + +### downloadBlobFile + +Blob文件下载工具函数,支持URL字符串、Blob对象和远程URL下载。 + +#### 参数 -```typescript -interface APIConfig { - file?: { - staticUrl?: string; - upload?: string; - download?: string; - }; -} +```javascript +downloadBlobFile(input, filename, locale) ``` -### Hooks +| 参数 | 类型 | 描述 | +|------|------|------| +| input | string \| Blob | 下载源,支持blob URL、Blob对象或远程URL | +| filename | string | 下载后的文件名,默认'file' | +| locale | string | 国际化语言标识 | -#### useFileUpload +--- -用于处理文件上传逻辑的自定义Hook。 +### download -```typescript -const { - uploadFile, - isUploading, - progress, - error -} = useFileUpload(config); +文件下载工具函数,通过创建a标签触发下载。 + +#### 参数 + +```javascript +download(url, filename) ``` -#### useDownload +| 参数 | 类型 | 描述 | +|------|------|------| +| url | string | 下载URL | +| filename | string | 下载后的文件名 | + +--- -用于处理文件下载逻辑的自定义Hook。 +### computedAccept -```typescript -const { - download, - isDownloading, - error -} = useDownload(config); +计算接受的文件类型,将文件类型数组转为浏览器accept属性值。 + +#### 参数 + +```javascript +computedAccept(accept) ``` -### 工具函数 +| 参数 | 类型 | 描述 | +|------|------|------| +| accept | string \| string[] | 文件类型,如'.jpg,.png'或['.jpg', '.png'] | + +#### 返回值 -#### computedAccept +| 类型 | 描述 | +|------|------| +| string | 浏览器accept属性值 | -计算接受的文件类型。 +--- -```typescript -computedAccept(accept: string | string[]): string +### typeFormat + +根据文件扩展名获取文件类型标识。 + +#### 参数 + +```javascript +typeFormat(filename) ``` -#### typeFormatComponent +| 参数 | 类型 | 描述 | +|------|------|------| +| filename | string | 文件名 | + +#### 返回值 + +| 类型 | 描述 | +|------|------| +| string | 文件类型标识,如'image'/'pdf'/'docx'等 | + +--- + +### typeFormatComponent 根据文件名获取对应的预览组件。 -```typescript -typeFormatComponent(filename: string): React.ComponentType +#### 参数 + +```javascript +typeFormatComponent(filename) ``` +| 参数 | 类型 | 描述 | +|------|------|------| +| filename | string | 文件名 | + +#### 返回值 + +| 类型 | 描述 | +|------|------| +| React.ComponentType | 对应的预览组件 | + 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/api.md b/doc/api.md index 5b78938..3fa6acf 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,3 +1,21 @@ +### File + +文件组件,通过OSS文件ID获取文件URL,使用render props模式。内部使用`withOSSFile` HOC获取文件URL。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID,必填 | +| children | function({ url, ...props }) | - | render props函数,参数包含获取到的文件URL | +| apis | object | - | 自定义API配置,会与全局preset.apis合并 | +| loading | ReactNode | null | 加载中显示的内容 | +| error | ReactNode | - | 加载失败显示的内容 | +| ttl | number | 6000000 | 缓存过期时间(毫秒) | +| cacheName | string | 'file-cache' | 缓存名称 | + +--- + ### FileUpload 文件上传组件,支持单文件和多文件上传。 @@ -6,56 +24,97 @@ | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| accept | string | undefined | 接受的文件类型,如 ".jpg,.png,.pdf" | -| multiple | boolean | false | 是否支持多文件上传 | -| onChange | function | - | 文件选择后的回调函数,参数为选择的文件数组 | -| disabled | boolean | false | 是否禁用上传功能 | -| maxSize | number | - | 单个文件最大尺寸(字节) | -| children | ReactNode | - | 自定义上传按钮内容 | +| value | array | [] | 已上传文件列表(受控) | +| defaultValue | array | [] | 默认文件列表 | +| onChange | function(list) | - | 文件列表变化回调 | +| accept | string[] | ['.pdf','.jpg','.png','.jpeg','.doc','.docx','.xls','.xlsx','.html','.msg','.eml','.zip'] | 接受的文件类型 | +| multiple | boolean | true | 是否支持多文件上传 | +| fileSize | number | 100 | 单个文件最大尺寸(MB) | +| maxLength | number | 10 | 最大文件数量 | +| concurrentCount | number | 10 | 上传并发数 | +| size | string | - | 按钮尺寸,同antd Button的size | +| showUploadList | boolean | true | 是否显示已上传文件列表 | +| children | ReactNode | '文件上传' | 上传按钮文字 | +| renderTips | function(defaultTips, { fileSize, maxLength, accept }) | v => v | 自定义提示信息渲染 | +| onSave | function(data, file, uuid) | - | 上传成功后数据处理回调,返回值将作为新的文件对象 | +| ossUpload | function | - | 自定义上传函数 | +| getPermission | function(type) | - | 文件列表操作权限控制函数,type为'preview'/'delete'等 | +| apis | object | - | API配置对象 | +| renderModal | function(modalProps) | props => Modal | 自定义弹窗渲染函数 | -### MarkdownPreview +--- + +### FileInput -Markdown文件预览组件,支持渲染Markdown格式的文档。 +文件输入组件,提供文件选择按钮。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | Markdown文件的URL地址 | -| className | string | - | 自定义容器类名 | -| maxWidth | string/number | - | 容器最大宽度 | +| accept | string[] | defaultAccept | 接受的文件类型 | +| multiple | boolean | true | 是否支持多选 | +| buttonText | string | '文件上传' | 按钮文字 | +| onChange | function(fileList) | - | 文件选择后的回调,参数为File数组 | +| children | function({ children, ...props }) | - | 自定义按钮渲染函数 | +| disabled | boolean | false | 是否禁用 | -### ZipPreview +--- -ZIP压缩包文件预览组件,支持查看压缩包内部的文件列表和目录结构。 +### FilePreview + +文件预览组件,根据文件类型自动选择合适的预览方式。提供`src`时使用`TypePreview`,提供`id`时使用`OSSFilePreview`。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | ZIP文件的URL地址 | -| className | string | - | 自定义容器类名 | -| maxWidth | string/number | - | 容器最大宽度 | +| id | string | - | OSS文件ID | +| src | string | - | 文件URL | +| filename | string | - | 文件名,用于确定文件类型 | +| originName | string | - | 原始文件名,作为filename的备选 | +| apis | object | - | API配置对象 | -#### 功能特性 +#### 预览子组件 + +以下子组件均可单独使用: + +| 子组件 | 描述 | +|--------|------| +| ImagePreview | 图片预览 | +| AudioPreview | 音频预览 | +| VideoPreview | 视频预览 | +| PdfPreview | PDF预览 | +| DocxPreview | Word文档预览 | +| XlsxPreview | Excel预览 | +| OfficePreview | Office文档预览(根据文件类型选择DocxPreview或XlsxPreview) | +| HtmlPreview | HTML预览 | +| MarkdownPreview | Markdown预览 | +| JsonPreview | JSON预览 | +| ZipPreview | ZIP压缩包预览 | +| TextPreview | 文本文件预览 | +| UnknownPreview | 未知类型文件预览 | +| OSSFilePreview | OSS文件预览(通过ID获取URL后自动选择预览方式) | + +--- -- 支持解析ZIP压缩包内容 -- 以树形结构展示文件和目录 -- 显示文件大小和压缩后大小 -- 支持嵌套目录结构 -- 自动格式化文件大小(B、KB、MB、GB) +### MarkdownPreview -#### 支持的压缩格式 +Markdown文件预览组件。 -- `.zip` - ZIP压缩文件 -- `.rar` - RAR压缩文件 -- `.7z` - 7-Zip压缩文件 -- `.tar` - TAR归档文件 -- `.gz` - Gzip压缩文件 +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| url | string | - | Markdown文件的URL地址 | +| className | string | - | 自定义容器类名 | +| maxWidth | string/number | - | 容器最大宽度 | + +--- ### JsonPreview -JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示。 +JSON文件预览组件,使用@kne/json-view提供友好的JSON数据展示。 #### 属性 @@ -71,28 +130,21 @@ JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示 | indentWidth | number | 20 | 缩进宽度(像素) | | showLineNumbers | boolean | true | 是否显示行号 | -#### 功能特性 - -- 语法高亮:不同数据类型使用不同颜色 -- 层级控制:支持展开/收起,可指定从第几级开始收起 -- 搜索功能:基于 Fuse.js 的模糊搜索,关键字高亮 -- 双主题支持:白色和黑色两种主题 -- 复制功能:一键复制格式化的 JSON 数据 -- 行号显示:左侧显示行号 +--- -### FilePreview +### ZipPreview -文件预览组件,支持多种文件格式的预览。 +ZIP压缩包文件预览组件,支持查看压缩包内部的文件列表和目录结构。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | 文件URL | -| filename | string | - | 文件名,用于确定文件类型 | -| type | string | - | 文件类型,可选,如不提供则根据文件名推断 | -| staticUrl | string | - | 静态资源URL前缀 | -| apis | object | - | API配置对象 | +| url | string | - | ZIP文件的URL地址 | +| className | string | - | 自定义容器类名 | +| maxWidth | string/number | - | 容器最大宽度 | + +--- ### FileList @@ -102,25 +154,116 @@ JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| files | array | [] | 文件列表数据 | -| onDelete | function | - | 删除文件的回调函数 | -| onPreview | function | - | 预览文件的回调函数 | -| onDownload | function | - | 下载文件的回调函数 | -| renderItem | function | - | 自定义渲染文件项的函数 | +| dataSource | array | [] | 文件列表数据,每项包含 id/filename/date/userName/type 等 | +| getPermission | function(type, item) | () => true | 操作权限控制函数,type为'preview'/'edit'/'download'/'delete' | +| infoItemRenders | array | [用户名, 日期] | 信息列渲染函数数组 | +| onDelete | function(item) | - | 删除文件的回调函数 | +| onEdit | function(item) | - | 编辑文件的回调函数 | +| apis | object | - | API配置对象 | +| renderModal | function(modalProps) | props => Modal | 自定义弹窗渲染函数 | + +#### dataSource 数据项 + +| 字段 | 类型 | 描述 | +|------|------|------| +| id | string | 文件ID | +| filename | string | 文件名 | +| src | string | 文件URL | +| date | string | 上传日期(ISO格式) | +| userName | string | 上传用户名 | +| type | string | 'uploading'表示上传中 | +| uuid | string | 上传中的临时标识 | + +--- + +### FileListOptionButtons + +文件列表操作按钮组件,提供预览、编辑、下载、删除操作。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| item | object | - | 文件数据项 | +| hasPreview | boolean | true | 是否显示预览按钮 | +| getPermission | function(type, item) | () => true | 操作权限控制函数 | +| apis | object | {} | API配置对象 | +| onDelete | function(item) | - | 删除回调 | +| onEdit | function(item) | - | 编辑回调 | +| onPreview | function(item) | - | 预览回调 | +| renderModal | function(modalProps) | props => Modal | 自定义弹窗渲染函数 | + +--- ### Download -文件下载组件。 +文件下载按钮组件,基于antd Button封装。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| url | string | - | 下载文件的URL | -| filename | string | - | 下载后的文件名 | -| children | ReactNode | - | 触发下载的元素 | +| id | string | - | OSS文件ID,通过API获取下载URL | +| src | string | - | 直接的下载URL | +| filename | string | '未命名下载文件' | 下载后的文件名 | +| api | object | - | 自定义API配置 | | onSuccess | function | - | 下载成功的回调函数 | | onError | function | - | 下载失败的回调函数 | +| onClick | function | - | 点击按钮的回调函数 | +| ...antd Button props | - | - | 支持所有antd Button属性 | + +#### 静态方法 + +| 方法 | 描述 | +|------|------| +| Download.useDownload | useDownload Hook | +| Download.download | download(url, filename) 下载工具函数 | +| Download.downloadBlobFile | downloadBlobFile(input, filename, locale) Blob下载函数 | + +--- + +### FileButton + +文件操作按钮组件,点击后弹出文件预览弹窗。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 文件URL | +| filename | string | - | 文件名 | +| originName | string | - | 原始文件名,作为filename的备选 | +| title | string | - | 弹窗标题,默认使用filename | +| modalProps | object | - | 传递给弹窗的属性,如{ width: 800 } | +| openDownload | boolean | false | 弹窗中是否显示下载按钮 | +| openPrint | boolean | false | 弹窗中是否显示打印按钮(仅txt/pdf/image/html类型支持) | +| children | ReactNode \| function(filename) | - | 按钮内容,支持函数返回 | +| ...antd Button props | - | - | 支持所有antd Button属性(默认icon为LinkOutlined) | + +--- + +### FileModal + +文件预览弹窗组件。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 文件URL | +| filename | string | - | 文件名 | +| title | string | - | 弹窗标题 | +| open | boolean | - | 是否显示(受控) | +| defaultOpen | boolean | - | 默认是否显示 | +| onOpenChange | function(open) | - | 显示状态变化回调 | +| openDownload | boolean | false | 是否显示下载按钮 | +| openPrint | boolean | false | 是否显示打印按钮 | +| footer | ReactNode | null | 弹窗底部内容 | +| apis | object | - | API配置对象 | + +--- ### Image @@ -130,106 +273,259 @@ JSON文件预览组件,使用 @kne/json-view 提供友好的 JSON 数据展示 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| +| id | string | - | OSS文件ID,通过ID获取图片URL | | src | string | - | 图片源URL | | alt | string | - | 图片替代文本 | -| defaultAvatar | string | - | 默认头像类型('male'/'female'/'default') | -| onError | function | - | 图片加载失败的回调函数 | +| loading | ReactNode | Skeleton.Avatar | 加载中显示的内容 | +| error | ReactNode | PhotoFail图标 | 加载失败显示的内容 | | className | string | - | 自定义类名 | -| style | object | - | 自定义样式 | +| apis | object | - | API配置对象 | +| staticUrl | string | - | 静态资源URL前缀 | +| onClick | function | - | 点击事件 | +| children | function({ alt, className, src }) | - | 自定义渲染函数 | -### FileButton +#### Image.Avatar + +头像子组件。 + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 图片源URL | +| gender | string | - | 性别,'F'/'female'显示女性头像,'M'/'male'显示男性头像 | +| shape | 'circle' \| 'square' | 'circle' | 头像形状 | +| size | number | 100 | 头像大小 | +| width | number | - | 自定义宽度(与height一起使用时shape自动变为square) | +| height | number | - | 自定义高度 | +| gap | number | - | 头像与边框的间距 | +| icon | ReactNode | - | 自定义图标 | +| defaultAvatar | ReactNode | AvatarDefault图标 | 默认头像SVG | +| loading | ReactNode | Skeleton.Avatar | 加载中显示的内容 | +| error | ReactNode | PhotoFail图标 | 加载失败显示的内容 | +| apis | object | - | API配置对象 | -文件操作按钮组件。 +--- + +### FileSystem + +文件系统浏览组件,提供图标、列表、分栏、画廊四种视图模式。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| -| onClick | function | - | 点击按钮的回调函数 | -| disabled | boolean | false | 是否禁用按钮 | -| children | ReactNode | - | 按钮内容 | +| items | array | - | 文件系统条目数组 | +| title | string | 'Files' | 标题,当路径为根目录时显示 | +| defaultView | 'icons' \| 'list' \| 'columns' \| 'gallery' | 'list' | 默认视图模式 | +| defaultPath | string | '' | 默认路径 | | className | string | - | 自定义类名 | +| onSelectionChange | function(entry) | - | 选中项变化回调 | +| onFileOpen | function(entry) | - | 打开文件回调 | +| renderFilePreview | function(entry) | - | 画廊视图中的文件预览渲染函数 | +| canPreviewFile | function(entry) | - | 判断文件是否可预览的函数 | + +#### items 数据项 + +| 字段 | 类型 | 描述 | +|------|------|------| +| kind | 'file' \| 'folder' | 条目类型 | +| path | string | 条目路径 | +| name | string | 显示名称(可选,默认从路径提取) | +| size | number | 文件大小(字节,仅file类型) | +| parentPath | string | 父路径(可选,默认从path提取) | + +--- ### PrintButton -打印按钮组件。 +打印按钮组件,触发浏览器打印功能。 #### 属性 | 属性 | 类型 | 默认值 | 描述 | |------|------|--------|------| +| contentRef | Ref | - | 需要打印内容的ref引用 | +| content | ReactNode | - | 打印内容(备用) | | onBeforePrint | function | - | 打印前的回调函数 | -| onAfterPrint | function | - | 打印后的回调函数 | -| children | ReactNode | - | 按钮内容 | - -### 类型定义 - -#### FileType - -```typescript -interface FileType { - id: string; - name: string; - url?: string; - size?: number; - type?: string; - lastModified?: number; -} +| onSuccess | function | - | 打印成功的回调函数 | +| onError | function | - | 打印失败的回调函数 | +| printProps | object | - | 传递给react-to-print的属性 | +| ...antd Button props | - | - | 支持所有antd Button属性 | + +--- + +### withOSSFile + +高阶组件,通过OSS文件ID获取文件URL并注入到被包裹组件。 + +#### 用法 + +```jsx +const MyComponent = withOSSFile(({ data, id, ...props }) => { + // data 为获取到的文件URL + return
{data}
; +}); ``` -#### APIConfig +#### 被注入的属性 + +| 属性 | 类型 | 描述 | +|------|------|------| +| data | string | 获取到的文件URL | +| fetchApi | object | react-fetch的API对象 | +| id | string | 格式化后的文件ID(去除了查询参数) | + +--- -```typescript -interface APIConfig { - file?: { - staticUrl?: string; - upload?: string; - download?: string; - }; -} +### useFileUpload + +文件上传Hook,处理文件上传逻辑。 + +#### 参数 + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| multiple | boolean | true | 是否支持多文件 | +| fileSize | number | 100 | 单个文件最大尺寸(MB) | +| maxLength | number | 10 | 最大文件数量 | +| value | array | [] | 已上传文件列表 | +| concurrentCount | number | 1 | 上传并发数 | +| onAdd | function(file) | - | 文件添加后回调(上传前) | +| onError | function({ file, error, errMsg }) | - | 上传失败回调 | +| onSave | function(data, file, uuid) | - | 上传成功后数据处理回调 | +| onChange | function(list) | - | 文件列表变化回调 | +| onUpload | function({ file }) | - | 自定义上传函数 | + +#### 返回值 + +| 字段 | 类型 | 描述 | +|------|------|------| +| fileList | array | 正在上传中的文件列表 | +| onFileSelected | function(fileList) | 文件选择处理函数 | + +--- + +### useDownload + +文件下载Hook。 + +#### 参数 + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| id | string | - | OSS文件ID | +| src | string | - | 直接的下载URL | +| filename | string | - | 下载后的文件名 | +| staticUrl | string | - | 静态资源URL前缀 | +| apis | object | - | 自定义API配置 | +| onError | function(error) | - | 下载失败回调 | +| onSuccess | function | - | 下载成功回调 | + +#### 返回值 + +| 字段 | 类型 | 描述 | +|------|------|------| +| isLoading | boolean | 是否正在下载 | +| download | function() | 触发下载 | +| ...react-fetch返回值 | - | 包含refresh等其他属性 | + +--- + +### downloadBlobFile + +Blob文件下载工具函数,支持URL字符串、Blob对象和远程URL下载。 + +#### 参数 + +```javascript +downloadBlobFile(input, filename, locale) ``` -### Hooks +| 参数 | 类型 | 描述 | +|------|------|------| +| input | string \| Blob | 下载源,支持blob URL、Blob对象或远程URL | +| filename | string | 下载后的文件名,默认'file' | +| locale | string | 国际化语言标识 | -#### useFileUpload +--- -用于处理文件上传逻辑的自定义Hook。 +### download -```typescript -const { - uploadFile, - isUploading, - progress, - error -} = useFileUpload(config); +文件下载工具函数,通过创建a标签触发下载。 + +#### 参数 + +```javascript +download(url, filename) ``` -#### useDownload +| 参数 | 类型 | 描述 | +|------|------|------| +| url | string | 下载URL | +| filename | string | 下载后的文件名 | + +--- + +### computedAccept + +计算接受的文件类型,将文件类型数组转为浏览器accept属性值。 -用于处理文件下载逻辑的自定义Hook。 +#### 参数 -```typescript -const { - download, - isDownloading, - error -} = useDownload(config); +```javascript +computedAccept(accept) ``` -### 工具函数 +| 参数 | 类型 | 描述 | +|------|------|------| +| accept | string \| string[] | 文件类型,如'.jpg,.png'或['.jpg', '.png'] | + +#### 返回值 + +| 类型 | 描述 | +|------|------| +| string | 浏览器accept属性值 | + +--- + +### typeFormat -#### computedAccept +根据文件扩展名获取文件类型标识。 -计算接受的文件类型。 +#### 参数 -```typescript -computedAccept(accept: string | string[]): string +```javascript +typeFormat(filename) ``` -#### typeFormatComponent +| 参数 | 类型 | 描述 | +|------|------|------| +| filename | string | 文件名 | + +#### 返回值 + +| 类型 | 描述 | +|------|------| +| string | 文件类型标识,如'image'/'pdf'/'docx'等 | + +--- + +### typeFormatComponent 根据文件名获取对应的预览组件。 -```typescript -typeFormatComponent(filename: string): React.ComponentType +#### 参数 + +```javascript +typeFormatComponent(filename) ``` + +| 参数 | 类型 | 描述 | +|------|------|------| +| filename | string | 文件名 | + +#### 返回值 + +| 类型 | 描述 | +|------|------| +| React.ComponentType | 对应的预览组件 | diff --git a/doc/example.json b/doc/example.json index 89a1d0a..0fb6dad 100644 --- a/doc/example.json +++ b/doc/example.json @@ -195,6 +195,29 @@ "packageName": "@kne/remote-loader" } ] + }, + { + "title": "FileSystem", + "description": "文件系统浏览", + "code": "./file-system.js", + "scope": [ + { + "name": "_ReactFile", + "packageName": "@kne/current-lib_react-file", + "importStatement": "import * as _ReactFile from \"@kne/react-file\"" + }, + { + "packageName": "@kne/current-lib_react-file/dist/index.css" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "remoteLoader", + "packageName": "@kne/remote-loader" + } + ] } ] } diff --git a/doc/file-system.js b/doc/file-system.js new file mode 100644 index 0000000..36d7355 --- /dev/null +++ b/doc/file-system.js @@ -0,0 +1,55 @@ +const { FileSystem } = _ReactFile; +const { createWithRemoteLoader, getPublicPath } = remoteLoader; +const { Segmented } = antd; +const { useState } = React; + +const BaseExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:InfoPage'] +})(({ remoteModules }) => { + const [PureGlobal, InfoPage] = remoteModules; + const [items] = useState([ + { kind: 'folder', path: 'documents/', name: 'Documents' }, + { kind: 'folder', path: 'documents/reports/', name: 'Reports' }, + { kind: 'file', path: 'documents/reports/Q3-report.pdf', name: 'Q3-report.pdf', size: 1024000 }, + { kind: 'file', path: 'documents/reports/Q4-report.xlsx', name: 'Q4-report.xlsx', size: 512000 }, + { kind: 'file', path: 'documents/meeting-notes.docx', name: 'meeting-notes.docx', size: 256000 }, + { kind: 'folder', path: 'images/', name: 'Images' }, + { kind: 'file', path: 'images/logo.png', name: 'logo.png', size: 45000 }, + { kind: 'file', path: 'images/banner.jpg', name: 'banner.jpg', size: 89000 }, + { kind: 'folder', path: 'archives/', name: 'Archives' }, + { kind: 'file', path: 'archives/project-backup.zip', name: 'project-backup.zip', size: 15728640 }, + { kind: 'file', path: 'readme.md', name: 'readme.md', size: 3200 }, + { kind: 'file', path: 'config.json', name: 'config.json', size: 1200 } + ]); + + return ( + { + return { data: { code: 0, data: api.loader() } }; + }, + apis: { + file: { + staticUrl: getPublicPath('react-file') || window.PUBLIC_URL + } + } + }}> + + + { + console.log('Open file:', entry); + }} + onSelectionChange={entry => { + console.log('Selection changed:', entry); + }} + /> + + + + ); +}); + +render(); 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/doc/summary.md b/doc/summary.md index c8ce1e8..acc76ae 100644 --- a/doc/summary.md +++ b/doc/summary.md @@ -1,80 +1,41 @@ -这是一个功能丰富的React文件操作组件库,提供了文件上传、预览、下载等完整的文件处理解决方案。 +React文件操作组件库,提供文件上传、多格式预览、下载、列表管理及文件系统浏览的完整解决方案,支持国际化。 ### 主要特性 -- 🚀 **文件上传** - 支持单文件和多文件上传,可自定义文件类型限制 -- 👀 **文件预览** - 支持多种文件格式的预览,包括: - - 图片(PNG, JPG, GIF等) - - 音频文件 - - 视频文件 - - PDF文档 - - Office文档(Word, Excel, PowerPoint) - - HTML文件 - - 文本文件 -- 📥 **文件下载** - 支持文件直接下载和blob数据下载 -- 📋 **文件列表** - 提供文件列表展示和管理功能 -- 🖼️ **图片处理** - 专门的图片组件,支持默认头像和加载失败处理 -- 🖨️ **打印功能** - 内置打印按钮组件 +- **文件上传** - 支持单文件/多文件上传,可自定义文件类型、大小限制和并发数 +- **文件预览** - 根据文件类型自动选择预览方式,支持图片、音频、视频、PDF、Office文档、HTML、Markdown、JSON、ZIP、文本等 +- **文件下载** - 支持OSS文件ID下载、直接URL下载和Blob数据下载 +- **文件列表** - 展示已上传文件列表,支持上传中状态、预览、下载、删除操作 +- **文件系统** - 提供文件系统浏览组件,支持图标、列表、分栏、画廊四种视图模式 +- **图片处理** - 支持默认头像、性别区分头像、加载骨架屏和失败占位图 +- **打印功能** - 内置打印按钮组件,支持打印前/后回调 +- **国际化** - 内置中英文语言包,支持多语言切换 ### 组件概览 +#### File +文件组件,通过OSS文件ID获取文件URL,使用render props模式。 + #### FileUpload 文件上传组件,支持单文件和多文件上传,可自定义接受的文件类型和上传限制。 #### FilePreview -文件预览组件,根据文件类型自动选择合适的预览方式。支持本地文件URL和OSS文件ID两种方式。 +文件预览组件,根据文件类型自动选择合适的预览方式。支持本地URL和OSS文件ID两种方式。 #### FileList -文件列表组件,用于展示和管理已上传的文件,支持预览、下载等操作。 +文件列表组件,用于展示和管理已上传的文件,支持上传中状态、预览、下载和删除操作。 + +#### FileButton +文件操作按钮组件,点击后弹出文件预览弹窗,支持下载和打印。 #### Download -文件下载组件,支持多种下载方式,包括直接下载和blob数据下载。 +文件下载按钮组件,支持OSS文件ID下载和直接URL下载。 #### Image 图片组件,支持默认头像、性别区分的头像显示,以及加载失败时的占位图。 -#### FileButton -文件操作按钮组件,提供文件相关操作的统一交互界面。 +#### FileSystem +文件系统浏览组件,提供图标、列表、分栏、画廊四种视图,支持导航历史和搜索。 #### PrintButton -打印按钮组件,用于触发打印功能。 - - -### 快速开始 - -```jsx -import { FileUpload, FileList, FilePreview } from '@your-org/react-file-components'; - -// 文件上传示例 -const UploadExample = () => ( - console.log('Uploaded files:', files)} - /> -); - -// 文件预览示例 -const PreviewExample = () => ( - -); - -// 文件列表示例 -const ListExample = () => ( - console.log('Delete file:', id)} - /> -); -``` - -## 注意事项 - -1. 确保服务器端支持相应的文件上传和处理功能 -2. 文件预览功能可能需要额外的依赖或服务器支持(如Office文档预览) +打印按钮组件,用于触发浏览器打印功能。 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..0504f68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@kne/react-file", - "version": "0.1.36", - "description": "提供了文件上传,文件预览,文件批量管理等功能", + "version": "0.1.37", + "description": "React文件操作组件库,提供文件上传、多格式预览、下载、列表管理及文件系统浏览,支持i18n", "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": [ @@ -46,7 +41,21 @@ "type": "git", "url": "git+https://github.com/kne-union/react-file.git" }, - "keywords": [], + "keywords": [ + "react", + "file", + "upload", + "preview", + "download", + "file-system", + "image", + "avatar", + "antd", + "i18n", + "office", + "pdf", + "zip" + ], "author": "linzp", "license": "ISC", "bugs": { @@ -88,6 +97,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..8e7a1eb 100644 --- a/src/components/FileButton/FileModal.js +++ b/src/components/FileButton/FileModal.js @@ -1,17 +1,17 @@ -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'; +import previewStyle from '../FilePreview/style.module.scss'; export const useFileModalProps = p => { + const { formatMessage } = useIntl(); const { title, filename, originName, openDownload, openPrint, id, src, apis, ...props } = Object.assign( {}, { @@ -40,29 +40,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 +77,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 80% rename from src/components/FileButton/style.modules.scss rename to src/components/FileButton/style.module.scss index 57e3e2e..68a0cc1 100644 --- a/src/components/FileButton/style.modules.scss +++ b/src/components/FileButton/style.module.scss @@ -1,5 +1,8 @@ .file-modal-outer { min-height: 100px; + height: 70vh; + display: flex; + flex-direction: column; } .file-title { 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