From 6c80239d45348877fcca364745d8a576d642ae65 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Mon, 29 Jun 2026 17:00:21 +0800 Subject: [PATCH] feat(createpdfdownload): add PDF export DOM helper --- docs/api/functions/createPDFDownload.md | 58 ++++++++++++++ docs/api/globals.md | 1 + docs/api/typedoc-sidebar.json | 6 +- src/createPDFDownload/__test__/index.test.ts | 75 +++++++++++++++++ src/createPDFDownload/index.ts | 84 ++++++++++++++++++++ src/index.ts | 1 + 6 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 docs/api/functions/createPDFDownload.md create mode 100644 src/createPDFDownload/__test__/index.test.ts create mode 100644 src/createPDFDownload/index.ts diff --git a/docs/api/functions/createPDFDownload.md b/docs/api/functions/createPDFDownload.md new file mode 100644 index 0000000..b7f3de9 --- /dev/null +++ b/docs/api/functions/createPDFDownload.md @@ -0,0 +1,58 @@ +[dt-utils](../globals.md) / createPDFDownload + +# Function: createPDFDownload() + +> **createPDFDownload**(`container`, `options`): `HTMLElement` \| `null` + +Defined in: [createPDFDownload/index.ts:50](https://github.com/DTStack/dt-utils/blob/master/src/createPDFDownload/index.ts#L50) + +创建用于 PDF 下载的 DOM 副本,并移除报告内容中的滚动条。 + +## Parameters + +### container + +`HTMLElement` \| `null` \| `undefined` + +需要导出的报告根节点 + +### options + +`CreatePDFDownloadOptions` = `{}` + +导出 DOM 配置 + +#### options.width + +`number` \| `string` + +导出根节点宽度。默认使用原始节点的 `getBoundingClientRect().width`。 + +#### options.maxWidth + +`string` + +导出根节点最大宽度。默认移除 `max-width` 限制,避免离屏截图时内容被再次收窄。 + +## Returns + +`HTMLElement` \| `null` + +可交给 PDF 导出工具的 DOM 副本 + +## Description + +PDF 导出通常会将页面 DOM 转成图片再写入 PDF。如果直接复用预览区域,内部组件的固定高度和 `overflow: auto/scroll` 会被一起截图,导致导出的 PDF 出现滚动条。 + +该函数会 clone 一份独立 DOM,展开其中的滚动容器,并保留原始节点宽度。原始 DOM 不会被修改。 + +## Example + +```typescript +import { createPDFDownload } from '@dtinsight/dt-utils'; + +exportPDF({ + filename: '分析报告', + getExportDom: () => createPDFDownload(reportRef.current), +}); +``` diff --git a/docs/api/globals.md b/docs/api/globals.md index 6f98284..ea4616c 100644 --- a/docs/api/globals.md +++ b/docs/api/globals.md @@ -10,6 +10,7 @@ ## Utils - [copy](functions/copy.md) +- [createPDFDownload](functions/createPDFDownload.md) - [downloadFile](functions/downloadFile.md) - [generateUniqueId](functions/generateUniqueId.md) - [generateUrlWithQuery](functions/generateUrlWithQuery.md) diff --git a/docs/api/typedoc-sidebar.json b/docs/api/typedoc-sidebar.json index ef46e2b..79116e3 100644 --- a/docs/api/typedoc-sidebar.json +++ b/docs/api/typedoc-sidebar.json @@ -29,6 +29,10 @@ "text": "copy", "link": "/api/functions/copy.md" }, + { + "text": "createPDFDownload", + "link": "/api/functions/createPDFDownload.md" + }, { "text": "downloadFile", "link": "/api/functions/downloadFile.md" @@ -149,4 +153,4 @@ } ] } -] \ No newline at end of file +] diff --git a/src/createPDFDownload/__test__/index.test.ts b/src/createPDFDownload/__test__/index.test.ts new file mode 100644 index 0000000..90652a5 --- /dev/null +++ b/src/createPDFDownload/__test__/index.test.ts @@ -0,0 +1,75 @@ +import createPDFDownload from '../index'; + +describe('createPDFDownload', () => { + test('return null when container is empty', () => { + expect(createPDFDownload(null)).toBeNull(); + expect(createPDFDownload(undefined)).toBeNull(); + }); + + test('clone dom without changing source element', () => { + const container = document.createElement('div'); + container.innerHTML = '

报告内容

'; + + const exportDom = createPDFDownload(container); + + expect(exportDom).not.toBe(container); + expect(exportDom?.innerHTML).toBe(container.innerHTML); + expect(container.getAttribute('style')).toBeNull(); + }); + + test('expand scroll containers in cloned dom', () => { + const container = document.createElement('div'); + container.innerHTML = ` +
+
报告内容
+
+ `; + + const exportDom = createPDFDownload(container); + const sourceSection = container.querySelector('.report-section'); + const exportSection = exportDom?.querySelector('.report-section'); + const exportContent = exportDom?.querySelector('.report-content'); + + expect(sourceSection?.style.overflow).toBe('auto'); + expect(sourceSection?.style.height).toBe('120px'); + expect(exportSection?.style.height).toBe('auto'); + expect(exportSection?.style.maxHeight).toBe('none'); + expect(exportSection?.style.overflow).toBe('visible'); + expect(exportSection?.style.overflowX).toBe('visible'); + expect(exportSection?.style.overflowY).toBe('visible'); + expect(exportContent?.style.height).toBe('auto'); + expect(exportContent?.style.overflowY).toBe('visible'); + }); + + test('set root width from container rect by default', () => { + const container = document.createElement('div'); + jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({ + width: 742, + height: 100, + top: 0, + right: 742, + bottom: 100, + left: 0, + x: 0, + y: 0, + toJSON: jest.fn(), + }); + + const exportDom = createPDFDownload(container); + + expect(exportDom?.style.width).toBe('742px'); + expect(exportDom?.style.maxWidth).toBe('none'); + }); + + test('support custom root width and max width', () => { + const container = document.createElement('div'); + + const exportDom = createPDFDownload(container, { + width: '100%', + maxWidth: '960px', + }); + + expect(exportDom?.style.width).toBe('100%'); + expect(exportDom?.style.maxWidth).toBe('960px'); + }); +}); diff --git a/src/createPDFDownload/index.ts b/src/createPDFDownload/index.ts new file mode 100644 index 0000000..437a6bb --- /dev/null +++ b/src/createPDFDownload/index.ts @@ -0,0 +1,84 @@ +export interface CreatePDFDownloadOptions { + /** + * 导出根节点宽度。默认使用原始节点的 getBoundingClientRect().width。 + */ + width?: number | string; + /** + * 导出根节点最大宽度。默认移除 max-width 限制,避免离屏截图时内容被再次收窄。 + */ + maxWidth?: string; +} + +const SCROLL_OVERFLOW_REGEXP = /auto|scroll/; + +function isScrollContainer(element: HTMLElement) { + const { overflow, overflowX, overflowY } = window.getComputedStyle(element); + return [overflow, overflowX, overflowY].some((value) => SCROLL_OVERFLOW_REGEXP.test(value)); +} + +function expandScrollContainer(element: HTMLElement) { + element.style.setProperty('height', 'auto', 'important'); + element.style.setProperty('max-height', 'none', 'important'); + element.style.setProperty('overflow', 'visible', 'important'); + element.style.setProperty('overflow-x', 'visible', 'important'); + element.style.setProperty('overflow-y', 'visible', 'important'); +} + +/** + * 创建用于 PDF 下载的 DOM 副本,并移除报告内容中的滚动条。 + * + * @category Utils + * @description + * PDF 导出通常会将页面 DOM 转成图片再写入 PDF。如果直接复用预览区域, + * 内部组件的固定高度和 `overflow: auto/scroll` 会被一起截图,导致导出的 PDF + * 出现滚动条。该函数会 clone 一份独立 DOM,展开其中的滚动容器,并保留原始节点宽度。 + * + * @param {HTMLElement | null | undefined} container - 需要导出的报告根节点 + * @param {CreatePDFDownloadOptions} options - 导出 DOM 配置 + * @returns {HTMLElement | null} 可交给 PDF 导出工具的 DOM 副本 + * + * @example + * ```typescript + * import { createPDFDownload } from '@dtinsight/dt-utils'; + * + * exportPDF({ + * filename: '分析报告', + * getExportDom: () => createPDFDownload(reportRef.current), + * }); + * ``` + */ +const createPDFDownload = ( + container?: HTMLElement | null, + options: CreatePDFDownloadOptions = {} +): HTMLElement | null => { + if (!container) return null; + + const exportDom = container.cloneNode(true) as HTMLElement; + const sourceElements = [container].concat( + Array.from(container.querySelectorAll('*')) + ); + const exportElements = [exportDom].concat( + Array.from(exportDom.querySelectorAll('*')) + ); + + sourceElements.forEach((sourceElement, index) => { + const exportElement = exportElements[index]; + if (!exportElement || !isScrollContainer(sourceElement)) return; + + expandScrollContainer(exportElement); + }); + + const width = + options.width === undefined ? container.getBoundingClientRect().width : options.width; + if (typeof width === 'number') { + exportDom.style.width = `${width}px`; + } else if (width) { + exportDom.style.width = width; + } + + exportDom.style.maxWidth = options.maxWidth || 'none'; + + return exportDom; +}; + +export default createPDFDownload; diff --git a/src/index.ts b/src/index.ts index f2e2ae9..698d1c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export * as lodash from 'lodash-es'; export { default as checkBrowserSupport } from './checkBrowserSupport'; export { default as Cookies } from './cookies'; export { default as copy } from './copy'; +export { default as createPDFDownload } from './createPDFDownload'; export { default as downloadFile } from './downloadFile'; export { default as formatBytes } from './formatBytes'; export { DateTimeFormat, default as formatDateTime } from './formatDateTime';