diff --git a/package-lock.json b/package-lock.json index a562b6e..5fcc1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^1.13.2", + "pdfjs-dist": "^5.4.449", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.9.6" @@ -985,6 +986,256 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.86.tgz", + "integrity": "sha512-hOkywnrkdFdVpsuaNsZWfEY7kc96eROV2DuMTTvGF15AZfwobzdG2w0eDlU5UBx3Lg/XlWUnqVT5zLUWyo5h6A==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.86", + "@napi-rs/canvas-darwin-arm64": "0.1.86", + "@napi-rs/canvas-darwin-x64": "0.1.86", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.86", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.86", + "@napi-rs/canvas-linux-arm64-musl": "0.1.86", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.86", + "@napi-rs/canvas-linux-x64-gnu": "0.1.86", + "@napi-rs/canvas-linux-x64-musl": "0.1.86", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.86", + "@napi-rs/canvas-win32-x64-msvc": "0.1.86" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.86.tgz", + "integrity": "sha512-IjkZFKUr6GzMzzrawJaN3v+yY3Fvpa71e0DcbePfxWelFKnESIir+XUcdAbim29JOd0JE0/hQJdfUCb5t/Fjrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.86.tgz", + "integrity": "sha512-PUCxDq0wSSJbtaOqoKj3+t5tyDbtxWumziOTykdn3T839hu6koMaBFpGk9lXpsGaPNgyFpPqjxhtsPljBGnDHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.86.tgz", + "integrity": "sha512-rlCFLv4Rrg45qFZq7mysrKnsUbMhwdNg3YPuVfo9u4RkOqm7ooAJvdyDFxiqfSsJJTqupYqa9VQCUt8WKxKhNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.86.tgz", + "integrity": "sha512-6xWwyMc9BlDBt+9XHN/GzUo3MozHta/2fxQHMb80x0K2zpZuAdDKUYHmYzx9dFWDY3SbPYnx6iRlQl6wxnwS1w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.86.tgz", + "integrity": "sha512-r2OX3w50xHxrToTovOSQWwkVfSq752CUzH9dzlVXyr8UDKFV8dMjfa9hePXvAJhN3NBp4TkHcGx15QCdaCIwnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.86.tgz", + "integrity": "sha512-jbXuh8zVFUPw6a9SGpgc6EC+fRbGGyP1NFfeQiVqGLs6bN93ROtPLPL6MH9Bp6yt0CXUFallk2vgKdWDbmW+bw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.86.tgz", + "integrity": "sha512-9IwHR2qbq2HceM9fgwyL7x37Jy3ptt1uxvikQEuWR0FisIx9QEdt7F3huljCky76aoouF2vSd0R2fHo3ESRoPw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.86.tgz", + "integrity": "sha512-Jor+rhRN6ubix+D2QkNn9XlPPVAYl+2qFrkZ4oZN9UgtqIUZ+n+HljxhlkkDFRaX1mlxXOXPQjxaZg17zDSFcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.86.tgz", + "integrity": "sha512-A28VTy91DbclopSGZ2tIon3p8hcVI1JhnNpDpJ5N9rYlUnVz1WQo4waEMh+FICTZF07O3coxBNZc4Vu4doFw7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.86.tgz", + "integrity": "sha512-q6G1YXUt3gBCAS2bcDMCaBL4y20di8eVVBi1XhjUqZSVyZZxxwIuRQHy31NlPJUCMiyNiMuc6zeI0uqgkWwAmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.86", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.86.tgz", + "integrity": "sha512-X0g46uRVgnvCM1cOjRXAOSFSG63ktUFIf/TIfbKCUc7QpmYUcHmSP9iR6DGOYfk+SggLsXoJCIhPTotYeZEAmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3565,6 +3816,18 @@ "dev": true, "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "5.4.449", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.449.tgz", + "integrity": "sha512-CegnUaT0QwAyQMS+7o2POr4wWUNNe8VaKKlcuoRHeYo98cVnqPpwOXNSx6Trl6szH02JrRcsPgletV6GmF3LtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.81" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index d86d4d7..d422150 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.13.2", + "pdfjs-dist": "^5.4.449", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.9.6" @@ -32,4 +33,4 @@ "typescript-eslint": "^8.46.4", "vite": "^7.2.4" } -} \ No newline at end of file +} diff --git a/src/components/exam/ExamPrintPanel.tsx b/src/components/exam/ExamPrintPanel.tsx index cb1ed5f..dca6a7b 100644 --- a/src/components/exam/ExamPrintPanel.tsx +++ b/src/components/exam/ExamPrintPanel.tsx @@ -1,4 +1,6 @@ import { useEffect, useRef, useState } from 'react'; +import * as pdfjsLib from 'pdfjs-dist'; +import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'; import { getDaojiandanPdf as getDaojiandanPdfApi, submitDaojiandanSign } from '../../api'; import type { ExamClient } from '../../data/mockData'; @@ -6,6 +8,22 @@ import { setExamActionRecord, setDaojiandanPdf, getDaojiandanPdf } from '../../u import type { SignaturePadHandle } from '../ui'; import { Button, SignaturePad } from '../ui'; +// Polyfill for Promise.withResolvers +if (typeof (Promise as any).withResolvers === 'undefined') { + (Promise as any).withResolvers = function () { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }; +} + +// 配置 PDF.js worker +pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; + export const ExamPrintPanel = ({ client }: { client: ExamClient }) => { const [pdfUrl, setPdfUrl] = useState(null); const [loading, setLoading] = useState(false); @@ -17,9 +35,10 @@ export const ExamPrintPanel = ({ client }: { client: ExamClient }) => { const [submitMessage, setSubmitMessage] = useState(null); const [showPreview, setShowPreview] = useState(false); const [fetchLoading, setFetchLoading] = useState(false); + const [pdfData, setPdfData] = useState(null); const signaturePadRef = useRef(null); const printRef = useRef(null); - const iframeRef = useRef(null); + const canvasContainerRef = useRef(null); const handleSubmitSign = async () => { const examId = Number(client.id); @@ -126,12 +145,14 @@ export const ExamPrintPanel = ({ client }: { client: ExamClient }) => { } }; + // 第一步:加载 PDF 数据 useEffect(() => { if (!pdfUrl) return; let objectUrl: string | null = null; setPdfReady(false); setLoading(true); + setPdfData(null); fetch(pdfUrl) .then((resp) => { @@ -141,12 +162,15 @@ export const ExamPrintPanel = ({ client }: { client: ExamClient }) => { .then((blob) => { objectUrl = URL.createObjectURL(blob); setPdfBlobUrl(objectUrl); + return blob.arrayBuffer(); + }) + .then((arrayBuffer) => { + setPdfData(arrayBuffer); + setLoading(false); }) .catch((err) => { console.error('PDF 拉取失败', err); setError('PDF 加载失败,请稍后重试'); - }) - .finally(() => { setLoading(false); }); @@ -157,15 +181,133 @@ export const ExamPrintPanel = ({ client }: { client: ExamClient }) => { }; }, [pdfUrl]); + // 第二步:渲染 PDF + useEffect(() => { + if (!pdfData || !canvasContainerRef.current) return; + + console.log('开始渲染 PDF'); + setPdfReady(false); + + const renderAllPages = async () => { + try { + const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; + console.log('PDF 加载成功,共 ' + pdf.numPages + ' 页'); + + if (!canvasContainerRef.current) { + console.log('canvasContainerRef 仍为 null'); + return; + } + + // 清空容器 + canvasContainerRef.current.innerHTML = ''; + + const scale = 1.2; + + for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { + const page = await pdf.getPage(pageNum); + const viewport = page.getViewport({ scale }); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (!context) { + console.log('无法获取 canvas context'); + continue; + } + + canvas.height = viewport.height; + canvas.width = viewport.width; + canvas.style.display = 'block'; + canvas.style.marginBottom = '10px'; + canvas.className = 'mx-auto border rounded-lg shadow-sm'; + + canvasContainerRef.current.appendChild(canvas); + + const renderContext = { + canvasContext: context, + viewport: viewport, + } as any; + + await page.render(renderContext).promise; + console.log('渲染完成第 ' + pageNum + ' 页'); + } + + setPdfReady(true); + console.log('所有页面渲染完成'); + } catch (err) { + console.error('PDF 渲染失败', err); + setError('PDF 渲染失败,请稍后重试: ' + err); + } + }; + + renderAllPages(); + }, [pdfData]); + const handlePrint = () => { - if (!pdfBlobUrl || !pdfReady) return; - const printWindow = window.open(pdfBlobUrl, '_blank'); - if (printWindow) { - printWindow.onload = () => { - printWindow.focus(); + if (!pdfBlobUrl || !pdfReady || !canvasContainerRef.current) return; + + // 创建打印窗口 + const printWindow = window.open('', '_blank'); + if (!printWindow) return; + + // 获取所有的 canvas + const canvases = canvasContainerRef.current.querySelectorAll('canvas'); + + // 构建打印页面 + printWindow.document.write(` + + + + 导检单打印 + + + + `); + + // 将每个 canvas 转换为图片并添加到打印窗口 + canvases.forEach((canvas) => { + const imgData = (canvas as HTMLCanvasElement).toDataURL('image/png'); + printWindow.document.write(``); + }); + + printWindow.document.write(` + + + `); + + printWindow.document.close(); + + // 等待图片加载完成后执行打印 + printWindow.onload = () => { + printWindow.focus(); + setTimeout(() => { printWindow.print(); - }; - } + }, 1000); + }; }; return ( @@ -306,13 +448,7 @@ export const ExamPrintPanel = ({ client }: { client: ExamClient }) => {
-