mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: export json && window.print to pdf
This commit is contained in:
@@ -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`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user