From 37353b06ead0b0a61e18b64c0c05fbb66a3290f7 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Fri, 20 Dec 2024 15:01:14 +0800 Subject: [PATCH] perf: export pdf --- apps/fronted/package.json | 1 + apps/fronted/src/app/layout.tsx | 7 +- .../src/components/shared/PdfExport.tsx | 180 +++++++++++------- apps/fronted/src/components/ui/sonner.tsx | 31 +++ pnpm-lock.yaml | 14 ++ 5 files changed, 161 insertions(+), 72 deletions(-) create mode 100644 apps/fronted/src/components/ui/sonner.tsx diff --git a/apps/fronted/package.json b/apps/fronted/package.json index 693cfed..9bc9947 100644 --- a/apps/fronted/package.json +++ b/apps/fronted/package.json @@ -53,6 +53,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18", "react-resizable-panels": "^2.0.20", + "sonner": "^1.7.1", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.1", diff --git a/apps/fronted/src/app/layout.tsx b/apps/fronted/src/app/layout.tsx index bdb518e..3aa56a7 100644 --- a/apps/fronted/src/app/layout.tsx +++ b/apps/fronted/src/app/layout.tsx @@ -1,18 +1,18 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { Providers } from "./providers"; - +import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "魔法简历", - description: "极度自由的在线简历编辑器" + description: "极度自由的在线简历编辑器", }; export default function RootLayout({ - children + children, }: Readonly<{ children: React.ReactNode; }>) { @@ -20,6 +20,7 @@ export default function RootLayout({ {children} + ); diff --git a/apps/fronted/src/components/shared/PdfExport.tsx b/apps/fronted/src/components/shared/PdfExport.tsx index e588271..7cb9f43 100644 --- a/apps/fronted/src/components/shared/PdfExport.tsx +++ b/apps/fronted/src/components/shared/PdfExport.tsx @@ -1,94 +1,136 @@ "use client"; - import React, { useState } from "react"; -import { motion } from "framer-motion"; import { Download, Loader2 } from "lucide-react"; +import { toast } from "sonner"; import { useResumeStore } from "@/store/useResumeStore"; -import { convertImagesToBase64 } from "@/utils"; import { Button } from "@/components/ui/button"; + +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; + if (styleCache.has(ruleText)) return false; + styleCache.set(ruleText, true); + + if (rule instanceof CSSFontFaceRule) 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; + 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 { activeResume } = useResumeStore(); - const { globalSettings = {} } = activeResume || {}; + const { globalSettings = {}, title } = activeResume || {}; const handleExport = async () => { + const exportStartTime = performance.now(); setIsExporting(true); - const pdfElement = document.querySelector("#resume-preview"); - if (!pdfElement) return; - const pageBreakLines = - pdfElement.querySelectorAll(".page-break-line"); - pageBreakLines.forEach((line: HTMLElement) => { - line.style.display = "none"; - }); - const pdfContent = await convertImagesToBase64(pdfElement); + try { + const pdfElement = document.querySelector("#resume-preview"); + if (!pdfElement) { + throw new Error("PDF element not found"); + } - // 不过滤字体 - // let styles = Array.from(document.styleSheets) - // .map((sheet) => { - // try { - // return Array.from(sheet.cssRules) - // .map((rule) => rule.cssText) - // .join("\n"); - // } catch (e) { - // return ""; - // } - // }) - // .join("\n"); + const clonedElement = pdfElement.cloneNode(true) as HTMLElement; - // 过滤字体 - let styles = Array.from(document.styleSheets) - .map((sheet) => { - try { - return Array.from(sheet.cssRules) - .filter((rule) => { - // 过滤掉 @font-face 规则 - if (rule instanceof CSSFontFaceRule) return false; - // 过滤掉包含 font 相关属性的规则 - if (rule.cssText.includes("font-family")) return false; - return true; - }) - .map((rule) => rule.cssText) - .join("\n"); - } catch (e) { - return ""; - } - }) - .join("\n"); + const pageBreakLines = + clonedElement.querySelectorAll(".page-break-line"); + pageBreakLines.forEach((line) => { + line.style.display = "none"; + }); - const response = await fetch("/generate-pdf", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - content: pdfContent, - styles: styles, - margin: globalSettings.pagePadding, - }), - }); + const [styles] = await Promise.all([ + getOptimizedStyles(), + optimizeImages(clonedElement), + ]); - if (!response.ok) { - throw new Error("Failed to generate PDF"); + const response = await fetch("/generate-pdf", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: clonedElement.outerHTML, + styles, + }), + }); + + 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("PDF 导出成功!"); + } catch (error) { + console.error("Export error:", error); + toast.error("PDF 导出失败,请重试"); + } finally { + setIsExporting(false); } - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = "document.pdf"; - link.click(); - window.URL.revokeObjectURL(url); - setIsExporting(false); - pageBreakLines.forEach((line) => { - line.style.display = "block"; - }); }; return (