From dee081e2e37d92d0b88401e5bcf02cb76dd067a9 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Mon, 9 Mar 2026 17:21:24 +0800 Subject: [PATCH] fix: previewDock bar export --- src/components/preview/PreviewDock.tsx | 154 +---------- src/components/shared/PdfExport.tsx | 339 ++----------------------- src/utils/export.ts | 169 ++++++++++++ src/utils/print.ts | 24 +- 4 files changed, 211 insertions(+), 475 deletions(-) create mode 100644 src/utils/export.ts diff --git a/src/components/preview/PreviewDock.tsx b/src/components/preview/PreviewDock.tsx index ca13c1c..56e64e0 100644 --- a/src/components/preview/PreviewDock.tsx +++ b/src/components/preview/PreviewDock.tsx @@ -17,6 +17,7 @@ import { toast } from "sonner"; import { useTranslations } from "@/i18n/compat/client"; import { useRouter } from "@/lib/navigation"; import { exportResumeToBrowserPrint } from "@/utils/print"; +import { exportToPdf } from "@/utils/export"; import { Dock, DockIcon } from "@/components/magicui/dock"; import { Button } from "@/components/ui/button"; import { @@ -92,151 +93,16 @@ const PreviewDock = ({ const { duplicateResume, setActiveResume, activeResumeId, activeResume, updateGlobalSettings } = useResumeStore(); const { globalSettings = {}, title } = activeResume || {}; - const getOptimizedStyles = () => { - const styleCache = new Map(); - const startTime = performance.now(); - - const styles = Array.from(document.styleSheets) - .map((sheet) => { - try { - return Array.from(sheet.cssRules) - .filter((rule) => { - const ruleText = rule.cssText; - const normalizedRuleText = ruleText.toLowerCase(); - if (styleCache.has(ruleText)) return false; - styleCache.set(ruleText, true); - - if (rule instanceof CSSFontFaceRule) return false; - if (rule instanceof CSSImportRule) return false; - if (normalizedRuleText.includes("fonts.googleapis.com")) return false; - if (normalizedRuleText.includes("fonts.gstatic.com")) return false; - if (ruleText.includes("font-family")) return false; - if (ruleText.includes("@keyframes")) return false; - if (ruleText.includes("animation")) return false; - if (ruleText.includes("transition")) return false; - if (ruleText.includes("hover")) return false; - return true; - }) - .map((rule) => rule.cssText) - .join("\n"); - } catch (e) { - console.warn("Style processing error:", e); - return ""; - } - }) - .join("\n"); - - console.log(`Style processing took ${performance.now() - startTime}ms`); - return styles; - }; - - const optimizeImages = async (element: HTMLElement) => { - const startTime = performance.now(); - const images = element.getElementsByTagName("img"); - - const imagePromises = Array.from(images) - .filter((img) => !img.src.startsWith("data:")) - .map(async (img) => { - try { - const response = await fetch(img.src); - const blob = await response.blob(); - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - img.src = reader.result as string; - resolve(); - }; - reader.readAsDataURL(blob); - }); - } catch (error) { - console.error("Image conversion error:", error); - return Promise.resolve(); - } - }); - - await Promise.all(imagePromises); - console.log(`Image processing took ${performance.now() - startTime}ms`); - }; - const handleExportPdf = async () => { - const exportStartTime = performance.now(); - setIsExporting(true); - - try { - const pdfElement = document.querySelector("#resume-preview"); - if (!pdfElement) { - throw new Error("PDF element not found"); - } - - const clonedElement = pdfElement.cloneNode(true) as HTMLElement; - const pagePadding = globalSettings?.pagePadding || 0; - const transformValue = clonedElement.style.transform || ""; - const scaleMatch = transformValue.match(/scale\(([\d.]+)\)/); - if (scaleMatch) { - const scale = Number(scaleMatch[1]); - if (Number.isFinite(scale) && scale > 0 && scale < 1) { - // 服务端导出前将 transform 缩放转为 zoom,避免分页计算偏差 - clonedElement.style.removeProperty("transform"); - clonedElement.style.removeProperty("transform-origin"); - clonedElement.style.setProperty("width", "100%", "important"); - clonedElement.style.setProperty("zoom", String(scale)); - } - } else { - // 未开启一页纸模式时,宽度需减去 padding - clonedElement.style.setProperty( - "width", - `calc(210mm - ${2 * pagePadding}px)`, - "important" - ); - } - clonedElement.style.setProperty("padding", "0", "important"); - clonedElement.style.setProperty("box-sizing", "border-box"); - - const pageBreakLines = - clonedElement.querySelectorAll(".page-break-line"); - pageBreakLines.forEach((line) => { - line.style.display = "none"; - }); - - const [styles] = await Promise.all([ - getOptimizedStyles(), - optimizeImages(clonedElement) - ]); - - const response = await fetch(PDF_EXPORT_CONFIG.SERVER_URL, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - content: clonedElement.outerHTML, - styles, - margin: globalSettings.pagePadding - }), - mode: "cors", - signal: AbortSignal.timeout(PDF_EXPORT_CONFIG.TIMEOUT) - }); - - if (!response.ok) { - throw new Error(`PDF generation failed: ${response.status}`); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `${title}.pdf`; - link.click(); - - window.URL.revokeObjectURL(url); - console.log(`Total export took ${performance.now() - exportStartTime}ms`); - toast.success(tPdf("toast.success")); - } catch (error) { - console.error("Export error:", error); - toast.error(tPdf("toast.error")); - } finally { - setIsExporting(false); - } + await exportToPdf({ + elementId: "resume-preview", + title: title || "resume", + pagePadding: globalSettings?.pagePadding || 0, + onStart: () => setIsExporting(true), + onEnd: () => setIsExporting(false), + successMessage: tPdf("toast.success"), + errorMessage: tPdf("toast.error") + }); }; const handleExportJson = () => { diff --git a/src/components/shared/PdfExport.tsx b/src/components/shared/PdfExport.tsx index e3379c6..efabb45 100644 --- a/src/components/shared/PdfExport.tsx +++ b/src/components/shared/PdfExport.tsx @@ -10,7 +10,8 @@ import { import { toast } from "sonner"; import { useResumeStore } from "@/store/useResumeStore"; import { Button } from "@/components/ui/button"; -import { PDF_EXPORT_CONFIG } from "@/config"; +import { exportToPdf } from "@/utils/export"; +import { exportResumeToBrowserPrint } from "@/utils/print"; import { DropdownMenu, DropdownMenuContent, @@ -18,72 +19,6 @@ import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -const getOptimizedStyles = () => { - const styleCache = new Map(); - const startTime = performance.now(); - - const styles = Array.from(document.styleSheets) - .map((sheet) => { - try { - return Array.from(sheet.cssRules) - .filter((rule) => { - const ruleText = rule.cssText; - const normalizedRuleText = ruleText.toLowerCase(); - if (styleCache.has(ruleText)) return false; - styleCache.set(ruleText, true); - - if (rule instanceof CSSFontFaceRule) return false; - if (rule instanceof CSSImportRule) return false; - if (normalizedRuleText.includes("fonts.googleapis.com")) return false; - if (normalizedRuleText.includes("fonts.gstatic.com")) return false; - if (ruleText.includes("font-family")) return false; - if (ruleText.includes("@keyframes")) return false; - if (ruleText.includes("animation")) return false; - if (ruleText.includes("transition")) return false; - if (ruleText.includes("hover")) return false; - return true; - }) - .map((rule) => rule.cssText) - .join("\n"); - } catch (e) { - console.warn("Style processing error:", e); - return ""; - } - }) - .join("\n"); - - console.log(`Style processing took ${performance.now() - startTime}ms`); - return styles; -}; - -const optimizeImages = async (element: HTMLElement) => { - const startTime = performance.now(); - const images = element.getElementsByTagName("img"); - - const imagePromises = Array.from(images) - .filter((img) => !img.src.startsWith("data:")) - .map(async (img) => { - try { - const response = await fetch(img.src); - const blob = await response.blob(); - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - img.src = reader.result as string; - resolve(); - }; - reader.readAsDataURL(blob); - }); - } catch (error) { - console.error("Image conversion error:", error); - return Promise.resolve(); - } - }); - - await Promise.all(imagePromises); - console.log(`Image processing took ${performance.now() - startTime}ms`); -}; - const PdfExport = () => { const [isExporting, setIsExporting] = useState(false); const [isExportingJson, setIsExportingJson] = useState(false); @@ -93,93 +28,15 @@ const PdfExport = () => { const printFrameRef = useRef(null); const handleExport = async () => { - const exportStartTime = performance.now(); - setIsExporting(true); - - try { - const pdfElement = document.querySelector("#resume-preview"); - if (!pdfElement) { - throw new Error("PDF element not found"); - } - - const clonedElement = pdfElement.cloneNode(true) as HTMLElement; - const transformValue = clonedElement.style.transform || ""; - const scaleMatch = transformValue.match(/scale\(([\d.]+)\)/); - if (scaleMatch) { - const scale = Number(scaleMatch[1]); - if (Number.isFinite(scale) && scale > 0 && scale < 1) { - // 服务端导出前将 transform 缩放转为 zoom,避免分页计算偏差 - clonedElement.style.removeProperty("transform"); - clonedElement.style.removeProperty("transform-origin"); - clonedElement.style.setProperty("width", "100%", "important"); - clonedElement.style.setProperty("zoom", String(scale)); - } - } - - // 服务端会强制 #resume-preview { width: 210mm; padding: 0 !important } - // 预览中内容宽度 = 210mm(父容器)- 2*pagePadding(padding) - // 通过 inline style 覆盖服务端样式,使内容宽度与预览一致 - const pagePadding = globalSettings?.pagePadding || 0; - clonedElement.style.setProperty( - "width", - "100%", - "important" - ); - clonedElement.style.setProperty("padding", "0", "important"); - clonedElement.style.setProperty("box-sizing", "border-box"); - - const pageBreakLines = - clonedElement.querySelectorAll(".page-break-line"); - pageBreakLines.forEach((line) => { - line.style.display = "none"; - }); - - const [capturedStyles] = await Promise.all([ - getOptimizedStyles(), - optimizeImages(clonedElement) - ]); - - const styles = ` - ${capturedStyles} - html, body { background: white !important; background-color: white !important; } - #resume-preview { background: white !important; background-color: white !important; } - `; - - const response = await fetch(PDF_EXPORT_CONFIG.SERVER_URL, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - content: clonedElement.outerHTML, - styles, - margin: globalSettings.pagePadding - }), - // 允许跨域请求 - mode: "cors", - signal: AbortSignal.timeout(PDF_EXPORT_CONFIG.TIMEOUT) - }); - - if (!response.ok) { - throw new Error(`PDF generation failed: ${response.status}`); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `${title}.pdf`; - link.click(); - - window.URL.revokeObjectURL(url); - console.log(`Total export took ${performance.now() - exportStartTime}ms`); - toast.success(t("toast.success")); - } catch (error) { - console.error("Export error:", error); - toast.error(t("toast.error")); - } finally { - setIsExporting(false); - } + await exportToPdf({ + elementId: "resume-preview", + title: title || "resume", + pagePadding: globalSettings?.pagePadding || 0, + onStart: () => setIsExporting(true), + onEnd: () => setIsExporting(false), + successMessage: t("toast.success"), + errorMessage: t("toast.error") + }); }; const handleJsonExport = () => { @@ -207,12 +64,7 @@ const PdfExport = () => { } }; - const handlePrint = () => { - if (!printFrameRef.current) { - console.error("Print frame not found"); - return; - } - + const handlePrint = async () => { const resumeContent = document.getElementById("resume-preview"); if (!resumeContent) { console.error("Resume content not found"); @@ -220,163 +72,15 @@ const PdfExport = () => { } const pagePadding = globalSettings?.pagePadding || 0; - const iframeWindow = printFrameRef.current.contentWindow; - if (!iframeWindow) { - console.error("IFrame window not found"); - return; - } - - try { - iframeWindow.document.open(); - const clonedContent = resumeContent.cloneNode(true) as HTMLElement; - const transformValue = clonedContent.style.transform || ""; - const match = transformValue.match(/scale\(([\d.]+)\)/); - if (match) { - const scale = Number(match[1]); - if (Number.isFinite(scale) && scale > 0 && scale < 1) { - // 打印时使用 zoom 参与分页布局计算,比 transform 更接近最终分页效果 - clonedContent.style.removeProperty("transform"); - clonedContent.style.removeProperty("transform-origin"); - clonedContent.style.setProperty("width", "100%"); - clonedContent.style.setProperty("zoom", String(scale)); - } - } - - const htmlContent = ` - - - - Print Resume - - - - - - - `; - - iframeWindow.document.write(htmlContent); - iframeWindow.document.close(); - - const printWhenReady = async () => { - try { - const doc = iframeWindow.document; - if (doc.fonts?.ready) { - await doc.fonts.ready; - } - - const images = Array.from(doc.images); - await Promise.all( - images - .filter((img) => !img.complete) - .map( - (img) => - new Promise((resolve) => { - img.onload = () => resolve(); - img.onerror = () => resolve(); - }) - ) - ); - - await new Promise((resolve) => { - iframeWindow.requestAnimationFrame(() => { - iframeWindow.requestAnimationFrame(() => resolve()); - }); - }); - - iframeWindow.focus(); - iframeWindow.print(); - } catch (error) { - console.error("Error print:", error); - } - }; - - void printWhenReady(); - } catch (error) { - console.error("Error setting up print:", error); - } + await exportResumeToBrowserPrint(resumeContent, pagePadding); }; const isLoading = isExporting || isExportingJson; const loadingText = isExporting ? t("button.exporting") : isExportingJson - ? t("button.exportingJson") - : ""; + ? t("button.exportingJson") + : ""; return ( <> @@ -416,19 +120,6 @@ const PdfExport = () => { -