feat: export json && window.print to pdf

This commit is contained in:
JOYCEQL
2025-01-15 23:58:22 +08:00
committed by qingchen
parent 56fe9759d8
commit 61531efa64
6 changed files with 219 additions and 25 deletions
@@ -229,7 +229,7 @@ export default function Home() {
return (
<main
className={cn(
"w-full h-screen overflow-hidden",
"w-full min-h-screen overflow-hidden",
"bg-white text-gray-900",
"dark:bg-neutral-900 dark:text-neutral-200"
)}
@@ -16,7 +16,8 @@ const EducationSection = ({
const visibleEducation = education?.filter((edu) => edu.visible);
return (
<motion.div
className="hover:cursor-pointer
className="
hover:cursor-pointer
hover:bg-gray-100
rounded-md
transition-all
@@ -64,7 +64,7 @@ const ProjectSection: React.FC<ProjectSectionProps> = ({
return (
<motion.div
className="hover:cursor-pointer hover:bg-gray-100 rounded-md transition-all duration-300 ease-in-out hover:shadow-md"
className=" hover:cursor-pointer hover:bg-gray-100 rounded-md transition-all duration-300 ease-in-out hover:shadow-md"
layout
style={{
marginTop: `${globalSettings?.sectionSpacing || 24}px`,
+204 -18
View File
@@ -1,10 +1,16 @@
"use client";
import React, { useState } from "react";
import { Download, Loader2 } from "lucide-react";
import React, { useState, useRef } from "react";
import { useTranslations } from "next-intl";
import { Download, Loader2, FileJson, Printer } from "lucide-react";
import { toast } from "sonner";
import { useResumeStore } from "@/store/useResumeStore";
import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
const getOptimizedStyles = () => {
const styleCache = new Map();
@@ -70,9 +76,11 @@ const optimizeImages = async (element: HTMLElement) => {
const PdfExport = () => {
const [isExporting, setIsExporting] = useState(false);
const [isExportingJson, setIsExportingJson] = useState(false);
const { activeResume } = useResumeStore();
const { globalSettings = {}, title } = activeResume || {};
const t = useTranslations("pdfExport");
const printFrameRef = useRef<HTMLIFrameElement>(null);
const handleExport = async () => {
const exportStartTime = performance.now();
@@ -131,22 +139,200 @@ const PdfExport = () => {
}
};
const handleJsonExport = () => {
try {
setIsExportingJson(true);
if (!activeResume) {
throw new Error("No active resume");
}
const jsonStr = JSON.stringify(activeResume, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${title}.json`;
link.click();
window.URL.revokeObjectURL(url);
toast.success(t("toast.jsonSuccess"));
} catch (error) {
console.error("JSON export error:", error);
toast.error(t("toast.jsonError"));
} finally {
setIsExportingJson(false);
}
};
const handlePrint = () => {
if (!printFrameRef.current) {
console.error("Print frame not found");
return;
}
const resumeContent = document.getElementById("resume-preview");
if (!resumeContent) {
console.error("Resume content not found");
return;
}
const actualContent = resumeContent.parentElement;
if (!actualContent) {
console.error("Actual content not found");
return;
}
console.log("Found content:", actualContent);
const pagePadding = globalSettings?.pagePadding;
const iframeWindow = printFrameRef.current.contentWindow;
if (!iframeWindow) {
console.error("IFrame window not found");
return;
}
try {
iframeWindow.document.open();
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<title>Print Resume</title>
<style>
@page {
size: A4;
margin: ${pagePadding}px;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
background: white;
}
body {
font-family: sans-serif;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
#resume-preview {
padding: 0 !important;
margin: 0 !important;
}
#print-content {
width: 210mm;
min-height: 297mm;
margin: 0 auto;
padding: 0;
background: white;
box-shadow: none;
}
#print-content * {
box-shadow: none !important;
transform: none !important;
scale: 1 !important;
}
.scale-90 {
transform: none !important;
}
${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">
${actualContent.innerHTML}
</div>
</body>
</html>
`;
iframeWindow.document.write(htmlContent);
iframeWindow.document.close();
setTimeout(() => {
try {
iframeWindow.focus();
iframeWindow.print();
} catch (error) {
console.error("Error print:", error);
}
}, 1000);
} catch (error) {
console.error("Error setting up print:", error);
}
};
const isLoading = isExporting || isExportingJson;
const loadingText = isExporting
? t("button.exporting")
: isExportingJson
? t("button.exportingJson")
: "";
return (
<div>
<Button
className="px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2
disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleExport}
disabled={isExporting}
>
{isExporting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Download className="w-4 h-4" />
)}
<span>{isExporting ? t("button.exporting") : t("button.default")}</span>
</Button>
</div>
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2
disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>{loadingText}</span>
</>
) : (
<>
<Download className="w-4 h-4" />
<span>{t("button.export")}</span>
</>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleExport} disabled={isLoading}>
<Download className="w-4 h-4 mr-2" />
{t("button.exportPdf")}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleJsonExport} disabled={isLoading}>
<FileJson className="w-4 h-4 mr-2" />
{t("button.exportJson")}
</DropdownMenuItem>
<DropdownMenuItem onClick={handlePrint} disabled={isLoading}>
<Printer className="w-4 h-4 mr-2" />
{t("button.print")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<iframe
ref={printFrameRef}
style={{
position: "absolute",
width: "210mm",
height: "297mm",
visibility: "hidden",
zIndex: -1,
}}
title="Print Frame"
/>
</>
);
};
@@ -30,6 +30,7 @@ const ClassicTemplate: React.FC<ClassicTemplateProps> = ({
return (
<>
<BaseInfo basic={data.basic} globalSettings={data.globalSettings} />
{data.basic.githubContributionsVisible && (
<GithubContribution
className="mt-2"
@@ -85,7 +86,7 @@ const ClassicTemplate: React.FC<ClassicTemplateProps> = ({
return (
<div
className="flex flex-col w-full"
className="flex flex-col w-full min-h-screen"
style={{
backgroundColor: colorScheme.background,
color: colorScheme.text,
+9 -3
View File
@@ -96,12 +96,18 @@
},
"pdfExport": {
"button": {
"export": "导出",
"exportPdf": "导出 PDF(服务端)",
"exportJson": "导出JSON配置",
"exporting": "导出中...",
"default": "导出 PDF"
"exportingJson": "导出中...",
"print": "浏览器打印"
},
"toast": {
"success": "PDF 导出成功",
"error": "PDF 导出失败,请重试"
"success": "PDF导出成功",
"error": "PDF导出失败",
"jsonSuccess": "配置导出成功",
"jsonError": "配置导出失败"
}
},
"workbench": {