From 243a6edc2e77097d0f4ef5f39dc35aa89cc50b27 Mon Sep 17 00:00:00 2001 From: xianyi Date: Mon, 29 Dec 2025 15:27:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BD=93=E6=A3=80=E7=9F=A5?= =?UTF-8?q?=E6=83=85=E5=90=8C=E6=84=8F=E4=B9=A6=E5=88=97=E8=A1=A8=E6=89=93?= =?UTF-8?q?=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/exam/ExamSignPanel.tsx | 507 +++++++++++++++++++++----- 1 file changed, 420 insertions(+), 87 deletions(-) diff --git a/src/components/exam/ExamSignPanel.tsx b/src/components/exam/ExamSignPanel.tsx index 781a882..760b770 100644 --- a/src/components/exam/ExamSignPanel.tsx +++ b/src/components/exam/ExamSignPanel.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 type { OutputTongyishuFileInfo } from '../../api'; import { getTongyishuPdf, signInMedicalExamCenter, submitTongyishuSign } from '../../api'; @@ -11,6 +13,22 @@ import { 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; + interface ExamSignPanelProps { examId?: number; onBusyChange?: (busy: boolean) => void; @@ -32,7 +50,12 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { const [submitLoading, setSubmitLoading] = useState(false); const [submitMessage, setSubmitMessage] = useState(null); const [signedCombinationCodes, setSignedCombinationCodes] = useState([]); - const busy = signLoading || submitLoading || consentLoading; + const [pdfData, setPdfData] = useState(null); + const [pdfReady, setPdfReady] = useState(false); + const [pdfLoading, setPdfLoading] = useState(false); + const [pdfBlobUrl, setPdfBlobUrl] = useState(null); + const canvasContainerRef = useRef(null); + const busy = signLoading || submitLoading || consentLoading || pdfLoading; useEffect(() => { onBusyChange?.(busy); @@ -259,6 +282,262 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { .finally(() => setConsentLoading(false)); }, [examId]); + // 加载 PDF 数据 + useEffect(() => { + if (!previewPdf?.pdf_url) { + setPdfData(null); + setPdfBlobUrl(null); + setPdfReady(false); + return; + } + + let objectUrl: string | null = null; + setPdfReady(false); + setPdfLoading(true); + setPdfData(null); + + fetch(previewPdf.pdf_url) + .then((resp) => { + if (!resp.ok) throw new Error('获取PDF文件失败'); + return resp.blob(); + }) + .then((blob) => { + objectUrl = URL.createObjectURL(blob); + setPdfBlobUrl(objectUrl); + return blob.arrayBuffer(); + }) + .then((arrayBuffer) => { + setPdfData(arrayBuffer); + setPdfLoading(false); + }) + .catch((err) => { + console.error('PDF 拉取失败', err); + setPdfLoading(false); + }); + + return () => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + }, [previewPdf?.pdf_url]); + + // 渲染 PDF + useEffect(() => { + if (!pdfData || !canvasContainerRef.current) return; + + setPdfReady(false); + + const renderAllPages = async () => { + try { + const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; + + if (!canvasContainerRef.current) 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) 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; + } + + setPdfReady(true); + } catch (err) { + console.error('PDF 渲染失败', err); + setPdfLoading(false); + } + }; + + renderAllPages(); + }, [pdfData]); + + const handlePrint = () => { + 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); + }; + }; + + const handleDirectPrint = async (pdfItem: OutputTongyishuFileInfo) => { + if (!pdfItem.pdf_url) return; + + try { + // 加载PDF + const response = await fetch(pdfItem.pdf_url); + if (!response.ok) throw new Error('获取PDF文件失败'); + const blob = await response.blob(); + const arrayBuffer = await blob.arrayBuffer(); + + // 渲染PDF到临时canvas + const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; + const scale = 1.2; + const canvasImages: string[] = []; + + 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) continue; + + canvas.height = viewport.height; + canvas.width = viewport.width; + + const renderContext = { + canvasContext: context, + viewport: viewport, + } as any; + + await page.render(renderContext).promise; + canvasImages.push(canvas.toDataURL('image/png')); + } + + // 创建打印窗口 + const printWindow = window.open('', '_blank'); + if (!printWindow) return; + + printWindow.document.write(` + + + + ${pdfItem.pdf_name || '知情同意书打印'} + + + + `); + + canvasImages.forEach((imgData) => { + printWindow.document.write(``); + }); + + printWindow.document.write(` + + + `); + + printWindow.document.close(); + + // 等待图片加载完成后执行打印 + printWindow.onload = () => { + printWindow.focus(); + setTimeout(() => { + printWindow.print(); + }, 1000); + }; + } catch (err) { + console.error('打印失败', err); + alert('打印失败,请稍后重试'); + } + }; + return (
@@ -313,10 +592,10 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { {consentList.map((item) => (
- {item.pdf_name} + {item.pdf_name.length > 12 ? item.pdf_name.slice(0, 12) + "..." : item.pdf_name} {item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && ( { /> )}
- + )} + + setPreviewPdf(target); + }} + disabled={busy} + > + 查看 + +
))}
)} - {previewPdf && ( -
-
-
{previewPdf.pdf_name}
-
- - -
-
-
-