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 (