现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!
常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。
接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步。
需求:
- 根据用户表单内容填写,完成
实时预览功能 - 支持
以pdf格式导出、打印文件
效果展示
表单数据填写
一键预览效果
支持导出与打印
导出
导出文件查看
打印
功能点划分
- 表单内容转成文件模板样式
- 文件模板支持导出
- 文件模板支持打印
使用到的第三方类库(react 版本哈)
前两个类库是 解决前端Html生成pdf
jspdfhtml2canvas
这个实现打印
react-to-print
后续还需要预览一些附件(.pdf文件)
其实预览 就是 把文件下载 用电脑打开- 可以考虑采用
iframe或者<a />
参考文章
- 前端几种下载文件方式 blog.csdn.net/Selina_lxh/…
- 前端html转pdf blog.csdn.net/qq_29184685…
组件区域划分
- 头部内容区域
-
- 返回按钮
-
- 文件名
-
- 功能按钮 导出 打印
- 文件回显区域
-
- 表格渲染
组件最终支持配置项
- 预览组件:外部关闭回调,外部表单填写内容
type PreviewProps = {
onClose: () => void
formData: TableDataProps
}
- 文件回显:...需要填充的表单内容
type TableDataProps = {
headerTitle?: string
title?: string
subTitle?: string
changeDesignBookCode: string
fillItems?: Array<{ name: string; label: string }>
tableData: TdPropType[]
}
- 每一行配置项:文本、值、宽度、是否存在孩子、类型
type TdPropType = {
label: string
value?: string
labelWidth?: string
children?: TdPropType[]
type?: string
fileType?: string
linkItems?: TdPropType[]
}
测试数据模板
const {
headerTitle = 'pdf测试文件',
title = 'pdf测试文件title',
subTitle = 'pdf测试文件code',
changeDesignBookCode = '--',
fillItems = [
{ label: '填充字段一', name: 'text' },
{ label: '填充字段二', name: 'text' },
{ label: '填充字段三', name: 'text' },
],
tableData = [
{ label: '第一列', value: 'text' },
{ label: '第一列', value: 'text' },
{ label: '第一列', value: 'text' },
{ label: '第一列', value: 'text' },
{ label: '第一列', value: 'text' },
{
label: '嵌套子列',
children: [
{ label: '嵌套子列-1', value: 'text', labelWidth: '150px' },
{ label: '嵌套子列-11', value: 'text', labelWidth: '150px' },
{ label: '嵌套子列-12', value: 'text', labelWidth: '150px' },
{ label: '嵌套子列-122', value: 'text', labelWidth: '150px' },
{ label: '嵌套子列-1222', value: 'text', labelWidth: '150px' },
{ label: '嵌套子列-1123', value: 'text', labelWidth: '150px' },
],
},
{
label: '嵌套子列-1222244',
type: 'link',
linkItems: [],
},
],
} = formData
组件布局
- 这里使用了Tailwind CSS
<div>
<header className="flex justify-between items-center p-4">
<div>
// 返回Icon
<LeftOutlined className="align-middle text-xl font-bold mr-4" onClick={onClose} />
// 文件名
<span>{`${headerTitle || 'XXXX'}.pdf`}</span>
</div>
// 功能按钮区域
<div className="flex">
<Button type="link" className="cursor-pointer" onClick={exportPdf}>
<DownloadOutlined />
导出
</Button>
<Button type="link" className="cursor-pointer" onClick={handlePrint}>
<PrinterOutlined />
打印
</Button>
</div>
</header>
// 文件回显区域
<main className="flex justify-center mt-2 rounded w-full items-center wrapper" ref={wrapperRef}>
<div className="bg-white py-4 px-12 w-2/5 text-center content" ref={contentRef}>
<h3>{title}</h3>
<span>{`${subTitle}: ${changeDesignBookCode}`}</span>
// 表格头部字段填充
<section className="flex justify-between mt-8">
{fillItems?.map(item => {
return (
<span key={item.name}>
{item.label}:<span style={{ display: 'inline-block', width: '120px' }} />
</span>
)
})}
</section>
// 表格内容渲染
<table className="table w-full border-collapse border border-black border-solid border-x-0 border-b-0">
<tbody>{bodyData}</tbody>
</table>
</div>
</main>
// pdf 预览文件
<FilePreview {...currentFile} />
</div>
组件-表格生成器
const generateItem = (data: TdPropType[]) => {
return data?.map((item, index) => {
if (item.type === TextType.LINK) {
return (
<tr key={item.label} className="flex items-center border border-black border-solid">
<td className="px-2 w-1/4 ">{item.label}</td>
<td className="flex-1 border border-black border-solid border-y-0 border-r-0">
<div className="flex flex-col attachment justify-center items-center">
{item?.linkItems?.map(ele => {
return (
<Button type="link" onClick={() => openPreview(ele)} key={item.value}>
{ele.label || ''}
</Button>
)
})}
</div>
</td>
</tr>
)
}
if (item.children) {
return (
<tr key={item.label} className="flex items-center border border-black border-solid border-b-0 border-r-0">
<td className="px-2 w-1/4">{item.label}</td>
<td className="flex-1 border border-black border-y-0 p-0">{generateItem(item.children)}</td>
</tr>
)
}
return (
<tr key={item.label} className="flex items-center">
<td
className={`px-2 border border-black border-solid py-2 border-r-0 border-b-0 ${!index && 'border-t-0'}`}
style={{ width: item.labelWidth ? item.labelWidth : '25%' }}
>
{item.label}
</td>
<td className={`flex-1 border border-black border-solid py-2 border-b-0 ${!index && 'border-t-0'}`}>
{item.value || '--'}
</td>
</tr>
)
})
}
组件样式
position: fixed;
z-index: 10000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(245, 245, 245, 1);
header {
border-bottom: 2px solid lightgray;
}
main {
min-width: 1440px;
.attachment {
min-height: 120px;
}
}
组件状态-定义
const [currentFile, setCurrentFile] = useState<DataType>()
const [bodyData, setBodyData] = useState([])
const wrapperRef = useRef(null)
const contentRef = useRef(null)
功能点逐一拆解
表单内容转成文件模板样式
- 首先,需要拿到用户填写的内容啦,通过props注入
- 其次,就是完成数据渲染
- 核心在于:我们表格如何回显?? `html功底啦
const generateItem = (data: TdPropType[]) => {
// 根据数据传进来的格式 遍历每一项
return data?.map((item, index) => {
// 这里是业务场景当中:存在上传的附件 需要特殊处理
// 判断每一项属于什么类型
if (item.type === TextType.LINK) {
return (
<tr key={item.label} className="flex items-center border border-black border-solid">
// 每一列的字段 item.label
<td className="px-2 w-1/4 ">{item.label}</td>
<td className="flex-1 border border-black border-solid border-y-0 border-r-0">
// 这里就是把附件列表 给展示出来
<div className="flex flex-col attachment justify-center items-center">
{item?.linkItems?.map(ele => {
return (
<Button type="link" onClick={() => openPreview(ele)} key={item.value}>
{ele.label || ''}
</Button>
)
})}
</div>
</td>
</tr>
)
}
// 如果存在孩子 递归处理 如果不明白,结合效果图一起看看哈
// 父级/子级-value/子级1-value...
if (item.children) {
return (
<tr key={item.label} className="flex items-center border border-black border-solid border-b-0 border-r-0">
<td className="px-2 w-1/4">{item.label}</td>
<td className="flex-1 border border-black border-y-0 p-0">{generateItem(item.children)}</td>
</tr>
)
}
// 否则,就正常返回 文本-value形式
return (
<tr key={item.label} className="flex items-center">
<td
className={`px-2 border border-black border-solid py-2 border-r-0 border-b-0 ${!index && 'border-t-0'}`}
style={{ width: item.labelWidth ? item.labelWidth : '25%' }}
>
{item.label}
</td>
<td className={`flex-1 border border-black border-solid py-2 border-b-0 ${!index && 'border-t-0'}`}>
{item.value || '--'}
</td>
</tr>
)
})
}
文件模板支持导出
- 导出核心:
html转化成canvas - 并且生成文件,通过浏览器下载
- 借助
html2canvas第三方类库 - 以下代码CV即可,我也是哈哈哈
- blog.csdn.net/qq_29184685…
const exportPdf = async () => {
// 配置项
const options = {
useCORS: true, // 使用跨域
}
// 需要传:导出内容(ref绑定一些丢给他),配置项
html2canvas(contentRef.current, options).then(canvas => {
const contentWidth = canvas.width
const contentHeight = canvas.height
// 一页pdf显示html页面生成的canvas高度
const pageHeight = (contentWidth / 592.28) * 841.89
// 未生成pdf的html页面高度
let leftHeight = contentHeight
// 页面偏移
let position = 0
// a4纸的尺寸[595.28,841.89] html页面生成的canvas在pdf的宽高
const imgWidth = 595.28
const imgHeight = (592.28 / contentWidth) * contentHeight
// 获取图片的base64数据
const pageData = canvas.toDataURL('image/jpeg', 1.0)
const PDF = new JsPdf('p', 'pt', 'a4')
if (leftHeight < pageHeight) {
PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
} else {
// 分页
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= 841.89
if (leftHeight > 0) {
PDF.addPage()
}
}
}
// 下载pdf
PDF.save(`${changeDesignBookCode}-${title}.pdf`)
})
}
文件模板支持打印
- 直接借助第三方类库
react-to-print
const handlePrint = useReactToPrint({
content: () => wrapperRef.current, // 打印内容 ref绑定一下塞
bodyClass: 'print-box', // 随便给个类名
documentTitle: `${Math.random()}-XXX建议表`,
})
附件预览功能
- 借助
iframe完成pdf文件预览 - blog.csdn.net/Selina_lxh/…
<iframe
style={{ width: '100%', height: '100%' }}
title="PDF文件"
frameBorder="0"
id="myIframe"
src={fileUrl} // 传入文件url即可
/>
结束
喜欢就点个赞吧,坐等收藏吃灰!!