一、组件介绍
本文介绍一个基于React + Ant Design实现的高服用组件预览与下载组件,支持图片,PDF,Office文档(Word,Excel,PPT)等常见文件类型。
二、适用场景
- 文件管理后台的文件预览与下载
- 富文本、附件、合同等场景的文件快速预览
- 支持多种文件类型的统一预览入口
- 需要高扩展性、可插拔的文件预览能力
三、核心实现思路
- 类型判断:通过工具函数判断文件类型(图片、视频、Office、PDF等)。
- 分发渲染:根据类型分发到不同的预览组件(如图片用 Antd Image,PDF 用 pdfjs 渲染,Office 用微软在线预览)。
- 下载能力:集成 file-saver 实现一键下载。
- 高扩展性:类型判断、渲染逻辑均可扩展,便于后续支持更多类型。
- 注释友好:关键逻辑均有中文注释,便于团队协作和二次开发。
四、关键代码实现
1. 实现文件类型判断工具
js
import { UploadFile } from 'antd';
/**
* 判断是否为图片
* @param file 上传的文件对象
* @returns true 表示是图片,false 表示不是
*/
export const isImageUrl = (file: UploadFile): boolean => {
if (file.type && !file.thumbUrl) {
return isImageFileType(file.type);
}
const url: string = (file.thumbUrl || file.url || '') as string;
const extension = extname(url);
if (/^data:image\//.test(url) || /(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico|heic|heif)$/i.test(extension)) {
return true;
}
if (/^data:/.test(url)) {
// other file types of base64
return false;
}
if (extension) {
// other file types which have extension
return false;
}
return true;
};
/**
* 判断是否为视频
* @param file 上传的文件对象
* @returns true 表示是视频文件,false 表示不是
*/
export const isVideoFileType = (type: string): boolean => type.indexOf('video/') === 0;
export const isVideoUrl = (file: UploadFile): boolean => {
if (file.type && !file.thumbUrl) {
return isVideoFileType(file.type);
}
const url: string = (file.thumbUrl || file.url || '') as string;
const extension = extname(url.toLowerCase()); // 转换为小写以进行不区分大小写的比较
if (
/^data:video\//.test(url) ||
/\.(mp4|avi|mov|mkv|wmv|flv|webm|m4v|mpeg|mpg)$/i.test(extension)
) {
return true;
}
if (/^data:/.test(url)) {
// 其他非视频类型的base64数据
return false;
}
if (extension) {
// 其他有扩展名的文件类型
return false;
}
return true;
};
/**
* 判断文件是否为微软 Office 文件
* @param file 上传的文件对象
* @returns true 表示是 Office 文件,false 表示不是
*/
export const isMicrosoftOfficeFile = (file: UploadFile): boolean => {
if (file.type && !file.thumbUrl) {
// 通过 MIME 类型判断文件类型
return isMicrosoftOfficeMimeType(file.type);
}
const url: string = (file.thumbUrl || file.url || '') as string;
const extension = extname(url.toLowerCase()); // 转换为小写以进行不区分大小写的比较
// 通过文件扩展名判断文件类型
if (
/\.(docx|xlsx|pptx|doc|xls|ppt|dotx|xltx|potx)$/i.test(extension)
) {
return true;
}
// 判断文件是否为 base64 编码的微软文件(一般不会是这种情况,但可以作为补充)
if (/^data:/.test(url)) {
return false;
}
return false;
};
/**
* 判断是否为PDF文件
* @param file 上传的文件对象
* @returns true 表示是PDF文件,false 表示不是
*/
export const isPdfFile=(url: string)=> {
return url?.toLowerCase().endsWith('.pdf');
}
2. 预览组件核心实现
js
import { Drawer, UploadFile, Image } from "antd";
import * as pdfjsLib from 'pdfjs-dist/build/pdf';
import React, { forwardRef, useImperativeHandle, useState, useEffect } from "react";
import { isImageUrl, isVideoUrl, isMicrosoftOfficeFile, isPdf } from "@/utils/tools/file";
import Video, { VideoPlayerModalRef } from "./components/VideoPlayer";
import fileSaver from "file-saver";
/**
* Office 文件在线预览
*/
const OfficeFilePreview: React.FC<{ url: string }> = ({ url }) => {
const office_prefix = `https://view.officeapps.live.com/op/embed.aspx?src=`;
return (
<iframe
title="Office File Preview"
src={`${office_prefix}${url}`}
width="100%"
height="100%"
style={{ border: 0 }}
/>
);
};
/**
* PDF 文件预览容器
*/
const PdfFilePreview: React.FC<{ url: string; containerRef: React.RefObject<HTMLDivElement> }> = ({ url, containerRef }) => {
return <div ref={containerRef} style={{ width: '100%' }} />;
};
/**
* 通用文件预览与下载组件
*/
export const Preview = forwardRef<FilePreviewDownloadRef, FilePreviewDownloadProps>((props, ref) => {
const { children, width = 960, title = '预览文件', closeText = '关闭' } = props
const [visible, setVisible] = useState(false)
const [imagePreview, setImagePreview] = useState<boolean>(false)
const [url, setUrl] = useState<string>('')
const pdfContainer = React.useRef<HTMLDivElement>(null)
const videoRef = React.useRef<VideoPlayerModalRef>(null)
pdfjsLib.GlobalWorkerOptions.workerSrc = '//static.xyb2b.com/public/pdfjs/3.6.172/pdf.worker.js';
// PDF 渲染逻辑
const onPdfPreview = async () => {
try {
const pdf = await pdfjsLib.getDocument(url).promise;
const numPages = pdf.numPages;
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const scale = 1.5;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.setAttribute("id", "the-canvas" + pageNum);
canvas.height = viewport.height;
canvas.width = viewport.width;
// 优先渲染容器防止乱序
pdfContainer.current?.appendChild(canvas);
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
await page.render(renderContext).promise;
}
} catch (error) {
console.log('error', error)
}
}
// 预览入口
const onPreview = (file: UploadFile) => {
const { url } = file || {}
if (!url) return
if (isImageUrl(file)) {
setImagePreview(true);
setUrl(url)
} else if (isVideoUrl(file)) {
videoRef.current?.open(url);
} else {
setUrl(url)
setVisible(true)
}
}
// 下载入口
const onDownload = (file: UploadFile, setLoading?: (loading: boolean) => void) => {
if (setLoading) setLoading(true);
try {
const { url, name } = file || {}
fileSaver.saveAs(url as string, name);
if (setLoading) setLoading(false);
} catch (error) {
throw new Error('下载失败: ' + error);
}
}
useImperativeHandle(ref, () => ({
onPreview,
onDownload
}), [url])
/**
* 渲染文件内容,根据类型分发
*/
const RenderFileContent = useMemo(() => {
const fakeFile = { url } as any;
if (isMicrosoftOfficeFile(fakeFile)) {
return <OfficeFilePreview url={url} />;
}
if (isPdfFile(url)) {
return <PdfFilePreview url={url} containerRef={pdfContainer} />;
}
return <div>暂不支持该类型文件预览</div>;
}, [url]);
const clearPdfContainer = () => {
if (pdfContainer.current) {
pdfContainer.current.innerHTML = '';
}
};
useEffect(() => {
const isRenderPDF = url && isPdfFile(url) && visible
if (isRenderPDF) {
clearPdfContainer()
onPdfPreview()
}
// 关闭时清理
return () => clearPdfContainer();
}, [url, visible])
return (
<>
{visible && <Drawer
title={title}
width={width}
visible={visible}
onClose={() => setVisible(false)}
destroyOnClose
>
<RenderFileContent />
</Drawer>}
<Image
height={0}
width={0}
src={url}
preview={{
visible: imagePreview,
}}
onPreviewClose={() => setImagePreview(false)}
/>
<Video.Modal ref={videoRef as any} />
{children ?? <div>{children}</div>}
</>)
})
五、使用方式
import
import { UploadFile } from 'antd';
const file: UploadFile = {
url: 'https://yourdomain.com/yourfile.pdf',
name: 'yourfile.pdf',
...自定义属性
};
const previewRef = useRef<FilePreviewDownloadRef>(null);
previewRef.current?.onPreview(file);
previewRef.current?.onDownload(file);