From ec5d61c94adf3abf52f724dc15e5c320555dc9be Mon Sep 17 00:00:00 2001 From: wlz Date: Wed, 24 Jun 2026 12:07:58 +0800 Subject: [PATCH 1/2] fix(irify): export full report content when downloading Word Wait for all paginated report items to render via flushSync and useLayoutEffect before capturing DOM for Word export, so multi-page reports include content beyond the first preview page. Co-authored-by: Cursor --- .../pages/assetViewer/ReportViewerPage.tsx | 76 +++++++++++++------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx b/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx index 32a12e54f4..405e5d85de 100644 --- a/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx +++ b/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { flushSync } from 'react-dom' import { YakitResizeBox } from '@/components/yakitUI/YakitResizeBox/YakitResizeBox' import { YakitCard } from '@/components/yakitUI/YakitCard/YakitCard' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' @@ -417,9 +418,24 @@ const ReportViewer: React.FC = (props) => { const [allReportItems, setAllReportItems] = useState([]) const [current, setCurrent] = useState(1) const [reportItems, setReportItems] = useState([]) + // Word 导出时临时渲染全量报告项(预览分页仅用于展示,不应限制导出范围) + const [wordExportItems, setWordExportItems] = useState(null) + const displayReportItems = wordExportItems ?? reportItems const divRef = useRef(null) + const wordExportRenderReadyRef = useRef<(() => void) | null>(null) const [downloadLoading, setDownloadLoading] = useState(false) + useLayoutEffect(() => { + if (!wordExportItems?.length) return + const frame = requestAnimationFrame(() => { + requestAnimationFrame(() => { + wordExportRenderReadyRef.current?.() + wordExportRenderReadyRef.current = null + }) + }) + return () => cancelAnimationFrame(frame) + }, [wordExportItems]) + useEffect(() => { if ((reportId || 0) <= 0) { setReport({ @@ -449,6 +465,11 @@ const ReportViewer: React.FC = (props) => { const newReportItems = truncateArrayBySize(items, 90) // 每页90KB setAllReportItems(newReportItems) setReportItems(newReportItems[0]) + setCurrent(1) + } else { + setAllReportItems([]) + setReportItems([]) + setCurrent(1) } } catch (e) { yakitNotify('error', `Parse Report[${props.reportId}]'s items failed`) @@ -510,22 +531,16 @@ const ReportViewer: React.FC = (props) => { ) } - // 下载Word - const downloadWord = () => { - if (!divRef || !divRef.current) return - setDownloadLoading(true) - // 此处定时器为了确保已处理其余任务 - setTimeout(() => { - exportToWord() - }, 300) - } - // 下载报告 - const exportToWord = async () => { - if (!divRef || !divRef.current) return + const waitForWordExportDom = () => + new Promise((resolve) => { + wordExportRenderReadyRef.current = resolve + }) + + const captureWordHtml = async () => { + if (!divRef.current) return const contentHTML = divRef.current if (isEchartsToImg.current) { isEchartsToImg.current = false - // 使用html2canvas将ECharts图表转换为图像 const echartsElements = contentHTML.querySelectorAll('[data-type="echarts-box"]') const promises = Array.from(echartsElements).map(async (element) => { const echartType = (element as HTMLElement).getAttribute('echart-type') @@ -535,8 +550,6 @@ const ReportViewer: React.FC = (props) => { }) const echartsImages = await Promise.all(promises) - - // 将图像插入到contentHTML中 echartsImages.forEach((imageDataUrl, index) => { const img = document.createElement('img') img.src = imageDataUrl @@ -544,7 +557,7 @@ const ReportViewer: React.FC = (props) => { echartsElements[index].appendChild(img) }) } - // word报告不要附录 table添加边框 移除南丁格尔玫瑰图点击详情(图像中已含) + let wordStr: string = contentHTML.outerHTML if (wordStr.includes('附录:')) { wordStr = wordStr.substring(0, contentHTML.outerHTML.indexOf('附录:')) @@ -554,14 +567,29 @@ const ReportViewer: React.FC = (props) => { .replace(//g, '') .replace(/]*id=("nightingle-rose-title"|"nightingle-rose-content")[^>]*>[\s\S]*?<\/div>/g, '') - saveAs( - //保存文件到本地 - htmlDocx.asBlob(wordStr), //将html转为docx - `${report.Title}.doc`, - ) - setDownloadLoading(false) + saveAs(htmlDocx.asBlob(wordStr), `${report.Title}.doc`) } + const downloadWord = useMemoizedFn(async () => { + const fullItems = allReportItems.flat() + if (!fullItems.length) return + + setDownloadLoading(true) + try { + const renderReady = waitForWordExportDom() + flushSync(() => { + setWordExportItems(fullItems) + }) + await renderReady + // 多页报告含图表时需额外等待 ECharts 初始化 + await new Promise((resolve) => setTimeout(resolve, 500)) + await captureWordHtml() + } finally { + setWordExportItems(null) + setDownloadLoading(false) + } + }) + const onChangePagination = (page: number) => { isEchartsToImg.current = true setReportItems(allReportItems[page - 1] || []) @@ -687,7 +715,7 @@ const ReportViewer: React.FC = (props) => { >
- {reportItems.map((i, index) => ( + {displayReportItems.map((i, index) => ( ))} From 0e1583138a3b24235be76b72fe6f4b8c4889c421 Mon Sep 17 00:00:00 2001 From: Youngster_yj <907985037@qq.com> Date: Mon, 13 Jul 2026 11:20:59 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20irify=E6=8A=A5=E5=91=8A=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/assetViewer/ReportViewerPage.tsx | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx b/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx index 405e5d85de..b558e176c5 100644 --- a/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx +++ b/app/renderer/src/main/src/pages/assetViewer/ReportViewerPage.tsx @@ -423,17 +423,25 @@ const ReportViewer: React.FC = (props) => { const displayReportItems = wordExportItems ?? reportItems const divRef = useRef(null) const wordExportRenderReadyRef = useRef<(() => void) | null>(null) + const wordExportInFlightRef = useRef(false) const [downloadLoading, setDownloadLoading] = useState(false) useLayoutEffect(() => { if (!wordExportItems?.length) return - const frame = requestAnimationFrame(() => { - requestAnimationFrame(() => { + let cancelled = false + let innerFrame = 0 + const outerFrame = requestAnimationFrame(() => { + innerFrame = requestAnimationFrame(() => { + if (cancelled) return wordExportRenderReadyRef.current?.() wordExportRenderReadyRef.current = null }) }) - return () => cancelAnimationFrame(frame) + return () => { + cancelled = true + cancelAnimationFrame(outerFrame) + cancelAnimationFrame(innerFrame) + } }, [wordExportItems]) useEffect(() => { @@ -531,9 +539,24 @@ const ReportViewer: React.FC = (props) => { ) } - const waitForWordExportDom = () => - new Promise((resolve) => { - wordExportRenderReadyRef.current = resolve + const waitForWordExportDom = (timeoutMs = 30000) => + new Promise((resolve, reject) => { + let settled = false + const onReady = () => { + if (settled) return + settled = true + window.clearTimeout(timer) + resolve() + } + const timer = window.setTimeout(() => { + if (settled) return + settled = true + if (wordExportRenderReadyRef.current === onReady) { + wordExportRenderReadyRef.current = null + } + reject(new Error('Word export DOM render timeout')) + }, timeoutMs) + wordExportRenderReadyRef.current = onReady }) const captureWordHtml = async () => { @@ -573,6 +596,9 @@ const ReportViewer: React.FC = (props) => { const downloadWord = useMemoizedFn(async () => { const fullItems = allReportItems.flat() if (!fullItems.length) return + // 同步拦截重复点击,避免 downloadLoading 尚未渲染时覆盖 resolve 导致先前导出永久挂起 + if (wordExportInFlightRef.current) return + wordExportInFlightRef.current = true setDownloadLoading(true) try { @@ -584,7 +610,11 @@ const ReportViewer: React.FC = (props) => { // 多页报告含图表时需额外等待 ECharts 初始化 await new Promise((resolve) => setTimeout(resolve, 500)) await captureWordHtml() + } catch (e) { + yakitNotify('error', `Export Word failed: ${e}`) } finally { + wordExportRenderReadyRef.current = null + wordExportInFlightRef.current = false setWordExportItems(null) setDownloadLoading(false) }