mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
fix: previewDock bar export
This commit is contained in:
@@ -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<void>((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<HTMLElement>("#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<HTMLElement>(".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 = () => {
|
||||
|
||||
@@ -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<void>((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<HTMLIFrameElement>(null);
|
||||
|
||||
const handleExport = async () => {
|
||||
const exportStartTime = performance.now();
|
||||
setIsExporting(true);
|
||||
|
||||
try {
|
||||
const pdfElement = document.querySelector<HTMLElement>("#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<HTMLElement>(".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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Print Resume</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Alibaba PuHuiTi";
|
||||
src: url("/fonts/AlibabaPuHuiTi-3-55-Regular.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
#resume-preview {
|
||||
margin: 0 !important;
|
||||
padding: ${pagePadding}px !important;
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
font-family: "Alibaba PuHuiTi", sans-serif !important;
|
||||
}
|
||||
|
||||
#print-content {
|
||||
width: 210mm;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background: white;
|
||||
box-shadow: none;
|
||||
}
|
||||
#print-content * {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#resume-preview .min-h-screen,
|
||||
#resume-preview .min-h-full,
|
||||
#resume-preview [style*="min-height"] {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.page-break-line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${Array.from(document.styleSheets)
|
||||
.map((sheet) => {
|
||||
try {
|
||||
return Array.from(sheet.cssRules)
|
||||
.map((rule) => rule.cssText)
|
||||
.join("\n");
|
||||
} catch (e) {
|
||||
console.warn("Could not copy styles from sheet:", e);
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.join("\n")}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="print-content">
|
||||
${clonedContent.outerHTML}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
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<void>((resolve) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => resolve();
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await new Promise<void>((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 = () => {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<iframe
|
||||
ref={printFrameRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "1px",
|
||||
height: "1px",
|
||||
left: "-9999px",
|
||||
top: 0,
|
||||
visibility: "hidden",
|
||||
zIndex: -1
|
||||
}}
|
||||
title="Print Frame"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { toast } from "sonner";
|
||||
import { PDF_EXPORT_CONFIG } from "@/config";
|
||||
|
||||
export 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;
|
||||
};
|
||||
|
||||
export 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<void>((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`);
|
||||
};
|
||||
|
||||
export interface ExportToPdfOptions {
|
||||
elementId: string;
|
||||
title: string;
|
||||
pagePadding: number;
|
||||
onStart?: () => void;
|
||||
onEnd?: () => void;
|
||||
successMessage?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const exportToPdf = async ({
|
||||
elementId,
|
||||
title,
|
||||
pagePadding,
|
||||
onStart,
|
||||
onEnd,
|
||||
successMessage,
|
||||
errorMessage
|
||||
}: ExportToPdfOptions) => {
|
||||
const exportStartTime = performance.now();
|
||||
onStart?.();
|
||||
|
||||
try {
|
||||
const pdfElement = document.querySelector<HTMLElement>(`#${elementId}`);
|
||||
if (!pdfElement) {
|
||||
throw new Error(`PDF element #${elementId} 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));
|
||||
}
|
||||
}
|
||||
|
||||
// 采用 PdfExport.tsx 中的逻辑,统一宽度和 padding 处理
|
||||
clonedElement.style.setProperty("width", "100%", "important");
|
||||
clonedElement.style.setProperty("padding", "0", "important");
|
||||
clonedElement.style.setProperty("box-sizing", "border-box");
|
||||
|
||||
const pageBreakLines = clonedElement.querySelectorAll<HTMLElement>(".page-break-line");
|
||||
pageBreakLines.forEach((line) => {
|
||||
line.style.display = "none";
|
||||
});
|
||||
|
||||
const [capturedStyles] = await Promise.all([
|
||||
getOptimizedStyles(),
|
||||
optimizeImages(clonedElement)
|
||||
]);
|
||||
|
||||
// 注入 PdfExport.tsx 中的样式增强
|
||||
const styles = `
|
||||
${capturedStyles}
|
||||
html, body { background: white !important; background-color: white !important; }
|
||||
#${elementId} { 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: 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);
|
||||
if (successMessage) toast.success(successMessage);
|
||||
console.log(`Total export took ${performance.now() - exportStartTime}ms`);
|
||||
} catch (error) {
|
||||
console.error("Export error:", error);
|
||||
if (errorMessage) toast.error(errorMessage);
|
||||
} finally {
|
||||
onEnd?.();
|
||||
}
|
||||
};
|
||||
+17
-7
@@ -1,4 +1,4 @@
|
||||
export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePadding: number) => {
|
||||
export const exportResumeToBrowserPrint = async (resumeContent: HTMLElement, pagePadding: number) => {
|
||||
const printFrame = document.createElement("iframe");
|
||||
printFrame.style.position = "absolute";
|
||||
printFrame.style.width = "1px";
|
||||
@@ -59,7 +59,7 @@ export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePaddi
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
background: white !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -75,6 +75,7 @@ export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePaddi
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
font-family: "Alibaba PuHuiTi", sans-serif !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
#print-content {
|
||||
@@ -89,7 +90,8 @@ export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePaddi
|
||||
}
|
||||
|
||||
#resume-preview .min-h-screen,
|
||||
#resume-preview .min-h-full {
|
||||
#resume-preview .min-h-full,
|
||||
#resume-preview [style*="min-height"] {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
@@ -102,13 +104,13 @@ export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePaddi
|
||||
try {
|
||||
return Array.from(sheet.cssRules)
|
||||
.map((rule) => rule.cssText)
|
||||
.join("\\n");
|
||||
.join("\n");
|
||||
} catch (e) {
|
||||
console.warn("Could not copy styles from sheet:", e);
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.join("\\n")}
|
||||
.join("\n")}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -125,10 +127,13 @@ export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePaddi
|
||||
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
|
||||
@@ -142,6 +147,7 @@ export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePaddi
|
||||
)
|
||||
);
|
||||
|
||||
// 给予额外的渲染帧缓冲
|
||||
await new Promise<void>((resolve) => {
|
||||
iframeWindow.requestAnimationFrame(() => {
|
||||
iframeWindow.requestAnimationFrame(() => resolve());
|
||||
@@ -159,13 +165,17 @@ export const exportResumeToBrowserPrint = (resumeContent: HTMLElement, pagePaddi
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error("Error print:", error);
|
||||
document.body.removeChild(printFrame);
|
||||
if (document.body.contains(printFrame)) {
|
||||
document.body.removeChild(printFrame);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void printWhenReady();
|
||||
} catch (error) {
|
||||
console.error("Error setting up print:", error);
|
||||
document.body.removeChild(printFrame);
|
||||
if (document.body.contains(printFrame)) {
|
||||
document.body.removeChild(printFrame);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user