自建”IT兵器库”,你值得一看!第三篇

765 阅读3分钟

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!

常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。

接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步

需求:

  • 根据用户表单内容填写,完成实时预览功能
  • 支持 以pdf格式导出打印文件

效果展示

表单数据填写

image.png

一键预览效果

image.png

支持导出与打印

image.png

导出

image.png

导出文件查看

image.png

打印

image.png

功能点划分

  • 表单内容转成文件模板样式
  • 文件模板支持导出
  • 文件模板支持打印

使用到的第三方类库(react 版本哈)

前两个类库是 解决前端Html生成pdf

  • jspdf
  • html2canvas

这个实现打印

  • react-to-print

后续还需要预览一些附件(.pdf文件)

  • 其实预览 就是 把文件下载 用电脑打开
  • 可以考虑采用iframe或者<a />

参考文章

组件区域划分

  • 头部内容区域
    • 返回按钮
    • 文件名
    • 功能按钮 导出 打印
  • 文件回显区域
    • 表格渲染

组件最终支持配置项

  • 预览组件:外部关闭回调,外部表单填写内容
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

组件布局

<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 
            style={{ width: '100%', height: '100%' }} 
            title="PDF文件" 
            frameBorder="0" 
            id="myIframe" 
            src={fileUrl} // 传入文件url即可
       />

结束

喜欢就点个赞吧,坐等收藏吃灰!!

上一篇

下一篇